Skip to content

Commit

Permalink
Formatting and fixes to testing polyfill
Browse files Browse the repository at this point in the history
  • Loading branch information
james-pre committed Jan 17, 2025
1 parent da7adb8 commit be90e55
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 138 deletions.
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Please read the ZenFS core documentation!

## Backends

- `WebStorage` stores files in a `Storage` object, like `localStorage` and `sessionStorage`.
- `IndexedDB` stores files into an `IndexedDB` object database.
- `WebAccess` uses the [File System Access API](https://developer.mozilla.org/Web/API/File_System_API).
- `XML` uses an `XMLDocument` to store files, which can be appended to the DOM.
- `WebStorage` stores files in a `Storage` object, like `localStorage` and `sessionStorage`.
- `IndexedDB` stores files into an `IndexedDB` object database.
- `WebAccess` uses the [File System Access API](https://developer.mozilla.org/Web/API/File_System_API).
- `XML` uses an `XMLDocument` to store files, which can be appended to the DOM.

For more information, see the [API documentation](https://zen-fs.github.io/dom).

Expand Down
249 changes: 128 additions & 121 deletions src/access.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Backend, CreationOptions, FileSystemMetadata, InodeLike } from '@zenfs/core';
import { Async, constants, Errno, ErrnoError, FileSystem, Index, InMemory, PreloadFile, Stats } from '@zenfs/core';
import type { Backend, CreationOptions, File, FileSystemMetadata, InodeLike, Stats } from '@zenfs/core';
import { _throw, Async, constants, Errno, ErrnoError, FileSystem, Index, InMemory, Inode, LazyFile } from '@zenfs/core';
import { S_IFDIR, S_IFREG } from '@zenfs/core/vfs/constants.js';
import { basename, dirname, join } from '@zenfs/core/vfs/path.js';
import { convertException, type ConvertException } from './utils.js';
import { convertException } from './utils.js';

export interface WebAccessOptions {
handle: FileSystemDirectoryHandle;
metadata?: string;
}

function isResizable(buffer: ArrayBufferLike): boolean {
Expand Down Expand Up @@ -33,12 +35,58 @@ export class WebAccessFS extends Async(FileSystem) {

protected _handles = new Map<string, FileSystemHandle>();

/**
* Loads all of the handles.
* @internal @hidden
*/
async _loadHandles(path: string, handle: FileSystemDirectoryHandle) {
for await (const [key, child] of handle.entries()) {
const p = join(path, key);
this._handles.set(p, child);
if (isKind(child, 'directory')) await this._loadHandles(p, child);
}
}

/**
* Loads metadata
* @internal @hidden
*/
async _loadMetadata(metadataPath?: string): Promise<void> {
if (metadataPath) {
const handle = this.get('file', metadataPath);
const file = await handle.getFile();
const raw = await file.text();
const data = JSON.parse(raw);
this.index.fromJSON(data);
return;
}

for (const [path, handle] of this._handles) {
if (isKind(handle, 'file')) {
const { lastModified, size } = await handle.getFile();
this.index.set(path, new Inode({ mode: 0o777 | constants.S_IFREG, size, mtimeMs: lastModified }));
return;
}

if (!isKind(handle, 'directory')) throw new ErrnoError(Errno.EIO, 'Invalid handle', path);

this.index.set(path, new Inode({ mode: 0o777 | constants.S_IFDIR, size: 0 }));
}
}

/**
* @hidden
*/
_sync: FileSystem = InMemory.create({ name: 'accessfs-cache' });

public constructor(handle: FileSystemDirectoryHandle) {
public constructor(
handle: FileSystemDirectoryHandle,
/**
* Disables index optimizations,
* like using the index for `readdir`
*/
public readonly disableIndexOptimizations: boolean = false
) {
super();
this._handles.set('/', handle);
}
Expand All @@ -48,58 +96,50 @@ export class WebAccessFS extends Async(FileSystem) {
...super.metadata(),
name: 'WebAccess',
noResizableBuffers: true,
// Not really, but we don't support opening directories so this prevent the VFS from trying
features: ['setid'],
};
}

public async sync(path: string, data?: Uint8Array, stats?: Partial<Readonly<InodeLike>>): Promise<void> {
this.index.get(path)!.update(stats);
if (data) await this.write(path, data, 0);
}

public async rename(oldPath: string, newPath: string): Promise<void> {
const handle = await this.getHandle(oldPath, 'rename');
const handle = this.get(null, oldPath, 'rename');
if (isKind(handle, 'directory')) {
const files = await this.readdir(oldPath);

await this.mkdir(newPath);
if (!files.length) {
await this.unlink(oldPath);
return;
}

for (const file of files) {
await this.rename(join(oldPath, file), join(newPath, file));
await this.unlink(oldPath);
}
for (const file of files) await this.rename(join(oldPath, file), join(newPath, file));

await this.unlink(oldPath);

return;
}

if (!isKind(handle, 'file')) {
throw new ErrnoError(Errno.ENOTSUP, 'Not a file or directory handle', oldPath, 'rename');
}
const oldFile = await handle.getFile().catch((ex: ConvertException) => {
throw convertException(ex, oldPath, 'rename');
});
const destFolder = await this.getHandle(dirname(newPath), 'rename');
const oldFile = await handle.getFile().catch(ex => _throw(convertException(ex, oldPath, 'rename')));

const destFolder = this.get('directory', dirname(newPath), 'rename');

if (!isKind(destFolder, 'directory')) throw ErrnoError.With('ENOTDIR', dirname(newPath), 'rename');
const newFile = await destFolder
.getFileHandle(basename(newPath), { create: true })
.catch(ex => _throw(convertException(ex, newPath, 'rename')));

const newFile = await destFolder.getFileHandle(basename(newPath), { create: true }).catch((ex: ConvertException) => {
throw convertException(ex, newPath, 'rename');
});
const writable = await newFile.createWritable();
await writable.write(await oldFile.arrayBuffer());

await writable.close();
await this.unlink(oldPath);
}

public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise<void> {
const handle = await this.getHandle(path, 'write');
public async unlink(path: string): Promise<void> {
if (path == '/') throw ErrnoError.With('EBUSY', '/', 'unlink');
const handle = this.get('directory', dirname(path), 'unlink');
await handle.removeEntry(basename(path), { recursive: true }).catch(ex => _throw(convertException(ex, path, 'unlink')));
this.index.delete(path);
}

if (!isKind(handle, 'file')) throw ErrnoError.With('EISDIR', path, 'write');
public async read(path: string, buffer: Uint8Array, offset: number, end: number): Promise<void> {
const handle = this.get('file', path, 'write');

const file = await handle.getFile();
const data = await file.arrayBuffer();
Expand All @@ -112,9 +152,7 @@ export class WebAccessFS extends Async(FileSystem) {
throw new ErrnoError(Errno.EINVAL, 'Resizable buffers can not be written', path, 'write');
}

const handle = await this.getHandle(path, 'write');

if (!isKind(handle, 'file')) throw ErrnoError.With('EISDIR', path, 'write');
const handle = this.get('file', path, 'write');

const writable = await handle.createWritable();

Expand All @@ -135,126 +173,91 @@ export class WebAccessFS extends Async(FileSystem) {
return this.write(path, data, 0);
}

public async createFile(path: string, flag: string): Promise<PreloadFile<this>> {
const handle = await this.getHandle(dirname(path), 'createFile');

if (!isKind(handle, 'directory')) throw ErrnoError.With('ENOTDIR', dirname(path), 'createFile');

const base = basename(path);

for await (const key of handle.keys()) {
if (key == base) throw ErrnoError.With('EEXIST', path, 'createFile');
}
public async stat(path: string): Promise<Stats> {

Check warning on line 176 in src/access.ts

View workflow job for this annotation

GitHub Actions / CI

Async method 'stat' has no 'await' expression
const inode = this.index.get(path);
if (!inode) throw ErrnoError.With('ENOENT', path, 'stat');

await handle.getFileHandle(base, { create: true });
return this.openFile(path, flag);
return inode.toStats();
}

public async stat(path: string): Promise<Stats> {
const handle = await this.getHandle(path, 'stat');
public async createFile(path: string, flag: string, mode: number, options: CreationOptions): Promise<File> {
const handle = this.get('directory', dirname(path), 'createFile');

if (isKind(handle, 'directory')) {
return new Stats({ mode: 0o777 | constants.S_IFDIR, size: 4096 });
}
if (isKind(handle, 'file')) {
const { lastModified, size } = await handle.getFile();
return new Stats({ mode: 0o777 | constants.S_IFREG, size, mtimeMs: lastModified });
}
throw new ErrnoError(Errno.EBADE, 'Handle is not a directory or file', path, 'stat');
}
if (this.index.has(path)) throw ErrnoError.With('EEXIST', path, 'createFile');

public async openFile(path: string, flag: string): Promise<PreloadFile<this>> {
const handle = await this.getHandle(path, 'openFile');
const file = await handle.getFileHandle(basename(path), { create: true });

if (!isKind(handle, 'file')) throw ErrnoError.With('EISDIR', path, 'openFile');
const inode = new Inode({ ...options, mode: mode ?? 0 | S_IFREG });
this.index.set(path, inode);
this._handles.set(path, file);

const file = await handle.getFile().catch((ex: ConvertException) => {
throw convertException(ex, path, 'openFile');
});
const data = new Uint8Array(await file.arrayBuffer());
const stats = new Stats({ mode: 0o777 | constants.S_IFREG, size: file.size, mtimeMs: file.lastModified });
return new PreloadFile(this, path, flag, stats, data);
return new LazyFile(this, path, flag, inode);
}

public async unlink(path: string): Promise<void> {
const handle = await this.getHandle(dirname(path), 'unlink');
if (!isKind(handle, 'directory')) {
throw ErrnoError.With('ENOTDIR', dirname(path), 'unlink');
}
await handle.removeEntry(basename(path), { recursive: true }).catch((ex: ConvertException) => {
throw convertException(ex, path, 'unlink');
});
public async openFile(path: string, flag: string): Promise<File> {

Check warning on line 197 in src/access.ts

View workflow job for this annotation

GitHub Actions / CI

Async method 'openFile' has no 'await' expression
const inode = this.index.get(path);
if (!inode) throw ErrnoError.With('ENOENT', path, 'stat');

return new LazyFile(this, path, flag, inode.toStats());
}

// eslint-disable-next-line @typescript-eslint/require-await
/**
* @todo Implement
*/
public async link(srcpath: string): Promise<void> {

Check warning on line 207 in src/access.ts

View workflow job for this annotation

GitHub Actions / CI

Async method 'link' has no 'await' expression

Check warning on line 207 in src/access.ts

View workflow job for this annotation

GitHub Actions / CI

'srcpath' is defined but never used
return;
}

public async sync(path: string, data?: Uint8Array, stats?: Readonly<Partial<InodeLike>>): Promise<void> {
const inode = this.index.get(path) ?? new Inode(stats);
inode.update(stats);
this.index.set(path, inode);
if (data) await this.write(path, data, 0);
}

public async rmdir(path: string): Promise<void> {
return this.unlink(path);
}

public async mkdir(path: string, mode?: number, options?: CreationOptions): Promise<void> {
if (await this.exists(path)) {
throw ErrnoError.With('EEXIST', path, 'mkdir');
}
if (this.index.has(path)) throw ErrnoError.With('EEXIST', path, 'mkdir');

const handle = await this.getHandle(dirname(path), 'mkdir');
if (!isKind(handle, 'directory')) {
throw ErrnoError.With('ENOTDIR', path, 'mkdir');
}
await handle.getDirectoryHandle(basename(path), { create: true });
const handle = this.get('directory', dirname(path), 'mkdir');

const dir = await handle.getDirectoryHandle(basename(path), { create: true });
this._handles.set(path, dir);

this.index.set(path, new Inode({ ...options, mode: mode ?? 0 | S_IFDIR }));
}

public async readdir(path: string): Promise<string[]> {
const handle = await this.getHandle(path, 'readdir');
if (!isKind(handle, 'directory')) {
throw ErrnoError.With('ENOTDIR', path, 'readdir');
if (!this.disableIndexOptimizations) {
if (!this.index.has(path)) throw ErrnoError.With('ENOENT', path, 'readdir');
const listing = this.index.directories().get(path);
if (!listing) throw ErrnoError.With('ENOTDIR', path, 'readdir');
return Object.keys(listing);
}

const handle = this.get('directory', path, 'readdir');

const entries = [];
for await (const k of handle.keys()) {
entries.push(k);
}
return entries;
}

protected async getHandle(path: string, syscall: string): Promise<FileSystemHandle> {
if (this._handles.has(path)) {
return this._handles.get(path)!;
}
protected get<const T extends FileSystemHandleKind | null>(
kind: T = null as T,
path: string,
syscall?: string
): T extends FileSystemHandleKind ? HKindToType<T> : FileSystemHandle {
const handle = this._handles.get(path);
if (!handle) throw ErrnoError.With('ENODATA', path, syscall);

let walked = '/';

for (const part of path.split('/').slice(1)) {
const handle = this._handles.get(walked);
if (!handle) throw ErrnoError.With('ENOENT', walked, syscall);
if (!isKind(handle, 'directory')) throw ErrnoError.With('ENOTDIR', walked, syscall);
walked = join(walked, part);

try {
const child = await handle.getDirectoryHandle(part);
this._handles.set(walked, child);
} catch (_ex: unknown) {
const ex = _ex as DOMException;

switch (ex.name) {
case 'TypeMismatchError':
try {
return await handle.getFileHandle(part);
} catch (ex: any) {
throw convertException(ex, walked, syscall);
}
case 'TypeError':
throw new ErrnoError(Errno.ENOENT, ex.message, walked, syscall);
default:
throw convertException(ex, walked, syscall);
}
}
}
if (kind && !isKind(handle, kind)) throw ErrnoError.With(kind == 'directory' ? 'ENOTDIR' : 'EISDIR', path, syscall);

return this._handles.get(path)!;
return handle as T extends FileSystemHandleKind ? HKindToType<T> : FileSystemHandle;
}
}

Expand All @@ -263,10 +266,14 @@ const _WebAccess = {

options: {
handle: { type: 'object', required: true },
metadata: { type: 'string', required: false },
},

create(options: WebAccessOptions) {
return new WebAccessFS(options.handle);
async create(options: WebAccessOptions) {
const fs = new WebAccessFS(options.handle);
await fs._loadHandles('/', options.handle);
await fs._loadMetadata(options.metadata);
return fs;
},
} as const satisfies Backend<WebAccessFS, WebAccessOptions>;
type _WebAccess = typeof _WebAccess;
Expand Down
13 changes: 6 additions & 7 deletions tests/setup-access.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { handle } from './web-access.js';
import { configureSingle } from '@zenfs/core';
import { WebAccess } from '../src/access.js';
import { configureSingle, mounts } from '@zenfs/core';
import { copy, data } from '@zenfs/core/tests/setup.js';
import { WebAccess, type WebAccessFS } from '../src/access.js';
import { handle } from './web-access.js';

await configureSingle({
backend: WebAccess,
handle,
});
await configureSingle({ backend: WebAccess, handle });

copy(data);

await (mounts.get('/') as WebAccessFS).queueDone();
Loading

0 comments on commit be90e55

Please sign in to comment.