@b9g/filesystem 0.1.4 → 0.1.6

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