filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
279 lines (237 loc) • 8 kB
text/typescript
/**
* 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)
}