Skip to content

Commit

Permalink
fileserver: BrowserFS statsCache and readFile
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursunny committed Jan 24, 2024
1 parent 7329e11 commit 4ded71b
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 29 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test": "vitest",
"typedoc": "bash mk/typedoc.sh"
},
"packageManager": "pnpm@8.14.2",
"packageManager": "pnpm@8.14.3",
"devDependencies": {
"@types/node": "^20.11.5",
"@types/wtfnode": "^0.7.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@ndn/node-transport": "workspace:*",
"@ndn/packet": "workspace:*",
"@ndn/util": "workspace:*",
"dotenv": "^16.3.2",
"dotenv": "^16.4.0",
"env-var": "^7.4.1",
"tslib": "^2.6.2",
"wtfnode": "^0.9.1"
Expand Down
20 changes: 18 additions & 2 deletions packages/fileserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,21 @@

This package is part of [NDNts](https://yoursunny.com/p/NDNts/), Named Data Networking libraries for the modern web.

This package implements [ndn6-file-server](https://github.com/yoursunny/ndn6-tools/blob/main/file-server.md) protocol, including metadata and directory listing.
`@ndn/cat` package has a `file-client` subcommand that uses this package.
This package implements [ndn6-file-server](https://github.com/yoursunny/ndn6-tools/blob/main/file-server.md) protocol.

## Features

Data structures, encoding and decoding:

* assigned numbers and keywords
* FileMetadata
* directory listing

Client (consumer):

* simple client
* demonstrated in `@ndn/cat` package `file-client` subcommand
* [BrowserFS](https://browser-fs.github.io/core/) wrapper
* rudimentary, not really usable

Server (producer): not yet.
2 changes: 2 additions & 0 deletions packages/fileserver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@ndn/segmented-object": "workspace:*",
"@ndn/tlv": "workspace:*",
"@ndn/util": "workspace:*",
"mnemonist": "^0.39.7",
"obliterator": "^2.0.4",
"streaming-iterables": "^8.0.1",
"tslib": "^2.6.2"
},
Expand Down
72 changes: 53 additions & 19 deletions packages/fileserver/src/browserfs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type BackendOption, CreateBackend } from "@browserfs/core/backends/backend.js";
import { ApiError, BaseFile, BaseFileSystem, type Cred, ErrorCode, type File, type FileFlag, type FileSystem, type FileSystemMetadata, FileType, Stats } from "@browserfs/core/index.js";
import { assert } from "@ndn/util";
import LRUCache from "mnemonist/lru-cache.js";
import { collect, map, pipeline } from "streaming-iterables";

import { Client } from "./client";
Expand All @@ -17,20 +18,38 @@ export class NDNFileSystem extends BaseFileSystem implements FileSystem {
validator(opt: Client) {
assert(opt instanceof Client);
},
},
} satisfies BackendOption<Client>,
statsCacheCapacity: {
type: "number",
description: "cache capacity for FileMetadata",
optional: true,
validator(opt: number) {
assert(typeof opt === "number");
},
} satisfies BackendOption<number>,
} satisfies Record<string, BackendOption<unknown>>;

public static isAvailable(): boolean {
return true;
}

constructor(opts?: Partial<NDNFileSystem.Options>) {
constructor({
client,
statsCacheCapacity = 16,
}: Partial<NDNFileSystem.Options> = {}) {
super();
assert(opts?.client);
this.client = opts.client;

// use Partial<Options> to avoid typing error in Create function
assert(!!client);
this.client = client;

if (statsCacheCapacity > 0) {
this.statsCache = new LRUCache(statsCacheCapacity);
}
}

private readonly client: Client;
private readonly statsCache?: LRUCache<string, FileMetadata>;

public override get metadata(): FileSystemMetadata {
return Object.assign(super.metadata, {
Expand All @@ -39,7 +58,12 @@ export class NDNFileSystem extends BaseFileSystem implements FileSystem {
}

private async getFileMetadata(p: string): Promise<FileMetadata> {
return this.client.stat(p.slice(1));
let m = this.statsCache?.get(p);
if (!m) {
m = await this.client.stat(p.slice(1));
this.statsCache?.set(p, m);
}
return m;
}

public override async stat(p: string, cred: Cred): Promise<Stats> {
Expand All @@ -66,10 +90,18 @@ export class NDNFileSystem extends BaseFileSystem implements FileSystem {
const m = await this.getFileMetadata(p);
return new NDNFile(this.client, m);
}

public override async readFile(p: string, flag: FileFlag, cred: Cred): Promise<Uint8Array> {
void flag;
void cred;
const m = await this.getFileMetadata(p);
return this.client.readFile(m);
}
}
export namespace NDNFileSystem {
export interface Options {
client: Client;
statsCacheCapacity?: number;
}
}

Expand All @@ -79,14 +111,13 @@ class NDNFile extends BaseFile implements File {
private readonly m: FileMetadata,
) {
super();
for (const methodName of fileMethods) {
if (methodName.endsWith("Sync")) {
this[methodName] = () => {
throw new ApiError(ErrorCode.ENOTSUP);
};
} else {
(this[methodName] as any) = async (...args: any[]) => (this as any)[`${methodName}Sync`](...args);
}
for (const methodName of fileMethodsNotsup) {
this[methodName] = () => {
throw new ApiError(ErrorCode.ENOTSUP);
};
}
for (const methodName of fileMethodsAsync) {
this[methodName] = async (...args: any[]) => (this[`${methodName}Sync`] as any)(...args);
}
}

Expand All @@ -110,14 +141,17 @@ class NDNFile extends BaseFile implements File {
return { bytesRead: length, buffer };
}
}
interface NDNFile extends Pick<File, typeof fileMethods[number]> {}

const fileMethods = [
interface NDNFile extends Pick<File, typeof fileMethodsNotsup[number] | typeof fileMethodsAsync[number]> {}
const fileMethodsNotsup = [
"truncateSync",
"writeSync",
"readSync",
] as const satisfies ReadonlyArray<keyof File>;
const fileMethodsAsync = [
"stat",
"close",
"truncate", "truncateSync",
"write", "writeSync",
"readSync",
"truncate",
"write",
] as const satisfies ReadonlyArray<keyof File>;

function statsFromFileMetadata(m: FileMetadata): Stats {
Expand Down
18 changes: 12 additions & 6 deletions packages/fileserver/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, type Name } from "@ndn/packet";
import { retrieveMetadata } from "@ndn/rdr";
import { fetch } from "@ndn/segmented-object";
import { assert } from "@ndn/util";
import map from "obliterator/map.js";

import { type DirEntry, parseDirectoryListing } from "./ls";
import { FileMetadata } from "./metadata";
Expand All @@ -13,7 +14,8 @@ export interface ClientOptions extends retrieveMetadata.Options, fetch.Options {
export class Client {
constructor(
public readonly prefix: Name,
private readonly opts: ClientOptions = {}) {}
private readonly opts: ClientOptions = {},
) {}

/**
* Retrieve metadata of given relative path.
Expand All @@ -28,11 +30,15 @@ export class Client {
*/
public stat(parentRelPath: string, de: DirEntry): Promise<FileMetadata>;

public stat(relPath: string, de?: DirEntry): Promise<FileMetadata> {
const name = this.prefix.append(
...(relPath === "" ? [] : relPath.split("/").map((comp) => new Component(undefined, comp))),
...(de ? [de.name] : []),
);
public stat(relPath: string, child?: DirEntry): Promise<FileMetadata> {
const name = this.prefix.append(...map((function*() {
if (relPath !== "") {
yield* relPath.split("/");
}
if (child) {
yield child.name;
}
})(), (comp) => new Component(undefined, comp)));
return retrieveMetadata(name, FileMetadata, this.opts);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/fileserver/tests/client.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ test("bfs open reject", async () => {
await expect(bfs.promises.open("/N/A/B.bin", "w")).rejects.toThrow();
});

test("bfs readdir", async () => {
await expect(bfs.promises.readdir("/N")).resolves.toEqual(["A"]);
await expect(bfs.promises.readdir("/N/A")).resolves.toEqual(["B.bin"]);
});

describe("bfs open", () => {
let fd: number;
beforeAll(async () => {
Expand Down

0 comments on commit 4ded71b

Please sign in to comment.