vvvfs 0.0.1-alpha.2 → 0.0.2

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/index.ts ADDED
@@ -0,0 +1,656 @@
1
+ import { Dexie, Table } from "dexie";
2
+ import packageJson from "./package.json" with { type: "json" };
3
+ import mime from "mime";
4
+
5
+ /**
6
+ * 虚拟文件系统
7
+ * @author IFTC
8
+ * @description 虚拟文件系统
9
+ */
10
+
11
+ /**
12
+ * 文件记录
13
+ * @param id 文件id
14
+ * @param name 文件名
15
+ * @param path 文件路径
16
+ * @param type 文件类型(file或dir)
17
+ * @param file 文件对象
18
+ */
19
+ export interface FileRecord {
20
+ id?: number;
21
+ name: string;
22
+ path: string;
23
+ type: string;
24
+ file: File | null;
25
+ }
26
+
27
+ /**
28
+ * 虚拟文件系统数据库
29
+ * @param files 文件存储数据表
30
+ */
31
+ export interface VVVFSDatabase extends Dexie {
32
+ files: Table<FileRecord, number>;
33
+ }
34
+
35
+ /**
36
+ * 虚拟文件系统配置项
37
+ * @param throwError 是否抛出错误
38
+ */
39
+ export interface VVVFSOptions {
40
+ throwError: boolean;
41
+ }
42
+
43
+ /**
44
+ * 虚拟文件系统错误类
45
+ */
46
+ class VVVFSError extends Error {
47
+ /**
48
+ * @param type 错误类型
49
+ * @param message 错误信息
50
+ */
51
+ constructor(type: string, message: string) {
52
+ super(message);
53
+ this.name = "VVVFS" + type + "Error";
54
+ }
55
+ }
56
+
57
+ const version = packageJson.version;
58
+ class VVVFS {
59
+ private db: VVVFSDatabase;
60
+ options: VVVFSOptions;
61
+ /**
62
+ * 虚拟文件系统版本
63
+ */
64
+ static version: string;
65
+ /**
66
+ * 创建虚拟文件系统
67
+ * @param name 虚拟文件系统名称
68
+ * @param options 配置项
69
+ */
70
+ constructor(
71
+ name = "vvvfs",
72
+ options = {
73
+ throwError: false,
74
+ },
75
+ ) {
76
+ this.options = options;
77
+ try {
78
+ this.db = new Dexie(name) as VVVFSDatabase;
79
+ this.db.version(1).stores({
80
+ files: "++id, name, path, type, file",
81
+ });
82
+ } catch (error) {
83
+ console.error("创建数据库失败", error);
84
+ throw new VVVFSError("CreateDatabase", "创建数据库失败");
85
+ }
86
+ }
87
+ /**
88
+ * 重置虚拟文件系统
89
+ */
90
+ async reset() {
91
+ try {
92
+ await this.db.delete();
93
+ this.db = new Dexie(this.db.name) as VVVFSDatabase;
94
+ this.db.version(1).stores({
95
+ files: "++id, name, path, type, file",
96
+ });
97
+ } catch (error) {
98
+ console.error("重置数据库失败", error);
99
+ throw new VVVFSError("ResetDatabase", "重置数据库失败" + error);
100
+ }
101
+ }
102
+ /**
103
+ * 创建文件
104
+ * @param path 文件路径
105
+ */
106
+ async createFile(path: string) {
107
+ if (await this.exists(path)) {
108
+ console.warn("文件已存在");
109
+ return true;
110
+ }
111
+ try {
112
+ const { name, parent } = parsePath(path);
113
+ if (!(await this.exists(parent))) {
114
+ await this.createDir(parent);
115
+ }
116
+ // console.log(name, parent);
117
+ await this.db.files.add({
118
+ name: name,
119
+ path: parent,
120
+ type: "file",
121
+ file: new File([], name, {
122
+ type: mime.getType(path) || "application/octet-stream",
123
+ }),
124
+ });
125
+ return true;
126
+ } catch (error) {
127
+ console.error("创建文件失败", error);
128
+ if (this.options.throwError) {
129
+ throw new VVVFSError("CreateFile", "创建文件失败" + error);
130
+ }
131
+ return false;
132
+ }
133
+ }
134
+ /**
135
+ * 创建目录
136
+ * @param path 目录路径
137
+ */
138
+ async createDir(path: string) {
139
+ if (await this.exists(path)) {
140
+ console.warn("目录已存在");
141
+ return true;
142
+ }
143
+ try {
144
+ const { name, parent } = parsePath(path);
145
+ if (!(await this.exists(parent))) {
146
+ if (parent == "/") {
147
+ await this.db.files.add({
148
+ name: "",
149
+ path: "/",
150
+ type: "dir",
151
+ file: new File([], ""),
152
+ });
153
+ if (name == "") return true;
154
+ } else {
155
+ await this.createDir(parent);
156
+ }
157
+ }
158
+ console.log(name, parent);
159
+ await this.db.files.add({
160
+ name: name,
161
+ path: parent,
162
+ type: "dir",
163
+ file: new File([], name),
164
+ });
165
+ return true;
166
+ } catch (error) {
167
+ console.error("创建目录失败", error);
168
+ if (this.options.throwError) {
169
+ throw new VVVFSError("CreateDir", "创建目录失败" + error);
170
+ }
171
+ return false;
172
+ }
173
+ }
174
+ /**
175
+ * 判断文件是否存在
176
+ * @param path 文件路径
177
+ */
178
+ async exists(path: string) {
179
+ try {
180
+ const { name, parent } = parsePath(path);
181
+ return (
182
+ (await this.db.files
183
+ .where({
184
+ name: name,
185
+ path: parent,
186
+ })
187
+ .count()) > 0
188
+ );
189
+ } catch (error) {
190
+ console.error("判断文件是否存在失败", error);
191
+ if (this.options.throwError) {
192
+ throw new VVVFSError("Exists", "判断文件是否存在失败" + error);
193
+ }
194
+ return false;
195
+ }
196
+ }
197
+ /**
198
+ * 读取文件内容
199
+ * @param path 文件路径
200
+ */
201
+ async write(path: string, content: Blob) {
202
+ try {
203
+ if (!(await this.exists(path))) {
204
+ const success = await this.createFile(path);
205
+ if (!success) {
206
+ console.warn("创建文件失败");
207
+ if (this.options.throwError) {
208
+ throw new VVVFSError("Write", "创建文件失败");
209
+ }
210
+ return false;
211
+ }
212
+ }
213
+ const { name, parent } = parsePath(path);
214
+ console.log(name, parent);
215
+ if (await this.isDir(parent + "/" + name)) {
216
+ console.warn("路径已存在");
217
+ if (this.options.throwError) {
218
+ throw new VVVFSError("Write", "路径已存在");
219
+ }
220
+ return false;
221
+ }
222
+ const file = new File([content], name, {
223
+ type: mime.getType(path) || "application/octet-stream",
224
+ });
225
+ console.log(file);
226
+ const fileRecord = await this.db.files.where({ name, path: parent }).first();
227
+ if (fileRecord) {
228
+ await this.db.files.put({
229
+ ...fileRecord,
230
+ file: file,
231
+ });
232
+ return true;
233
+ } else {
234
+ console.warn("文件记录未找到,无法写入内容");
235
+ if (this.options.throwError) {
236
+ throw new VVVFSError("Write", "文件记录未找到,无法写入内容");
237
+ }
238
+ return false;
239
+ }
240
+ } catch (error) {
241
+ console.error("写入文件失败", error);
242
+ if (this.options.throwError) {
243
+ throw new VVVFSError("Write", "写入文件失败" + error);
244
+ }
245
+ return false;
246
+ }
247
+ }
248
+ /**
249
+ * 写入文本内容
250
+ * @param path 文件路径
251
+ * @param content 文本内容
252
+ */
253
+ async writeText(path: string, content: string) {
254
+ try {
255
+ const blob = new Blob([content], { type: "text/plain" });
256
+ return await this.write(path, blob);
257
+ } catch (error) {
258
+ console.error("写入文件失败", error);
259
+ if (this.options.throwError) {
260
+ throw new VVVFSError("Write", "写入文件失败" + error);
261
+ }
262
+ return false;
263
+ }
264
+ }
265
+ /**
266
+ * 写入JSON内容
267
+ * @param path 文件路径
268
+ * @param content JSON内容
269
+ * @param format 是否格式化
270
+ */
271
+ async writeJson(path: string, content: Record<string, any>, format?: boolean) {
272
+ try {
273
+ return await this.writeText(
274
+ path,
275
+ JSON.stringify(content, null, format ? 4 : undefined),
276
+ );
277
+ } catch (error) {
278
+ console.error("写入文件失败", error);
279
+ if (this.options.throwError) {
280
+ throw new VVVFSError("Write", "写入文件失败" + error);
281
+ }
282
+ return false;
283
+ }
284
+ }
285
+ /**
286
+ * 读取文件内容
287
+ * @param path 文件路径
288
+ */
289
+ async read(path: string) {
290
+ try {
291
+ if (!(await this.exists(path))) {
292
+ console.warn("文件不存在");
293
+ if (this.options.throwError) {
294
+ throw new VVVFSError("Read", "文件不存在");
295
+ }
296
+ return null;
297
+ }
298
+ const { name, parent } = parsePath(path);
299
+ return (await this.db.files.where({ name, path: parent }).first())?.file;
300
+ } catch (error) {
301
+ console.error("读取文件失败", error);
302
+ if (this.options.throwError) {
303
+ throw new VVVFSError("Read", "读取文件失败" + error);
304
+ }
305
+ return null;
306
+ }
307
+ }
308
+ /**
309
+ * 读取文件内容
310
+ * @param path 文件路径
311
+ */
312
+ async readText(path: string) {
313
+ try {
314
+ const file = await this.read(path);
315
+ if (file) {
316
+ return await file.text();
317
+ } else {
318
+ console.warn("文件不存在");
319
+ if (this.options.throwError) {
320
+ throw new VVVFSError("Read", "文件不存在");
321
+ }
322
+ return null;
323
+ }
324
+ } catch (error) {
325
+ console.error("读取文件失败", error);
326
+ if (this.options.throwError) {
327
+ throw new VVVFSError("Read", "读取文件失败" + error);
328
+ }
329
+ return null;
330
+ }
331
+ }
332
+ /**
333
+ * 读取JSON内容
334
+ * @param path 文件路径
335
+ */
336
+ async readJson(path: string) {
337
+ try {
338
+ const text = await this.readText(path);
339
+ if (text) {
340
+ try {
341
+ return JSON.parse(text);
342
+ } catch (error) {
343
+ console.error("解析JSON失败", error);
344
+ if (this.options.throwError) {
345
+ throw new VVVFSError("Read", "解析JSON失败" + error);
346
+ }
347
+ return null;
348
+ }
349
+ } else {
350
+ return null;
351
+ }
352
+ } catch (error) {
353
+ console.error("读取文件失败", error);
354
+ if (this.options.throwError) {
355
+ throw new VVVFSError("Read", "读取文件失败" + error);
356
+ }
357
+ return null;
358
+ }
359
+ }
360
+ /**
361
+ * 判断是否是文件
362
+ * @param path 文件路径
363
+ */
364
+ async isFile(path: string) {
365
+ try {
366
+ const { name, parent } = parsePath(path);
367
+ return (await this.db.files.where({ name, path: parent, type: "file" }).count()) > 0;
368
+ } catch (error) {
369
+ console.error("判断文件类型失败", error);
370
+ if (this.options.throwError) {
371
+ throw new VVVFSError("IsFile", "判断文件类型失败" + error);
372
+ }
373
+ return false;
374
+ }
375
+ }
376
+ /**
377
+ * 判断是否是目录
378
+ * @param path 文件路径
379
+ */
380
+ async isDir(path: string) {
381
+ try {
382
+ const { name, parent } = parsePath(path);
383
+ return (await this.db.files.where({ path: parent, name, type: "dir" }).count()) > 0;
384
+ } catch (error) {
385
+ console.error("判断文件类型失败", error);
386
+ if (this.options.throwError) {
387
+ throw new VVVFSError("IsDir", "判断文件类型失败" + error);
388
+ }
389
+ return false;
390
+ }
391
+ }
392
+ /**
393
+ * 列出目录下的文件
394
+ * @param path 目录路径
395
+ */
396
+ async list(path: string) {
397
+ try {
398
+ const { name, parent } = parsePath(path);
399
+ return (await this.db.files.where({ path: parent }).toArray()).map((file) => file.name);
400
+ } catch (error) {
401
+ console.error("列出目录下的文件失败", error);
402
+ if (this.options.throwError) {
403
+ throw new VVVFSError("List", "列出目录下的文件失败" + error);
404
+ }
405
+ return [];
406
+ }
407
+ }
408
+ /**
409
+ * 重命名文件
410
+ * @param path 文件路径
411
+ * @param newName 新文件名
412
+ */
413
+ async rename(path: string, newName: string) {
414
+ try {
415
+ if (!(await this.exists(path))) {
416
+ console.warn("文件不存在");
417
+ if (this.options.throwError) {
418
+ throw new VVVFSError("Rename", "文件不存在");
419
+ }
420
+ return false;
421
+ }
422
+ const { name, parent } = parsePath(path);
423
+ const file = await this.db.files.get({ path, name });
424
+ await this.db.files.put({
425
+ name: newName,
426
+ path: parent,
427
+ type: "file",
428
+ file: file!.file,
429
+ });
430
+ return true;
431
+ } catch (e) {
432
+ console.error(e);
433
+ if (this.options.throwError) {
434
+ throw new VVVFSError("Rename", "重命名文件失败" + e);
435
+ }
436
+ return false;
437
+ }
438
+ }
439
+ /**
440
+ * 删除文件
441
+ * @param path 文件路径
442
+ */
443
+ async delete(path: string) {
444
+ try {
445
+ if (!(await this.exists(path))) {
446
+ console.warn("文件不存在");
447
+ if (this.options.throwError) {
448
+ throw new VVVFSError("Delete", "文件不存在");
449
+ }
450
+ return false;
451
+ }
452
+ const { name, parent } = parsePath(path);
453
+ if (await this.isDir(path)) {
454
+ const files = await this.list(path);
455
+ for (const file of files) {
456
+ if (await this.isDir(joinPath(path, file))) {
457
+ const fileRecord = await this.db.files
458
+ .where({ name, path: parent })
459
+ .first();
460
+ await this.db.files.delete(fileRecord!.id!);
461
+ }
462
+ await this.delete(joinPath(parent, file));
463
+ }
464
+ return true;
465
+ } else {
466
+ const fileRecord = await this.db.files.where({ name, path: parent }).first();
467
+ await this.db.files.delete(fileRecord!.id!);
468
+ return true;
469
+ }
470
+ } catch (e) {
471
+ console.error(e);
472
+ if (this.options.throwError) {
473
+ throw new VVVFSError("Delete", "删除文件失败" + e);
474
+ }
475
+ return false;
476
+ }
477
+ }
478
+ /**
479
+ * 移动文件
480
+ * @param path 文件路径
481
+ * @param newPath 新路径
482
+ */
483
+ async move(path: string, newPath: string) {
484
+ try {
485
+ if (await this.exists(newPath)) {
486
+ console.warn("目标文件已存在");
487
+ if (this.options.throwError) {
488
+ throw new VVVFSError("Move", "目标文件已存在");
489
+ }
490
+ return false;
491
+ }
492
+ if (!(await this.exists(path))) {
493
+ console.warn("源文件不存在");
494
+ if (this.options.throwError) {
495
+ throw new VVVFSError("Move", "源文件不存在");
496
+ }
497
+ return false;
498
+ }
499
+ const { name, parent } = parsePath(path);
500
+ const { name: newName, parent: newParent } = parsePath(newPath);
501
+ await this.createDir(newParent);
502
+ if (await this.isDir(path)) {
503
+ const dirs = await this.list(path);
504
+ for (const dir of dirs) {
505
+ await this.move(path + "/" + dir, newPath + "/" + dir);
506
+ }
507
+ } else {
508
+ const fileRecord = await this.db.files.where({ name, path: parent }).first();
509
+ await this.db.files.update(fileRecord!.id!, { name: newName, path: newParent });
510
+ }
511
+ await this.delete(path);
512
+ return true;
513
+ } catch (e) {
514
+ console.error(e);
515
+ if (this.options.throwError) {
516
+ throw new VVVFSError("Move", "移动文件失败" + e);
517
+ }
518
+ return false;
519
+ }
520
+ }
521
+ /**
522
+ * 复制文件
523
+ * @param path 文件路径
524
+ * @param newPath 新路径
525
+ */
526
+ async copy(path: string, newPath: string) {
527
+ try {
528
+ if (await this.exists(newPath)) {
529
+ console.warn("目标文件已存在");
530
+ if (this.options.throwError) {
531
+ throw new VVVFSError("Copy", "目标文件已存在");
532
+ }
533
+ return false;
534
+ }
535
+ if (!(await this.exists(path))) {
536
+ console.warn("源文件不存在");
537
+ if (this.options.throwError) {
538
+ throw new VVVFSError("Copy", "源文件不存在");
539
+ }
540
+ return false;
541
+ }
542
+ const { name, parent } = parsePath(path);
543
+ const { name: newName, parent: newParent } = parsePath(newPath);
544
+ await this.createDir(newParent);
545
+ if (await this.isDir(path)) {
546
+ const dirs = await this.list(path);
547
+ for (const dir of dirs) {
548
+ await this.move(path + "/" + dir, newPath + "/" + dir);
549
+ }
550
+ } else {
551
+ const fileRecord = await this.db.files.where({ name, path: parent }).first();
552
+ await this.db.files.update(fileRecord!.id!, { name: newName, path: newParent });
553
+ }
554
+ return true;
555
+ } catch (e) {
556
+ console.error(e);
557
+ if (this.options.throwError) {
558
+ throw new VVVFSError("Copy", "复制文件失败" + e);
559
+ }
560
+ return false;
561
+ }
562
+ }
563
+ /**
564
+ * 搜索文件
565
+ * @param basePath 基础路径
566
+ * @param query 查询字符串
567
+ */
568
+ async search(basePath: string, query: string) {
569
+ try {
570
+ if (!(await this.isDir(basePath))) {
571
+ console.warn("基础路径不是目录");
572
+ if (this.options.throwError) {
573
+ throw new VVVFSError("Search", "基础路径不是目录");
574
+ }
575
+ return null;
576
+ }
577
+ const that = this;
578
+ const dirs = await this.list(basePath);
579
+ return await search(basePath, dirs);
580
+ async function search(parent: string, files: string[]) {
581
+ const result: string[] = [];
582
+ console.log(files);
583
+ for (const file of files) {
584
+ if (file == "") continue;
585
+ console.log(file);
586
+ if (await that.isDir(joinPath(parent, file))) {
587
+ if (file.includes(query)) {
588
+ result.push(joinPath(parent, file) + "/");
589
+ }
590
+ result.push(
591
+ ...(await search(
592
+ joinPath(parent, file),
593
+ await that.list(joinPath(parent, file)),
594
+ )),
595
+ );
596
+ } else {
597
+ if (file.includes(query)) {
598
+ result.push(joinPath(parent, file));
599
+ }
600
+ }
601
+ }
602
+ return result;
603
+ }
604
+ } catch (e) {
605
+ console.error(e);
606
+ if (this.options.throwError) {
607
+ throw new VVVFSError("Search", "搜索文件失败" + e);
608
+ }
609
+ return null;
610
+ }
611
+ }
612
+ }
613
+ function parsePath(path: string) {
614
+ path = joinPath(path);
615
+ const oldParts = path.split("/");
616
+ const parts: string[] = [];
617
+ for (let i = 0; i < oldParts.length; i++) {
618
+ if (oldParts[i]) parts.push(oldParts[i]);
619
+ }
620
+ const name = parts.pop() || "";
621
+ const parent = "/" + parts.join("/");
622
+ if ((parent + "/" + name).length > 255) throw new Error("文件名过长");
623
+ return { name, parent };
624
+ }
625
+ function joinPath(...paths: string[]) {
626
+ const segments = paths.map((p) => String(p)).filter((p) => p.length > 0);
627
+ if (segments.length === 0) return ".";
628
+ const isAbsolute = segments[0].startsWith("/");
629
+ const parts = segments.join("/").split("/");
630
+ const stack = [];
631
+ for (const part of parts) {
632
+ if (part === "" || part === ".") {
633
+ continue;
634
+ } else if (part === "..") {
635
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
636
+ stack.pop();
637
+ } else if (!isAbsolute) {
638
+ stack.push("..");
639
+ }
640
+ } else {
641
+ stack.push(part);
642
+ }
643
+ }
644
+ let result = stack.join("/");
645
+ if (isAbsolute) {
646
+ result = "/" + result;
647
+ }
648
+ return result || (isAbsolute ? "/" : ".");
649
+ }
650
+ Object.defineProperty(VVVFS, "version", {
651
+ value: version,
652
+ writable: false,
653
+ enumerable: true,
654
+ configurable: false,
655
+ });
656
+ (globalThis as any).VVVFS = VVVFS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vvvfs",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.2",
4
4
  "description": "一个使用IndexDB实现的虚拟文件系统",
5
5
  "keywords": [
6
6
  "Virtual",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "license": "MIT",
17
17
  "author": "IFTC",
18
- "type": "commonjs",
18
+ "type": "module",
19
19
  "main": "index.js",
20
20
  "scripts": {
21
21
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "dexie": "^4.3.0",
26
- "esbuild": "^0.27.3"
26
+ "esbuild": "^0.27.3",
27
+ "mime": "^4.1.0"
27
28
  }
28
29
  }