@statewalker/webrun-files 0.2.0 → 0.3.0

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.
@@ -1,30 +1,165 @@
1
1
  import FilesApi from "./FilesApi.js";
2
2
  import getMimeType from "./getMimeType.js";
3
3
 
4
+ /**
5
+ * Stateless adapter for the "File and Directory Entries API".
6
+ * See https://developer.mozilla.org/en-US/docs/Web/API/File_and_Directory_Entries_API
7
+ *
8
+ * ## File and Directory Entries API
9
+ * The File and Directory Entries API simulates a local file system
10
+ * this web apps can navigate within and access files in. You can
11
+ * develop apps which read, write, and create files and/or directories
12
+ * in a virtual, sandboxed file system.
13
+ */
14
+ class AdapterForFilesAndDirectoryEntriesApi {
15
+ static canApply(handle) {
16
+ return ((handle?.isDirectory || handle?.isFile) &&
17
+ (typeof handle?.name === "string"));
18
+ }
19
+
20
+ async *listDirectoryChildren(handle) {
21
+ let dirReader = handle.createReader();
22
+ while (true) {
23
+ const list = await new Promise((resolve, reject) =>
24
+ dirReader.readEntries(resolve, reject)
25
+ );
26
+ if (!list.length) break;
27
+ yield* list;
28
+ }
29
+ }
30
+
31
+ isDirectoryHandle(handle) {
32
+ return handle?.isDirectory;
33
+ }
34
+ isFileHandle(handle) {
35
+ return handle?.isFile;
36
+ }
37
+
38
+ async getSubdirectoryHandle(dirHandle, directoryName, create) {
39
+ return new Promise((resolve, reject) =>
40
+ dirHandle.getDirectory(
41
+ directoryName,
42
+ { create },
43
+ resolve,
44
+ reject,
45
+ )
46
+ );
47
+ }
48
+
49
+ async getFileHandle(dirHandle, fileName, create = true) {
50
+ return new Promise((resolve, reject) =>
51
+ dirHandle.getFile(
52
+ fileName,
53
+ { create },
54
+ resolve,
55
+ reject,
56
+ )
57
+ );
58
+ }
59
+
60
+ async removeDirectoryEntry(dirHandle, handle) {
61
+ return new Promise((resolve, reject) => handle.remove(resolve, reject));
62
+ }
63
+
64
+ async getFile(handle) {
65
+ const file = await new Promise((resolve, reject) =>
66
+ handle.file(resolve, reject)
67
+ );
68
+ return file;
69
+ }
70
+
71
+ async getFileWriteStream(handle) {
72
+ throw new Error("Not implemented");
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Stateless adapter for the "File System API" and "File System Access API".
78
+ * See:
79
+ * - https://developer.mozilla.org/en-US/docs/Web/API/File_System_API
80
+ * - https://wicg.github.io/file-system-access/
81
+ */
82
+ class AdapterForFileSystemApi {
83
+ static canApply(handle) {
84
+ return (handle?.kind === "file" || handle?.kind === "directory");
85
+ }
86
+
87
+ async *listDirectoryChildren(handle) {
88
+ yield* handle.values();
89
+ }
90
+
91
+ isDirectoryHandle(handle) {
92
+ return handle?.kind === "directory";
93
+ }
94
+ isFileHandle(handle) {
95
+ return handle?.kind === "file";
96
+ }
97
+
98
+ async getSubdirectoryHandle(dirHandle, directoryName, create) {
99
+ return await dirHandle.getDirectoryHandle(directoryName, { create });
100
+ }
101
+
102
+ async getFileHandle(dirHandle, fileName, create = true) {
103
+ return await dirHandle.getFileHandle(fileName, { create });
104
+ }
105
+
106
+ async removeDirectoryEntry(dirHandle, handle) {
107
+ return await dirHandle.removeEntry(handle.name, { recursive: true });
108
+ }
109
+
110
+ async getFile(handle) {
111
+ return await handle.getFile();
112
+ }
113
+
114
+ async getFileWriteStream(handle) {
115
+ return await handle.createWritable(); // Write the contents of the file to the stream.
116
+ }
117
+ }
118
+
4
119
  export default class BrowserFilesApi extends FilesApi {
5
120
  constructor(options) {
6
121
  super(options);
7
- if (!this.rootHandler) {
8
- throw new Error("Root directory handler is not defined");
122
+ if (!this.rootHandle) {
123
+ throw new Error("Root directory handle is not defined");
124
+ }
125
+ if (!this.adapter) {
126
+ throw new Error(
127
+ "Can not define a browser adapter for the specified root handle",
128
+ );
9
129
  }
10
130
  }
11
131
 
12
- get rootHandler() {
13
- return this.options.rootHandler;
132
+ get rootHandle() {
133
+ return this.options.rootHandle || this.options.rootHandler;
134
+ }
135
+
136
+ get adapter() {
137
+ if (!this._adapter) {
138
+ const handle = this.rootHandle;
139
+ let adapter = AdapterForFilesAndDirectoryEntriesApi.canApply(handle)
140
+ ? new AdapterForFilesAndDirectoryEntriesApi()
141
+ : AdapterForFileSystemApi.canApply(handle)
142
+ ? new AdapterForFileSystemApi()
143
+ : null;
144
+ this._adapter = adapter;
145
+ }
146
+ return this._adapter;
14
147
  }
15
148
 
16
149
  async *list(file, { recursive = false } = {}) {
17
150
  const segments = this._getPathSegments(file);
18
- const handle = await this._getHandler(segments, false);
151
+ const handle = await this._getHandle(segments);
152
+ if (!handle) return;
19
153
  const that = this;
20
- if (handle.kind === "directory") {
154
+ if (this.adapter.isDirectoryHandle(handle)) {
155
+ // yield await this._getFileInfo(segments, handle);
21
156
  yield* listChildren(segments, handle, recursive);
22
157
  }
23
158
 
24
159
  async function* listChildren(segments, dirHandle, recursive) {
25
- for await (const handle of dirHandle.values()) {
160
+ for await (const handle of that._listDirectoryContent(dirHandle)) {
26
161
  yield await that._getFileInfo([...segments, handle.name], handle);
27
- if (recursive && handle.kind == "directory") {
162
+ if (recursive && that.adapter.isDirectoryHandle(handle)) {
28
163
  yield* listChildren(
29
164
  [...segments, handle.name],
30
165
  handle,
@@ -37,32 +172,45 @@ export default class BrowserFilesApi extends FilesApi {
37
172
 
38
173
  async stats(file) {
39
174
  const segments = this._getPathSegments(file);
40
- const handle = await this._getHandler(segments, false);
175
+ const handle = await this._getHandle(segments);
41
176
  return handle ? await this._getFileInfo(segments, handle) : null;
42
177
  }
43
178
 
44
179
  async remove(file) {
45
- let segments = this._getPathSegments(file);
46
- const name = segments.pop();
47
- const parentHandle = await this._getHandler(segments, false);
48
- const toRemove = [];
49
- if (name) {
50
- toRemove.push(name);
51
- } else {
52
- for await (const childHandle of parentHandle.values()) {
53
- toRemove.push(childHandle.name);
180
+ const doRemove = async (dirHandle, handle) => {
181
+ let result = true;
182
+ if (this.adapter.isDirectoryHandle(handle)) {
183
+ const children = [];
184
+ for await (let child of this.adapter.listDirectoryChildren(handle)) {
185
+ children.push(child);
186
+ }
187
+ for (let child of children) {
188
+ result = result && await doRemove(handle, child);
189
+ }
54
190
  }
55
- }
56
- for (const name of toRemove) {
57
- await parentHandle.removeEntry(name, { recursive: true });
58
- }
59
- return true;
191
+ if (dirHandle) {
192
+ result &= await this.adapter.removeDirectoryEntry(dirHandle, handle);
193
+ }
194
+ return result;
195
+ };
196
+
197
+ const segments = this._getPathSegments(file);
198
+ const handle = await this._getHandle(segments);
199
+ segments.pop();
200
+ const dirHandle = await this._getHandle(segments);
201
+ return doRemove(dirHandle !== handle ? dirHandle : null, handle);
60
202
  }
61
203
 
62
204
  async write(file, content) {
63
205
  const segments = this._getPathSegments(file);
64
- const fileHandle = await this._getFileHandler(segments);
65
- const writable = await fileHandle.createWritable(); // Write the contents of the file to the stream.
206
+ const fileName = segments.pop();
207
+ const dirHandle = await this._getDirectoryHandle(segments, true);
208
+ const fileHandle = await this.adapter.getFileHandle(
209
+ dirHandle,
210
+ fileName,
211
+ true,
212
+ );
213
+ const writable = await this.adapter.getFileWriteStream(fileHandle);
66
214
  try {
67
215
  if (typeof content === "function") content = content();
68
216
  for await (let block of content) {
@@ -75,18 +223,26 @@ export default class BrowserFilesApi extends FilesApi {
75
223
 
76
224
  async *read(file /* { start = 0, bufferSize = 1024 * 8 } = {} */) {
77
225
  const segments = this._getPathSegments(file);
78
- const fileHandle = await this._getFileHandler(segments, false);
226
+
227
+ const fileName = segments.pop();
228
+ const dirHandle = await this._getDirectoryHandle(segments, false);
229
+ const fileHandle = await this.adapter.getFileHandle(
230
+ dirHandle,
231
+ fileName,
232
+ false,
233
+ );
79
234
  if (!fileHandle) return;
80
- const fileData = await fileHandle.getFile();
235
+ const fileData = await this.adapter.getFile(fileHandle);
81
236
  const stream = fileData.stream();
82
237
  const reader = stream.getReader();
83
238
  try {
84
239
  let chunk;
85
- while ((chunk = await reader.read()) && !chunk.done) {
86
- yield chunk.value;
240
+ while (chunk = await reader.read()) {
241
+ if (chunk.done) break;
242
+ yield await chunk.value;
87
243
  }
88
244
  } finally {
89
- reader.cancel && reader.cancel();
245
+ await reader.cancel && reader.cancel();
90
246
  // stream.cancel && stream.cancel();
91
247
  }
92
248
  }
@@ -97,41 +253,8 @@ export default class BrowserFilesApi extends FilesApi {
97
253
  // TODO:
98
254
  // async move(fromPath, toPath, options = {}) { ... }
99
255
 
100
- async _getHandler(segments, create = false) {
101
- let handle;
102
- try {
103
- handle = await this._getFileHandler(segments, create);
104
- } catch (e) {
105
- try {
106
- handle = await this._getDirectoryHandler(segments, create);
107
- } catch (e) {
108
- // Just ignore it
109
- }
110
- }
111
- return handle;
112
- }
113
-
114
- async _getDirectoryHandler(segments, create = true) {
115
- segments = [...segments];
116
- let dirHandle = this.rootHandler;
117
- for (let directoryName of segments) {
118
- dirHandle = await dirHandle.getDirectoryHandle(directoryName, { create });
119
- }
120
- return dirHandle;
121
- }
122
-
123
- async _getFileHandler(segments, create = true) {
124
- segments = [...segments];
125
- const filename = segments.pop();
126
- let dirHandle = this.rootHandler;
127
- for (let directoryName of segments) {
128
- dirHandle = await dirHandle.getDirectoryHandle(directoryName, {
129
- create: true,
130
- });
131
- }
132
- const fileHandler = await dirHandle.getFileHandle(filename, { create });
133
- return fileHandler;
134
- }
256
+ // ---------------------------------------------------------
257
+ // Private methods
135
258
 
136
259
  async _getFileInfo(segments, handle) {
137
260
  segments = [...segments];
@@ -143,11 +266,12 @@ export default class BrowserFilesApi extends FilesApi {
143
266
  path,
144
267
  name,
145
268
  };
146
- if (handle.kind === "directory") {
269
+ if (this.adapter.isDirectoryHandle(handle)) {
147
270
  info.kind = "directory";
148
271
  } else {
149
272
  info.kind = "file";
150
- const file = await handle.getFile();
273
+
274
+ const file = await this.adapter.getFile(handle);
151
275
  info.size = file.size;
152
276
  info.type = getMimeType(path);
153
277
  info.lastModified = file.lastModified;
@@ -161,4 +285,51 @@ export default class BrowserFilesApi extends FilesApi {
161
285
  const segments = filePath.split("/").filter((s) => !!s);
162
286
  return segments;
163
287
  }
288
+
289
+ async _getHandle(segments) {
290
+ const parentDirSegments = [...segments];
291
+ const name = parentDirSegments.pop();
292
+ const create = false;
293
+ try {
294
+ const dirHandle = await this._getDirectoryHandle(
295
+ parentDirSegments,
296
+ create,
297
+ );
298
+ try {
299
+ if (!name) return dirHandle;
300
+ return await this.adapter.getSubdirectoryHandle(
301
+ dirHandle,
302
+ name,
303
+ create,
304
+ );
305
+ } catch (error) {
306
+ return await this.adapter.getFileHandle(dirHandle, name, create);
307
+ }
308
+ } catch (error) {
309
+ return undefined;
310
+ }
311
+ }
312
+
313
+ async _getDirectoryHandle(segments, create = true) {
314
+ segments = [...segments];
315
+ let dirHandle = this.rootHandle;
316
+ for (let directoryName of segments) {
317
+ dirHandle = await this.adapter.getSubdirectoryHandle(
318
+ dirHandle,
319
+ directoryName,
320
+ create,
321
+ );
322
+ if (!dirHandle) break;
323
+ }
324
+ return dirHandle;
325
+ }
326
+
327
+ async *_listDirectoryContent(handle) {
328
+ let entries = [];
329
+ for await (const entry of this.adapter.listDirectoryChildren(handle)) {
330
+ entries.push(entry);
331
+ }
332
+ entries = entries.sort((a, b) => b.name > a.name ? 1 : -1);
333
+ yield* entries;
334
+ }
164
335
  }