-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfs.ts
242 lines (200 loc) · 6.31 KB
/
fs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { DurableObject } from "cloudflare:workers";
export interface Env {
VIRTUAL_FS: DurableObjectNamespace;
}
export class VirtualFileSystemDO extends DurableObject {
private sql: SqlStorage;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
// Initialize the SQLite database with our schema
this.sql = state.storage.sql;
// Create our files table
// path: The full path to the file
// type: 'file' or 'directory'
// content: File contents (null for directories)
// created_at: Creation timestamp
// updated_at: Last modification timestamp
this.sql.exec(`
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY,
type TEXT NOT NULL CHECK (type IN ('file', 'directory')),
content BLOB,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Index for faster directory listings
CREATE INDEX IF NOT EXISTS idx_parent_path ON files(path);
`);
}
// File System Operations
async writeFile(path: string, content: string | ArrayBuffer): Promise<void> {
const now = Date.now();
// Ensure parent directory exists
const parentDir = path.split("/").slice(0, -1).join("/");
if (parentDir) {
await this.mkdir(parentDir);
}
// Convert string content to Uint8Array if necessary
const contentBuffer =
typeof content === "string"
? new TextEncoder().encode(content)
: new Uint8Array(content);
await this.sql.exec(
`INSERT INTO files (path, type, content, created_at, updated_at)
VALUES (?, 'file', ?, ?, ?)
ON CONFLICT (path) DO UPDATE SET
content = excluded.content,
updated_at = excluded.updated_at`,
path,
contentBuffer,
now,
now,
);
}
readFile(path: string, encoding?: string): string | ArrayBuffer {
const result = this.sql
.exec("SELECT content FROM files WHERE path = ? AND type = 'file'", [
path,
])
.one();
if (!result) {
throw new Error(`ENOENT: no such file or directory: ${path}`);
}
const content = result.content as ArrayBuffer;
return encoding === "utf8" ? new TextDecoder().decode(content) : content;
}
async unlink(path: string): Promise<void> {
const result = this.sql.exec(
"DELETE FROM files WHERE path = ? AND type = 'file'",
path,
);
result.rowsWritten;
if (result.rowsWritten === 0) {
throw new Error(`ENOENT: no such file or directory: ${path}`);
}
}
async mkdir(path: string): Promise<void> {
const sql = this.sql;
const now = Date.now();
// Ensure parent directory exists
const parentDir = path.split("/").slice(0, -1).join("/");
if (parentDir) {
await this.mkdir(parentDir);
}
// Create the directory (ignore if it already exists)
await sql.exec(
`INSERT OR IGNORE INTO files (path, type, created_at, updated_at)
VALUES (?, 'directory', ?, ?)`,
path,
now,
now,
);
}
async rmdir(path: string): Promise<void> {
// Check if directory is empty
const hasChildren = this.sql
.exec("SELECT 1 FROM files WHERE path LIKE ? || '/%' LIMIT 1", [path])
.one();
if (hasChildren) {
throw new Error(`ENOTEMPTY: directory not empty: ${path}`);
}
const result = await this.sql.exec(
"DELETE FROM files WHERE path = ? AND type = 'directory'",
path,
);
if (result.rowsWritten === 0) {
throw new Error(`ENOENT: no such file or directory: ${path}`);
}
}
async readdir(path: string): Promise<string[]> {
const sql = this.sql;
// First check if directory exists
const dirExists = sql
.exec("SELECT 1 FROM files WHERE path = ? AND type = 'directory'", [path])
.one();
if (!dirExists) {
throw new Error(`ENOENT: no such file or directory: ${path}`);
}
// Get immediate children of the directory
const children = sql
.exec(
`SELECT path FROM files
WHERE path LIKE ? || '/%'
AND path NOT LIKE ? || '/%/%'`,
path,
path,
)
.toArray();
return children.map((row) => {
const parts = (row.path as string).split("/");
return parts[parts.length - 1];
});
}
async stat(path: string): Promise<FileStat> {
const result = this.sql
.exec("SELECT type, created_at, updated_at FROM files WHERE path = ?", [
path,
])
.one();
if (!result) {
throw new Error(`ENOENT: no such file or directory: ${path}`);
}
return {
isFile: () => result.type === "file",
isDirectory: () => result.type === "directory",
created: new Date(result.created_at as string),
modified: new Date(result.updated_at as string),
};
}
}
// Now let's create a friendly API wrapper that handles DO creation and routing
export class VirtualFS {
constructor(private env: any) {}
private async getDOStub(path: string) {
// Use the first directory component as the DO name
const rootDir = path.split("/")[0];
if (!rootDir) {
throw new Error("Invalid path: must start with a root directory");
}
const id = this.env.VIRTUAL_FS.idFromName(rootDir);
return this.env.VIRTUAL_FS.get(id);
}
async writeFile(path: string, content: string | ArrayBuffer): Promise<void> {
const stub = await this.getDOStub(path);
return stub.writeFile(path, content);
}
async readFile(
path: string,
encoding?: string,
): Promise<string | ArrayBuffer> {
const stub = await this.getDOStub(path);
return stub.readFile(path, encoding);
}
async unlink(path: string): Promise<void> {
const stub = await this.getDOStub(path);
return stub.unlink(path);
}
async mkdir(path: string): Promise<void> {
const stub = await this.getDOStub(path);
return stub.mkdir(path);
}
async rmdir(path: string): Promise<void> {
const stub = await this.getDOStub(path);
return stub.rmdir(path);
}
async readdir(path: string): Promise<string[]> {
const stub = await this.getDOStub(path);
return stub.readdir(path);
}
async stat(path: string): Promise<FileStat> {
const stub = await this.getDOStub(path);
return stub.stat(path);
}
}
// Types
interface FileStat {
isFile(): boolean;
isDirectory(): boolean;
created: Date;
modified: Date;
}