filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
238 lines • 8.69 kB
JavaScript
/**
* 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