gitx.do 0.0.3 → 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.
- package/README.md +319 -92
- package/dist/cli/commands/add.d.ts +174 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +131 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/merge.d.ts +106 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +55 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/fsx-cli-adapter.d.ts +359 -0
- package/dist/cli/fsx-cli-adapter.d.ts.map +1 -0
- package/dist/cli/fsx-cli-adapter.js +619 -0
- package/dist/cli/fsx-cli-adapter.js.map +1 -0
- package/dist/cli/index.js +0 -0
- package/dist/do/BashModule.d.ts +871 -0
- package/dist/do/BashModule.d.ts.map +1 -0
- package/dist/do/BashModule.js +1143 -0
- package/dist/do/BashModule.js.map +1 -0
- package/dist/do/FsModule.d.ts +601 -0
- package/dist/do/FsModule.d.ts.map +1 -0
- package/dist/do/FsModule.js +1120 -0
- package/dist/do/FsModule.js.map +1 -0
- package/dist/do/GitModule.d.ts +635 -0
- package/dist/do/GitModule.d.ts.map +1 -0
- package/dist/do/GitModule.js +781 -0
- package/dist/do/GitModule.js.map +1 -0
- package/dist/do/GitRepoDO.d.ts +281 -0
- package/dist/do/GitRepoDO.d.ts.map +1 -0
- package/dist/do/GitRepoDO.js +479 -0
- package/dist/do/GitRepoDO.js.map +1 -0
- package/dist/do/bash-ast.d.ts +246 -0
- package/dist/do/bash-ast.d.ts.map +1 -0
- package/dist/do/bash-ast.js +888 -0
- package/dist/do/bash-ast.js.map +1 -0
- package/dist/do/container-executor.d.ts +491 -0
- package/dist/do/container-executor.d.ts.map +1 -0
- package/dist/do/container-executor.js +730 -0
- package/dist/do/container-executor.js.map +1 -0
- package/dist/do/index.d.ts +53 -0
- package/dist/do/index.d.ts.map +1 -0
- package/dist/do/index.js +91 -0
- package/dist/do/index.js.map +1 -0
- package/dist/do/tiered-storage.d.ts +403 -0
- package/dist/do/tiered-storage.d.ts.map +1 -0
- package/dist/do/tiered-storage.js +689 -0
- package/dist/do/tiered-storage.js.map +1 -0
- package/dist/do/withBash.d.ts +231 -0
- package/dist/do/withBash.d.ts.map +1 -0
- package/dist/do/withBash.js +244 -0
- package/dist/do/withBash.js.map +1 -0
- package/dist/do/withFs.d.ts +237 -0
- package/dist/do/withFs.d.ts.map +1 -0
- package/dist/do/withFs.js +387 -0
- package/dist/do/withFs.js.map +1 -0
- package/dist/do/withGit.d.ts +180 -0
- package/dist/do/withGit.d.ts.map +1 -0
- package/dist/do/withGit.js +271 -0
- package/dist/do/withGit.js.map +1 -0
- package/dist/durable-object/object-store.d.ts +157 -15
- package/dist/durable-object/object-store.d.ts.map +1 -1
- package/dist/durable-object/object-store.js +432 -47
- package/dist/durable-object/object-store.js.map +1 -1
- package/dist/durable-object/schema.d.ts +12 -1
- package/dist/durable-object/schema.d.ts.map +1 -1
- package/dist/durable-object/schema.js +87 -2
- package/dist/durable-object/schema.js.map +1 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts +22 -0
- package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +1 -0
- package/dist/mcp/sandbox/miniflare-evaluator.js +140 -0
- package/dist/mcp/sandbox/miniflare-evaluator.js.map +1 -0
- package/dist/mcp/sandbox/object-store-proxy.d.ts +32 -0
- package/dist/mcp/sandbox/object-store-proxy.d.ts.map +1 -0
- package/dist/mcp/sandbox/object-store-proxy.js +30 -0
- package/dist/mcp/sandbox/object-store-proxy.js.map +1 -0
- package/dist/mcp/sandbox/template.d.ts +17 -0
- package/dist/mcp/sandbox/template.d.ts.map +1 -0
- package/dist/mcp/sandbox/template.js +71 -0
- package/dist/mcp/sandbox/template.js.map +1 -0
- package/dist/mcp/sandbox.d.ts.map +1 -1
- package/dist/mcp/sandbox.js +16 -4
- package/dist/mcp/sandbox.js.map +1 -1
- package/dist/mcp/tools/do.d.ts +32 -0
- package/dist/mcp/tools/do.d.ts.map +1 -0
- package/dist/mcp/tools/do.js +115 -0
- package/dist/mcp/tools/do.js.map +1 -0
- package/dist/pack/delta.d.ts +8 -0
- package/dist/pack/delta.d.ts.map +1 -1
- package/dist/pack/delta.js +236 -29
- package/dist/pack/delta.js.map +1 -1
- package/dist/refs/branch.d.ts +22 -24
- package/dist/refs/branch.d.ts.map +1 -1
- package/dist/refs/branch.js +409 -66
- package/dist/refs/branch.js.map +1 -1
- package/dist/refs/storage.d.ts +77 -5
- package/dist/refs/storage.d.ts.map +1 -1
- package/dist/refs/storage.js +193 -43
- package/dist/refs/storage.js.map +1 -1
- package/dist/refs/tag.d.ts +44 -24
- package/dist/refs/tag.d.ts.map +1 -1
- package/dist/refs/tag.js +411 -70
- package/dist/refs/tag.js.map +1 -1
- package/dist/storage/backend.d.ts +425 -0
- package/dist/storage/backend.d.ts.map +1 -0
- package/dist/storage/backend.js +41 -0
- package/dist/storage/backend.js.map +1 -0
- package/dist/storage/fsx-adapter.d.ts +204 -0
- package/dist/storage/fsx-adapter.d.ts.map +1 -0
- package/dist/storage/fsx-adapter.js +470 -0
- package/dist/storage/fsx-adapter.js.map +1 -0
- package/dist/types/capability.d.ts +1385 -0
- package/dist/types/capability.d.ts.map +1 -0
- package/dist/types/capability.js +36 -0
- package/dist/types/capability.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/objects.d.ts +182 -0
- package/dist/types/objects.d.ts.map +1 -1
- package/dist/types/objects.js +249 -4
- package/dist/types/objects.js.map +1 -1
- package/dist/types/storage.d.ts +114 -0
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/storage.js +160 -1
- package/dist/types/storage.js.map +1 -1
- package/dist/types/worker-loader.d.ts +60 -0
- package/dist/types/worker-loader.d.ts.map +1 -0
- package/dist/types/worker-loader.js +62 -0
- package/dist/types/worker-loader.js.map +1 -0
- package/dist/utils/hash.d.ts +125 -80
- package/dist/utils/hash.d.ts.map +1 -1
- package/dist/utils/hash.js +187 -100
- package/dist/utils/hash.js.map +1 -1
- package/dist/utils/sha1.d.ts +171 -0
- package/dist/utils/sha1.d.ts.map +1 -1
- package/dist/utils/sha1.js +352 -0
- package/dist/utils/sha1.js.map +1 -1
- package/dist/wire/path-security.d.ts +157 -0
- package/dist/wire/path-security.d.ts.map +1 -0
- package/dist/wire/path-security.js +307 -0
- package/dist/wire/path-security.js.map +1 -0
- package/dist/wire/receive-pack.d.ts +7 -0
- package/dist/wire/receive-pack.d.ts.map +1 -1
- package/dist/wire/receive-pack.js +29 -1
- package/dist/wire/receive-pack.js.map +1 -1
- package/package.json +10 -1
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FsModule for Durable Object Integration
|
|
3
|
+
*
|
|
4
|
+
* This module provides a FsModule class that integrates with dotdo's $ WorkflowContext,
|
|
5
|
+
* providing $.fs.read(), $.fs.write(), and POSIX-like filesystem operations.
|
|
6
|
+
*
|
|
7
|
+
* The module uses SQLite for metadata storage and supports tiered blob storage
|
|
8
|
+
* with R2 integration. Implements lazy initialization - schema is only created
|
|
9
|
+
* on first use.
|
|
10
|
+
*
|
|
11
|
+
* @module do/FsModule
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { FsModule } from 'gitx.do/do'
|
|
16
|
+
*
|
|
17
|
+
* class MyDO extends DO {
|
|
18
|
+
* fs = new FsModule({
|
|
19
|
+
* sql: this.ctx.storage.sql
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* async loadConfig() {
|
|
23
|
+
* await this.fs.initialize()
|
|
24
|
+
* const content = await this.fs.readFile('/config.json')
|
|
25
|
+
* return JSON.parse(content)
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Constants
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* File type mode bits (POSIX).
|
|
35
|
+
*/
|
|
36
|
+
export const S_IFMT = 0o170000; // Mask for file type
|
|
37
|
+
export const S_IFREG = 0o100000; // Regular file
|
|
38
|
+
export const S_IFDIR = 0o040000; // Directory
|
|
39
|
+
export const S_IFLNK = 0o120000; // Symbolic link
|
|
40
|
+
/**
|
|
41
|
+
* SQL schema for filesystem metadata.
|
|
42
|
+
*/
|
|
43
|
+
const SCHEMA = `
|
|
44
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
45
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
+
path TEXT UNIQUE NOT NULL,
|
|
47
|
+
name TEXT NOT NULL,
|
|
48
|
+
parent_id INTEGER,
|
|
49
|
+
type TEXT NOT NULL CHECK(type IN ('file', 'directory', 'symlink')),
|
|
50
|
+
mode INTEGER NOT NULL DEFAULT 420,
|
|
51
|
+
uid INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
gid INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
blob_id TEXT,
|
|
55
|
+
link_target TEXT,
|
|
56
|
+
tier TEXT NOT NULL DEFAULT 'hot' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
57
|
+
atime INTEGER NOT NULL,
|
|
58
|
+
mtime INTEGER NOT NULL,
|
|
59
|
+
ctime INTEGER NOT NULL,
|
|
60
|
+
birthtime INTEGER NOT NULL,
|
|
61
|
+
nlink INTEGER NOT NULL DEFAULT 1,
|
|
62
|
+
FOREIGN KEY (parent_id) REFERENCES files(id) ON DELETE CASCADE
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_files_parent ON files(parent_id);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_files_tier ON files(tier);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS blobs (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
data BLOB,
|
|
72
|
+
size INTEGER NOT NULL,
|
|
73
|
+
checksum TEXT,
|
|
74
|
+
tier TEXT NOT NULL DEFAULT 'hot' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
75
|
+
created_at INTEGER NOT NULL
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_blobs_tier ON blobs(tier);
|
|
79
|
+
`;
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Error Classes
|
|
82
|
+
// ============================================================================
|
|
83
|
+
/**
|
|
84
|
+
* Base class for filesystem errors.
|
|
85
|
+
*/
|
|
86
|
+
class FsError extends Error {
|
|
87
|
+
code;
|
|
88
|
+
path;
|
|
89
|
+
constructor(code, message, path) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = 'FsError';
|
|
92
|
+
this.code = code;
|
|
93
|
+
this.path = path;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* ENOENT: No such file or directory.
|
|
98
|
+
*/
|
|
99
|
+
export class ENOENT extends FsError {
|
|
100
|
+
constructor(message, path) {
|
|
101
|
+
super('ENOENT', message ?? 'no such file or directory', path);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* EEXIST: File already exists.
|
|
106
|
+
*/
|
|
107
|
+
export class EEXIST extends FsError {
|
|
108
|
+
constructor(message, path) {
|
|
109
|
+
super('EEXIST', message ?? 'file already exists', path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* EISDIR: Illegal operation on a directory.
|
|
114
|
+
*/
|
|
115
|
+
export class EISDIR extends FsError {
|
|
116
|
+
constructor(message, path) {
|
|
117
|
+
super('EISDIR', message ?? 'illegal operation on a directory', path);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* ENOTDIR: Not a directory.
|
|
122
|
+
*/
|
|
123
|
+
export class ENOTDIR extends FsError {
|
|
124
|
+
constructor(message, path) {
|
|
125
|
+
super('ENOTDIR', message ?? 'not a directory', path);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* ENOTEMPTY: Directory not empty.
|
|
130
|
+
*/
|
|
131
|
+
export class ENOTEMPTY extends FsError {
|
|
132
|
+
constructor(message, path) {
|
|
133
|
+
super('ENOTEMPTY', message ?? 'directory not empty', path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// FsModule Class
|
|
138
|
+
// ============================================================================
|
|
139
|
+
/**
|
|
140
|
+
* FsModule - Filesystem capability module for Durable Object integration.
|
|
141
|
+
*
|
|
142
|
+
* Implements POSIX-like file operations with lazy initialization.
|
|
143
|
+
* Uses SQLite for metadata and supports tiered storage with R2.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const fs = new FsModule({ sql: ctx.storage.sql })
|
|
148
|
+
*
|
|
149
|
+
* // First operation triggers initialization
|
|
150
|
+
* await fs.writeFile('/config.json', JSON.stringify(config))
|
|
151
|
+
*
|
|
152
|
+
* // Subsequent operations use existing schema
|
|
153
|
+
* const data = await fs.readFile('/config.json', { encoding: 'utf-8' })
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export class FsModule {
|
|
157
|
+
/**
|
|
158
|
+
* Capability module name for identification.
|
|
159
|
+
*/
|
|
160
|
+
name = 'fs';
|
|
161
|
+
sql;
|
|
162
|
+
r2;
|
|
163
|
+
archive;
|
|
164
|
+
basePath;
|
|
165
|
+
hotMaxSize;
|
|
166
|
+
defaultMode;
|
|
167
|
+
defaultDirMode;
|
|
168
|
+
initialized = false;
|
|
169
|
+
/**
|
|
170
|
+
* Create a new FsModule instance.
|
|
171
|
+
*
|
|
172
|
+
* @param options - Configuration options
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const fs = new FsModule({
|
|
177
|
+
* sql: ctx.storage.sql,
|
|
178
|
+
* r2: env.R2_BUCKET,
|
|
179
|
+
* hotMaxSize: 512 * 1024 // 512KB
|
|
180
|
+
* })
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
constructor(options) {
|
|
184
|
+
this.sql = options.sql;
|
|
185
|
+
this.r2 = options.r2;
|
|
186
|
+
this.archive = options.archive;
|
|
187
|
+
this.basePath = options.basePath ?? '/';
|
|
188
|
+
this.hotMaxSize = options.hotMaxSize ?? 1024 * 1024; // 1MB
|
|
189
|
+
this.defaultMode = options.defaultMode ?? 0o644;
|
|
190
|
+
this.defaultDirMode = options.defaultDirMode ?? 0o755;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Initialize the module - creates schema and root directory.
|
|
194
|
+
* This is called automatically on first operation (lazy initialization).
|
|
195
|
+
*/
|
|
196
|
+
async initialize() {
|
|
197
|
+
if (this.initialized)
|
|
198
|
+
return;
|
|
199
|
+
// Create schema
|
|
200
|
+
this.sql.exec(SCHEMA);
|
|
201
|
+
// Create root directory if not exists
|
|
202
|
+
const root = this.sql.exec('SELECT * FROM files WHERE path = ?', '/').one();
|
|
203
|
+
if (!root) {
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, tier, atime, mtime, ctime, birthtime, nlink)
|
|
206
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, '/', '', null, 'directory', this.defaultDirMode, 0, 0, 0, 'hot', now, now, now, now, 2);
|
|
207
|
+
}
|
|
208
|
+
this.initialized = true;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Cleanup hook for capability disposal.
|
|
212
|
+
*/
|
|
213
|
+
async dispose() {
|
|
214
|
+
// No cleanup needed for SQLite-backed storage
|
|
215
|
+
}
|
|
216
|
+
// ===========================================================================
|
|
217
|
+
// Path Utilities
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
normalizePath(path) {
|
|
220
|
+
// Handle base path
|
|
221
|
+
if (!path.startsWith('/')) {
|
|
222
|
+
path = this.basePath + (this.basePath.endsWith('/') ? '' : '/') + path;
|
|
223
|
+
}
|
|
224
|
+
// Remove trailing slashes (except root)
|
|
225
|
+
if (path !== '/' && path.endsWith('/')) {
|
|
226
|
+
path = path.slice(0, -1);
|
|
227
|
+
}
|
|
228
|
+
// Resolve . and ..
|
|
229
|
+
const parts = path.split('/').filter(Boolean);
|
|
230
|
+
const resolved = [];
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
if (part === '.')
|
|
233
|
+
continue;
|
|
234
|
+
if (part === '..') {
|
|
235
|
+
resolved.pop();
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
resolved.push(part);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return '/' + resolved.join('/');
|
|
242
|
+
}
|
|
243
|
+
getParentPath(path) {
|
|
244
|
+
const normalized = this.normalizePath(path);
|
|
245
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
246
|
+
if (lastSlash <= 0)
|
|
247
|
+
return '/';
|
|
248
|
+
return normalized.substring(0, lastSlash);
|
|
249
|
+
}
|
|
250
|
+
getFileName(path) {
|
|
251
|
+
const normalized = this.normalizePath(path);
|
|
252
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
253
|
+
return normalized.substring(lastSlash + 1);
|
|
254
|
+
}
|
|
255
|
+
// ===========================================================================
|
|
256
|
+
// Internal File Operations
|
|
257
|
+
// ===========================================================================
|
|
258
|
+
async getFile(path) {
|
|
259
|
+
await this.initialize();
|
|
260
|
+
const normalized = this.normalizePath(path);
|
|
261
|
+
const result = this.sql.exec('SELECT * FROM files WHERE path = ?', normalized).one();
|
|
262
|
+
return result || null;
|
|
263
|
+
}
|
|
264
|
+
selectTier(size) {
|
|
265
|
+
if (size <= this.hotMaxSize)
|
|
266
|
+
return 'hot';
|
|
267
|
+
if (this.r2)
|
|
268
|
+
return 'warm';
|
|
269
|
+
return 'hot'; // Fall back to hot if R2 not configured
|
|
270
|
+
}
|
|
271
|
+
async storeBlob(id, data, tier) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
if (tier === 'hot') {
|
|
274
|
+
this.sql.exec('INSERT OR REPLACE INTO blobs (id, data, size, tier, created_at) VALUES (?, ?, ?, ?, ?)', id, data.buffer, data.length, tier, now);
|
|
275
|
+
}
|
|
276
|
+
else if (tier === 'warm' && this.r2) {
|
|
277
|
+
await this.r2.put(id, data);
|
|
278
|
+
this.sql.exec('INSERT OR REPLACE INTO blobs (id, size, tier, created_at) VALUES (?, ?, ?, ?)', id, data.length, tier, now);
|
|
279
|
+
}
|
|
280
|
+
else if (tier === 'cold' && this.archive) {
|
|
281
|
+
await this.archive.put(id, data);
|
|
282
|
+
this.sql.exec('INSERT OR REPLACE INTO blobs (id, size, tier, created_at) VALUES (?, ?, ?, ?)', id, data.length, tier, now);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async getBlob(id, tier) {
|
|
286
|
+
if (tier === 'hot') {
|
|
287
|
+
const blob = this.sql.exec('SELECT data FROM blobs WHERE id = ?', id).one();
|
|
288
|
+
if (!blob?.data)
|
|
289
|
+
return null;
|
|
290
|
+
return new Uint8Array(blob.data);
|
|
291
|
+
}
|
|
292
|
+
if (tier === 'warm' && this.r2) {
|
|
293
|
+
const obj = await this.r2.get(id);
|
|
294
|
+
if (!obj)
|
|
295
|
+
return null;
|
|
296
|
+
return new Uint8Array(await obj.arrayBuffer());
|
|
297
|
+
}
|
|
298
|
+
if (tier === 'cold' && this.archive) {
|
|
299
|
+
const obj = await this.archive.get(id);
|
|
300
|
+
if (!obj)
|
|
301
|
+
return null;
|
|
302
|
+
return new Uint8Array(await obj.arrayBuffer());
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
async deleteBlob(id, tier) {
|
|
307
|
+
this.sql.exec('DELETE FROM blobs WHERE id = ?', id);
|
|
308
|
+
if (tier === 'warm' && this.r2) {
|
|
309
|
+
await this.r2.delete(id);
|
|
310
|
+
}
|
|
311
|
+
else if (tier === 'cold' && this.archive) {
|
|
312
|
+
await this.archive.delete(id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ===========================================================================
|
|
316
|
+
// File Operations
|
|
317
|
+
// ===========================================================================
|
|
318
|
+
/**
|
|
319
|
+
* Read a file's contents.
|
|
320
|
+
*
|
|
321
|
+
* @param path - File path to read
|
|
322
|
+
* @param options - Read options
|
|
323
|
+
* @returns File contents as string or Uint8Array
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* // Read as bytes
|
|
328
|
+
* const bytes = await fs.readFile('/data.bin')
|
|
329
|
+
*
|
|
330
|
+
* // Read as string
|
|
331
|
+
* const text = await fs.readFile('/config.json', { encoding: 'utf-8' })
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
async readFile(path, options) {
|
|
335
|
+
await this.initialize();
|
|
336
|
+
const normalized = this.normalizePath(path);
|
|
337
|
+
const file = await this.getFile(normalized);
|
|
338
|
+
if (!file) {
|
|
339
|
+
throw new ENOENT(undefined, normalized);
|
|
340
|
+
}
|
|
341
|
+
if (file.type === 'directory') {
|
|
342
|
+
throw new EISDIR(undefined, normalized);
|
|
343
|
+
}
|
|
344
|
+
// Follow symlinks
|
|
345
|
+
if (file.type === 'symlink' && file.link_target) {
|
|
346
|
+
return this.readFile(file.link_target, options);
|
|
347
|
+
}
|
|
348
|
+
if (!file.blob_id) {
|
|
349
|
+
return options?.encoding ? '' : new Uint8Array(0);
|
|
350
|
+
}
|
|
351
|
+
const data = await this.getBlob(file.blob_id, file.tier);
|
|
352
|
+
if (!data) {
|
|
353
|
+
return options?.encoding ? '' : new Uint8Array(0);
|
|
354
|
+
}
|
|
355
|
+
// Handle range reads
|
|
356
|
+
let result = data;
|
|
357
|
+
if (options?.start !== undefined || options?.end !== undefined) {
|
|
358
|
+
const start = options.start ?? 0;
|
|
359
|
+
const end = options.end !== undefined ? options.end + 1 : data.length;
|
|
360
|
+
result = data.slice(start, end);
|
|
361
|
+
}
|
|
362
|
+
// Update atime
|
|
363
|
+
this.sql.exec('UPDATE files SET atime = ? WHERE id = ?', Date.now(), file.id);
|
|
364
|
+
if (options?.encoding) {
|
|
365
|
+
return new TextDecoder().decode(result);
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Write data to a file.
|
|
371
|
+
*
|
|
372
|
+
* @param path - File path to write
|
|
373
|
+
* @param data - Data to write
|
|
374
|
+
* @param options - Write options
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```typescript
|
|
378
|
+
* // Write string
|
|
379
|
+
* await fs.writeFile('/hello.txt', 'Hello, World!')
|
|
380
|
+
*
|
|
381
|
+
* // Write bytes
|
|
382
|
+
* await fs.writeFile('/data.bin', new Uint8Array([1, 2, 3]))
|
|
383
|
+
*
|
|
384
|
+
* // Append mode
|
|
385
|
+
* await fs.writeFile('/log.txt', 'New entry\n', { flag: 'a' })
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
async writeFile(path, data, options) {
|
|
389
|
+
await this.initialize();
|
|
390
|
+
const normalized = this.normalizePath(path);
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
const parentPath = this.getParentPath(normalized);
|
|
393
|
+
const name = this.getFileName(normalized);
|
|
394
|
+
// Check parent exists
|
|
395
|
+
const parent = await this.getFile(parentPath);
|
|
396
|
+
if (!parent) {
|
|
397
|
+
throw new ENOENT(undefined, parentPath);
|
|
398
|
+
}
|
|
399
|
+
// Convert data to bytes
|
|
400
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
401
|
+
// Determine tier
|
|
402
|
+
const tier = options?.tier ?? this.selectTier(bytes.length);
|
|
403
|
+
// Check if file exists
|
|
404
|
+
const existing = await this.getFile(normalized);
|
|
405
|
+
// Handle exclusive flag
|
|
406
|
+
if (options?.flag === 'wx' || options?.flag === 'ax') {
|
|
407
|
+
if (existing) {
|
|
408
|
+
throw new EEXIST(undefined, normalized);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Handle append flag
|
|
412
|
+
if (options?.flag === 'a' || options?.flag === 'ax') {
|
|
413
|
+
if (existing && existing.blob_id) {
|
|
414
|
+
const existingData = await this.getBlob(existing.blob_id, existing.tier);
|
|
415
|
+
if (existingData) {
|
|
416
|
+
const combined = new Uint8Array(existingData.length + bytes.length);
|
|
417
|
+
combined.set(existingData);
|
|
418
|
+
combined.set(bytes, existingData.length);
|
|
419
|
+
const blobId = crypto.randomUUID();
|
|
420
|
+
await this.storeBlob(blobId, combined, tier);
|
|
421
|
+
// Delete old blob
|
|
422
|
+
await this.deleteBlob(existing.blob_id, existing.tier);
|
|
423
|
+
// Update file
|
|
424
|
+
this.sql.exec('UPDATE files SET blob_id = ?, size = ?, tier = ?, mtime = ?, ctime = ? WHERE id = ?', blobId, combined.length, tier, now, now, existing.id);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Store blob
|
|
430
|
+
const blobId = crypto.randomUUID();
|
|
431
|
+
await this.storeBlob(blobId, bytes, tier);
|
|
432
|
+
if (existing) {
|
|
433
|
+
// Delete old blob
|
|
434
|
+
if (existing.blob_id) {
|
|
435
|
+
await this.deleteBlob(existing.blob_id, existing.tier);
|
|
436
|
+
}
|
|
437
|
+
// Update file
|
|
438
|
+
this.sql.exec('UPDATE files SET blob_id = ?, size = ?, tier = ?, mtime = ?, ctime = ? WHERE id = ?', blobId, bytes.length, tier, now, now, existing.id);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// Create new file
|
|
442
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, blob_id, tier, atime, mtime, ctime, birthtime, nlink)
|
|
443
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, normalized, name, parent.id, 'file', options?.mode ?? this.defaultMode, 0, 0, bytes.length, blobId, tier, now, now, now, now, 1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Append data to a file.
|
|
448
|
+
*
|
|
449
|
+
* @param path - File path to append to
|
|
450
|
+
* @param data - Data to append
|
|
451
|
+
*/
|
|
452
|
+
async appendFile(path, data) {
|
|
453
|
+
return this.writeFile(path, data, { flag: 'a' });
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Delete a file.
|
|
457
|
+
*
|
|
458
|
+
* @param path - File path to delete
|
|
459
|
+
*/
|
|
460
|
+
async unlink(path) {
|
|
461
|
+
await this.initialize();
|
|
462
|
+
const normalized = this.normalizePath(path);
|
|
463
|
+
const file = await this.getFile(normalized);
|
|
464
|
+
if (!file) {
|
|
465
|
+
throw new ENOENT(undefined, normalized);
|
|
466
|
+
}
|
|
467
|
+
if (file.type === 'directory') {
|
|
468
|
+
throw new EISDIR(undefined, normalized);
|
|
469
|
+
}
|
|
470
|
+
// Delete blob
|
|
471
|
+
if (file.blob_id) {
|
|
472
|
+
await this.deleteBlob(file.blob_id, file.tier);
|
|
473
|
+
}
|
|
474
|
+
// Delete file entry
|
|
475
|
+
this.sql.exec('DELETE FROM files WHERE id = ?', file.id);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Rename/move a file or directory.
|
|
479
|
+
*
|
|
480
|
+
* @param oldPath - Current path
|
|
481
|
+
* @param newPath - New path
|
|
482
|
+
* @param options - Move options
|
|
483
|
+
*/
|
|
484
|
+
async rename(oldPath, newPath, options) {
|
|
485
|
+
await this.initialize();
|
|
486
|
+
const oldNormalized = this.normalizePath(oldPath);
|
|
487
|
+
const newNormalized = this.normalizePath(newPath);
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const file = await this.getFile(oldNormalized);
|
|
490
|
+
if (!file) {
|
|
491
|
+
throw new ENOENT(undefined, oldNormalized);
|
|
492
|
+
}
|
|
493
|
+
// Check if destination exists
|
|
494
|
+
const existing = await this.getFile(newNormalized);
|
|
495
|
+
if (existing && !options?.overwrite) {
|
|
496
|
+
throw new EEXIST(undefined, newNormalized);
|
|
497
|
+
}
|
|
498
|
+
// Get new parent
|
|
499
|
+
const newParentPath = this.getParentPath(newNormalized);
|
|
500
|
+
const newParent = await this.getFile(newParentPath);
|
|
501
|
+
if (!newParent) {
|
|
502
|
+
throw new ENOENT(undefined, newParentPath);
|
|
503
|
+
}
|
|
504
|
+
const newName = this.getFileName(newNormalized);
|
|
505
|
+
// Delete existing if overwriting
|
|
506
|
+
if (existing) {
|
|
507
|
+
if (existing.blob_id) {
|
|
508
|
+
await this.deleteBlob(existing.blob_id, existing.tier);
|
|
509
|
+
}
|
|
510
|
+
this.sql.exec('DELETE FROM files WHERE id = ?', existing.id);
|
|
511
|
+
}
|
|
512
|
+
// Update file
|
|
513
|
+
this.sql.exec('UPDATE files SET path = ?, name = ?, parent_id = ?, ctime = ? WHERE id = ?', newNormalized, newName, newParent.id, now, file.id);
|
|
514
|
+
// If directory, update all children paths
|
|
515
|
+
if (file.type === 'directory') {
|
|
516
|
+
const children = this.sql.exec('SELECT * FROM files WHERE path LIKE ?', oldNormalized + '/%').toArray();
|
|
517
|
+
for (const child of children) {
|
|
518
|
+
const newChildPath = newNormalized + child.path.substring(oldNormalized.length);
|
|
519
|
+
this.sql.exec('UPDATE files SET path = ? WHERE id = ?', newChildPath, child.id);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Copy a file.
|
|
525
|
+
*
|
|
526
|
+
* @param src - Source file path
|
|
527
|
+
* @param dest - Destination file path
|
|
528
|
+
* @param options - Copy options
|
|
529
|
+
*/
|
|
530
|
+
async copyFile(src, dest, options) {
|
|
531
|
+
await this.initialize();
|
|
532
|
+
const srcNormalized = this.normalizePath(src);
|
|
533
|
+
const destNormalized = this.normalizePath(dest);
|
|
534
|
+
const srcFile = await this.getFile(srcNormalized);
|
|
535
|
+
if (!srcFile) {
|
|
536
|
+
throw new ENOENT(undefined, srcNormalized);
|
|
537
|
+
}
|
|
538
|
+
// Check destination
|
|
539
|
+
const existing = await this.getFile(destNormalized);
|
|
540
|
+
if (existing && !options?.overwrite) {
|
|
541
|
+
throw new EEXIST(undefined, destNormalized);
|
|
542
|
+
}
|
|
543
|
+
// Read source content
|
|
544
|
+
const content = await this.readFile(srcNormalized);
|
|
545
|
+
// Write to destination
|
|
546
|
+
await this.writeFile(destNormalized, content);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Truncate a file to a specified length.
|
|
550
|
+
*
|
|
551
|
+
* @param path - File path
|
|
552
|
+
* @param length - New length (default: 0)
|
|
553
|
+
*/
|
|
554
|
+
async truncate(path, length = 0) {
|
|
555
|
+
await this.initialize();
|
|
556
|
+
const normalized = this.normalizePath(path);
|
|
557
|
+
const file = await this.getFile(normalized);
|
|
558
|
+
if (!file) {
|
|
559
|
+
throw new ENOENT(undefined, normalized);
|
|
560
|
+
}
|
|
561
|
+
if (file.type === 'directory') {
|
|
562
|
+
throw new EISDIR(undefined, normalized);
|
|
563
|
+
}
|
|
564
|
+
const now = Date.now();
|
|
565
|
+
if (file.blob_id) {
|
|
566
|
+
const data = await this.getBlob(file.blob_id, file.tier);
|
|
567
|
+
if (data) {
|
|
568
|
+
const truncated = data.slice(0, length);
|
|
569
|
+
const newBlobId = crypto.randomUUID();
|
|
570
|
+
const newTier = this.selectTier(truncated.length);
|
|
571
|
+
await this.storeBlob(newBlobId, truncated, newTier);
|
|
572
|
+
await this.deleteBlob(file.blob_id, file.tier);
|
|
573
|
+
this.sql.exec('UPDATE files SET blob_id = ?, size = ?, tier = ?, mtime = ?, ctime = ? WHERE id = ?', newBlobId, truncated.length, newTier, now, now, file.id);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// ===========================================================================
|
|
578
|
+
// Directory Operations
|
|
579
|
+
// ===========================================================================
|
|
580
|
+
/**
|
|
581
|
+
* Create a directory.
|
|
582
|
+
*
|
|
583
|
+
* @param path - Directory path
|
|
584
|
+
* @param options - Mkdir options
|
|
585
|
+
*/
|
|
586
|
+
async mkdir(path, options) {
|
|
587
|
+
await this.initialize();
|
|
588
|
+
const normalized = this.normalizePath(path);
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
if (options?.recursive) {
|
|
591
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
592
|
+
let currentPath = '';
|
|
593
|
+
for (const part of parts) {
|
|
594
|
+
currentPath += '/' + part;
|
|
595
|
+
const existing = await this.getFile(currentPath);
|
|
596
|
+
if (!existing) {
|
|
597
|
+
const parentPath = this.getParentPath(currentPath);
|
|
598
|
+
const parent = await this.getFile(parentPath);
|
|
599
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, tier, atime, mtime, ctime, birthtime, nlink)
|
|
600
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, currentPath, part, parent?.id ?? null, 'directory', options?.mode ?? this.defaultDirMode, 0, 0, 0, 'hot', now, now, now, now, 2);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
const parentPath = this.getParentPath(normalized);
|
|
606
|
+
const name = this.getFileName(normalized);
|
|
607
|
+
const parent = await this.getFile(parentPath);
|
|
608
|
+
if (!parent) {
|
|
609
|
+
throw new ENOENT(undefined, parentPath);
|
|
610
|
+
}
|
|
611
|
+
const existing = await this.getFile(normalized);
|
|
612
|
+
if (existing) {
|
|
613
|
+
throw new EEXIST(undefined, normalized);
|
|
614
|
+
}
|
|
615
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, tier, atime, mtime, ctime, birthtime, nlink)
|
|
616
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, normalized, name, parent.id, 'directory', options?.mode ?? this.defaultDirMode, 0, 0, 0, 'hot', now, now, now, now, 2);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Remove a directory.
|
|
621
|
+
*
|
|
622
|
+
* @param path - Directory path
|
|
623
|
+
* @param options - Rmdir options
|
|
624
|
+
*/
|
|
625
|
+
async rmdir(path, options) {
|
|
626
|
+
await this.initialize();
|
|
627
|
+
const normalized = this.normalizePath(path);
|
|
628
|
+
const file = await this.getFile(normalized);
|
|
629
|
+
if (!file) {
|
|
630
|
+
throw new ENOENT(undefined, normalized);
|
|
631
|
+
}
|
|
632
|
+
if (file.type !== 'directory') {
|
|
633
|
+
throw new ENOTDIR(undefined, normalized);
|
|
634
|
+
}
|
|
635
|
+
const children = this.sql.exec('SELECT * FROM files WHERE parent_id = ?', file.id).toArray();
|
|
636
|
+
if (children.length > 0 && !options?.recursive) {
|
|
637
|
+
throw new ENOTEMPTY(undefined, normalized);
|
|
638
|
+
}
|
|
639
|
+
if (options?.recursive) {
|
|
640
|
+
// Delete all descendants recursively
|
|
641
|
+
await this.deleteRecursive(file);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.sql.exec('DELETE FROM files WHERE id = ?', file.id);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async deleteRecursive(file) {
|
|
648
|
+
const children = this.sql.exec('SELECT * FROM files WHERE parent_id = ?', file.id).toArray();
|
|
649
|
+
for (const child of children) {
|
|
650
|
+
if (child.type === 'directory') {
|
|
651
|
+
await this.deleteRecursive(child);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
if (child.blob_id) {
|
|
655
|
+
await this.deleteBlob(child.blob_id, child.tier);
|
|
656
|
+
}
|
|
657
|
+
this.sql.exec('DELETE FROM files WHERE id = ?', child.id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
this.sql.exec('DELETE FROM files WHERE id = ?', file.id);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Remove a file or directory.
|
|
664
|
+
*
|
|
665
|
+
* @param path - Path to remove
|
|
666
|
+
* @param options - Remove options
|
|
667
|
+
*/
|
|
668
|
+
async rm(path, options) {
|
|
669
|
+
await this.initialize();
|
|
670
|
+
const normalized = this.normalizePath(path);
|
|
671
|
+
const file = await this.getFile(normalized);
|
|
672
|
+
if (!file) {
|
|
673
|
+
if (options?.force)
|
|
674
|
+
return;
|
|
675
|
+
throw new ENOENT(undefined, normalized);
|
|
676
|
+
}
|
|
677
|
+
if (file.type === 'directory') {
|
|
678
|
+
await this.rmdir(normalized, { recursive: options?.recursive });
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
await this.unlink(normalized);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Read directory contents.
|
|
686
|
+
*
|
|
687
|
+
* @param path - Directory path
|
|
688
|
+
* @param options - Readdir options
|
|
689
|
+
* @returns Array of filenames or Dirent objects
|
|
690
|
+
*/
|
|
691
|
+
async readdir(path, options) {
|
|
692
|
+
await this.initialize();
|
|
693
|
+
const normalized = this.normalizePath(path);
|
|
694
|
+
const file = await this.getFile(normalized);
|
|
695
|
+
if (!file) {
|
|
696
|
+
throw new ENOENT(undefined, normalized);
|
|
697
|
+
}
|
|
698
|
+
if (file.type !== 'directory') {
|
|
699
|
+
throw new ENOTDIR(undefined, normalized);
|
|
700
|
+
}
|
|
701
|
+
const children = this.sql.exec('SELECT * FROM files WHERE parent_id = ?', file.id).toArray();
|
|
702
|
+
if (options?.withFileTypes) {
|
|
703
|
+
const result = children.map((child) => ({
|
|
704
|
+
name: child.name,
|
|
705
|
+
parentPath: normalized,
|
|
706
|
+
path: child.path,
|
|
707
|
+
isFile: () => child.type === 'file',
|
|
708
|
+
isDirectory: () => child.type === 'directory',
|
|
709
|
+
isSymbolicLink: () => child.type === 'symlink',
|
|
710
|
+
isBlockDevice: () => false,
|
|
711
|
+
isCharacterDevice: () => false,
|
|
712
|
+
isFIFO: () => false,
|
|
713
|
+
isSocket: () => false,
|
|
714
|
+
}));
|
|
715
|
+
if (options.recursive) {
|
|
716
|
+
for (const child of children) {
|
|
717
|
+
if (child.type === 'directory') {
|
|
718
|
+
const subEntries = (await this.readdir(child.path, options));
|
|
719
|
+
result.push(...subEntries);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
const names = children.map((c) => c.name);
|
|
726
|
+
if (options?.recursive) {
|
|
727
|
+
for (const child of children) {
|
|
728
|
+
if (child.type === 'directory') {
|
|
729
|
+
const subNames = (await this.readdir(child.path, options));
|
|
730
|
+
names.push(...subNames.map((n) => child.name + '/' + n));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return names;
|
|
735
|
+
}
|
|
736
|
+
// ===========================================================================
|
|
737
|
+
// Metadata Operations
|
|
738
|
+
// ===========================================================================
|
|
739
|
+
/**
|
|
740
|
+
* Get file stats (follows symlinks).
|
|
741
|
+
*
|
|
742
|
+
* @param path - File path
|
|
743
|
+
* @returns Stats object
|
|
744
|
+
*/
|
|
745
|
+
async stat(path) {
|
|
746
|
+
await this.initialize();
|
|
747
|
+
const normalized = this.normalizePath(path);
|
|
748
|
+
let file = await this.getFile(normalized);
|
|
749
|
+
if (!file) {
|
|
750
|
+
throw new ENOENT(undefined, normalized);
|
|
751
|
+
}
|
|
752
|
+
// Follow symlinks
|
|
753
|
+
while (file.type === 'symlink' && file.link_target) {
|
|
754
|
+
file = await this.getFile(file.link_target);
|
|
755
|
+
if (!file) {
|
|
756
|
+
throw new ENOENT(undefined, normalized);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return this.fileToStats(file);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get file stats (does not follow symlinks).
|
|
763
|
+
*
|
|
764
|
+
* @param path - File path
|
|
765
|
+
* @returns Stats object
|
|
766
|
+
*/
|
|
767
|
+
async lstat(path) {
|
|
768
|
+
await this.initialize();
|
|
769
|
+
const normalized = this.normalizePath(path);
|
|
770
|
+
const file = await this.getFile(normalized);
|
|
771
|
+
if (!file) {
|
|
772
|
+
throw new ENOENT(undefined, normalized);
|
|
773
|
+
}
|
|
774
|
+
return this.fileToStats(file);
|
|
775
|
+
}
|
|
776
|
+
fileToStats(file) {
|
|
777
|
+
const typeMode = file.type === 'directory' ? S_IFDIR : file.type === 'symlink' ? S_IFLNK : S_IFREG;
|
|
778
|
+
const mode = typeMode | file.mode;
|
|
779
|
+
return {
|
|
780
|
+
dev: 0,
|
|
781
|
+
ino: file.id,
|
|
782
|
+
mode,
|
|
783
|
+
nlink: file.nlink,
|
|
784
|
+
uid: file.uid,
|
|
785
|
+
gid: file.gid,
|
|
786
|
+
rdev: 0,
|
|
787
|
+
size: file.size,
|
|
788
|
+
blksize: 4096,
|
|
789
|
+
blocks: Math.ceil(file.size / 512),
|
|
790
|
+
atimeMs: file.atime,
|
|
791
|
+
mtimeMs: file.mtime,
|
|
792
|
+
ctimeMs: file.ctime,
|
|
793
|
+
birthtimeMs: file.birthtime,
|
|
794
|
+
atime: new Date(file.atime),
|
|
795
|
+
mtime: new Date(file.mtime),
|
|
796
|
+
ctime: new Date(file.ctime),
|
|
797
|
+
birthtime: new Date(file.birthtime),
|
|
798
|
+
isFile: () => file.type === 'file',
|
|
799
|
+
isDirectory: () => file.type === 'directory',
|
|
800
|
+
isSymbolicLink: () => file.type === 'symlink',
|
|
801
|
+
isBlockDevice: () => false,
|
|
802
|
+
isCharacterDevice: () => false,
|
|
803
|
+
isFIFO: () => false,
|
|
804
|
+
isSocket: () => false,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Check if a path exists.
|
|
809
|
+
*
|
|
810
|
+
* @param path - Path to check
|
|
811
|
+
* @returns True if path exists
|
|
812
|
+
*/
|
|
813
|
+
async exists(path) {
|
|
814
|
+
await this.initialize();
|
|
815
|
+
const file = await this.getFile(path);
|
|
816
|
+
return file !== null;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Get the integer rowid (file_id) for a file path.
|
|
820
|
+
* This is useful for foreign key references from other tables.
|
|
821
|
+
*
|
|
822
|
+
* @param path - File path to look up
|
|
823
|
+
* @returns The file's integer rowid, or null if file doesn't exist
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```typescript
|
|
827
|
+
* const fileId = await fs.getFileId('/config.json')
|
|
828
|
+
* if (fileId !== null) {
|
|
829
|
+
* // Use fileId as foreign key reference
|
|
830
|
+
* }
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
async getFileId(path) {
|
|
834
|
+
await this.initialize();
|
|
835
|
+
const normalized = this.normalizePath(path);
|
|
836
|
+
const result = this.sql.exec('SELECT id FROM files WHERE path = ?', normalized).one();
|
|
837
|
+
return result?.id ?? null;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Check access to a file.
|
|
841
|
+
*
|
|
842
|
+
* @param path - Path to check
|
|
843
|
+
* @param _mode - Access mode (not fully implemented)
|
|
844
|
+
*/
|
|
845
|
+
async access(path, _mode) {
|
|
846
|
+
await this.initialize();
|
|
847
|
+
const file = await this.getFile(path);
|
|
848
|
+
if (!file) {
|
|
849
|
+
throw new ENOENT(undefined, path);
|
|
850
|
+
}
|
|
851
|
+
// Simplified: just check existence for now
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Change file mode.
|
|
855
|
+
*
|
|
856
|
+
* @param path - File path
|
|
857
|
+
* @param mode - New mode
|
|
858
|
+
*/
|
|
859
|
+
async chmod(path, mode) {
|
|
860
|
+
await this.initialize();
|
|
861
|
+
const normalized = this.normalizePath(path);
|
|
862
|
+
const file = await this.getFile(normalized);
|
|
863
|
+
if (!file) {
|
|
864
|
+
throw new ENOENT(undefined, normalized);
|
|
865
|
+
}
|
|
866
|
+
this.sql.exec('UPDATE files SET mode = ?, ctime = ? WHERE id = ?', mode, Date.now(), file.id);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Change file ownership.
|
|
870
|
+
*
|
|
871
|
+
* @param path - File path
|
|
872
|
+
* @param uid - User ID
|
|
873
|
+
* @param gid - Group ID
|
|
874
|
+
*/
|
|
875
|
+
async chown(path, uid, gid) {
|
|
876
|
+
await this.initialize();
|
|
877
|
+
const normalized = this.normalizePath(path);
|
|
878
|
+
const file = await this.getFile(normalized);
|
|
879
|
+
if (!file) {
|
|
880
|
+
throw new ENOENT(undefined, normalized);
|
|
881
|
+
}
|
|
882
|
+
this.sql.exec('UPDATE files SET uid = ?, gid = ?, ctime = ? WHERE id = ?', uid, gid, Date.now(), file.id);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Update access and modification times.
|
|
886
|
+
*
|
|
887
|
+
* @param path - File path
|
|
888
|
+
* @param atime - Access time
|
|
889
|
+
* @param mtime - Modification time
|
|
890
|
+
*/
|
|
891
|
+
async utimes(path, atime, mtime) {
|
|
892
|
+
await this.initialize();
|
|
893
|
+
const normalized = this.normalizePath(path);
|
|
894
|
+
const file = await this.getFile(normalized);
|
|
895
|
+
if (!file) {
|
|
896
|
+
throw new ENOENT(undefined, normalized);
|
|
897
|
+
}
|
|
898
|
+
const atimeMs = atime instanceof Date ? atime.getTime() : atime;
|
|
899
|
+
const mtimeMs = mtime instanceof Date ? mtime.getTime() : mtime;
|
|
900
|
+
this.sql.exec('UPDATE files SET atime = ?, mtime = ?, ctime = ? WHERE id = ?', atimeMs, mtimeMs, Date.now(), file.id);
|
|
901
|
+
}
|
|
902
|
+
// ===========================================================================
|
|
903
|
+
// Symbolic Links
|
|
904
|
+
// ===========================================================================
|
|
905
|
+
/**
|
|
906
|
+
* Create a symbolic link.
|
|
907
|
+
*
|
|
908
|
+
* @param target - Target path the symlink points to
|
|
909
|
+
* @param path - Path of the symlink
|
|
910
|
+
*/
|
|
911
|
+
async symlink(target, path) {
|
|
912
|
+
await this.initialize();
|
|
913
|
+
const normalized = this.normalizePath(path);
|
|
914
|
+
const now = Date.now();
|
|
915
|
+
const parentPath = this.getParentPath(normalized);
|
|
916
|
+
const name = this.getFileName(normalized);
|
|
917
|
+
const parent = await this.getFile(parentPath);
|
|
918
|
+
if (!parent) {
|
|
919
|
+
throw new ENOENT(undefined, parentPath);
|
|
920
|
+
}
|
|
921
|
+
const existing = await this.getFile(normalized);
|
|
922
|
+
if (existing) {
|
|
923
|
+
throw new EEXIST(undefined, normalized);
|
|
924
|
+
}
|
|
925
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, link_target, tier, atime, mtime, ctime, birthtime, nlink)
|
|
926
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, normalized, name, parent.id, 'symlink', 0o777, 0, 0, target.length, target, 'hot', now, now, now, now, 1);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Create a hard link.
|
|
930
|
+
*
|
|
931
|
+
* @param existingPath - Path to existing file
|
|
932
|
+
* @param newPath - Path for new link
|
|
933
|
+
*/
|
|
934
|
+
async link(existingPath, newPath) {
|
|
935
|
+
await this.initialize();
|
|
936
|
+
const existingNormalized = this.normalizePath(existingPath);
|
|
937
|
+
const newNormalized = this.normalizePath(newPath);
|
|
938
|
+
const now = Date.now();
|
|
939
|
+
const file = await this.getFile(existingNormalized);
|
|
940
|
+
if (!file) {
|
|
941
|
+
throw new ENOENT(undefined, existingNormalized);
|
|
942
|
+
}
|
|
943
|
+
const existing = await this.getFile(newNormalized);
|
|
944
|
+
if (existing) {
|
|
945
|
+
throw new EEXIST(undefined, newNormalized);
|
|
946
|
+
}
|
|
947
|
+
const parentPath = this.getParentPath(newNormalized);
|
|
948
|
+
const name = this.getFileName(newNormalized);
|
|
949
|
+
const parent = await this.getFile(parentPath);
|
|
950
|
+
if (!parent) {
|
|
951
|
+
throw new ENOENT(undefined, parentPath);
|
|
952
|
+
}
|
|
953
|
+
// Increment nlink
|
|
954
|
+
this.sql.exec('UPDATE files SET nlink = nlink + 1 WHERE id = ?', file.id);
|
|
955
|
+
// Create new entry
|
|
956
|
+
this.sql.exec(`INSERT INTO files (path, name, parent_id, type, mode, uid, gid, size, blob_id, tier, atime, mtime, ctime, birthtime, nlink)
|
|
957
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, newNormalized, name, parent.id, file.type, file.mode, file.uid, file.gid, file.size, file.blob_id, file.tier, now, now, now, now, file.nlink + 1);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Read a symbolic link's target.
|
|
961
|
+
*
|
|
962
|
+
* @param path - Symlink path
|
|
963
|
+
* @returns Target path
|
|
964
|
+
*/
|
|
965
|
+
async readlink(path) {
|
|
966
|
+
await this.initialize();
|
|
967
|
+
const normalized = this.normalizePath(path);
|
|
968
|
+
const file = await this.getFile(normalized);
|
|
969
|
+
if (!file) {
|
|
970
|
+
throw new ENOENT(undefined, normalized);
|
|
971
|
+
}
|
|
972
|
+
if (file.type !== 'symlink' || !file.link_target) {
|
|
973
|
+
throw Object.assign(new Error('invalid argument'), { code: 'EINVAL', path: normalized });
|
|
974
|
+
}
|
|
975
|
+
return file.link_target;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get the real path (resolve symlinks).
|
|
979
|
+
*
|
|
980
|
+
* @param path - Path to resolve
|
|
981
|
+
* @returns Resolved path
|
|
982
|
+
*/
|
|
983
|
+
async realpath(path) {
|
|
984
|
+
await this.initialize();
|
|
985
|
+
const normalized = this.normalizePath(path);
|
|
986
|
+
let file = await this.getFile(normalized);
|
|
987
|
+
if (!file) {
|
|
988
|
+
throw new ENOENT(undefined, normalized);
|
|
989
|
+
}
|
|
990
|
+
// Follow symlinks
|
|
991
|
+
let depth = 0;
|
|
992
|
+
while (file.type === 'symlink' && file.link_target) {
|
|
993
|
+
if (depth++ > 40) {
|
|
994
|
+
throw Object.assign(new Error('too many symbolic links'), { code: 'ELOOP', path: normalized });
|
|
995
|
+
}
|
|
996
|
+
let target = file.link_target;
|
|
997
|
+
if (!target.startsWith('/')) {
|
|
998
|
+
const parentPath = this.getParentPath(file.path);
|
|
999
|
+
target = this.normalizePath(parentPath + '/' + target);
|
|
1000
|
+
}
|
|
1001
|
+
file = await this.getFile(target);
|
|
1002
|
+
if (!file) {
|
|
1003
|
+
throw new ENOENT(undefined, target);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return file.path;
|
|
1007
|
+
}
|
|
1008
|
+
// ===========================================================================
|
|
1009
|
+
// Tiered Storage Operations
|
|
1010
|
+
// ===========================================================================
|
|
1011
|
+
/**
|
|
1012
|
+
* Get the current storage tier of a file.
|
|
1013
|
+
*
|
|
1014
|
+
* @param path - File path
|
|
1015
|
+
* @returns Current tier
|
|
1016
|
+
*/
|
|
1017
|
+
async getTier(path) {
|
|
1018
|
+
await this.initialize();
|
|
1019
|
+
const file = await this.getFile(path);
|
|
1020
|
+
if (!file) {
|
|
1021
|
+
throw new ENOENT(undefined, path);
|
|
1022
|
+
}
|
|
1023
|
+
return file.tier;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Promote a file to a hotter storage tier.
|
|
1027
|
+
*
|
|
1028
|
+
* @param path - File path
|
|
1029
|
+
* @param tier - Target tier ('hot' or 'warm')
|
|
1030
|
+
*/
|
|
1031
|
+
async promote(path, tier) {
|
|
1032
|
+
await this.initialize();
|
|
1033
|
+
const normalized = this.normalizePath(path);
|
|
1034
|
+
const file = await this.getFile(normalized);
|
|
1035
|
+
if (!file) {
|
|
1036
|
+
throw new ENOENT(undefined, normalized);
|
|
1037
|
+
}
|
|
1038
|
+
if (!file.blob_id)
|
|
1039
|
+
return;
|
|
1040
|
+
const currentTier = file.tier;
|
|
1041
|
+
if (currentTier === tier)
|
|
1042
|
+
return;
|
|
1043
|
+
// Read from current tier
|
|
1044
|
+
const data = await this.getBlob(file.blob_id, currentTier);
|
|
1045
|
+
if (!data)
|
|
1046
|
+
return;
|
|
1047
|
+
// Store in new tier
|
|
1048
|
+
const newBlobId = crypto.randomUUID();
|
|
1049
|
+
await this.storeBlob(newBlobId, data, tier);
|
|
1050
|
+
// Delete from old tier
|
|
1051
|
+
await this.deleteBlob(file.blob_id, currentTier);
|
|
1052
|
+
// Update file
|
|
1053
|
+
this.sql.exec('UPDATE files SET blob_id = ?, tier = ? WHERE id = ?', newBlobId, tier, file.id);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Demote a file to a colder storage tier.
|
|
1057
|
+
*
|
|
1058
|
+
* @param path - File path
|
|
1059
|
+
* @param tier - Target tier ('warm' or 'cold')
|
|
1060
|
+
*/
|
|
1061
|
+
async demote(path, tier) {
|
|
1062
|
+
await this.initialize();
|
|
1063
|
+
const normalized = this.normalizePath(path);
|
|
1064
|
+
const file = await this.getFile(normalized);
|
|
1065
|
+
if (!file) {
|
|
1066
|
+
throw new ENOENT(undefined, normalized);
|
|
1067
|
+
}
|
|
1068
|
+
if (!file.blob_id)
|
|
1069
|
+
return;
|
|
1070
|
+
const currentTier = file.tier;
|
|
1071
|
+
if (currentTier === tier)
|
|
1072
|
+
return;
|
|
1073
|
+
// Read from current tier
|
|
1074
|
+
const data = await this.getBlob(file.blob_id, currentTier);
|
|
1075
|
+
if (!data)
|
|
1076
|
+
return;
|
|
1077
|
+
// Store in new tier
|
|
1078
|
+
const newBlobId = crypto.randomUUID();
|
|
1079
|
+
await this.storeBlob(newBlobId, data, tier);
|
|
1080
|
+
// Delete from old tier
|
|
1081
|
+
await this.deleteBlob(file.blob_id, currentTier);
|
|
1082
|
+
// Update file
|
|
1083
|
+
this.sql.exec('UPDATE files SET blob_id = ?, tier = ? WHERE id = ?', newBlobId, tier, file.id);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// ============================================================================
|
|
1087
|
+
// Factory Functions
|
|
1088
|
+
// ============================================================================
|
|
1089
|
+
/**
|
|
1090
|
+
* Create an FsModule instance with the given options.
|
|
1091
|
+
*
|
|
1092
|
+
* @param options - Configuration options for the module
|
|
1093
|
+
* @returns A new FsModule instance
|
|
1094
|
+
*
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```typescript
|
|
1097
|
+
* import { createFsModule } from 'gitx.do/do'
|
|
1098
|
+
*
|
|
1099
|
+
* const fs = createFsModule({
|
|
1100
|
+
* sql: ctx.storage.sql,
|
|
1101
|
+
* r2: env.R2_BUCKET
|
|
1102
|
+
* })
|
|
1103
|
+
* ```
|
|
1104
|
+
*/
|
|
1105
|
+
export function createFsModule(options) {
|
|
1106
|
+
return new FsModule(options);
|
|
1107
|
+
}
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
// Type Guards
|
|
1110
|
+
// ============================================================================
|
|
1111
|
+
/**
|
|
1112
|
+
* Check if a value is an FsModule instance.
|
|
1113
|
+
*
|
|
1114
|
+
* @param value - Value to check
|
|
1115
|
+
* @returns True if value is an FsModule
|
|
1116
|
+
*/
|
|
1117
|
+
export function isFsModule(value) {
|
|
1118
|
+
return value instanceof FsModule;
|
|
1119
|
+
}
|
|
1120
|
+
//# sourceMappingURL=FsModule.js.map
|