import { CID } from "multiformats" import { default as init, PublicDirectory, PublicFile, PublicNode } from "wnfs" import * as Depot from "../../components/depot/implementation.js" import * as Manners from "../../components/manners/implementation.js" import { WASM_WNFS_VERSION } from "../../common/version.js" import { Segments as Path } from "../../path/index.js" import { UnixTree, Puttable, File, Links, PuttableUnixTree } from "../types.js" import { BlockStore, DepotBlockStore } from "./DepotBlockStore.js" import { BaseFile } from "../base/file.js" import { Metadata } from "../metadata.js" // This is some global mutable state to work around global mutable state // issues with wasm-bindgen. It's important we *never* accidentally initialize the // "wnfs" Wasm module twice. let initialized = false async function loadWasm({ manners }: Dependencies) { // MUST be prevented from initializing twice: // https://github.com/fission-codes/webnative/issues/429 // https://github.com/rustwasm/wasm-bindgen/issues/3307 if (initialized) return initialized = true manners.log(`⏬ Loading WNFS WASM`) const before = performance.now() // init accepts Promises as arguments await init(manners.wnfsWasmLookup(WASM_WNFS_VERSION)) const time = performance.now() - before manners.log(`🧪 Loaded WNFS WASM (${time.toFixed(0)}ms)`) } type Dependencies = { depot: Depot.Implementation manners: Manners.Implementation } interface DirEntry { name: string metadata: { version: "3.0.0" unixMeta: { created: number modified: number mode: number kind: "raw" | "dir" | "file" | "metadata" | "symlink" | "hamtShard" } } } interface OpResult { rootDir: PublicDirectory result: A } // ROOT export class PublicRootWasm implements UnixTree, Puttable { dependencies: Dependencies root: Promise lastRoot: PublicDirectory store: BlockStore readOnly: boolean constructor(dependencies: Dependencies, root: PublicDirectory, store: BlockStore, readOnly: boolean) { this.dependencies = dependencies this.root = Promise.resolve(root) this.lastRoot = root this.store = store this.readOnly = readOnly } static async empty(dependencies: Dependencies): Promise { await loadWasm(dependencies) const store = new DepotBlockStore(dependencies.depot) const root = new PublicDirectory(new Date()) return new PublicRootWasm(dependencies, root, store, false) } static async fromCID(dependencies: Dependencies, cid: CID): Promise { await loadWasm(dependencies) const store = new DepotBlockStore(dependencies.depot) const root = await PublicDirectory.load(cid.bytes, store) return new PublicRootWasm(dependencies, root, store, false) } private async atomically(fn: (root: PublicDirectory) => Promise) { const root = await this.root this.root = fn(root) await this.root } private async withError(operation: Promise, opDescription: string): Promise { try { return await operation } catch (e) { console.error(`Error during WASM operation ${opDescription}:`) throw e } } async ls(path: Path): Promise { const root = await this.root const { result: node } = await this.withError( root.getNode(path, this.store), `ls(${path.join("/")})` ) as OpResult if (node == null) { throw new Error(`Can't ls ${path.join("/")}: No such directory`) } if (!node.isDir()) { throw new Error(`Can't ls ${path.join("/")}: Not a directory`) } const directory = node.asDir() const { result: entries } = await this.withError( root.ls(path, this.store), `ls(${path.join("/")})` ) as OpResult const result: Links = {} for (const entry of entries) { const node = await directory.lookupNode(entry.name, this.store) as PublicNode const cid = node.isFile() ? CID.decode(await node.asFile().store(this.store)) : CID.decode(await node.asDir().store(this.store)) result[ entry.name ] = { name: entry.name, isFile: entry.metadata.unixMeta.kind === "file", size: 0, // TODO size? cid, } } return result } async mkdir(path: Path): Promise { await this.atomically(async root => { const { rootDir } = await this.withError( root.mkdir(path, new Date(), this.store), `mkdir(${path.join("/")})` ) as OpResult return rootDir }) return this } async cat(path: Path): Promise { const root = await this.root const { result: cidBytes } = await this.withError( root.read(path, this.store), `read(${path.join("/")})` ) as OpResult const cid = CID.decode(cidBytes) return this.dependencies.depot.getUnixFile(cid) } async add(path: Path, content: Uint8Array): Promise { const { cid } = await this.dependencies.depot.putChunked(content) await this.atomically(async root => { const { rootDir } = await this.withError( root.write(path, cid.bytes, new Date(), this.store), `write(${path.join("/")})` ) as OpResult return rootDir }) return this } async rm(path: Path): Promise { await this.atomically(async root => { const { rootDir } = await this.withError( root.rm(path, this.store), `rm(${path.join("/")})` ) as OpResult return rootDir }) return this } async mv(from: Path, to: Path): Promise { await this.atomically(async root => { const { rootDir } = await this.withError( root.basicMv(from, to, new Date(), this.store), `basicMv(${from.join("/")}, ${to.join("/")})` ) as OpResult return rootDir }) return this } async get(path: Path): Promise { const root = await this.root const { result: node } = await this.withError( root.getNode(path, this.store), `getNode(${path.join("/")})` ) as OpResult if (node == null) { return null } if (node.isFile()) { const cachedFile = node.asFile() const content = await this.cat(path) const directory = path.slice(0, -1) const filename = path[ path.length - 1 ] return new PublicFileWasm(content, directory, filename, this, cachedFile) } else if (node.isDir()) { const cachedDir = node.asDir() return new PublicDirectoryWasm(this.readOnly, path, this, cachedDir) } throw new Error(`Unknown node type. Can only handle files and directories.`) } async exists(path: Path): Promise { const root = await this.root try { await root.getNode(path, this.store) return true } catch { return false } } async historyStep(): Promise { await this.atomically(async root => { const { rootDir: rebasedRoot } = await root.baseHistoryOn(this.lastRoot, this.store) as OpResult this.lastRoot = root return rebasedRoot }) return await this.root } async put(): Promise { const rebasedRoot = await this.historyStep() const cidBytes = await rebasedRoot.store(this.store) as Uint8Array return CID.decode(cidBytes) } async putDetailed(): Promise { return { cid: await this.put(), size: 0, // TODO figure out size isFile: false, } } } // DIRECTORY export class PublicDirectoryWasm implements UnixTree, Puttable { readOnly: boolean private directory: string[] private publicRoot: PublicRootWasm private cachedDir: PublicDirectory constructor(readOnly: boolean, directory: string[], publicRoot: PublicRootWasm, cachedDir: PublicDirectory) { this.readOnly = readOnly this.directory = directory this.publicRoot = publicRoot this.cachedDir = cachedDir } private checkMutability(operation: string) { if (this.readOnly) throw new Error(`Directory is read-only. Cannot ${operation}`) } private async updateCache() { const root = await this.publicRoot.root const node = await root.getNode(this.directory, this.publicRoot.store) this.cachedDir = node.asDir() } get header(): { metadata: Metadata; previous?: CID } { return nodeHeader(this.cachedDir) } async ls(path: Path): Promise { return await this.publicRoot.ls([ ...this.directory, ...path ]) } async mkdir(path: Path): Promise { this.checkMutability(`mkdir at ${[ ...this.directory, ...path ].join("/")}`) await this.publicRoot.mkdir([ ...this.directory, ...path ]) await this.updateCache() return this } async cat(path: Path): Promise { return await this.publicRoot.cat([ ...this.directory, ...path ]) } async add(path: Path, content: Uint8Array): Promise { this.checkMutability(`write at ${[ ...this.directory, ...path ].join("/")}`) await this.publicRoot.add([ ...this.directory, ...path ], content) await this.updateCache() return this } async rm(path: Path): Promise { this.checkMutability(`remove at ${[ ...this.directory, ...path ].join("/")}`) await this.publicRoot.rm([ ...this.directory, ...path ]) await this.updateCache() return this } async mv(from: Path, to: Path): Promise { this.checkMutability(`mv from ${[ ...this.directory, ...from ].join("/")} to ${[ ...this.directory, ...to ].join("/")}`) await this.publicRoot.mv([ ...this.directory, ...from ], [ ...this.directory, ...to ]) await this.updateCache() return this } async get(path: Path): Promise { return await this.publicRoot.get([ ...this.directory, ...path ]) } async exists(path: Path): Promise { return await this.publicRoot.exists([ ...this.directory, ...path ]) } async put(): Promise { await this.publicRoot.put() const root = await this.publicRoot.root const cidBytes: Uint8Array = await root.store(this.publicRoot.store) return CID.decode(cidBytes) } async putDetailed(): Promise { return { isFile: false, size: 0, cid: await this.put() } } } // FILE // This is somewhat of a weird hack of providing a result for a `get()` operation. export class PublicFileWasm extends BaseFile { private directory: string[] private filename: string private publicRoot: PublicRootWasm private cachedFile: PublicFile constructor(content: Uint8Array, directory: string[], filename: string, publicRoot: PublicRootWasm, cachedFile: PublicFile) { super(content) this.directory = directory this.filename = filename this.publicRoot = publicRoot this.cachedFile = cachedFile } private async updateCache() { const root = await this.publicRoot.root const node = await root.getNode([ ...this.directory, this.filename ], this.publicRoot.store) this.cachedFile = node.asFile() } get header(): { metadata: Metadata; previous?: CID } { return nodeHeader(this.cachedFile) } async updateContent(content: Uint8Array): Promise { await super.updateContent(content) await this.updateCache() return this } async putDetailed(): Promise { const root = await this.publicRoot.root const path = [ ...this.directory, this.filename ] const { result: node } = await root.getNode(path, this.publicRoot.store) as OpResult if (node == null) { throw new Error(`No file at /${path.join("/")}.`) } if (!node.isFile()) { throw new Error(`Not a file at /${path.join("/")}`) } const file = node.asFile() return { isFile: true, size: 0, cid: CID.decode(await file.store(this.publicRoot.store)) } } } function nodeHeader(node: PublicFile | PublicDirectory): { metadata: Metadata; previous?: CID } { // There's some differences between the two. const meta = node.metadata() const metadata: Metadata = { isFile: meta.unixMeta.kind === "file", version: meta.version, unixMeta: { _type: meta.unixMeta.kind, ctime: Number(meta.unixMeta.created), mtime: Number(meta.unixMeta.modified), mode: meta.unixMeta.mode, } } const previous = node.previousCid() return previous == null ? { metadata } : { metadata, previous: CID.decode(previous), } }