UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

203 lines 9.1 kB
/** * File and directory add functionality * * This module handles adding files and directories to Filecoin via Synapse SDK. * It encodes content as UnixFS, creates CAR files, and uploads to Filecoin. */ import { readFile, stat } from 'node:fs/promises'; import pc from 'picocolors'; import pino from 'pino'; import { warnAboutCDNPricingLimitations } from '../common/cdn-warning.js'; import { TELEMETRY_CLI_APP_NAME } from '../common/constants.js'; import { displayUploadResults, performAutoFunding, performUpload, validatePaymentSetup } from '../common/upload-flow.js'; import { normalizeMetadataConfig } from '../core/metadata/index.js'; import { cleanupSynapseService, createStorageContext, initializeSynapse, } from '../core/synapse/index.js'; import { cleanupTempCar, createCarFromPath } from '../core/unixfs/index.js'; import { parseCLIAuth, parseProviderOptions } from '../utils/cli-auth.js'; import { cancel, createSpinner, formatFileSize, intro, outro } from '../utils/cli-helpers.js'; import { log } from '../utils/cli-logger.js'; /** * Validate that a path exists and is a regular file or directory */ async function validatePath(path, options) { try { const stats = await stat(path); if (stats.isFile()) { return { exists: true, stats, isDirectory: false }; } if (stats.isDirectory()) { // Check if bare flag is used with directory if (options.bare) { return { exists: false, error: `--bare flag is not supported for directories` }; } return { exists: true, stats, isDirectory: true }; } // Not a file or directory (could be symlink, socket, etc.) return { exists: false, error: `Not a file or directory: ${path}` }; } catch (error) { // Differentiate between not found and other errors if (error?.code === 'ENOENT') { return { exists: false, error: `Path not found: ${path}` }; } // Other errors like permission denied, etc. return { exists: false, error: `Cannot access path: ${path} (${error?.message || 'unknown error'})` }; } } /** * Run the file or directory add process * * @param options - Add configuration */ export async function runAdd(options) { intro(pc.bold('Filecoin Pin Add')); const spinner = createSpinner(); const { pieceMetadata, dataSetMetadata } = normalizeMetadataConfig({ pieceMetadata: options.pieceMetadata, dataSetMetadata: options.dataSetMetadata, }); // Initialize logger (silent for CLI output) const logger = pino({ level: process.env.LOG_LEVEL || 'silent', }); // Check CDN status and warn if enabled const withCDN = process.env.WITH_CDN === 'true'; if (withCDN) { const proceed = await warnAboutCDNPricingLimitations(); if (!proceed) { cancel('Add cancelled'); throw new Error('CDN pricing limitations warning cancelled'); } } let tempCarPath; try { // Validate path exists and is readable spinner.start('Validating path...'); const pathValidation = await validatePath(options.filePath, options); if (!pathValidation.exists || !pathValidation.stats) { spinner.stop(`${pc.red('✗')} ${pathValidation.error}`); cancel('Add cancelled'); throw new Error(pathValidation.error); } const pathStat = pathValidation.stats; const isDirectory = pathValidation.isDirectory || false; const pathType = isDirectory ? 'Directory' : 'File'; const sizeDisplay = isDirectory ? '' : ` (${formatFileSize(pathStat.size)})`; spinner.stop(`${pc.green('✓')} ${pathType} validated${sizeDisplay}`); // Initialize Synapse SDK (without storage context) spinner.start('Initializing Synapse SDK...'); // Parse authentication options from CLI and environment const config = parseCLIAuth(options); if (dataSetMetadata) { config.dataSetMetadata = dataSetMetadata; } if (withCDN) config.withCDN = true; // Initialize just the Synapse SDK const synapse = await initializeSynapse({ ...config, telemetry: { sentrySetTags: { appName: TELEMETRY_CLI_APP_NAME } } }, logger); const network = synapse.getNetwork(); spinner.stop(`${pc.green('✓')} Connected to ${pc.bold(network)}`); // Check payment setup (may configure permissions if needed) // Actual CAR size will be checked later spinner.start('Checking payment setup...'); await validatePaymentSetup(synapse, 0, spinner, { suppressSuggestions: true }); // Create CAR from file or directory const packingMsg = isDirectory ? 'Packing directory for IPFS...' : `Packing file for IPFS${options.bare ? ' (bare mode)' : ''}...`; spinner.start(packingMsg); const { carPath, rootCid } = await createCarFromPath(options.filePath, { logger, spinner, isDirectory, ...(options.bare !== undefined && { bare: options.bare }), }); tempCarPath = carPath; spinner.stop(`${pc.green('✓')} ${isDirectory ? 'Directory' : 'File'} packed with root CID: ${rootCid.toString()}`); // Read CAR data spinner.start('Loading packed IPFS content ...'); const carData = await readFile(tempCarPath); const carSize = carData.length; spinner.stop(`${pc.green('✓')} IPFS content loaded (${formatFileSize(carSize)})`); if (options.autoFund) { // Perform auto-funding if requested (now that we know the file size) await performAutoFunding(synapse, carSize, spinner); } else { // Check payment setup and capacity for actual file size spinner.start('Checking payment capacity...'); await validatePaymentSetup(synapse, carSize, spinner); } // Create storage context spinner.start('Creating storage context...'); // Parse provider selection from CLI options and environment variables const providerOptions = parseProviderOptions(options); const storageContextOptions = { logger, ...providerOptions, dataset: { ...(dataSetMetadata && { metadata: dataSetMetadata }), }, callbacks: { onProviderSelected: (provider) => { spinner.message(`Connecting to storage provider: ${provider.name || provider.serviceProvider}...`); }, onDataSetResolved: (info) => { if (info.isExisting) { spinner.message(`Using existing data set #${info.dataSetId}`); } else { spinner.message(`Created new data set #${info.dataSetId}`); } }, }, }; const { storage, providerInfo } = await createStorageContext(synapse, storageContextOptions); spinner.stop(`${pc.green('✓')} Storage context ready`); log.spinnerSection('Storage Context', [ pc.gray(`Data Set ID: ${storage.dataSetId}`), pc.gray(`Provider: ${providerInfo.name || providerInfo.serviceProvider}`), ]); // Create service object for upload function const synapseService = { synapse, storage, providerInfo }; // Upload to Synapse const uploadResult = await performUpload(synapseService, carData, rootCid, { contextType: 'add', fileSize: carSize, logger, spinner, ...(pieceMetadata && { metadata: pieceMetadata }), }); // Display results spinner.stop('━━━ Add Complete ━━━'); const result = { filePath: options.filePath, fileSize: carSize, ...(isDirectory && { isDirectory }), rootCid: rootCid.toString(), pieceCid: uploadResult.pieceCid, pieceId: uploadResult.pieceId, dataSetId: uploadResult.dataSetId, transactionHash: uploadResult.transactionHash, providerInfo: uploadResult.providerInfo, }; displayUploadResults(result, 'Add', network); // Clean up WebSocket providers to allow process termination await cleanupSynapseService(); outro('Add completed successfully'); return result; } catch (error) { spinner.stop(`${pc.red('✗')} Add failed: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error({ event: 'add.failed', error }, 'Add failed'); // Clean up even on error await cleanupSynapseService(); // Always cleanup temp CAR even on error if (tempCarPath) { await cleanupTempCar(tempCarPath, logger); } cancel('Add failed'); throw error; } } //# sourceMappingURL=add.js.map