diff --git a/src/commands/add.ts b/src/commands/add.ts index 4c58194..9aa5dfc 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -2,6 +2,8 @@ import { Command } from 'commander' import { runAdd } from '../add/add.js' import type { AddOptions } from '../add/types.js' import { MIN_RUNWAY_DAYS } from '../common/constants.js' +import { isCarFile } from '../utils/car-detection.js' +import { log } from '../utils/cli-logger.js' import { addAuthOptions, addProviderOptions } from '../utils/cli-options.js' import { addMetadataOptions, resolveMetadataOptions } from '../utils/cli-options-metadata.js' @@ -29,6 +31,15 @@ export const addCommand = new Command('add') ...(dataSetMetadata && { dataSetMetadata }), } + // Check if the file is a CAR file and warn the user + if (await isCarFile(path)) { + log.warn( + `Warning: You are adding a CAR file. Did you mean to 'import' it? +'add' wraps the file in a new UnixFS DAG. +To import existing CAR data, use 'filecoin-pin import'.` + ) + } + await runAdd(addOptions) } catch (error) { console.error('Add failed:', error instanceof Error ? error.message : error) diff --git a/src/test/unit/car-detection.test.ts b/src/test/unit/car-detection.test.ts new file mode 100644 index 0000000..f4d4978 --- /dev/null +++ b/src/test/unit/car-detection.test.ts @@ -0,0 +1,58 @@ +import { createWriteStream } from 'node:fs' +import { unlink } from 'node:fs/promises' +import { join } from 'node:path' +import { CarWriter } from '@ipld/car' +import { describe, expect, it } from 'vitest' +import { isCarFile } from '../../utils/car-detection.js' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' + +describe('isCarFile', () => { + const tempDir = process.cwd() + const validCarPath = join(tempDir, 'valid.car') + const invalidCarPath = join(tempDir, 'invalid.car') + const textFilePath = join(tempDir, 'text.txt') + + it('should return true for a valid CAR file', async () => { + // Create a valid CAR file + const { out, writer } = await CarWriter.create([ + CID.create(1, raw.code, await sha256.digest(new Uint8Array([1, 2, 3]))) + ]) + const { Readable } = await import('node:stream') + const stream = createWriteStream(validCarPath) + await new Promise((resolve, reject) => { + Readable.from(out).pipe(stream) + stream.on('error', reject) + stream.on('finish', () => resolve()) + writer.close() + }) + + expect(await isCarFile(validCarPath)).toBe(true) + }) + + it('should return false for a text file', async () => { + const stream = createWriteStream(textFilePath) + stream.write('This is just a text file') + stream.end() + await new Promise((resolve) => stream.on('finish', () => resolve())) + + expect(await isCarFile(textFilePath)).toBe(false) + }) + + it('should return false for a random binary file', async () => { + const stream = createWriteStream(invalidCarPath) + stream.write(Buffer.from([0, 1, 2, 3, 4, 5])) + stream.end() + await new Promise((resolve) => stream.on('finish', () => resolve())) + + expect(await isCarFile(invalidCarPath)).toBe(false) + }) + + // Cleanup + it('cleanup', async () => { + try { await unlink(validCarPath) } catch { } + try { await unlink(textFilePath) } catch { } + try { await unlink(invalidCarPath) } catch { } + }) +}) diff --git a/src/utils/car-detection.ts b/src/utils/car-detection.ts new file mode 100644 index 0000000..ec3eacd --- /dev/null +++ b/src/utils/car-detection.ts @@ -0,0 +1,20 @@ +import { createReadStream } from 'node:fs' +import { asyncIterableReader, createDecoder } from '@ipld/car/decoder' + +export async function isCarFile(filePath: string): Promise { + let stream: ReturnType | undefined + + try { + stream = createReadStream(filePath) + const reader = asyncIterableReader(stream) + const decoder = createDecoder(reader) + const header = await decoder.header() + return !!(header && header.version === 1 && header.roots) + } catch (error) { + return false + } finally { + if (stream) { + stream.destroy() + } + } +} \ No newline at end of file