sliftutils 1.2.18 → 1.2.19
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/package.json +1 -1
- package/storage/FileFolderAPI.tsx +145 -1
package/package.json
CHANGED
|
@@ -221,8 +221,146 @@ class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
function isNotAllowedError(e: unknown): boolean {
|
|
225
|
+
return !!e && typeof e === "object" && (e as any).name === "NotAllowedError";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Deduplicates concurrent retry-prompts so only one button is shown to the user,
|
|
229
|
+
// and all in-flight callers wait on the same grant.
|
|
230
|
+
let pendingPermissionPrompt: Promise<void> | undefined;
|
|
231
|
+
async function requestPermissionInteractive(handle: any, mode: "read" | "readwrite" = "readwrite"): Promise<void> {
|
|
232
|
+
if (pendingPermissionPrompt) return pendingPermissionPrompt;
|
|
233
|
+
pendingPermissionPrompt = (async () => {
|
|
234
|
+
// Some browsers grant without user activation (persistent permissions, multi-tab).
|
|
235
|
+
// Try once cheaply before showing UI.
|
|
236
|
+
try {
|
|
237
|
+
if (typeof handle?.requestPermission === "function") {
|
|
238
|
+
const result = await handle.requestPermission({ mode });
|
|
239
|
+
if (result === "granted") return;
|
|
240
|
+
}
|
|
241
|
+
} catch { /* user activation required — fall through to button */ }
|
|
242
|
+
|
|
243
|
+
let root = document.createElement("div");
|
|
244
|
+
document.body.appendChild(root);
|
|
245
|
+
preact.render(<DirectoryPrompter />, root);
|
|
246
|
+
try {
|
|
247
|
+
await new Promise<void>(resolve => {
|
|
248
|
+
displayData.ui = (
|
|
249
|
+
<button
|
|
250
|
+
className={css.fontSize(40).pad2(80, 40)}
|
|
251
|
+
onClick={async () => {
|
|
252
|
+
try {
|
|
253
|
+
const result = await handle.requestPermission({ mode });
|
|
254
|
+
if (result === "granted") {
|
|
255
|
+
resolve();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error("requestPermission failed:", (err as Error).stack ?? err);
|
|
260
|
+
}
|
|
261
|
+
// Permission still not granted — fall back to a fresh directory pick.
|
|
262
|
+
try {
|
|
263
|
+
const pickedHandle = await window.showDirectoryPicker();
|
|
264
|
+
const grantResult = await (pickedHandle as any).requestPermission({ mode: "readwrite" });
|
|
265
|
+
if (grantResult !== "granted") return;
|
|
266
|
+
let newStoredId = await storeFileSystemPointer({ mode: "readwrite", handle: pickedHandle });
|
|
267
|
+
localStorage.setItem(getFileAPIKey(), newStoredId);
|
|
268
|
+
// Reload so all storage layers re-bind to the new root.
|
|
269
|
+
window.location.reload();
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error("showDirectoryPicker failed:", (err as Error).stack ?? err);
|
|
272
|
+
}
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
Click to allow file system access
|
|
276
|
+
</button>
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
displayData.ui = undefined;
|
|
281
|
+
preact.render(null, root);
|
|
282
|
+
root.remove();
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
try {
|
|
286
|
+
await pendingPermissionPrompt;
|
|
287
|
+
} finally {
|
|
288
|
+
pendingPermissionPrompt = undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function withPermissionRetry<T>(permissionHandle: any, op: () => Promise<T>): Promise<T> {
|
|
293
|
+
try {
|
|
294
|
+
return await op();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
if (!isNotAllowedError(e)) throw e;
|
|
297
|
+
await requestPermissionInteractive(permissionHandle);
|
|
298
|
+
return await op();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function wrapFileWithRetry(file: FileWrapper, permissionHandle: any): FileWrapper {
|
|
303
|
+
return {
|
|
304
|
+
getFile: () => withPermissionRetry(permissionHandle, () => file.getFile()),
|
|
305
|
+
createWritable: async (config) => {
|
|
306
|
+
const writable = await withPermissionRetry(permissionHandle, () => file.createWritable(config));
|
|
307
|
+
return {
|
|
308
|
+
seek: (offset) => withPermissionRetry(permissionHandle, () => writable.seek(offset)),
|
|
309
|
+
write: (value) => withPermissionRetry(permissionHandle, () => writable.write(value)),
|
|
310
|
+
close: () => withPermissionRetry(permissionHandle, () => writable.close()),
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function wrapDirectoryWithRetry(handle: DirectoryWrapper): DirectoryWrapper {
|
|
317
|
+
const permissionHandle = handle;
|
|
318
|
+
return {
|
|
319
|
+
removeEntry: (key, options) => withPermissionRetry(permissionHandle, () => handle.removeEntry(key, options)),
|
|
320
|
+
getFileHandle: (async (key: string, options?: { create?: boolean }) =>
|
|
321
|
+
wrapFileWithRetry(
|
|
322
|
+
await withPermissionRetry(permissionHandle, () => handle.getFileHandle(key, options)),
|
|
323
|
+
permissionHandle,
|
|
324
|
+
)) as DirectoryWrapper["getFileHandle"],
|
|
325
|
+
getDirectoryHandle: async (key, options) =>
|
|
326
|
+
wrapDirectoryWithRetry(await withPermissionRetry(permissionHandle, () => handle.getDirectoryHandle(key, options))),
|
|
327
|
+
async *[Symbol.asyncIterator]() {
|
|
328
|
+
let iter: AsyncIterator<any> | undefined;
|
|
329
|
+
while (true) {
|
|
330
|
+
let result: IteratorResult<any>;
|
|
331
|
+
try {
|
|
332
|
+
if (!iter) iter = handle[Symbol.asyncIterator]();
|
|
333
|
+
result = await iter.next();
|
|
334
|
+
} catch (e) {
|
|
335
|
+
if (!isNotAllowedError(e)) throw e;
|
|
336
|
+
await requestPermissionInteractive(permissionHandle);
|
|
337
|
+
// Restart iteration after re-grant. Callers of getKeys/reset
|
|
338
|
+
// collect into arrays, so a fresh full iteration is correct.
|
|
339
|
+
iter = undefined;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (result.done) return;
|
|
343
|
+
const [name, entry] = result.value;
|
|
344
|
+
if (entry.kind === "directory") {
|
|
345
|
+
yield [name, {
|
|
346
|
+
...entry,
|
|
347
|
+
getDirectoryHandle: async (k: string, o?: { create?: boolean }) =>
|
|
348
|
+
wrapDirectoryWithRetry(await withPermissionRetry(permissionHandle, () => entry.getDirectoryHandle(k, o))),
|
|
349
|
+
}];
|
|
350
|
+
} else {
|
|
351
|
+
yield [name, {
|
|
352
|
+
...entry,
|
|
353
|
+
getFile: async () =>
|
|
354
|
+
wrapFileWithRetry(await withPermissionRetry(permissionHandle, () => entry.getFile()), permissionHandle),
|
|
355
|
+
}];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
224
362
|
// NOTE: Blocks until the user provides a directory
|
|
225
|
-
|
|
363
|
+
const getDirectoryHandleRaw = lazy(async function getDirectoryHandle(): Promise<DirectoryWrapper> {
|
|
226
364
|
if (isNode()) {
|
|
227
365
|
return new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
|
|
228
366
|
}
|
|
@@ -329,6 +467,12 @@ export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Prom
|
|
|
329
467
|
}
|
|
330
468
|
});
|
|
331
469
|
|
|
470
|
+
export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Promise<DirectoryWrapper> {
|
|
471
|
+
const handle = await getDirectoryHandleRaw();
|
|
472
|
+
if (isNode()) return handle;
|
|
473
|
+
return wrapDirectoryWithRetry(handle);
|
|
474
|
+
});
|
|
475
|
+
|
|
332
476
|
export const getFileStorageNested = cache(async function getFileStorage(path: string): Promise<FileStorage> {
|
|
333
477
|
let base = await getDirectoryHandle();
|
|
334
478
|
for (let part of path.split("/")) {
|