@zenfs/core 0.3.4 → 0.4.0

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.
Files changed (44) hide show
  1. package/dist/FileIndex.d.ts +6 -0
  2. package/dist/FileIndex.js +3 -3
  3. package/dist/backends/AsyncMirror.d.ts +3 -5
  4. package/dist/backends/AsyncMirror.js +7 -6
  5. package/dist/backends/AsyncStore.d.ts +9 -4
  6. package/dist/backends/AsyncStore.js +7 -2
  7. package/dist/backends/Locked.d.ts +8 -8
  8. package/dist/backends/Locked.js +2 -1
  9. package/dist/backends/Overlay.d.ts +13 -1
  10. package/dist/backends/Overlay.js +16 -16
  11. package/dist/backends/SyncStore.d.ts +8 -5
  12. package/dist/backends/SyncStore.js +9 -5
  13. package/dist/backends/backend.js +8 -8
  14. package/dist/browser.min.js +4 -5
  15. package/dist/browser.min.js.map +4 -4
  16. package/dist/cred.d.ts +1 -1
  17. package/dist/cred.js +1 -1
  18. package/dist/emulation/callbacks.d.ts +1 -1
  19. package/dist/emulation/callbacks.js +1 -1
  20. package/dist/emulation/constants.d.ts +48 -42
  21. package/dist/emulation/constants.js +68 -59
  22. package/dist/emulation/dir.d.ts +2 -2
  23. package/dist/emulation/promises.d.ts +1 -1
  24. package/dist/emulation/promises.js +6 -6
  25. package/dist/emulation/sync.d.ts +1 -1
  26. package/dist/emulation/sync.js +7 -7
  27. package/dist/file.d.ts +26 -12
  28. package/dist/file.js +68 -29
  29. package/dist/filesystem.d.ts +3 -3
  30. package/dist/index.d.ts +6 -3
  31. package/dist/index.js +4 -5
  32. package/dist/inode.d.ts +21 -15
  33. package/dist/inode.js +52 -40
  34. package/dist/mutex.d.ts +1 -2
  35. package/dist/mutex.js +1 -1
  36. package/dist/stats.d.ts +70 -18
  37. package/dist/stats.js +12 -18
  38. package/dist/utils.d.ts +3 -8
  39. package/dist/utils.js +60 -39
  40. package/package.json +5 -1
  41. package/readme.md +14 -10
  42. package/scripts/make-index.js +100 -0
  43. package/dist/backends/index.d.ts +0 -10
  44. package/dist/backends/index.js +0 -12
package/dist/stats.js CHANGED
@@ -10,7 +10,8 @@ export var FileType;
10
10
  FileType[FileType["SYMLINK"] = S_IFLNK] = "SYMLINK";
11
11
  })(FileType = FileType || (FileType = {}));
12
12
  /**
13
- * Common code used by both Stats and BigIntStats
13
+ * Provides information about a particular entry in the file system.
14
+ * Common code used by both Stats and BigIntStats.
14
15
  */
15
16
  export class StatsCommon {
16
17
  get _typename() {
@@ -47,19 +48,9 @@ export class StatsCommon {
47
48
  this.birthtimeMs = this._convert(value.getTime());
48
49
  }
49
50
  /**
50
- * Provides information about a particular entry in the file system.
51
- * @param itemType Type of the item (FILE, DIRECTORY, SYMLINK, or SOCKET)
52
- * @param size Size of the item in bytes. For directories/symlinks,
53
- * this is normally the size of the struct that represents the item.
54
- * @param mode Unix-style file mode (e.g. 0o644)
55
- * @param atimeMs time of last access, in milliseconds since epoch
56
- * @param mtimeMs time of last modification, in milliseconds since epoch
57
- * @param ctimeMs time of last time file status was changed, in milliseconds since epoch
58
- * @param uid the id of the user that owns the file
59
- * @param gid the id of the group that owns the file
60
- * @param birthtimeMs time of file creation, in milliseconds since epoch
51
+ * Creates a new stats instance from a stats-like object. Can be used to copy stats (note)
61
52
  */
62
- constructor(itemType = FileType.FILE, size = -1, mode, atimeMs, mtimeMs, ctimeMs, uid, gid, birthtimeMs) {
53
+ constructor({ atimeMs, mtimeMs, ctimeMs, birthtimeMs, uid, gid, size, mode } = {}) {
63
54
  /**
64
55
  * ID of device containing file
65
56
  */
@@ -93,7 +84,7 @@ export class StatsCommon {
93
84
  */
94
85
  this.fileData = null;
95
86
  const currentTime = Date.now();
96
- const resolveT = (v, def) => (typeof v == this._typename ? v : this._convert(typeof v == this._typename_inverse ? v : def));
87
+ const resolveT = (val, _default) => (typeof val == this._typename ? val : this._convert(typeof val == this._typename_inverse ? val : _default));
97
88
  this.atimeMs = resolveT(atimeMs, currentTime);
98
89
  this.mtimeMs = resolveT(mtimeMs, currentTime);
99
90
  this.ctimeMs = resolveT(ctimeMs, currentTime);
@@ -101,6 +92,7 @@ export class StatsCommon {
101
92
  this.uid = resolveT(uid, 0);
102
93
  this.gid = resolveT(gid, 0);
103
94
  this.size = this._convert(size);
95
+ const itemType = Number(mode) & S_IFMT || FileType.FILE;
104
96
  if (mode) {
105
97
  this.mode = this._convert(mode);
106
98
  }
@@ -229,9 +221,10 @@ export class Stats extends StatsCommon {
229
221
  }
230
222
  /**
231
223
  * Clones the stats object.
224
+ * @deprecated use `new Stats(stats)`
232
225
  */
233
- static clone(s) {
234
- return new Stats(s.mode & S_IFMT, s.size, s.mode & ~S_IFMT, s.atimeMs, s.mtimeMs, s.ctimeMs, s.uid, s.gid, s.birthtimeMs);
226
+ static clone(stats) {
227
+ return new Stats(stats);
235
228
  }
236
229
  }
237
230
  Stats;
@@ -246,9 +239,10 @@ export class BigIntStats extends StatsCommon {
246
239
  }
247
240
  /**
248
241
  * Clone a stats object.
242
+ * @deprecated use `new BigIntStats(stats)`
249
243
  */
250
- static clone(s) {
251
- return new BigIntStats(Number(s.mode) & S_IFMT, BigInt(s.size), BigInt(s.mode) & BigInt(~S_IFMT), BigInt(s.atimeMs), BigInt(s.mtimeMs), BigInt(s.ctimeMs), BigInt(s.uid), BigInt(s.gid), BigInt(s.birthtimeMs));
244
+ static clone(stats) {
245
+ return new BigIntStats(stats);
252
246
  }
253
247
  }
254
248
  BigIntStats;
package/dist/utils.d.ts CHANGED
@@ -7,23 +7,18 @@ declare global {
7
7
  }
8
8
  /**
9
9
  * Synchronous recursive makedir.
10
- * @internal
10
+ * @hidden
11
11
  */
12
12
  export declare function mkdirpSync(p: string, mode: number, cred: Cred, fs: FileSystem): void;
13
13
  /**
14
14
  * Calculates levenshtein distance.
15
- * @internal
15
+ * @hidden
16
16
  */
17
17
  export declare function levenshtein(a: string, b: string): number;
18
18
  /** Waits n ms. */
19
19
  export declare function wait(ms: number): Promise<void>;
20
20
  /**
21
- * Converts a callback into a promise. Assumes last parameter is the callback
22
- * @todo Look at changing resolve value from cbArgs[0] to include other callback arguments?
23
- */
24
- export declare function toPromise(fn: (...fnArgs: unknown[]) => unknown): (...args: unknown[]) => Promise<unknown>;
25
- /**
26
- * @internal
21
+ * @hidden
27
22
  */
28
23
  export declare const setImmediate: (callback: () => unknown) => void;
29
24
  /**
package/dist/utils.js CHANGED
@@ -1,25 +1,21 @@
1
1
  import { ApiError, ErrorCode } from './ApiError.js';
2
- import * as path from './emulation/path.js';
2
+ import { dirname } from './emulation/path.js';
3
3
  /**
4
4
  * Synchronous recursive makedir.
5
- * @internal
5
+ * @hidden
6
6
  */
7
7
  export function mkdirpSync(p, mode, cred, fs) {
8
8
  if (!fs.existsSync(p, cred)) {
9
- mkdirpSync(path.dirname(p), mode, cred, fs);
9
+ mkdirpSync(dirname(p), mode, cred, fs);
10
10
  fs.mkdirSync(p, mode, cred);
11
11
  }
12
12
  }
13
- /*
14
- * Levenshtein distance, from the `js-levenshtein` NPM module.
15
- * Copied here to avoid complexity of adding another CommonJS module dependency.
16
- */
17
13
  function _min(d0, d1, d2, bx, ay) {
18
14
  return Math.min(d0 + 1, d1 + 1, d2 + 1, bx === ay ? d1 : d1 + 1);
19
15
  }
20
16
  /**
21
17
  * Calculates levenshtein distance.
22
- * @internal
18
+ * @hidden
23
19
  */
24
20
  export function levenshtein(a, b) {
25
21
  if (a === b) {
@@ -94,26 +90,7 @@ export function wait(ms) {
94
90
  });
95
91
  }
96
92
  /**
97
- * Converts a callback into a promise. Assumes last parameter is the callback
98
- * @todo Look at changing resolve value from cbArgs[0] to include other callback arguments?
99
- */
100
- export function toPromise(fn) {
101
- return function (...args) {
102
- return new Promise((resolve, reject) => {
103
- args.push((e, ...cbArgs) => {
104
- if (e) {
105
- reject(e);
106
- }
107
- else {
108
- resolve(cbArgs[0]);
109
- }
110
- });
111
- fn(...args);
112
- });
113
- };
114
- }
115
- /**
116
- * @internal
93
+ * @hidden
117
94
  */
118
95
  export const setImmediate = typeof globalThis.setImmediate == 'function' ? globalThis.setImmediate : cb => setTimeout(cb, 0);
119
96
  /**
@@ -121,21 +98,45 @@ export const setImmediate = typeof globalThis.setImmediate == 'function' ? globa
121
98
  * @internal
122
99
  */
123
100
  export function encode(input, encoding = 'utf8') {
101
+ if (typeof input != 'string') {
102
+ throw new ApiError(ErrorCode.EINVAL, 'Can not encode a non-string');
103
+ }
124
104
  switch (encoding) {
125
105
  case 'ascii':
126
- return new globalThis.TextEncoder().encode(input).map(v => v & 0x7f);
127
106
  case 'latin1':
128
107
  case 'binary':
108
+ return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0)));
129
109
  case 'utf8':
130
110
  case 'utf-8':
111
+ return new Uint8Array(Array.from(input).flatMap(char => {
112
+ const code = char.charCodeAt(0);
113
+ if (code < 0x80) {
114
+ return code;
115
+ }
116
+ const a = (code & 0x3f) | 0x80;
117
+ if (code < 0x800) {
118
+ return [(code >> 6) | 0xc0, a];
119
+ }
120
+ const b = ((code >> 6) & 0x3f) | 0x80;
121
+ if (code < 0x10000) {
122
+ return [(code >> 12) | 0xe0, b, a];
123
+ }
124
+ return [(code >> 18) | 0xf0, ((code >> 12) & 0x3f) | 0x80, b, a];
125
+ }));
131
126
  case 'base64':
127
+ return encode(atob(input), 'utf-8');
132
128
  case 'base64url':
129
+ return encode(input.replace('_', '/').replace('-', '+'), 'base64');
133
130
  case 'hex':
134
- return new globalThis.TextEncoder().encode(input);
131
+ return new Uint8Array(input.match(/.{1,2}/g).map(e => parseInt(e, 16)));
135
132
  case 'utf16le':
136
133
  case 'ucs2':
137
134
  case 'ucs-2':
138
- return new globalThis.TextEncoder().encode(input).slice(0, -1);
135
+ const u16 = new Uint16Array(new ArrayBuffer(input.length * 2));
136
+ for (let i = 0; i < input.length; i++) {
137
+ u16[i] = input.charCodeAt(i);
138
+ }
139
+ return new Uint8Array(u16.buffer);
139
140
  default:
140
141
  throw new ApiError(ErrorCode.EINVAL, 'Invalid encoding: ' + encoding);
141
142
  }
@@ -145,14 +146,36 @@ export function encode(input, encoding = 'utf8') {
145
146
  * @internal
146
147
  */
147
148
  export function decode(input, encoding = 'utf8') {
149
+ if (!(input instanceof Uint8Array)) {
150
+ throw new ApiError(ErrorCode.EINVAL, 'Can not decode a non-Uint8Array');
151
+ }
148
152
  switch (encoding) {
149
153
  case 'ascii':
150
- case 'utf8':
151
- case 'utf-8':
152
- return new globalThis.TextDecoder().decode(input);
153
154
  case 'latin1':
154
155
  case 'binary':
155
- return new globalThis.TextDecoder('latin1').decode(input);
156
+ return Array.from(input)
157
+ .map(char => String.fromCharCode(char))
158
+ .join('');
159
+ case 'utf8':
160
+ case 'utf-8':
161
+ let utf8String = '';
162
+ for (let i = 0; i < input.length; i++) {
163
+ let code;
164
+ if (input[i] < 0x80) {
165
+ code = input[i];
166
+ }
167
+ else if (input[i] < 0xe0) {
168
+ code = ((input[i] & 0x1f) << 6) | (input[++i] & 0x3f);
169
+ }
170
+ else if (input[i] < 0xf0) {
171
+ code = ((input[i] & 0x0f) << 12) | ((input[++i] & 0x3f) << 6) | (input[++i] & 0x3f);
172
+ }
173
+ else {
174
+ code = ((input[i] & 0x07) << 18) | ((input[++i] & 0x3f) << 12) | ((input[++i] & 0x3f) << 6) | (input[++i] & 0x3f);
175
+ }
176
+ utf8String += String.fromCharCode(code);
177
+ }
178
+ return utf8String;
156
179
  case 'utf16le':
157
180
  case 'ucs2':
158
181
  case 'ucs-2':
@@ -163,14 +186,12 @@ export function decode(input, encoding = 'utf8') {
163
186
  }
164
187
  return utf16leString;
165
188
  case 'base64':
166
- return btoa(Array.from(input)
167
- .map(v => String.fromCharCode(v))
168
- .join(''));
189
+ return btoa(decode(input, 'utf-8'));
169
190
  case 'base64url':
170
191
  return decode(input, 'base64').replace('/', '_').replace('+', '-');
171
192
  case 'hex':
172
193
  return Array.from(input)
173
- .map(e => e.toString(16))
194
+ .map(e => e.toString(16).padStart(2, '0'))
174
195
  .join('');
175
196
  default:
176
197
  throw new ApiError(ErrorCode.EINVAL, 'Invalid encoding: ' + encoding);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "A filesystem in your browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist",
@@ -9,6 +9,9 @@
9
9
  "node",
10
10
  "storage"
11
11
  ],
12
+ "bin": {
13
+ "make-index": "./scripts/make-index.js"
14
+ },
12
15
  "type": "module",
13
16
  "homepage": "https://github.com/zen-fs/core",
14
17
  "author": "James P. <jp@drvortex.dev> (https://drvortex.dev)",
@@ -46,6 +49,7 @@
46
49
  "dependencies": {
47
50
  "@types/node": "^14.0.0",
48
51
  "@types/readable-stream": "^4.0.10",
52
+ "minimatch": "^9.0.3",
49
53
  "readable-stream": "^4.5.2"
50
54
  },
51
55
  "devDependencies": {
package/readme.md CHANGED
@@ -15,7 +15,7 @@ ZenFS is highly extensible, and includes a few built-in backends:
15
15
 
16
16
  More backends can be defined by separate libraries, as long as they implement `FileSystem`.
17
17
 
18
- ZenFS supports a number of other backends (many are provided as seperate packages under `@zenfs`).
18
+ ZenFS supports a number of other backends. Many are provided as seperate packages under `@zenfs`.
19
19
 
20
20
  For more information, see the [API documentation for ZenFS](https://zen-fs.github.io/core).
21
21
 
@@ -39,7 +39,7 @@ const contents = fs.readFileSync('/test.txt', 'utf-8');
39
39
  console.log(contents);
40
40
  ```
41
41
 
42
- #### Using different and/or different backends
42
+ #### Using different and/or multiple backends
43
43
 
44
44
  A single `InMemory` backend is created by default, mounted on `/`.
45
45
 
@@ -50,7 +50,7 @@ You can use multiple backends by passing an object to `configure` which maps pat
50
50
  The following example mounts a zip file to `/zip`, in-memory storage to `/tmp`, and IndexedDB to `/home`. Note that `/` has the default in-memory backend.
51
51
 
52
52
  ```js
53
- import { configure } from '@zenfs/core';
53
+ import { configure, InMemory } from '@zenfs/core';
54
54
  import { IndexedDB } from '@zenfs/dom';
55
55
  import { Zip } from '@zenfs/zip';
56
56
 
@@ -58,13 +58,17 @@ const zipData = await (await fetch('mydata.zip')).arrayBuffer();
58
58
 
59
59
  await configure({
60
60
  '/mnt/zip': { backend: Zip, zipData },
61
- '/tmp': 'InMemory',
61
+ '/tmp': InMemory,
62
62
  '/home': IndexedDB,
63
63
  };
64
64
  ```
65
65
 
66
66
  > [!TIP]
67
- > When configuring a mount point, you can pass in 1. A string that maps to a built-in backend 2. A `Backend` object, if the backend has no required options 3. An object that has a `backend` property which is a `Backend` or a string that maps to a built-in backend and the options accepted by the backend
67
+ > When configuring a mount point, you can pass in
68
+ >
69
+ > 1. A string that maps to a built-in backend
70
+ > 2. A `Backend` object, if the backend has no required options
71
+ > 3. An object that has the options accepted by the backend and a `backend` property which is (1) or (2)
68
72
 
69
73
  Here is an example that mounts the `Storage` backend from `@zenfs/dom` on `/`:
70
74
 
@@ -109,13 +113,13 @@ if (!exists) {
109
113
  You may have noticed that attempting to use a synchronous function on an asynchronous backend (e.g. `IndexedDB`) results in a "not supplied" error (`ENOTSUP`). If you would like to use an asynchronous backend synchronously you need to wrap it in an `AsyncMirror`:
110
114
 
111
115
  ```js
112
- import { configure, fs } from '@zenfs/core';
116
+ import { configure, fs, AsyncMirror, InMemory } from '@zenfs/core';
113
117
  import { IndexedDB } from '@zenfs/dom';
114
118
 
115
119
  await configure({
116
120
  '/': {
117
- backend: 'AsyncMirror',
118
- sync: 'InMemory',
121
+ backend: AsyncMirror,
122
+ sync: InMemory,
119
123
  async: IndexedDB,
120
124
  },
121
125
  });
@@ -130,12 +134,12 @@ If you would like to create backends without configure (e.g. to do something dyn
130
134
  You can then mount and unmount the backend instance by using `mount` and `umount`.
131
135
 
132
136
  ```js
133
- import { configure, createBackend } from '@zenfs/core';
137
+ import { configure, createBackend, InMemory } from '@zenfs/core';
134
138
  import { IndexedDB } from '@zenfs/dom';
135
139
  import { Zip } from '@zenfs/zip';
136
140
 
137
141
  await configure({
138
- '/tmp': 'InMemory',
142
+ '/tmp': InMemory,
139
143
  '/home': IndexedDB,
140
144
  };
141
145
 
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'util';
3
+ import { statSync, readdirSync, writeFileSync } from 'fs';
4
+ import { join } from 'path/posix';
5
+ import { resolve } from 'path';
6
+ import { minimatch } from 'minimatch';
7
+
8
+ const {
9
+ values: options,
10
+ positionals: [root],
11
+ } = parseArgs({
12
+ options: {
13
+ help: { short: 'h', type: 'boolean', default: false },
14
+ ignore: { short: 'i', type: 'string', multiple: true, default: [] },
15
+ output: { short: 'o', type: 'string', default: 'index.json' },
16
+ quiet: { short: 'q', type: 'boolean', default: false },
17
+ verbose: { type: 'boolean', default: false },
18
+ },
19
+ allowPositionals: true,
20
+ strict: true,
21
+ });
22
+
23
+ if (options.help) {
24
+ console.log(`make-index <path> [...options]
25
+ path: The path to create a listing for
26
+
27
+ options:
28
+ --help, -h Outputs this help message
29
+ --quiet, -q Do not output messages about individual files
30
+ --verbose Output verbose messages
31
+
32
+ --output, -o <path> Path to the output file. Defaults to listing.
33
+ --ignore, -i <pattern> Ignores files which match the glob <pattern>. Can be passed multiple times.
34
+ `);
35
+ process.exit();
36
+ }
37
+
38
+ if (options.quiet && options.verbose) {
39
+ console.log('Can not use both --verbose and --quiet.');
40
+ process.exit();
41
+ }
42
+
43
+ function pathToPosix(path) {
44
+ return path.replaceAll('\\', '/');
45
+ }
46
+
47
+ const colors = {
48
+ reset: 0,
49
+ black: 30,
50
+ red: 31,
51
+ green: 32,
52
+ yellow: 33,
53
+ blue: 34,
54
+ magenta: 35,
55
+ cyan: 36,
56
+ white: 37,
57
+ bright_black: 90,
58
+ bright_red: 91,
59
+ bright_green: 92,
60
+ bright_yellow: 93,
61
+ bright_blue: 94,
62
+ bright_magenta: 95,
63
+ bright_cyan: 96,
64
+ bright_white: 97,
65
+ };
66
+
67
+ function color(color, text) {
68
+ return `\x1b[${colors[color]}m${text}\x1b[0m`;
69
+ }
70
+
71
+ function makeListing(path, seen = new Set()) {
72
+ try {
73
+ const stats = statSync(path);
74
+
75
+ if (stats.isFile()) {
76
+ return null;
77
+ }
78
+
79
+ let entries = {};
80
+ for (const file of readdirSync(path)) {
81
+ const full = join(path, file);
82
+ if (options.ignore.some(pattern => minimatch(full, pattern))) {
83
+ if (!options.quiet) console.log(`${color('yellow', 'skip')} ${full}`);
84
+ continue;
85
+ }
86
+
87
+ entries[file] = makeListing(full, seen);
88
+ }
89
+ return entries;
90
+ } catch (e) {
91
+ if (!options.quiet) {
92
+ console.log(`${color('red', 'fail')} ${path}: ${e.message}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ const listing = makeListing(pathToPosix(root));
98
+ if (!options.quiet) console.log('Generated listing for ' + pathToPosix(resolve(root)));
99
+
100
+ writeFileSync(options.output, JSON.stringify(listing));
@@ -1,10 +0,0 @@
1
- import { AsyncMirror } from './AsyncMirror.js';
2
- import { InMemory } from './InMemory.js';
3
- import { Overlay } from './Overlay.js';
4
- import { Backend } from './backend.js';
5
- export declare const backends: {
6
- [backend: string]: Backend;
7
- };
8
- export default backends;
9
- export { AsyncMirror, InMemory, Overlay };
10
- export declare function registerBackend(..._backends: Backend[]): void;
@@ -1,12 +0,0 @@
1
- import { AsyncMirror } from './AsyncMirror.js';
2
- import { InMemory } from './InMemory.js';
3
- import { Overlay } from './Overlay.js';
4
- export const backends = {};
5
- export default backends;
6
- export { AsyncMirror, InMemory, Overlay };
7
- export function registerBackend(..._backends) {
8
- for (const backend of _backends) {
9
- backends[backend.name] = backend;
10
- }
11
- }
12
- registerBackend(AsyncMirror, InMemory, Overlay);