@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/node.js CHANGED
@@ -1,266 +1,152 @@
1
1
  /// <reference types="./node.d.ts" />
2
2
  // src/node.ts
3
- import * as fs from "fs/promises";
4
- import * as path from "path";
5
- import { createWriteStream } from "fs";
6
- var NodeFileSystemWritableFileStream = class extends WritableStream {
7
- constructor(filePath) {
8
- const writeStream = createWriteStream(filePath);
9
- super({
10
- write(chunk) {
11
- return new Promise((resolve, reject) => {
12
- writeStream.write(chunk, (error) => {
13
- if (error)
14
- reject(error);
15
- else
16
- resolve();
17
- });
18
- });
19
- },
20
- close() {
21
- return new Promise((resolve, reject) => {
22
- writeStream.end((error) => {
23
- if (error)
24
- reject(error);
25
- else
26
- resolve();
27
- });
28
- });
29
- },
30
- abort() {
31
- writeStream.destroy();
32
- return Promise.resolve();
33
- }
34
- });
35
- this.filePath = filePath;
36
- }
37
- // File System Access API compatibility methods
38
- async write(data) {
39
- const writer = this.getWriter();
3
+ import { ShovelDirectoryHandle } from "./index.js";
4
+ import * as FS from "fs/promises";
5
+ import * as Path from "path";
6
+ var NodeFileSystemBackend = class {
7
+ #rootPath;
8
+ constructor(rootPath) {
9
+ this.#rootPath = rootPath;
10
+ }
11
+ async stat(filePath) {
40
12
  try {
41
- if (typeof data === "string") {
42
- await writer.write(new TextEncoder().encode(data));
13
+ const fullPath = this.#resolvePath(filePath);
14
+ const stats = await FS.stat(fullPath);
15
+ if (stats.isFile()) {
16
+ return { kind: "file" };
17
+ } else if (stats.isDirectory()) {
18
+ return { kind: "directory" };
43
19
  } else {
44
- await writer.write(data);
20
+ return null;
45
21
  }
46
- } finally {
47
- writer.releaseLock();
48
- }
49
- }
50
- async close() {
51
- const writer = this.getWriter();
52
- try {
53
- await writer.close();
54
- } finally {
55
- writer.releaseLock();
56
- }
57
- }
58
- };
59
- var NodeFileSystemFileHandle = class _NodeFileSystemFileHandle {
60
- constructor(filePath) {
61
- this.filePath = filePath;
62
- this.name = path.basename(filePath);
63
- }
64
- kind = "file";
65
- name;
66
- async getFile() {
67
- try {
68
- const stats = await fs.stat(this.filePath);
69
- const buffer = await fs.readFile(this.filePath);
70
- return new File([buffer], this.name, {
71
- lastModified: stats.mtime.getTime(),
72
- // Attempt to determine MIME type from extension
73
- type: this.getMimeType(this.filePath)
74
- });
75
22
  } catch (error) {
76
23
  if (error.code === "ENOENT") {
77
- throw new DOMException("File not found", "NotFoundError");
24
+ return null;
78
25
  }
79
26
  throw error;
80
27
  }
81
28
  }
82
- async createWritable() {
83
- await fs.mkdir(path.dirname(this.filePath), { recursive: true });
84
- return new NodeFileSystemWritableFileStream(this.filePath);
85
- }
86
- async createSyncAccessHandle() {
87
- throw new DOMException(
88
- "Synchronous access handles are only available in workers",
89
- "InvalidStateError"
90
- );
91
- }
92
- async isSameEntry(other) {
93
- if (other.kind !== "file")
94
- return false;
95
- if (!(other instanceof _NodeFileSystemFileHandle))
96
- return false;
97
- return this.filePath === other.filePath;
98
- }
99
- async queryPermission() {
100
- return "granted";
101
- }
102
- async requestPermission() {
103
- return "granted";
104
- }
105
- getMimeType(filePath) {
106
- const ext = path.extname(filePath).toLowerCase();
107
- const mimeTypes = {
108
- ".txt": "text/plain",
109
- ".html": "text/html",
110
- ".css": "text/css",
111
- ".js": "text/javascript",
112
- ".json": "application/json",
113
- ".png": "image/png",
114
- ".jpg": "image/jpeg",
115
- ".jpeg": "image/jpeg",
116
- ".gif": "image/gif",
117
- ".svg": "image/svg+xml",
118
- ".pdf": "application/pdf",
119
- ".zip": "application/zip"
120
- };
121
- return mimeTypes[ext] || "application/octet-stream";
122
- }
123
- };
124
- var NodeFileSystemDirectoryHandle = class _NodeFileSystemDirectoryHandle {
125
- constructor(dirPath) {
126
- this.dirPath = dirPath;
127
- this.name = path.basename(dirPath);
128
- }
129
- kind = "directory";
130
- name;
131
- async getFileHandle(name, options) {
132
- const filePath = path.join(this.dirPath, name);
29
+ async readFile(filePath) {
133
30
  try {
134
- const stats = await fs.stat(filePath);
135
- if (!stats.isFile()) {
136
- throw new DOMException(
137
- "Path exists but is not a file",
138
- "TypeMismatchError"
139
- );
140
- }
31
+ const fullPath = this.#resolvePath(filePath);
32
+ const [buffer, stats] = await Promise.all([
33
+ FS.readFile(fullPath),
34
+ FS.stat(fullPath)
35
+ ]);
36
+ return {
37
+ content: new Uint8Array(buffer),
38
+ lastModified: stats.mtimeMs
39
+ };
141
40
  } catch (error) {
142
41
  if (error.code === "ENOENT") {
143
- if (options?.create) {
144
- await fs.mkdir(this.dirPath, { recursive: true });
145
- await fs.writeFile(filePath, "");
146
- } else {
147
- throw new DOMException("File not found", "NotFoundError");
148
- }
149
- } else {
150
- throw error;
42
+ throw new DOMException("File not found", "NotFoundError");
151
43
  }
44
+ throw error;
152
45
  }
153
- return new NodeFileSystemFileHandle(filePath);
154
46
  }
155
- async getDirectoryHandle(name, options) {
156
- const subDirPath = path.join(this.dirPath, name);
47
+ async writeFile(filePath, data) {
157
48
  try {
158
- const stats = await fs.stat(subDirPath);
159
- if (!stats.isDirectory()) {
160
- throw new DOMException(
161
- "Path exists but is not a directory",
162
- "TypeMismatchError"
163
- );
164
- }
49
+ const fullPath = this.#resolvePath(filePath);
50
+ await FS.mkdir(Path.dirname(fullPath), { recursive: true });
51
+ await FS.writeFile(fullPath, data);
165
52
  } catch (error) {
166
- if (error.code === "ENOENT") {
167
- if (options?.create) {
168
- await fs.mkdir(subDirPath, { recursive: true });
169
- } else {
170
- throw new DOMException("Directory not found", "NotFoundError");
171
- }
172
- } else {
173
- throw error;
174
- }
53
+ throw new DOMException(
54
+ `Failed to write file: ${error}`,
55
+ "InvalidModificationError"
56
+ );
175
57
  }
176
- return new _NodeFileSystemDirectoryHandle(subDirPath);
177
58
  }
178
- async removeEntry(name, options) {
179
- const entryPath = path.join(this.dirPath, name);
59
+ async listDir(dirPath) {
180
60
  try {
181
- const stats = await fs.stat(entryPath);
182
- if (stats.isDirectory()) {
183
- await fs.rmdir(entryPath, { recursive: options?.recursive });
184
- } else {
185
- await fs.unlink(entryPath);
61
+ const fullPath = this.#resolvePath(dirPath);
62
+ const entries = await FS.readdir(fullPath, { withFileTypes: true });
63
+ const results = [];
64
+ for (const entry of entries) {
65
+ if (entry.isFile()) {
66
+ results.push({ name: entry.name, kind: "file" });
67
+ } else if (entry.isDirectory()) {
68
+ results.push({ name: entry.name, kind: "directory" });
69
+ }
186
70
  }
71
+ return results;
187
72
  } catch (error) {
188
73
  if (error.code === "ENOENT") {
189
- throw new DOMException("Entry not found", "NotFoundError");
74
+ throw new DOMException("Directory not found", "NotFoundError");
190
75
  }
191
76
  throw error;
192
77
  }
193
78
  }
194
- async resolve(_possibleDescendant) {
195
- return null;
79
+ async createDir(dirPath) {
80
+ try {
81
+ const fullPath = this.#resolvePath(dirPath);
82
+ await FS.mkdir(fullPath, { recursive: true });
83
+ } catch (error) {
84
+ throw new DOMException(
85
+ `Failed to create directory: ${error}`,
86
+ "InvalidModificationError"
87
+ );
88
+ }
196
89
  }
197
- async *entries() {
90
+ async remove(entryPath, recursive) {
198
91
  try {
199
- const entries = await fs.readdir(this.dirPath, { withFileTypes: true });
200
- for (const entry of entries) {
201
- const entryPath = path.join(this.dirPath, entry.name);
202
- if (entry.isDirectory()) {
203
- yield [entry.name, new _NodeFileSystemDirectoryHandle(entryPath)];
204
- } else if (entry.isFile()) {
205
- yield [entry.name, new NodeFileSystemFileHandle(entryPath)];
92
+ const fullPath = this.#resolvePath(entryPath);
93
+ const stats = await FS.stat(fullPath);
94
+ if (stats.isFile()) {
95
+ await FS.unlink(fullPath);
96
+ } else if (stats.isDirectory()) {
97
+ if (recursive) {
98
+ await FS.rm(fullPath, { recursive: true, force: true });
99
+ } else {
100
+ const entries = await FS.readdir(fullPath);
101
+ if (entries.length > 0) {
102
+ throw new DOMException(
103
+ "Directory is not empty",
104
+ "InvalidModificationError"
105
+ );
106
+ }
107
+ await FS.rmdir(fullPath);
206
108
  }
207
109
  }
208
110
  } catch (error) {
209
111
  if (error.code === "ENOENT") {
210
- throw new DOMException("Directory not found", "NotFoundError");
112
+ throw new DOMException("Entry not found", "NotFoundError");
211
113
  }
212
114
  throw error;
213
115
  }
214
116
  }
215
- async *keys() {
216
- for await (const [name] of this.entries()) {
217
- yield name;
117
+ #resolvePath(relativePath) {
118
+ const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
119
+ if (!cleanPath) {
120
+ return this.#rootPath;
218
121
  }
219
- }
220
- async *values() {
221
- for await (const [, handle] of this.entries()) {
222
- yield handle;
122
+ if (cleanPath.includes("..") || cleanPath.includes("\0")) {
123
+ throw new DOMException(
124
+ "Invalid path: contains path traversal or null bytes",
125
+ "NotAllowedError"
126
+ );
223
127
  }
224
- }
225
- async isSameEntry(other) {
226
- if (other.kind !== "directory")
227
- return false;
228
- if (!(other instanceof _NodeFileSystemDirectoryHandle))
229
- return false;
230
- return this.dirPath === other.dirPath;
231
- }
232
- async queryPermission() {
233
- return "granted";
234
- }
235
- async requestPermission() {
236
- return "granted";
128
+ const resolvedPath = Path.resolve(this.#rootPath, cleanPath);
129
+ if (!resolvedPath.startsWith(Path.resolve(this.#rootPath))) {
130
+ throw new DOMException(
131
+ "Invalid path: outside of root directory",
132
+ "NotAllowedError"
133
+ );
134
+ }
135
+ return resolvedPath;
237
136
  }
238
137
  };
239
- var LocalBucket = class {
240
- config;
241
- rootPath;
242
- constructor(config = {}) {
243
- this.config = {
244
- name: "node",
245
- ...config
246
- };
247
- this.rootPath = config.rootPath || path.join(process.cwd(), "dist");
248
- }
249
- async getDirectoryHandle(name) {
250
- const dirPath = name ? path.join(this.rootPath, name) : this.rootPath;
251
- try {
252
- await fs.mkdir(dirPath, { recursive: true });
253
- } catch (error) {
254
- }
255
- return new NodeFileSystemDirectoryHandle(dirPath);
138
+ var NodeBucket = class extends ShovelDirectoryHandle {
139
+ #rootPath;
140
+ constructor(rootPath) {
141
+ super(new NodeFileSystemBackend(rootPath), "/");
142
+ this.#rootPath = rootPath;
256
143
  }
257
- getConfig() {
258
- return { ...this.config };
144
+ // Override name to use the directory basename instead of "/"
145
+ get name() {
146
+ return Path.basename(this.#rootPath) || "root";
259
147
  }
260
148
  };
261
149
  export {
262
- LocalBucket,
263
- NodeFileSystemDirectoryHandle,
264
- NodeFileSystemFileHandle,
265
- NodeFileSystemWritableFileStream
150
+ NodeBucket,
151
+ NodeFileSystemBackend
266
152
  };
@@ -1,50 +0,0 @@
1
- import type { Bucket } from "./types.js";
2
- /**
3
- * BucketStorage implements bucket storage with a configurable factory
4
- * The factory function receives the bucket name and can return different filesystem types
5
- * This mirrors the CustomCacheStorage pattern for consistency
6
- *
7
- * Example usage:
8
- * ```typescript
9
- * const buckets = new BucketStorage((name) => {
10
- * if (name === 'uploads') return new S3Bucket('my-bucket');
11
- * if (name === 'temp') return new LocalBucket('/tmp');
12
- * return new LocalBucket('./dist'); // Default to dist
13
- * });
14
- * ```
15
- */
16
- export declare class BucketStorage {
17
- private factory;
18
- private instances;
19
- constructor(factory: (name: string) => Bucket | Promise<Bucket>);
20
- /**
21
- * Opens a bucket with the given name
22
- * Returns existing instance if already opened, otherwise creates a new one
23
- */
24
- open(name: string): Promise<FileSystemDirectoryHandle>;
25
- /**
26
- * Returns true if a bucket with the given name exists (has been opened)
27
- */
28
- has(name: string): Promise<boolean>;
29
- /**
30
- * Deletes a bucket with the given name
31
- * Disposes of the instance if it exists
32
- */
33
- delete(name: string): Promise<boolean>;
34
- /**
35
- * Returns a list of all opened bucket names
36
- */
37
- keys(): Promise<string[]>;
38
- /**
39
- * Get statistics about the bucket storage
40
- */
41
- getStats(): {
42
- openInstances: number;
43
- bucketNames: string[];
44
- };
45
- /**
46
- * Dispose of all open adapter instances
47
- * Useful for cleanup during shutdown
48
- */
49
- dispose(): Promise<void>;
50
- }
@@ -1,74 +0,0 @@
1
- /// <reference types="./directory-storage.d.ts" />
2
- // src/directory-storage.ts
3
- var BucketStorage = class {
4
- constructor(factory) {
5
- this.factory = factory;
6
- }
7
- instances = /* @__PURE__ */ new Map();
8
- /**
9
- * Opens a bucket with the given name
10
- * Returns existing instance if already opened, otherwise creates a new one
11
- */
12
- async open(name) {
13
- const existingInstance = this.instances.get(name);
14
- if (existingInstance) {
15
- return await existingInstance.getDirectoryHandle("");
16
- }
17
- const adapter = await this.factory(name);
18
- this.instances.set(name, adapter);
19
- return await adapter.getDirectoryHandle("");
20
- }
21
- /**
22
- * Returns true if a bucket with the given name exists (has been opened)
23
- */
24
- async has(name) {
25
- return this.instances.has(name);
26
- }
27
- /**
28
- * Deletes a bucket with the given name
29
- * Disposes of the instance if it exists
30
- */
31
- async delete(name) {
32
- const instance = this.instances.get(name);
33
- if (instance) {
34
- if (instance.dispose) {
35
- await instance.dispose();
36
- }
37
- this.instances.delete(name);
38
- return true;
39
- }
40
- return false;
41
- }
42
- /**
43
- * Returns a list of all opened bucket names
44
- */
45
- async keys() {
46
- return Array.from(this.instances.keys());
47
- }
48
- /**
49
- * Get statistics about the bucket storage
50
- */
51
- getStats() {
52
- return {
53
- openInstances: this.instances.size,
54
- bucketNames: Array.from(this.instances.keys())
55
- };
56
- }
57
- /**
58
- * Dispose of all open adapter instances
59
- * Useful for cleanup during shutdown
60
- */
61
- async dispose() {
62
- const disposePromises = [];
63
- for (const [_name, instance] of this.instances) {
64
- if (instance.dispose) {
65
- disposePromises.push(instance.dispose());
66
- }
67
- }
68
- await Promise.all(disposePromises);
69
- this.instances.clear();
70
- }
71
- };
72
- export {
73
- BucketStorage
74
- };
package/src/registry.d.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * Filesystem adapter registry
3
- * Manages registration and retrieval of filesystem adapters
4
- */
5
- import type { FileSystemAdapter } from "./types.js";
6
- /**
7
- * Global registry of filesystem adapters
8
- */
9
- declare class Registry {
10
- private adapters;
11
- private defaultAdapter;
12
- constructor();
13
- /**
14
- * Register a filesystem adapter with a name
15
- */
16
- register(name: string, adapter: FileSystemAdapter): void;
17
- /**
18
- * Get a filesystem adapter by name
19
- */
20
- get(name?: string): FileSystemAdapter | null;
21
- /**
22
- * Set the default filesystem adapter
23
- */
24
- setDefault(adapter: FileSystemAdapter): void;
25
- /**
26
- * Get all registered adapter names
27
- */
28
- getAdapterNames(): string[];
29
- /**
30
- * Clear all registered adapters
31
- */
32
- clear(): void;
33
- }
34
- /**
35
- * Global filesystem registry instance
36
- */
37
- export declare const FileSystemRegistry: Registry;
38
- /**
39
- * Get a file system directory handle using the registered adapters
40
- * @param name Directory name. Use "" for root directory
41
- * @param adapterName Optional adapter name (uses default if not specified)
42
- */
43
- export declare function getDirectoryHandle(name: string, adapterName?: string): Promise<FileSystemDirectoryHandle>;
44
- /**
45
- * @deprecated Use getDirectoryHandle() instead
46
- */
47
- export declare function getBucket(name?: string, adapterName?: string): Promise<FileSystemDirectoryHandle>;
48
- /**
49
- * @deprecated Use getDirectoryHandle() instead
50
- */
51
- export declare function getFileSystemRoot(name?: string): Promise<FileSystemDirectoryHandle>;
52
- export {};
package/src/registry.js DELETED
@@ -1,79 +0,0 @@
1
- /// <reference types="./registry.d.ts" />
2
- // src/registry.ts
3
- import { MemoryBucket } from "./memory.js";
4
- var Registry = class {
5
- adapters = /* @__PURE__ */ new Map();
6
- defaultAdapter;
7
- constructor() {
8
- this.defaultAdapter = new MemoryBucket();
9
- }
10
- /**
11
- * Register a filesystem adapter with a name
12
- */
13
- register(name, adapter) {
14
- this.adapters.set(name, adapter);
15
- if (!this.defaultAdapter) {
16
- this.defaultAdapter = adapter;
17
- }
18
- }
19
- /**
20
- * Get a filesystem adapter by name
21
- */
22
- get(name) {
23
- if (!name) {
24
- return this.defaultAdapter;
25
- }
26
- return this.adapters.get(name) || this.defaultAdapter;
27
- }
28
- /**
29
- * Set the default filesystem adapter
30
- */
31
- setDefault(adapter) {
32
- this.defaultAdapter = adapter;
33
- }
34
- /**
35
- * Get all registered adapter names
36
- */
37
- getAdapterNames() {
38
- return Array.from(this.adapters.keys());
39
- }
40
- /**
41
- * Clear all registered adapters
42
- */
43
- clear() {
44
- this.adapters.clear();
45
- this.defaultAdapter = null;
46
- }
47
- };
48
- var FileSystemRegistry = new Registry();
49
- async function getDirectoryHandle(name, adapterName) {
50
- const adapter = FileSystemRegistry.get(adapterName);
51
- if (!adapter) {
52
- if (adapterName) {
53
- throw new Error(`No filesystem adapter registered with name: ${adapterName}`);
54
- } else {
55
- throw new Error("No default filesystem adapter registered");
56
- }
57
- }
58
- return await adapter.getDirectoryHandle(name);
59
- }
60
- async function getBucket(name, adapterName) {
61
- const adapter = FileSystemRegistry.get(adapterName);
62
- if (!adapter) {
63
- throw new Error("No default filesystem adapter registered");
64
- }
65
- return await adapter.getDirectoryHandle(name || "");
66
- }
67
- async function getFileSystemRoot(name) {
68
- const adapter = FileSystemRegistry.get();
69
- if (!adapter) {
70
- throw new Error("No default filesystem adapter registered");
71
- }
72
- return await adapter.getDirectoryHandle(name || "");
73
- }
74
- export {
75
- FileSystemRegistry,
76
- getBucket,
77
- getDirectoryHandle,
78
- getFileSystemRoot
79
- };
package/src/types.d.ts DELETED
@@ -1,31 +0,0 @@
1
- /**
2
- * Core filesystem adapter interface and types
3
- */
4
- /**
5
- * Configuration for filesystem adapters
6
- */
7
- export interface FileSystemConfig {
8
- /** Human readable name for this filesystem */
9
- name?: string;
10
- /** Platform-specific configuration */
11
- [key: string]: any;
12
- }
13
- /**
14
- * Core interface that all buckets must implement
15
- * Provides File System Access API compatibility across all platforms
16
- */
17
- export interface Bucket {
18
- /**
19
- * Get a directory handle for the bucket
20
- * @param name Directory name. Use "" for root directory
21
- */
22
- getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
23
- /**
24
- * Get configuration for this bucket
25
- */
26
- getConfig(): FileSystemConfig;
27
- /**
28
- * Cleanup resources when bucket is no longer needed
29
- */
30
- dispose?(): Promise<void>;
31
- }
package/src/types.js DELETED
@@ -1 +0,0 @@
1
- /// <reference types="./types.d.ts" />