@b9g/filesystem 0.1.4 → 0.1.5
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 +35 -22
- package/package.json +2 -27
- package/src/bun-s3.d.ts +48 -47
- package/src/bun-s3.js +212 -185
- package/src/index.d.ts +212 -6
- package/src/index.js +335 -17
- package/src/memory.d.ts +70 -12
- package/src/memory.js +189 -171
- package/src/node.d.ts +25 -58
- package/src/node.js +100 -218
- package/src/directory-storage.d.ts +0 -50
- package/src/directory-storage.js +0 -74
- package/src/registry.d.ts +0 -52
- package/src/registry.js +0 -79
- package/src/types.d.ts +0 -31
- package/src/types.js +0 -1
package/src/index.js
CHANGED
|
@@ -1,21 +1,339 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
var ShovelWritableFileStream = class extends WritableStream {
|
|
4
|
+
#chunks;
|
|
5
|
+
#backend;
|
|
6
|
+
#path;
|
|
7
|
+
constructor(backend, path) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
super({
|
|
10
|
+
write: (chunk) => {
|
|
11
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
12
|
+
chunks.push(bytes);
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
},
|
|
15
|
+
close: async () => {
|
|
16
|
+
const totalLength = chunks.reduce(
|
|
17
|
+
(sum, chunk) => sum + chunk.length,
|
|
18
|
+
0
|
|
19
|
+
);
|
|
20
|
+
const content = new Uint8Array(totalLength);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) {
|
|
23
|
+
content.set(chunk, offset);
|
|
24
|
+
offset += chunk.length;
|
|
25
|
+
}
|
|
26
|
+
await backend.writeFile(path, content);
|
|
27
|
+
},
|
|
28
|
+
abort: () => {
|
|
29
|
+
chunks.length = 0;
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.#chunks = chunks;
|
|
34
|
+
this.#backend = backend;
|
|
35
|
+
this.#path = path;
|
|
36
|
+
}
|
|
37
|
+
// File System Access API write method
|
|
38
|
+
async write(data) {
|
|
39
|
+
const writer = this.getWriter();
|
|
40
|
+
try {
|
|
41
|
+
if (typeof data === "string") {
|
|
42
|
+
await writer.write(new TextEncoder().encode(data));
|
|
43
|
+
} else if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
|
|
44
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
45
|
+
await writer.write(bytes);
|
|
46
|
+
} else {
|
|
47
|
+
await writer.write(new TextEncoder().encode(String(data)));
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
writer.releaseLock();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// File System Access API seek method
|
|
54
|
+
async seek(_position) {
|
|
55
|
+
throw new DOMException("Seek operation not supported", "NotSupportedError");
|
|
56
|
+
}
|
|
57
|
+
// File System Access API truncate method
|
|
58
|
+
async truncate(_size) {
|
|
59
|
+
throw new DOMException(
|
|
60
|
+
"Truncate operation not supported",
|
|
61
|
+
"NotSupportedError"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var ShovelHandle = class _ShovelHandle {
|
|
66
|
+
path;
|
|
67
|
+
#backend;
|
|
68
|
+
constructor(backend, path) {
|
|
69
|
+
this.#backend = backend;
|
|
70
|
+
this.path = path;
|
|
71
|
+
}
|
|
72
|
+
// Use getter so subclasses can override
|
|
73
|
+
get name() {
|
|
74
|
+
return this.path.split("/").filter(Boolean).pop() || "root";
|
|
75
|
+
}
|
|
76
|
+
get backend() {
|
|
77
|
+
return this.#backend;
|
|
78
|
+
}
|
|
79
|
+
async isSameEntry(other) {
|
|
80
|
+
if (other.kind !== this.kind)
|
|
81
|
+
return false;
|
|
82
|
+
if (!(other instanceof _ShovelHandle))
|
|
83
|
+
return false;
|
|
84
|
+
return this.path === other.path;
|
|
85
|
+
}
|
|
86
|
+
async queryPermission(descriptor) {
|
|
87
|
+
const _mode = descriptor?.mode || "read";
|
|
88
|
+
return "granted";
|
|
89
|
+
}
|
|
90
|
+
async requestPermission(descriptor) {
|
|
91
|
+
const _mode = descriptor?.mode || "read";
|
|
92
|
+
return "granted";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validates that a name is actually a name and not a path
|
|
96
|
+
* The File System Access API only accepts names, not paths
|
|
97
|
+
*/
|
|
98
|
+
validateName(name) {
|
|
99
|
+
if (!name || name.trim() === "") {
|
|
100
|
+
throw new DOMException("Name cannot be empty", "NotAllowedError");
|
|
101
|
+
}
|
|
102
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
103
|
+
throw new DOMException(
|
|
104
|
+
"Name cannot contain path separators",
|
|
105
|
+
"NotAllowedError"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (name === "." || name === "..") {
|
|
109
|
+
throw new DOMException("Name cannot be '.' or '..'", "NotAllowedError");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var ShovelFileHandle = class extends ShovelHandle {
|
|
114
|
+
kind;
|
|
115
|
+
constructor(backend, path) {
|
|
116
|
+
super(backend, path);
|
|
117
|
+
this.kind = "file";
|
|
118
|
+
}
|
|
119
|
+
async getFile() {
|
|
120
|
+
try {
|
|
121
|
+
const content = await this.backend.readFile(this.path);
|
|
122
|
+
const filename = this.name;
|
|
123
|
+
const mimeType = this.#getMimeType(filename);
|
|
124
|
+
const buffer = content.slice().buffer;
|
|
125
|
+
return new File([buffer], filename, {
|
|
126
|
+
type: mimeType,
|
|
127
|
+
lastModified: Date.now()
|
|
128
|
+
// TODO: Could be stored in backend if needed
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new DOMException(`File not found: ${this.path}`, "NotFoundError");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async createWritable() {
|
|
135
|
+
return new ShovelWritableFileStream(this.backend, this.path);
|
|
136
|
+
}
|
|
137
|
+
async createSyncAccessHandle() {
|
|
138
|
+
throw new DOMException(
|
|
139
|
+
"Synchronous access handles are not supported",
|
|
140
|
+
"InvalidStateError"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
#getMimeType(filename) {
|
|
144
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
|
|
145
|
+
const mimeTypes = {
|
|
146
|
+
".html": "text/html",
|
|
147
|
+
".css": "text/css",
|
|
148
|
+
".js": "application/javascript",
|
|
149
|
+
".json": "application/json",
|
|
150
|
+
".png": "image/png",
|
|
151
|
+
".jpg": "image/jpeg",
|
|
152
|
+
".jpeg": "image/jpeg",
|
|
153
|
+
".gif": "image/gif",
|
|
154
|
+
".svg": "image/svg+xml",
|
|
155
|
+
".pdf": "application/pdf",
|
|
156
|
+
".zip": "application/zip"
|
|
157
|
+
};
|
|
158
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var ShovelDirectoryHandle = class _ShovelDirectoryHandle extends ShovelHandle {
|
|
162
|
+
kind;
|
|
163
|
+
constructor(backend, path) {
|
|
164
|
+
super(backend, path);
|
|
165
|
+
this.kind = "directory";
|
|
166
|
+
}
|
|
167
|
+
async getFileHandle(name, options) {
|
|
168
|
+
this.validateName(name);
|
|
169
|
+
const filePath = this.#joinPath(this.path, name);
|
|
170
|
+
const stat = await this.backend.stat(filePath);
|
|
171
|
+
if (!stat && options?.create) {
|
|
172
|
+
await this.backend.writeFile(filePath, new Uint8Array(0));
|
|
173
|
+
} else if (!stat) {
|
|
174
|
+
throw new DOMException("File not found", "NotFoundError");
|
|
175
|
+
} else if (stat.kind !== "file") {
|
|
176
|
+
throw new DOMException(
|
|
177
|
+
"Path exists but is not a file",
|
|
178
|
+
"TypeMismatchError"
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return new ShovelFileHandle(this.backend, filePath);
|
|
182
|
+
}
|
|
183
|
+
async getDirectoryHandle(name, options) {
|
|
184
|
+
this.validateName(name);
|
|
185
|
+
const dirPath = this.#joinPath(this.path, name);
|
|
186
|
+
const stat = await this.backend.stat(dirPath);
|
|
187
|
+
if (!stat && options?.create) {
|
|
188
|
+
if (this.backend.createDir) {
|
|
189
|
+
await this.backend.createDir(dirPath);
|
|
190
|
+
}
|
|
191
|
+
} else if (!stat) {
|
|
192
|
+
throw new DOMException("Directory not found", "NotFoundError");
|
|
193
|
+
} else if (stat.kind !== "directory") {
|
|
194
|
+
throw new DOMException(
|
|
195
|
+
"Path exists but is not a directory",
|
|
196
|
+
"TypeMismatchError"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return new _ShovelDirectoryHandle(this.backend, dirPath);
|
|
200
|
+
}
|
|
201
|
+
async removeEntry(name, options) {
|
|
202
|
+
this.validateName(name);
|
|
203
|
+
if (!this.backend.remove) {
|
|
204
|
+
throw new DOMException(
|
|
205
|
+
"Remove operation not supported by this backend",
|
|
206
|
+
"NotSupportedError"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const entryPath = this.#joinPath(this.path, name);
|
|
210
|
+
const stat = await this.backend.stat(entryPath);
|
|
211
|
+
if (!stat) {
|
|
212
|
+
throw new DOMException("Entry not found", "NotFoundError");
|
|
213
|
+
}
|
|
214
|
+
await this.backend.remove(entryPath, options?.recursive);
|
|
215
|
+
}
|
|
216
|
+
async resolve(possibleDescendant) {
|
|
217
|
+
if (!(possibleDescendant instanceof _ShovelDirectoryHandle || possibleDescendant instanceof ShovelFileHandle)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const shovelHandle = possibleDescendant;
|
|
221
|
+
const descendantPath = shovelHandle.path;
|
|
222
|
+
if (!descendantPath.startsWith(this.path)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const relativePath = descendantPath.slice(this.path.length);
|
|
226
|
+
return relativePath.split("/").filter(Boolean);
|
|
227
|
+
}
|
|
228
|
+
async *entries() {
|
|
229
|
+
try {
|
|
230
|
+
const entries = await this.backend.listDir(this.path);
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
const entryPath = this.#joinPath(this.path, entry.name);
|
|
233
|
+
if (entry.kind === "file") {
|
|
234
|
+
yield [entry.name, new ShovelFileHandle(this.backend, entryPath)];
|
|
235
|
+
} else {
|
|
236
|
+
yield [
|
|
237
|
+
entry.name,
|
|
238
|
+
new _ShovelDirectoryHandle(this.backend, entryPath)
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async *keys() {
|
|
247
|
+
for await (const [name] of this.entries()) {
|
|
248
|
+
yield name;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async *values() {
|
|
252
|
+
for await (const [, handle] of this.entries()) {
|
|
253
|
+
yield handle;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
[Symbol.asyncIterator]() {
|
|
257
|
+
return this.entries();
|
|
258
|
+
}
|
|
259
|
+
#joinPath(base, name) {
|
|
260
|
+
if (base === "/" || base === "") {
|
|
261
|
+
return `/${name}`;
|
|
262
|
+
}
|
|
263
|
+
return `${base}/${name}`;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var CustomBucketStorage = class {
|
|
267
|
+
#instances;
|
|
268
|
+
#factory;
|
|
269
|
+
/**
|
|
270
|
+
* @param factory Function that creates bucket instances by name
|
|
271
|
+
*/
|
|
272
|
+
constructor(factory) {
|
|
273
|
+
this.#instances = /* @__PURE__ */ new Map();
|
|
274
|
+
this.#factory = factory;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Open a named bucket - creates if it doesn't exist
|
|
278
|
+
*
|
|
279
|
+
* @param name Bucket name (e.g., 'tmp', 'dist', 'uploads')
|
|
280
|
+
* @returns FileSystemDirectoryHandle for the bucket
|
|
281
|
+
*/
|
|
282
|
+
async open(name) {
|
|
283
|
+
const existing = this.#instances.get(name);
|
|
284
|
+
if (existing) {
|
|
285
|
+
return existing;
|
|
286
|
+
}
|
|
287
|
+
const bucket = await this.#factory(name);
|
|
288
|
+
this.#instances.set(name, bucket);
|
|
289
|
+
return bucket;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Check if a named bucket exists
|
|
293
|
+
*
|
|
294
|
+
* @param name Bucket name to check
|
|
295
|
+
* @returns true if bucket has been opened
|
|
296
|
+
*/
|
|
297
|
+
async has(name) {
|
|
298
|
+
return this.#instances.has(name);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Delete a named bucket
|
|
302
|
+
*
|
|
303
|
+
* @param name Bucket name to delete
|
|
304
|
+
* @returns true if bucket was deleted, false if it didn't exist
|
|
305
|
+
*/
|
|
306
|
+
async delete(name) {
|
|
307
|
+
const instance = this.#instances.get(name);
|
|
308
|
+
if (instance) {
|
|
309
|
+
this.#instances.delete(name);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* List all opened bucket names
|
|
316
|
+
*
|
|
317
|
+
* @returns Array of bucket names
|
|
318
|
+
*/
|
|
319
|
+
async keys() {
|
|
320
|
+
return Array.from(this.#instances.keys());
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get statistics about opened buckets (non-standard utility method)
|
|
324
|
+
*
|
|
325
|
+
* @returns Object with bucket statistics
|
|
326
|
+
*/
|
|
327
|
+
getStats() {
|
|
328
|
+
return {
|
|
329
|
+
openInstances: this.#instances.size,
|
|
330
|
+
bucketNames: Array.from(this.#instances.keys())
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
8
334
|
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
LocalBucket,
|
|
14
|
-
MemoryBucket,
|
|
15
|
-
NodeFileSystemDirectoryHandle,
|
|
16
|
-
NodeFileSystemFileHandle,
|
|
17
|
-
S3Bucket,
|
|
18
|
-
getBucket,
|
|
19
|
-
getDirectoryHandle,
|
|
20
|
-
getFileSystemRoot
|
|
335
|
+
CustomBucketStorage,
|
|
336
|
+
ShovelDirectoryHandle,
|
|
337
|
+
ShovelFileHandle,
|
|
338
|
+
ShovelHandle
|
|
21
339
|
};
|
package/src/memory.d.ts
CHANGED
|
@@ -1,18 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory implementation
|
|
2
|
+
* In-memory filesystem implementation
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
4
|
+
* Provides MemoryBucket (root) and MemoryFileSystemBackend for storage operations
|
|
5
|
+
* using in-memory data structures.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { type FileSystemBackend } from "./index.js";
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* In-memory file data
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
getConfig(): FileSystemConfig;
|
|
17
|
-
dispose(): Promise<void>;
|
|
11
|
+
interface MemoryFile {
|
|
12
|
+
name: string;
|
|
13
|
+
content: Uint8Array;
|
|
14
|
+
lastModified: number;
|
|
15
|
+
type: string;
|
|
18
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* In-memory directory data
|
|
19
|
+
*/
|
|
20
|
+
interface MemoryDirectory {
|
|
21
|
+
name: string;
|
|
22
|
+
files: Map<string, MemoryFile>;
|
|
23
|
+
directories: Map<string, MemoryDirectory>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* In-memory storage backend that implements FileSystemBackend
|
|
27
|
+
*/
|
|
28
|
+
export declare class MemoryFileSystemBackend implements FileSystemBackend {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(root: MemoryDirectory);
|
|
31
|
+
stat(path: string): Promise<{
|
|
32
|
+
kind: "file" | "directory";
|
|
33
|
+
} | null>;
|
|
34
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
35
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
36
|
+
listDir(path: string): Promise<Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
kind: "file" | "directory";
|
|
39
|
+
}>>;
|
|
40
|
+
createDir(path: string): Promise<void>;
|
|
41
|
+
remove(path: string, recursive?: boolean): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Memory bucket - root entry point for in-memory filesystem
|
|
45
|
+
* Implements FileSystemDirectoryHandle and owns the root data structure
|
|
46
|
+
*/
|
|
47
|
+
export declare class MemoryBucket implements FileSystemDirectoryHandle {
|
|
48
|
+
#private;
|
|
49
|
+
readonly kind: "directory";
|
|
50
|
+
readonly name: string;
|
|
51
|
+
constructor(name?: string);
|
|
52
|
+
getFileHandle(name: string, options?: {
|
|
53
|
+
create?: boolean;
|
|
54
|
+
}): Promise<FileSystemFileHandle>;
|
|
55
|
+
getDirectoryHandle(name: string, options?: {
|
|
56
|
+
create?: boolean;
|
|
57
|
+
}): Promise<FileSystemDirectoryHandle>;
|
|
58
|
+
removeEntry(name: string, options?: {
|
|
59
|
+
recursive?: boolean;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>;
|
|
62
|
+
entries(): AsyncIterableIterator<[
|
|
63
|
+
string,
|
|
64
|
+
FileSystemFileHandle | FileSystemDirectoryHandle
|
|
65
|
+
]>;
|
|
66
|
+
keys(): AsyncIterableIterator<string>;
|
|
67
|
+
values(): AsyncIterableIterator<FileSystemFileHandle | FileSystemDirectoryHandle>;
|
|
68
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<[
|
|
69
|
+
string,
|
|
70
|
+
FileSystemFileHandle | FileSystemDirectoryHandle
|
|
71
|
+
]>;
|
|
72
|
+
isSameEntry(other: FileSystemHandle): Promise<boolean>;
|
|
73
|
+
queryPermission(): Promise<PermissionState>;
|
|
74
|
+
requestPermission(): Promise<PermissionState>;
|
|
75
|
+
}
|
|
76
|
+
export {};
|