import fs, { ReadStream } from 'fs'; import path from 'path'; import { IPFS } from 'ipfs-core-types'; import { CID, IPFSHTTPClient } from 'ipfs-http-client'; import invariant from 'ts-invariant'; import { generatorToArray } from '~/utils/generatorToArray'; import { FileNotFoundError } from './errors'; type IPFSAddAllOptions = Parameters[1]; export class UploadedMediaBundle { constructor( public readonly cid: CID, private readonly srcDstMap: Map, ) {} getPathForSrc(srcPath: string): string | undefined { return this.srcDstMap.get(srcPath); } getUriForSrc(srcPath: string): string | undefined { const path = this.getPathForSrc(srcPath); if (!path) { return undefined; } const cidHashV0 = this.cid.toV0().toString(); return new URL(path, `ipfs://${cidHashV0}`).toString(); } } export class MediaBundle { private srcDstMap = new Map(); addFileByStem(src: string, filenameStem: string): string { const dst = `${filenameStem}${path.extname(src)}`; return this.addFile(src, dst); } addFile(src: string, proposedDst: string): string { if (!fs.existsSync(src)) { throw new FileNotFoundError(`addFile: file does not exist: ${src}`); } let actualDst = this.srcDstMap.get(src); invariant( typeof actualDst === 'undefined' || typeof actualDst === 'string', 'dstFilename must be a string', ); if (typeof actualDst === 'undefined') { actualDst = proposedDst; this.srcDstMap.set(src, actualDst); } return actualDst; } async upload( ipfsClient: IPFSHTTPClient, addAllOptions?: IPFSAddAllOptions, ): Promise { const options: IPFSAddAllOptions = { ...addAllOptions, cidVersion: 0, wrapWithDirectory: true, }; const results = await generatorToArray( ipfsClient.addAll(this.getFiles(), options), ); const rootCID = results.find(({ path }) => path === '')?.cid; invariant( typeof rootCID !== 'undefined', 'ipfsClient.addAll expected to contain a root CID', ); return new UploadedMediaBundle(rootCID, this.srcDstMap); } private *getFiles(): Generator<{ path: string; content: ReadStream }> { for (const [src, path] of this.srcDstMap.entries()) { yield { path, content: fs.createReadStream(src), }; } } }