import { importer } from "ipfs-unixfs-importer"; import type { FileCandidate } from "ipfs-unixfs-importer"; import { fixedSize } from "ipfs-unixfs-importer/chunker"; import { balanced } from "ipfs-unixfs-importer/layout"; import { CarWriter } from "@ipld/car"; import { CID } from "multiformats/cid"; import { walk } from "@std/fs"; import { join, relative } from "@std/path"; /** * Create a CAR file of a directory. */ export async function createCAR( dirPath: string, carPath: string, ): Promise { const blockstore = new Map(); const options = { cidVersion: 1 as const, rawLeaves: true, chunker: fixedSize({ chunkSize: 1048576 }), layout: balanced({ maxChildrenPerNode: 1024 }), hashAlg: "sha2-256", }; let rootCid: CID | undefined; for await ( const entry of importer(walkDir(dirPath), { put: async (cid: CID, block: Uint8Array): Promise => { blockstore.set(cid.toString(), block); return cid; }, }, options) ) { rootCid = entry.cid; } if (rootCid === undefined) throw new Error("importer produced no root CID"); const { writer, out } = CarWriter.create([rootCid]); const file = await Deno.open(carPath, { write: true, create: true, truncate: true, }); const writePromise = ReadableStream.from(out).pipeTo(file.writable); for (const [cidStr, block] of blockstore) { await writer.put({ cid: CID.parse(cidStr), bytes: block }); } await writer.close(); await writePromise; return rootCid.toString(); } // 🛠️ async function* walkDir(dirPath: string): AsyncGenerator { // Collect and sort paths so the resulting CID is deterministic regardless // of filesystem entry order (which changes when a directory is copied). const paths: string[] = []; for await ( const entry of walk(dirPath, { includeFiles: true, includeDirs: false }) ) { paths.push(relative(dirPath, entry.path)); } paths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); // Prefix all paths with a common root directory so the importer builds a // wrapping root directory node. Without this, the importer only yields // subdirectory nodes (e.g. "vendor") and never a root directory, so the // last entry's CID would be a subdirectory rather than the true root. // The prefix name doesn't affect any CIDs — only the directory structure // matters, and the root's link names come from the last path component. for (const path of paths) { yield { path: "root/" + path, content: await Deno.readFile(join(dirPath, path)), }; } }