fix: make RDP drive redirection writable on the stock deployment (#1333)

* fix: make RDP drive redirection writable on the stock deployment

The default drive-path was /drive on the guacd side, which the official
guacd image cannot create as its non-root user, so every upload was
refused with guacd's raw "FAIL (CANNOT OPEN)" ack. Default to
GUACD_DRIVE_PATH (set to the shared termix-data volume in compose) with
one folder per user, and explain guacd's refusal in the file browser.

* style: format RDP drive settings
This commit is contained in:
ZacharyZcR
2026-08-25 01:40:20 +08:00
committed by GitHub
parent 8260af2d57
commit 672f5ba80b
9 changed files with 113 additions and 7 deletions
@@ -19,6 +19,7 @@ import {
parentPath,
saveBlobAs,
uploadFile,
describeUploadError,
type RemoteFileEntry,
} from "./guacamole-filesystem.ts";
@@ -76,9 +77,9 @@ export function GuacamoleFileBrowser({
toast.success(t("guacamole.files.uploaded", { name: file.name }));
} catch (err) {
toast.error(
err instanceof Error
? err.message
: t("guacamole.files.uploadFailed", { name: file.name }),
describeUploadError(err, (key) =>
t(`guacamole.files.${key}`, { name: file.name }),
),
);
} finally {
setBusyName(null);
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { describeUploadError } from "./guacamole-filesystem";
const t = (key: string) => `#${key}`;
describe("describeUploadError", () => {
it("explains guacd's refusal to open the target file as a drive-folder problem", () => {
expect(describeUploadError(new Error("FAIL (CANNOT OPEN)"), t)).toBe(
"#driveNotWritable",
);
expect(describeUploadError(new Error("FAIL (NO FS)"), t)).toBe(
"#driveUnavailable",
);
});
it("passes other messages through and falls back for unknown errors", () => {
expect(describeUploadError(new Error("disk full"), t)).toBe("disk full");
expect(describeUploadError(undefined, t)).toBe("#uploadFailed");
});
});
@@ -163,3 +163,22 @@ export function saveBlobAs(blob: Blob, filename: string): void {
link.click();
URL.revokeObjectURL(url);
}
/**
* Turns an upload failure into something a user can act on. guacd answers a
* refused stream with an ack like "FAIL (CANNOT OPEN)" - accurate, but it
* means "the drive folder isn't writable", which is what people need to hear.
*/
export function describeUploadError(
error: unknown,
t: (key: "driveNotWritable" | "driveUnavailable" | "uploadFailed") => string,
): string {
const message = error instanceof Error ? error.message : "";
if (/cannot open|can't open|permission denied/i.test(message)) {
return t("driveNotWritable");
}
if (/no fs/i.test(message)) {
return t("driveUnavailable");
}
return message || t("uploadFailed");
}