@zenfs/core 0.11.2 → 0.12.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.
@@ -1,7 +1,7 @@
1
- import { NoSyncFile } from '../file.js';
2
1
  import type { FileSystemMetadata } from '../filesystem.js';
3
2
  import { Stats } from '../stats.js';
4
- import { type ListingTree, type IndexFileInode, AsyncIndexFS } from './Index.js';
3
+ import { IndexFS } from './index/fs.js';
4
+ import type { IndexData } from './index/index.js';
5
5
  /**
6
6
  * Configuration options for FetchFS.
7
7
  */
@@ -10,60 +10,47 @@ export interface FetchOptions {
10
10
  * URL to a file index as a JSON file or the file index object itself.
11
11
  * Defaults to `index.json`.
12
12
  */
13
- index?: string | ListingTree;
13
+ index?: string | IndexData;
14
14
  /** Used as the URL prefix for fetched files.
15
15
  * Default: Fetch files relative to the index.
16
16
  */
17
17
  baseUrl?: string;
18
18
  }
19
19
  /**
20
- * A simple filesystem backed by HTTP using the fetch API.
20
+ * A simple filesystem backed by HTTP using the `fetch` API.
21
21
  *
22
22
  *
23
- * Listings objects look like the following:
23
+ * Index objects look like the following:
24
24
  *
25
25
  * ```json
26
26
  * {
27
- * "home": {
28
- * "jvilk": {
29
- * "someFile.txt": null,
30
- * "someDir": {
31
- * // Empty directory
32
- * }
33
- * }
34
- * }
27
+ * "version": 1,
28
+ * "entries": {
29
+ * "/home": { ... },
30
+ * "/home/jvilk": { ... },
31
+ * "/home/james": { ... }
32
+ * }
35
33
  * }
36
34
  * ```
37
35
  *
38
- * This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
36
+ * Each entry contains the stats associated with the file.
39
37
  */
40
- export declare class FetchFS extends AsyncIndexFS<Stats> {
41
- readonly prefixUrl: string;
42
- protected _init: Promise<void>;
43
- protected _initialize(index: string | ListingTree): Promise<void>;
38
+ export declare class FetchFS extends IndexFS {
39
+ readonly baseUrl: string;
44
40
  ready(): Promise<void>;
45
41
  constructor({ index, baseUrl }: FetchOptions);
46
42
  metadata(): FileSystemMetadata;
47
- empty(): void;
48
43
  /**
49
- * Special function: Preload the given file into the index.
44
+ * Preload the given file into the index.
50
45
  * @param path
51
46
  * @param buffer
52
47
  */
53
- preloadFile(path: string, buffer: Uint8Array): void;
54
- protected statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats>;
55
- protected openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>>;
56
- private _getRemotePath;
48
+ preload(path: string, buffer: Uint8Array): void;
57
49
  /**
58
- * Asynchronously download the given file.
50
+ * @todo Be lazier about actually requesting the data?
59
51
  */
60
- protected _fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
61
- protected _fetchFile(path: string, type: 'json'): Promise<object>;
62
- protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object>;
63
- /**
64
- * Only requests the HEAD content, for the file size.
65
- */
66
- protected _fetchSize(path: string): Promise<number>;
52
+ protected getData(path: string, stats: Stats): Promise<Uint8Array>;
53
+ protected getDataSync(path: string, stats: Stats): Uint8Array;
67
54
  }
68
55
  export declare const Fetch: {
69
56
  readonly name: "Fetch";
@@ -1,7 +1,5 @@
1
- import { ErrnoError, Errno } from '../error.js';
2
- import { NoSyncFile } from '../file.js';
3
- import { Stats } from '../stats.js';
4
- import { FileIndex, AsyncIndexFS } from './Index.js';
1
+ import { Errno, ErrnoError } from '../error.js';
2
+ import { IndexFS } from './index/fs.js';
5
3
  async function fetchFile(path, type) {
6
4
  const response = await fetch(path).catch(e => {
7
5
  throw new ErrnoError(Errno.EIO, e.message);
@@ -24,64 +22,44 @@ async function fetchFile(path, type) {
24
22
  }
25
23
  }
26
24
  /**
27
- * Asynchronously retrieves the size of the given file in bytes.
28
- * @hidden
29
- */
30
- async function fetchSize(path) {
31
- const response = await fetch(path, { method: 'HEAD' }).catch(e => {
32
- throw new ErrnoError(Errno.EIO, e.message);
33
- });
34
- if (!response.ok) {
35
- throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status);
36
- }
37
- return parseInt(response.headers.get('Content-Length') || '-1', 10);
38
- }
39
- /**
40
- * A simple filesystem backed by HTTP using the fetch API.
25
+ * A simple filesystem backed by HTTP using the `fetch` API.
41
26
  *
42
27
  *
43
- * Listings objects look like the following:
28
+ * Index objects look like the following:
44
29
  *
45
30
  * ```json
46
31
  * {
47
- * "home": {
48
- * "jvilk": {
49
- * "someFile.txt": null,
50
- * "someDir": {
51
- * // Empty directory
52
- * }
53
- * }
54
- * }
32
+ * "version": 1,
33
+ * "entries": {
34
+ * "/home": { ... },
35
+ * "/home/jvilk": { ... },
36
+ * "/home/james": { ... }
37
+ * }
55
38
  * }
56
39
  * ```
57
40
  *
58
- * This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
41
+ * Each entry contains the stats associated with the file.
59
42
  */
60
- export class FetchFS extends AsyncIndexFS {
61
- async _initialize(index) {
62
- if (typeof index != 'string') {
63
- this._index = FileIndex.FromListing(index);
43
+ export class FetchFS extends IndexFS {
44
+ async ready() {
45
+ if (this._isInitialized) {
64
46
  return;
65
47
  }
66
- try {
67
- const response = await fetch(index);
68
- this._index = FileIndex.FromListing((await response.json()));
69
- }
70
- catch (e) {
71
- throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree');
48
+ await super.ready();
49
+ /**
50
+ * Iterate over all of the files and cache their contents
51
+ */
52
+ for (const [path, stats] of this.index.files()) {
53
+ await this.getData(path, stats);
72
54
  }
73
55
  }
74
- async ready() {
75
- await this._init;
76
- }
77
56
  constructor({ index = 'index.json', baseUrl = '' }) {
78
- super({});
57
+ super(typeof index != 'string' ? index : fetchFile(index, 'json'));
79
58
  // prefix url must end in a directory separator.
80
59
  if (baseUrl.at(-1) != '/') {
81
60
  baseUrl += '/';
82
61
  }
83
- this.prefixUrl = baseUrl;
84
- this._init = this._initialize(index);
62
+ this.baseUrl = baseUrl;
85
63
  }
86
64
  metadata() {
87
65
  return {
@@ -90,63 +68,38 @@ export class FetchFS extends AsyncIndexFS {
90
68
  readonly: true,
91
69
  };
92
70
  }
93
- empty() {
94
- for (const file of this._index.files()) {
95
- delete file.data.fileData;
96
- }
97
- }
98
71
  /**
99
- * Special function: Preload the given file into the index.
72
+ * Preload the given file into the index.
100
73
  * @param path
101
74
  * @param buffer
102
75
  */
103
- preloadFile(path, buffer) {
104
- const inode = this._index.get(path);
105
- if (!inode) {
76
+ preload(path, buffer) {
77
+ const stats = this.index.get(path);
78
+ if (!stats) {
106
79
  throw ErrnoError.With('ENOENT', path, 'preloadFile');
107
80
  }
108
- if (!inode.isFile()) {
81
+ if (!stats.isFile()) {
109
82
  throw ErrnoError.With('EISDIR', path, 'preloadFile');
110
83
  }
111
- const stats = inode.data;
112
84
  stats.size = buffer.length;
113
85
  stats.fileData = buffer;
114
86
  }
115
- async statFileInode(inode, path) {
116
- const stats = inode.data;
117
- // At this point, a non-opened file will still have default stats from the listing.
118
- if (stats.size < 0) {
119
- stats.size = await this._fetchSize(path);
120
- }
121
- return stats;
122
- }
123
- async openFileInode(inode, path, flag) {
124
- const stats = inode.data;
125
- // Use existing file contents. This maintains the previously-used flag.
87
+ /**
88
+ * @todo Be lazier about actually requesting the data?
89
+ */
90
+ async getData(path, stats) {
126
91
  if (stats.fileData) {
127
- return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData);
92
+ return stats.fileData;
128
93
  }
129
- // @todo be lazier about actually requesting the file
130
- const data = await this._fetchFile(path, 'buffer');
131
- // we don't initially have file sizes
132
- stats.size = data.length;
94
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
133
95
  stats.fileData = data;
134
- return new NoSyncFile(this, path, flag, new Stats(stats), data);
96
+ return data;
135
97
  }
136
- _getRemotePath(filePath) {
137
- if (filePath.charAt(0) === '/') {
138
- filePath = filePath.slice(1);
98
+ getDataSync(path, stats) {
99
+ if (stats.fileData) {
100
+ return stats.fileData;
139
101
  }
140
- return this.prefixUrl + filePath;
141
- }
142
- _fetchFile(path, type) {
143
- return fetchFile(this._getRemotePath(path), type);
144
- }
145
- /**
146
- * Only requests the HEAD content, for the file size.
147
- */
148
- _fetchSize(path) {
149
- return fetchSize(this._getRemotePath(path));
102
+ throw new ErrnoError(Errno.ENODATA, '', path, 'getData');
150
103
  }
151
104
  }
152
105
  export const Fetch = {
@@ -0,0 +1,49 @@
1
+ import type { Cred } from '../../cred.js';
2
+ import { NoSyncFile } from '../../file.js';
3
+ import { FileSystem } from '../../filesystem.js';
4
+ import type { Stats } from '../../stats.js';
5
+ import { Index, IndexData } from './index.js';
6
+ declare const IndexFS_base: (abstract new (...args: any[]) => {
7
+ metadata(): import("../../filesystem.js").FileSystemMetadata;
8
+ rename(oldPath: string, newPath: string, cred: Cred): Promise<void>;
9
+ renameSync(oldPath: string, newPath: string, cred: Cred): void;
10
+ createFile(path: string, flag: string, mode: number, cred: Cred): Promise<import("../../file.js").File>;
11
+ createFileSync(path: string, flag: string, mode: number, cred: Cred): import("../../file.js").File;
12
+ unlink(path: string, cred: Cred): Promise<void>;
13
+ unlinkSync(path: string, cred: Cred): void;
14
+ rmdir(path: string, cred: Cred): Promise<void>;
15
+ rmdirSync(path: string, cred: Cred): void;
16
+ mkdir(path: string, mode: number, cred: Cred): Promise<void>;
17
+ mkdirSync(path: string, mode: number, cred: Cred): void;
18
+ link(srcpath: string, dstpath: string, cred: Cred): Promise<void>;
19
+ linkSync(srcpath: string, dstpath: string, cred: Cred): void;
20
+ sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void>;
21
+ syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
22
+ ready(): Promise<void>;
23
+ stat(path: string, cred: Cred): Promise<Stats>;
24
+ statSync(path: string, cred: Cred): Stats;
25
+ openFile(path: string, flag: string, cred: Cred): Promise<import("../../file.js").File>;
26
+ openFileSync(path: string, flag: string, cred: Cred): import("../../file.js").File;
27
+ readdir(path: string, cred: Cred): Promise<string[]>;
28
+ readdirSync(path: string, cred: Cred): string[];
29
+ exists(path: string, cred: Cred): Promise<boolean>;
30
+ existsSync(path: string, cred: Cred): boolean;
31
+ }) & typeof FileSystem;
32
+ export declare abstract class IndexFS extends IndexFS_base {
33
+ private indexData;
34
+ protected index: Index;
35
+ protected _isInitialized: boolean;
36
+ ready(): Promise<void>;
37
+ constructor(indexData: IndexData | Promise<IndexData>);
38
+ reloadFiles(): Promise<void>;
39
+ reloadFilesSync(): void;
40
+ stat(path: string): Promise<Stats>;
41
+ statSync(path: string): Stats;
42
+ openFile(path: string, flag: string, cred: Cred): Promise<NoSyncFile<this>>;
43
+ openFileSync(path: string, flag: string, cred: Cred): NoSyncFile<this>;
44
+ readdir(path: string): Promise<string[]>;
45
+ readdirSync(path: string): string[];
46
+ protected abstract getData(path: string, stats: Stats): Promise<Uint8Array>;
47
+ protected abstract getDataSync(path: string, stats: Stats): Uint8Array;
48
+ }
49
+ export {};
@@ -0,0 +1,86 @@
1
+ import { ErrnoError, Errno } from '../../error.js';
2
+ import { NoSyncFile, isWriteable, flagToMode } from '../../file.js';
3
+ import { Readonly, FileSystem } from '../../filesystem.js';
4
+ import { decode } from '../../utils.js';
5
+ import { Index } from './index.js';
6
+ export class IndexFS extends Readonly(FileSystem) {
7
+ async ready() {
8
+ await super.ready();
9
+ if (this._isInitialized) {
10
+ return;
11
+ }
12
+ this.index.fromJSON(await this.indexData);
13
+ this._isInitialized = true;
14
+ }
15
+ constructor(indexData) {
16
+ super();
17
+ this.indexData = indexData;
18
+ this.index = new Index();
19
+ this._isInitialized = false;
20
+ }
21
+ async reloadFiles() {
22
+ for (const [path, stats] of this.index.files()) {
23
+ delete stats.fileData;
24
+ stats.fileData = await this.getData(path, stats);
25
+ }
26
+ }
27
+ reloadFilesSync() {
28
+ for (const [path, stats] of this.index.files()) {
29
+ delete stats.fileData;
30
+ stats.fileData = this.getDataSync(path, stats);
31
+ }
32
+ }
33
+ async stat(path) {
34
+ return this.statSync(path);
35
+ }
36
+ statSync(path) {
37
+ if (!this.index.has(path)) {
38
+ throw ErrnoError.With('ENOENT', path, 'stat');
39
+ }
40
+ return this.index.get(path);
41
+ }
42
+ async openFile(path, flag, cred) {
43
+ if (isWriteable(flag)) {
44
+ // You can't write to files on this file system.
45
+ throw new ErrnoError(Errno.EPERM, path);
46
+ }
47
+ // Check if the path exists, and is a file.
48
+ const stats = this.index.get(path);
49
+ if (!stats) {
50
+ throw ErrnoError.With('ENOENT', path, 'openFile');
51
+ }
52
+ if (!stats.hasAccess(flagToMode(flag), cred)) {
53
+ throw ErrnoError.With('EACCES', path, 'openFile');
54
+ }
55
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : await this.getData(path, stats));
56
+ }
57
+ openFileSync(path, flag, cred) {
58
+ if (isWriteable(flag)) {
59
+ // You can't write to files on this file system.
60
+ throw new ErrnoError(Errno.EPERM, path);
61
+ }
62
+ // Check if the path exists, and is a file.
63
+ const stats = this.index.get(path);
64
+ if (!stats) {
65
+ throw ErrnoError.With('ENOENT', path, 'openFile');
66
+ }
67
+ if (!stats.hasAccess(flagToMode(flag), cred)) {
68
+ throw ErrnoError.With('EACCES', path, 'openFile');
69
+ }
70
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : this.getDataSync(path, stats));
71
+ }
72
+ async readdir(path) {
73
+ return this.readdirSync(path);
74
+ }
75
+ readdirSync(path) {
76
+ // Check if it exists.
77
+ const stats = this.index.get(path);
78
+ if (!stats) {
79
+ throw ErrnoError.With('ENOENT', path, 'readdir');
80
+ }
81
+ if (!stats.isDirectory()) {
82
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
83
+ }
84
+ return JSON.parse(decode(stats.fileData));
85
+ }
86
+ }
@@ -0,0 +1,37 @@
1
+ import { Stats, StatsLike } from '../../stats.js';
2
+ export interface IndexData {
3
+ version: 1;
4
+ entries: Record<string, StatsLike<number>>;
5
+ }
6
+ export declare const version = 1;
7
+ /**
8
+ * An index of files
9
+ */
10
+ export declare class Index extends Map<string, Stats> {
11
+ constructor();
12
+ /**
13
+ * Convience method
14
+ */
15
+ files(): Map<string, Stats>;
16
+ /**
17
+ * Converts the index to JSON
18
+ */
19
+ toJSON(): IndexData;
20
+ /**
21
+ * Converts the index to a string
22
+ */
23
+ toString(): string;
24
+ /**
25
+ * Returns the files in the directory `dir`.
26
+ * This is expensive so it is only called once per directory.
27
+ */
28
+ protected dirEntries(dir: string): string[];
29
+ /**
30
+ * Loads the index from JSON data
31
+ */
32
+ fromJSON(json: IndexData): void;
33
+ /**
34
+ * Parses an index from a string
35
+ */
36
+ static parse(data: string): Index;
37
+ }
@@ -0,0 +1,82 @@
1
+ import { isJSON } from 'utilium';
2
+ import { Errno, ErrnoError } from '../../error.js';
3
+ import { Stats } from '../../stats.js';
4
+ import { encode } from '../../utils.js';
5
+ import { basename, dirname } from '../../emulation/path.js';
6
+ export const version = 1;
7
+ /**
8
+ * An index of files
9
+ */
10
+ export class Index extends Map {
11
+ constructor() {
12
+ super();
13
+ }
14
+ /**
15
+ * Convience method
16
+ */
17
+ files() {
18
+ const files = new Map();
19
+ for (const [path, stats] of this) {
20
+ if (stats.isFile()) {
21
+ files.set(path, stats);
22
+ }
23
+ }
24
+ return files;
25
+ }
26
+ /**
27
+ * Converts the index to JSON
28
+ */
29
+ toJSON() {
30
+ return {
31
+ version,
32
+ entries: Object.fromEntries(this),
33
+ };
34
+ }
35
+ /**
36
+ * Converts the index to a string
37
+ */
38
+ toString() {
39
+ return JSON.stringify(this.toJSON());
40
+ }
41
+ /**
42
+ * Returns the files in the directory `dir`.
43
+ * This is expensive so it is only called once per directory.
44
+ */
45
+ dirEntries(dir) {
46
+ const entries = [];
47
+ for (const entry of this.keys()) {
48
+ if (dirname(entry) == dir) {
49
+ entries.push(basename(entry));
50
+ }
51
+ }
52
+ return entries;
53
+ }
54
+ /**
55
+ * Loads the index from JSON data
56
+ */
57
+ fromJSON(json) {
58
+ if (json.version != version) {
59
+ throw new ErrnoError(Errno.EINVAL, 'Index version mismatch');
60
+ }
61
+ this.clear();
62
+ for (const [path, data] of Object.entries(json.entries)) {
63
+ const stats = new Stats(data);
64
+ if (stats.isDirectory()) {
65
+ stats.fileData = encode(JSON.stringify(this.dirEntries(path)));
66
+ }
67
+ this.set(path, stats);
68
+ }
69
+ }
70
+ /**
71
+ * Parses an index from a string
72
+ */
73
+ static parse(data) {
74
+ if (!isJSON(data)) {
75
+ throw new ErrnoError(Errno.EINVAL, 'Invalid JSON');
76
+ }
77
+ const json = JSON.parse(data);
78
+ const index = new Index();
79
+ index.fromJSON(json);
80
+ return index;
81
+ }
82
+ }