vvvfs 0.1.3 → 0.1.5
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/README.md +9 -0
- package/index.html +1 -31
- package/index.js +34 -0
- package/index.ts +306 -686
- package/package.json +1 -1
- package/types/index.d.ts +396 -0
package/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import mime from "mime";
|
|
|
15
15
|
* @param path 文件路径
|
|
16
16
|
* @param type 文件类型(file或dir)
|
|
17
17
|
* @param file 文件对象
|
|
18
|
+
* @param locked 是否锁定
|
|
18
19
|
*/
|
|
19
20
|
export interface FileRecord {
|
|
20
21
|
id?: number;
|
|
@@ -22,6 +23,7 @@ export interface FileRecord {
|
|
|
22
23
|
path: string;
|
|
23
24
|
type: string;
|
|
24
25
|
file: File | null;
|
|
26
|
+
locked?: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -163,7 +165,7 @@ class VVVFSFile {
|
|
|
163
165
|
* 写入文件JSON内容
|
|
164
166
|
* @param json 文件JSON内容
|
|
165
167
|
*/
|
|
166
|
-
async writeJSON(json: Record<string,
|
|
168
|
+
async writeJSON(json: Record<string, unknown>, format: boolean = true) {
|
|
167
169
|
return await this._vvvfs.writeJson(this._path, json, format);
|
|
168
170
|
}
|
|
169
171
|
/**
|
|
@@ -264,9 +266,16 @@ class VVVFSFile {
|
|
|
264
266
|
async unlock() {
|
|
265
267
|
return await this._vvvfs.unlock(this._path);
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* 判断文件是否已锁定
|
|
271
|
+
*/
|
|
272
|
+
async isLocked() {
|
|
273
|
+
return await this._vvvfs.isLocked(this._path);
|
|
274
|
+
}
|
|
267
275
|
}
|
|
268
276
|
|
|
269
277
|
const version = packageJson.version;
|
|
278
|
+
|
|
270
279
|
class VVVFS {
|
|
271
280
|
static defaultDBName = "vvvfs";
|
|
272
281
|
#db: VVVFSDatabase;
|
|
@@ -283,10 +292,74 @@ class VVVFS {
|
|
|
283
292
|
* 虚拟文件系统监听器
|
|
284
293
|
*/
|
|
285
294
|
#watchers: Record<string, Array<(type: string) => Promise<boolean>>> = {};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 检查文件访问权限(监听器和锁定状态)
|
|
298
|
+
* @param path 文件路径
|
|
299
|
+
* @param operation 操作类型
|
|
300
|
+
* @returns 是否允许访问
|
|
301
|
+
*/
|
|
302
|
+
async #checkAccess(path: string, operation: string): Promise<boolean> {
|
|
303
|
+
if (this.#watchers[path]) {
|
|
304
|
+
for (const handler of this.#watchers[path]) {
|
|
305
|
+
if (await handler(operation)) {
|
|
306
|
+
if (this.options.throwError) {
|
|
307
|
+
throw new VVVFSError(
|
|
308
|
+
operation.charAt(0).toUpperCase() + operation.slice(1),
|
|
309
|
+
`${operation}操作被监听器取消`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (await this.#isLocked(path)) {
|
|
317
|
+
if (this.options.throwError) {
|
|
318
|
+
throw new VVVFSError(
|
|
319
|
+
operation.charAt(0).toUpperCase() + operation.slice(1),
|
|
320
|
+
"文件已被锁定",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 内部判断是否锁定(跳过错误包装)
|
|
330
|
+
*/
|
|
331
|
+
async #isLocked(path: string): Promise<boolean> {
|
|
332
|
+
const { name, parent } = parsePath(path);
|
|
333
|
+
const fileRecord = await this.#db.files.where({ name, path: parent }).first();
|
|
334
|
+
return !!fileRecord?.locked;
|
|
335
|
+
}
|
|
336
|
+
|
|
286
337
|
/**
|
|
287
|
-
*
|
|
338
|
+
* 错误处理包装器
|
|
339
|
+
* @param operation 操作名称
|
|
340
|
+
* @param fn 操作函数
|
|
341
|
+
* @param fallback 失败时的默认返回值
|
|
288
342
|
*/
|
|
289
|
-
#
|
|
343
|
+
async #withErrorHandling<T>(
|
|
344
|
+
operation: string,
|
|
345
|
+
fn: () => Promise<T>,
|
|
346
|
+
fallback: T,
|
|
347
|
+
): Promise<T> {
|
|
348
|
+
try {
|
|
349
|
+
return await fn();
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error instanceof VVVFSError) throw error;
|
|
352
|
+
console.error(`${operation}失败`, error);
|
|
353
|
+
if (this.options.throwError) {
|
|
354
|
+
throw new VVVFSError(
|
|
355
|
+
operation.charAt(0).toUpperCase() + operation.slice(1),
|
|
356
|
+
`${operation}失败${error}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return fallback;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
290
363
|
/**
|
|
291
364
|
* 创建虚拟文件系统
|
|
292
365
|
* @param name 虚拟文件系统名称
|
|
@@ -304,6 +377,15 @@ class VVVFS {
|
|
|
304
377
|
this.#db.version(1).stores({
|
|
305
378
|
files: "++id, name, path, type, file, [name+path+type]",
|
|
306
379
|
});
|
|
380
|
+
this.#db.version(2).stores({
|
|
381
|
+
files: "++id, name, path, type, file, locked, [name+path+type]",
|
|
382
|
+
}).upgrade(async (tx) => {
|
|
383
|
+
await tx.table("files").toCollection().modify((file: FileRecord) => {
|
|
384
|
+
if (file.locked === undefined) {
|
|
385
|
+
file.locked = false;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
307
389
|
} catch (error) {
|
|
308
390
|
console.error("创建数据库失败", error);
|
|
309
391
|
throw new VVVFSError("CreateDatabase", "创建数据库失败");
|
|
@@ -314,128 +396,25 @@ class VVVFS {
|
|
|
314
396
|
* @description 将Linux的系统初始文件初始化到数据库中
|
|
315
397
|
*/
|
|
316
398
|
async init(user?: string) {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
type: "dir",
|
|
322
|
-
file: new File([], "root"),
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
name: "boot",
|
|
326
|
-
path: "/",
|
|
327
|
-
type: "dir",
|
|
328
|
-
file: new File([], "boot"),
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
name: "bin",
|
|
332
|
-
path: "/",
|
|
333
|
-
type: "dir",
|
|
334
|
-
file: new File([], "bin"),
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
name: "dev",
|
|
338
|
-
path: "/",
|
|
339
|
-
type: "dir",
|
|
340
|
-
file: new File([], "dev"),
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: "etc",
|
|
344
|
-
path: "/",
|
|
345
|
-
type: "dir",
|
|
346
|
-
file: new File([], "etc"),
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
name: "home",
|
|
350
|
-
path: "/",
|
|
351
|
-
type: "dir",
|
|
352
|
-
file: new File([], "home"),
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
name: user || "root",
|
|
356
|
-
path: "/home",
|
|
357
|
-
type: "dir",
|
|
358
|
-
file: new File([], "home"),
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
name: "lib",
|
|
362
|
-
path: "/",
|
|
363
|
-
type: "dir",
|
|
364
|
-
file: new File([], "lib"),
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
name: "lib64",
|
|
368
|
-
path: "/",
|
|
369
|
-
type: "dir",
|
|
370
|
-
file: new File([], "lib64"),
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
name: "media",
|
|
374
|
-
path: "/",
|
|
375
|
-
type: "dir",
|
|
376
|
-
file: new File([], "media"),
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
name: "mnt",
|
|
380
|
-
path: "/",
|
|
381
|
-
type: "dir",
|
|
382
|
-
file: new File([], "mnt"),
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
name: "opt",
|
|
386
|
-
path: "/",
|
|
387
|
-
type: "dir",
|
|
388
|
-
file: new File([], "opt"),
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
name: "proc",
|
|
392
|
-
path: "/",
|
|
393
|
-
type: "dir",
|
|
394
|
-
file: new File([], "proc"),
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
name: "run",
|
|
398
|
-
path: "/",
|
|
399
|
-
type: "dir",
|
|
400
|
-
file: new File([], "run"),
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
name: "sbin",
|
|
404
|
-
path: "/",
|
|
405
|
-
type: "dir",
|
|
406
|
-
file: new File([], "sbin"),
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
name: "srv",
|
|
410
|
-
path: "/",
|
|
411
|
-
type: "dir",
|
|
412
|
-
file: new File([], "srv"),
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
name: "sys",
|
|
416
|
-
path: "/",
|
|
417
|
-
type: "dir",
|
|
418
|
-
file: new File([], "sys"),
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
name: "tmp",
|
|
422
|
-
path: "/",
|
|
423
|
-
type: "dir",
|
|
424
|
-
file: new File([], "tmp"),
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
name: "usr",
|
|
428
|
-
path: "/",
|
|
429
|
-
type: "dir",
|
|
430
|
-
file: new File([], "usr"),
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
name: "var",
|
|
434
|
-
path: "/",
|
|
435
|
-
type: "dir",
|
|
436
|
-
file: new File([], "var"),
|
|
437
|
-
},
|
|
399
|
+
const linuxDirs = [
|
|
400
|
+
"root", "boot", "bin", "dev", "etc", "home",
|
|
401
|
+
"lib", "lib64", "media", "mnt", "opt", "proc",
|
|
402
|
+
"run", "sbin", "srv", "sys", "tmp", "usr", "var",
|
|
438
403
|
];
|
|
404
|
+
const linuxInitFiles = linuxDirs.map((name) => ({
|
|
405
|
+
name,
|
|
406
|
+
path: "/",
|
|
407
|
+
type: "dir" as const,
|
|
408
|
+
file: new File([], name),
|
|
409
|
+
locked: false,
|
|
410
|
+
}));
|
|
411
|
+
linuxInitFiles.push({
|
|
412
|
+
name: user || "root",
|
|
413
|
+
path: "/home",
|
|
414
|
+
type: "dir" as const,
|
|
415
|
+
file: new File([], "home"),
|
|
416
|
+
locked: false,
|
|
417
|
+
});
|
|
439
418
|
try {
|
|
440
419
|
for (const file of linuxInitFiles) {
|
|
441
420
|
if (await this.exists(file.path)) continue;
|
|
@@ -456,6 +435,9 @@ class VVVFS {
|
|
|
456
435
|
this.#db.version(1).stores({
|
|
457
436
|
files: "++id, name, path, type, file, [name+path+type]",
|
|
458
437
|
});
|
|
438
|
+
this.#db.version(2).stores({
|
|
439
|
+
files: "++id, name, path, type, file, locked, [name+path+type]",
|
|
440
|
+
});
|
|
459
441
|
} catch (error) {
|
|
460
442
|
console.error("重置数据库失败", error);
|
|
461
443
|
throw new VVVFSError("ResetDatabase", "重置数据库失败" + error);
|
|
@@ -466,18 +448,9 @@ class VVVFS {
|
|
|
466
448
|
* @param path 文件路径
|
|
467
449
|
*/
|
|
468
450
|
async createFile(path: string) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (this.#
|
|
472
|
-
for (const handler of this.#watchers[targetPath]) {
|
|
473
|
-
if (await handler("create")) {
|
|
474
|
-
if (this.options.throwError) {
|
|
475
|
-
throw new VVVFSError("CreateFile", "创建文件失败:监听器取消了操作");
|
|
476
|
-
}
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
451
|
+
return this.#withErrorHandling("createFile", async () => {
|
|
452
|
+
const targetPath = joinPath(path);
|
|
453
|
+
if (!(await this.#checkAccess(targetPath, "create"))) return false;
|
|
481
454
|
if (await this.exists(targetPath)) {
|
|
482
455
|
console.warn("文件已存在");
|
|
483
456
|
return true;
|
|
@@ -487,162 +460,101 @@ class VVVFS {
|
|
|
487
460
|
await this.createDir(parent);
|
|
488
461
|
}
|
|
489
462
|
await this.#db.files.add({
|
|
490
|
-
name
|
|
463
|
+
name,
|
|
491
464
|
path: parent,
|
|
492
465
|
type: "file",
|
|
493
466
|
file: new File([], name, {
|
|
494
467
|
type: mime.getType(targetPath) || "application/octet-stream",
|
|
495
468
|
}),
|
|
469
|
+
locked: false,
|
|
496
470
|
});
|
|
497
471
|
return true;
|
|
498
|
-
}
|
|
499
|
-
console.error("创建文件失败", error);
|
|
500
|
-
if (this.options.throwError) {
|
|
501
|
-
throw new VVVFSError("CreateFile", "创建文件失败" + error);
|
|
502
|
-
}
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
472
|
+
}, false);
|
|
505
473
|
}
|
|
506
474
|
/**
|
|
507
475
|
* 创建目录
|
|
508
476
|
* @param path 目录路径
|
|
509
477
|
*/
|
|
510
478
|
async createDir(path: string) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (this.#
|
|
514
|
-
for (const handler of this.#watchers[targetPath]) {
|
|
515
|
-
if (await handler("create")) {
|
|
516
|
-
if (this.options.throwError) {
|
|
517
|
-
throw new VVVFSError("CreateDir", "创建目录失败:监听器取消了操作");
|
|
518
|
-
}
|
|
519
|
-
return false;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
479
|
+
return this.#withErrorHandling("createDir", async () => {
|
|
480
|
+
const targetPath = joinPath(path);
|
|
481
|
+
if (!(await this.#checkAccess(targetPath, "create"))) return false;
|
|
523
482
|
if (await this.exists(targetPath)) {
|
|
524
483
|
console.warn("目录已存在");
|
|
525
484
|
return true;
|
|
526
485
|
}
|
|
527
486
|
const { name, parent } = parsePath(targetPath);
|
|
528
487
|
if (!(await this.exists(parent))) {
|
|
529
|
-
if (parent
|
|
488
|
+
if (parent === "/") {
|
|
530
489
|
await this.#db.files.add({
|
|
531
490
|
name: "",
|
|
532
491
|
path: "/",
|
|
533
492
|
type: "dir",
|
|
534
493
|
file: new File([], ""),
|
|
494
|
+
locked: false,
|
|
535
495
|
});
|
|
536
|
-
if (name
|
|
496
|
+
if (name === "") return true;
|
|
537
497
|
} else {
|
|
538
498
|
await this.createDir(parent);
|
|
539
499
|
}
|
|
540
500
|
}
|
|
541
501
|
await this.#db.files.add({
|
|
542
|
-
name
|
|
502
|
+
name,
|
|
543
503
|
path: parent,
|
|
544
504
|
type: "dir",
|
|
545
505
|
file: new File([], name),
|
|
506
|
+
locked: false,
|
|
546
507
|
});
|
|
547
508
|
return true;
|
|
548
|
-
}
|
|
549
|
-
console.error("创建目录失败", error);
|
|
550
|
-
if (this.options.throwError) {
|
|
551
|
-
throw new VVVFSError("CreateDir", "创建目录失败" + error);
|
|
552
|
-
}
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
509
|
+
}, false);
|
|
555
510
|
}
|
|
556
511
|
/**
|
|
557
512
|
* 判断文件是否存在
|
|
558
513
|
* @param path 文件路径
|
|
559
514
|
*/
|
|
560
515
|
async exists(path: string) {
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
const { name, parent } = parsePath(targetPath);
|
|
516
|
+
return this.#withErrorHandling("exists", async () => {
|
|
517
|
+
const { name, parent } = parsePath(path);
|
|
564
518
|
return (
|
|
565
519
|
(await this.#db.files
|
|
566
|
-
.where({
|
|
567
|
-
name: name,
|
|
568
|
-
path: parent,
|
|
569
|
-
})
|
|
520
|
+
.where({ name, path: parent })
|
|
570
521
|
.count()) > 0
|
|
571
522
|
);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
523
|
+
}, false);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* 内部写入(跳过访问检查,供内部方法调用)
|
|
527
|
+
*/
|
|
528
|
+
async #internalWrite(targetPath: string, content: Blob): Promise<boolean> {
|
|
529
|
+
if (!(await this.exists(targetPath))) {
|
|
530
|
+
const success = await this.createFile(targetPath);
|
|
531
|
+
if (!success) return false;
|
|
532
|
+
}
|
|
533
|
+
if (await this.isDir(targetPath)) {
|
|
534
|
+
console.warn("路径已存在");
|
|
577
535
|
return false;
|
|
578
536
|
}
|
|
537
|
+
const { name, parent } = parsePath(targetPath);
|
|
538
|
+
const file = new File([content], name, {
|
|
539
|
+
type: mime.getType(targetPath) || "application/octet-stream",
|
|
540
|
+
});
|
|
541
|
+
const fileRecord = await this.#db.files.where({ name, path: parent }).first();
|
|
542
|
+
if (fileRecord) {
|
|
543
|
+
await this.#db.files.put({ ...fileRecord, file });
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
579
547
|
}
|
|
580
548
|
/**
|
|
581
549
|
* 读取文件内容
|
|
582
550
|
* @param path 文件路径
|
|
583
551
|
*/
|
|
584
552
|
async write(path: string, content: Blob) {
|
|
585
|
-
|
|
553
|
+
return this.#withErrorHandling("write", async () => {
|
|
586
554
|
const targetPath = joinPath(path);
|
|
587
|
-
if (this.#
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (this.options.throwError) {
|
|
591
|
-
throw new VVVFSError("Write", "写入文件失败:监听器取消了操作");
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
if (this.#lockedFiles[targetPath]) {
|
|
598
|
-
console.warn("文件已被锁定");
|
|
599
|
-
if (this.options.throwError) {
|
|
600
|
-
throw new VVVFSError("Write", "文件已被锁定");
|
|
601
|
-
}
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
if (!(await this.exists(targetPath))) {
|
|
605
|
-
const success = await this.createFile(targetPath);
|
|
606
|
-
if (!success) {
|
|
607
|
-
console.warn("创建文件失败");
|
|
608
|
-
if (this.options.throwError) {
|
|
609
|
-
throw new VVVFSError("Write", "创建文件失败");
|
|
610
|
-
}
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (await this.isDir(targetPath)) {
|
|
615
|
-
console.warn("路径已存在");
|
|
616
|
-
if (this.options.throwError) {
|
|
617
|
-
throw new VVVFSError("Write", "路径已存在");
|
|
618
|
-
}
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
const { name, parent } = parsePath(targetPath);
|
|
622
|
-
const file = new File([content], name, {
|
|
623
|
-
type: mime.getType(targetPath) || "application/octet-stream",
|
|
624
|
-
});
|
|
625
|
-
const fileRecord = await this.#db.files.where({ name, path: parent }).first();
|
|
626
|
-
if (fileRecord) {
|
|
627
|
-
await this.#db.files.put({
|
|
628
|
-
...fileRecord,
|
|
629
|
-
file: file,
|
|
630
|
-
});
|
|
631
|
-
return true;
|
|
632
|
-
} else {
|
|
633
|
-
console.warn("文件记录未找到,无法写入内容");
|
|
634
|
-
if (this.options.throwError) {
|
|
635
|
-
throw new VVVFSError("Write", "文件记录未找到,无法写入内容");
|
|
636
|
-
}
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
639
|
-
} catch (error) {
|
|
640
|
-
console.error("写入文件失败", error);
|
|
641
|
-
if (this.options.throwError) {
|
|
642
|
-
throw new VVVFSError("Write", "写入文件失败" + error);
|
|
643
|
-
}
|
|
644
|
-
return false;
|
|
645
|
-
}
|
|
555
|
+
if (!(await this.#checkAccess(targetPath, "write"))) return false;
|
|
556
|
+
return await this.#internalWrite(targetPath, content);
|
|
557
|
+
}, false);
|
|
646
558
|
}
|
|
647
559
|
/**
|
|
648
560
|
* 写入文本内容
|
|
@@ -650,16 +562,10 @@ class VVVFS {
|
|
|
650
562
|
* @param content 文本内容
|
|
651
563
|
*/
|
|
652
564
|
async writeText(path: string, content: string) {
|
|
653
|
-
|
|
565
|
+
return this.#withErrorHandling("writeText", async () => {
|
|
654
566
|
const blob = new Blob([content], { type: "text/plain" });
|
|
655
567
|
return await this.write(path, blob);
|
|
656
|
-
}
|
|
657
|
-
console.error("写入文件失败", error);
|
|
658
|
-
if (this.options.throwError) {
|
|
659
|
-
throw new VVVFSError("Write", "写入文件失败" + error);
|
|
660
|
-
}
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
568
|
+
}, false);
|
|
663
569
|
}
|
|
664
570
|
/**
|
|
665
571
|
* 写入JSON内容
|
|
@@ -667,19 +573,13 @@ class VVVFS {
|
|
|
667
573
|
* @param content JSON内容
|
|
668
574
|
* @param format 是否格式化
|
|
669
575
|
*/
|
|
670
|
-
async writeJson(path: string, content: Record<string,
|
|
671
|
-
|
|
576
|
+
async writeJson(path: string, content: Record<string, unknown>, format: boolean = true) {
|
|
577
|
+
return this.#withErrorHandling("writeJson", async () => {
|
|
672
578
|
return await this.writeText(
|
|
673
579
|
path,
|
|
674
580
|
JSON.stringify(content, null, format ? 4 : undefined),
|
|
675
581
|
);
|
|
676
|
-
}
|
|
677
|
-
console.error("写入文件失败", error);
|
|
678
|
-
if (this.options.throwError) {
|
|
679
|
-
throw new VVVFSError("Write", "写入文件失败" + error);
|
|
680
|
-
}
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
582
|
+
}, false);
|
|
683
583
|
}
|
|
684
584
|
/**
|
|
685
585
|
* 追加内容
|
|
@@ -687,41 +587,18 @@ class VVVFS {
|
|
|
687
587
|
* @param content 追加内容
|
|
688
588
|
*/
|
|
689
589
|
async append(path: string, content: Blob) {
|
|
690
|
-
|
|
590
|
+
return this.#withErrorHandling("append", async () => {
|
|
691
591
|
const targetPath = joinPath(path);
|
|
692
|
-
if (this.#
|
|
693
|
-
|
|
694
|
-
if (await handler("append")) {
|
|
695
|
-
if (this.options.throwError) {
|
|
696
|
-
throw new VVVFSError("Append", "追加文件失败:监听器取消了操作");
|
|
697
|
-
}
|
|
698
|
-
return false;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (this.#lockedFiles[targetPath]) {
|
|
703
|
-
console.warn("文件已被锁定");
|
|
704
|
-
if (this.options.throwError) {
|
|
705
|
-
throw new VVVFSError("Append", "文件已被锁定");
|
|
706
|
-
}
|
|
707
|
-
return false;
|
|
708
|
-
}
|
|
709
|
-
const existingFile = await this.read(targetPath);
|
|
592
|
+
if (!(await this.#checkAccess(targetPath, "append"))) return false;
|
|
593
|
+
const existingFile = await this.#internalRead(targetPath);
|
|
710
594
|
if (existingFile) {
|
|
711
595
|
const blob = new Blob([existingFile, content], {
|
|
712
596
|
type: "application/octet-stream",
|
|
713
597
|
});
|
|
714
|
-
return await this
|
|
715
|
-
} else {
|
|
716
|
-
return await this.write(targetPath, content);
|
|
598
|
+
return await this.#internalWrite(targetPath, blob);
|
|
717
599
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (this.options.throwError) {
|
|
721
|
-
throw new VVVFSError("Append", "追加文件失败" + error);
|
|
722
|
-
}
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
600
|
+
return await this.#internalWrite(targetPath, content);
|
|
601
|
+
}, false);
|
|
725
602
|
}
|
|
726
603
|
/**
|
|
727
604
|
* 追加文本内容
|
|
@@ -729,109 +606,58 @@ class VVVFS {
|
|
|
729
606
|
* @param content 追加内容
|
|
730
607
|
*/
|
|
731
608
|
async appendText(path: string, content: string) {
|
|
732
|
-
|
|
609
|
+
return this.#withErrorHandling("appendText", async () => {
|
|
733
610
|
const blob = new Blob([content], { type: "text/plain" });
|
|
734
611
|
return await this.append(path, blob);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
612
|
+
}, false);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* 内部读取(跳过访问检查,供内部方法调用)
|
|
616
|
+
*/
|
|
617
|
+
async #internalRead(targetPath: string): Promise<File | null> {
|
|
618
|
+
if (!(await this.exists(targetPath))) return null;
|
|
619
|
+
const { name, parent } = parsePath(targetPath);
|
|
620
|
+
return (await this.#db.files.where({ name, path: parent }).first())?.file ?? null;
|
|
742
621
|
}
|
|
743
622
|
/**
|
|
744
623
|
* 读取文件内容
|
|
745
624
|
* @param path 文件路径
|
|
746
625
|
*/
|
|
747
626
|
async read(path: string) {
|
|
748
|
-
|
|
627
|
+
return this.#withErrorHandling("read", async () => {
|
|
749
628
|
const targetPath = joinPath(path);
|
|
750
|
-
if (this.#
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (this.options.throwError) {
|
|
754
|
-
throw new VVVFSError("Read", "读取文件失败:监听器取消了操作");
|
|
755
|
-
}
|
|
756
|
-
return null;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
if (this.#lockedFiles[targetPath]) {
|
|
761
|
-
console.warn("文件已被锁定");
|
|
762
|
-
if (this.options.throwError) {
|
|
763
|
-
throw new VVVFSError("Read", "文件已被锁定");
|
|
764
|
-
}
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
if (!(await this.exists(targetPath))) {
|
|
768
|
-
console.warn("文件不存在");
|
|
769
|
-
if (this.options.throwError) {
|
|
770
|
-
throw new VVVFSError("Read", "文件不存在");
|
|
771
|
-
}
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
const { name, parent } = parsePath(targetPath);
|
|
775
|
-
return (await this.#db.files.where({ name, path: parent }).first())?.file;
|
|
776
|
-
} catch (error) {
|
|
777
|
-
console.error("读取文件失败", error);
|
|
778
|
-
if (this.options.throwError) {
|
|
779
|
-
throw new VVVFSError("Read", "读取文件失败" + error);
|
|
780
|
-
}
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
629
|
+
if (!(await this.#checkAccess(targetPath, "read"))) return null;
|
|
630
|
+
return await this.#internalRead(targetPath);
|
|
631
|
+
}, null as File | null);
|
|
783
632
|
}
|
|
784
633
|
/**
|
|
785
634
|
* 读取文件内容
|
|
786
635
|
* @param path 文件路径
|
|
787
636
|
*/
|
|
788
637
|
async readText(path: string) {
|
|
789
|
-
|
|
638
|
+
return this.#withErrorHandling("readText", async () => {
|
|
790
639
|
const file = await this.read(path);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
} else {
|
|
794
|
-
console.warn("文件不存在");
|
|
795
|
-
if (this.options.throwError) {
|
|
796
|
-
throw new VVVFSError("Read", "文件不存在");
|
|
797
|
-
}
|
|
798
|
-
return null;
|
|
799
|
-
}
|
|
800
|
-
} catch (error) {
|
|
801
|
-
console.error("读取文件失败", error);
|
|
802
|
-
if (this.options.throwError) {
|
|
803
|
-
throw new VVVFSError("Read", "读取文件失败" + error);
|
|
804
|
-
}
|
|
805
|
-
return null;
|
|
806
|
-
}
|
|
640
|
+
return file ? await file.text() : null;
|
|
641
|
+
}, null as string | null);
|
|
807
642
|
}
|
|
808
643
|
/**
|
|
809
644
|
* 读取JSON内容
|
|
810
645
|
* @param path 文件路径
|
|
811
646
|
*/
|
|
812
647
|
async readJson(path: string) {
|
|
813
|
-
|
|
648
|
+
return this.#withErrorHandling("readJson", async () => {
|
|
814
649
|
const text = await this.readText(path);
|
|
815
|
-
if (text)
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
return null;
|
|
650
|
+
if (!text) return null;
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(text);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.error("解析JSON失败", error);
|
|
655
|
+
if (this.options.throwError) {
|
|
656
|
+
throw new VVVFSError("Read", "解析JSON失败" + error);
|
|
824
657
|
}
|
|
825
|
-
} else {
|
|
826
658
|
return null;
|
|
827
659
|
}
|
|
828
|
-
}
|
|
829
|
-
console.error("读取文件失败", error);
|
|
830
|
-
if (this.options.throwError) {
|
|
831
|
-
throw new VVVFSError("Read", "读取文件失败" + error);
|
|
832
|
-
}
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
660
|
+
}, null as Record<string, unknown> | null);
|
|
835
661
|
}
|
|
836
662
|
/**
|
|
837
663
|
* 读取文件内容
|
|
@@ -840,41 +666,10 @@ class VVVFS {
|
|
|
840
666
|
* @param end 结束位置
|
|
841
667
|
*/
|
|
842
668
|
async readChunk(path: string, start: number, end: number) {
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (await handler("read")) {
|
|
848
|
-
if (this.options.throwError) {
|
|
849
|
-
throw new VVVFSError("Read", "读取文件失败:监听器取消了操作");
|
|
850
|
-
}
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
if (this.#lockedFiles[targetPath]) {
|
|
856
|
-
console.warn("文件已被锁定");
|
|
857
|
-
if (this.options.throwError) {
|
|
858
|
-
throw new VVVFSError("Read", "文件已被锁定");
|
|
859
|
-
}
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
if (!(await this.exists(targetPath))) {
|
|
863
|
-
console.warn("文件不存在");
|
|
864
|
-
if (this.options.throwError) {
|
|
865
|
-
throw new VVVFSError("Read", "文件不存在");
|
|
866
|
-
}
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
const file = await this.read(targetPath);
|
|
870
|
-
return file?.slice(start, end) || null;
|
|
871
|
-
} catch (error) {
|
|
872
|
-
console.error("读取文件失败", error);
|
|
873
|
-
if (this.options.throwError) {
|
|
874
|
-
throw new VVVFSError("Read", "读取文件失败" + error);
|
|
875
|
-
}
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
669
|
+
return this.#withErrorHandling("readChunk", async () => {
|
|
670
|
+
const file = await this.read(path);
|
|
671
|
+
return file?.slice(start, end) ?? null;
|
|
672
|
+
}, null as Blob | null);
|
|
878
673
|
}
|
|
879
674
|
/**
|
|
880
675
|
* 读取文件内容
|
|
@@ -883,102 +678,51 @@ class VVVFS {
|
|
|
883
678
|
* @param end 读取结束位置
|
|
884
679
|
*/
|
|
885
680
|
async readTextChunk(path: string, start: number, end: number) {
|
|
886
|
-
|
|
681
|
+
return this.#withErrorHandling("readTextChunk", async () => {
|
|
887
682
|
const chunk = await this.readChunk(path, start, end);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
} else {
|
|
891
|
-
return null;
|
|
892
|
-
}
|
|
893
|
-
} catch (error) {
|
|
894
|
-
console.error("读取文件失败", error);
|
|
895
|
-
if (this.options.throwError) {
|
|
896
|
-
throw new VVVFSError("Read", "读取文件失败" + error);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
683
|
+
return chunk ? await chunk.text() : null;
|
|
684
|
+
}, null as string | null);
|
|
899
685
|
}
|
|
900
686
|
/**
|
|
901
687
|
* 判断是否是文件
|
|
902
688
|
* @param path 文件路径
|
|
903
689
|
*/
|
|
904
690
|
async isFile(path: string) {
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
const { name, parent } = parsePath(targetPath);
|
|
691
|
+
return this.#withErrorHandling("isFile", async () => {
|
|
692
|
+
const { name, parent } = parsePath(path);
|
|
908
693
|
return (await this.#db.files.where({ name, path: parent, type: "file" }).count()) > 0;
|
|
909
|
-
}
|
|
910
|
-
console.error("判断文件类型失败", error);
|
|
911
|
-
if (this.options.throwError) {
|
|
912
|
-
throw new VVVFSError("IsFile", "判断文件类型失败" + error);
|
|
913
|
-
}
|
|
914
|
-
return false;
|
|
915
|
-
}
|
|
694
|
+
}, false);
|
|
916
695
|
}
|
|
917
696
|
/**
|
|
918
697
|
* 判断是否是目录
|
|
919
698
|
* @param path 文件路径
|
|
920
699
|
*/
|
|
921
700
|
async isDir(path: string) {
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
} catch (error) {
|
|
927
|
-
console.error("判断文件类型失败", error);
|
|
928
|
-
if (this.options.throwError) {
|
|
929
|
-
throw new VVVFSError("IsDir", "判断文件类型失败" + error);
|
|
930
|
-
}
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
701
|
+
return this.#withErrorHandling("isDir", async () => {
|
|
702
|
+
const { name, parent } = parsePath(path);
|
|
703
|
+
return (await this.#db.files.where({ name, path: parent, type: "dir" }).count()) > 0;
|
|
704
|
+
}, false);
|
|
933
705
|
}
|
|
934
706
|
/**
|
|
935
707
|
* 列出目录下的文件
|
|
936
708
|
* @param path 目录路径
|
|
937
709
|
*/
|
|
938
710
|
async list(path: string) {
|
|
939
|
-
|
|
711
|
+
return this.#withErrorHandling("list", async () => {
|
|
940
712
|
const targetPath = joinPath(path);
|
|
941
|
-
if (this.#
|
|
942
|
-
for (const handler of this.#watchers[targetPath]) {
|
|
943
|
-
if (await handler("list")) {
|
|
944
|
-
if (this.options.throwError) {
|
|
945
|
-
throw new VVVFSError("List", "列出目录下的文件失败:监听器取消了操作");
|
|
946
|
-
}
|
|
947
|
-
return [];
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
if (this.#lockedFiles[targetPath]) {
|
|
952
|
-
console.warn("文件已被锁定");
|
|
953
|
-
if (this.options.throwError) {
|
|
954
|
-
throw new VVVFSError("List", "文件已被锁定");
|
|
955
|
-
}
|
|
956
|
-
return [];
|
|
957
|
-
}
|
|
713
|
+
if (!(await this.#checkAccess(targetPath, "list"))) return [];
|
|
958
714
|
if (!(await this.exists(targetPath))) {
|
|
959
715
|
console.warn("路径不存在");
|
|
960
|
-
if (this.options.throwError) {
|
|
961
|
-
throw new VVVFSError("List", "路径不存在");
|
|
962
|
-
}
|
|
963
716
|
return [];
|
|
964
717
|
}
|
|
965
718
|
if (!(await this.isDir(targetPath))) {
|
|
966
719
|
console.warn("路径不是目录");
|
|
967
|
-
if (this.options.throwError) {
|
|
968
|
-
throw new VVVFSError("List", "路径不是目录");
|
|
969
|
-
}
|
|
970
720
|
return [];
|
|
971
721
|
}
|
|
972
722
|
return (await this.#db.files.where({ path: targetPath }).toArray())
|
|
973
723
|
.map((file) => file.name)
|
|
974
|
-
.filter((item) => item
|
|
975
|
-
}
|
|
976
|
-
console.error("列出目录下的文件失败", error);
|
|
977
|
-
if (this.options.throwError) {
|
|
978
|
-
throw new VVVFSError("List", "列出目录下的文件失败" + error);
|
|
979
|
-
}
|
|
980
|
-
return [];
|
|
981
|
-
}
|
|
724
|
+
.filter((item) => item !== "");
|
|
725
|
+
}, [] as string[]);
|
|
982
726
|
}
|
|
983
727
|
/**
|
|
984
728
|
* 重命名文件
|
|
@@ -986,50 +730,23 @@ class VVVFS {
|
|
|
986
730
|
* @param newName 新文件名
|
|
987
731
|
*/
|
|
988
732
|
async rename(path: string, newName: string) {
|
|
989
|
-
|
|
733
|
+
return this.#withErrorHandling("rename", async () => {
|
|
990
734
|
const sourcePath = joinPath(path);
|
|
991
|
-
|
|
992
|
-
if (this.#watchers[sourcePath]) {
|
|
993
|
-
for (const handler of this.#watchers[sourcePath]) {
|
|
994
|
-
if (await handler("rename")) {
|
|
995
|
-
if (this.options.throwError) {
|
|
996
|
-
throw new VVVFSError("Rename", "重命名文件失败:监听器取消了操作");
|
|
997
|
-
}
|
|
998
|
-
return false;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
if (this.#lockedFiles[sourcePath]) {
|
|
1003
|
-
console.warn("文件已被锁定");
|
|
1004
|
-
if (this.options.throwError) {
|
|
1005
|
-
throw new VVVFSError("Rename", "文件已被锁定");
|
|
1006
|
-
}
|
|
1007
|
-
return false;
|
|
1008
|
-
}
|
|
735
|
+
if (!(await this.#checkAccess(sourcePath, "rename"))) return false;
|
|
1009
736
|
if (!(await this.exists(sourcePath))) {
|
|
1010
737
|
console.warn("文件不存在");
|
|
1011
|
-
if (this.options.throwError) {
|
|
1012
|
-
throw new VVVFSError("Rename", "文件不存在");
|
|
1013
|
-
}
|
|
1014
738
|
return false;
|
|
1015
739
|
}
|
|
740
|
+
const { name, parent } = parsePath(sourcePath);
|
|
1016
741
|
const newPath = joinPath(parent, newName);
|
|
1017
|
-
if (newPath === sourcePath)
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
742
|
+
if (newPath === sourcePath) return true;
|
|
1020
743
|
if (await this.exists(newPath)) {
|
|
1021
744
|
console.warn("目标文件已存在");
|
|
1022
|
-
if (this.options.throwError) {
|
|
1023
|
-
throw new VVVFSError("Rename", "目标文件已存在");
|
|
1024
|
-
}
|
|
1025
745
|
return false;
|
|
1026
746
|
}
|
|
1027
747
|
const fileRecord = await this.#db.files.where({ path: parent, name }).first();
|
|
1028
748
|
if (!fileRecord) {
|
|
1029
749
|
console.warn("文件记录未找到");
|
|
1030
|
-
if (this.options.throwError) {
|
|
1031
|
-
throw new VVVFSError("Rename", "文件记录未找到");
|
|
1032
|
-
}
|
|
1033
750
|
return false;
|
|
1034
751
|
}
|
|
1035
752
|
if (fileRecord.type === "dir") {
|
|
@@ -1045,48 +762,20 @@ class VVVFS {
|
|
|
1045
762
|
await this.#db.files.update(descendant.id!, { path: updatedPath });
|
|
1046
763
|
}
|
|
1047
764
|
}
|
|
1048
|
-
await this.#db.files.update(fileRecord.id!, {
|
|
1049
|
-
name: newName,
|
|
1050
|
-
path: parent,
|
|
1051
|
-
});
|
|
765
|
+
await this.#db.files.update(fileRecord.id!, { name: newName, path: parent });
|
|
1052
766
|
return true;
|
|
1053
|
-
}
|
|
1054
|
-
console.error(e);
|
|
1055
|
-
if (this.options.throwError) {
|
|
1056
|
-
throw new VVVFSError("Rename", "重命名文件失败" + e);
|
|
1057
|
-
}
|
|
1058
|
-
return false;
|
|
1059
|
-
}
|
|
767
|
+
}, false);
|
|
1060
768
|
}
|
|
1061
769
|
/**
|
|
1062
770
|
* 删除文件
|
|
1063
771
|
* @param path 文件路径
|
|
1064
772
|
*/
|
|
1065
773
|
async delete(path: string) {
|
|
1066
|
-
|
|
774
|
+
return this.#withErrorHandling("delete", async () => {
|
|
1067
775
|
const targetPath = joinPath(path);
|
|
1068
|
-
if (this.#
|
|
1069
|
-
for (const handler of this.#watchers[targetPath]) {
|
|
1070
|
-
if (await handler("delete")) {
|
|
1071
|
-
if (this.options.throwError) {
|
|
1072
|
-
throw new VVVFSError("Delete", "删除文件失败:监听器取消了操作");
|
|
1073
|
-
}
|
|
1074
|
-
return false;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
if (this.#lockedFiles[targetPath]) {
|
|
1079
|
-
console.warn("文件已被锁定");
|
|
1080
|
-
if (this.options.throwError) {
|
|
1081
|
-
throw new VVVFSError("Delete", "文件已被锁定");
|
|
1082
|
-
}
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
776
|
+
if (!(await this.#checkAccess(targetPath, "delete"))) return false;
|
|
1085
777
|
if (!(await this.exists(targetPath))) {
|
|
1086
778
|
console.warn("文件不存在");
|
|
1087
|
-
if (this.options.throwError) {
|
|
1088
|
-
throw new VVVFSError("Delete", "文件不存在");
|
|
1089
|
-
}
|
|
1090
779
|
return false;
|
|
1091
780
|
}
|
|
1092
781
|
const { name, parent } = parsePath(targetPath);
|
|
@@ -1102,22 +791,15 @@ class VVVFS {
|
|
|
1102
791
|
await this.#db.files.delete(dirRecord.id!);
|
|
1103
792
|
}
|
|
1104
793
|
return true;
|
|
1105
|
-
} else {
|
|
1106
|
-
const fileRecord = await this.#db.files
|
|
1107
|
-
.where({ name, path: parent, type: "file" })
|
|
1108
|
-
.first();
|
|
1109
|
-
if (fileRecord) {
|
|
1110
|
-
await this.#db.files.delete(fileRecord.id!);
|
|
1111
|
-
}
|
|
1112
|
-
return true;
|
|
1113
794
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
795
|
+
const fileRecord = await this.#db.files
|
|
796
|
+
.where({ name, path: parent, type: "file" })
|
|
797
|
+
.first();
|
|
798
|
+
if (fileRecord) {
|
|
799
|
+
await this.#db.files.delete(fileRecord.id!);
|
|
1118
800
|
}
|
|
1119
|
-
return
|
|
1120
|
-
}
|
|
801
|
+
return true;
|
|
802
|
+
}, false);
|
|
1121
803
|
}
|
|
1122
804
|
/**
|
|
1123
805
|
* 移动文件
|
|
@@ -1125,48 +807,21 @@ class VVVFS {
|
|
|
1125
807
|
* @param newPath 新路径
|
|
1126
808
|
*/
|
|
1127
809
|
async move(path: string, newPath: string) {
|
|
1128
|
-
|
|
810
|
+
return this.#withErrorHandling("move", async () => {
|
|
1129
811
|
const sourcePath = joinPath(path);
|
|
1130
812
|
const destinationPath = joinPath(newPath);
|
|
1131
|
-
if (this.#
|
|
1132
|
-
|
|
1133
|
-
if (await handler("move")) {
|
|
1134
|
-
if (this.options.throwError) {
|
|
1135
|
-
throw new VVVFSError("Move", "移动文件失败:监听器取消了操作");
|
|
1136
|
-
}
|
|
1137
|
-
return false;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
if (this.#lockedFiles[sourcePath]) {
|
|
1142
|
-
console.warn("文件已被锁定");
|
|
1143
|
-
if (this.options.throwError) {
|
|
1144
|
-
throw new VVVFSError("Move", "文件已被锁定");
|
|
1145
|
-
}
|
|
1146
|
-
return false;
|
|
1147
|
-
}
|
|
1148
|
-
if (sourcePath === destinationPath) {
|
|
1149
|
-
return true;
|
|
1150
|
-
}
|
|
813
|
+
if (!(await this.#checkAccess(sourcePath, "move"))) return false;
|
|
814
|
+
if (sourcePath === destinationPath) return true;
|
|
1151
815
|
if (await this.exists(destinationPath)) {
|
|
1152
816
|
console.warn("目标文件已存在");
|
|
1153
|
-
if (this.options.throwError) {
|
|
1154
|
-
throw new VVVFSError("Move", "目标文件已存在");
|
|
1155
|
-
}
|
|
1156
817
|
return false;
|
|
1157
818
|
}
|
|
1158
819
|
if (!(await this.exists(sourcePath))) {
|
|
1159
820
|
console.warn("源文件不存在");
|
|
1160
|
-
if (this.options.throwError) {
|
|
1161
|
-
throw new VVVFSError("Move", "源文件不存在");
|
|
1162
|
-
}
|
|
1163
821
|
return false;
|
|
1164
822
|
}
|
|
1165
823
|
if (destinationPath.startsWith(sourcePath + "/")) {
|
|
1166
824
|
console.warn("目标路径是源路径的子路径");
|
|
1167
|
-
if (this.options.throwError) {
|
|
1168
|
-
throw new VVVFSError("Move", "目标路径是源路径的子路径");
|
|
1169
|
-
}
|
|
1170
825
|
return false;
|
|
1171
826
|
}
|
|
1172
827
|
const { name, parent } = parsePath(sourcePath);
|
|
@@ -1184,24 +839,17 @@ class VVVFS {
|
|
|
1184
839
|
await this.#db.files.delete(dirRecord.id!);
|
|
1185
840
|
}
|
|
1186
841
|
return true;
|
|
1187
|
-
} else {
|
|
1188
|
-
await this.createDir(newParent);
|
|
1189
|
-
const fileRecord = await this.#db.files
|
|
1190
|
-
.where({ name, path: parent, type: "file" })
|
|
1191
|
-
.first();
|
|
1192
|
-
if (fileRecord) {
|
|
1193
|
-
await this.#db.files.update(fileRecord.id!, { name: newName, path: newParent });
|
|
1194
|
-
return true;
|
|
1195
|
-
}
|
|
1196
|
-
return false;
|
|
1197
842
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
843
|
+
await this.createDir(newParent);
|
|
844
|
+
const fileRecord = await this.#db.files
|
|
845
|
+
.where({ name, path: parent, type: "file" })
|
|
846
|
+
.first();
|
|
847
|
+
if (fileRecord) {
|
|
848
|
+
await this.#db.files.update(fileRecord.id!, { name: newName, path: newParent });
|
|
849
|
+
return true;
|
|
1202
850
|
}
|
|
1203
851
|
return false;
|
|
1204
|
-
}
|
|
852
|
+
}, false);
|
|
1205
853
|
}
|
|
1206
854
|
/**
|
|
1207
855
|
* 复制文件
|
|
@@ -1209,45 +857,20 @@ class VVVFS {
|
|
|
1209
857
|
* @param newPath 新路径
|
|
1210
858
|
*/
|
|
1211
859
|
async copy(path: string, newPath: string) {
|
|
1212
|
-
|
|
860
|
+
return this.#withErrorHandling("copy", async () => {
|
|
1213
861
|
const sourcePath = joinPath(path);
|
|
1214
862
|
const destinationPath = joinPath(newPath);
|
|
1215
|
-
if (this.#
|
|
1216
|
-
for (const handler of this.#watchers[sourcePath]) {
|
|
1217
|
-
if (await handler("copy")) {
|
|
1218
|
-
if (this.options.throwError) {
|
|
1219
|
-
throw new VVVFSError("Copy", "复制文件失败:监听器取消了操作");
|
|
1220
|
-
}
|
|
1221
|
-
return false;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (this.#lockedFiles[sourcePath]) {
|
|
1226
|
-
console.warn("文件已被锁定");
|
|
1227
|
-
if (this.options.throwError) {
|
|
1228
|
-
throw new VVVFSError("Copy", "文件已被锁定");
|
|
1229
|
-
}
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
863
|
+
if (!(await this.#checkAccess(sourcePath, "copy"))) return false;
|
|
1232
864
|
if (await this.exists(destinationPath)) {
|
|
1233
865
|
console.warn("目标文件已存在");
|
|
1234
|
-
if (this.options.throwError) {
|
|
1235
|
-
throw new VVVFSError("Copy", "目标文件已存在");
|
|
1236
|
-
}
|
|
1237
866
|
return false;
|
|
1238
867
|
}
|
|
1239
868
|
if (!(await this.exists(sourcePath))) {
|
|
1240
869
|
console.warn("源文件不存在");
|
|
1241
|
-
if (this.options.throwError) {
|
|
1242
|
-
throw new VVVFSError("Copy", "源文件不存在");
|
|
1243
|
-
}
|
|
1244
870
|
return false;
|
|
1245
871
|
}
|
|
1246
872
|
if (destinationPath.startsWith(sourcePath + "/")) {
|
|
1247
873
|
console.warn("目标路径是源路径的子路径");
|
|
1248
|
-
if (this.options.throwError) {
|
|
1249
|
-
throw new VVVFSError("Copy", "目标路径是源路径的子路径");
|
|
1250
|
-
}
|
|
1251
874
|
return false;
|
|
1252
875
|
}
|
|
1253
876
|
const { name, parent } = parsePath(sourcePath);
|
|
@@ -1259,28 +882,22 @@ class VVVFS {
|
|
|
1259
882
|
await this.copy(joinPath(sourcePath, child), joinPath(destinationPath, child));
|
|
1260
883
|
}
|
|
1261
884
|
return true;
|
|
1262
|
-
} else {
|
|
1263
|
-
const fileRecord = await this.#db.files
|
|
1264
|
-
.where({ name, path: parent, type: "file" })
|
|
1265
|
-
.first();
|
|
1266
|
-
if (fileRecord) {
|
|
1267
|
-
await this.#db.files.add({
|
|
1268
|
-
name: newName,
|
|
1269
|
-
path: newParent,
|
|
1270
|
-
type: fileRecord.type,
|
|
1271
|
-
file: fileRecord.file,
|
|
1272
|
-
});
|
|
1273
|
-
return true;
|
|
1274
|
-
}
|
|
1275
|
-
return false;
|
|
1276
885
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
886
|
+
const fileRecord = await this.#db.files
|
|
887
|
+
.where({ name, path: parent, type: "file" })
|
|
888
|
+
.first();
|
|
889
|
+
if (fileRecord) {
|
|
890
|
+
await this.#db.files.add({
|
|
891
|
+
name: newName,
|
|
892
|
+
path: newParent,
|
|
893
|
+
type: fileRecord.type,
|
|
894
|
+
file: fileRecord.file,
|
|
895
|
+
locked: false,
|
|
896
|
+
});
|
|
897
|
+
return true;
|
|
1281
898
|
}
|
|
1282
899
|
return false;
|
|
1283
|
-
}
|
|
900
|
+
}, false);
|
|
1284
901
|
}
|
|
1285
902
|
/**
|
|
1286
903
|
* 搜索文件
|
|
@@ -1288,49 +905,35 @@ class VVVFS {
|
|
|
1288
905
|
* @param query 查询字符串
|
|
1289
906
|
*/
|
|
1290
907
|
async search(basePath: string, query: string) {
|
|
1291
|
-
|
|
908
|
+
return this.#withErrorHandling("search", async () => {
|
|
1292
909
|
if (!(await this.isDir(basePath))) {
|
|
1293
910
|
console.warn("基础路径不是目录");
|
|
1294
|
-
if (this.options.throwError) {
|
|
1295
|
-
throw new VVVFSError("Search", "基础路径不是目录");
|
|
1296
|
-
}
|
|
1297
911
|
return null;
|
|
1298
912
|
}
|
|
1299
913
|
const that = this;
|
|
1300
914
|
const dirs = await this.list(basePath);
|
|
1301
|
-
return await
|
|
1302
|
-
async function
|
|
915
|
+
return await searchRecursive(basePath, dirs);
|
|
916
|
+
async function searchRecursive(parent: string, files: string[]) {
|
|
1303
917
|
const result: string[] = [];
|
|
1304
|
-
console.log(files);
|
|
1305
918
|
for (const file of files) {
|
|
1306
|
-
if (file
|
|
1307
|
-
|
|
1308
|
-
if (await that.isDir(
|
|
919
|
+
if (file === "") continue;
|
|
920
|
+
const fullPath = joinPath(parent, file);
|
|
921
|
+
if (await that.isDir(fullPath)) {
|
|
1309
922
|
if (file.includes(query)) {
|
|
1310
|
-
result.push(
|
|
923
|
+
result.push(fullPath + "/");
|
|
1311
924
|
}
|
|
1312
925
|
result.push(
|
|
1313
|
-
...(await
|
|
1314
|
-
joinPath(parent, file),
|
|
1315
|
-
await that.list(joinPath(parent, file)),
|
|
1316
|
-
)),
|
|
926
|
+
...(await searchRecursive(fullPath, await that.list(fullPath))),
|
|
1317
927
|
);
|
|
1318
|
-
} else if (await that.isFile(
|
|
928
|
+
} else if (await that.isFile(fullPath)) {
|
|
1319
929
|
if (file.includes(query)) {
|
|
1320
|
-
|
|
1321
|
-
result.push(joinPath(parent, file));
|
|
930
|
+
result.push(fullPath);
|
|
1322
931
|
}
|
|
1323
932
|
}
|
|
1324
933
|
}
|
|
1325
934
|
return result;
|
|
1326
935
|
}
|
|
1327
|
-
}
|
|
1328
|
-
console.error(e);
|
|
1329
|
-
if (this.options.throwError) {
|
|
1330
|
-
throw new VVVFSError("Search", "搜索文件失败" + e);
|
|
1331
|
-
}
|
|
1332
|
-
return null;
|
|
1333
|
-
}
|
|
936
|
+
}, null as string[] | null);
|
|
1334
937
|
}
|
|
1335
938
|
/**
|
|
1336
939
|
* 监听文件
|
|
@@ -1349,46 +952,63 @@ class VVVFS {
|
|
|
1349
952
|
* @param path 文件路径
|
|
1350
953
|
*/
|
|
1351
954
|
async lock(path: string) {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
955
|
+
return this.#withErrorHandling("lock", async () => {
|
|
956
|
+
const targetPath = joinPath(path);
|
|
957
|
+
if (!(await this.exists(targetPath))) {
|
|
958
|
+
console.warn("文件不存在");
|
|
959
|
+
return false;
|
|
1357
960
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
console.warn("文件已被锁定");
|
|
1362
|
-
if (this.options.throwError) {
|
|
1363
|
-
throw new VVVFSError("Lock", "文件已被锁定");
|
|
961
|
+
if (await this.#isLocked(targetPath)) {
|
|
962
|
+
console.warn("文件已被锁定");
|
|
963
|
+
return false;
|
|
1364
964
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
965
|
+
const { name, parent } = parsePath(targetPath);
|
|
966
|
+
const fileRecord = await this.#db.files.where({ name, path: parent }).first();
|
|
967
|
+
if (!fileRecord?.id) {
|
|
968
|
+
console.warn("文件记录未找到");
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
await this.#db.files.update(fileRecord.id, { locked: true });
|
|
972
|
+
return true;
|
|
973
|
+
}, false);
|
|
1369
974
|
}
|
|
1370
975
|
/**
|
|
1371
976
|
* 解锁文件
|
|
1372
977
|
* @param path 文件路径
|
|
1373
978
|
*/
|
|
1374
979
|
async unlock(path: string) {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
980
|
+
return this.#withErrorHandling("unlock", async () => {
|
|
981
|
+
const targetPath = joinPath(path);
|
|
982
|
+
if (!(await this.exists(targetPath))) {
|
|
983
|
+
console.warn("文件不存在");
|
|
984
|
+
return false;
|
|
1380
985
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
console.warn("文件未被锁定");
|
|
1385
|
-
if (this.options.throwError) {
|
|
1386
|
-
throw new VVVFSError("Unlock", "文件未被锁定");
|
|
986
|
+
if (!(await this.#isLocked(targetPath))) {
|
|
987
|
+
console.warn("文件未被锁定");
|
|
988
|
+
return false;
|
|
1387
989
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
990
|
+
const { name, parent } = parsePath(targetPath);
|
|
991
|
+
const fileRecord = await this.#db.files.where({ name, path: parent }).first();
|
|
992
|
+
if (!fileRecord?.id) {
|
|
993
|
+
console.warn("文件记录未找到");
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
await this.#db.files.update(fileRecord.id, { locked: false });
|
|
997
|
+
return true;
|
|
998
|
+
}, false);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* 判断文件是否已锁定
|
|
1002
|
+
* @param path 文件路径
|
|
1003
|
+
*/
|
|
1004
|
+
async isLocked(path: string) {
|
|
1005
|
+
return this.#withErrorHandling("isLocked", async () => {
|
|
1006
|
+
const targetPath = joinPath(path);
|
|
1007
|
+
if (!(await this.exists(targetPath))) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
return await this.#isLocked(targetPath);
|
|
1011
|
+
}, false);
|
|
1392
1012
|
}
|
|
1393
1013
|
}
|
|
1394
1014
|
/**
|
|
@@ -1418,7 +1038,7 @@ function joinPath(...paths: string[]) {
|
|
|
1418
1038
|
if (segments.length === 0) return ".";
|
|
1419
1039
|
const isAbsolute = segments[0].startsWith("/");
|
|
1420
1040
|
const parts = segments.join("/").split("/");
|
|
1421
|
-
const stack = [];
|
|
1041
|
+
const stack: string[] = [];
|
|
1422
1042
|
for (const part of parts) {
|
|
1423
1043
|
if (part === "" || part === ".") {
|
|
1424
1044
|
continue;
|
|
@@ -1450,4 +1070,4 @@ Object.defineProperty(VVVFS, "author", {
|
|
|
1450
1070
|
enumerable: true,
|
|
1451
1071
|
configurable: false,
|
|
1452
1072
|
});
|
|
1453
|
-
(globalThis as any).VVVFS = VVVFS;
|
|
1073
|
+
(globalThis as any).VVVFS = VVVFS;
|