@tinycloud/vfs 0.1.0-beta.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.
- package/dist/index.cjs +910 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +394 -0
- package/dist/index.d.ts +394 -0
- package/dist/index.js +869 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.cjs +611 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.js +609 -0
- package/dist/worker.js.map +1 -0
- package/package.json +41 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
// src/worker.ts
|
|
2
|
+
import { parentPort } from "worker_threads";
|
|
3
|
+
import { TinyCloudNode } from "@tinycloud/node-sdk";
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var ERRNO = {
|
|
7
|
+
EPERM: -1,
|
|
8
|
+
ENOENT: -2,
|
|
9
|
+
EIO: -5,
|
|
10
|
+
EBADF: -9,
|
|
11
|
+
EACCES: -13,
|
|
12
|
+
EBUSY: -16,
|
|
13
|
+
EEXIST: -17,
|
|
14
|
+
ENOTDIR: -20,
|
|
15
|
+
EISDIR: -21,
|
|
16
|
+
EINVAL: -22,
|
|
17
|
+
ENOTEMPTY: -39,
|
|
18
|
+
EROFS: -30
|
|
19
|
+
};
|
|
20
|
+
function createNodeError(code, message, syscall, path) {
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.errno = ERRNO[code] ?? -1;
|
|
24
|
+
error.syscall = syscall;
|
|
25
|
+
if (path) {
|
|
26
|
+
error.path = path;
|
|
27
|
+
}
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
function createENOENT(syscall, path) {
|
|
31
|
+
return createNodeError("ENOENT", `no such file or directory, ${syscall} '${path}'`, syscall, path);
|
|
32
|
+
}
|
|
33
|
+
function createEISDIR(syscall, path) {
|
|
34
|
+
return createNodeError("EISDIR", `illegal operation on a directory, ${syscall} '${path}'`, syscall, path);
|
|
35
|
+
}
|
|
36
|
+
function createENOTDIR(syscall, path) {
|
|
37
|
+
return createNodeError("ENOTDIR", `not a directory, ${syscall} '${path}'`, syscall, path);
|
|
38
|
+
}
|
|
39
|
+
function createENOTEMPTY(syscall, path) {
|
|
40
|
+
return createNodeError("ENOTEMPTY", `directory not empty, ${syscall} '${path}'`, syscall, path);
|
|
41
|
+
}
|
|
42
|
+
function createEEXIST(syscall, path) {
|
|
43
|
+
return createNodeError("EEXIST", `file already exists, ${syscall} '${path}'`, syscall, path);
|
|
44
|
+
}
|
|
45
|
+
function createEACCES(syscall, path, message = "permission denied") {
|
|
46
|
+
return createNodeError("EACCES", `${message}, ${syscall} '${path}'`, syscall, path);
|
|
47
|
+
}
|
|
48
|
+
function createEIO(syscall, path, message) {
|
|
49
|
+
return createNodeError("EIO", `${message}, ${syscall} '${path}'`, syscall, path);
|
|
50
|
+
}
|
|
51
|
+
function createEBUSY(syscall, path, message = "resource busy or locked") {
|
|
52
|
+
return createNodeError("EBUSY", `${message}, ${syscall} '${path}'`, syscall, path);
|
|
53
|
+
}
|
|
54
|
+
function createEINVAL(syscall, path, message = "invalid argument") {
|
|
55
|
+
return createNodeError("EINVAL", `${message}, ${syscall} '${path}'`, syscall, path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/pathing.ts
|
|
59
|
+
import { posix as pathPosix } from "path";
|
|
60
|
+
var INTERNAL_META_PREFIX = ".tcvfs-meta";
|
|
61
|
+
function normalizeVfsPath(inputPath) {
|
|
62
|
+
const normalized = pathPosix.normalize(inputPath.replace(/\\/g, "/"));
|
|
63
|
+
const absolute = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
64
|
+
return absolute;
|
|
65
|
+
}
|
|
66
|
+
function toLogicalPath(inputPath) {
|
|
67
|
+
const normalized = normalizeVfsPath(inputPath);
|
|
68
|
+
if (normalized.includes("\0")) {
|
|
69
|
+
throw new Error("invalid path");
|
|
70
|
+
}
|
|
71
|
+
if (normalized === "/") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
const logical = normalized.slice(1);
|
|
75
|
+
if (logical === INTERNAL_META_PREFIX || logical.startsWith(`${INTERNAL_META_PREFIX}/`) || logical.split("/").includes("..")) {
|
|
76
|
+
throw new Error("path escapes virtual root");
|
|
77
|
+
}
|
|
78
|
+
return logical;
|
|
79
|
+
}
|
|
80
|
+
function normalizeStoragePrefix(prefix) {
|
|
81
|
+
if (!prefix) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
return prefix.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
85
|
+
}
|
|
86
|
+
function joinStoragePath(...parts) {
|
|
87
|
+
const cleaned = parts.filter((part) => Boolean(part)).map((part) => normalizeStoragePrefix(part));
|
|
88
|
+
return cleaned.filter(Boolean).join("/");
|
|
89
|
+
}
|
|
90
|
+
function dirnameOf(logicalPath) {
|
|
91
|
+
if (!logicalPath) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
const dir = pathPosix.dirname(`/${logicalPath}`);
|
|
95
|
+
return dir === "/" ? "" : dir.slice(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/metadata.ts
|
|
99
|
+
function dataKey(storageRoot, logicalPath) {
|
|
100
|
+
return joinStoragePath(storageRoot, logicalPath);
|
|
101
|
+
}
|
|
102
|
+
function metadataKey(storageRoot, logicalPath) {
|
|
103
|
+
return joinStoragePath(storageRoot, INTERNAL_META_PREFIX, logicalPath);
|
|
104
|
+
}
|
|
105
|
+
function metadataPrefix(storageRoot, logicalPath = "") {
|
|
106
|
+
return joinStoragePath(storageRoot, INTERNAL_META_PREFIX, logicalPath);
|
|
107
|
+
}
|
|
108
|
+
function encodeEnvelope(content) {
|
|
109
|
+
return {
|
|
110
|
+
version: 1,
|
|
111
|
+
encoding: "base64",
|
|
112
|
+
data: content.toString("base64")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isEnvelopeShape(value) {
|
|
116
|
+
return Boolean(
|
|
117
|
+
value && typeof value === "object" && value.version === 1 && value.encoding === "base64" && typeof value.data === "string"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function encodeFileValue(content) {
|
|
121
|
+
const utf8 = content.toString("utf8");
|
|
122
|
+
if (Buffer.from(utf8, "utf8").equals(content)) {
|
|
123
|
+
return utf8;
|
|
124
|
+
}
|
|
125
|
+
return encodeEnvelope(content);
|
|
126
|
+
}
|
|
127
|
+
function decodeEnvelope(value) {
|
|
128
|
+
if (isEnvelopeShape(value)) {
|
|
129
|
+
return Buffer.from(value.data, "base64");
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === "string") {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(value);
|
|
134
|
+
if (isEnvelopeShape(parsed)) {
|
|
135
|
+
return Buffer.from(parsed.data, "base64");
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
return Buffer.from(value, "utf8");
|
|
140
|
+
}
|
|
141
|
+
throw new Error("unsupported file payload");
|
|
142
|
+
}
|
|
143
|
+
function isMetadataShape(value) {
|
|
144
|
+
return Boolean(
|
|
145
|
+
value && typeof value === "object" && (value.kind === "file" || value.kind === "directory") && typeof value.size === "number" && typeof value.mode === "number" && typeof value.ctimeMs === "number" && typeof value.mtimeMs === "number" && typeof value.birthtimeMs === "number"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function decodeMetadata(value) {
|
|
149
|
+
if (isMetadataShape(value)) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
const parsed = JSON.parse(value);
|
|
154
|
+
if (isMetadataShape(parsed)) {
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw new Error("unsupported metadata payload");
|
|
159
|
+
}
|
|
160
|
+
function nowMetadata(kind, size, mode, existing) {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
return {
|
|
163
|
+
kind,
|
|
164
|
+
size,
|
|
165
|
+
mode,
|
|
166
|
+
ctimeMs: now,
|
|
167
|
+
mtimeMs: now,
|
|
168
|
+
birthtimeMs: existing?.birthtimeMs ?? now
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function normalizeMode(kind, mode) {
|
|
172
|
+
if (typeof mode === "number" && Number.isFinite(mode)) {
|
|
173
|
+
return mode;
|
|
174
|
+
}
|
|
175
|
+
return kind === "directory" ? 493 : 420;
|
|
176
|
+
}
|
|
177
|
+
function stripStoragePrefix(fullKey, prefix) {
|
|
178
|
+
const normalizedPrefix = normalizeStoragePrefix(prefix);
|
|
179
|
+
if (!normalizedPrefix) {
|
|
180
|
+
return fullKey.replace(/^\/+/, "");
|
|
181
|
+
}
|
|
182
|
+
const withSlash = `${normalizedPrefix}/`;
|
|
183
|
+
if (fullKey === normalizedPrefix) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
if (fullKey.startsWith(withSlash)) {
|
|
187
|
+
return fullKey.slice(withSlash.length);
|
|
188
|
+
}
|
|
189
|
+
return fullKey;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/worker.ts
|
|
193
|
+
var state = {
|
|
194
|
+
kv: null,
|
|
195
|
+
kvPrefix: "",
|
|
196
|
+
storageRoot: ""
|
|
197
|
+
};
|
|
198
|
+
function toWorkerError(error) {
|
|
199
|
+
if (error && typeof error === "object" && "code" in error && "message" in error) {
|
|
200
|
+
const typed = error;
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: {
|
|
204
|
+
code: typed.code ?? "EIO",
|
|
205
|
+
message: typed.message,
|
|
206
|
+
syscall: typed.syscall,
|
|
207
|
+
path: typed.path
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
error: {
|
|
214
|
+
code: "EIO",
|
|
215
|
+
message: error instanceof Error ? error.message : String(error),
|
|
216
|
+
syscall: "vfs"
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function ensureKv() {
|
|
221
|
+
if (!state.kv) {
|
|
222
|
+
throw createEIO("init", "/", "worker is not initialized");
|
|
223
|
+
}
|
|
224
|
+
return state.kv;
|
|
225
|
+
}
|
|
226
|
+
function ensureMountedLogicalPath(inputPath) {
|
|
227
|
+
try {
|
|
228
|
+
return toLogicalPath(inputPath);
|
|
229
|
+
} catch {
|
|
230
|
+
throw createENOENT("resolve", inputPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function effectiveStorageRoot() {
|
|
234
|
+
return joinStoragePath(state.kvPrefix, state.storageRoot);
|
|
235
|
+
}
|
|
236
|
+
function effectiveStoragePath(logicalPath = "") {
|
|
237
|
+
return joinStoragePath(effectiveStorageRoot(), logicalPath);
|
|
238
|
+
}
|
|
239
|
+
function scopedDataKey(logicalPath) {
|
|
240
|
+
return dataKey(effectiveStorageRoot(), logicalPath);
|
|
241
|
+
}
|
|
242
|
+
function scopedMetaKey(logicalPath) {
|
|
243
|
+
return metadataKey(effectiveStorageRoot(), logicalPath);
|
|
244
|
+
}
|
|
245
|
+
function scopedMetaPrefix(logicalPath = "") {
|
|
246
|
+
return metadataPrefix(effectiveStorageRoot(), logicalPath);
|
|
247
|
+
}
|
|
248
|
+
function effectiveListPrefix(logicalPath = "", options) {
|
|
249
|
+
const prefix = effectiveStoragePath(logicalPath);
|
|
250
|
+
if (!options?.trailingSlash || !prefix) {
|
|
251
|
+
return prefix;
|
|
252
|
+
}
|
|
253
|
+
return prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
254
|
+
}
|
|
255
|
+
async function kvGet(key) {
|
|
256
|
+
const kv = ensureKv();
|
|
257
|
+
const result = await kv.get(key);
|
|
258
|
+
if (!result.ok) {
|
|
259
|
+
if (result.error?.code === "KV_NOT_FOUND") {
|
|
260
|
+
throw createENOENT("get", key);
|
|
261
|
+
}
|
|
262
|
+
if (result.error?.code === "AUTH_UNAUTHORIZED") {
|
|
263
|
+
throw createEACCES("get", key, result.error.message);
|
|
264
|
+
}
|
|
265
|
+
throw createEIO("get", key, result.error?.message ?? "kv get failed");
|
|
266
|
+
}
|
|
267
|
+
return result.data.data;
|
|
268
|
+
}
|
|
269
|
+
async function kvPut(key, value) {
|
|
270
|
+
const kv = ensureKv();
|
|
271
|
+
const result = await kv.put(key, value);
|
|
272
|
+
if (!result.ok) {
|
|
273
|
+
if (result.error?.code === "AUTH_UNAUTHORIZED") {
|
|
274
|
+
throw createEACCES("put", key, result.error.message);
|
|
275
|
+
}
|
|
276
|
+
throw createEIO("put", key, result.error?.message ?? "kv put failed");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function kvDelete(key) {
|
|
280
|
+
const kv = ensureKv();
|
|
281
|
+
const result = await kv.delete(key);
|
|
282
|
+
if (!result.ok) {
|
|
283
|
+
if (result.error?.code === "KV_NOT_FOUND") {
|
|
284
|
+
throw createENOENT("unlink", key);
|
|
285
|
+
}
|
|
286
|
+
if (result.error?.code === "AUTH_UNAUTHORIZED") {
|
|
287
|
+
throw createEACCES("unlink", key, result.error.message);
|
|
288
|
+
}
|
|
289
|
+
throw createEIO("unlink", key, result.error?.message ?? "kv delete failed");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function kvList(prefix) {
|
|
293
|
+
const kv = ensureKv();
|
|
294
|
+
const listOptions = prefix ? { prefix, removePrefix: false } : { removePrefix: false };
|
|
295
|
+
const result = await kv.list(listOptions);
|
|
296
|
+
if (!result.ok) {
|
|
297
|
+
if (result.error?.code === "AUTH_UNAUTHORIZED") {
|
|
298
|
+
throw createEACCES("scandir", prefix, result.error.message);
|
|
299
|
+
}
|
|
300
|
+
throw createEIO("scandir", prefix, result.error?.message ?? "kv list failed");
|
|
301
|
+
}
|
|
302
|
+
return result.data.keys ?? [];
|
|
303
|
+
}
|
|
304
|
+
async function tryMetadata(logicalPath) {
|
|
305
|
+
try {
|
|
306
|
+
return decodeMetadata(await kvGet(scopedMetaKey(logicalPath)));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const typed = error;
|
|
309
|
+
if (typed.code === "ENOENT") {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function tryRawFile(logicalPath) {
|
|
316
|
+
try {
|
|
317
|
+
const content = decodeEnvelope(await kvGet(scopedDataKey(logicalPath)));
|
|
318
|
+
const metadata = nowMetadata(
|
|
319
|
+
"file",
|
|
320
|
+
content.length,
|
|
321
|
+
normalizeMode("file")
|
|
322
|
+
);
|
|
323
|
+
return { content, metadata };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const typed = error;
|
|
326
|
+
if (typed.code === "ENOENT") {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function listDescendantKeys(logicalPath) {
|
|
333
|
+
const dataPrefixValue = effectiveListPrefix(logicalPath, { trailingSlash: true });
|
|
334
|
+
const metaPrefixValue = effectiveListPrefix(
|
|
335
|
+
joinStoragePath(".tcvfs-meta", logicalPath),
|
|
336
|
+
{ trailingSlash: true }
|
|
337
|
+
);
|
|
338
|
+
const [dataKeys, metaKeys] = await Promise.all([
|
|
339
|
+
kvList(dataPrefixValue),
|
|
340
|
+
kvList(metaPrefixValue)
|
|
341
|
+
]);
|
|
342
|
+
return { dataKeys, metaKeys };
|
|
343
|
+
}
|
|
344
|
+
async function tryInferredDirectory(logicalPath) {
|
|
345
|
+
const { dataKeys, metaKeys } = await listDescendantKeys(logicalPath);
|
|
346
|
+
if (dataKeys.length === 0 && metaKeys.length === 0) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
return nowMetadata(
|
|
350
|
+
"directory",
|
|
351
|
+
4096,
|
|
352
|
+
normalizeMode("directory")
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
async function ensureDirectory(logicalPath) {
|
|
356
|
+
if (!logicalPath) {
|
|
357
|
+
return nowMetadata("directory", 4096, normalizeMode("directory"));
|
|
358
|
+
}
|
|
359
|
+
const metadata = await tryMetadata(logicalPath);
|
|
360
|
+
if (metadata) {
|
|
361
|
+
if (metadata.kind !== "directory") {
|
|
362
|
+
throw createENOTDIR("stat", `/${logicalPath}`);
|
|
363
|
+
}
|
|
364
|
+
return metadata;
|
|
365
|
+
}
|
|
366
|
+
const inferred = await tryInferredDirectory(logicalPath);
|
|
367
|
+
if (!inferred) {
|
|
368
|
+
throw createENOENT("stat", `/${logicalPath}`);
|
|
369
|
+
}
|
|
370
|
+
return inferred;
|
|
371
|
+
}
|
|
372
|
+
async function readFileEntry(logicalPath) {
|
|
373
|
+
const metadata = await tryMetadata(logicalPath);
|
|
374
|
+
if (metadata) {
|
|
375
|
+
if (metadata.kind !== "file") {
|
|
376
|
+
throw createEISDIR("open", `/${logicalPath}`);
|
|
377
|
+
}
|
|
378
|
+
const content = decodeEnvelope(await kvGet(scopedDataKey(logicalPath)));
|
|
379
|
+
return { content, metadata };
|
|
380
|
+
}
|
|
381
|
+
const rawFile = await tryRawFile(logicalPath);
|
|
382
|
+
if (rawFile) {
|
|
383
|
+
return rawFile;
|
|
384
|
+
}
|
|
385
|
+
if (await tryInferredDirectory(logicalPath)) {
|
|
386
|
+
throw createEISDIR("open", `/${logicalPath}`);
|
|
387
|
+
}
|
|
388
|
+
throw createENOENT("open", `/${logicalPath}`);
|
|
389
|
+
}
|
|
390
|
+
async function statPath(logicalPath) {
|
|
391
|
+
if (!logicalPath) {
|
|
392
|
+
return nowMetadata("directory", 4096, normalizeMode("directory"));
|
|
393
|
+
}
|
|
394
|
+
const metadata = await tryMetadata(logicalPath);
|
|
395
|
+
if (metadata) {
|
|
396
|
+
return metadata;
|
|
397
|
+
}
|
|
398
|
+
const rawFile = await tryRawFile(logicalPath);
|
|
399
|
+
if (rawFile) {
|
|
400
|
+
return rawFile.metadata;
|
|
401
|
+
}
|
|
402
|
+
const inferredDirectory = await tryInferredDirectory(logicalPath);
|
|
403
|
+
if (inferredDirectory) {
|
|
404
|
+
return inferredDirectory;
|
|
405
|
+
}
|
|
406
|
+
throw createENOENT("stat", `/${logicalPath}`);
|
|
407
|
+
}
|
|
408
|
+
async function ensureParentDirectory(logicalPath) {
|
|
409
|
+
const parent = dirnameOf(logicalPath);
|
|
410
|
+
if (!parent) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await ensureDirectory(parent);
|
|
414
|
+
}
|
|
415
|
+
async function collectDirChildren(logicalPath) {
|
|
416
|
+
await ensureDirectory(logicalPath);
|
|
417
|
+
const { dataKeys, metaKeys } = await listDescendantKeys(logicalPath);
|
|
418
|
+
const names = /* @__PURE__ */ new Set();
|
|
419
|
+
const relativePrefix = effectiveStoragePath(logicalPath);
|
|
420
|
+
const relativeMetaPrefix = scopedMetaPrefix(logicalPath);
|
|
421
|
+
for (const fullKey of dataKeys) {
|
|
422
|
+
const relative = stripStoragePrefix(fullKey, relativePrefix);
|
|
423
|
+
const first = relative.split("/").filter(Boolean)[0];
|
|
424
|
+
if (first) {
|
|
425
|
+
names.add(first);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
for (const fullKey of metaKeys) {
|
|
429
|
+
const relative = stripStoragePrefix(fullKey, relativeMetaPrefix);
|
|
430
|
+
const first = relative.split("/").filter(Boolean)[0];
|
|
431
|
+
if (first) {
|
|
432
|
+
names.add(first);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const entries = await Promise.all(
|
|
436
|
+
[...names].filter((name) => name !== INTERNAL_META_PREFIX).sort().map(async (name) => {
|
|
437
|
+
const childPath = joinStoragePath(logicalPath, name);
|
|
438
|
+
const metadata = await statPath(childPath);
|
|
439
|
+
return {
|
|
440
|
+
name,
|
|
441
|
+
kind: metadata.kind,
|
|
442
|
+
parentPath: logicalPath ? `/${logicalPath}` : "/"
|
|
443
|
+
};
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
return entries;
|
|
447
|
+
}
|
|
448
|
+
async function writeFileEntry(logicalPath, content, mode) {
|
|
449
|
+
await ensureParentDirectory(logicalPath);
|
|
450
|
+
const existing = await tryMetadata(logicalPath);
|
|
451
|
+
if (existing) {
|
|
452
|
+
if (existing.kind !== "file") {
|
|
453
|
+
throw createEISDIR("writeFile", `/${logicalPath}`);
|
|
454
|
+
}
|
|
455
|
+
} else if (await tryInferredDirectory(logicalPath)) {
|
|
456
|
+
throw createEISDIR("writeFile", `/${logicalPath}`);
|
|
457
|
+
}
|
|
458
|
+
const buffer = Buffer.from(content);
|
|
459
|
+
const metadata = nowMetadata(
|
|
460
|
+
"file",
|
|
461
|
+
buffer.length,
|
|
462
|
+
normalizeMode("file", mode ?? existing?.mode),
|
|
463
|
+
existing ?? void 0
|
|
464
|
+
);
|
|
465
|
+
await kvPut(scopedDataKey(logicalPath), encodeFileValue(buffer));
|
|
466
|
+
await kvPut(scopedMetaKey(logicalPath), metadata);
|
|
467
|
+
return metadata;
|
|
468
|
+
}
|
|
469
|
+
async function mkdirEntry(logicalPath, recursive = false, mode) {
|
|
470
|
+
if (!logicalPath) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const existing = await tryMetadata(logicalPath);
|
|
474
|
+
if (existing) {
|
|
475
|
+
if (existing.kind === "directory") {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
throw createEEXIST("mkdir", `/${logicalPath}`);
|
|
479
|
+
}
|
|
480
|
+
if (await tryRawFile(logicalPath)) {
|
|
481
|
+
throw createEEXIST("mkdir", `/${logicalPath}`);
|
|
482
|
+
}
|
|
483
|
+
if (await tryInferredDirectory(logicalPath)) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const segments = logicalPath.split("/").filter(Boolean);
|
|
487
|
+
const targets = recursive ? segments.map((_, index) => segments.slice(0, index + 1).join("/")) : [logicalPath];
|
|
488
|
+
for (const target of targets) {
|
|
489
|
+
const current = await tryMetadata(target);
|
|
490
|
+
if (current) {
|
|
491
|
+
if (current.kind !== "directory") {
|
|
492
|
+
throw createENOTDIR("mkdir", `/${target}`);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
await ensureParentDirectory(target);
|
|
497
|
+
const metadata = nowMetadata("directory", 4096, normalizeMode("directory", mode));
|
|
498
|
+
await kvPut(scopedMetaKey(target), metadata);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function rmdirEntry(logicalPath) {
|
|
502
|
+
if (!logicalPath) {
|
|
503
|
+
throw createEBUSY("rmdir", "/");
|
|
504
|
+
}
|
|
505
|
+
const metadata = await ensureDirectory(logicalPath);
|
|
506
|
+
const children = await collectDirChildren(logicalPath);
|
|
507
|
+
if (children.length > 0) {
|
|
508
|
+
throw createENOTEMPTY("rmdir", `/${logicalPath}`);
|
|
509
|
+
}
|
|
510
|
+
await kvDelete(scopedMetaKey(logicalPath));
|
|
511
|
+
}
|
|
512
|
+
async function unlinkEntry(logicalPath) {
|
|
513
|
+
const metadata = await statPath(logicalPath);
|
|
514
|
+
if (metadata.kind !== "file") {
|
|
515
|
+
throw createEISDIR("unlink", `/${logicalPath}`);
|
|
516
|
+
}
|
|
517
|
+
await kvDelete(scopedDataKey(logicalPath));
|
|
518
|
+
try {
|
|
519
|
+
await kvDelete(scopedMetaKey(logicalPath));
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const typed = error;
|
|
522
|
+
if (typed.code !== "ENOENT") {
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function renameEntry(oldLogicalPath, newLogicalPath) {
|
|
528
|
+
const source = await statPath(oldLogicalPath);
|
|
529
|
+
if (source.kind !== "file") {
|
|
530
|
+
throw createEINVAL("rename", `/${oldLogicalPath}`, "directory rename is not supported in v1");
|
|
531
|
+
}
|
|
532
|
+
const content = decodeEnvelope(await kvGet(scopedDataKey(oldLogicalPath)));
|
|
533
|
+
await writeFileEntry(newLogicalPath, content, source.mode);
|
|
534
|
+
await unlinkEntry(oldLogicalPath);
|
|
535
|
+
}
|
|
536
|
+
async function initialize(init) {
|
|
537
|
+
const node = new TinyCloudNode({ host: init.source.host });
|
|
538
|
+
await node.restoreSession(init.source.session);
|
|
539
|
+
const kv = node.kv;
|
|
540
|
+
const servicePrefix = init.source.kind === "resolved-delegation" ? normalizeStoragePrefix(init.source.kvPrefix) : normalizeStoragePrefix(kv.config?.prefix);
|
|
541
|
+
const mountPrefix = normalizeStoragePrefix(init.mountPrefix);
|
|
542
|
+
state.kv = kv;
|
|
543
|
+
state.kvPrefix = servicePrefix;
|
|
544
|
+
state.storageRoot = mountPrefix;
|
|
545
|
+
}
|
|
546
|
+
async function handleRequest(request) {
|
|
547
|
+
switch (request.type) {
|
|
548
|
+
case "init":
|
|
549
|
+
await initialize(request.init);
|
|
550
|
+
return { ok: true, result: null };
|
|
551
|
+
case "stat": {
|
|
552
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
553
|
+
const metadata = await statPath(logicalPath);
|
|
554
|
+
return { ok: true, result: { metadata } };
|
|
555
|
+
}
|
|
556
|
+
case "readFile": {
|
|
557
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
558
|
+
const { content, metadata } = await readFileEntry(logicalPath);
|
|
559
|
+
return { ok: true, result: { content, metadata } };
|
|
560
|
+
}
|
|
561
|
+
case "writeFile": {
|
|
562
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
563
|
+
await writeFileEntry(logicalPath, request.content, request.mode);
|
|
564
|
+
return { ok: true, result: null };
|
|
565
|
+
}
|
|
566
|
+
case "readdir": {
|
|
567
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
568
|
+
const entries = await collectDirChildren(logicalPath);
|
|
569
|
+
return { ok: true, result: { entries } };
|
|
570
|
+
}
|
|
571
|
+
case "mkdir": {
|
|
572
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
573
|
+
await mkdirEntry(logicalPath, request.recursive, request.mode);
|
|
574
|
+
return { ok: true, result: null };
|
|
575
|
+
}
|
|
576
|
+
case "rmdir": {
|
|
577
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
578
|
+
await rmdirEntry(logicalPath);
|
|
579
|
+
return { ok: true, result: null };
|
|
580
|
+
}
|
|
581
|
+
case "unlink": {
|
|
582
|
+
const logicalPath = ensureMountedLogicalPath(request.path);
|
|
583
|
+
await unlinkEntry(logicalPath);
|
|
584
|
+
return { ok: true, result: null };
|
|
585
|
+
}
|
|
586
|
+
case "rename": {
|
|
587
|
+
const oldLogicalPath = ensureMountedLogicalPath(request.oldPath);
|
|
588
|
+
const newLogicalPath = ensureMountedLogicalPath(request.newPath);
|
|
589
|
+
await renameEntry(oldLogicalPath, newLogicalPath);
|
|
590
|
+
return { ok: true, result: null };
|
|
591
|
+
}
|
|
592
|
+
default:
|
|
593
|
+
throw createEINVAL("vfs", "/");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
parentPort?.on("message", async (message) => {
|
|
597
|
+
const view = new Int32Array(message.waitBuffer);
|
|
598
|
+
try {
|
|
599
|
+
const response = await handleRequest(message.request);
|
|
600
|
+
message.replyPort.postMessage(response);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
message.replyPort.postMessage(toWorkerError(error));
|
|
603
|
+
} finally {
|
|
604
|
+
Atomics.store(view, 0, 1);
|
|
605
|
+
Atomics.notify(view, 0, 1);
|
|
606
|
+
message.replyPort.close();
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
//# sourceMappingURL=worker.js.map
|