fileport-gateway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/THIRD_PARTY_NOTICES.md +12 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +87 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/client.d.ts +1 -0
  8. package/dist/client.js +22 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +30 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/duration.d.ts +1 -0
  14. package/dist/duration.js +14 -0
  15. package/dist/duration.js.map +1 -0
  16. package/dist/gateway.d.ts +20 -0
  17. package/dist/gateway.js +188 -0
  18. package/dist/gateway.js.map +1 -0
  19. package/dist/paths.d.ts +8 -0
  20. package/dist/paths.js +88 -0
  21. package/dist/paths.js.map +1 -0
  22. package/dist/proxy.d.ts +2 -0
  23. package/dist/proxy.js +29 -0
  24. package/dist/proxy.js.map +1 -0
  25. package/dist/registry.d.ts +19 -0
  26. package/dist/registry.js +93 -0
  27. package/dist/registry.js.map +1 -0
  28. package/dist/save.d.ts +36 -0
  29. package/dist/save.js +165 -0
  30. package/dist/save.js.map +1 -0
  31. package/dist/types.d.ts +18 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/ui.d.ts +2 -0
  35. package/dist/ui.js +20 -0
  36. package/dist/ui.js.map +1 -0
  37. package/dist/workers.d.ts +23 -0
  38. package/dist/workers.js +110 -0
  39. package/dist/workers.js.map +1 -0
  40. package/docs/CURRENT_ARCHITECTURE.md +69 -0
  41. package/docs/DUFS_COMPATIBILITY.md +38 -0
  42. package/docs/MIGRATION_PLAN.md +53 -0
  43. package/package.json +69 -0
  44. package/scripts/dufs-compatibility.mjs +32 -0
  45. package/scripts/install-dufs.sh +15 -0
  46. package/vendor/LICENSE-DUFS-APACHE +201 -0
  47. package/vendor/LICENSE-DUFS-MIT +21 -0
  48. package/vendor/dufs +0 -0
package/dist/proxy.js ADDED
@@ -0,0 +1,29 @@
1
+ import http from "node:http";
2
+ const hopByHop = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);
3
+ export function proxyToUnixSocket(req, res, socketPath, upstreamPath, publicPrefix) {
4
+ return new Promise((resolve, reject) => {
5
+ const headers = {};
6
+ for (const [key, value] of Object.entries(req.headers))
7
+ if (!hopByHop.has(key.toLowerCase()) && key.toLowerCase() !== "host")
8
+ headers[key] = value;
9
+ const upstream = http.request({ socketPath, path: upstreamPath, method: req.method, headers }, (response) => {
10
+ res.statusCode = response.statusCode || 502;
11
+ for (const [key, value] of Object.entries(response.headers)) {
12
+ if (hopByHop.has(key.toLowerCase()) || value === undefined)
13
+ continue;
14
+ if (key.toLowerCase() === "location" && typeof value === "string" && value.startsWith("/"))
15
+ res.setHeader(key, `${publicPrefix}${value.slice(1)}`);
16
+ else
17
+ res.setHeader(key, value);
18
+ }
19
+ res.setHeader("referrer-policy", "no-referrer");
20
+ res.setHeader("x-content-type-options", "nosniff");
21
+ response.on("error", reject);
22
+ response.on("end", resolve);
23
+ response.pipe(res);
24
+ });
25
+ upstream.on("error", reject);
26
+ req.pipe(upstream);
27
+ });
28
+ }
29
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,IAAmD,MAAM,WAAW,CAAC;AAE5E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,EAAE,SAAS,CAAC,CAAC,CAAC;AAErJ,MAAM,UAAU,iBAAiB,CAAC,GAAoB,EAAE,GAAmB,EAAE,UAAkB,EAAE,YAAoB,EAAE,YAAoB;IACzI,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAA6B,EAAE,CAAC;QAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,MAAM;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnJ,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE;YAC1G,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU,IAAI,GAAG,CAAC;YAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5D,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,KAAK,KAAK,SAAS;oBAAE,SAAS;gBACrE,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,UAAU,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;;oBAC9I,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACjC,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;YAChD,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;YACnD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChF,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Share, SharePermission } from "./types.js";
2
+ export declare class ShareRegistry {
3
+ private readonly stateDir;
4
+ private readonly file;
5
+ private shares;
6
+ private mutationQueue;
7
+ constructor(stateDir: string);
8
+ load(): Promise<void>;
9
+ create(input: string, options?: {
10
+ permission?: SharePermission;
11
+ expiresAt?: string;
12
+ }): Promise<Share>;
13
+ get(id: string): Share | undefined;
14
+ list(): Share[];
15
+ revoke(id: string): Promise<boolean>;
16
+ cleanupExpired(): Promise<void>;
17
+ private mutate;
18
+ private persist;
19
+ }
@@ -0,0 +1,93 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { canonicalTarget } from "./paths.js";
5
+ export class ShareRegistry {
6
+ stateDir;
7
+ file;
8
+ shares = new Map();
9
+ mutationQueue = Promise.resolve();
10
+ constructor(stateDir) {
11
+ this.stateDir = stateDir;
12
+ this.file = path.join(stateDir, "shares.json");
13
+ }
14
+ async load() {
15
+ await fs.mkdir(this.stateDir, { recursive: true, mode: 0o700 });
16
+ await fs.chmod(this.stateDir, 0o700);
17
+ try {
18
+ const parsed = JSON.parse(await fs.readFile(this.file, "utf8"));
19
+ this.shares = new Map(parsed.map((share) => [share.id, { ...share, permission: share.permission === "rw" ? "rw" : "ro" }]));
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "ENOENT")
23
+ throw error;
24
+ }
25
+ await this.cleanupExpired();
26
+ }
27
+ async create(input, options = {}) {
28
+ const canonical = await canonicalTarget(input);
29
+ const permission = options.permission ?? "ro";
30
+ if (!["ro", "rw"].includes(permission))
31
+ throw new Error("invalid permission");
32
+ const expiresAt = options.expiresAt;
33
+ if (expiresAt && !(Date.parse(expiresAt) > Date.now()))
34
+ throw new Error("expiration must be in the future");
35
+ return this.mutate(async () => {
36
+ const share = { id: crypto.randomBytes(24).toString("base64url"), ...canonical, permission, createdAt: new Date().toISOString(), expiresAt };
37
+ this.shares.set(share.id, share);
38
+ try {
39
+ await this.persist();
40
+ }
41
+ catch (error) {
42
+ this.shares.delete(share.id);
43
+ throw error;
44
+ }
45
+ return share;
46
+ });
47
+ }
48
+ get(id) {
49
+ const share = this.shares.get(id);
50
+ if (share?.expiresAt && Date.parse(share.expiresAt) <= Date.now())
51
+ return undefined;
52
+ return share;
53
+ }
54
+ list() { return [...this.shares.values()].filter((share) => !share.expiresAt || Date.parse(share.expiresAt) > Date.now()); }
55
+ async revoke(id) { return this.mutate(async () => { const previous = this.shares.get(id); if (!previous)
56
+ return false; this.shares.delete(id); try {
57
+ await this.persist();
58
+ }
59
+ catch (error) {
60
+ this.shares.set(id, previous);
61
+ throw error;
62
+ } return true; }); }
63
+ async cleanupExpired() {
64
+ await this.mutate(async () => {
65
+ const expired = [...this.shares].filter(([, share]) => share.expiresAt && Date.parse(share.expiresAt) <= Date.now());
66
+ if (!expired.length)
67
+ return;
68
+ for (const [id] of expired)
69
+ this.shares.delete(id);
70
+ try {
71
+ await this.persist();
72
+ }
73
+ catch (error) {
74
+ for (const [id, share] of expired)
75
+ this.shares.set(id, share);
76
+ throw error;
77
+ }
78
+ });
79
+ }
80
+ mutate(operation) {
81
+ const result = this.mutationQueue.then(operation, operation);
82
+ this.mutationQueue = result.then(() => undefined, () => undefined);
83
+ return result;
84
+ }
85
+ async persist() {
86
+ const temp = `${this.file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
87
+ await fs.writeFile(temp, `${JSON.stringify([...this.shares.values()], null, 2)}\n`, { mode: 0o600 });
88
+ await fs.chmod(temp, 0o600);
89
+ await fs.rename(temp, this.file);
90
+ await fs.chmod(this.file, 0o600);
91
+ }
92
+ }
93
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
package/dist/save.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Share } from "./types.js";
2
+ export declare const MAX_TEXT_BYTES: number;
3
+ export declare const MAX_SAVE_REQUEST_BYTES: number;
4
+ export declare function isEditableText(target: string): boolean;
5
+ export declare function fileVersion(stat: {
6
+ dev: number | bigint;
7
+ ino: number | bigint;
8
+ size: number | bigint;
9
+ mtimeNs?: bigint;
10
+ mtimeMs?: number | bigint;
11
+ }): string;
12
+ export declare class SaveManager {
13
+ private readonly afterAuthorize;
14
+ private queues;
15
+ constructor(afterAuthorize?: ((target: string) => Promise<void>) | undefined);
16
+ private inside;
17
+ private assertOpenedScope;
18
+ read(share: Share, rawRelative: string): Promise<{
19
+ content: string;
20
+ version: string;
21
+ size: number;
22
+ editable: boolean;
23
+ permission: Share["permission"];
24
+ }>;
25
+ save(share: Share, rawRelative: string, content: unknown, expectedVersion: unknown): Promise<{
26
+ version: string;
27
+ size: number;
28
+ }>;
29
+ private serial;
30
+ private atomicSave;
31
+ }
32
+ export declare class SaveError extends Error {
33
+ readonly status: number;
34
+ readonly code: string;
35
+ constructor(status: number, code: string);
36
+ }
package/dist/save.js ADDED
@@ -0,0 +1,165 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { authorizeSharePath, decodeRelativePath } from "./paths.js";
5
+ export const MAX_TEXT_BYTES = 2 * 1024 * 1024;
6
+ export const MAX_SAVE_REQUEST_BYTES = MAX_TEXT_BYTES * 6 + 65536;
7
+ const textExtensions = new Set([
8
+ ".md", ".markdown", ".txt", ".log", ".json", ".jsonl", ".yaml", ".yml", ".csv", ".tsv",
9
+ ".html", ".htm", ".css", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go",
10
+ ".rs", ".java", ".kt", ".kts", ".c", ".h", ".cc", ".cpp", ".hpp", ".sh", ".bash", ".zsh",
11
+ ".fish", ".sql", ".xml", ".toml", ".ini", ".conf", ".properties", ".graphql", ".proto",
12
+ ".dot", ".gv", ".puml", ".plantuml", ".svg",
13
+ ]);
14
+ const textBasenames = new Set(["readme", "license", "makefile", "dockerfile", "gemfile", "rakefile"]);
15
+ export function isEditableText(target) {
16
+ const basename = path.basename(target).toLowerCase();
17
+ return textExtensions.has(path.extname(basename)) || textBasenames.has(basename);
18
+ }
19
+ export function fileVersion(stat) {
20
+ return crypto.createHash("sha256").update(`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs ?? stat.mtimeMs}`).digest("base64url").slice(0, 24);
21
+ }
22
+ export class SaveManager {
23
+ afterAuthorize;
24
+ queues = new Map();
25
+ constructor(afterAuthorize = undefined) {
26
+ this.afterAuthorize = afterAuthorize;
27
+ }
28
+ inside(parent, child) {
29
+ const relative = path.relative(parent, child);
30
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
31
+ }
32
+ assertOpenedScope(share, opened) {
33
+ if (share.kind === "file" ? opened !== share.target : !this.inside(share.target, opened))
34
+ throw new SaveError(403, "opened_target_outside_scope");
35
+ }
36
+ async read(share, rawRelative) {
37
+ const upstream = await authorizeSharePath(share, rawRelative);
38
+ const relative = decodeRelativePath(upstream);
39
+ const target = path.resolve(share.root, relative);
40
+ await this.afterAuthorize?.(target);
41
+ if (!isEditableText(target))
42
+ throw new SaveError(415, "unsupported_text_type");
43
+ const handle = await fs.open(target, "r");
44
+ try {
45
+ const opened = await fs.realpath(`/proc/self/fd/${handle.fd}`);
46
+ this.assertOpenedScope(share, opened);
47
+ const before = await handle.stat({ bigint: true });
48
+ if (!before.isFile() || before.size > BigInt(MAX_TEXT_BYTES))
49
+ throw new SaveError(before.isFile() ? 413 : 409, before.isFile() ? "existing_file_too_large" : "target_not_regular_file");
50
+ const buffer = Buffer.alloc(Number(before.size));
51
+ let offset = 0;
52
+ while (offset < buffer.length) {
53
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
54
+ if (!bytesRead)
55
+ break;
56
+ offset += bytesRead;
57
+ }
58
+ const after = await handle.stat({ bigint: true });
59
+ if (before.dev !== after.dev || before.ino !== after.ino || fileVersion(before) !== fileVersion(after) || offset !== buffer.length)
60
+ throw new SaveError(409, "target_changed_during_read");
61
+ if (buffer.includes(0))
62
+ throw new SaveError(415, "binary_content_denied");
63
+ let content;
64
+ try {
65
+ content = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
66
+ }
67
+ catch {
68
+ throw new SaveError(415, "invalid_utf8_denied");
69
+ }
70
+ return { content, version: fileVersion(after), size: buffer.length, editable: share.permission === "rw", permission: share.permission };
71
+ }
72
+ finally {
73
+ await handle.close();
74
+ }
75
+ }
76
+ async save(share, rawRelative, content, expectedVersion) {
77
+ if (share.permission !== "rw")
78
+ throw new SaveError(403, "read_only");
79
+ if (typeof content !== "string")
80
+ throw new SaveError(400, "content_must_be_string");
81
+ if (Buffer.byteLength(content, "utf8") > MAX_TEXT_BYTES)
82
+ throw new SaveError(413, "content_too_large");
83
+ if (content.includes("\0"))
84
+ throw new SaveError(415, "binary_content_denied");
85
+ if (typeof expectedVersion !== "string" || !expectedVersion)
86
+ throw new SaveError(428, "expected_version_required");
87
+ const upstream = await authorizeSharePath(share, rawRelative);
88
+ const relative = decodeRelativePath(upstream);
89
+ const target = path.resolve(share.root, relative);
90
+ await this.afterAuthorize?.(target);
91
+ if (!isEditableText(target))
92
+ throw new SaveError(415, "unsupported_text_type");
93
+ return this.serial(target, () => this.atomicSave(target, content, expectedVersion));
94
+ }
95
+ serial(target, operation) {
96
+ const previous = this.queues.get(target) ?? Promise.resolve();
97
+ const result = previous.then(operation, operation);
98
+ const tail = result.then(() => undefined, () => undefined);
99
+ this.queues.set(target, tail);
100
+ void tail.finally(() => { if (this.queues.get(target) === tail)
101
+ this.queues.delete(target); });
102
+ return result;
103
+ }
104
+ // Save deliberately never writes through an fd opened on the authoritative inode.
105
+ // It brackets creation of a same-directory temp file with two bigint lstat +
106
+ // optimistic-version checks, then replaces only the verified pathname by atomic rename.
107
+ async atomicSave(target, content, expectedVersion) {
108
+ const parent = path.dirname(target);
109
+ const canonicalParent = await fs.realpath(parent);
110
+ if (canonicalParent !== parent)
111
+ throw new SaveError(409, "parent_changed");
112
+ const before = await fs.lstat(target, { bigint: true });
113
+ if (!before.isFile() || before.isSymbolicLink())
114
+ throw new SaveError(409, "target_not_regular_file");
115
+ if (before.size > MAX_TEXT_BYTES)
116
+ throw new SaveError(413, "existing_file_too_large");
117
+ if (fileVersion(before) !== expectedVersion)
118
+ throw new SaveError(409, "version_conflict");
119
+ const temp = path.join(parent, `.${path.basename(target)}.fileport-${process.pid}-${crypto.randomBytes(8).toString("hex")}.tmp`);
120
+ let handle;
121
+ try {
122
+ handle = await fs.open(temp, "wx", Number(before.mode & 511n));
123
+ await handle.writeFile(content, "utf8");
124
+ await handle.chown(Number(before.uid), Number(before.gid));
125
+ await handle.chmod(Number(before.mode & 511n));
126
+ await handle.sync();
127
+ await handle.close();
128
+ handle = undefined;
129
+ const [parentNow, current] = await Promise.all([fs.realpath(parent), fs.lstat(target, { bigint: true })]);
130
+ if (parentNow !== canonicalParent || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino || fileVersion(current) !== expectedVersion) {
131
+ throw new SaveError(409, "target_changed_during_save");
132
+ }
133
+ await fs.rename(temp, target);
134
+ const directory = await fs.open(parent, "r");
135
+ try {
136
+ await directory.sync();
137
+ }
138
+ finally {
139
+ await directory.close();
140
+ }
141
+ const after = await fs.stat(target, { bigint: true });
142
+ return { version: fileVersion(after), size: Number(after.size) };
143
+ }
144
+ finally {
145
+ try {
146
+ await handle?.close();
147
+ }
148
+ catch { }
149
+ try {
150
+ await fs.unlink(temp);
151
+ }
152
+ catch { }
153
+ }
154
+ }
155
+ }
156
+ export class SaveError extends Error {
157
+ status;
158
+ code;
159
+ constructor(status, code) {
160
+ super(code);
161
+ this.status = status;
162
+ this.code = code;
163
+ }
164
+ }
165
+ //# sourceMappingURL=save.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save.js","sourceRoot":"","sources":["../src/save.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export type ShareKind = "file" | "directory";
2
+ export type SharePermission = "ro" | "rw";
3
+ export interface Share {
4
+ id: string;
5
+ target: string;
6
+ root: string;
7
+ kind: ShareKind;
8
+ permission: SharePermission;
9
+ createdAt: string;
10
+ expiresAt?: string;
11
+ }
12
+ export interface FileportConfig {
13
+ stateDir: string;
14
+ host: string;
15
+ port: number;
16
+ dufsBin: string;
17
+ publicBaseUrl?: string;
18
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/dist/ui.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Share } from "./types.js";
2
+ export declare function previewHtml(share: Share): string;
package/dist/ui.js ADDED
@@ -0,0 +1,20 @@
1
+ export function previewHtml(share) {
2
+ const config = JSON.stringify({ shareId: share.id, kind: share.kind, permission: share.permission, initialPath: share.kind === "file" ? share.target.split("/").pop() : "" }).replace(/</g, "\\u003c");
3
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Fileport</title>
4
+ <style>:root{font:15px system-ui,sans-serif;color:#202124;background:#f6f7f9}*{box-sizing:border-box}body{margin:0}header{height:52px;display:flex;align-items:center;gap:12px;padding:0 18px;background:#fff;border-bottom:1px solid #dfe1e5}header strong{font-size:17px}.badge{font-size:12px;padding:3px 7px;border-radius:12px;background:#e8f0fe;color:#174ea6}main{max-width:1200px;margin:18px auto;padding:0 16px}.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:12px}.path{font-family:ui-monospace,monospace;overflow-wrap:anywhere}.status{margin-left:auto;color:#5f6368;font-size:13px}.panel{background:#fff;border:1px solid #dfe1e5;border-radius:10px;overflow:hidden;min-height:160px}.entry{display:flex;gap:10px;padding:10px 14px;border-bottom:1px solid #eee;color:#174ea6;text-decoration:none}.entry:hover{background:#f8faff}.content{padding:18px;overflow:auto}pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.5 ui-monospace,SFMono-Regular,monospace}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px;text-align:left}img{max-width:100%;height:auto}.error{color:#b3261e;padding:18px}button{border:1px solid #ccd;background:#fff;border-radius:6px;padding:6px 10px;cursor:pointer}button:disabled{opacity:.5}.save{background:#174ea6;color:#fff}textarea{width:100%;height:70vh;border:0;padding:16px;resize:vertical;font:13px/1.5 ui-monospace,SFMono-Regular,monospace}</style></head><body>
5
+ <header><strong>Fileport</strong><span class="badge">${share.permission === "rw" ? "RW · saves modify the authoritative file" : "RO · read-only capability"}</span></header>
6
+ <main><div class="toolbar"><button id="up">Up</button><button id="edit" hidden>Edit</button><button id="save" class="save" hidden>Save</button><span class="path" id="path"></span><span class="status" id="status"></span></div><section class="panel" id="view"><div class="content">Loading…</div></section></main>
7
+ <script>const CONFIG=${config};
8
+ const view=document.getElementById('view'),pathLabel=document.getElementById('path'),up=document.getElementById('up'),edit=document.getElementById('edit'),save=document.getElementById('save'),status=document.getElementById('status');let active={path:'',text:'',version:'',editable:false,editing:false,type:''};
9
+ const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
10
+ const enc=p=>p.split('/').filter(Boolean).map(encodeURIComponent).join('/');const fsUrl=p=>'/s/'+CONFIG.shareId+'/fs/'+enc(p);const apiUrl=(kind,p)=>'/s/'+CONFIG.shareId+'/api/'+kind+'/'+enc(p);const current=()=>new URL(location.href).searchParams.get('path')??CONFIG.initialPath;
11
+ function navigate(p){const u=new URL(location.href);p?u.searchParams.set('path',p):u.searchParams.delete('path');history.pushState({},'',u);render()}
12
+ function markdown(text){let code=false;return text.split('\\n').map(line=>{const safe=esc(line);if(line.startsWith(String.fromCharCode(96,96,96))){code=!code;return code?'<pre><code>':'</code></pre>'}if(code)return safe+'\\n';const h=line.match(/^(#{1,6})\\s+(.*)$/);if(h)return '<h'+h[1].length+'>'+esc(h[2])+'</h'+h[1].length+'>';if(/^[-*] /.test(line))return '<div>• '+esc(line.slice(2))+'</div>';return line?'<p>'+safe+'</p>':'<br>'}).join('')}
13
+ function csv(text){const rows=text.trim().split(/\\r?\\n/).map(r=>r.split(','));return '<table>'+rows.map((r,i)=>'<tr>'+r.map(c=>'<'+(i?'td':'th')+'>'+esc(c)+'</'+(i?'td':'th')+'>').join('')+'</tr>').join('')+'</table>'}
14
+ function sourceView(p,text,type){const ext=p.toLowerCase().split('.').pop();if(type.includes('text/html')||ext==='html'){const url=URL.createObjectURL(new Blob([text],{type:'text/html'}));view.innerHTML='<iframe sandbox style="width:100%;height:75vh;border:0" src="'+url+'"></iframe>';return}let formatted=text;if(ext==='json'){try{formatted=JSON.stringify(JSON.parse(text),null,2)}catch{}}if(ext==='md'||ext==='markdown'){view.innerHTML='<article class="content">'+markdown(formatted)+'</article>';return}if(ext==='csv'||ext==='tsv'){view.innerHTML='<div class="content">'+(ext==='csv'?csv(formatted):csv(formatted.replaceAll('\\t',',')))+'</div>';return}view.innerHTML='<div class="content"><pre>'+esc(formatted)+'</pre></div>'}
15
+ async function showFile(p){const response=await fetch(fsUrl(p));if(!response.ok)throw new Error('HTTP '+response.status);const type=response.headers.get('content-type')||'';if(type.startsWith('image/')){const url=URL.createObjectURL(await response.blob());view.innerHTML='<div class="content"><img alt="preview" src="'+url+'"></div>';return}let text=await response.text(),version='',editable=false;if(CONFIG.permission==='rw'){const editResponse=await fetch(apiUrl('edit',p));if(editResponse.ok){const snapshot=await editResponse.json();text=snapshot.content;version=snapshot.version;editable=snapshot.editable===true}}active={path:p,text,version,editable,editing:false,type};sourceView(p,text,type);edit.hidden=!active.editable;edit.onclick=()=>{active.editing=true;view.innerHTML='<textarea id="editor"></textarea>';document.getElementById('editor').value=active.text;edit.hidden=true;save.hidden=false;status.textContent='Unsaved changes'};save.onclick=async()=>{if(!confirm('Save changes to the authoritative file?'))return;save.disabled=true;const content=document.getElementById('editor').value,response=await fetch(apiUrl('save',active.path),{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({content,expectedVersion:active.version})}),data=await response.json();save.disabled=false;if(!response.ok){status.textContent='Save failed: '+data.error;return}active.text=content;active.version=data.version;active.editing=false;status.textContent='Saved';edit.hidden=false;save.hidden=true;sourceView(active.path,active.text,active.type)}}
16
+ async function showDirectory(p){const response=await fetch((p?fsUrl(p)+'/':fsUrl(''))+'?json');if(!response.ok)throw new Error('HTTP '+response.status);const data=await response.json(),entries=data.paths||[];view.innerHTML=entries.length?entries.map(item=>{const name=String(item.name),leaf=name.includes('/')?name.slice(name.lastIndexOf('/')+1):name,next=[p,leaf].filter(Boolean).join('/'),icon=item.path_type==='Dir'?'📁':'📄';return '<a class="entry" href="?path='+encodeURIComponent(next)+'" data-path="'+esc(next)+'"><span>'+icon+'</span><span>'+esc(leaf)+'</span></a>'}).join(''):'<div class="content">Empty directory</div>';view.querySelectorAll('[data-path]').forEach(a=>a.addEventListener('click',e=>{e.preventDefault();navigate(a.dataset.path)}))}
17
+ async function render(){const p=current();edit.hidden=true;save.hidden=true;status.textContent='';pathLabel.textContent='/'+p;up.disabled=!p||CONFIG.kind==='file';up.onclick=()=>navigate(p.split('/').slice(0,-1).join('/'));view.innerHTML='<div class="content">Loading…</div>';try{if(CONFIG.kind==='file')await showFile(CONFIG.initialPath);else{const probe=await fetch(fsUrl(p)+(p?'/':'')+'?json');if(probe.ok){const data=await probe.clone().json().catch(()=>null);if(data?.kind==='Index'){await showDirectory(p);return}}await showFile(p)}}catch(e){view.innerHTML='<div class="error">'+esc(e.message||e)+'</div>'}}
18
+ addEventListener('popstate',render);render();</script></body></html>`;
19
+ }
20
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACxM,OAAO;;uDAE8C,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAC,2BAA2B;;uBAEpI,MAAM;;;;;;;;;;;qEAWwC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ interface Worker {
3
+ root: string;
4
+ socket: string;
5
+ child: ChildProcess;
6
+ }
7
+ export declare class DufsWorkerManager {
8
+ private readonly stateDir;
9
+ private readonly dufsBin;
10
+ private workers;
11
+ private starts;
12
+ private closing;
13
+ private workerDir;
14
+ constructor(stateDir: string, dufsBin: string);
15
+ private prepareWorkerDir;
16
+ get(root: string): Promise<Worker>;
17
+ private start;
18
+ socketPathFor(root: string): string;
19
+ count(): number;
20
+ pidFor(root: string): number | undefined;
21
+ close(): Promise<void>;
22
+ }
23
+ export {};
@@ -0,0 +1,110 @@
1
+ import { spawn } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import http from "node:http";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ export class DufsWorkerManager {
8
+ stateDir;
9
+ dufsBin;
10
+ workers = new Map();
11
+ starts = new Map();
12
+ closing = false;
13
+ workerDir;
14
+ constructor(stateDir, dufsBin) {
15
+ this.stateDir = stateDir;
16
+ this.dufsBin = dufsBin;
17
+ const identity = crypto.createHash("sha256").update(path.resolve(stateDir)).digest("hex").slice(0, 12);
18
+ this.workerDir = path.join(os.tmpdir(), `fileport-${process.getuid?.() ?? "user"}-${identity}`);
19
+ }
20
+ async prepareWorkerDir() {
21
+ await fs.mkdir(this.workerDir, { recursive: true, mode: 0o700 });
22
+ const stat = await fs.lstat(this.workerDir);
23
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
24
+ throw new Error("unsafe Fileport runtime directory");
25
+ await fs.chmod(this.workerDir, 0o700);
26
+ }
27
+ async get(root) {
28
+ if (this.closing)
29
+ throw new Error("Dufs worker manager is closing");
30
+ const current = this.workers.get(root);
31
+ if (current && current.child.exitCode === null)
32
+ return current;
33
+ const pending = this.starts.get(root);
34
+ if (pending)
35
+ return pending;
36
+ const start = this.start(root).finally(() => this.starts.delete(root));
37
+ this.starts.set(root, start);
38
+ return start;
39
+ }
40
+ async start(root) {
41
+ await fs.access(this.dufsBin, fs.constants.X_OK);
42
+ await this.prepareWorkerDir();
43
+ const socket = this.socketPathFor(root);
44
+ try {
45
+ await fs.unlink(socket);
46
+ }
47
+ catch (error) {
48
+ if (error.code !== "ENOENT")
49
+ throw error;
50
+ }
51
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("DUFS_")));
52
+ const child = spawn(this.dufsBin, ["--bind", socket, "--allow-search", "--hidden", ".git,.ssh,.gnupg,.env,.env.*,.netrc,*.pem,*.p12,*.pfx,*token*,*cookie*,*credential*,*private-key*", root], { stdio: ["ignore", "ignore", "pipe"], env });
53
+ let stderr = "";
54
+ child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
55
+ await new Promise((resolve, reject) => {
56
+ const deadline = Date.now() + 5000;
57
+ const poll = async () => {
58
+ if (child.exitCode !== null)
59
+ return reject(new Error(`dufs exited (${child.exitCode}): ${stderr}`));
60
+ try {
61
+ const stat = await fs.stat(socket);
62
+ if (stat.isSocket()) {
63
+ await fs.chmod(socket, 0o600);
64
+ const ready = await new Promise((done) => {
65
+ const request = http.request({ socketPath: socket, path: "/?json", method: "HEAD" }, (response) => {
66
+ response.resume();
67
+ response.once("end", () => done(response.statusCode === 200));
68
+ });
69
+ request.once("error", () => done(false));
70
+ request.end();
71
+ });
72
+ if (ready)
73
+ return resolve();
74
+ }
75
+ }
76
+ catch { }
77
+ if (Date.now() >= deadline) {
78
+ child.kill();
79
+ return reject(new Error(`dufs socket timeout: ${stderr}`));
80
+ }
81
+ setTimeout(poll, 25);
82
+ };
83
+ child.once("error", reject);
84
+ void poll();
85
+ });
86
+ if (this.closing) {
87
+ child.kill("SIGTERM");
88
+ throw new Error("Dufs worker manager is closing");
89
+ }
90
+ const worker = { root, socket, child };
91
+ child.once("exit", () => { if (this.workers.get(root)?.child === child)
92
+ this.workers.delete(root); });
93
+ this.workers.set(root, worker);
94
+ return worker;
95
+ }
96
+ socketPathFor(root) { return path.join(this.workerDir, `${crypto.createHash("sha256").update(root).digest("hex").slice(0, 20)}.sock`); }
97
+ count() { return [...this.workers.values()].filter((worker) => worker.child.exitCode === null).length; }
98
+ pidFor(root) { return this.workers.get(root)?.child.pid; }
99
+ async close() {
100
+ this.closing = true;
101
+ await Promise.allSettled([...this.starts.values()]);
102
+ this.starts.clear();
103
+ const workers = [...this.workers.values()];
104
+ this.workers.clear();
105
+ await Promise.all(workers.map((worker) => new Promise((resolve) => { if (worker.child.exitCode !== null)
106
+ return resolve(); worker.child.once("exit", () => resolve()); worker.child.kill("SIGTERM"); setTimeout(() => { if (worker.child.exitCode === null)
107
+ worker.child.kill("SIGKILL"); }, 1000).unref(); })));
108
+ }
109
+ }
110
+ //# sourceMappingURL=workers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workers.js","sourceRoot":"","sources":["../src/workers.ts"],"names":[],"mappings":"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"}