mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
fix: showSaveFilePicker from external websites
Saving a file via puter.ui.showSaveFilePicker from a third-party website (popup flow) consistently failed with a DataCloneError alert after clicking Save, and saving over an existing filename showed a raw error instead of the Replace/Cancel prompt. Two bugs: 1. privacy_aware_path is a curried factory (world => fspath => ...), and initgui.js is the only module that imports it directly — so the popup save handler's privacy_aware_path(res.path) returned the inner function, which postMessage cannot structured-clone. Every other call site resolves the bare name to the correctly bound window.privacy_aware_path global, which is why only the external-site popup flow was broken. Use the global at the call site and import the factory under a distinct name so a bare call can't silently resolve to it again. 2. The v2 backend returns `conflict` for a same-name write, but the v1 wire contract is `item_with_same_name_exists` + `entry_name`, which the GUI's save dialogs key on to offer the overwrite prompt. Restore the legacy code/field on the write-conflict error and carry HttpError.fields through the /batch per-op error serializer. Verified end-to-end against a local backend: fresh save resolves the caller's promise with the signed saved_file and closes the popup; saving an existing name shows Replace/Cancel and Replace overwrites. Backend suite shows no new failures.
This commit is contained in:
@@ -167,8 +167,7 @@ export class LegacyFSController extends PuterController {
|
||||
|
||||
router.get('/get-launch-apps', apiOptions, async (req, res) => {
|
||||
const recommendedSvc = this.services.recommendedApps as unknown as
|
||||
| { getRecommendedApps?: () => Promise<unknown[]> }
|
||||
| undefined;
|
||||
{ getRecommendedApps?: () => Promise<unknown[]> } | undefined;
|
||||
const recommended = recommendedSvc?.getRecommendedApps
|
||||
? await recommendedSvc.getRecommendedApps()
|
||||
: [];
|
||||
@@ -640,9 +639,7 @@ export class LegacyFSController extends PuterController {
|
||||
// Trash, and `null`/`{}` when restoring. See
|
||||
// `src/gui/src/helpers.js` → `window.move_items`.
|
||||
newMetadata: (body.new_metadata ?? undefined) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined,
|
||||
Record<string, unknown> | null | undefined,
|
||||
});
|
||||
const oldPath = source.path;
|
||||
await this.#emitGuiEvent('outer.gui.item.moved', moved, {
|
||||
@@ -1071,8 +1068,7 @@ export class LegacyFSController extends PuterController {
|
||||
}
|
||||
|
||||
type SignedOrEmpty =
|
||||
| (SignedFile & { path?: string })
|
||||
| Record<string, never>;
|
||||
(SignedFile & { path?: string }) | Record<string, never>;
|
||||
const result: { signatures: SignedOrEmpty[]; token?: string } = {
|
||||
signatures: [],
|
||||
};
|
||||
@@ -1612,10 +1608,7 @@ export class LegacyFSController extends PuterController {
|
||||
const subjectRef = body.subject;
|
||||
const appRef = body.app;
|
||||
const mode = (getString(body, 'mode') ?? 'read') as
|
||||
| 'see'
|
||||
| 'list'
|
||||
| 'read'
|
||||
| 'write';
|
||||
'see' | 'list' | 'read' | 'write';
|
||||
if (!subjectRef || !appRef)
|
||||
throw new HttpError(400, '`subject` and `app` are required', {
|
||||
legacyCode: 'bad_request',
|
||||
@@ -2271,12 +2264,21 @@ export class LegacyFSController extends PuterController {
|
||||
|
||||
#serializeBatchError(err: unknown): Record<string, unknown> {
|
||||
if (err instanceof HttpError) {
|
||||
return {
|
||||
const payload: Record<string, unknown> = {
|
||||
error: true,
|
||||
status: err.statusCode,
|
||||
message: err.message,
|
||||
code: err.legacyCode ?? err.code,
|
||||
};
|
||||
// Same as the terminal errorHandler: extra fields (e.g.
|
||||
// `entry_name` on item_with_same_name_exists) ride along
|
||||
// without clobbering the canonical slots.
|
||||
if (err.fields) {
|
||||
for (const [k, v] of Object.entries(err.fields)) {
|
||||
if (!(k in payload)) payload[k] = v;
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return { error: true, status: 500, message: err.message };
|
||||
|
||||
@@ -558,10 +558,20 @@ export class FSService extends PuterService {
|
||||
);
|
||||
}
|
||||
if (existingEntry && !normalizedInput.overwrite) {
|
||||
// v1 wire contract: clients (the GUI's save dialogs among
|
||||
// them) key on `item_with_same_name_exists` + `entry_name`
|
||||
// to offer an overwrite prompt.
|
||||
throw new HttpError(
|
||||
409,
|
||||
'A file already exists at this path and overwrite was not requested',
|
||||
{ legacyCode: 'conflict' },
|
||||
{
|
||||
legacyCode: 'item_with_same_name_exists',
|
||||
fields: {
|
||||
entry_name: pathPosix.basename(
|
||||
normalizedInput.path,
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ import { LaunchOnInitService } from './services/LaunchOnInitService.js';
|
||||
import { LocaleService } from './services/LocaleService.js';
|
||||
import { ProcessService } from './services/ProcessService.js';
|
||||
import { ThemeService } from './services/ThemeService.js';
|
||||
import { privacy_aware_path } from './util/desktop.js';
|
||||
// Curried: takes `{ window }` and returns the path mapper. Import under a
|
||||
// factory name so a bare `privacy_aware_path(path)` call in this module can't
|
||||
// silently resolve to the factory — use `window.privacy_aware_path` instead.
|
||||
import { privacy_aware_path as privacy_aware_path_factory } from './util/desktop.js';
|
||||
|
||||
const postAuthActions = async (action) => {
|
||||
// -------------------------------------------------------------------------------------
|
||||
@@ -378,7 +381,7 @@ const postAuthActions = async (action) => {
|
||||
metadataURL: file_signature.metadata_url,
|
||||
type: file_signature.type,
|
||||
uid: file_signature.uid,
|
||||
path: privacy_aware_path(res.path),
|
||||
path: window.privacy_aware_path(res.path),
|
||||
},
|
||||
}, '*');
|
||||
|
||||
@@ -2076,7 +2079,7 @@ $(document).on('contextmenu', '.disable-context-menu', function (e) {
|
||||
});
|
||||
|
||||
// util/desktop.js
|
||||
window.privacy_aware_path = privacy_aware_path({ window });
|
||||
window.privacy_aware_path = privacy_aware_path_factory({ window });
|
||||
|
||||
$(window).on('system-logout-event', function () {
|
||||
// Clear cookie
|
||||
|
||||
Reference in New Issue
Block a user