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,7 +99,8 @@ 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
@@ -86,33 +109,43 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
86109 * node: /foo/bar/file.txt -> path = /foo/bar
87110 * destination: /foo
88111 * Allow move of /foo does not start with /foo/bar so allow
112+ * But allow copy a file to the same directory
89113 */
90- if ( destination . path . startsWith ( node . path ) ) {
114+ if ( destination . path . startsWith ( node . path ) && destination . path !== node . path ) {
91115 throw new Error ( t ( 'files' , 'You cannot move a file/folder onto itself or into a subfolder of itself' ) )
92116 }
93117
94- const relativePath = join ( destination . path , node . basename )
95- const destinationUrl = generateRemoteUrl ( `dav/files/${ getCurrentUser ( ) ?. uid } ${ relativePath } ` )
96-
97118 // Set loading state
98119 Vue . set ( node , 'status' , NodeStatus . LOADING )
99120
100121 const queue = getQueue ( )
101122 return await queue . add ( async ( ) => {
102123 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- } )
124+ const client = davGetClient ( )
125+ const currentPath = join ( davRootPath , node . path )
126+ const destinationPath = join ( davRootPath , destination . path )
111127
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 ) {
128+ if ( method === MoveCopyAction . COPY ) {
129+ let target = node . basename
130+ // If we do not allow overwriting than find an unique name
131+ if ( ! overwrite ) {
132+ const otherNodes = await client . getDirectoryContents ( destinationPath ) as FileStat [ ]
133+ target = makeUniqueName ( node , otherNodes )
134+ }
135+ await client . copyFile ( currentPath , join ( destinationPath , target ) )
136+ // If the node is copied into current directory the view needs to be updated
137+ if ( node . dirname === destination . path ) {
138+ const { data } = await client . stat (
139+ join ( destinationPath , target ) ,
140+ {
141+ details : true ,
142+ data : davGetDefaultPropfind ( ) ,
143+ } ,
144+ ) as ResponseDataDetailed < FileStat >
145+ emit ( 'files:node:created' , davResultToNode ( data ) )
146+ }
147+ } else {
148+ await client . moveFile ( currentPath , join ( destinationPath , node . basename ) )
116149 // Delete the node as it will be fetched again
117150 // when navigating to the destination folder
118151 emit ( 'files:node:deleted' , node )
@@ -129,6 +162,7 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
129162 throw new Error ( error . message )
130163 }
131164 }
165+ logger . debug ( error as Error )
132166 throw new Error ( )
133167 } finally {
134168 Vue . set ( node , 'status' , undefined )
@@ -165,16 +199,6 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
165199 const dirnames = nodes . map ( node => node . dirname )
166200 const paths = nodes . map ( node => node . path )
167201
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-
178202 if ( action === MoveCopyAction . COPY || action === MoveCopyAction . MOVE_OR_COPY ) {
179203 buttons . push ( {
180204 label : target ? t ( 'files' , 'Copy to {target}' , { target } ) : t ( 'files' , 'Copy' ) ,
@@ -189,6 +213,17 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
189213 } )
190214 }
191215
216+ // Invalid MOVE targets (but valid copy targets)
217+ if ( dirnames . includes ( path ) ) {
218+ // This file/folder is already in that directory
219+ return buttons
220+ }
221+
222+ if ( paths . includes ( path ) ) {
223+ // You cannot move a file/folder onto itself
224+ return buttons
225+ }
226+
192227 if ( action === MoveCopyAction . MOVE || action === MoveCopyAction . MOVE_OR_COPY ) {
193228 buttons . push ( {
194229 label : target ? t ( 'files' , 'Move to {target}' , { target } ) : t ( 'files' , 'Move' ) ,
@@ -207,7 +242,8 @@ const openFilePickerForAction = async (action: MoveCopyAction, dir = '/', nodes:
207242 } )
208243
209244 const picker = filePicker . build ( )
210- picker . pick ( ) . catch ( ( ) => {
245+ picker . pick ( ) . catch ( ( error ) => {
246+ logger . debug ( error as Error )
211247 reject ( new Error ( t ( 'files' , 'Cancelled move or copy operation' ) ) )
212248 } )
213249 } )
@@ -236,7 +272,13 @@ export const action = new FileAction({
236272
237273 async exec ( node : Node , view : View , dir : string ) {
238274 const action = getActionForNodes ( [ node ] )
239- const result = await openFilePickerForAction ( action , dir , [ node ] )
275+ let result
276+ try {
277+ result = await openFilePickerForAction ( action , dir , [ node ] )
278+ } catch ( e ) {
279+ logger . error ( e as Error )
280+ return false
281+ }
240282 try {
241283 await handleCopyMoveNodeTo ( node , result . destination , result . action )
242284 return true
0 commit comments