UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

279 lines (237 loc) 8 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') export interface CreateCarOptions { bare?: boolean onProgress?: (bytesProcessed: number, totalBytes: number) => void } export interface CreateCarResult { carBytes: Uint8Array rootCid: CID } /** * 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: File, options: CreateCarOptions = {}): Promise<CreateCarResult> { 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: CID 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: File[], options: CreateCarOptions = {}): Promise<CreateCarResult> { 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: File[], options: CreateCarOptions = {}): Promise<CreateCarResult> { 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 as any).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 as any).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: (fs: any) => Promise<CID> ): Promise<CreateCarResult> { // 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: Uint8Array, rootCid: CID): Promise<Uint8Array> { // 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: Uint8Array[] = [] ;(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) }