UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

238 lines 8.69 kB
/** * Browser-compatible UnixFS to CAR conversion functionality * * This module provides utilities to create CAR files from browser Files * using @helia/unixfs and BrowserCARBlockstore. */ import { unixfs } from '@helia/unixfs'; import { CarReader, CarWriter } from '@ipld/car'; import toBuffer from 'it-to-buffer'; import { CID } from 'multiformats/cid'; import { CARWritingBlockstore } from '../car/browser-car-blockstore.js'; // Placeholder CID used during CAR creation (will be replaced with actual root) const PLACEHOLDER_CID = CID.parse('bafyaaiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); /** * Create a CAR file from a File using UnixFS encoding * * @param file - Browser File object to encode * @param options - Optional bare flag and progress callback * @returns CAR bytes and root CID */ export async function createCarFromFile(file, options = {}) { const { bare = false } = options; const onProgress = options.onProgress; let bytesProcessed = 0; const totalBytes = file.size; return createCar(async (fs) => { // Create async iterable from file stream async function* fileContent() { const reader = file.stream().getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { bytesProcessed += value.length; onProgress?.(bytesProcessed, totalBytes); yield value; } } } finally { reader.releaseLock(); } } let rootCid; if (bare) { // Bare mode: add file directly as byte stream without any wrapper rootCid = await fs.addByteStream(fileContent()); } else { // Directory wrapper mode: use addFile which automatically creates a directory wrapper rootCid = await fs.addFile({ path: file.name, content: fileContent(), }); } return rootCid; }); } /** * Create a CAR file from multiple Files using UnixFS encoding * * @param files - Array of browser File objects to encode * @param options - Optional progress callback * @returns CAR bytes and root CID */ export async function createCarFromFiles(files, options = {}) { if (files.length === 0) { throw new Error('At least one file is required'); } // If bare mode is requested with multiple files, throw error if (options.bare && files.length > 1) { throw new Error('--bare flag is not supported for multiple files'); } // Single file with bare mode if (options.bare && files.length === 1 && files[0] != null) { return createCarFromFile(files[0], options); } return createCar(async (fs) => { // Convert files to addAll format async function* fileGenerator() { for (const file of files) { // Create async iterable from file stream async function* fileContent() { const reader = file.stream().getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) yield value; } } finally { reader.releaseLock(); } } yield { path: file.name, content: fileContent(), }; } } // Add all entries using addAll const entries = []; for await (const entry of fs.addAll(fileGenerator())) { entries.push(entry); } // The last entry should be the root directory const rootCid = entries[entries.length - 1]?.cid; if (!rootCid) { // Empty - create a single empty directory block const emptyDirCid = await fs.addDirectory(); return emptyDirCid; } return rootCid; }); } /** * Create a CAR file from Files with directory structure preserved * Handles webkitRelativePath for directory uploads * * @param files - Array of browser File objects with potential webkitRelativePath * @param options - Optional progress callback * @returns CAR bytes and root CID */ export async function createCarFromFileList(files, options = {}) { if (files.length === 0) { throw new Error('At least one file is required'); } // Check if files have webkitRelativePath (directory upload) const hasDirectoryStructure = files.some((f) => f.webkitRelativePath); if (!hasDirectoryStructure) { // No directory structure, treat as regular files return createCarFromFiles(files, options); } // Has directory structure - preserve it return createCar(async (fs) => { // Convert files to addAll format with paths async function* fileGenerator() { for (const file of files) { const path = file.webkitRelativePath || file.name; // Create async iterable from file stream async function* fileContent() { const reader = file.stream().getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) yield value; } } finally { reader.releaseLock(); } } yield { path, content: fileContent(), }; } } // Add all entries using addAll const entries = []; for await (const entry of fs.addAll(fileGenerator())) { entries.push(entry); } // The last entry should be the root directory const rootCid = entries[entries.length - 1]?.cid; if (!rootCid) { // Empty - create a single empty directory block const emptyDirCid = await fs.addDirectory(); return emptyDirCid; } return rootCid; }); } /** * Common CAR creation logic * * @param content - File or Files to encode * @param options - Options including type * @param addContent - Function that adds content to UnixFS and returns the root CID * @returns CAR bytes and root CID */ async function createCar( // content: File | File[], // options: CreateCarOptions & { type: 'file' | 'directory' }, addContent) { // Create blockstore with placeholder CID const blockstore = new CARWritingBlockstore({ rootCID: PLACEHOLDER_CID, }); // Initialize blockstore (writes CAR header with placeholder) await blockstore.initialize(); // Create UnixFS instance with our blockstore const fs = unixfs({ blockstore }); // Add content using the provided function const rootCid = await addContent(fs); // Finalize CAR (close writer, flush to memory) await blockstore.finalize(); // Get the CAR bytes let carBytes = blockstore.getCarBytes(); // Update the root CID in the CAR bytes carBytes = await updateRootCidInCar(carBytes, rootCid); return { carBytes, rootCid }; } /** * Update the root CID in CAR bytes * This creates a new CAR with the correct root CID */ async function updateRootCidInCar(carBytes, rootCid) { // We need to replace the placeholder CID with the actual root CID // The easiest way is to re-read the CAR and write a new one with the correct root const reader = await CarReader.fromBytes(carBytes); // Create new CAR writer with correct root const { writer, out } = CarWriter.create([rootCid]); // Collect new CAR chunks const newChunks = []; (async () => { for await (const chunk of out) { newChunks.push(chunk); } })(); // Copy all blocks from old CAR to new CAR for await (const { cid, bytes } of reader.blocks()) { await writer.put({ cid, bytes }); } // Close writer await writer.close(); // Combine chunks return toBuffer(newChunks); } //# sourceMappingURL=browser-car-builder.js.map