vvvfs 0.1.7 → 0.1.9

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 CHANGED
@@ -27,6 +27,7 @@ const VVVFS = require("vvvfs"); // CommonJS
27
27
  <script src="https://cdn.jsdelivr.net/npm/vvvfs@latest/dist/vvvfs.min.js"></script>
28
28
  <script src="https://unpkg.com/vvvfs@latest/dist/vvvfs.min.js"></script>
29
29
  <!-- 浏览器 -->
30
+ <!-- GitHub Release: https://github.com/IFTC-XLKJ/vvvfs/releases -->
30
31
  ```
31
32
 
32
33
  3. 初始化
@@ -95,6 +96,16 @@ const vvvfs = new VVVFS("vvvfs", {
95
96
 
96
97
  ## 更新日志
97
98
 
99
+ ### 0.1.9
100
+
101
+ - 新增 `saveFile` 方法,用于保存文件
102
+ - 新增 `downloadFile` 方法,用于下载文件并保存到IndexedDB(支持`fetch`和`xhr`两种模式,以及下载进度、错误和成功事件)
103
+ - `VVVFS.File` 类新增 `save` 和 `download` 方法
104
+
105
+ ### 0.1.8
106
+
107
+ - 新增 `writeJsonValue` 和 `readJsonValue` 方法,用于读取和写入JSON值(键支持字符串、数字数组下标以及嵌套路径,如 `["a", "b"]`)
108
+
98
109
  ### 0.1.7
99
110
 
100
111
  - 新增 `init` 选项,用于是否自动初始化,传入字符串时,则为初始化的用户名,传入true时,则为默认用户名。
@@ -268,6 +279,16 @@ Using the `VVVFS.File` class:
268
279
 
269
280
  ## Changelog
270
281
 
282
+ ### 0.1.9
283
+
284
+ - Added the `saveFile` method for saving files
285
+ - Added the `downloadFile` method for downloading files to IndexedDB (supports both `fetch` and `xhr` modes, as well as progress, error, and success callbacks)
286
+ - Added the `save` and `download` methods to the `VVVFS.File` class
287
+
288
+ ### 0.1.8
289
+
290
+ - Added the `writeJsonValue` and `readJsonValue` methods for reading and writing JSON values (keys support strings, numeric array indexes, and nested paths such as `["a", "b"]`)
291
+
271
292
  ### 0.1.7
272
293
 
273
294
  - Added the `init` option to control automatic initialization: when passed a string, it is used as the username for initialization; when passed `true`, the default username is used.
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 虚拟文件系统
3
+ * @author IFTC
4
+ * @description 虚拟文件系统
5
+ */
6
+ export * from "./types/index";
package/index.ts CHANGED
@@ -44,6 +44,22 @@ export interface VVVFSOptions {
44
44
  init: boolean | string;
45
45
  }
46
46
 
47
+ /**
48
+ * 虚拟文件系统下载配置项
49
+ * @param headers 请求头
50
+ * @param mode 下载模式(fetch或xhr)
51
+ * @param onProgress 下载进度事件
52
+ * @param onError 下载出错事件
53
+ * @param onSuccess 下载成功事件
54
+ */
55
+ export interface VVVFSDownloadOptions {
56
+ headers?: Record<string, string>;
57
+ mode?: "fetch" | "xhr";
58
+ onProgress?: (progress: number) => void;
59
+ onError?: (error: any) => void;
60
+ onSuccess?: (success: boolean) => void;
61
+ }
62
+
47
63
  /**
48
64
  * 虚拟文件系统错误类
49
65
  */
@@ -170,6 +186,21 @@ class VVVFSFile {
170
186
  async writeJSON(json: Record<string, unknown>, format: boolean = true) {
171
187
  return await this._vvvfs.writeJson(this._path, json, format);
172
188
  }
189
+ /**
190
+ * 写入文件JSON值
191
+ * @param key 键名
192
+ * @param value 键值
193
+ */
194
+ async writeJsonValue(key: string | number | Array<string | number>, value: any) {
195
+ return await this._vvvfs.writeJsonValue(this._path, key, value);
196
+ }
197
+ /**
198
+ * 读取文件JSON值
199
+ * @param key 键名
200
+ */
201
+ async readJsonValue(key: string | number | Array<string | number>): Promise<any> {
202
+ return await this._vvvfs.readJsonValue(this._path, key);
203
+ }
173
204
  /**
174
205
  * 追加文件内容
175
206
  * @param file 文件对象
@@ -274,6 +305,21 @@ class VVVFSFile {
274
305
  async isLocked() {
275
306
  return await this._vvvfs.isLocked(this._path);
276
307
  }
308
+ /**
309
+ * 保存文件
310
+ * @param name 文件名
311
+ */
312
+ async save(name: string) {
313
+ return await this._vvvfs.saveFile(this._path, name);
314
+ }
315
+ /**
316
+ * 下载文件
317
+ * @param url 文件链接
318
+ * @param options 下载配置项
319
+ */
320
+ async download(url: string | URL | Request, options: VVVFSDownloadOptions = {}) {
321
+ return await this._vvvfs.downloadFile(url, this._path, options);
322
+ }
277
323
  }
278
324
 
279
325
  /**
@@ -828,6 +874,61 @@ class VVVFS {
828
874
  return await this.append(path, blob);
829
875
  }, false);
830
876
  }
877
+ /**
878
+ * 写入 JSON 值
879
+ * @param path 文件路径
880
+ * @param key 键名
881
+ * @param value 键值
882
+ * @example await vvvfs.writeJsonValue("/example.json", "a", { b: "example"});
883
+ * @example await vvvfs.writeJsonValue("/example.json", ["a", "b"], "example2");
884
+ * @example await vvvfs.writeJsonValue("/example.json", 0, "example");
885
+ */
886
+ async writeJsonValue(path: string, key: string | number | Array<string | number>, value: any) {
887
+ return this.#withErrorHandling("writeJsonValue", async () => {
888
+ let root: any = (await this.readJson(path)) ?? {};
889
+ const keys = Array.isArray(key) ? key : [key];
890
+ if (keys.length > 0) {
891
+ let json: any = root;
892
+ for (let i = 0; i < keys.length - 1; i++) {
893
+ const currentKey = keys[i];
894
+ const nextKey = keys[i + 1];
895
+ if (
896
+ json[currentKey] === undefined ||
897
+ json[currentKey] === null ||
898
+ typeof json[currentKey] !== "object"
899
+ ) {
900
+ json[currentKey] = typeof nextKey === "number" ? [] : {};
901
+ }
902
+ json = json[currentKey];
903
+ }
904
+ json[keys[keys.length - 1]] = value;
905
+ } else {
906
+ root = value;
907
+ }
908
+ return await this.writeJson(path, root);
909
+ }, false);
910
+ }
911
+ /**
912
+ * 读取 JSON 值
913
+ * @param path 文件路径
914
+ * @param key 键名
915
+ * @example await vvvfs.readJsonValue("/example.json", "a");
916
+ * @example await vvvfs.readJsonValue("/example.json", ["a", "b"]);
917
+ * @example await vvvfs.readJsonValue("/example.json", 0);
918
+ */
919
+ async readJsonValue(path: string, key: string | number | Array<string | number>): Promise<any> {
920
+ return this.#withErrorHandling("readJsonValue", async () => {
921
+ const json = await this.readJson(path);
922
+ if (json === null || json === undefined) return null;
923
+ const keys = Array.isArray(key) ? key : [key];
924
+ let current: any = json;
925
+ for (const keyPart of keys) {
926
+ if (current === null || current === undefined) return null;
927
+ current = current[keyPart];
928
+ }
929
+ return current === undefined ? null : current;
930
+ }, null as any);
931
+ }
831
932
  /**
832
933
  * 内部读取(跳过访问检查,供内部方法调用)
833
934
  */
@@ -1227,6 +1328,166 @@ class VVVFS {
1227
1328
  return await this.#isLocked(targetPath);
1228
1329
  }, false);
1229
1330
  }
1331
+ /**
1332
+ * 保存文件
1333
+ * @param path 文件路径
1334
+ * @param name 文件名
1335
+ */
1336
+ async saveFile(path: string, name?: string) {
1337
+ return this.#withErrorHandling("saveFile", async () => {
1338
+ const targetPath = joinPath(path);
1339
+ if (!(await this.#checkAccess(targetPath, "read"))) return false;
1340
+ const file = await this.#internalRead(targetPath);
1341
+ if (!file) {
1342
+ console.warn("文件不存在");
1343
+ return false;
1344
+ }
1345
+ const a = document.createElement("a");
1346
+ a.href = URL.createObjectURL(file);
1347
+ a.download = name || Path.basename(targetPath);
1348
+ a.click();
1349
+ URL.revokeObjectURL(a.href);
1350
+ return true;
1351
+ }, false);
1352
+ }
1353
+ /**
1354
+ * 下载文件
1355
+ * @param url 文件链接
1356
+ * @param path 保存路径
1357
+ * @param options 下载配置项
1358
+ */
1359
+ async downloadFile(
1360
+ url: string | URL | Request,
1361
+ path: string,
1362
+ options: VVVFSDownloadOptions = {},
1363
+ ) {
1364
+ return this.#withErrorHandling("downloadFile", async () => {
1365
+ const targetPath = joinPath(path);
1366
+ if (!(await this.#checkAccess(targetPath, "write"))) return false;
1367
+ const content =
1368
+ (options.mode || "fetch") === "xhr"
1369
+ ? await this.#downloadWithXHR(url, options)
1370
+ : await this.#downloadWithFetch(url, options);
1371
+ if (!content) return false;
1372
+ const blob = new Blob([content], {
1373
+ type: mime.getType(targetPath) || "application/octet-stream",
1374
+ });
1375
+ const success = await this.#internalWrite(targetPath, blob);
1376
+ if (success && options.onSuccess) {
1377
+ options.onSuccess(success);
1378
+ }
1379
+ return success;
1380
+ }, false);
1381
+ }
1382
+ /**
1383
+ * 使用fetch下载文件
1384
+ * @param url 文件链接
1385
+ * @param options 下载配置项
1386
+ */
1387
+ async #downloadWithFetch(
1388
+ url: string | URL | Request,
1389
+ options: VVVFSDownloadOptions,
1390
+ ): Promise<Blob | null> {
1391
+ const request =
1392
+ url instanceof Request
1393
+ ? url
1394
+ : new Request(url, { headers: options.headers });
1395
+ const response = await fetch(request);
1396
+ if (!response.ok) {
1397
+ const error = new Error(`下载失败,HTTP ${response.status}`);
1398
+ if (options.onError) {
1399
+ options.onError(error);
1400
+ return null;
1401
+ }
1402
+ throw error;
1403
+ }
1404
+ const total = Number(response.headers.get("Content-Length") || 0);
1405
+ if (options.onProgress && response.body && total > 0) {
1406
+ const reader = response.body.getReader();
1407
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
1408
+ let loaded = 0;
1409
+ while (true) {
1410
+ const { done, value } = await reader.read();
1411
+ if (done) break;
1412
+ const chunk = value as Uint8Array<ArrayBuffer>;
1413
+ chunks.push(chunk);
1414
+ loaded += chunk.length;
1415
+ options.onProgress(Math.round((loaded / total) * 100));
1416
+ }
1417
+ return new Blob(chunks);
1418
+ }
1419
+ return await response.blob();
1420
+ }
1421
+ /**
1422
+ * 使用XHR下载文件
1423
+ * @param url 文件链接
1424
+ * @param options 下载配置项
1425
+ */
1426
+ #downloadWithXHR(
1427
+ url: string | URL | Request,
1428
+ options: VVVFSDownloadOptions,
1429
+ ): Promise<Blob | null> {
1430
+ return new Promise((resolve, reject) => {
1431
+ const requestUrl =
1432
+ url instanceof URL
1433
+ ? url.href
1434
+ : url instanceof Request
1435
+ ? url.url
1436
+ : url;
1437
+ const headers = url instanceof Request ? url.headers : options.headers;
1438
+ const xhr = new XMLHttpRequest();
1439
+ xhr.open("GET", requestUrl);
1440
+ if (headers instanceof Headers) {
1441
+ headers.forEach((value, key) => {
1442
+ xhr.setRequestHeader(key, value);
1443
+ });
1444
+ } else if (headers) {
1445
+ for (const [key, value] of Object.entries(headers)) {
1446
+ xhr.setRequestHeader(key, value);
1447
+ }
1448
+ }
1449
+ xhr.responseType = "blob";
1450
+ xhr.onprogress = (event) => {
1451
+ if (options.onProgress && event.total > 0) {
1452
+ options.onProgress(
1453
+ Math.round((event.loaded / event.total) * 100),
1454
+ );
1455
+ }
1456
+ };
1457
+ xhr.onload = () => {
1458
+ if (xhr.status >= 200 && xhr.status < 300) {
1459
+ resolve(xhr.response as Blob);
1460
+ } else {
1461
+ const error = new Error(`下载失败,HTTP ${xhr.status}`);
1462
+ if (options.onError) {
1463
+ options.onError(error);
1464
+ resolve(null);
1465
+ } else {
1466
+ reject(error);
1467
+ }
1468
+ }
1469
+ };
1470
+ xhr.onerror = () => {
1471
+ const error = new Error("下载失败");
1472
+ if (options.onError) {
1473
+ options.onError(error);
1474
+ resolve(null);
1475
+ } else {
1476
+ reject(error);
1477
+ }
1478
+ };
1479
+ try {
1480
+ xhr.send();
1481
+ } catch (error) {
1482
+ if (options.onError) {
1483
+ options.onError(error);
1484
+ resolve(null);
1485
+ } else {
1486
+ reject(error);
1487
+ }
1488
+ }
1489
+ });
1490
+ }
1230
1491
  }
1231
1492
  /**
1232
1493
  * 解析路径
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vvvfs",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "一个使用IndexDB实现的虚拟文件系统",
5
5
  "keywords": [
6
6
  "Virtual",
@@ -17,6 +17,7 @@
17
17
  "author": "IFTC",
18
18
  "type": "module",
19
19
  "main": "index.js",
20
+ "types": "index.d.ts",
20
21
  "scripts": {
21
22
  "test": "echo \"Error: no test specified\" && exit 1",
22
23
  "build": "node esbuild.config.js"
package/types/index.d.ts CHANGED
@@ -1,17 +1,28 @@
1
1
  import { Dexie, Table } from "dexie";
2
2
 
3
+ /**
4
+ * VVVFS 类构造签名(避免在内 declare global 中自引用)
5
+ */
6
+ type VVVFSConstructor = typeof VVVFS;
7
+
8
+ /**
9
+ * VVVFS 类实例类型
10
+ */
11
+ type VVVFSInstance = VVVFS;
12
+
3
13
  /**
4
14
  * 虚拟文件系统
5
15
  */
6
16
  declare global {
7
17
  // 扩展 Window 接口
8
- interface window {
9
- VVVFS: typeof VVVFS; // 如果 VVFS 是个类,这里可能需要调整,或者直接定义 vvfs 实例
18
+ interface Window {
19
+ VVVFS: VVVFSConstructor;
20
+ vvvfs: VVVFSInstance;
10
21
  }
11
22
 
12
23
  // 扩展 GlobalThis (针对你在 index.js 里的 globalThis.vvfs 写法)
13
- // 注意:GlobalThis 和 Window 往往指向同一个对象,但在 TS 类型中有时需要分别处理
14
- // var VVVFS: typeof VVVFS;
24
+ var VVVFS: VVVFSConstructor;
25
+ var vvvfs: VVVFSInstance;
15
26
  }
16
27
 
17
28
 
@@ -24,7 +35,7 @@ declare global {
24
35
  * @param file 文件对象
25
36
  * @param locked 是否锁定
26
37
  */
27
- declare interface FileRecord {
38
+ export interface FileRecord {
28
39
  id?: number;
29
40
  name: string;
30
41
  path: string;
@@ -37,7 +48,7 @@ declare interface FileRecord {
37
48
  * 虚拟文件系统数据库
38
49
  * @param files 文件存储数据表
39
50
  */
40
- declare interface VVVFSDatabase extends Dexie {
51
+ export interface VVVFSDatabase extends Dexie {
41
52
  files: Table<FileRecord, number>;
42
53
  }
43
54
 
@@ -51,10 +62,26 @@ export interface VVVFSOptions {
51
62
  init: boolean | string;
52
63
  }
53
64
 
65
+ /**
66
+ * 虚拟文件系统下载配置项
67
+ * @param headers 请求头
68
+ * @param mode 下载模式(fetch或xhr)
69
+ * @param onProgress 下载进度事件
70
+ * @param onError 下载出错事件
71
+ * @param onSuccess 下载成功事件
72
+ */
73
+ export interface VVVFSDownloadOptions {
74
+ headers?: Record<string, string>;
75
+ mode?: "fetch" | "xhr";
76
+ onProgress?: (progress: number) => void;
77
+ onError?: (error: any) => void;
78
+ onSuccess?: (success: boolean) => void;
79
+ }
80
+
54
81
  /**
55
82
  * 虚拟文件系统错误类
56
83
  */
57
- declare class VVVFSError extends Error {
84
+ export class VVVFSError extends Error {
58
85
  /**
59
86
  * @param type 错误类型
60
87
  * @param message 错误信息
@@ -66,7 +93,7 @@ declare class VVVFSError extends Error {
66
93
  * 虚拟文件系统文件类
67
94
  * @param path 文件路径
68
95
  */
69
- declare class VVVFSFile {
96
+ export class VVVFSFile {
70
97
  /**
71
98
  * 获取文件路径
72
99
  */
@@ -133,6 +160,17 @@ declare class VVVFSFile {
133
160
  * @param json 文件JSON内容
134
161
  */
135
162
  writeJSON(json: Record<string, unknown>, format?: boolean): Promise<boolean>;
163
+ /**
164
+ * 写入文件JSON值
165
+ * @param key 键名
166
+ * @param value 键值
167
+ */
168
+ writeJsonValue(key: string | number | Array<string | number>, value: any): Promise<boolean>;
169
+ /**
170
+ * 读取文件JSON值
171
+ * @param key 键名
172
+ */
173
+ readJsonValue(key: string | number | Array<string | number>): Promise<any>;
136
174
  /**
137
175
  * 追加文件内容
138
176
  * @param file 文件对象
@@ -203,12 +241,23 @@ declare class VVVFSFile {
203
241
  * 判断文件是否已锁定
204
242
  */
205
243
  isLocked(): Promise<boolean>;
244
+ /**
245
+ * 保存文件
246
+ * @param name 文件名
247
+ */
248
+ save(name: string): Promise<boolean>;
249
+ /**
250
+ * 下载文件
251
+ * @param url 文件链接
252
+ * @param options 下载配置项
253
+ */
254
+ download(url: string | URL | Request, options?: VVVFSDownloadOptions): Promise<boolean>;
206
255
  }
207
256
 
208
257
  /**
209
258
  * path类(类似于NodeJS中的path模块)
210
259
  */
211
- declare class Path {
260
+ export class Path {
212
261
  /**
213
262
  * 路径分隔符
214
263
  */
@@ -314,7 +363,7 @@ declare class Path {
314
363
  * @author IFTC
315
364
  * @description 虚拟文件系统
316
365
  */
317
- declare class VVVFS {
366
+ export class VVVFS {
318
367
  static defaultDBName: string;
319
368
  /**
320
369
  * 虚拟文件系统版本
@@ -400,6 +449,25 @@ declare class VVVFS {
400
449
  * @param format 是否格式化
401
450
  */
402
451
  writeJson(path: string, content: Record<string, unknown>, format?: boolean): Promise<boolean>;
452
+ /**
453
+ * 写入 JSON 值
454
+ * @param path 文件路径
455
+ * @param key 键名
456
+ * @param value 键值
457
+ * @example await vvvfs.writeJsonValue("/example.json", "a", { b: "example"});
458
+ * @example await vvvfs.writeJsonValue("/example.json", ["a", "b"], "example2");
459
+ * @example await vvvfs.writeJsonValue("/example.json", 0, "example");
460
+ */
461
+ writeJsonValue(path: string, key: string | number | Array<string | number>, value: any): Promise<boolean>;
462
+ /**
463
+ * 读取 JSON 值
464
+ * @param path 文件路径
465
+ * @param key 键名
466
+ * @example await vvvfs.readJsonValue("/example.json", "a");
467
+ * @example await vvvfs.readJsonValue("/example.json", ["a", "b"]);
468
+ * @example await vvvfs.readJsonValue("/example.json", 0);
469
+ */
470
+ readJsonValue(path: string, key: string | number | Array<string | number>): Promise<any>;
403
471
  /**
404
472
  * 追加内容
405
473
  * @param path 文件路径
@@ -491,16 +559,33 @@ declare class VVVFS {
491
559
  * @param path 文件路径
492
560
  */
493
561
  isLocked(path: string): Promise<boolean>;
562
+ /**
563
+ * 保存文件
564
+ * @param path 文件路径
565
+ * @param name 文件名
566
+ */
567
+ saveFile(path: string, name?: string): Promise<boolean>;
568
+ /**
569
+ * 下载文件
570
+ * @param url 文件链接
571
+ * @param path 保存路径
572
+ * @param options 下载配置项
573
+ */
574
+ downloadFile(
575
+ url: string | URL | Request,
576
+ path: string,
577
+ options?: VVVFSDownloadOptions,
578
+ ): Promise<boolean>;
494
579
  }
495
580
 
496
581
  /**
497
582
  * 解析路径
498
583
  * @param path 路径
499
584
  */
500
- declare function parsePath(path: string): { name: string; parent: string };
585
+ export function parsePath(path: string): { name: string; parent: string };
501
586
 
502
587
  /**
503
588
  * 合并路径
504
589
  * @param paths 路径
505
590
  */
506
- declare function joinPath(...paths: string[]): string;
591
+ export function joinPath(...paths: string[]): string;