@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/src/index.js CHANGED
@@ -1,21 +1,339 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
- import { MemoryBucket } from "./memory.js";
4
- import { LocalBucket, NodeFileSystemDirectoryHandle, NodeFileSystemFileHandle } from "./node.js";
5
- import { S3Bucket, BunS3FileSystemDirectoryHandle, BunS3FileSystemFileHandle } from "./bun-s3.js";
6
- import { BucketStorage } from "./directory-storage.js";
7
- import { FileSystemRegistry, getDirectoryHandle, getBucket, getFileSystemRoot } from "./registry.js";
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
- BucketStorage,
10
- BunS3FileSystemDirectoryHandle,
11
- BunS3FileSystemFileHandle,
12
- FileSystemRegistry,
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 of File System Access API
2
+ * In-memory filesystem implementation
3
3
  *
4
- * Provides a complete filesystem interface using in-memory data structures.
5
- * Useful for testing, development, and temporary storage scenarios.
4
+ * Provides MemoryBucket (root) and MemoryFileSystemBackend for storage operations
5
+ * using in-memory data structures.
6
6
  */
7
- import type { Bucket, FileSystemConfig } from "./types.js";
7
+ import { type FileSystemBackend } from "./index.js";
8
8
  /**
9
- * Memory bucket
9
+ * In-memory file data
10
10
  */
11
- export declare class MemoryBucket implements Bucket {
12
- private config;
13
- private filesystems;
14
- constructor(config?: FileSystemConfig);
15
- getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
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 {};