-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathopfs_ext.ts
251 lines (219 loc) · 9.19 KB
/
opfs_ext.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import { basename, dirname, join } from '@std/path/posix';
import { Err, Ok, RESULT_FALSE, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult, type IOResult } from 'happy-rusty';
import invariant from 'tiny-invariant';
import { assertAbsolutePath } from './assertions.ts';
import type { CopyOptions, ExistsOptions, MoveOptions, WriteFileContent } from './defines.ts';
import { getDirHandle, getFinalResult, isNotFoundError } from './helpers.ts';
import { mkdir, readDir, readFile, remove, stat, writeFile } from './opfs_core.ts';
import { isDirectoryHandle, isFileHandle } from './utils.ts';
/**
* Moves a file handle to a new path.
*
* @param fileHandle - The file handle to move.
* @param newPath - The new path of the file handle.
* @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file handle was successfully moved.
*/
async function moveHandle(fileHandle: FileSystemFileHandle, newPath: string): AsyncVoidIOResult {
const newDirPath = dirname(newPath);
return (await getDirHandle(newDirPath, {
create: true,
})).andThenAsync(async newDirHandle => {
const newName = basename(newPath);
try {
// TODO ts not support yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (fileHandle as any).move(newDirHandle, newName);
return RESULT_VOID;
} catch (e) {
return Err(e as DOMException);
}
});
}
/**
* @param srcFileHandle - The source file handle to move or copy.
* @param destFilePath - The destination file path.
*/
type handleSrcFileToDest = (srcFileHandle: FileSystemFileHandle, destFilePath: string) => AsyncVoidIOResult;
/**
* Copy or move a file or directory from one path to another.
* @param srcPath - The source file/directory path.
* @param destPath - The destination file/directory path.
* @param handler - How to handle the file handle to the destination.
* @param overwrite - Whether to overwrite the destination file if it exists.
* @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file was successfully copied/moved.
*/
async function mkDestFromSrc(srcPath: string, destPath: string, handler: handleSrcFileToDest, overwrite = true): AsyncVoidIOResult {
assertAbsolutePath(destPath);
return (await stat(srcPath)).andThenAsync(async srcHandle => {
// if overwrite is false, we need this flag to determine whether to write file.
let destExists = false;
const destHandleRes = await stat(destPath);
if (destHandleRes.isErr()) {
if (!isNotFoundError(destHandleRes.unwrapErr())) {
return destHandleRes.asErr();
}
} else {
destExists = true;
// check
const destHandle = destHandleRes.unwrap();
if (!((isFileHandle(srcHandle) && isFileHandle(destHandle))
|| (isDirectoryHandle(srcHandle) && isDirectoryHandle(destHandle)))) {
return Err(new Error(`Both 'srcPath' and 'destPath' must both be a file or directory.`));
}
}
// both are files
if (isFileHandle(srcHandle)) {
return (overwrite || !destExists) ? await handler(srcHandle, destPath) : RESULT_VOID;
}
// both are directories
const readDirRes = await readDir(srcPath, {
recursive: true,
});
return readDirRes.andThenAsync(async entries => {
const tasks: AsyncVoidIOResult[] = [
// make sure new dir created
mkdir(destPath),
];
for await (const { path, handle } of entries) {
const newEntryPath = join(destPath, path);
let newPathExists = false;
if (destExists) {
// should check every file
const existsRes = await exists(newEntryPath);
if (existsRes.isErr()) {
tasks.push(Promise.resolve(existsRes.asErr()));
continue;
}
newPathExists = existsRes.unwrap();
}
const res: AsyncVoidIOResult = isFileHandle(handle)
? (overwrite || !newPathExists ? handler(handle, newEntryPath) : Promise.resolve(RESULT_VOID))
: mkdir(newEntryPath);
tasks.push(res);
}
return getFinalResult(tasks);
});
});
}
/**
* Appends content to a file at the specified path.
*
* @param filePath - The path of the file to append to.
* @param contents - The content to append to the file.
* @returns A promise that resolves to an `AsyncIOResult` indicating whether the content was successfully appended.
*/
export function appendFile(filePath: string, contents: WriteFileContent): AsyncVoidIOResult {
return writeFile(filePath, contents, {
append: true,
});
}
/**
* Copies a file or directory from one location to another same as `cp -r`.
*
* Both `srcPath` and `destPath` must both be a file or directory.
*
* @param srcPath - The source file/directory path.
* @param destPath - The destination file/directory path.
* @param options - The copy options.
* @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file was successfully copied.
*/
export async function copy(srcPath: string, destPath: string, options?: CopyOptions): AsyncVoidIOResult {
const {
overwrite = true,
} = options ?? {};
return mkDestFromSrc(srcPath, destPath, async (srcHandle, destPath) => {
return await writeFile(destPath, await srcHandle.getFile());
}, overwrite);
}
/**
* Empties the contents of a directory at the specified path.
*
* @param dirPath - The path of the directory to empty.
* @returns A promise that resolves to an `AsyncIOResult` indicating whether the directory was successfully emptied.
*/
export async function emptyDir(dirPath: string): AsyncVoidIOResult {
const readDirRes = await readDir(dirPath);
if (readDirRes.isErr()) {
// create if not exist
return isNotFoundError(readDirRes.unwrapErr()) ? mkdir(dirPath) : readDirRes.asErr();
}
const tasks: AsyncVoidIOResult[] = [];
for await (const { path } of readDirRes.unwrap()) {
tasks.push(remove(join(dirPath, path)));
}
return getFinalResult(tasks);
}
/**
* Checks whether a file or directory exists at the specified path.
*
* @param path - The path of the file or directory to check for existence.
* @param options - Optional existence options.
* @returns A promise that resolves to an `AsyncIOResult` indicating whether the file or directory exists.
*/
export async function exists(path: string, options?: ExistsOptions): AsyncIOResult<boolean> {
const { isDirectory = false, isFile = false } = options ?? {};
invariant(!(isDirectory && isFile), () => 'ExistsOptions.isDirectory and ExistsOptions.isFile must not be true together.');
const statRes = await stat(path);
return statRes.andThen(handle => {
const notExist =
(isDirectory && isFileHandle(handle))
|| (isFile && isDirectoryHandle(handle));
return Ok(!notExist);
}).orElse((err): IOResult<boolean> => {
return isNotFoundError(err) ? RESULT_FALSE : statRes.asErr();
});
}
/**
* Move a file or directory from an old path to a new path.
*
* @param srcPath - The current path of the file or directory.
* @param destPath - The new path of the file or directory.
* @param options - Options of move.
* @returns A promise that resolves to an `AsyncIOResult` indicating whether the file or directory was successfully moved.
*/
export async function move(srcPath: string, destPath: string, options?: MoveOptions): AsyncVoidIOResult {
const {
overwrite = true,
} = options ?? {};
return (await mkDestFromSrc(srcPath, destPath, moveHandle, overwrite)).andThenAsync(() => {
// finally remove src
return remove(srcPath);
});
}
/**
* Reads the content of a file at the specified path as a File.
*
* @param filePath - The path of the file to read.
* @returns A promise that resolves to an `AsyncIOResult` containing the file content as a File.
*/
export function readBlobFile(filePath: string): AsyncIOResult<File> {
return readFile(filePath, {
encoding: 'blob',
});
}
/**
* Reads the content of a file at the specified path as a string and returns it as a JSON object.
*
* @param filePath - The path of the file to read.
* @returns A promise that resolves to an `AsyncIOResult` containing the file content as a JSON object.
*/
export async function readJsonFile<T>(filePath: string): AsyncIOResult<T> {
return (await readTextFile(filePath)).andThenAsync(async contents => {
try {
return Ok(JSON.parse(contents));
} catch (e) {
return Err(e as Error);
}
});
}
/**
* Reads the content of a file at the specified path as a string.
*
* @param filePath - The path of the file to read.
* @returns A promise that resolves to an `AsyncIOResult` containing the file content as a string.
*/
export function readTextFile(filePath: string): AsyncIOResult<string> {
return readFile(filePath, {
encoding: 'utf8',
});
}