diff --git a/src/backend/services/fs/FSService.ts b/src/backend/services/fs/FSService.ts index 13753f2f2..607e0988d 100644 --- a/src/backend/services/fs/FSService.ts +++ b/src/backend/services/fs/FSService.ts @@ -3496,10 +3496,16 @@ export class FSService extends PuterService { } else if (input.dedupeName) { name = await this.#findDedupedName(destinationParent, name); } else { + // v1 wire contract: clients (the GUI's move/paste flows among + // them) key on `item_with_same_name_exists` + `entry_name` to + // offer a replace/skip prompt. throw new HttpError( 409, `An entry already exists at ${targetPath}`, - { legacyCode: 'conflict' }, + { + legacyCode: 'item_with_same_name_exists', + fields: { entry_name: name }, + }, ); } } @@ -3599,10 +3605,14 @@ export class FSService extends PuterService { } else if (input.dedupeName) { name = await this.#findDedupedName(destinationParent, name); } else { + // v1 wire contract, as in move() above. throw new HttpError( 409, `An entry already exists at ${targetPath}`, - { legacyCode: 'conflict' }, + { + legacyCode: 'item_with_same_name_exists', + fields: { entry_name: name }, + }, ); } } diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js index 8d56cd985..9f81909ba 100644 --- a/src/gui/src/UI/Dashboard/TabFiles.js +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -3052,17 +3052,53 @@ const TabFiles = { return; } + const { html_encode } = window; + const multiple_items = window.clipboard.length > 1; + // Set once the user picks "Replace all" on a conflict; later items + // then overwrite without asking again. + let overwrite_all = false; + for ( const item of window.clipboard ) { // Handle both object format { path, uid } and legacy string format const source = item.uid || item.path || item; - try { - await puter.fs.move({ - source: source, - destination: destPath, - }); - } catch ( err ) { - console.error('Failed to move item:', err); - } + let overwrite = overwrite_all; + let retry; + do { + retry = false; + try { + await puter.fs.move({ + source: source, + destination: destPath, + overwrite: overwrite, + }); + } catch ( err ) { + // Same conflict resolution as the desktop's move_items: + // ask, then retry with overwrite or leave the item be. + if ( err.code === 'item_with_same_name_exists' ) { + const alert_resp = await UIAlert({ + message: `${html_encode(err.entry_name)} already exists.`, + buttons: [ + { label: i18n('replace'), type: 'primary', value: 'replace' }, + ... multiple_items ? [{ label: i18n('replace_all'), value: 'replace_all' }] : [], + ... multiple_items ? [{ label: i18n('skip'), value: 'skip' }] : [{ label: i18n('cancel'), value: 'cancel' }], + ], + }); + if ( alert_resp === 'replace' ) { + overwrite = true; + retry = true; + } else if ( alert_resp === 'replace_all' ) { + overwrite = true; + overwrite_all = true; + retry = true; + } + // skip/cancel: the item stays where it was cut from + } else { + console.error('Failed to move item:', err); + const item_name = String(item.path || source).split('/').pop(); + UIAlert(`
Moving ${html_encode(item_name)}
${html_encode(err.message ?? '')}`); + } + } + } while ( retry ); } window.clipboard = []; diff --git a/src/gui/src/keyboard.js b/src/gui/src/keyboard.js index a33f008bd..3523bf15e 100644 --- a/src/gui/src/keyboard.js +++ b/src/gui/src/keyboard.js @@ -870,6 +870,15 @@ $(document).bind('keyup keydown', async function (e) { if ( parent_container ) { target_el = parent_container; target_path = $(parent_container).attr('data-path'); + // No path means this container isn't a filesystem view this + // handler can paste into — the dashboard's Files tab is one such + // (it has its own paste handler); pasting here anyway would + // double-move the clipboard, and reading .startsWith off the + // undefined path used to throw on every paste in dashboard mode. + if ( ! target_path ) + { + return; + } // don't allow pasting in Trash if ( (target_path === window.trash_path || target_path.startsWith(`${window.trash_path }/`)) && window.clipboard_op !== 'move' ) {