@pistonite/pure 0.0.12
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/LICENSE +21 -0
- package/README.md +8 -0
- package/package.json +33 -0
- package/src/fs/FsError.ts +55 -0
- package/src/fs/FsFile.ts +67 -0
- package/src/fs/FsFileImpl.ts +225 -0
- package/src/fs/FsFileMgr.ts +29 -0
- package/src/fs/FsFileSystem.ts +71 -0
- package/src/fs/FsFileSystemInternal.ts +30 -0
- package/src/fs/FsImplEntryAPI.ts +188 -0
- package/src/fs/FsImplFileAPI.ts +126 -0
- package/src/fs/FsImplHandleAPI.ts +237 -0
- package/src/fs/FsOpen.ts +307 -0
- package/src/fs/FsPath.ts +137 -0
- package/src/fs/FsSave.ts +12 -0
- package/src/fs/FsSupportStatus.ts +91 -0
- package/src/fs/index.ts +129 -0
- package/src/log/index.ts +56 -0
- package/src/pref/dark.ts +184 -0
- package/src/pref/index.ts +12 -0
- package/src/pref/injectStyle.ts +22 -0
- package/src/pref/locale.ts +341 -0
- package/src/result/index.ts +215 -0
- package/src/sync/Debounce.ts +35 -0
- package/src/sync/Latest.ts +75 -0
- package/src/sync/RwLock.ts +95 -0
- package/src/sync/Serial.ts +170 -0
- package/src/sync/index.ts +12 -0
package/src/fs/FsOpen.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { tryCatch, tryAsync, errstr } from "../result/index.ts";
|
|
2
|
+
|
|
3
|
+
import type { FsFileSystem, FsFileSystemUninit } from "./FsFileSystem.ts";
|
|
4
|
+
import {
|
|
5
|
+
FsErr,
|
|
6
|
+
type FsError,
|
|
7
|
+
type FsResult,
|
|
8
|
+
fsErr,
|
|
9
|
+
fsFail,
|
|
10
|
+
} from "./FsError.ts";
|
|
11
|
+
import { fsGetSupportStatus } from "./FsSupportStatus.ts";
|
|
12
|
+
import { FsImplFileAPI } from "./FsImplFileAPI.ts";
|
|
13
|
+
import { FsImplEntryAPI } from "./FsImplEntryAPI.ts";
|
|
14
|
+
import { FsImplHandleAPI } from "./FsImplHandleAPI.ts";
|
|
15
|
+
|
|
16
|
+
/** Handle for handling top level open errors, and decide if the operation should be retried */
|
|
17
|
+
export type FsOpenRetryHandler = (
|
|
18
|
+
error: FsError,
|
|
19
|
+
attempt: number,
|
|
20
|
+
) => Promise<FsResult<boolean>>;
|
|
21
|
+
|
|
22
|
+
const MAX_RETRY = 10;
|
|
23
|
+
|
|
24
|
+
/** Open a file system for read-only access with a directory picker dialog */
|
|
25
|
+
export async function fsOpenRead(
|
|
26
|
+
retryHandler?: FsOpenRetryHandler,
|
|
27
|
+
): Promise<FsResult<FsFileSystem>> {
|
|
28
|
+
const fs = await createWithPicker(false, retryHandler);
|
|
29
|
+
if (fs.err) {
|
|
30
|
+
return fs;
|
|
31
|
+
}
|
|
32
|
+
return await init(fs.val, retryHandler);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Open a file system for read-write access with a directory picker dialog */
|
|
36
|
+
export async function fsOpenReadWrite(
|
|
37
|
+
retryHandler?: FsOpenRetryHandler,
|
|
38
|
+
): Promise<FsResult<FsFileSystem>> {
|
|
39
|
+
const fs = await createWithPicker(true, retryHandler);
|
|
40
|
+
if (fs.err) {
|
|
41
|
+
return fs;
|
|
42
|
+
}
|
|
43
|
+
return await init(fs.val, retryHandler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Open a file system for read-only access from a DataTransferItem from a drag and drop event */
|
|
47
|
+
export async function fsOpenReadFrom(
|
|
48
|
+
item: DataTransferItem,
|
|
49
|
+
retryHandler?: FsOpenRetryHandler,
|
|
50
|
+
): Promise<FsResult<FsFileSystem>> {
|
|
51
|
+
const fs = await createFromDataTransferItem(item, false, retryHandler);
|
|
52
|
+
if (fs.err) {
|
|
53
|
+
return fs;
|
|
54
|
+
}
|
|
55
|
+
return await init(fs.val, retryHandler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Open a file system for read-write access from a DataTransferItem from a drag and drop event */
|
|
59
|
+
export async function fsOpenReadWriteFrom(
|
|
60
|
+
item: DataTransferItem,
|
|
61
|
+
retryHandler?: FsOpenRetryHandler,
|
|
62
|
+
): Promise<FsResult<FsFileSystem>> {
|
|
63
|
+
const fs = await createFromDataTransferItem(item, true, retryHandler);
|
|
64
|
+
if (fs.err) {
|
|
65
|
+
return fs;
|
|
66
|
+
}
|
|
67
|
+
return await init(fs.val, retryHandler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function createWithPicker(
|
|
71
|
+
write: boolean,
|
|
72
|
+
retryHandler: FsOpenRetryHandler | undefined,
|
|
73
|
+
): Promise<FsResult<FsFileSystemUninit>> {
|
|
74
|
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
|
75
|
+
const { implementation } = fsGetSupportStatus();
|
|
76
|
+
if (implementation === "FileSystemAccess") {
|
|
77
|
+
const handle = await tryAsync(() => showDirectoryPicker(write));
|
|
78
|
+
if (handle.val) {
|
|
79
|
+
return createFromFileSystemHandle(handle.val, write);
|
|
80
|
+
}
|
|
81
|
+
if (retryHandler) {
|
|
82
|
+
const error = isAbortError(handle.err)
|
|
83
|
+
? fsErr(FsErr.UserAbort, "User cancelled the operation")
|
|
84
|
+
: fsFail(errstr(handle.err));
|
|
85
|
+
const shouldRetry = await retryHandler(error, attempt);
|
|
86
|
+
if (shouldRetry.err) {
|
|
87
|
+
// retry handler failed
|
|
88
|
+
return shouldRetry;
|
|
89
|
+
}
|
|
90
|
+
if (!shouldRetry.val) {
|
|
91
|
+
// don't retry
|
|
92
|
+
return { err: error };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Retry with FileSystemAccess API
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// FileEntry API only supported through drag and drop
|
|
100
|
+
// so fallback to File API
|
|
101
|
+
const inputElement = document.createElement("input");
|
|
102
|
+
inputElement.id = "temp";
|
|
103
|
+
inputElement.style.display = "none";
|
|
104
|
+
document.body.appendChild(inputElement);
|
|
105
|
+
inputElement.type = "file";
|
|
106
|
+
inputElement.webkitdirectory = true;
|
|
107
|
+
|
|
108
|
+
const fsUninit = await new Promise<FsResult<FsFileSystemUninit>>(
|
|
109
|
+
(resolve) => {
|
|
110
|
+
inputElement.addEventListener("change", (event) => {
|
|
111
|
+
const files = (event.target as HTMLInputElement).files;
|
|
112
|
+
if (!files) {
|
|
113
|
+
const err = fsFail(
|
|
114
|
+
"Failed to get files from input element",
|
|
115
|
+
);
|
|
116
|
+
return resolve({ err });
|
|
117
|
+
}
|
|
118
|
+
resolve(createFromFileList(files));
|
|
119
|
+
});
|
|
120
|
+
inputElement.click();
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
inputElement.remove();
|
|
124
|
+
|
|
125
|
+
if (fsUninit.val) {
|
|
126
|
+
return fsUninit;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (retryHandler) {
|
|
130
|
+
const shouldRetry = await retryHandler(fsUninit.err, attempt);
|
|
131
|
+
if (shouldRetry.err) {
|
|
132
|
+
// retry handler failed
|
|
133
|
+
return shouldRetry;
|
|
134
|
+
}
|
|
135
|
+
if (!shouldRetry.val) {
|
|
136
|
+
// don't retry
|
|
137
|
+
return fsUninit;
|
|
138
|
+
}
|
|
139
|
+
// fall through to retry
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { err: fsFail("Max retry count reached") };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function createFromDataTransferItem(
|
|
146
|
+
item: DataTransferItem,
|
|
147
|
+
write: boolean,
|
|
148
|
+
retryHandler: FsOpenRetryHandler | undefined,
|
|
149
|
+
): Promise<FsResult<FsFileSystemUninit>> {
|
|
150
|
+
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
|
151
|
+
let error: FsError | undefined = undefined;
|
|
152
|
+
const { implementation } = fsGetSupportStatus();
|
|
153
|
+
// Prefer File System Access API since it supports writing
|
|
154
|
+
if (
|
|
155
|
+
"getAsFileSystemHandle" in item &&
|
|
156
|
+
implementation === "FileSystemAccess"
|
|
157
|
+
) {
|
|
158
|
+
const handle = await tryAsync(() => getAsFileSystemHandle(item));
|
|
159
|
+
if (handle.val) {
|
|
160
|
+
return createFromFileSystemHandle(handle.val, write);
|
|
161
|
+
}
|
|
162
|
+
error = fsFail(
|
|
163
|
+
"Failed to get handle from DataTransferItem: " +
|
|
164
|
+
errstr(handle.err),
|
|
165
|
+
);
|
|
166
|
+
} else if (
|
|
167
|
+
"webkitGetAsEntry" in item &&
|
|
168
|
+
implementation === "FileEntry"
|
|
169
|
+
) {
|
|
170
|
+
const entry = tryCatch(() => webkitGetAsEntry(item));
|
|
171
|
+
if (entry.val) {
|
|
172
|
+
return createFromFileSystemEntry(entry.val);
|
|
173
|
+
}
|
|
174
|
+
error = fsFail(
|
|
175
|
+
"Failed to get entry from DataTransferItem: " +
|
|
176
|
+
errstr(entry.err),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!error) {
|
|
180
|
+
const err = fsErr(
|
|
181
|
+
FsErr.NotSupported,
|
|
182
|
+
"No supported API found on the DataTransferItem",
|
|
183
|
+
);
|
|
184
|
+
return { err };
|
|
185
|
+
}
|
|
186
|
+
// handle error
|
|
187
|
+
if (retryHandler) {
|
|
188
|
+
const shouldRetry = await retryHandler(error, attempt);
|
|
189
|
+
if (shouldRetry.err) {
|
|
190
|
+
// retry handler failed
|
|
191
|
+
return shouldRetry;
|
|
192
|
+
}
|
|
193
|
+
if (!shouldRetry.val) {
|
|
194
|
+
// don't retry
|
|
195
|
+
return { err: error };
|
|
196
|
+
}
|
|
197
|
+
// fall through to retry
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { err: fsFail("Max retry count reached") };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function init(
|
|
204
|
+
fs: FsFileSystemUninit,
|
|
205
|
+
retryHandler: FsOpenRetryHandler | undefined,
|
|
206
|
+
): Promise<FsResult<FsFileSystem>> {
|
|
207
|
+
let attempt = -1;
|
|
208
|
+
while (true) {
|
|
209
|
+
attempt++;
|
|
210
|
+
const inited = await fs.init();
|
|
211
|
+
if (!inited.err) {
|
|
212
|
+
return inited;
|
|
213
|
+
}
|
|
214
|
+
if (!retryHandler) {
|
|
215
|
+
return inited;
|
|
216
|
+
}
|
|
217
|
+
const shouldRetry = await retryHandler(inited.err, attempt);
|
|
218
|
+
if (shouldRetry.err) {
|
|
219
|
+
// retry handler failed
|
|
220
|
+
return shouldRetry;
|
|
221
|
+
}
|
|
222
|
+
if (!shouldRetry.val) {
|
|
223
|
+
// should not retry
|
|
224
|
+
return inited;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Wrapper for window.showDirectoryPicker */
|
|
230
|
+
function showDirectoryPicker(write: boolean): Promise<FileSystemHandle> {
|
|
231
|
+
// @ts-expect-error showDirectoryPicker is not in the TS lib
|
|
232
|
+
return globalThis.showDirectoryPicker({
|
|
233
|
+
mode: write ? "readwrite" : "read",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if an error is an AbortError from Chromium
|
|
239
|
+
*
|
|
240
|
+
* @param e The error to check
|
|
241
|
+
*/
|
|
242
|
+
function isAbortError(e: unknown): boolean {
|
|
243
|
+
return (
|
|
244
|
+
!!e && typeof e === "object" && "name" in e && e.name === "AbortError"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Wrapper for DataTransferItem.getAsFileSystemHandle */
|
|
249
|
+
async function getAsFileSystemHandle(
|
|
250
|
+
item: DataTransferItem,
|
|
251
|
+
): Promise<FileSystemHandle> {
|
|
252
|
+
// @ts-expect-error getAsFileSystemHandle is not in the TS lib
|
|
253
|
+
const handle = await item.getAsFileSystemHandle();
|
|
254
|
+
if (!handle) {
|
|
255
|
+
throw new Error("handle is null");
|
|
256
|
+
}
|
|
257
|
+
return handle;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Wrapper for DataTransferItem.webkitGetAsEntry */
|
|
261
|
+
function webkitGetAsEntry(item: DataTransferItem): FileSystemEntry {
|
|
262
|
+
const entry = item.webkitGetAsEntry();
|
|
263
|
+
if (!entry) {
|
|
264
|
+
throw new Error("entry is null");
|
|
265
|
+
}
|
|
266
|
+
return entry;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createFromFileSystemHandle(
|
|
270
|
+
handle: FileSystemHandle,
|
|
271
|
+
write: boolean,
|
|
272
|
+
): FsResult<FsFileSystemUninit> {
|
|
273
|
+
if (handle.kind !== "directory") {
|
|
274
|
+
const err = fsErr(FsErr.IsFile, "Expected directory");
|
|
275
|
+
return { err };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fs = new FsImplHandleAPI(
|
|
279
|
+
handle.name,
|
|
280
|
+
handle as FileSystemDirectoryHandle,
|
|
281
|
+
write,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return { val: fs };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createFromFileSystemEntry(
|
|
288
|
+
entry: FileSystemEntry,
|
|
289
|
+
): FsResult<FsFileSystemUninit> {
|
|
290
|
+
if (entry.isFile || !entry.isDirectory) {
|
|
291
|
+
const err = fsErr(FsErr.IsFile, "Expected directory");
|
|
292
|
+
return { err };
|
|
293
|
+
}
|
|
294
|
+
const fs = new FsImplEntryAPI(
|
|
295
|
+
entry.name,
|
|
296
|
+
entry as FileSystemDirectoryEntry,
|
|
297
|
+
);
|
|
298
|
+
return { val: fs };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createFromFileList(files: FileList): FsResult<FsFileSystemUninit> {
|
|
302
|
+
if (!files.length) {
|
|
303
|
+
const err = fsFail("Expected at least one file");
|
|
304
|
+
return { err };
|
|
305
|
+
}
|
|
306
|
+
return { val: new FsImplFileAPI(files) };
|
|
307
|
+
}
|
package/src/fs/FsPath.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities
|
|
3
|
+
*
|
|
4
|
+
* The library has the following path standard:
|
|
5
|
+
* - All paths are relative (without leading /) to the root
|
|
6
|
+
* of the file system (i.e. the uploaded directory)
|
|
7
|
+
* - Paths are always separated by /
|
|
8
|
+
* - Empty string denotes root
|
|
9
|
+
* - Paths cannot lead outside of root
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { FsErr, type FsResult, fsErr } from "./FsError.ts";
|
|
15
|
+
|
|
16
|
+
/** Get the root path. Current implementation is empty string. */
|
|
17
|
+
export function fsRoot(): string {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Check if a path is the root directory, also handles badly formatted paths like ".///../" */
|
|
22
|
+
export function fsIsRoot(p: string): boolean {
|
|
23
|
+
if (!p) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < p.length; i++) {
|
|
27
|
+
if (p[i] !== "/" || p[i] !== "." || p[i] !== "\\") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the base name of a path (i.e. remove the last component)
|
|
36
|
+
*
|
|
37
|
+
* If this path is the root directory, return InvalidPath.
|
|
38
|
+
*/
|
|
39
|
+
export function fsGetBase(p: string): FsResult<string> {
|
|
40
|
+
if (fsIsRoot(p)) {
|
|
41
|
+
const err = fsErr(FsErr.InvalidPath, "Trying to get the base of root");
|
|
42
|
+
return { err };
|
|
43
|
+
}
|
|
44
|
+
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
45
|
+
if (i < 0) {
|
|
46
|
+
return { val: fsRoot() };
|
|
47
|
+
}
|
|
48
|
+
return { val: p.substring(0, i) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the name of a path (i.e. the last component)
|
|
53
|
+
*
|
|
54
|
+
* Returns the last component of the path.
|
|
55
|
+
* Does not include leading or trailing slashes.
|
|
56
|
+
*
|
|
57
|
+
* If this path is the root directory, return IsRoot.
|
|
58
|
+
*/
|
|
59
|
+
export function fsGetName(p: string): FsResult<string> {
|
|
60
|
+
p = stripTrailingSlashes(p);
|
|
61
|
+
if (fsIsRoot(p)) {
|
|
62
|
+
const err = fsErr(FsErr.IsRoot, "Root directory has no name");
|
|
63
|
+
return { err };
|
|
64
|
+
}
|
|
65
|
+
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
66
|
+
if (i < 0) {
|
|
67
|
+
return { val: p };
|
|
68
|
+
}
|
|
69
|
+
return { val: p.substring(i + 1) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalize .. and . in a path
|
|
74
|
+
*
|
|
75
|
+
* Returns InvalidPath if the path tries to escape the root directory.
|
|
76
|
+
*/
|
|
77
|
+
export function fsNormalize(p: string): FsResult<string> {
|
|
78
|
+
let s = fsRoot();
|
|
79
|
+
for (const comp of fsComponents(p)) {
|
|
80
|
+
if (comp === "..") {
|
|
81
|
+
const base = fsGetBase(s);
|
|
82
|
+
if (base.err) {
|
|
83
|
+
return base;
|
|
84
|
+
}
|
|
85
|
+
s = base.val;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
s = fsJoin(s, comp);
|
|
89
|
+
}
|
|
90
|
+
return { val: s };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Join two paths */
|
|
94
|
+
export function fsJoin(p1: string, p2: string): string {
|
|
95
|
+
if (fsIsRoot(p1)) {
|
|
96
|
+
return p2;
|
|
97
|
+
}
|
|
98
|
+
return p1 + "/" + p2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Iterate through the components of a path. Empty components and . are skipped */
|
|
102
|
+
export function* fsComponents(p: string): Iterable<string> {
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < p.length) {
|
|
105
|
+
let nextSlash = p.indexOf("/", i);
|
|
106
|
+
if (nextSlash < 0) {
|
|
107
|
+
nextSlash = p.length;
|
|
108
|
+
}
|
|
109
|
+
let nextBackslash = p.indexOf("\\", i);
|
|
110
|
+
if (nextBackslash < 0) {
|
|
111
|
+
nextBackslash = p.length;
|
|
112
|
+
}
|
|
113
|
+
let j = Math.min(nextSlash, nextBackslash);
|
|
114
|
+
if (j < 0) {
|
|
115
|
+
j = p.length;
|
|
116
|
+
}
|
|
117
|
+
const c = p.substring(i, j);
|
|
118
|
+
if (c && c !== ".") {
|
|
119
|
+
yield c;
|
|
120
|
+
}
|
|
121
|
+
i = j + 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Remove trailing slashes from a path */
|
|
126
|
+
function stripTrailingSlashes(p: string): string {
|
|
127
|
+
let i = p.length - 1;
|
|
128
|
+
for (; i >= 0; i--) {
|
|
129
|
+
if (p[i] !== "/" && p[i] !== "\\") {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (i === p.length - 1) {
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
return p.substring(0, i + 1);
|
|
137
|
+
}
|
package/src/fs/FsSave.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// workaround for CommonJS
|
|
2
|
+
import pkg from "file-saver";
|
|
3
|
+
const { saveAs } = pkg;
|
|
4
|
+
|
|
5
|
+
/** Save (download) a file using Blob */
|
|
6
|
+
export function fsSave(content: string | Uint8Array, filename: string) {
|
|
7
|
+
const blob = new Blob([content], {
|
|
8
|
+
// maybe lying, but should be fine
|
|
9
|
+
type: "text/plain;charset=utf-8",
|
|
10
|
+
});
|
|
11
|
+
saveAs(blob, filename);
|
|
12
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** What is supported by the current environment */
|
|
2
|
+
export type FsSupportStatus = {
|
|
3
|
+
/** Returned by window.isSecureContext */
|
|
4
|
+
isSecureContext: boolean;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The implementation for FsFileSystem used
|
|
8
|
+
*
|
|
9
|
+
* See README.md for more information
|
|
10
|
+
*/
|
|
11
|
+
implementation: "File" | "FileSystemAccess" | "FileEntry";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Get which implementation will be used for the current environment */
|
|
15
|
+
export function fsGetSupportStatus(): FsSupportStatus {
|
|
16
|
+
if (isFileSystemAccessSupported()) {
|
|
17
|
+
return {
|
|
18
|
+
isSecureContext: globalThis.isSecureContext,
|
|
19
|
+
implementation: "FileSystemAccess",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (isFileEntrySupported()) {
|
|
23
|
+
return {
|
|
24
|
+
isSecureContext: globalThis.isSecureContext,
|
|
25
|
+
implementation: "FileEntry",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
isSecureContext: !!globalThis && globalThis.isSecureContext,
|
|
31
|
+
implementation: "File",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isFileSystemAccessSupported() {
|
|
36
|
+
if (!globalThis) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (!globalThis.isSecureContext) {
|
|
40
|
+
// In Chrome, you can still access the APIs but they just crash the page entirely
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (!globalThis.FileSystemDirectoryHandle) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!globalThis.FileSystemFileHandle) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// since TSlib doesn't have these, let's check here
|
|
52
|
+
|
|
53
|
+
// @ts-expect-error FileSystemDirectoryHandle should have a values() method
|
|
54
|
+
if (!globalThis.FileSystemDirectoryHandle.prototype.values) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// @ts-expect-error window should have showDirectoryPicker
|
|
59
|
+
if (!globalThis.showDirectoryPicker) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isFileEntrySupported(): boolean {
|
|
67
|
+
if (!globalThis) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Chrome/Edge has this but it's named DirectoryEntry
|
|
72
|
+
// AND, they don't work (I forgot how exactly they don't work)
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
navigator &&
|
|
76
|
+
navigator.userAgent &&
|
|
77
|
+
navigator.userAgent.includes("Chrome")
|
|
78
|
+
) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!globalThis.FileSystemDirectoryEntry) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!globalThis.FileSystemFileEntry) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
package/src/fs/index.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High level browser to file system integration library.
|
|
3
|
+
*
|
|
4
|
+
* This library integrates the `File`, `FileEntry` and `FileSystemAccess` API
|
|
5
|
+
* to provide different levels of integration with file system in web apps.
|
|
6
|
+
*
|
|
7
|
+
* Basically, user can select a directory as a mount point, and browser can access
|
|
8
|
+
* read and sometimes write in the directory.
|
|
9
|
+
*
|
|
10
|
+
* ## Support
|
|
11
|
+
* Use `fsGetSupportStatus()` to inspect which implementation will be used.
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { fsGetSupportStatus } from "@pistonite/pure/fs";
|
|
15
|
+
*
|
|
16
|
+
* const { implementation, isSecureContext } = fsGetSupportStatus();
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* `implementation` can be 3 values:
|
|
20
|
+
* 1. `FileSystemAccess`: This is used for Google Chrome and Edge, and possibly other browsers, under secure context.
|
|
21
|
+
* 2. `FileEntry`: This is used for Firefox when the FS is mounted through a drag-and-drop interface.
|
|
22
|
+
* 3. `File`: This is used for Firefox when the FS is mounted by a directory picker dialog
|
|
23
|
+
*
|
|
24
|
+
* The implementation is also chosen in this order and the first supported one is selected. If you are on Chrome/Edge and `FileSystemAccess` is not used, you can use `isSecureContext` to narrow down the reason.
|
|
25
|
+
*
|
|
26
|
+
* If you are wondering why Safari is not mentioned, it's because Apple made it so I have to buy a Mac to test, which I didn't.
|
|
27
|
+
*
|
|
28
|
+
* After you get an instance of `FsFileSystem`, you can use `capabilities` to inspect
|
|
29
|
+
* what is and is not supported.
|
|
30
|
+
*
|
|
31
|
+
* See `FsCapabilities` for more info. This is the support matrix:
|
|
32
|
+
* |Implementation|`write`?|`live`?|
|
|
33
|
+
* |--------------|--------|-------|
|
|
34
|
+
* |`FileSystemAccess`|Yes*|Yes |
|
|
35
|
+
* |`FileEntry` |No |Yes |
|
|
36
|
+
* |`File` |No |No |
|
|
37
|
+
*
|
|
38
|
+
* `*` = Need to request permission from user.
|
|
39
|
+
*
|
|
40
|
+
*
|
|
41
|
+
* ## Usage
|
|
42
|
+
* First you need to get an instance of `FsFileSystem`. You can:
|
|
43
|
+
* 1. Call `fsOpenRead()` or `fsOpenReadWrite()` to show a directory picker,
|
|
44
|
+
* 2. Call `fsOpenReadFrom` or `fsOpenReadWriteFrom()` and pass in a `DataTransferItem` from a drag-and-drop interface.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: `fsOpenReadWrite` does not guarantee the implementation supports writing. You should check
|
|
47
|
+
* with `capabilities` afterward.
|
|
48
|
+
*
|
|
49
|
+
* This is an example drop zone implementation in TypeScript
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { fsOpenReadWriteFrom } from "@pistonite/pure/fs";
|
|
52
|
+
*
|
|
53
|
+
* const div = document.createElement("div");
|
|
54
|
+
*
|
|
55
|
+
* div.addEventListener("dragover", (e) => {
|
|
56
|
+
* if (e.dataTransfer) {
|
|
57
|
+
* // setting this will allow dropping
|
|
58
|
+
* e.dataTransfer.dropEffect = "link";
|
|
59
|
+
* }
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* div.addEventListener("drop", async (e) => {
|
|
63
|
+
* const item = e.dataTransfer?.items[0];
|
|
64
|
+
* if (!item) {
|
|
65
|
+
* console.error("no item");
|
|
66
|
+
* return;
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* const result = await fsOpenReadWriteFrom(item);
|
|
70
|
+
* if (result.err) {
|
|
71
|
+
* console.error(result.err);
|
|
72
|
+
* return;
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* const fs = result.val;
|
|
76
|
+
* const { write, live } = fs.capabilities;
|
|
77
|
+
* // check capabilities and use fs
|
|
78
|
+
* // ...
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* ## Retry open
|
|
83
|
+
* You can pass in a retry handler and return true to retry, when opening fails.
|
|
84
|
+
* The handler is async so you can ask user.
|
|
85
|
+
*
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { FsError, FsResult } from "@pistonite/pure/fs";
|
|
88
|
+
*
|
|
89
|
+
* async function shouldRetry(error: FsError, attempt: number): Promise<FsResult<boolean>> {
|
|
90
|
+
* if (attempt < 10 && error === FsError.PermissionDenied) {
|
|
91
|
+
* alert("you must give permission to use this feature!");
|
|
92
|
+
* return { val: true };
|
|
93
|
+
* }
|
|
94
|
+
* return { val: false };
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* const result = await fsOpenReadWrite(shouldRetry);
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @module
|
|
101
|
+
*/
|
|
102
|
+
export { fsSave } from "./FsSave.ts";
|
|
103
|
+
export {
|
|
104
|
+
fsOpenRead,
|
|
105
|
+
fsOpenReadWrite,
|
|
106
|
+
fsOpenReadFrom,
|
|
107
|
+
fsOpenReadWriteFrom,
|
|
108
|
+
} from "./FsOpen.ts";
|
|
109
|
+
export { fsGetSupportStatus } from "./FsSupportStatus.ts";
|
|
110
|
+
export {
|
|
111
|
+
fsRoot,
|
|
112
|
+
fsIsRoot,
|
|
113
|
+
fsGetBase,
|
|
114
|
+
fsGetName,
|
|
115
|
+
fsNormalize,
|
|
116
|
+
fsJoin,
|
|
117
|
+
fsComponents,
|
|
118
|
+
} from "./FsPath.ts";
|
|
119
|
+
export { FsErr, fsErr, fsFail } from "./FsError.ts";
|
|
120
|
+
|
|
121
|
+
export type { FsOpenRetryHandler } from "./FsOpen.ts";
|
|
122
|
+
export type { FsSupportStatus } from "./FsSupportStatus.ts";
|
|
123
|
+
export type {
|
|
124
|
+
FsFileSystem,
|
|
125
|
+
FsFileSystemUninit,
|
|
126
|
+
FsCapabilities,
|
|
127
|
+
} from "./FsFileSystem.ts";
|
|
128
|
+
export type { FsFile } from "./FsFile.ts";
|
|
129
|
+
export type { FsError, FsResult, FsVoid } from "./FsError.ts";
|