@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/memory.js CHANGED
@@ -1,187 +1,236 @@
1
1
  /// <reference types="./memory.d.ts" />
2
2
  // src/memory.ts
3
- var MemoryFileSystemWritableFileStream = class extends WritableStream {
4
- constructor(onClose) {
5
- super({
6
- write: (chunk) => {
7
- this.chunks.push(chunk);
8
- return Promise.resolve();
9
- },
10
- close: () => {
11
- const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
12
- const content = new Uint8Array(totalLength);
13
- let offset = 0;
14
- for (const chunk of this.chunks) {
15
- content.set(chunk, offset);
16
- offset += chunk.length;
17
- }
18
- this.onClose(content);
19
- return Promise.resolve();
20
- },
21
- abort: () => {
22
- this.chunks = [];
23
- return Promise.resolve();
24
- }
25
- });
26
- this.onClose = onClose;
27
- }
28
- chunks = [];
29
- };
30
- var MemoryFileSystemFileHandle = class _MemoryFileSystemFileHandle {
31
- constructor(file, updateFile) {
32
- this.file = file;
33
- this.updateFile = updateFile;
34
- this.name = file.name;
35
- }
36
- kind = "file";
37
- name;
38
- async getFile() {
39
- return new File([this.file.content], this.file.name, {
40
- lastModified: this.file.lastModified,
41
- type: this.file.type
42
- });
43
- }
44
- async createWritable() {
45
- return new MemoryFileSystemWritableFileStream((content) => {
46
- this.updateFile(content);
47
- });
48
- }
49
- async createSyncAccessHandle() {
50
- throw new DOMException(
51
- "Synchronous access handles are not supported in memory filesystem",
52
- "InvalidStateError"
53
- );
54
- }
55
- async isSameEntry(other) {
56
- if (other.kind !== "file")
57
- return false;
58
- if (!(other instanceof _MemoryFileSystemFileHandle))
59
- return false;
60
- return this.file === other.file;
61
- }
62
- async queryPermission() {
63
- return "granted";
64
- }
65
- async requestPermission() {
66
- return "granted";
3
+ import {
4
+ ShovelDirectoryHandle,
5
+ ShovelFileHandle
6
+ } from "./index.js";
7
+ var MemoryFileSystemBackend = class {
8
+ #root;
9
+ constructor(root) {
10
+ this.#root = root;
11
+ }
12
+ async stat(path) {
13
+ const entry = this.#resolvePath(path);
14
+ if (!entry)
15
+ return null;
16
+ if ("content" in entry) {
17
+ return { kind: "file" };
18
+ } else {
19
+ return { kind: "directory" };
20
+ }
67
21
  }
68
- };
69
- var MemoryFileSystemDirectoryHandle = class _MemoryFileSystemDirectoryHandle {
70
- constructor(directory) {
71
- this.directory = directory;
72
- this.name = directory.name;
22
+ async readFile(path) {
23
+ const entry = this.#resolvePath(path);
24
+ if (!entry || !("content" in entry)) {
25
+ throw new DOMException("File not found", "NotFoundError");
26
+ }
27
+ return {
28
+ content: entry.content,
29
+ lastModified: entry.lastModified
30
+ };
73
31
  }
74
- kind = "directory";
75
- name;
76
- async getFileHandle(name, options) {
77
- const file = this.directory.files.get(name);
78
- if (!file && options?.create) {
79
- const newFile = {
32
+ async writeFile(path, data) {
33
+ const { parentDir, name } = this.#resolveParent(path);
34
+ if (!parentDir) {
35
+ throw new DOMException("Parent directory not found", "NotFoundError");
36
+ }
37
+ const existingFile = parentDir.files.get(name);
38
+ if (existingFile) {
39
+ existingFile.content = data;
40
+ existingFile.lastModified = Date.now();
41
+ } else {
42
+ parentDir.files.set(name, {
80
43
  name,
81
- content: new Uint8Array(0),
44
+ content: data,
82
45
  lastModified: Date.now(),
83
46
  type: "application/octet-stream"
84
- };
85
- this.directory.files.set(name, newFile);
86
- return new MemoryFileSystemFileHandle(newFile, (content, type) => {
87
- newFile.content = content;
88
- newFile.lastModified = Date.now();
89
- if (type)
90
- newFile.type = type;
91
47
  });
92
- } else if (!file) {
93
- throw new DOMException("File not found", "NotFoundError");
94
48
  }
95
- return new MemoryFileSystemFileHandle(file, (content, type) => {
96
- file.content = content;
97
- file.lastModified = Date.now();
98
- if (type)
99
- file.type = type;
100
- });
101
49
  }
102
- async getDirectoryHandle(name, options) {
103
- const dir = this.directory.directories.get(name);
104
- if (!dir && options?.create) {
105
- const newDir = {
50
+ async listDir(path) {
51
+ const entry = this.#resolvePath(path);
52
+ if (!entry || "content" in entry) {
53
+ throw new DOMException("Directory not found", "NotFoundError");
54
+ }
55
+ const results = [];
56
+ for (const fileName of entry.files.keys()) {
57
+ results.push({ name: fileName, kind: "file" });
58
+ }
59
+ for (const dirName of entry.directories.keys()) {
60
+ results.push({ name: dirName, kind: "directory" });
61
+ }
62
+ return results;
63
+ }
64
+ async createDir(path) {
65
+ const { parentDir, name } = this.#resolveParent(path);
66
+ if (!parentDir) {
67
+ throw new DOMException("Parent directory not found", "NotFoundError");
68
+ }
69
+ if (!parentDir.directories.has(name)) {
70
+ parentDir.directories.set(name, {
106
71
  name,
107
72
  files: /* @__PURE__ */ new Map(),
108
73
  directories: /* @__PURE__ */ new Map()
109
- };
110
- this.directory.directories.set(name, newDir);
111
- return new _MemoryFileSystemDirectoryHandle(newDir);
112
- } else if (!dir) {
113
- throw new DOMException("Directory not found", "NotFoundError");
74
+ });
114
75
  }
115
- return new _MemoryFileSystemDirectoryHandle(dir);
116
76
  }
117
- async removeEntry(name, options) {
118
- if (this.directory.files.has(name)) {
119
- this.directory.files.delete(name);
77
+ async remove(path, recursive) {
78
+ const { parentDir, name } = this.#resolveParent(path);
79
+ if (!parentDir) {
80
+ throw new DOMException("Entry not found", "NotFoundError");
81
+ }
82
+ if (parentDir.files.has(name)) {
83
+ parentDir.files.delete(name);
120
84
  return;
121
85
  }
122
- const dir = this.directory.directories.get(name);
86
+ const dir = parentDir.directories.get(name);
123
87
  if (dir) {
124
- if (dir.files.size > 0 || dir.directories.size > 0) {
125
- if (!options?.recursive) {
126
- throw new DOMException(
127
- "Directory is not empty",
128
- "InvalidModificationError"
129
- );
130
- }
88
+ if ((dir.files.size > 0 || dir.directories.size > 0) && !recursive) {
89
+ throw new DOMException(
90
+ "Directory is not empty",
91
+ "InvalidModificationError"
92
+ );
131
93
  }
132
- this.directory.directories.delete(name);
94
+ parentDir.directories.delete(name);
133
95
  return;
134
96
  }
135
97
  throw new DOMException("Entry not found", "NotFoundError");
136
98
  }
137
- async resolve(possibleDescendant) {
138
- if (possibleDescendant instanceof MemoryFileSystemFileHandle) {
139
- if (this.directory.files.has(possibleDescendant.name)) {
140
- return [possibleDescendant.name];
99
+ #resolvePath(path) {
100
+ if (path.includes("..") || path.includes("\0")) {
101
+ throw new DOMException(
102
+ "Invalid path: contains path traversal or null bytes",
103
+ "NotAllowedError"
104
+ );
105
+ }
106
+ const parts = path.split("/").filter(Boolean);
107
+ if (parts.length === 0) {
108
+ return this.#root;
109
+ }
110
+ for (const part of parts) {
111
+ if (part === "." || part === ".." || part.includes("/") || part.includes("\\")) {
112
+ throw new DOMException("Invalid path component", "NotAllowedError");
141
113
  }
142
114
  }
143
- if (possibleDescendant instanceof _MemoryFileSystemDirectoryHandle) {
144
- if (this.directory.directories.has(possibleDescendant.name)) {
145
- return [possibleDescendant.name];
115
+ let current = this.#root;
116
+ for (let i = 0; i < parts.length - 1; i++) {
117
+ const nextDir = current.directories.get(parts[i]);
118
+ if (!nextDir)
119
+ return null;
120
+ current = nextDir;
121
+ }
122
+ const finalName = parts[parts.length - 1];
123
+ const file = current.files.get(finalName);
124
+ if (file)
125
+ return file;
126
+ const dir = current.directories.get(finalName);
127
+ if (dir)
128
+ return dir;
129
+ return null;
130
+ }
131
+ #resolveParent(path) {
132
+ const parts = path.split("/").filter(Boolean);
133
+ const name = parts.pop() || "";
134
+ if (parts.length === 0) {
135
+ return { parentDir: this.#root, name };
136
+ }
137
+ let current = this.#root;
138
+ for (const part of parts) {
139
+ const nextDir = current.directories.get(part);
140
+ if (!nextDir) {
141
+ return { parentDir: null, name };
146
142
  }
143
+ current = nextDir;
144
+ }
145
+ return { parentDir: current, name };
146
+ }
147
+ };
148
+ var MemoryBucket = class _MemoryBucket {
149
+ kind;
150
+ name;
151
+ #backend;
152
+ constructor(name = "root") {
153
+ this.kind = "directory";
154
+ this.name = name;
155
+ const root = {
156
+ name,
157
+ files: /* @__PURE__ */ new Map(),
158
+ directories: /* @__PURE__ */ new Map()
159
+ };
160
+ this.#backend = new MemoryFileSystemBackend(root);
161
+ }
162
+ async getFileHandle(name, options) {
163
+ const filePath = `/${name}`;
164
+ const stat = await this.#backend.stat(filePath);
165
+ if (!stat && options?.create) {
166
+ await this.#backend.writeFile(filePath, new Uint8Array(0));
167
+ } else if (!stat) {
168
+ throw new DOMException("File not found", "NotFoundError");
169
+ } else if (stat.kind !== "file") {
170
+ throw new DOMException(
171
+ "Path exists but is not a file",
172
+ "TypeMismatchError"
173
+ );
174
+ }
175
+ return new ShovelFileHandle(this.#backend, filePath);
176
+ }
177
+ async getDirectoryHandle(name, options) {
178
+ const dirPath = `/${name}`;
179
+ const stat = await this.#backend.stat(dirPath);
180
+ if (!stat && options?.create) {
181
+ await this.#backend.createDir(dirPath);
182
+ } else if (!stat) {
183
+ throw new DOMException("Directory not found", "NotFoundError");
184
+ } else if (stat.kind !== "directory") {
185
+ throw new DOMException(
186
+ "Path exists but is not a directory",
187
+ "TypeMismatchError"
188
+ );
189
+ }
190
+ return new ShovelDirectoryHandle(this.#backend, dirPath);
191
+ }
192
+ async removeEntry(name, options) {
193
+ const entryPath = `/${name}`;
194
+ await this.#backend.remove(entryPath, options?.recursive);
195
+ }
196
+ async resolve(possibleDescendant) {
197
+ if (!(possibleDescendant instanceof ShovelDirectoryHandle || possibleDescendant instanceof ShovelFileHandle)) {
198
+ return null;
199
+ }
200
+ const descendantPath = possibleDescendant.path;
201
+ if (typeof descendantPath === "string" && descendantPath.startsWith("/")) {
202
+ return descendantPath.split("/").filter(Boolean);
147
203
  }
148
204
  return null;
149
205
  }
150
206
  async *entries() {
151
- for (const [name, file] of this.directory.files) {
152
- yield [
153
- name,
154
- new MemoryFileSystemFileHandle(file, (content, type) => {
155
- file.content = content;
156
- file.lastModified = Date.now();
157
- if (type)
158
- file.type = type;
159
- })
160
- ];
161
- }
162
- for (const [name, dir] of this.directory.directories) {
163
- yield [name, new _MemoryFileSystemDirectoryHandle(dir)];
207
+ const entries = await this.#backend.listDir("/");
208
+ for (const entry of entries) {
209
+ const entryPath = `/${entry.name}`;
210
+ if (entry.kind === "file") {
211
+ yield [entry.name, new ShovelFileHandle(this.#backend, entryPath)];
212
+ } else {
213
+ yield [entry.name, new ShovelDirectoryHandle(this.#backend, entryPath)];
214
+ }
164
215
  }
165
216
  }
166
217
  async *keys() {
167
- for (const name of this.directory.files.keys()) {
168
- yield name;
169
- }
170
- for (const name of this.directory.directories.keys()) {
218
+ for await (const [name] of this.entries()) {
171
219
  yield name;
172
220
  }
173
221
  }
174
222
  async *values() {
175
- for (const [, handle] of this.entries()) {
223
+ for await (const [, handle] of this.entries()) {
176
224
  yield handle;
177
225
  }
178
226
  }
227
+ [Symbol.asyncIterator]() {
228
+ return this.entries();
229
+ }
179
230
  async isSameEntry(other) {
180
231
  if (other.kind !== "directory")
181
232
  return false;
182
- if (!(other instanceof _MemoryFileSystemDirectoryHandle))
183
- return false;
184
- return this.directory === other.directory;
233
+ return other instanceof _MemoryBucket && other.name === this.name;
185
234
  }
186
235
  async queryPermission() {
187
236
  return "granted";
@@ -190,35 +239,7 @@ var MemoryFileSystemDirectoryHandle = class _MemoryFileSystemDirectoryHandle {
190
239
  return "granted";
191
240
  }
192
241
  };
193
- var MemoryBucket = class {
194
- config;
195
- filesystems = /* @__PURE__ */ new Map();
196
- constructor(config = {}) {
197
- this.config = {
198
- name: "memory",
199
- ...config
200
- };
201
- }
202
- async getDirectoryHandle(name) {
203
- const dirName = name || "root";
204
- if (!this.filesystems.has(dirName)) {
205
- const root2 = {
206
- name: "root",
207
- files: /* @__PURE__ */ new Map(),
208
- directories: /* @__PURE__ */ new Map()
209
- };
210
- this.filesystems.set(dirName, root2);
211
- }
212
- const root = this.filesystems.get(dirName);
213
- return new MemoryFileSystemDirectoryHandle(root);
214
- }
215
- getConfig() {
216
- return { ...this.config };
217
- }
218
- async dispose() {
219
- this.filesystems.clear();
220
- }
221
- };
222
242
  export {
223
- MemoryBucket
243
+ MemoryBucket,
244
+ MemoryFileSystemBackend
224
245
  };
package/src/node.d.ts CHANGED
@@ -1,67 +1,37 @@
1
1
  /**
2
- * Node.js implementation of File System Access API
2
+ * Node.js filesystem implementation
3
3
  *
4
- * Implements FileSystemDirectoryHandle and FileSystemFileHandle using fs/promises
5
- * to provide universal File System Access API on Node.js platforms.
4
+ * Provides NodeBucket (root) and NodeFileSystemBackend for storage operations
5
+ * using Node.js fs module.
6
6
  */
7
- import type { Bucket, FileSystemConfig } from "./types.js";
7
+ import { type FileSystemBackend, ShovelDirectoryHandle } from "./index.js";
8
8
  /**
9
- * Node.js implementation of FileSystemWritableFileStream
9
+ * Node.js storage backend that implements FileSystemBackend
10
10
  */
11
- export declare class NodeFileSystemWritableFileStream extends WritableStream<Uint8Array> {
12
- private filePath;
13
- constructor(filePath: string);
14
- write(data: Uint8Array | string): Promise<void>;
15
- close(): Promise<void>;
11
+ export declare class NodeFileSystemBackend implements FileSystemBackend {
12
+ #private;
13
+ constructor(rootPath: string);
14
+ stat(filePath: string): Promise<{
15
+ kind: "file" | "directory";
16
+ } | null>;
17
+ readFile(filePath: string): Promise<{
18
+ content: Uint8Array;
19
+ lastModified?: number;
20
+ }>;
21
+ writeFile(filePath: string, data: Uint8Array): Promise<void>;
22
+ listDir(dirPath: string): Promise<Array<{
23
+ name: string;
24
+ kind: "file" | "directory";
25
+ }>>;
26
+ createDir(dirPath: string): Promise<void>;
27
+ remove(entryPath: string, recursive?: boolean): Promise<void>;
16
28
  }
17
29
  /**
18
- * Node.js implementation of FileSystemFileHandle
30
+ * Node bucket - root entry point for Node.js filesystem
31
+ * Extends ShovelDirectoryHandle with "/" as root path
19
32
  */
20
- export declare class NodeFileSystemFileHandle implements FileSystemFileHandle {
21
- private filePath;
22
- readonly kind: "file";
23
- readonly name: string;
24
- constructor(filePath: string);
25
- getFile(): Promise<File>;
26
- createWritable(): Promise<FileSystemWritableFileStream>;
27
- createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>;
28
- isSameEntry(other: FileSystemHandle): Promise<boolean>;
29
- queryPermission(): Promise<PermissionState>;
30
- requestPermission(): Promise<PermissionState>;
31
- private getMimeType;
32
- }
33
- /**
34
- * Node.js implementation of FileSystemDirectoryHandle
35
- */
36
- export declare class NodeFileSystemDirectoryHandle implements FileSystemDirectoryHandle {
37
- private dirPath;
38
- readonly kind: "directory";
39
- readonly name: string;
40
- constructor(dirPath: string);
41
- getFileHandle(name: string, options?: {
42
- create?: boolean;
43
- }): Promise<FileSystemFileHandle>;
44
- getDirectoryHandle(name: string, options?: {
45
- create?: boolean;
46
- }): Promise<FileSystemDirectoryHandle>;
47
- removeEntry(name: string, options?: {
48
- recursive?: boolean;
49
- }): Promise<void>;
50
- resolve(_possibleDescendant: FileSystemHandle): Promise<string[] | null>;
51
- entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
52
- keys(): AsyncIterableIterator<string>;
53
- values(): AsyncIterableIterator<FileSystemHandle>;
54
- isSameEntry(other: FileSystemHandle): Promise<boolean>;
55
- queryPermission(): Promise<PermissionState>;
56
- requestPermission(): Promise<PermissionState>;
57
- }
58
- /**
59
- * Local filesystem bucket using Node.js fs
60
- */
61
- export declare class LocalBucket implements Bucket {
62
- private config;
63
- private rootPath;
64
- constructor(config?: FileSystemConfig);
65
- getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
66
- getConfig(): FileSystemConfig;
33
+ export declare class NodeBucket extends ShovelDirectoryHandle {
34
+ #private;
35
+ constructor(rootPath: string);
36
+ get name(): string;
67
37
  }