mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-23 22:47:19 +00:00
fix: surface name collisions when pasting instead of failing silently (#3492)
Cut+paste onto a folder containing an item with the same name failed with no feedback: the dashboard's moveClipboardItems swallowed the error into console.error and cleared the clipboard, and the rewritten backend broke the v1 wire contract the GUI's conflict prompts key on. - FSService: move() and copy() collisions again return item_with_same_name_exists + entry_name (the contract the write path already preserves) instead of a generic 'conflict' code. This also restores the desktop's existing Replace/Skip dialogs, which were silently broken against the new backend. - Dashboard: moveClipboardItems now mirrors the desktop's move_items conflict flow (Replace / Replace all / Skip / Cancel, retry with overwrite) and surfaces other move errors in an alert. - keyboard.js: the desktop's global Ctrl+V handler threw an uncaught TypeError on every paste in dashboard mode (its file container has no data-path); guard it — which also prevents a double-paste if dashboard containers ever gain one.
This commit is contained in:
@@ -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 },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: `<strong>${html_encode(err.entry_name)}</strong> 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(`<p>Moving <strong>${html_encode(item_name)}</strong></p>${html_encode(err.message ?? '')}`);
|
||||
}
|
||||
}
|
||||
} while ( retry );
|
||||
}
|
||||
|
||||
window.clipboard = [];
|
||||
|
||||
@@ -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' )
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user