aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/actions/moveOrCopyAction.ts102
-rw-r--r--apps/files/src/init-templates.ts5
-rw-r--r--apps/files/src/init.ts3
-rw-r--r--apps/files/src/newMenu/newFolder.ts14
-rw-r--r--apps/files/src/types.ts9
-rw-r--r--apps/files/src/utils/fileUtils.ts19
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('/')