diff options
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/actions/moveOrCopyAction.ts | 102 | ||||
-rw-r--r-- | apps/files/src/init-templates.ts | 5 | ||||
-rw-r--r-- | apps/files/src/init.ts | 3 | ||||
-rw-r--r-- | apps/files/src/newMenu/newFolder.ts | 14 | ||||
-rw-r--r-- | apps/files/src/types.ts | 9 | ||||
-rw-r--r-- | apps/files/src/utils/fileUtils.ts | 19 |
6 files changed, 100 insertions, 52 deletions
diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts index 3d27df37226..a4e70caf37d 100644 --- a/apps/files/src/actions/moveOrCopyAction.ts +++ b/apps/files/src/actions/moveOrCopyAction.ts @@ -22,18 +22,16 @@ import '@nextcloud/dialogs/style.css' import type { Folder, Node, View } from '@nextcloud/files' import type { IFilePickerButton } from '@nextcloud/dialogs' +import type { FileStat, ResponseDataDetailed } from 'webdav' import type { MoveCopyResult } from './moveOrCopyActionUtils' // eslint-disable-next-line n/no-extraneous-import import { AxiosError } from 'axios' import { basename, join } from 'path' import { emit } from '@nextcloud/event-bus' -import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' import { getFilePickerBuilder, showError } from '@nextcloud/dialogs' -import { Permission, FileAction, FileType, NodeStatus } from '@nextcloud/files' +import { Permission, FileAction, FileType, NodeStatus, davGetClient, davRootPath, davResultToNode, davGetDefaultPropfind } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' import Vue from 'vue' import CopyIconSvg from '@mdi/svg/svg/folder-multiple.svg?raw' @@ -41,6 +39,7 @@ import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw' import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils' import logger from '../logger' +import { getUniqueName } from '../utils/fileUtils' /** * Return the action that is possible for the given nodes @@ -77,42 +76,64 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth throw new Error(t('files', 'Destination is not a folder')) } - if (node.dirname === destination.path) { + // Do not allow to MOVE a node to the same folder it is already located + if (method === MoveCopyAction.MOVE && node.dirname === destination.path) { throw new Error(t('files', 'This file/folder is already in that directory')) } /** * Example: - * node: /foo/bar/file.txt -> path = /foo/bar - * destination: /foo - * Allow move of /foo does not start with /foo/bar so allow + * - node: /foo/bar/file.txt -> path = /foo/bar/file.txt, destination: /foo + * Allow move of /foo does not start with /foo/bar/file.txt so allow + * - node: /foo , destination: /foo/bar + * Do not allow as it would copy foo within itself + * - node: /foo/bar.txt, destination: /foo + * Allow copy a file to the same directory + * - node: "/foo/bar", destination: "/foo/bar 1" + * Allow to move or copy but we need to check with trailing / otherwise it would report false positive */ - if (destination.path.startsWith(node.path)) { + if (`${destination.path}/`.startsWith(`${node.path}/`)) { throw new Error(t('files', 'You cannot move a file/folder onto itself or into a subfolder of itself')) } - const relativePath = join(destination.path, node.basename) - const destinationUrl = generateRemoteUrl(`dav/files/${getCurrentUser()?.uid}${relativePath}`) - // Set loading state Vue.set(node, 'status', NodeStatus.LOADING) const queue = getQueue() return await queue.add(async () => { + const copySuffix = (index: number) => { + if (index === 1) { + return t('files', '(copy)') // TRANSLATORS: Mark a file as a copy of another file + } + return t('files', '(copy %n)', undefined, index) // TRANSLATORS: Meaning it is the n'th copy of a file + } + try { - await axios({ - method: method === MoveCopyAction.COPY ? 'COPY' : 'MOVE', - url: node.encodedSource, - headers: { - Destination: encodeURI(destinationUrl), - Overwrite: overwrite ? undefined : 'F', - }, - }) + const client = davGetClient() + const currentPath = join(davRootPath, node.path) + const destinationPath = join(davRootPath, destination.path) - // If we're moving, update the node - // if we're copying, we don't need to update the node - // the view will refresh itself - if (method === MoveCopyAction.MOVE) { + if (method === MoveCopyAction.COPY) { + let target = node.basename + // If we do not allow overwriting then find an unique name + if (!overwrite) { + const otherNodes = await client.getDirectoryContents(destinationPath) as FileStat[] + target = getUniqueName(node.basename, otherNodes.map((n) => n.basename), copySuffix) + } + await client.copyFile(currentPath, join(destinationPath, target)) + // If the node is copied into current directory the view needs to be updated + if (node.dirname === destination.path) { + const { data } = await client.stat( + join(destinationPath, target), + { + details: true, + data: davGetDefaultPropfind(), + }, + ) as ResponseDataDetailed<FileStat> + emit('files:node:created', davResultToNode(data)) + } + } else { + await client.moveFile(currentPath, join(destinationPath, node.basename)) // Delete the node as it will be fetched again // when navigating to the destination folder emit('files:node:deleted', node) @@ -129,6 +150,7 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth throw new Error(error.message) } } + logger.debug(error as Error) throw new Error() } finally { Vue.set(node, 'status', undefined) @@ -165,16 +187,6 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: const dirnames = nodes.map(node => node.dirname) const paths = nodes.map(node => node.path) - if (dirnames.includes(path)) { - // This file/folder is already in that directory - return buttons - } - - if (paths.includes(path)) { - // You cannot move a file/folder onto itself - return buttons - } - if (action === MoveCopyAction.COPY || action === MoveCopyAction.MOVE_OR_COPY) { buttons.push({ label: target ? t('files', 'Copy to {target}', { target }) : t('files', 'Copy'), @@ -189,6 +201,17 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: }) } + // Invalid MOVE targets (but valid copy targets) + if (dirnames.includes(path)) { + // This file/folder is already in that directory + return buttons + } + + if (paths.includes(path)) { + // You cannot move a file/folder onto itself + return buttons + } + if (action === MoveCopyAction.MOVE || action === MoveCopyAction.MOVE_OR_COPY) { buttons.push({ label: target ? t('files', 'Move to {target}', { target }) : t('files', 'Move'), @@ -207,7 +230,8 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes: }) const picker = filePicker.build() - picker.pick().catch(() => { + picker.pick().catch((error) => { + logger.debug(error as Error) reject(new Error(t('files', 'Cancelled move or copy operation'))) }) }) @@ -236,7 +260,13 @@ export const action = new FileAction({ async exec(node: Node, view: View, dir: string) { const action = getActionForNodes([node]) - const result = await openFilePickerForAction(action, dir, [node]) + let result + try { + result = await openFilePickerForAction(action, dir, [node]) + } catch (e) { + logger.error(e as Error) + return false + } try { await handleCopyMoveNodeTo(node, result.destination, result.action) return true diff --git a/apps/files/src/init-templates.ts b/apps/files/src/init-templates.ts index 879b60d0ee4..6803143d4b2 100644 --- a/apps/files/src/init-templates.ts +++ b/apps/files/src/init-templates.ts @@ -21,6 +21,7 @@ * */ import type { Entry } from '@nextcloud/files' +import type { TemplateFile } from './types' import { Folder, Node, Permission, addNewFileMenuEntry, removeNewFileMenuEntry } from '@nextcloud/files' import { generateOcsUrl } from '@nextcloud/router' @@ -35,7 +36,7 @@ import Vue from 'vue' import PlusSvg from '@mdi/svg/svg/plus.svg?raw' import TemplatePickerView from './views/TemplatePicker.vue' -import { getUniqueName } from './newMenu/newFolder' +import { getUniqueName } from './utils/fileUtils.ts' import { getCurrentUser } from '@nextcloud/auth' // Set up logger @@ -58,7 +59,7 @@ TemplatePickerRoot.id = 'template-picker' document.body.appendChild(TemplatePickerRoot) // Retrieve and init templates -let templates = loadState('files', 'templates', []) +let templates = loadState<TemplateFile[]>('files', 'templates', []) let templatesPath = loadState('files', 'templates_path', false) logger.debug('Templates providers', { templates }) logger.debug('Templates folder', { templatesPath }) diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index aa855ed69b2..8002f33ff56 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -import MenuIcon from '@mdi/svg/svg/sun-compass.svg?raw' -import { FileAction, addNewFileMenuEntry, registerDavProperty, registerFileAction } from '@nextcloud/files' +import { addNewFileMenuEntry, registerDavProperty, registerFileAction } from '@nextcloud/files' import { action as deleteAction } from './actions/deleteAction' import { action as downloadAction } from './actions/downloadAction' diff --git a/apps/files/src/newMenu/newFolder.ts b/apps/files/src/newMenu/newFolder.ts index d4da1baaab7..37dcf6d3d89 100644 --- a/apps/files/src/newMenu/newFolder.ts +++ b/apps/files/src/newMenu/newFolder.ts @@ -21,7 +21,7 @@ */ import type { Entry, Node } from '@nextcloud/files' -import { basename, extname } from 'path' +import { basename } from 'path' import { emit } from '@nextcloud/event-bus' import { getCurrentUser } from '@nextcloud/auth' import { Permission, Folder } from '@nextcloud/files' @@ -31,6 +31,7 @@ import axios from '@nextcloud/axios' import FolderPlusSvg from '@mdi/svg/svg/folder-plus.svg?raw' +import { getUniqueName } from '../utils/fileUtils.ts' import logger from '../logger' type createFolderResponse = { @@ -55,17 +56,6 @@ const createNewFolder = async (root: Folder, name: string): Promise<createFolder } } -// TODO: move to @nextcloud/files -export const getUniqueName = (name: string, names: string[]): string => { - let newName = name - let i = 1 - while (names.includes(newName)) { - const ext = extname(name) - newName = `${basename(name, ext)} (${i++})${ext}` - } - return newName -} - export const entry = { id: 'newFolder', displayName: t('files', 'New folder'), diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 778e9ff2971..d2bfcaed0ee 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -111,3 +111,12 @@ export interface UploaderStore { export interface DragAndDropStore { dragging: FileId[] } + +export interface TemplateFile { + app: string + label: string + extension: string + iconClass?: string + mimetypes: string[] + ratio?: number +} diff --git a/apps/files/src/utils/fileUtils.ts b/apps/files/src/utils/fileUtils.ts index 9e2bfc44417..126739242a0 100644 --- a/apps/files/src/utils/fileUtils.ts +++ b/apps/files/src/utils/fileUtils.ts @@ -21,6 +21,25 @@ */ import { FileType, type Node } from '@nextcloud/files' import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import { basename, extname } from 'path' + +// TODO: move to @nextcloud/files +/** + * Create an unique file name + * @param name The initial name to use + * @param otherNames Other names that are already used + * @param suffix A function that takes an index an returns a suffix to add, defaults to '(index)' + * @return Either the initial name, if unique, or the name with the suffix so that the name is unique + */ +export const getUniqueName = (name: string, otherNames: string[], suffix = (n: number) => `(${n})`): string => { + let newName = name + let i = 1 + while (otherNames.includes(newName)) { + const ext = extname(name) + newName = `${basename(name, ext)} ${suffix(i++)}${ext}` + } + return newName +} export const encodeFilePath = function(path) { const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') |