@kuralle-agents/fs 0.10.0 → 0.12.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.
@@ -0,0 +1,691 @@
1
+ import { fromBuffer } from '../encoding.js';
2
+ import { createGlobMatcher, DEFAULT_DIR_MODE, DEFAULT_FILE_MODE, dirname, MAX_SYMLINK_DEPTH, normalizePath, resolvePath as resolvePathUtil, sortPaths, SYMLINK_MODE, validatePath, } from '../path-utils.js';
3
+ const DEFAULT_INLINE_THRESHOLD = 1_500_000;
4
+ const VALID_NAMESPACE = /^[a-z][a-z0-9_]*$/i;
5
+ const TEXT_ENCODER = new TextEncoder();
6
+ function split(normalized) {
7
+ return normalized === '/' ? [] : normalized.slice(1).split('/');
8
+ }
9
+ function basename(path) {
10
+ const norm = normalizePath(path);
11
+ if (norm === '/')
12
+ return '';
13
+ return norm.slice(norm.lastIndexOf('/') + 1);
14
+ }
15
+ function bytesToBase64(bytes) {
16
+ const chunk = 8192;
17
+ let binary = '';
18
+ for (let i = 0; i < bytes.byteLength; i += chunk) {
19
+ binary += String.fromCharCode(...bytes.subarray(i, Math.min(i + chunk, bytes.byteLength)));
20
+ }
21
+ return btoa(binary);
22
+ }
23
+ function base64ToBytes(b64) {
24
+ const binary = atob(b64);
25
+ const bytes = new Uint8Array(binary.length);
26
+ for (let i = 0; i < binary.length; i++) {
27
+ bytes[i] = binary.charCodeAt(i);
28
+ }
29
+ return bytes;
30
+ }
31
+ function rowMtime(row) {
32
+ return new Date(row.modified_at * 1000);
33
+ }
34
+ function rowToStat(row) {
35
+ return {
36
+ type: row.type,
37
+ size: row.type === 'symlink' ? (row.target?.length ?? 0) : row.size,
38
+ mtime: rowMtime(row),
39
+ mode: row.type === 'directory'
40
+ ? DEFAULT_DIR_MODE
41
+ : row.type === 'symlink'
42
+ ? SYMLINK_MODE
43
+ : DEFAULT_FILE_MODE,
44
+ };
45
+ }
46
+ export class SqlFileSystem {
47
+ backend;
48
+ namespace;
49
+ tableName;
50
+ indexName;
51
+ blobs;
52
+ threshold;
53
+ initPromise = null;
54
+ constructor(opts) {
55
+ const ns = opts.namespace ?? 'default';
56
+ if (!VALID_NAMESPACE.test(ns)) {
57
+ throw new Error(`Invalid namespace "${ns}": must start with a letter and contain only alphanumeric characters or underscores`);
58
+ }
59
+ this.backend = opts.backend;
60
+ this.namespace = ns;
61
+ this.tableName = `${ns}_files`;
62
+ this.indexName = `${ns}_files_parent`;
63
+ this.blobs = opts.blobs;
64
+ this.threshold = opts.inlineThreshold ?? DEFAULT_INLINE_THRESHOLD;
65
+ }
66
+ async init() {
67
+ await this.ensureInit();
68
+ }
69
+ async ensureInit() {
70
+ if (!this.initPromise) {
71
+ this.initPromise = this.doInit();
72
+ }
73
+ await this.initPromise;
74
+ }
75
+ async doInit() {
76
+ const T = this.tableName;
77
+ const I = this.indexName;
78
+ await this.backend.run(`
79
+ CREATE TABLE IF NOT EXISTS ${T} (
80
+ path TEXT PRIMARY KEY,
81
+ parent_path TEXT NOT NULL,
82
+ name TEXT NOT NULL,
83
+ type TEXT NOT NULL CHECK(type IN ('file','directory','symlink')),
84
+ mime_type TEXT NOT NULL DEFAULT 'text/plain',
85
+ size INTEGER NOT NULL DEFAULT 0,
86
+ storage_backend TEXT NOT NULL DEFAULT 'inline' CHECK(storage_backend IN ('inline','blob')),
87
+ blob_key TEXT,
88
+ target TEXT,
89
+ content_encoding TEXT NOT NULL DEFAULT 'utf8',
90
+ content TEXT,
91
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
92
+ modified_at INTEGER NOT NULL DEFAULT (unixepoch())
93
+ )
94
+ `);
95
+ await this.backend.run(`CREATE INDEX IF NOT EXISTS ${I} ON ${T}(parent_path)`);
96
+ const hasRoot = (await this.backend.query(`SELECT COUNT(*) AS cnt FROM ${T} WHERE path = '/'`))[0]?.cnt ?? 0;
97
+ if (hasRoot === 0) {
98
+ const now = Math.floor(Date.now() / 1000);
99
+ await this.backend.run(`INSERT INTO ${T}
100
+ (path, parent_path, name, type, size, created_at, modified_at)
101
+ VALUES ('/', '', '', 'directory', 0, ?, ?)`, now, now);
102
+ }
103
+ }
104
+ missing(op, path) {
105
+ return new Error(`ENOENT: no such file or directory, ${op} '${path}'`);
106
+ }
107
+ blobKey(filePath) {
108
+ return `${this.namespace}:${filePath}`;
109
+ }
110
+ async getRow(path) {
111
+ const T = this.tableName;
112
+ const rows = await this.backend.query(`SELECT path, parent_path, name, type, mime_type, size,
113
+ storage_backend, blob_key, target, content_encoding, content,
114
+ created_at, modified_at
115
+ FROM ${T} WHERE path = ?`, path);
116
+ return rows[0] ?? null;
117
+ }
118
+ async readBytesFromRow(row) {
119
+ if (row.storage_backend === 'blob' && row.blob_key) {
120
+ if (!this.blobs) {
121
+ throw new Error(`File ${row.path} is stored in blob but no BlobStore was provided`);
122
+ }
123
+ const data = await this.blobs.get(row.blob_key);
124
+ return data ?? new Uint8Array(0);
125
+ }
126
+ if (row.content_encoding === 'base64' && row.content) {
127
+ return base64ToBytes(row.content);
128
+ }
129
+ return TEXT_ENCODER.encode(row.content ?? '');
130
+ }
131
+ async deleteBlobIfNeeded(row) {
132
+ if (row.storage_backend === 'blob' &&
133
+ row.blob_key &&
134
+ this.blobs) {
135
+ await this.blobs.delete(row.blob_key);
136
+ }
137
+ }
138
+ async insertDirectory(path) {
139
+ const T = this.tableName;
140
+ const parent = dirname(path);
141
+ const name = basename(path);
142
+ const now = Math.floor(Date.now() / 1000);
143
+ await this.backend.run(`INSERT INTO ${T}
144
+ (path, parent_path, name, type, size, created_at, modified_at)
145
+ VALUES (?, ?, ?, 'directory', 0, ?, ?)`, path, parent, name, now, now);
146
+ }
147
+ async scaffoldForPath(normalized) {
148
+ const segs = split(normalized);
149
+ if (segs.length <= 1)
150
+ return;
151
+ let current = '/';
152
+ for (let i = 0; i < segs.length - 1; i++) {
153
+ const childPath = current === '/' ? `/${segs[i]}` : `${current}/${segs[i]}`;
154
+ const row = await this.getRow(childPath);
155
+ if (row) {
156
+ if (row.type === 'directory') {
157
+ current = childPath;
158
+ continue;
159
+ }
160
+ await this.deleteBlobIfNeeded(row);
161
+ const T = this.tableName;
162
+ await this.backend.run(`DELETE FROM ${T} WHERE path = ?`, childPath);
163
+ await this.insertDirectory(childPath);
164
+ current = childPath;
165
+ }
166
+ else {
167
+ await this.insertDirectory(childPath);
168
+ current = childPath;
169
+ }
170
+ }
171
+ }
172
+ async locate(rawPath, followLast, op) {
173
+ const norm = normalizePath(rawPath);
174
+ if (norm === '/')
175
+ return null;
176
+ const pending = [...split(norm)];
177
+ const trail = [];
178
+ let budget = MAX_SYMLINK_DEPTH;
179
+ while (pending.length > 0) {
180
+ const seg = pending.shift();
181
+ const currentPath = trail.length === 0 ? `/${seg}` : `/${trail.join('/')}/${seg}`;
182
+ const row = await this.getRow(currentPath);
183
+ if (!row)
184
+ return null;
185
+ const last = pending.length === 0;
186
+ if (row.type === 'symlink' && (!last || followLast)) {
187
+ if (--budget < 0) {
188
+ throw new Error(`ELOOP: too many levels of symbolic links, ${op} '${rawPath}'`);
189
+ }
190
+ const base = trail.length > 0 ? '/' + trail.join('/') : '';
191
+ const abs = row.target.startsWith('/')
192
+ ? normalizePath(row.target)
193
+ : normalizePath(base === '/' ? `/${row.target}` : `${base}/${row.target}`);
194
+ pending.unshift(...split(abs));
195
+ trail.length = 0;
196
+ continue;
197
+ }
198
+ if (last)
199
+ return row;
200
+ if (row.type !== 'directory')
201
+ return null;
202
+ trail.push(seg);
203
+ }
204
+ return null;
205
+ }
206
+ async canonicalize(rawPath) {
207
+ const norm = normalizePath(rawPath);
208
+ if (norm === '/')
209
+ return '/';
210
+ const pending = [...split(norm)];
211
+ const resolved = [];
212
+ let budget = MAX_SYMLINK_DEPTH;
213
+ while (pending.length > 0) {
214
+ const seg = pending.shift();
215
+ const currentPath = resolved.length === 0
216
+ ? `/${seg}`
217
+ : `/${resolved.join('/')}/${seg}`;
218
+ const row = await this.getRow(currentPath);
219
+ if (!row)
220
+ return null;
221
+ if (row.type === 'symlink') {
222
+ if (--budget < 0) {
223
+ throw new Error(`ELOOP: too many levels of symbolic links, realpath '${rawPath}'`);
224
+ }
225
+ const base = resolved.length > 0 ? '/' + resolved.join('/') : '';
226
+ const abs = row.target.startsWith('/')
227
+ ? normalizePath(row.target)
228
+ : normalizePath(base === '/' ? `/${row.target}` : `${base}/${row.target}`);
229
+ pending.unshift(...split(abs));
230
+ resolved.length = 0;
231
+ continue;
232
+ }
233
+ resolved.push(seg);
234
+ if (row.type !== 'directory' && pending.length > 0)
235
+ return null;
236
+ }
237
+ return '/' + resolved.join('/');
238
+ }
239
+ async readFile(path) {
240
+ return fromBuffer(await this.readFileBytes(path));
241
+ }
242
+ async readFileBytes(path) {
243
+ await this.ensureInit();
244
+ validatePath(path, 'open');
245
+ if (normalizePath(path) === '/') {
246
+ throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
247
+ }
248
+ const row = await this.locate(path, true, 'open');
249
+ if (!row)
250
+ throw this.missing('open', path);
251
+ if (row.type === 'directory' || row.type === 'symlink') {
252
+ throw new Error(`EISDIR: illegal operation on a directory, read '${path}'`);
253
+ }
254
+ return this.readBytesFromRow(row);
255
+ }
256
+ async writeFile(path, content) {
257
+ await this.ensureInit();
258
+ validatePath(path, 'write');
259
+ const norm = normalizePath(path);
260
+ if (norm === '/') {
261
+ throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
262
+ }
263
+ await this.scaffoldForPath(norm);
264
+ const bytes = TEXT_ENCODER.encode(content);
265
+ const size = bytes.byteLength;
266
+ const parent = dirname(norm);
267
+ const name = basename(norm);
268
+ const now = Math.floor(Date.now() / 1000);
269
+ const T = this.tableName;
270
+ const existing = await this.getRow(norm);
271
+ if (existing) {
272
+ await this.deleteBlobIfNeeded(existing);
273
+ }
274
+ if (size >= this.threshold && this.blobs) {
275
+ const key = this.blobKey(norm);
276
+ await this.blobs.put(key, bytes);
277
+ await this.backend.run(`INSERT INTO ${T}
278
+ (path, parent_path, name, type, mime_type, size,
279
+ storage_backend, blob_key, content_encoding, content, created_at, modified_at)
280
+ VALUES (?, ?, ?, 'file', 'text/plain', ?, 'blob', ?, 'utf8', NULL, ?, ?)
281
+ ON CONFLICT(path) DO UPDATE SET
282
+ parent_path = excluded.parent_path,
283
+ name = excluded.name,
284
+ type = 'file',
285
+ mime_type = excluded.mime_type,
286
+ size = excluded.size,
287
+ storage_backend = 'blob',
288
+ blob_key = excluded.blob_key,
289
+ content_encoding = 'utf8',
290
+ content = NULL,
291
+ modified_at = excluded.modified_at`, norm, parent, name, size, key, now, now);
292
+ return;
293
+ }
294
+ await this.backend.run(`INSERT INTO ${T}
295
+ (path, parent_path, name, type, mime_type, size,
296
+ storage_backend, blob_key, content_encoding, content, created_at, modified_at)
297
+ VALUES (?, ?, ?, 'file', 'text/plain', ?, 'inline', NULL, 'utf8', ?, ?, ?)
298
+ ON CONFLICT(path) DO UPDATE SET
299
+ parent_path = excluded.parent_path,
300
+ name = excluded.name,
301
+ type = 'file',
302
+ mime_type = excluded.mime_type,
303
+ size = excluded.size,
304
+ storage_backend = 'inline',
305
+ blob_key = NULL,
306
+ content_encoding = 'utf8',
307
+ content = excluded.content,
308
+ modified_at = excluded.modified_at`, norm, parent, name, size, content, now, now);
309
+ }
310
+ async writeFileBytes(path, content) {
311
+ await this.ensureInit();
312
+ validatePath(path, 'write');
313
+ const norm = normalizePath(path);
314
+ if (norm === '/') {
315
+ throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
316
+ }
317
+ await this.scaffoldForPath(norm);
318
+ const parent = dirname(norm);
319
+ const name = basename(norm);
320
+ const size = content.byteLength;
321
+ const now = Math.floor(Date.now() / 1000);
322
+ const T = this.tableName;
323
+ const existing = await this.getRow(norm);
324
+ if (existing) {
325
+ await this.deleteBlobIfNeeded(existing);
326
+ }
327
+ if (size >= this.threshold && this.blobs) {
328
+ const key = this.blobKey(norm);
329
+ await this.blobs.put(key, content);
330
+ await this.backend.run(`INSERT INTO ${T}
331
+ (path, parent_path, name, type, mime_type, size,
332
+ storage_backend, blob_key, content_encoding, content, created_at, modified_at)
333
+ VALUES (?, ?, ?, 'file', 'application/octet-stream', ?, 'blob', ?, 'base64', NULL, ?, ?)
334
+ ON CONFLICT(path) DO UPDATE SET
335
+ parent_path = excluded.parent_path,
336
+ name = excluded.name,
337
+ type = 'file',
338
+ mime_type = excluded.mime_type,
339
+ size = excluded.size,
340
+ storage_backend = 'blob',
341
+ blob_key = excluded.blob_key,
342
+ content_encoding = 'base64',
343
+ content = NULL,
344
+ modified_at = excluded.modified_at`, norm, parent, name, size, key, now, now);
345
+ return;
346
+ }
347
+ const b64 = bytesToBase64(content);
348
+ await this.backend.run(`INSERT INTO ${T}
349
+ (path, parent_path, name, type, mime_type, size,
350
+ storage_backend, blob_key, content_encoding, content, created_at, modified_at)
351
+ VALUES (?, ?, ?, 'file', 'application/octet-stream', ?, 'inline', NULL, 'base64', ?, ?, ?)
352
+ ON CONFLICT(path) DO UPDATE SET
353
+ parent_path = excluded.parent_path,
354
+ name = excluded.name,
355
+ type = 'file',
356
+ mime_type = excluded.mime_type,
357
+ size = excluded.size,
358
+ storage_backend = 'inline',
359
+ blob_key = NULL,
360
+ content_encoding = 'base64',
361
+ content = excluded.content,
362
+ modified_at = excluded.modified_at`, norm, parent, name, size, b64, now, now);
363
+ }
364
+ async appendFile(path, content) {
365
+ await this.ensureInit();
366
+ validatePath(path, 'append');
367
+ const extra = typeof content === 'string' ? TEXT_ENCODER.encode(content) : content;
368
+ const row = await this.locate(path, true, 'append');
369
+ if (row?.type === 'directory') {
370
+ throw new Error(`EISDIR: illegal operation on a directory, write '${path}'`);
371
+ }
372
+ if (!row) {
373
+ await this.writeFileBytes(path, extra);
374
+ return;
375
+ }
376
+ if (row.type === 'symlink') {
377
+ await this.writeFileBytes(path, extra);
378
+ return;
379
+ }
380
+ const existing = await this.readBytesFromRow(row);
381
+ const merged = new Uint8Array(existing.length + extra.length);
382
+ merged.set(existing);
383
+ merged.set(extra, existing.length);
384
+ await this.writeFileBytes(row.path, merged);
385
+ }
386
+ async exists(path) {
387
+ await this.ensureInit();
388
+ if (path.includes('\0'))
389
+ return false;
390
+ try {
391
+ if (normalizePath(path) === '/')
392
+ return true;
393
+ return (await this.locate(path, true, 'access')) !== null;
394
+ }
395
+ catch {
396
+ return false;
397
+ }
398
+ }
399
+ async stat(path) {
400
+ await this.ensureInit();
401
+ validatePath(path, 'stat');
402
+ if (normalizePath(path) === '/') {
403
+ const root = await this.getRow('/');
404
+ return {
405
+ type: 'directory',
406
+ size: 0,
407
+ mtime: root ? rowMtime(root) : new Date(),
408
+ mode: DEFAULT_DIR_MODE,
409
+ };
410
+ }
411
+ const row = await this.locate(path, true, 'stat');
412
+ if (!row)
413
+ throw this.missing('stat', path);
414
+ return rowToStat(row);
415
+ }
416
+ async lstat(path) {
417
+ await this.ensureInit();
418
+ validatePath(path, 'lstat');
419
+ if (normalizePath(path) === '/') {
420
+ const root = await this.getRow('/');
421
+ return {
422
+ type: 'directory',
423
+ size: 0,
424
+ mtime: root ? rowMtime(root) : new Date(),
425
+ mode: DEFAULT_DIR_MODE,
426
+ };
427
+ }
428
+ const row = await this.locate(path, false, 'lstat');
429
+ if (!row)
430
+ throw this.missing('lstat', path);
431
+ if (row.type === 'symlink') {
432
+ return {
433
+ type: 'symlink',
434
+ size: row.target?.length ?? 0,
435
+ mtime: rowMtime(row),
436
+ mode: SYMLINK_MODE,
437
+ };
438
+ }
439
+ return rowToStat(row);
440
+ }
441
+ async mkdir(path, options) {
442
+ await this.ensureInit();
443
+ validatePath(path, 'mkdir');
444
+ const norm = normalizePath(path);
445
+ if (norm === '/') {
446
+ if (!options?.recursive) {
447
+ throw new Error(`EEXIST: directory already exists, mkdir '${path}'`);
448
+ }
449
+ return;
450
+ }
451
+ const existing = await this.getRow(norm);
452
+ if (existing) {
453
+ if (existing.type === 'directory') {
454
+ if (!options?.recursive) {
455
+ throw new Error(`EEXIST: directory already exists, mkdir '${path}'`);
456
+ }
457
+ return;
458
+ }
459
+ throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
460
+ }
461
+ const segs = split(norm);
462
+ let dirPath = '/';
463
+ for (let i = 0; i < segs.length; i++) {
464
+ const last = i === segs.length - 1;
465
+ const childPath = dirPath === '/' ? `/${segs[i]}` : `${dirPath}/${segs[i]}`;
466
+ const child = await this.getRow(childPath);
467
+ if (child) {
468
+ if (child.type === 'directory') {
469
+ if (last) {
470
+ if (!options?.recursive) {
471
+ throw new Error(`EEXIST: directory already exists, mkdir '${path}'`);
472
+ }
473
+ return;
474
+ }
475
+ dirPath = childPath;
476
+ }
477
+ else if (last) {
478
+ throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
479
+ }
480
+ else if (options?.recursive) {
481
+ await this.deleteBlobIfNeeded(child);
482
+ const T = this.tableName;
483
+ await this.backend.run(`DELETE FROM ${T} WHERE path = ?`, childPath);
484
+ await this.insertDirectory(childPath);
485
+ dirPath = childPath;
486
+ }
487
+ else {
488
+ throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);
489
+ }
490
+ }
491
+ else if (last) {
492
+ await this.insertDirectory(childPath);
493
+ }
494
+ else if (options?.recursive) {
495
+ await this.insertDirectory(childPath);
496
+ dirPath = childPath;
497
+ }
498
+ else {
499
+ throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);
500
+ }
501
+ }
502
+ }
503
+ async readdir(path) {
504
+ return (await this.readdirWithFileTypes(path)).map((d) => d.name);
505
+ }
506
+ async readdirWithFileTypes(path) {
507
+ await this.ensureInit();
508
+ validatePath(path, 'scandir');
509
+ const norm = normalizePath(path);
510
+ const row = await this.locate(path, true, 'scandir');
511
+ if (!row)
512
+ throw this.missing('scandir', path);
513
+ if (row.type !== 'directory') {
514
+ throw new Error(`ENOTDIR: not a directory, scandir '${path}'`);
515
+ }
516
+ const T = this.tableName;
517
+ const rows = await this.backend.query(`SELECT name, type FROM ${T} WHERE parent_path = ? ORDER BY name`, norm);
518
+ return rows.map((r) => ({
519
+ name: r.name,
520
+ type: r.type,
521
+ }));
522
+ }
523
+ async rm(path, options) {
524
+ await this.ensureInit();
525
+ validatePath(path, 'rm');
526
+ const norm = normalizePath(path);
527
+ if (norm === '/') {
528
+ if (options?.force)
529
+ return;
530
+ throw new Error(`EPERM: cannot remove root, rm '${path}'`);
531
+ }
532
+ const row = await this.getRow(norm);
533
+ if (!row) {
534
+ if (options?.force)
535
+ return;
536
+ throw this.missing('rm', path);
537
+ }
538
+ if (row.type === 'directory') {
539
+ const T = this.tableName;
540
+ const children = await this.backend.query(`SELECT COUNT(*) AS cnt FROM ${T} WHERE parent_path = ?`, norm);
541
+ if ((children[0]?.cnt ?? 0) > 0) {
542
+ if (!options?.recursive) {
543
+ throw new Error(`ENOTEMPTY: directory not empty, rm '${path}'`);
544
+ }
545
+ await this.deleteDescendants(norm);
546
+ }
547
+ }
548
+ else {
549
+ await this.deleteBlobIfNeeded(row);
550
+ }
551
+ const T = this.tableName;
552
+ await this.backend.run(`DELETE FROM ${T} WHERE path = ?`, norm);
553
+ }
554
+ async deleteDescendants(dirPath) {
555
+ const T = this.tableName;
556
+ const pattern = `${dirPath}/%`;
557
+ const blobRows = await this.backend.query(`SELECT blob_key FROM ${T}
558
+ WHERE path LIKE ?
559
+ AND storage_backend = 'blob'
560
+ AND blob_key IS NOT NULL`, pattern);
561
+ if (this.blobs) {
562
+ for (const r of blobRows) {
563
+ await this.blobs.delete(r.blob_key);
564
+ }
565
+ }
566
+ await this.backend.run(`DELETE FROM ${T} WHERE path LIKE ?`, pattern);
567
+ }
568
+ async cp(src, dest, options) {
569
+ await this.ensureInit();
570
+ validatePath(src, 'cp');
571
+ validatePath(dest, 'cp');
572
+ const srcNorm = normalizePath(src);
573
+ const destNorm = normalizePath(dest);
574
+ const srcRow = await this.locate(src, false, 'cp');
575
+ if (!srcRow)
576
+ throw this.missing('cp', src);
577
+ if (srcRow.type === 'symlink') {
578
+ await this.symlink(srcRow.target, dest);
579
+ return;
580
+ }
581
+ if (srcRow.type === 'directory') {
582
+ if (!options?.recursive) {
583
+ throw new Error(`EISDIR: is a directory, cp '${src}'`);
584
+ }
585
+ await this.mkdir(destNorm, { recursive: true });
586
+ const children = await this.readdirWithFileTypes(srcNorm);
587
+ for (const child of children) {
588
+ const childSrc = srcNorm === '/' ? `/${child.name}` : `${srcNorm}/${child.name}`;
589
+ const childDest = destNorm === '/' ? `/${child.name}` : `${destNorm}/${child.name}`;
590
+ await this.cp(childSrc, childDest, options);
591
+ }
592
+ return;
593
+ }
594
+ const bytes = await this.readFileBytes(srcNorm);
595
+ await this.writeFileBytes(destNorm, bytes);
596
+ }
597
+ async mv(src, dest) {
598
+ await this.ensureInit();
599
+ validatePath(src, 'mv');
600
+ validatePath(dest, 'mv');
601
+ const srcNorm = normalizePath(src);
602
+ const destNorm = normalizePath(dest);
603
+ const srcRow = await this.locate(src, false, 'mv');
604
+ if (!srcRow)
605
+ throw this.missing('mv', src);
606
+ if (srcRow.type === 'directory') {
607
+ await this.cp(src, dest, { recursive: true });
608
+ await this.rm(src, { recursive: true });
609
+ return;
610
+ }
611
+ const destParent = dirname(destNorm);
612
+ const destName = basename(destNorm);
613
+ await this.scaffoldForPath(destNorm);
614
+ const existingDest = await this.getRow(destNorm);
615
+ if (existingDest) {
616
+ await this.deleteBlobIfNeeded(existingDest);
617
+ const T = this.tableName;
618
+ await this.backend.run(`DELETE FROM ${T} WHERE path = ?`, destNorm);
619
+ }
620
+ const now = Math.floor(Date.now() / 1000);
621
+ const T = this.tableName;
622
+ if (srcRow.type === 'file' &&
623
+ srcRow.storage_backend === 'blob' &&
624
+ srcRow.blob_key) {
625
+ await this.backend.run(`UPDATE ${T} SET
626
+ path = ?,
627
+ parent_path = ?,
628
+ name = ?,
629
+ modified_at = ?
630
+ WHERE path = ?`, destNorm, destParent, destName, now, srcNorm);
631
+ return;
632
+ }
633
+ await this.backend.run(`UPDATE ${T} SET
634
+ path = ?,
635
+ parent_path = ?,
636
+ name = ?,
637
+ modified_at = ?
638
+ WHERE path = ?`, destNorm, destParent, destName, now, srcNorm);
639
+ }
640
+ async symlink(target, linkPath) {
641
+ await this.ensureInit();
642
+ validatePath(linkPath, 'symlink');
643
+ const norm = normalizePath(linkPath);
644
+ const segs = split(norm);
645
+ if (segs.length === 0) {
646
+ throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
647
+ }
648
+ await this.scaffoldForPath(norm);
649
+ const existing = await this.getRow(norm);
650
+ if (existing) {
651
+ throw new Error(`EEXIST: file already exists, symlink '${linkPath}'`);
652
+ }
653
+ const parent = dirname(norm);
654
+ const name = basename(norm);
655
+ const now = Math.floor(Date.now() / 1000);
656
+ const T = this.tableName;
657
+ await this.backend.run(`INSERT INTO ${T}
658
+ (path, parent_path, name, type, target, size, created_at, modified_at)
659
+ VALUES (?, ?, ?, 'symlink', ?, 0, ?, ?)`, norm, parent, name, target, now, now);
660
+ }
661
+ async readlink(path) {
662
+ await this.ensureInit();
663
+ validatePath(path, 'readlink');
664
+ const row = await this.locate(path, false, 'readlink');
665
+ if (!row)
666
+ throw this.missing('readlink', path);
667
+ if (row.type !== 'symlink' || !row.target) {
668
+ throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
669
+ }
670
+ return row.target;
671
+ }
672
+ async realpath(path) {
673
+ await this.ensureInit();
674
+ validatePath(path, 'realpath');
675
+ const canon = await this.canonicalize(path);
676
+ if (canon === null)
677
+ throw this.missing('realpath', path);
678
+ return canon;
679
+ }
680
+ resolvePath(base, path) {
681
+ return resolvePathUtil(base, path);
682
+ }
683
+ async glob(pattern) {
684
+ await this.ensureInit();
685
+ const re = createGlobMatcher(pattern);
686
+ const T = this.tableName;
687
+ const rows = await this.backend.query(`SELECT path FROM ${T}`);
688
+ const hits = rows.map((r) => r.path).filter((p) => re.test(p));
689
+ return sortPaths(hits);
690
+ }
691
+ }
@@ -0,0 +1,10 @@
1
+ export type SqlParam = string | number | boolean | null;
2
+ export interface SqlBackend {
3
+ query<T = Record<string, SqlParam>>(sql: string, ...params: SqlParam[]): T[] | Promise<T[]>;
4
+ run(sql: string, ...params: SqlParam[]): void | Promise<void>;
5
+ }
6
+ export interface BlobStore {
7
+ get(key: string): Promise<Uint8Array | null>;
8
+ put(key: string, data: Uint8Array): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ }