sliftutils 0.1.1
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/.cursorrules +161 -0
- package/.eslintrc.js +38 -0
- package/.vscode/settings.json +39 -0
- package/bundler/buffer.js +2370 -0
- package/bundler/bundleEntry.ts +32 -0
- package/bundler/bundleEntryCaller.ts +8 -0
- package/bundler/bundleRequire.ts +244 -0
- package/bundler/bundleWrapper.ts +115 -0
- package/bundler/bundler.ts +72 -0
- package/bundler/flattenSourceMaps.ts +0 -0
- package/bundler/sourceMaps.ts +261 -0
- package/misc/environment.ts +11 -0
- package/misc/types.ts +3 -0
- package/misc/zip.ts +37 -0
- package/package.json +24 -0
- package/spec.txt +33 -0
- package/storage/CachedStorage.ts +32 -0
- package/storage/DelayedStorage.ts +30 -0
- package/storage/DiskCollection.ts +272 -0
- package/storage/FileFolderAPI.tsx +427 -0
- package/storage/IStorage.ts +40 -0
- package/storage/IndexedDBFileFolderAPI.ts +170 -0
- package/storage/JSONStorage.ts +35 -0
- package/storage/PendingManager.tsx +63 -0
- package/storage/PendingStorage.ts +47 -0
- package/storage/PrivateFileSystemStorage.ts +192 -0
- package/storage/StorageObservable.ts +122 -0
- package/storage/TransactionStorage.ts +485 -0
- package/storage/fileSystemPointer.ts +81 -0
- package/storage/storage.d.ts +41 -0
- package/tsconfig.json +31 -0
- package/web/DropdownCustom.tsx +150 -0
- package/web/FullscreenModal.tsx +75 -0
- package/web/GenericFormat.tsx +186 -0
- package/web/Input.tsx +350 -0
- package/web/InputLabel.tsx +288 -0
- package/web/InputPicker.tsx +158 -0
- package/web/LocalStorageParam.ts +56 -0
- package/web/SyncedController.ts +405 -0
- package/web/SyncedLoadingIndicator.tsx +37 -0
- package/web/Table.tsx +188 -0
- package/web/URLParam.ts +84 -0
- package/web/asyncObservable.ts +40 -0
- package/web/colors.tsx +14 -0
- package/web/mobxTyped.ts +29 -0
- package/web/modal.tsx +18 -0
- package/web/observer.tsx +35 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { isNode } from "typesafecss";
|
|
2
|
+
import { DelayedStorage } from "./DelayedStorage";
|
|
3
|
+
import { FileStorage, getFileStorage, getFileStorageNested } from "./FileFolderAPI";
|
|
4
|
+
import { IStorage, IStorageSync } from "./IStorage";
|
|
5
|
+
import { JSONStorage } from "./JSONStorage";
|
|
6
|
+
import { StorageSync } from "./StorageObservable";
|
|
7
|
+
import { TransactionStorage } from "./TransactionStorage";
|
|
8
|
+
import { PendingStorage } from "./PendingStorage";
|
|
9
|
+
import { isDefined } from "../misc/types";
|
|
10
|
+
import { PrivateFileSystemStorage } from "./PrivateFileSystemStorage";
|
|
11
|
+
import { isInChromeExtension } from "../misc/environment";
|
|
12
|
+
|
|
13
|
+
export class DiskCollection<T> implements IStorageSync<T> {
|
|
14
|
+
constructor(
|
|
15
|
+
private collectionName: string,
|
|
16
|
+
private writeDelay?: number,
|
|
17
|
+
) {
|
|
18
|
+
}
|
|
19
|
+
public transactionStorage: TransactionStorage | undefined;
|
|
20
|
+
async initStorage(): Promise<IStorage<T>> {
|
|
21
|
+
// If a Chrome extension, just return null.
|
|
22
|
+
if (isInChromeExtension()) return null as any;
|
|
23
|
+
let fileStorage = await getFileStorage();
|
|
24
|
+
let collections = await fileStorage.folder.getStorage("collections");
|
|
25
|
+
let curCollection = await collections.folder.getStorage(this.collectionName);
|
|
26
|
+
let baseStorage = new TransactionStorage(curCollection, this.collectionName, this.writeDelay);
|
|
27
|
+
this.transactionStorage = baseStorage;
|
|
28
|
+
return new JSONStorage<T>(baseStorage);
|
|
29
|
+
}
|
|
30
|
+
public baseStorage = this.initStorage();
|
|
31
|
+
private synced = new StorageSync(
|
|
32
|
+
new PendingStorage(`Collection (${this.collectionName})`,
|
|
33
|
+
new DelayedStorage<T>(this.baseStorage)
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
public get(key: string): T | undefined {
|
|
38
|
+
return this.synced.get(key);
|
|
39
|
+
}
|
|
40
|
+
public async getPromise(key: string): Promise<T | undefined> {
|
|
41
|
+
let base = await this.baseStorage;
|
|
42
|
+
return base.get(key);
|
|
43
|
+
}
|
|
44
|
+
public set(key: string, value: T): void {
|
|
45
|
+
this.synced.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
public remove(key: string): void {
|
|
48
|
+
this.synced.remove(key);
|
|
49
|
+
}
|
|
50
|
+
public getKeys(): string[] {
|
|
51
|
+
return this.synced.getKeys();
|
|
52
|
+
}
|
|
53
|
+
public getKeysPromise(): Promise<string[]> {
|
|
54
|
+
return this.synced.getKeysPromise();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public getEntries(): [string, T][] {
|
|
58
|
+
let keys = this.getKeys();
|
|
59
|
+
return keys.map(key => [key, this.get(key)]).filter(([_, value]) => isDefined(value)) as [string, T][];
|
|
60
|
+
}
|
|
61
|
+
public getValues(): T[] {
|
|
62
|
+
let keys = this.getKeys();
|
|
63
|
+
return keys.map(key => this.get(key)).filter(isDefined);
|
|
64
|
+
}
|
|
65
|
+
public async getValuesPromise(): Promise<T[]> {
|
|
66
|
+
let keys = await this.getKeysPromise();
|
|
67
|
+
let values: T[] = [];
|
|
68
|
+
for (let key of keys) {
|
|
69
|
+
let value = await this.getPromise(key);
|
|
70
|
+
if (isDefined(value)) {
|
|
71
|
+
values.push(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return values;
|
|
75
|
+
}
|
|
76
|
+
public getInfo(key: string) {
|
|
77
|
+
return this.synced.getInfo(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async reset() {
|
|
81
|
+
await this.synced.reset();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class DiskCollectionBrowser<T> implements IStorageSync<T> {
|
|
86
|
+
constructor(
|
|
87
|
+
private collectionName: string,
|
|
88
|
+
private writeDelay?: number,
|
|
89
|
+
) {
|
|
90
|
+
}
|
|
91
|
+
public transactionStorage: TransactionStorage | undefined;
|
|
92
|
+
async initStorage(): Promise<IStorage<T>> {
|
|
93
|
+
if (isNode()) return undefined as any;
|
|
94
|
+
let curCollection = await new PrivateFileSystemStorage(`collections/${this.collectionName}`);
|
|
95
|
+
let baseStorage = new TransactionStorage(curCollection, this.collectionName);
|
|
96
|
+
return new JSONStorage<T>(baseStorage);
|
|
97
|
+
}
|
|
98
|
+
public baseStorage = this.initStorage();
|
|
99
|
+
private synced = new StorageSync(
|
|
100
|
+
new PendingStorage(`Collection (${this.collectionName})`,
|
|
101
|
+
new DelayedStorage<T>(this.baseStorage)
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
public get(key: string): T | undefined {
|
|
106
|
+
return this.synced.get(key);
|
|
107
|
+
}
|
|
108
|
+
public async getPromise(key: string): Promise<T | undefined> {
|
|
109
|
+
let base = await this.baseStorage;
|
|
110
|
+
return base.get(key);
|
|
111
|
+
}
|
|
112
|
+
public set(key: string, value: T): void {
|
|
113
|
+
this.synced.set(key, value);
|
|
114
|
+
}
|
|
115
|
+
public remove(key: string): void {
|
|
116
|
+
this.synced.remove(key);
|
|
117
|
+
}
|
|
118
|
+
public getKeys(): string[] {
|
|
119
|
+
return this.synced.getKeys();
|
|
120
|
+
}
|
|
121
|
+
public getKeysPromise(): Promise<string[]> {
|
|
122
|
+
return this.synced.getKeysPromise();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public getEntries(): [string, T][] {
|
|
126
|
+
let keys = this.getKeys();
|
|
127
|
+
return keys.map(key => [key, this.get(key)]).filter(([_, value]) => isDefined(value)) as [string, T][];
|
|
128
|
+
}
|
|
129
|
+
public getValues(): T[] {
|
|
130
|
+
let keys = this.getKeys();
|
|
131
|
+
return keys.map(key => this.get(key)).filter(isDefined);
|
|
132
|
+
}
|
|
133
|
+
public async getValuesPromise(): Promise<T[]> {
|
|
134
|
+
let keys = await this.getKeysPromise();
|
|
135
|
+
let values: T[] = [];
|
|
136
|
+
for (let key of keys) {
|
|
137
|
+
let value = await this.getPromise(key);
|
|
138
|
+
if (isDefined(value)) {
|
|
139
|
+
values.push(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return values;
|
|
143
|
+
}
|
|
144
|
+
public getInfo(key: string) {
|
|
145
|
+
return this.synced.getInfo(key);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async reset() {
|
|
149
|
+
await this.synced.reset();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class DiskCollectionPromise<T> implements IStorage<T> {
|
|
154
|
+
constructor(
|
|
155
|
+
private collectionName: string,
|
|
156
|
+
private writeDelay?: number,
|
|
157
|
+
) { }
|
|
158
|
+
async initStorage(): Promise<IStorage<T>> {
|
|
159
|
+
let fileStorage = await getFileStorage();
|
|
160
|
+
let collections = await fileStorage.folder.getStorage("collections");
|
|
161
|
+
let curCollection = await collections.folder.getStorage(this.collectionName);
|
|
162
|
+
let baseStorage = new TransactionStorage(curCollection, this.collectionName, this.writeDelay);
|
|
163
|
+
return new JSONStorage<T>(baseStorage);
|
|
164
|
+
}
|
|
165
|
+
private synced = (
|
|
166
|
+
new PendingStorage(`Collection (${this.collectionName})`,
|
|
167
|
+
new DelayedStorage<T>(this.initStorage())
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
public async get(key: string): Promise<T | undefined> {
|
|
172
|
+
return await this.synced.get(key);
|
|
173
|
+
}
|
|
174
|
+
public async set(key: string, value: T): Promise<void> {
|
|
175
|
+
await this.synced.set(key, value);
|
|
176
|
+
}
|
|
177
|
+
public async remove(key: string): Promise<void> {
|
|
178
|
+
await this.synced.remove(key);
|
|
179
|
+
}
|
|
180
|
+
public async getKeys(): Promise<string[]> {
|
|
181
|
+
return await this.synced.getKeys();
|
|
182
|
+
}
|
|
183
|
+
public async getInfo(key: string) {
|
|
184
|
+
return await this.synced.getInfo(key);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async reset() {
|
|
188
|
+
await this.synced.reset();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class DiskCollectionRaw implements IStorage<Buffer> {
|
|
193
|
+
constructor(private collectionName: string) { }
|
|
194
|
+
async initStorage(): Promise<IStorage<Buffer>> {
|
|
195
|
+
let fileStorage = await getFileStorage();
|
|
196
|
+
let collections = await fileStorage.folder.getStorage("collections");
|
|
197
|
+
let baseStorage = await collections.folder.getStorage(this.collectionName);
|
|
198
|
+
return baseStorage;
|
|
199
|
+
}
|
|
200
|
+
private synced = (
|
|
201
|
+
new PendingStorage(`Collection (${this.collectionName})`,
|
|
202
|
+
new DelayedStorage(this.initStorage())
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
public async get(key: string): Promise<Buffer | undefined> {
|
|
207
|
+
return await this.synced.get(key);
|
|
208
|
+
}
|
|
209
|
+
public async set(key: string, value: Buffer): Promise<void> {
|
|
210
|
+
await this.synced.set(key, value);
|
|
211
|
+
}
|
|
212
|
+
public async remove(key: string): Promise<void> {
|
|
213
|
+
await this.synced.remove(key);
|
|
214
|
+
}
|
|
215
|
+
public async getKeys(): Promise<string[]> {
|
|
216
|
+
return await this.synced.getKeys();
|
|
217
|
+
}
|
|
218
|
+
public async getInfo(key: string) {
|
|
219
|
+
return await this.synced.getInfo(key);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async reset() {
|
|
223
|
+
await this.synced.reset();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export class DiskCollectionRawBrowser {
|
|
228
|
+
constructor(private collectionName: string) { }
|
|
229
|
+
async initStorage(): Promise<IStorage<Buffer>> {
|
|
230
|
+
return await new PrivateFileSystemStorage(`collections/${this.collectionName}`);
|
|
231
|
+
}
|
|
232
|
+
private synced = new StorageSync(
|
|
233
|
+
new PendingStorage(`Collection (${this.collectionName})`,
|
|
234
|
+
new DelayedStorage(this.initStorage())
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
public get(key: string): Buffer | undefined {
|
|
239
|
+
return this.synced.get(key);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public async getPromise(key: string): Promise<Buffer | undefined> {
|
|
243
|
+
return await this.synced.get(key);
|
|
244
|
+
}
|
|
245
|
+
public set(key: string, value: Buffer) {
|
|
246
|
+
this.synced.set(key, value);
|
|
247
|
+
}
|
|
248
|
+
public async getKeys(): Promise<string[]> {
|
|
249
|
+
return await this.synced.getKeys();
|
|
250
|
+
}
|
|
251
|
+
public async getInfo(key: string) {
|
|
252
|
+
return await this.synced.getInfo(key);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public async reset() {
|
|
256
|
+
await this.synced.reset();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function newFileStorageBufferSyncer(folder = "") {
|
|
261
|
+
let base = new PendingStorage(`FileStorageBufferSyncer`,
|
|
262
|
+
new DelayedStorage(getFileStorageNested(folder))
|
|
263
|
+
);
|
|
264
|
+
return new StorageSync(base);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function newFileStorageJSONSyncer<T>(folder = "") {
|
|
268
|
+
let base = new PendingStorage(`FileStorageJSONSyncer`,
|
|
269
|
+
new DelayedStorage(getFileStorageNested(folder))
|
|
270
|
+
);
|
|
271
|
+
return new StorageSync(new JSONStorage<T>(base));
|
|
272
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { getFileSystemPointer, storeFileSystemPointer } from "./fileSystemPointer";
|
|
3
|
+
import { observable } from "mobx";
|
|
4
|
+
import { observer } from "../web/observer";
|
|
5
|
+
import { cache, lazy } from "socket-function/src/caching";
|
|
6
|
+
import { css, isNode } from "typesafecss";
|
|
7
|
+
import { IStorageRaw } from "./IStorage";
|
|
8
|
+
import { runInSerial } from "socket-function/src/batching";
|
|
9
|
+
import { getFileStorageIndexDB } from "./IndexedDBFileFolderAPI";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
// NOTE: IndexedDB is required for iOS, at least. We MIGHT want to make
|
|
14
|
+
// this a user supported toggle too, so they can choose during runtime if they want it.
|
|
15
|
+
// DO NOT enable this is isNode
|
|
16
|
+
const USE_INDEXED_DB = false;
|
|
17
|
+
|
|
18
|
+
type FileWrapper = {
|
|
19
|
+
getFile(): Promise<{
|
|
20
|
+
size: number;
|
|
21
|
+
lastModified: number;
|
|
22
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
23
|
+
}>;
|
|
24
|
+
createWritable(config?: { keepExistingData?: boolean }): Promise<{
|
|
25
|
+
seek(offset: number): Promise<void>;
|
|
26
|
+
write(value: Buffer): Promise<void>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
type DirectoryWrapper = {
|
|
31
|
+
removeEntry(key: string, options?: { recursive?: boolean }): Promise<void>;
|
|
32
|
+
getFileHandle(key: string, options?: { create?: boolean }): Promise<FileWrapper>;
|
|
33
|
+
getDirectoryHandle(key: string, options?: { create?: boolean }): Promise<DirectoryWrapper>;
|
|
34
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<[string, {
|
|
35
|
+
kind: "file";
|
|
36
|
+
name: string;
|
|
37
|
+
getFile(): Promise<FileWrapper>;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "directory";
|
|
40
|
+
name: string;
|
|
41
|
+
getDirectoryHandle(key: string, options?: { create?: boolean }): Promise<DirectoryWrapper>;
|
|
42
|
+
}]>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let displayData = observable({
|
|
46
|
+
ui: undefined as undefined | preact.ComponentChildren,
|
|
47
|
+
}, undefined, { deep: false });
|
|
48
|
+
|
|
49
|
+
const storageKey = "syncFileSystemCamera3";
|
|
50
|
+
|
|
51
|
+
@observer
|
|
52
|
+
class DirectoryPrompter extends preact.Component {
|
|
53
|
+
render() {
|
|
54
|
+
if (!displayData.ui) return undefined;
|
|
55
|
+
return (
|
|
56
|
+
<div className={
|
|
57
|
+
css.position("fixed").pos(0, 0).size("100vw", "100vh")
|
|
58
|
+
.zIndex(1)
|
|
59
|
+
.background("white")
|
|
60
|
+
.center
|
|
61
|
+
.fontSize(40)
|
|
62
|
+
}>
|
|
63
|
+
{displayData.ui}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class NodeJSFileHandleWrapper implements FileWrapper {
|
|
70
|
+
constructor(private filePath: string) {
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getFile() {
|
|
74
|
+
const stats = await fs.promises.stat(this.filePath);
|
|
75
|
+
return {
|
|
76
|
+
size: stats.size,
|
|
77
|
+
lastModified: stats.mtimeMs,
|
|
78
|
+
arrayBuffer: async () => {
|
|
79
|
+
const buffer = await fs.promises.readFile(this.filePath);
|
|
80
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async createWritable(config?: { keepExistingData?: boolean }) {
|
|
86
|
+
let fileHandle: fs.promises.FileHandle;
|
|
87
|
+
const flags = config?.keepExistingData ? "r+" : "w";
|
|
88
|
+
|
|
89
|
+
// Ensure the directory exists
|
|
90
|
+
await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
91
|
+
|
|
92
|
+
// Open or create the file
|
|
93
|
+
if (config?.keepExistingData && await fs.promises.access(this.filePath).then(() => true).catch(() => false)) {
|
|
94
|
+
fileHandle = await fs.promises.open(this.filePath, flags);
|
|
95
|
+
} else {
|
|
96
|
+
fileHandle = await fs.promises.open(this.filePath, "w");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let position = 0;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
seek: async (offset: number) => {
|
|
103
|
+
position = offset;
|
|
104
|
+
},
|
|
105
|
+
write: async (value: Buffer) => {
|
|
106
|
+
await fileHandle.write(value, 0, value.length, position);
|
|
107
|
+
position += value.length;
|
|
108
|
+
},
|
|
109
|
+
close: async () => {
|
|
110
|
+
await fileHandle.close();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
117
|
+
constructor(private rootPath: string) {
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async removeEntry(key: string, options?: { recursive?: boolean }) {
|
|
121
|
+
const entryPath = path.join(this.rootPath, key);
|
|
122
|
+
if (options?.recursive) {
|
|
123
|
+
await fs.promises.rm(entryPath, { recursive: true, force: true });
|
|
124
|
+
} else {
|
|
125
|
+
await fs.promises.unlink(entryPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getFileHandle(key: string, options?: { create?: boolean }): Promise<FileWrapper> {
|
|
130
|
+
const filePath = path.join(this.rootPath, key);
|
|
131
|
+
|
|
132
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
133
|
+
|
|
134
|
+
if (!exists && options?.create) {
|
|
135
|
+
// Ensure the directory exists
|
|
136
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
137
|
+
// Create the file
|
|
138
|
+
await fs.promises.writeFile(filePath, Buffer.alloc(0));
|
|
139
|
+
} else if (!exists) {
|
|
140
|
+
throw new Error(`File not found: ${filePath}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new NodeJSFileHandleWrapper(filePath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getDirectoryHandle(key: string, options?: { create?: boolean }): Promise<DirectoryWrapper> {
|
|
147
|
+
const dirPath = path.join(this.rootPath, key);
|
|
148
|
+
|
|
149
|
+
if (options?.create) {
|
|
150
|
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
151
|
+
} else {
|
|
152
|
+
const exists = await fs.promises.access(dirPath).then(() => true).catch(() => false);
|
|
153
|
+
if (!exists) {
|
|
154
|
+
throw new Error(`Directory not found: ${dirPath}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new NodeJSDirectoryHandleWrapper(dirPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async *[Symbol.asyncIterator](): AsyncIterableIterator<[string, {
|
|
162
|
+
kind: "file";
|
|
163
|
+
name: string;
|
|
164
|
+
getFile(): Promise<FileWrapper>;
|
|
165
|
+
} | {
|
|
166
|
+
kind: "directory";
|
|
167
|
+
name: string;
|
|
168
|
+
getDirectoryHandle(key: string, options?: { create?: boolean }): Promise<DirectoryWrapper>;
|
|
169
|
+
}]> {
|
|
170
|
+
// Ensure directory exists
|
|
171
|
+
await fs.promises.mkdir(this.rootPath, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const entries = await fs.promises.readdir(this.rootPath, { withFileTypes: true });
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (entry.isFile()) {
|
|
177
|
+
yield [entry.name, {
|
|
178
|
+
kind: "file",
|
|
179
|
+
name: entry.name,
|
|
180
|
+
getFile: async () => new NodeJSFileHandleWrapper(path.join(this.rootPath, entry.name))
|
|
181
|
+
}];
|
|
182
|
+
} else if (entry.isDirectory()) {
|
|
183
|
+
const dirPath = path.join(this.rootPath, entry.name);
|
|
184
|
+
yield [entry.name, {
|
|
185
|
+
kind: "directory",
|
|
186
|
+
name: entry.name,
|
|
187
|
+
getDirectoryHandle: async (key: string, options?: { create?: boolean }) => {
|
|
188
|
+
return new NodeJSDirectoryHandleWrapper(dirPath).getDirectoryHandle(key, options);
|
|
189
|
+
}
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// NOTE: Blocks until the user provides a directory
|
|
198
|
+
export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Promise<DirectoryWrapper> {
|
|
199
|
+
if (isNode()) {
|
|
200
|
+
return new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
|
|
201
|
+
}
|
|
202
|
+
let root = document.createElement("div");
|
|
203
|
+
document.body.appendChild(root);
|
|
204
|
+
preact.render(<DirectoryPrompter />, root);
|
|
205
|
+
try {
|
|
206
|
+
|
|
207
|
+
let handle: DirectoryWrapper | undefined;
|
|
208
|
+
|
|
209
|
+
let storedId = localStorage.getItem(storageKey);
|
|
210
|
+
if (storedId) {
|
|
211
|
+
let doneLoad = false;
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (doneLoad) return;
|
|
214
|
+
console.log("Waiting for user to click");
|
|
215
|
+
displayData.ui = "Click anywhere to allow file system access";
|
|
216
|
+
}, 500);
|
|
217
|
+
try {
|
|
218
|
+
handle = await tryToLoadPointer(storedId);
|
|
219
|
+
} catch { }
|
|
220
|
+
doneLoad = true;
|
|
221
|
+
if (handle) {
|
|
222
|
+
return handle;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let fileCallback: (handle: DirectoryWrapper) => void;
|
|
226
|
+
let promise = new Promise<DirectoryWrapper>(resolve => {
|
|
227
|
+
fileCallback = resolve;
|
|
228
|
+
});
|
|
229
|
+
displayData.ui = (
|
|
230
|
+
<button
|
|
231
|
+
className={css.fontSize(40).pad2(80, 40)}
|
|
232
|
+
onClick={async () => {
|
|
233
|
+
console.log("Waiting for user to give permission");
|
|
234
|
+
const handle = await window.showDirectoryPicker();
|
|
235
|
+
await handle.requestPermission({ mode: "readwrite" });
|
|
236
|
+
let storedId = await storeFileSystemPointer({ mode: "readwrite", handle });
|
|
237
|
+
localStorage.setItem(storageKey, storedId);
|
|
238
|
+
fileCallback(handle as any);
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
Pick Media Directory
|
|
242
|
+
</button>
|
|
243
|
+
);
|
|
244
|
+
return await promise;
|
|
245
|
+
} finally {
|
|
246
|
+
preact.render(null, root);
|
|
247
|
+
root.remove();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export const getFileStorageNested = cache(async function getFileStorage(path: string): Promise<FileStorage> {
|
|
252
|
+
let base = await getDirectoryHandle();
|
|
253
|
+
for (let part of path.split("/")) {
|
|
254
|
+
if (!part) continue;
|
|
255
|
+
base = await base.getDirectoryHandle(part, { create: true });
|
|
256
|
+
}
|
|
257
|
+
return wrapHandle(base);
|
|
258
|
+
});
|
|
259
|
+
export const getFileStorage = lazy(async function getFileStorage(): Promise<FileStorage> {
|
|
260
|
+
if (USE_INDEXED_DB) {
|
|
261
|
+
return await getFileStorageIndexDB();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let handle = await getDirectoryHandle();
|
|
265
|
+
return wrapHandle(handle);
|
|
266
|
+
});
|
|
267
|
+
export function resetStorageLocation() {
|
|
268
|
+
localStorage.removeItem(storageKey);
|
|
269
|
+
window.location.reload();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export type NestedFileStorage = {
|
|
273
|
+
hasKey(key: string): Promise<boolean>;
|
|
274
|
+
getStorage(key: string): Promise<FileStorage>;
|
|
275
|
+
removeStorage(key: string): Promise<void>;
|
|
276
|
+
getKeys(): Promise<string[]>;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export type FileStorage = IStorageRaw & {
|
|
280
|
+
folder: NestedFileStorage;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
let appendQueue = cache((key: string) => {
|
|
284
|
+
return runInSerial((fnc: () => Promise<void>) => fnc());
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async function fixedGetFileHandle(config: {
|
|
289
|
+
handle: DirectoryWrapper;
|
|
290
|
+
key: string;
|
|
291
|
+
create: true;
|
|
292
|
+
}): Promise<FileWrapper>;
|
|
293
|
+
async function fixedGetFileHandle(config: {
|
|
294
|
+
handle: DirectoryWrapper;
|
|
295
|
+
key: string;
|
|
296
|
+
create?: boolean;
|
|
297
|
+
}): Promise<FileWrapper | undefined>;
|
|
298
|
+
async function fixedGetFileHandle(config: {
|
|
299
|
+
handle: DirectoryWrapper;
|
|
300
|
+
key: string;
|
|
301
|
+
create?: boolean;
|
|
302
|
+
}): Promise<FileWrapper | undefined> {
|
|
303
|
+
if (config.key.includes("/")) {
|
|
304
|
+
throw new Error(`Cannot use folders directly in file system read / writes. Use a wrapper which handles the folder navigation. Path was ${JSON.stringify(config.key)}`);
|
|
305
|
+
}
|
|
306
|
+
// ALWAYS try without create, because the sshfs-win sucks and doesn't support `create: true`? Wtf...
|
|
307
|
+
try {
|
|
308
|
+
return await config.handle.getFileHandle(config.key);
|
|
309
|
+
} catch {
|
|
310
|
+
if (!config.create) return undefined;
|
|
311
|
+
}
|
|
312
|
+
return await config.handle.getFileHandle(config.key, { create: true });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
316
|
+
return {
|
|
317
|
+
async getInfo(key: string) {
|
|
318
|
+
try {
|
|
319
|
+
const file = await handle.getFileHandle(key);
|
|
320
|
+
const fileContent = await file.getFile();
|
|
321
|
+
return {
|
|
322
|
+
size: fileContent.size,
|
|
323
|
+
lastModified: fileContent.lastModified,
|
|
324
|
+
};
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
async get(key: string): Promise<Buffer | undefined> {
|
|
330
|
+
try {
|
|
331
|
+
const file = await handle.getFileHandle(key);
|
|
332
|
+
const fileContent = await file.getFile();
|
|
333
|
+
const arrayBuffer = await fileContent.arrayBuffer();
|
|
334
|
+
return Buffer.from(arrayBuffer);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
async append(key: string, value: Buffer): Promise<void> {
|
|
341
|
+
await appendQueue(key)(async () => {
|
|
342
|
+
// NOTE: Interesting point. Chrome doesn't optimize this to be an append, and instead
|
|
343
|
+
// rewrites the entire file.
|
|
344
|
+
const file = await fixedGetFileHandle({ handle, key, create: true });
|
|
345
|
+
const writable = await file.createWritable({ keepExistingData: true });
|
|
346
|
+
let offset = (await file.getFile()).size;
|
|
347
|
+
await writable.seek(offset);
|
|
348
|
+
await writable.write(value);
|
|
349
|
+
await writable.close();
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
async set(key: string, value: Buffer): Promise<void> {
|
|
354
|
+
const file = await fixedGetFileHandle({ handle, key, create: true });
|
|
355
|
+
const writable = await file.createWritable();
|
|
356
|
+
await writable.write(value);
|
|
357
|
+
await writable.close();
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async remove(key: string): Promise<void> {
|
|
361
|
+
await handle.removeEntry(key);
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async getKeys(): Promise<string[]> {
|
|
365
|
+
const keys: string[] = [];
|
|
366
|
+
for await (const [name, entry] of handle) {
|
|
367
|
+
if (entry.kind === "file") {
|
|
368
|
+
keys.push(entry.name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return keys;
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
async reset() {
|
|
375
|
+
for await (const [name, entry] of handle) {
|
|
376
|
+
await handle.removeEntry(entry.name, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function wrapHandleNested(handle: DirectoryWrapper): NestedFileStorage {
|
|
383
|
+
return {
|
|
384
|
+
async hasKey(key: string): Promise<boolean> {
|
|
385
|
+
try {
|
|
386
|
+
await handle.getDirectoryHandle(key);
|
|
387
|
+
return true;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async getStorage(key: string): Promise<FileStorage> {
|
|
394
|
+
const subDirectory = await handle.getDirectoryHandle(key, { create: true });
|
|
395
|
+
return wrapHandle(subDirectory);
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
async removeStorage(key: string): Promise<void> {
|
|
399
|
+
await handle.removeEntry(key, { recursive: true });
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
async getKeys(): Promise<string[]> {
|
|
403
|
+
const keys: string[] = [];
|
|
404
|
+
for await (const [name, entry] of handle) {
|
|
405
|
+
if (entry.kind === "directory") {
|
|
406
|
+
keys.push(entry.name);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return keys;
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function wrapHandle(handle: DirectoryWrapper): FileStorage {
|
|
415
|
+
return {
|
|
416
|
+
...wrapHandleFiles(handle),
|
|
417
|
+
folder: wrapHandleNested(handle),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function tryToLoadPointer(pointer: string) {
|
|
422
|
+
let result = await getFileSystemPointer({ pointer });
|
|
423
|
+
if (!result) return;
|
|
424
|
+
let handle = await result?.onUserActivation();
|
|
425
|
+
if (!handle) return;
|
|
426
|
+
return handle as any as DirectoryWrapper;
|
|
427
|
+
}
|