UNPKG

@zenfs/core

Version:

A filesystem, anywhere

207 lines (173 loc) 5.11 kB
/* Note: this file is named file_index.ts because Typescript has special behavior regarding index.ts which can't be disabled. */ import { isJSON } from 'utilium'; import { basename, dirname } from '../emulation/path.js'; import { Errno, ErrnoError } from '../error.js'; import { NoSyncFile, isWriteable } from '../file.js'; import { FileSystem } from '../filesystem.js'; import { Readonly } from '../mixins/readonly.js'; import type { StatsLike } from '../stats.js'; import { Stats } from '../stats.js'; import { decodeUTF8, encodeUTF8 } from '../utils.js'; /** * An Index in JSON form * @internal */ export interface IndexData { version: 1; entries: Record<string, StatsLike<number>>; } export const version = 1; /** * An index of files * @internal */ export class Index extends Map<string, Stats> { /** * Convenience method */ public files(): Map<string, Stats> { const files = new Map<string, Stats>(); for (const [path, stats] of this) { if (stats.isFile()) { files.set(path, stats); } } return files; } /** * Converts the index to JSON */ public toJSON(): IndexData { return { version, entries: Object.fromEntries(this), }; } /** * Converts the index to a string */ public toString(): string { return JSON.stringify(this.toJSON()); } /** * Returns the files in the directory `dir`. * This is expensive so it is only called once per directory. */ protected dirEntries(dir: string): string[] { const entries = []; for (const entry of this.keys()) { if (dirname(entry) == dir) { entries.push(basename(entry)); } } return entries; } /** * Loads the index from JSON data */ public fromJSON(json: IndexData): void { if (json.version != version) { throw new ErrnoError(Errno.EINVAL, 'Index version mismatch'); } this.clear(); for (const [path, data] of Object.entries(json.entries)) { const stats = new Stats(data); if (stats.isDirectory()) { stats.fileData = encodeUTF8(JSON.stringify(this.dirEntries(path))); } this.set(path, stats); } } /** * Parses an index from a string */ public static parse(data: string): Index { if (!isJSON(data)) { throw new ErrnoError(Errno.EINVAL, 'Invalid JSON'); } const json = JSON.parse(data) as IndexData; const index = new Index(); index.fromJSON(json); return index; } } export abstract class IndexFS extends Readonly(FileSystem) { protected index: Index = new Index(); protected _isInitialized: boolean = false; public async ready(): Promise<void> { await super.ready(); if (this._isInitialized) { return; } this.index.fromJSON(await this.indexData); this._isInitialized = true; } public constructor(private indexData: IndexData | Promise<IndexData>) { super(); } public async reloadFiles(): Promise<void> { for (const [path, stats] of this.index.files()) { delete stats.fileData; stats.fileData = await this.getData(path, stats); } } public reloadFilesSync(): void { for (const [path, stats] of this.index.files()) { delete stats.fileData; stats.fileData = this.getDataSync(path, stats); } } public stat(path: string): Promise<Stats> { return Promise.resolve(this.statSync(path)); } public statSync(path: string): Stats { if (!this.index.has(path)) { throw ErrnoError.With('ENOENT', path, 'stat'); } return this.index.get(path)!; } public async openFile(path: string, flag: string): Promise<NoSyncFile<this>> { if (isWriteable(flag)) { // You can't write to files on this file system. throw new ErrnoError(Errno.EPERM, path); } // Check if the path exists, and is a file. const stats = this.index.get(path); if (!stats) { throw ErrnoError.With('ENOENT', path, 'openFile'); } return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : await this.getData(path, stats)); } public openFileSync(path: string, flag: string): NoSyncFile<this> { if (isWriteable(flag)) { // You can't write to files on this file system. throw new ErrnoError(Errno.EPERM, path); } // Check if the path exists, and is a file. const stats = this.index.get(path); if (!stats) { throw ErrnoError.With('ENOENT', path, 'openFile'); } return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : this.getDataSync(path, stats)); } public readdir(path: string): Promise<string[]> { return Promise.resolve(this.readdirSync(path)); } public readdirSync(path: string): string[] { // Check if it exists. const stats = this.index.get(path); if (!stats) { throw ErrnoError.With('ENOENT', path, 'readdir'); } const content: unknown = JSON.parse(decodeUTF8(stats.fileData)); if (!Array.isArray(content)) { throw ErrnoError.With('ENODATA', path, 'readdir'); } if (!content.every(item => typeof item == 'string')) { throw ErrnoError.With('ENODATA', path, 'readdir'); } return content; } protected abstract getData(path: string, stats: Stats): Promise<Uint8Array>; protected abstract getDataSync(path: string, stats: Stats): Uint8Array; }