agentfs-sdk 0.4.0-pre.1 → 0.4.0-pre.3
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/errors.d.ts +20 -0
- package/dist/errors.js +13 -0
- package/dist/filesystem.d.ts +40 -1
- package/dist/filesystem.js +476 -41
- package/dist/guards.d.ts +18 -0
- package/dist/guards.js +145 -0
- package/package.json +1 -1
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSIX-style error codes for filesystem operations.
|
|
3
|
+
*/
|
|
4
|
+
export type FsErrorCode = 'ENOENT' | 'EEXIST' | 'EISDIR' | 'ENOTDIR' | 'ENOTEMPTY' | 'EPERM' | 'EINVAL' | 'ENOSYS';
|
|
5
|
+
/**
|
|
6
|
+
* Filesystem syscall names for error reporting.
|
|
7
|
+
* rm, scandir and copyFile are not actual syscall but used for convenience
|
|
8
|
+
*/
|
|
9
|
+
export type FsSyscall = 'open' | 'stat' | 'mkdir' | 'rmdir' | 'rm' | 'unlink' | 'rename' | 'scandir' | 'copyfile' | 'access';
|
|
10
|
+
export interface ErrnoException extends Error {
|
|
11
|
+
code?: FsErrorCode;
|
|
12
|
+
syscall?: FsSyscall;
|
|
13
|
+
path?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createFsError(params: {
|
|
16
|
+
code: FsErrorCode;
|
|
17
|
+
syscall: FsSyscall;
|
|
18
|
+
path?: string;
|
|
19
|
+
message?: string;
|
|
20
|
+
}): ErrnoException;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createFsError(params) {
|
|
2
|
+
const { code, syscall, path, message } = params;
|
|
3
|
+
const base = message ?? code;
|
|
4
|
+
const suffix = path !== undefined
|
|
5
|
+
? ` '${path}'`
|
|
6
|
+
: '';
|
|
7
|
+
const err = new Error(`${code}: ${base}, ${syscall}${suffix}`);
|
|
8
|
+
err.code = code;
|
|
9
|
+
err.syscall = syscall;
|
|
10
|
+
if (path !== undefined)
|
|
11
|
+
err.path = path;
|
|
12
|
+
return err;
|
|
13
|
+
}
|
package/dist/filesystem.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { DatabasePromise } from '@tursodatabase/database-common';
|
|
2
|
+
export declare const S_IFMT = 61440;
|
|
3
|
+
export declare const S_IFREG = 32768;
|
|
4
|
+
export declare const S_IFDIR = 16384;
|
|
5
|
+
export declare const S_IFLNK = 40960;
|
|
2
6
|
export interface Stats {
|
|
3
7
|
ino: number;
|
|
4
8
|
mode: number;
|
|
@@ -40,6 +44,7 @@ export declare class Filesystem {
|
|
|
40
44
|
* Split path into components
|
|
41
45
|
*/
|
|
42
46
|
private splitPath;
|
|
47
|
+
private resolvePathOrThrow;
|
|
43
48
|
/**
|
|
44
49
|
* Resolve a path to an inode number
|
|
45
50
|
*/
|
|
@@ -64,12 +69,46 @@ export declare class Filesystem {
|
|
|
64
69
|
* Get link count for an inode
|
|
65
70
|
*/
|
|
66
71
|
private getLinkCount;
|
|
67
|
-
|
|
72
|
+
private getInodeMode;
|
|
73
|
+
writeFile(path: string, content: string | Buffer, options?: BufferEncoding | {
|
|
74
|
+
encoding?: BufferEncoding;
|
|
75
|
+
}): Promise<void>;
|
|
68
76
|
private updateFileContent;
|
|
69
77
|
readFile(path: string, options?: BufferEncoding | {
|
|
70
78
|
encoding?: BufferEncoding;
|
|
71
79
|
}): Promise<Buffer | string>;
|
|
72
80
|
readdir(path: string): Promise<string[]>;
|
|
81
|
+
unlink(path: string): Promise<void>;
|
|
73
82
|
deleteFile(path: string): Promise<void>;
|
|
74
83
|
stat(path: string): Promise<Stats>;
|
|
84
|
+
/**
|
|
85
|
+
* Create a directory (non-recursive, no options yet)
|
|
86
|
+
*/
|
|
87
|
+
mkdir(path: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Remove a file or directory
|
|
90
|
+
*/
|
|
91
|
+
rm(path: string, options?: {
|
|
92
|
+
force?: boolean;
|
|
93
|
+
recursive?: boolean;
|
|
94
|
+
}): Promise<void>;
|
|
95
|
+
private rmDirContentsRecursive;
|
|
96
|
+
private removeDentryAndMaybeInode;
|
|
97
|
+
/**
|
|
98
|
+
* Remove an empty directory
|
|
99
|
+
*/
|
|
100
|
+
rmdir(path: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Rename (move) a file or directory
|
|
103
|
+
*/
|
|
104
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Copy a file. Overwrites destination if it exists.
|
|
107
|
+
*/
|
|
108
|
+
copyFile(src: string, dest: string): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Test a user's permissions for the file or directory specified by path.
|
|
111
|
+
* Currently supports existence checks only (F_OK semantics).
|
|
112
|
+
*/
|
|
113
|
+
access(path: string): Promise<void>;
|
|
75
114
|
}
|
package/dist/filesystem.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { createFsError } from './errors.js';
|
|
2
|
+
import { assertInodeIsDirectory, assertNotRoot, assertNotSymlinkMode, assertReadableExistingInode, assertReaddirTargetInode, assertUnlinkTargetInode, assertWritableExistingInode, getInodeModeOrThrow, normalizeRmOptions, throwENOENTUnlessForce, } from './guards.js';
|
|
1
3
|
// File types for mode field
|
|
2
|
-
const S_IFMT = 0o170000; // File type mask
|
|
3
|
-
const S_IFREG = 0o100000; // Regular file
|
|
4
|
-
const S_IFDIR = 0o040000; // Directory
|
|
5
|
-
const S_IFLNK = 0o120000; // Symbolic link
|
|
4
|
+
export const S_IFMT = 0o170000; // File type mask
|
|
5
|
+
export const S_IFREG = 0o100000; // Regular file
|
|
6
|
+
export const S_IFDIR = 0o040000; // Directory
|
|
7
|
+
export const S_IFLNK = 0o120000; // Symbolic link
|
|
6
8
|
// Default permissions
|
|
7
9
|
const DEFAULT_FILE_MODE = S_IFREG | 0o644; // Regular file, rw-r--r--
|
|
8
10
|
const DEFAULT_DIR_MODE = S_IFDIR | 0o755; // Directory, rwxr-xr-x
|
|
@@ -43,6 +45,7 @@ export class Filesystem {
|
|
|
43
45
|
CREATE TABLE IF NOT EXISTS fs_inode (
|
|
44
46
|
ino INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
47
|
mode INTEGER NOT NULL,
|
|
48
|
+
nlink INTEGER NOT NULL DEFAULT 0,
|
|
46
49
|
uid INTEGER NOT NULL DEFAULT 0,
|
|
47
50
|
gid INTEGER NOT NULL DEFAULT 0,
|
|
48
51
|
size INTEGER NOT NULL DEFAULT 0,
|
|
@@ -109,8 +112,8 @@ export class Filesystem {
|
|
|
109
112
|
if (!root) {
|
|
110
113
|
const now = Math.floor(Date.now() / 1000);
|
|
111
114
|
const insertStmt = this.db.prepare(`
|
|
112
|
-
INSERT INTO fs_inode (ino, mode, uid, gid, size, atime, mtime, ctime)
|
|
113
|
-
VALUES (?, ?, 0, 0, 0, ?, ?, ?)
|
|
115
|
+
INSERT INTO fs_inode (ino, mode, nlink, uid, gid, size, atime, mtime, ctime)
|
|
116
|
+
VALUES (?, ?, 1, 0, 0, 0, ?, ?, ?)
|
|
114
117
|
`);
|
|
115
118
|
await insertStmt.run(this.rootIno, DEFAULT_DIR_MODE, now, now, now);
|
|
116
119
|
}
|
|
@@ -134,6 +137,19 @@ export class Filesystem {
|
|
|
134
137
|
return [];
|
|
135
138
|
return normalized.split('/').filter(p => p);
|
|
136
139
|
}
|
|
140
|
+
async resolvePathOrThrow(path, syscall) {
|
|
141
|
+
const normalizedPath = this.normalizePath(path);
|
|
142
|
+
const ino = await this.resolvePath(normalizedPath);
|
|
143
|
+
if (ino === null) {
|
|
144
|
+
throw createFsError({
|
|
145
|
+
code: 'ENOENT',
|
|
146
|
+
syscall,
|
|
147
|
+
path: normalizedPath,
|
|
148
|
+
message: 'no such file or directory',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { normalizedPath, ino };
|
|
152
|
+
}
|
|
137
153
|
/**
|
|
138
154
|
* Resolve a path to an inode number
|
|
139
155
|
*/
|
|
@@ -198,6 +214,9 @@ export class Filesystem {
|
|
|
198
214
|
VALUES (?, ?, ?)
|
|
199
215
|
`);
|
|
200
216
|
await stmt.run(name, parentIno, ino);
|
|
217
|
+
// Increment link count
|
|
218
|
+
const updateStmt = this.db.prepare('UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?');
|
|
219
|
+
await updateStmt.run(ino);
|
|
201
220
|
}
|
|
202
221
|
/**
|
|
203
222
|
* Ensure parent directories exist
|
|
@@ -207,9 +226,7 @@ export class Filesystem {
|
|
|
207
226
|
// Remove the filename, keep only directory parts
|
|
208
227
|
parts.pop();
|
|
209
228
|
let currentIno = this.rootIno;
|
|
210
|
-
let currentPath = '';
|
|
211
229
|
for (const name of parts) {
|
|
212
|
-
currentPath += '/' + name;
|
|
213
230
|
// Check if this directory exists
|
|
214
231
|
const stmt = this.db.prepare(`
|
|
215
232
|
SELECT ino FROM fs_dentry
|
|
@@ -223,6 +240,8 @@ export class Filesystem {
|
|
|
223
240
|
currentIno = dirIno;
|
|
224
241
|
}
|
|
225
242
|
else {
|
|
243
|
+
// Ensure existing path component is actually a directory
|
|
244
|
+
await assertInodeIsDirectory(this.db, result.ino, 'open', this.normalizePath(path));
|
|
226
245
|
currentIno = result.ino;
|
|
227
246
|
}
|
|
228
247
|
}
|
|
@@ -231,35 +250,54 @@ export class Filesystem {
|
|
|
231
250
|
* Get link count for an inode
|
|
232
251
|
*/
|
|
233
252
|
async getLinkCount(ino) {
|
|
234
|
-
const stmt = this.db.prepare('SELECT
|
|
253
|
+
const stmt = this.db.prepare('SELECT nlink FROM fs_inode WHERE ino = ?');
|
|
235
254
|
const result = await stmt.get(ino);
|
|
236
|
-
return result
|
|
255
|
+
return result?.nlink ?? 0;
|
|
237
256
|
}
|
|
238
|
-
async
|
|
257
|
+
async getInodeMode(ino) {
|
|
258
|
+
const stmt = this.db.prepare('SELECT mode FROM fs_inode WHERE ino = ?');
|
|
259
|
+
const row = await stmt.get(ino);
|
|
260
|
+
return row?.mode ?? null;
|
|
261
|
+
}
|
|
262
|
+
async writeFile(path, content, options) {
|
|
239
263
|
// Ensure parent directories exist
|
|
240
264
|
await this.ensureParentDirs(path);
|
|
241
265
|
// Check if file already exists
|
|
242
266
|
const ino = await this.resolvePath(path);
|
|
267
|
+
const encoding = typeof options === 'string'
|
|
268
|
+
? options
|
|
269
|
+
: options?.encoding;
|
|
270
|
+
const normalizedPath = this.normalizePath(path);
|
|
243
271
|
if (ino !== null) {
|
|
272
|
+
await assertWritableExistingInode(this.db, ino, 'open', normalizedPath);
|
|
244
273
|
// Update existing file
|
|
245
|
-
await this.updateFileContent(ino, content);
|
|
274
|
+
await this.updateFileContent(ino, content, encoding);
|
|
246
275
|
}
|
|
247
276
|
else {
|
|
248
277
|
// Create new file
|
|
249
278
|
const parent = await this.resolveParent(path);
|
|
250
279
|
if (!parent) {
|
|
251
|
-
throw
|
|
280
|
+
throw createFsError({
|
|
281
|
+
code: 'ENOENT',
|
|
282
|
+
syscall: 'open',
|
|
283
|
+
path: normalizedPath,
|
|
284
|
+
message: 'no such file or directory',
|
|
285
|
+
});
|
|
252
286
|
}
|
|
287
|
+
// Ensure parent is a directory
|
|
288
|
+
await assertInodeIsDirectory(this.db, parent.parentIno, 'open', normalizedPath);
|
|
253
289
|
// Create inode
|
|
254
290
|
const fileIno = await this.createInode(DEFAULT_FILE_MODE);
|
|
255
291
|
// Create directory entry
|
|
256
292
|
await this.createDentry(parent.parentIno, parent.name, fileIno);
|
|
257
293
|
// Write content
|
|
258
|
-
await this.updateFileContent(fileIno, content);
|
|
294
|
+
await this.updateFileContent(fileIno, content, encoding);
|
|
259
295
|
}
|
|
260
296
|
}
|
|
261
|
-
async updateFileContent(ino, content) {
|
|
262
|
-
const buffer = typeof content === 'string'
|
|
297
|
+
async updateFileContent(ino, content, encoding) {
|
|
298
|
+
const buffer = typeof content === 'string'
|
|
299
|
+
? this.bufferCtor.from(content, encoding ?? 'utf8')
|
|
300
|
+
: content;
|
|
263
301
|
const now = Math.floor(Date.now() / 1000);
|
|
264
302
|
// Delete existing data chunks
|
|
265
303
|
const deleteStmt = this.db.prepare('DELETE FROM fs_data WHERE ino = ?');
|
|
@@ -290,10 +328,8 @@ export class Filesystem {
|
|
|
290
328
|
const encoding = typeof options === 'string'
|
|
291
329
|
? options
|
|
292
330
|
: options?.encoding;
|
|
293
|
-
const ino = await this.
|
|
294
|
-
|
|
295
|
-
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
296
|
-
}
|
|
331
|
+
const { normalizedPath, ino } = await this.resolvePathOrThrow(path, 'open');
|
|
332
|
+
await assertReadableExistingInode(this.db, ino, 'open', normalizedPath);
|
|
297
333
|
// Get all data chunks
|
|
298
334
|
const stmt = this.db.prepare(`
|
|
299
335
|
SELECT data FROM fs_data
|
|
@@ -320,10 +356,8 @@ export class Filesystem {
|
|
|
320
356
|
return combined;
|
|
321
357
|
}
|
|
322
358
|
async readdir(path) {
|
|
323
|
-
const ino = await this.
|
|
324
|
-
|
|
325
|
-
throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
|
|
326
|
-
}
|
|
359
|
+
const { normalizedPath, ino } = await this.resolvePathOrThrow(path, 'scandir');
|
|
360
|
+
await assertReaddirTargetInode(this.db, ino, normalizedPath);
|
|
327
361
|
// Get all directory entries
|
|
328
362
|
const stmt = this.db.prepare(`
|
|
329
363
|
SELECT name FROM fs_dentry
|
|
@@ -333,21 +367,22 @@ export class Filesystem {
|
|
|
333
367
|
const rows = await stmt.all(ino);
|
|
334
368
|
return rows.map(row => row.name);
|
|
335
369
|
}
|
|
336
|
-
async
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const parent = await this.resolveParent(
|
|
342
|
-
|
|
343
|
-
throw new Error(`Cannot delete root directory`);
|
|
344
|
-
}
|
|
370
|
+
async unlink(path) {
|
|
371
|
+
const normalizedPath = this.normalizePath(path);
|
|
372
|
+
assertNotRoot(normalizedPath, 'unlink');
|
|
373
|
+
const { ino } = await this.resolvePathOrThrow(normalizedPath, 'unlink');
|
|
374
|
+
await assertUnlinkTargetInode(this.db, ino, normalizedPath);
|
|
375
|
+
const parent = (await this.resolveParent(normalizedPath));
|
|
376
|
+
// parent is guaranteed to exist here since normalizedPath !== '/'
|
|
345
377
|
// Delete the directory entry
|
|
346
378
|
const stmt = this.db.prepare(`
|
|
347
379
|
DELETE FROM fs_dentry
|
|
348
380
|
WHERE parent_ino = ? AND name = ?
|
|
349
381
|
`);
|
|
350
382
|
await stmt.run(parent.parentIno, parent.name);
|
|
383
|
+
// Decrement link count
|
|
384
|
+
const decrementStmt = this.db.prepare('UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?');
|
|
385
|
+
await decrementStmt.run(ino);
|
|
351
386
|
// Check if this was the last link to the inode
|
|
352
387
|
const linkCount = await this.getLinkCount(ino);
|
|
353
388
|
if (linkCount === 0) {
|
|
@@ -359,25 +394,30 @@ export class Filesystem {
|
|
|
359
394
|
await deleteDataStmt.run(ino);
|
|
360
395
|
}
|
|
361
396
|
}
|
|
397
|
+
// Backwards-compatible alias
|
|
398
|
+
async deleteFile(path) {
|
|
399
|
+
return await this.unlink(path);
|
|
400
|
+
}
|
|
362
401
|
async stat(path) {
|
|
363
|
-
const ino = await this.
|
|
364
|
-
if (ino === null) {
|
|
365
|
-
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
366
|
-
}
|
|
402
|
+
const { normalizedPath, ino } = await this.resolvePathOrThrow(path, 'stat');
|
|
367
403
|
const stmt = this.db.prepare(`
|
|
368
|
-
SELECT ino, mode, uid, gid, size, atime, mtime, ctime
|
|
404
|
+
SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
|
|
369
405
|
FROM fs_inode
|
|
370
406
|
WHERE ino = ?
|
|
371
407
|
`);
|
|
372
408
|
const row = await stmt.get(ino);
|
|
373
409
|
if (!row) {
|
|
374
|
-
throw
|
|
410
|
+
throw createFsError({
|
|
411
|
+
code: 'ENOENT',
|
|
412
|
+
syscall: 'stat',
|
|
413
|
+
path: normalizedPath,
|
|
414
|
+
message: 'no such file or directory',
|
|
415
|
+
});
|
|
375
416
|
}
|
|
376
|
-
const nlink = await this.getLinkCount(ino);
|
|
377
417
|
return {
|
|
378
418
|
ino: row.ino,
|
|
379
419
|
mode: row.mode,
|
|
380
|
-
nlink: nlink,
|
|
420
|
+
nlink: row.nlink,
|
|
381
421
|
uid: row.uid,
|
|
382
422
|
gid: row.gid,
|
|
383
423
|
size: row.size,
|
|
@@ -389,4 +429,399 @@ export class Filesystem {
|
|
|
389
429
|
isSymbolicLink: () => (row.mode & S_IFMT) === S_IFLNK,
|
|
390
430
|
};
|
|
391
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Create a directory (non-recursive, no options yet)
|
|
434
|
+
*/
|
|
435
|
+
async mkdir(path) {
|
|
436
|
+
const normalizedPath = this.normalizePath(path);
|
|
437
|
+
const existing = await this.resolvePath(normalizedPath);
|
|
438
|
+
if (existing !== null) {
|
|
439
|
+
throw createFsError({
|
|
440
|
+
code: 'EEXIST',
|
|
441
|
+
syscall: 'mkdir',
|
|
442
|
+
path: normalizedPath,
|
|
443
|
+
message: 'file already exists',
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const parent = await this.resolveParent(normalizedPath);
|
|
447
|
+
if (!parent) {
|
|
448
|
+
throw createFsError({
|
|
449
|
+
code: 'ENOENT',
|
|
450
|
+
syscall: 'mkdir',
|
|
451
|
+
path: normalizedPath,
|
|
452
|
+
message: 'no such file or directory',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
await assertInodeIsDirectory(this.db, parent.parentIno, 'mkdir', normalizedPath);
|
|
456
|
+
const dirIno = await this.createInode(DEFAULT_DIR_MODE);
|
|
457
|
+
try {
|
|
458
|
+
await this.createDentry(parent.parentIno, parent.name, dirIno);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
throw createFsError({
|
|
462
|
+
code: 'EEXIST',
|
|
463
|
+
syscall: 'mkdir',
|
|
464
|
+
path: normalizedPath,
|
|
465
|
+
message: 'file already exists',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Remove a file or directory
|
|
471
|
+
*/
|
|
472
|
+
async rm(path, options) {
|
|
473
|
+
const normalizedPath = this.normalizePath(path);
|
|
474
|
+
const { force, recursive } = normalizeRmOptions(options);
|
|
475
|
+
assertNotRoot(normalizedPath, 'rm');
|
|
476
|
+
const ino = await this.resolvePath(normalizedPath);
|
|
477
|
+
if (ino === null) {
|
|
478
|
+
throwENOENTUnlessForce(normalizedPath, 'rm', force);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const mode = await getInodeModeOrThrow(this.db, ino, 'rm', normalizedPath);
|
|
482
|
+
assertNotSymlinkMode(mode, 'rm', normalizedPath);
|
|
483
|
+
const parent = await this.resolveParent(normalizedPath);
|
|
484
|
+
if (!parent) {
|
|
485
|
+
throw createFsError({
|
|
486
|
+
code: 'EPERM',
|
|
487
|
+
syscall: 'rm',
|
|
488
|
+
path: normalizedPath,
|
|
489
|
+
message: 'operation not permitted',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if ((mode & S_IFMT) === S_IFDIR) {
|
|
493
|
+
if (!recursive) {
|
|
494
|
+
throw createFsError({
|
|
495
|
+
code: 'EISDIR',
|
|
496
|
+
syscall: 'rm',
|
|
497
|
+
path: normalizedPath,
|
|
498
|
+
message: 'illegal operation on a directory',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
await this.rmDirContentsRecursive(ino);
|
|
502
|
+
await this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Regular file
|
|
506
|
+
await this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
|
|
507
|
+
}
|
|
508
|
+
async rmDirContentsRecursive(dirIno) {
|
|
509
|
+
const stmt = this.db.prepare(`
|
|
510
|
+
SELECT name, ino FROM fs_dentry
|
|
511
|
+
WHERE parent_ino = ?
|
|
512
|
+
ORDER BY name ASC
|
|
513
|
+
`);
|
|
514
|
+
const children = await stmt.all(dirIno);
|
|
515
|
+
for (const child of children) {
|
|
516
|
+
const mode = await this.getInodeMode(child.ino);
|
|
517
|
+
if (mode === null) {
|
|
518
|
+
// DB inconsistency; treat as already gone
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if ((mode & S_IFMT) === S_IFDIR) {
|
|
522
|
+
await this.rmDirContentsRecursive(child.ino);
|
|
523
|
+
await this.removeDentryAndMaybeInode(dirIno, child.name, child.ino);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Not supported yet (symlinks)
|
|
527
|
+
assertNotSymlinkMode(mode, 'rm', '<symlink>');
|
|
528
|
+
await this.removeDentryAndMaybeInode(dirIno, child.name, child.ino);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async removeDentryAndMaybeInode(parentIno, name, ino) {
|
|
533
|
+
const stmt = this.db.prepare(`
|
|
534
|
+
DELETE FROM fs_dentry
|
|
535
|
+
WHERE parent_ino = ? AND name = ?
|
|
536
|
+
`);
|
|
537
|
+
await stmt.run(parentIno, name);
|
|
538
|
+
// Decrement link count
|
|
539
|
+
const decrementStmt = this.db.prepare('UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?');
|
|
540
|
+
await decrementStmt.run(ino);
|
|
541
|
+
const linkCount = await this.getLinkCount(ino);
|
|
542
|
+
if (linkCount === 0) {
|
|
543
|
+
const deleteInodeStmt = this.db.prepare('DELETE FROM fs_inode WHERE ino = ?');
|
|
544
|
+
await deleteInodeStmt.run(ino);
|
|
545
|
+
const deleteDataStmt = this.db.prepare('DELETE FROM fs_data WHERE ino = ?');
|
|
546
|
+
await deleteDataStmt.run(ino);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Remove an empty directory
|
|
551
|
+
*/
|
|
552
|
+
async rmdir(path) {
|
|
553
|
+
const normalizedPath = this.normalizePath(path);
|
|
554
|
+
assertNotRoot(normalizedPath, 'rmdir');
|
|
555
|
+
const { ino } = await this.resolvePathOrThrow(normalizedPath, 'rmdir');
|
|
556
|
+
const mode = await getInodeModeOrThrow(this.db, ino, 'rmdir', normalizedPath);
|
|
557
|
+
assertNotSymlinkMode(mode, 'rmdir', normalizedPath);
|
|
558
|
+
if ((mode & S_IFMT) !== S_IFDIR) {
|
|
559
|
+
throw createFsError({
|
|
560
|
+
code: 'ENOTDIR',
|
|
561
|
+
syscall: 'rmdir',
|
|
562
|
+
path: normalizedPath,
|
|
563
|
+
message: 'not a directory',
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const stmt = this.db.prepare(`
|
|
567
|
+
SELECT 1 as one FROM fs_dentry
|
|
568
|
+
WHERE parent_ino = ?
|
|
569
|
+
LIMIT 1
|
|
570
|
+
`);
|
|
571
|
+
const child = await stmt.get(ino);
|
|
572
|
+
if (child) {
|
|
573
|
+
throw createFsError({
|
|
574
|
+
code: 'ENOTEMPTY',
|
|
575
|
+
syscall: 'rmdir',
|
|
576
|
+
path: normalizedPath,
|
|
577
|
+
message: 'directory not empty',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const parent = await this.resolveParent(normalizedPath);
|
|
581
|
+
if (!parent) {
|
|
582
|
+
throw createFsError({
|
|
583
|
+
code: 'EPERM',
|
|
584
|
+
syscall: 'rmdir',
|
|
585
|
+
path: normalizedPath,
|
|
586
|
+
message: 'operation not permitted',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
await this.removeDentryAndMaybeInode(parent.parentIno, parent.name, ino);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Rename (move) a file or directory
|
|
593
|
+
*/
|
|
594
|
+
async rename(oldPath, newPath) {
|
|
595
|
+
const oldNormalized = this.normalizePath(oldPath);
|
|
596
|
+
const newNormalized = this.normalizePath(newPath);
|
|
597
|
+
// No-op
|
|
598
|
+
if (oldNormalized === newNormalized)
|
|
599
|
+
return;
|
|
600
|
+
assertNotRoot(oldNormalized, 'rename');
|
|
601
|
+
assertNotRoot(newNormalized, 'rename');
|
|
602
|
+
const oldParent = await this.resolveParent(oldNormalized);
|
|
603
|
+
if (!oldParent) {
|
|
604
|
+
throw createFsError({
|
|
605
|
+
code: 'EPERM',
|
|
606
|
+
syscall: 'rename',
|
|
607
|
+
path: oldNormalized,
|
|
608
|
+
message: 'operation not permitted',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const newParent = await this.resolveParent(newNormalized);
|
|
612
|
+
if (!newParent) {
|
|
613
|
+
throw createFsError({
|
|
614
|
+
code: 'ENOENT',
|
|
615
|
+
syscall: 'rename',
|
|
616
|
+
path: newNormalized,
|
|
617
|
+
message: 'no such file or directory',
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
// Ensure destination parent exists and is a directory
|
|
621
|
+
await assertInodeIsDirectory(this.db, newParent.parentIno, 'rename', newNormalized);
|
|
622
|
+
await this.db.exec('BEGIN');
|
|
623
|
+
try {
|
|
624
|
+
const oldResolved = await this.resolvePathOrThrow(oldNormalized, 'rename');
|
|
625
|
+
const oldIno = oldResolved.ino;
|
|
626
|
+
const oldMode = await getInodeModeOrThrow(this.db, oldIno, 'rename', oldNormalized);
|
|
627
|
+
assertNotSymlinkMode(oldMode, 'rename', oldNormalized);
|
|
628
|
+
const oldIsDir = (oldMode & S_IFMT) === S_IFDIR;
|
|
629
|
+
// Prevent renaming a directory into its own subtree (would create cycles).
|
|
630
|
+
if (oldIsDir && newNormalized.startsWith(oldNormalized + '/')) {
|
|
631
|
+
throw createFsError({
|
|
632
|
+
code: 'EINVAL',
|
|
633
|
+
syscall: 'rename',
|
|
634
|
+
path: newNormalized,
|
|
635
|
+
message: 'invalid argument',
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const newIno = await this.resolvePath(newNormalized);
|
|
639
|
+
if (newIno !== null) {
|
|
640
|
+
const newMode = await getInodeModeOrThrow(this.db, newIno, 'rename', newNormalized);
|
|
641
|
+
assertNotSymlinkMode(newMode, 'rename', newNormalized);
|
|
642
|
+
const newIsDir = (newMode & S_IFMT) === S_IFDIR;
|
|
643
|
+
if (newIsDir && !oldIsDir) {
|
|
644
|
+
throw createFsError({
|
|
645
|
+
code: 'EISDIR',
|
|
646
|
+
syscall: 'rename',
|
|
647
|
+
path: newNormalized,
|
|
648
|
+
message: 'illegal operation on a directory',
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (!newIsDir && oldIsDir) {
|
|
652
|
+
throw createFsError({
|
|
653
|
+
code: 'ENOTDIR',
|
|
654
|
+
syscall: 'rename',
|
|
655
|
+
path: newNormalized,
|
|
656
|
+
message: 'not a directory',
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// If replacing a directory, it must be empty.
|
|
660
|
+
if (newIsDir) {
|
|
661
|
+
const stmt = this.db.prepare(`
|
|
662
|
+
SELECT 1 as one FROM fs_dentry
|
|
663
|
+
WHERE parent_ino = ?
|
|
664
|
+
LIMIT 1
|
|
665
|
+
`);
|
|
666
|
+
const child = await stmt.get(newIno);
|
|
667
|
+
if (child) {
|
|
668
|
+
throw createFsError({
|
|
669
|
+
code: 'ENOTEMPTY',
|
|
670
|
+
syscall: 'rename',
|
|
671
|
+
path: newNormalized,
|
|
672
|
+
message: 'directory not empty',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Remove the destination entry (and inode if this was the last link)
|
|
677
|
+
await this.removeDentryAndMaybeInode(newParent.parentIno, newParent.name, newIno);
|
|
678
|
+
}
|
|
679
|
+
// Move the directory entry
|
|
680
|
+
const stmt = this.db.prepare(`
|
|
681
|
+
UPDATE fs_dentry
|
|
682
|
+
SET parent_ino = ?, name = ?
|
|
683
|
+
WHERE parent_ino = ? AND name = ?
|
|
684
|
+
`);
|
|
685
|
+
await stmt.run(newParent.parentIno, newParent.name, oldParent.parentIno, oldParent.name);
|
|
686
|
+
// Update timestamps
|
|
687
|
+
const now = Math.floor(Date.now() / 1000);
|
|
688
|
+
const updateInodeCtimeStmt = this.db.prepare(`
|
|
689
|
+
UPDATE fs_inode
|
|
690
|
+
SET ctime = ?
|
|
691
|
+
WHERE ino = ?
|
|
692
|
+
`);
|
|
693
|
+
await updateInodeCtimeStmt.run(now, oldIno);
|
|
694
|
+
const updateDirTimesStmt = this.db.prepare(`
|
|
695
|
+
UPDATE fs_inode
|
|
696
|
+
SET mtime = ?, ctime = ?
|
|
697
|
+
WHERE ino = ?
|
|
698
|
+
`);
|
|
699
|
+
await updateDirTimesStmt.run(now, now, oldParent.parentIno);
|
|
700
|
+
if (newParent.parentIno !== oldParent.parentIno) {
|
|
701
|
+
await updateDirTimesStmt.run(now, now, newParent.parentIno);
|
|
702
|
+
}
|
|
703
|
+
await this.db.exec('COMMIT');
|
|
704
|
+
}
|
|
705
|
+
catch (e) {
|
|
706
|
+
await this.db.exec('ROLLBACK');
|
|
707
|
+
throw e;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Copy a file. Overwrites destination if it exists.
|
|
712
|
+
*/
|
|
713
|
+
async copyFile(src, dest) {
|
|
714
|
+
const srcNormalized = this.normalizePath(src);
|
|
715
|
+
const destNormalized = this.normalizePath(dest);
|
|
716
|
+
if (srcNormalized === destNormalized) {
|
|
717
|
+
throw createFsError({
|
|
718
|
+
code: 'EINVAL',
|
|
719
|
+
syscall: 'copyfile',
|
|
720
|
+
path: destNormalized,
|
|
721
|
+
message: 'invalid argument',
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
// Resolve and validate source
|
|
725
|
+
// node uses copyfile as syscall name even though it's not a syscall
|
|
726
|
+
const { ino: srcIno } = await this.resolvePathOrThrow(srcNormalized, 'copyfile');
|
|
727
|
+
await assertReadableExistingInode(this.db, srcIno, 'copyfile', srcNormalized);
|
|
728
|
+
const stmt = this.db.prepare(`
|
|
729
|
+
SELECT mode, uid, gid, size FROM fs_inode WHERE ino = ?
|
|
730
|
+
`);
|
|
731
|
+
const srcRow = await stmt.get(srcIno);
|
|
732
|
+
if (!srcRow) {
|
|
733
|
+
throw createFsError({
|
|
734
|
+
code: 'ENOENT',
|
|
735
|
+
syscall: 'copyfile',
|
|
736
|
+
path: srcNormalized,
|
|
737
|
+
message: 'no such file or directory',
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
// Destination parent must exist and be a directory (Node does not create parents)
|
|
741
|
+
const destParent = await this.resolveParent(destNormalized);
|
|
742
|
+
if (!destParent) {
|
|
743
|
+
throw createFsError({
|
|
744
|
+
code: 'ENOENT',
|
|
745
|
+
syscall: 'copyfile',
|
|
746
|
+
path: destNormalized,
|
|
747
|
+
message: 'no such file or directory',
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
await assertInodeIsDirectory(this.db, destParent.parentIno, 'copyfile', destNormalized);
|
|
751
|
+
await this.db.exec('BEGIN');
|
|
752
|
+
try {
|
|
753
|
+
const now = Math.floor(Date.now() / 1000);
|
|
754
|
+
// If destination exists, it must be a file (overwrite semantics).
|
|
755
|
+
const destIno = await this.resolvePath(destNormalized);
|
|
756
|
+
if (destIno !== null) {
|
|
757
|
+
const destMode = await getInodeModeOrThrow(this.db, destIno, 'copyfile', destNormalized);
|
|
758
|
+
assertNotSymlinkMode(destMode, 'copyfile', destNormalized);
|
|
759
|
+
if ((destMode & S_IFMT) === S_IFDIR) {
|
|
760
|
+
throw createFsError({
|
|
761
|
+
code: 'EISDIR',
|
|
762
|
+
syscall: 'copyfile',
|
|
763
|
+
path: destNormalized,
|
|
764
|
+
message: 'illegal operation on a directory',
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
// Replace destination contents
|
|
768
|
+
const deleteStmt = this.db.prepare('DELETE FROM fs_data WHERE ino = ?');
|
|
769
|
+
await deleteStmt.run(destIno);
|
|
770
|
+
const copyStmt = this.db.prepare(`
|
|
771
|
+
INSERT INTO fs_data (ino, chunk_index, data)
|
|
772
|
+
SELECT ?, chunk_index, data
|
|
773
|
+
FROM fs_data
|
|
774
|
+
WHERE ino = ?
|
|
775
|
+
ORDER BY chunk_index ASC
|
|
776
|
+
`);
|
|
777
|
+
await copyStmt.run(destIno, srcIno);
|
|
778
|
+
const updateStmt = this.db.prepare(`
|
|
779
|
+
UPDATE fs_inode
|
|
780
|
+
SET mode = ?, uid = ?, gid = ?, size = ?, mtime = ?, ctime = ?
|
|
781
|
+
WHERE ino = ?
|
|
782
|
+
`);
|
|
783
|
+
await updateStmt.run(srcRow.mode, srcRow.uid, srcRow.gid, srcRow.size, now, now, destIno);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
// Create new destination inode + dentry
|
|
787
|
+
const destInoCreated = await this.createInode(srcRow.mode, srcRow.uid, srcRow.gid);
|
|
788
|
+
await this.createDentry(destParent.parentIno, destParent.name, destInoCreated);
|
|
789
|
+
const copyStmt = this.db.prepare(`
|
|
790
|
+
INSERT INTO fs_data (ino, chunk_index, data)
|
|
791
|
+
SELECT ?, chunk_index, data
|
|
792
|
+
FROM fs_data
|
|
793
|
+
WHERE ino = ?
|
|
794
|
+
ORDER BY chunk_index ASC
|
|
795
|
+
`);
|
|
796
|
+
await copyStmt.run(destInoCreated, srcIno);
|
|
797
|
+
const updateStmt = this.db.prepare(`
|
|
798
|
+
UPDATE fs_inode
|
|
799
|
+
SET size = ?, mtime = ?, ctime = ?
|
|
800
|
+
WHERE ino = ?
|
|
801
|
+
`);
|
|
802
|
+
await updateStmt.run(srcRow.size, now, now, destInoCreated);
|
|
803
|
+
}
|
|
804
|
+
await this.db.exec('COMMIT');
|
|
805
|
+
}
|
|
806
|
+
catch (e) {
|
|
807
|
+
await this.db.exec('ROLLBACK');
|
|
808
|
+
throw e;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Test a user's permissions for the file or directory specified by path.
|
|
813
|
+
* Currently supports existence checks only (F_OK semantics).
|
|
814
|
+
*/
|
|
815
|
+
async access(path) {
|
|
816
|
+
const normalizedPath = this.normalizePath(path);
|
|
817
|
+
const ino = await this.resolvePath(normalizedPath);
|
|
818
|
+
if (ino === null) {
|
|
819
|
+
throw createFsError({
|
|
820
|
+
code: 'ENOENT',
|
|
821
|
+
syscall: 'access',
|
|
822
|
+
path: normalizedPath,
|
|
823
|
+
message: 'no such file or directory',
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
392
827
|
}
|
package/dist/guards.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DatabasePromise } from '@tursodatabase/database-common';
|
|
2
|
+
import { type FsSyscall } from './errors.js';
|
|
3
|
+
export declare function getInodeModeOrThrow(db: DatabasePromise, ino: number, syscall: FsSyscall, path: string): Promise<number>;
|
|
4
|
+
export declare function assertNotRoot(path: string, syscall: FsSyscall): void;
|
|
5
|
+
export declare function normalizeRmOptions(options?: {
|
|
6
|
+
force?: boolean;
|
|
7
|
+
recursive?: boolean;
|
|
8
|
+
}): {
|
|
9
|
+
force: boolean;
|
|
10
|
+
recursive: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function throwENOENTUnlessForce(path: string, syscall: FsSyscall, force: boolean): void;
|
|
13
|
+
export declare function assertNotSymlinkMode(mode: number, syscall: FsSyscall, path: string): void;
|
|
14
|
+
export declare function assertInodeIsDirectory(db: DatabasePromise, ino: number, syscall: FsSyscall, fullPathForError: string): Promise<void>;
|
|
15
|
+
export declare function assertWritableExistingInode(db: DatabasePromise, ino: number, syscall: FsSyscall, fullPathForError: string): Promise<void>;
|
|
16
|
+
export declare function assertReadableExistingInode(db: DatabasePromise, ino: number, syscall: FsSyscall, fullPathForError: string): Promise<void>;
|
|
17
|
+
export declare function assertReaddirTargetInode(db: DatabasePromise, ino: number, fullPathForError: string): Promise<void>;
|
|
18
|
+
export declare function assertUnlinkTargetInode(db: DatabasePromise, ino: number, fullPathForError: string): Promise<void>;
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createFsError } from './errors.js';
|
|
2
|
+
import { S_IFDIR, S_IFLNK, S_IFMT } from './filesystem.js';
|
|
3
|
+
async function getInodeMode(db, ino) {
|
|
4
|
+
const stmt = db.prepare('SELECT mode FROM fs_inode WHERE ino = ?');
|
|
5
|
+
const row = await stmt.get(ino);
|
|
6
|
+
return row?.mode ?? null;
|
|
7
|
+
}
|
|
8
|
+
function isDirMode(mode) {
|
|
9
|
+
return (mode & S_IFMT) === S_IFDIR;
|
|
10
|
+
}
|
|
11
|
+
export async function getInodeModeOrThrow(db, ino, syscall, path) {
|
|
12
|
+
const mode = await getInodeMode(db, ino);
|
|
13
|
+
if (mode === null) {
|
|
14
|
+
throw createFsError({
|
|
15
|
+
code: 'ENOENT',
|
|
16
|
+
syscall,
|
|
17
|
+
path,
|
|
18
|
+
message: 'no such file or directory',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return mode;
|
|
22
|
+
}
|
|
23
|
+
export function assertNotRoot(path, syscall) {
|
|
24
|
+
if (path === '/') {
|
|
25
|
+
throw createFsError({
|
|
26
|
+
code: 'EPERM',
|
|
27
|
+
syscall,
|
|
28
|
+
path,
|
|
29
|
+
message: 'operation not permitted on root directory',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function normalizeRmOptions(options) {
|
|
34
|
+
return {
|
|
35
|
+
force: options?.force === true,
|
|
36
|
+
recursive: options?.recursive === true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function throwENOENTUnlessForce(path, syscall, force) {
|
|
40
|
+
if (force)
|
|
41
|
+
return;
|
|
42
|
+
throw createFsError({
|
|
43
|
+
code: 'ENOENT',
|
|
44
|
+
syscall,
|
|
45
|
+
path,
|
|
46
|
+
message: 'no such file or directory',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function assertNotSymlinkMode(mode, syscall, path) {
|
|
50
|
+
if ((mode & S_IFMT) === S_IFLNK) {
|
|
51
|
+
throw createFsError({
|
|
52
|
+
code: 'ENOSYS',
|
|
53
|
+
syscall,
|
|
54
|
+
path,
|
|
55
|
+
message: 'symbolic links not supported yet',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function assertExistingNonDirNonSymlinkInode(db, ino, syscall, fullPathForError) {
|
|
60
|
+
const mode = await getInodeMode(db, ino);
|
|
61
|
+
if (mode === null) {
|
|
62
|
+
throw createFsError({
|
|
63
|
+
code: 'ENOENT',
|
|
64
|
+
syscall,
|
|
65
|
+
path: fullPathForError,
|
|
66
|
+
message: 'no such file or directory',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (isDirMode(mode)) {
|
|
70
|
+
throw createFsError({
|
|
71
|
+
code: 'EISDIR',
|
|
72
|
+
syscall,
|
|
73
|
+
path: fullPathForError,
|
|
74
|
+
message: 'illegal operation on a directory',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
assertNotSymlinkMode(mode, syscall, fullPathForError);
|
|
78
|
+
}
|
|
79
|
+
export async function assertInodeIsDirectory(db, ino, syscall, fullPathForError) {
|
|
80
|
+
const mode = await getInodeMode(db, ino);
|
|
81
|
+
if (mode === null) {
|
|
82
|
+
throw createFsError({
|
|
83
|
+
code: 'ENOENT',
|
|
84
|
+
syscall,
|
|
85
|
+
path: fullPathForError,
|
|
86
|
+
message: 'no such file or directory',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!isDirMode(mode)) {
|
|
90
|
+
throw createFsError({
|
|
91
|
+
code: 'ENOTDIR',
|
|
92
|
+
syscall,
|
|
93
|
+
path: fullPathForError,
|
|
94
|
+
message: 'not a directory',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function assertWritableExistingInode(db, ino, syscall, fullPathForError) {
|
|
99
|
+
await assertExistingNonDirNonSymlinkInode(db, ino, syscall, fullPathForError);
|
|
100
|
+
}
|
|
101
|
+
export async function assertReadableExistingInode(db, ino, syscall, fullPathForError) {
|
|
102
|
+
await assertExistingNonDirNonSymlinkInode(db, ino, syscall, fullPathForError);
|
|
103
|
+
}
|
|
104
|
+
export async function assertReaddirTargetInode(db, ino, fullPathForError) {
|
|
105
|
+
const syscall = 'scandir';
|
|
106
|
+
const mode = await getInodeMode(db, ino);
|
|
107
|
+
if (mode === null) {
|
|
108
|
+
throw createFsError({
|
|
109
|
+
code: 'ENOENT',
|
|
110
|
+
syscall,
|
|
111
|
+
path: fullPathForError,
|
|
112
|
+
message: 'no such file or directory',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
assertNotSymlinkMode(mode, syscall, fullPathForError);
|
|
116
|
+
if (!isDirMode(mode)) {
|
|
117
|
+
throw createFsError({
|
|
118
|
+
code: 'ENOTDIR',
|
|
119
|
+
syscall,
|
|
120
|
+
path: fullPathForError,
|
|
121
|
+
message: 'not a directory',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function assertUnlinkTargetInode(db, ino, fullPathForError) {
|
|
126
|
+
const syscall = 'unlink';
|
|
127
|
+
const mode = await getInodeMode(db, ino);
|
|
128
|
+
if (mode === null) {
|
|
129
|
+
throw createFsError({
|
|
130
|
+
code: 'ENOENT',
|
|
131
|
+
syscall,
|
|
132
|
+
path: fullPathForError,
|
|
133
|
+
message: 'no such file or directory',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (isDirMode(mode)) {
|
|
137
|
+
throw createFsError({
|
|
138
|
+
code: 'EISDIR',
|
|
139
|
+
syscall,
|
|
140
|
+
path: fullPathForError,
|
|
141
|
+
message: 'illegal operation on a directory',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
assertNotSymlinkMode(mode, syscall, fullPathForError);
|
|
145
|
+
}
|