2222import '@nextcloud/dialogs/style.css'
2323import type { Folder , Node , View } from '@nextcloud/files'
2424import type { IFilePickerButton } from '@nextcloud/dialogs'
25+ import type { FileStat , ResponseDataDetailed } from 'webdav'
2526import type { MoveCopyResult } from './moveOrCopyActionUtils'
2627
2728// eslint-disable-next-line n/no-extraneous-import
2829import { AxiosError } from 'axios'
2930import { basename , join } from 'path'
3031import { emit } from '@nextcloud/event-bus'
31- import { generateRemoteUrl } from '@nextcloud/router'
32- import { getCurrentUser } from '@nextcloud/auth'
3332import { getFilePickerBuilder , showError } from '@nextcloud/dialogs'
34- import { Permission , FileAction , FileType , NodeStatus } from '@nextcloud/files'
33+ import { Permission , FileAction , FileType , NodeStatus , davGetClient , davRootPath , davResultToNode , davGetDefaultPropfind } from '@nextcloud/files'
3534import { translate as t } from '@nextcloud/l10n'
36- import axios from '@nextcloud/axios'
3735import Vue from 'vue'
3836
3937import CopyIconSvg from '@mdi/svg/svg/folder-multiple.svg?raw'
@@ -69,6 +67,30 @@ const getActionForNodes = (nodes: Node[]): MoveCopyAction => {
6967 * @return {Promise<void> } A promise that resolves when the copy/move is done
7068 */
7169export const handleCopyMoveNodeTo = async ( node : Node , destination : Folder , method : MoveCopyAction . COPY | MoveCopyAction . MOVE , overwrite = false ) => {
70+ /**
71+ * Create an unique name for a node
72+ * @param node Node that is copied
73+ * @param otherNodes Other nodes in the target directory to check for unique name
74+ * @return Either the node basename, if unique, or the name with a `(copy N)` suffix that is unique
75+ */
76+ const makeUniqueName = ( node : Node , otherNodes : Node [ ] | FileStat [ ] ) => {
77+ const basename = node . basename . slice ( 0 , node . basename . lastIndexOf ( '.' ) )
78+ let index = 0
79+
80+ const currentName = ( ) => {
81+ switch ( index ) {
82+ case 0 : return node . basename
83+ case 1 : return `${ basename } (copy)${ node . extension ?? '' } `
84+ default : return `${ basename } ${ t ( 'files' , '(copy %n)' , undefined , index ) } ${ node . extension ?? '' } ` // TRANSLATORS: Meaning it is the n'th copy of a file
85+ }
86+ }
87+
88+ while ( otherNodes . some ( ( other : Node | FileStat ) => currentName ( ) === other . basename ) ) {
89+ index += 1
90+ }
91+ return currentName ( )
92+ }
93+
7294 if ( ! destination ) {
7395 return
7496 }
@@ -77,42 +99,55 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
7799 throw new Error ( t ( 'files' , 'Destination is not a folder' ) )
78100 }
79101
80- if ( node . dirname === destination . path ) {
102+ // Do not allow to MOVE a node to the same folder it is already located
103+ if ( method === MoveCopyAction . MOVE && node . dirname === destination . path ) {
81104 throw new Error ( t ( 'files' , 'This file/folder is already in that directory' ) )
82105 }
83106
84107 /**
85108 * Example:
86- * node: /foo/bar/file.txt -> path = /foo/bar
87- * destination: /foo
88- * Allow move of /foo does not start with /foo/bar so allow
109+ * - node: /foo/bar/file.txt -> path = /foo/bar/file.txt, destination: /foo
110+ * Allow move of /foo does not start with /foo/bar/file.txt so allow
111+ * - node: /foo , destination: /foo/bar
112+ * Do not allow as it would copy foo within itself
113+ * - node: /foo/bar.txt, destination: /foo
114+ * Allow copy a file to the same directory
89115 */
90116 if ( destination . path . startsWith ( node . path ) ) {
91117 throw new Error ( t ( 'files' , 'You cannot move a file/folder onto itself or into a subfolder of itself' ) )
92118 }
93119
94- const relativePath = join ( destination . path , node . basename )
95- const destinationUrl = generateRemoteUrl ( `dav/files/${ getCurrentUser ( ) ?. uid } ${ relativePath } ` )
96-
97120 // Set loading state
98121 Vue . set ( node , 'status' , NodeStatus . LOADING )
99122
100123 const queue = getQueue ( )
101124 return await queue . add ( async ( ) => {
102125 try {
103- await axios ( {
104- method : method === MoveCopyAction . COPY ? 'COPY' : 'MOVE' ,
105- url : node . encodedSource ,
106- headers : {
107- Destination : encodeURI ( destinationUrl ) ,
108- Overwrite : overwrite ? undefined : 'F' ,
109- } ,
110- } )
126+ const client = davGetClient ( )
127+ const currentPath = join ( davRootPath , node . path )
128+ const destinationPath = join ( davRootPath , destination . path )
111129
112- // If we're moving, update the node
113- // if we're copying, we don't need to update the node
114- // the view will refresh itself
115- if ( method === MoveCopyAction . MOVE ) {
130+ if ( method === MoveCopyAction . COPY ) {
131+ let target = node . basename
132+ // If we do not allow overwriting then find an unique name
133+ if ( ! overwrite ) {
134+ const otherNodes = await client . getDirectoryContents ( destinationPath ) as FileStat [ ]
135+ target = makeUniqueName ( node , otherNodes )
136+ }
137+ await client . copyFile ( currentPath , join ( destinationPath , target ) )
138+ // If the node is copied into current directory the view needs to be updated
139+ if ( node . dirname === destination . path ) {
140+ const { data } = await client . stat (
141+ join ( destinationPath , target ) ,
142+ {
143+ details : true ,
144+ data : davGetDefaultPropfind ( ) ,
145+ } ,
146+ ) as ResponseDataDetailed < FileStat >
147+ emit ( 'files:node:created' , davResultToNode ( data ) )
148+ }
149+ } else {
150+ await client . moveFile ( currentPath , join ( destinationPath , node . basename ) )
116151 // Delete the node as it will be fetched again
117152 // when navigating to the destination folder
118153 emit ( 'files:node:deleted' , node )
@@ -129,6 +164,7 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
129164 throw new Error ( error . message )
130165 }
131166 }
167+ logger . debug ( error as Error )
132168 throw new Error ( )
133169 } finally {
134170 Vue . set ( node , 'status' , undefined )
@@ -165,16 +201,6 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
165201 const dirnames = nodes . map ( node => node . dirname )
166202 const paths = nodes . map ( node => node . path )
167203
168- if ( dirnames . includes ( path ) ) {
169- // This file/folder is already in that directory
170- return buttons
171- }
172-
173- if ( paths . includes ( path ) ) {
174- // You cannot move a file/folder onto itself
175- return buttons
176- }
177-
178204 if ( action === MoveCopyAction . COPY || action === MoveCopyAction . MOVE_OR_COPY ) {
179205 buttons . push ( {
180206 label : target ? t ( 'files' , 'Copy to {target}' , { target } ) : t ( 'files' , 'Copy' ) ,
@@ -189,6 +215,17 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
189215 } )
190216 }
191217
218+ // Invalid MOVE targets (but valid copy targets)
219+ if ( dirnames . includes ( path ) ) {
220+ // This file/folder is already in that directory
221+ return buttons
222+ }
223+
224+ if ( paths . includes ( path ) ) {
225+ // You cannot move a file/folder onto itself
226+ return buttons
227+ }
228+
192229 if ( action === MoveCopyAction . MOVE || action === MoveCopyAction . MOVE_OR_COPY ) {
193230 buttons . push ( {
194231 label : target ? t ( 'files' , 'Move to {target}' , { target } ) : t ( 'files' , 'Move' ) ,
@@ -207,7 +244,8 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
207244 } )
208245
209246 const picker = filePicker . build ( )
210- picker . pick ( ) . catch ( ( ) => {
247+ picker . pick ( ) . catch ( ( error ) => {
248+ logger . debug ( error as Error )
211249 reject ( new Error ( t ( 'files' , 'Cancelled move or copy operation' ) ) )
212250 } )
213251 } )
@@ -236,7 +274,13 @@ export const action = new FileAction({
236274
237275 async exec ( node : Node , view : View , dir : string ) {
238276 const action = getActionForNodes ( [ node ] )
239- const result = await openFilePickerForAction ( action , dir , [ node ] )
277+ let result
278+ try {
279+ result = await openFilePickerForAction ( action , dir , [ node ] )
280+ } catch ( e ) {
281+ logger . error ( e as Error )
282+ return false
283+ }
240284 try {
241285 await handleCopyMoveNodeTo ( node , result . destination , result . action )
242286 return true
0 commit comments