diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index 55b80cfa5..73c506429 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -399,6 +399,54 @@ const actions = () => ({ } }), + /** + * Reads a CSV file containing CIDs and adds each one to IPFS at the given root path. + * @param {FileStream[]} source - The CSV file containing CIDs + * @param {string} root - Destination directory in IPFS + */ + doFilesAddBulkCid: (source, root) => spawn(ACTIONS.ADD_BY_PATH, async function * (ipfs, { store }) { + ensureMFS(store) + + if (source.length !== 1) { + throw new Error('Please provide exactly one CSV file') + } + + // Read the CSV file content + const file = source[0] + const content = await new Response(file.content).text() + + // Split content into CIDs (assuming one CID per line, comma-separated) + const cids = content.split(/[\n,]/).map(cid => cid.trim()).filter(Boolean) + + /** @type {Array<{ path: string, cid: string }>} */ + const entries = [] + let progress = 0 + const totalCids = cids.length + + yield { entries, progress: 0 } + + for (const cid of cids) { + try { + const src = `/ipfs/${cid}` + const dst = realMfsPath(join(root || '/files', cid)) + + await ipfs.files.cp(src, dst) + + entries.push({ path: dst, cid }) + progress = (entries.length / totalCids) * 100 + + yield { entries, progress } + } catch (err) { + console.error(`Failed to add CID ${cid}:`, err) + // Continue with next CID even if one fails + } + } + + yield { entries, progress: 100 } + await store.doFilesFetch() + return entries + }), + /** * Creates a download link for the provided files. * @param {FileStat[]} files diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index d9a837330..ccad9f994 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -21,7 +21,7 @@ import FileImportStatus from './file-import-status/FileImportStatus.js' import { useExplore } from 'ipld-explorer-components/providers' const FilesPage = ({ - doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doFilesAddPath, doUpdateHash, + doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doFilesAddBulkCid, doFilesAddPath, doUpdateHash, doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins, ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey, files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t @@ -72,6 +72,11 @@ const FilesPage = ({ doFilesWrite(raw, root) } + const onAddBulkCid = (raw, root = '') => { + if (root === '') root = files.path + doFilesAddBulkCid(raw, root) + } + const onAddByPath = (path, name) => doFilesAddPath(files.path, path, name) const onInspect = (cid) => doUpdateHash(`/explore/${cid}`) const showModal = (modal, files = null) => setModals({ show: modal, files }) @@ -204,6 +209,7 @@ const FilesPage = ({ files={files} onNavigate={doFilesNavigateTo} onAddFiles={onAddFiles} + onAddBulkCid={onAddBulkCid} onMove={doFilesMove} onAddByPath={(files) => showModal(ADD_BY_PATH, files)} onNewFolder={(files) => showModal(NEW_FOLDER, files)} @@ -277,6 +283,7 @@ export default connect( 'selectFilesSorting', 'selectToursEnabled', 'doFilesWrite', + 'doFilesAddBulkCid', 'doFilesDownloadLink', 'doFilesDownloadCarLink', 'doFilesSizeGet', diff --git a/src/files/file-input/FileInput.js b/src/files/file-input/FileInput.js index ec9bfb6e6..6a1ba964f 100644 --- a/src/files/file-input/FileInput.js +++ b/src/files/file-input/FileInput.js @@ -40,11 +40,21 @@ class FileInput extends React.Component { return this.filesInput.click() } + onAddBulkCid = async () => { + this.toggleDropdown() + return this.bulkCidInput.click() + } + onInputChange = (input) => async () => { this.props.onAddFiles(normalizeFiles(input.files)) input.value = null } + onBulkCidInputChange = (input) => async () => { + this.props.onAddBulkCid(normalizeFiles(input.files)) + input.value = null + } + onAddByPath = () => { this.props.onAddByPath() this.toggleDropdown() @@ -92,8 +102,12 @@ class FileInput extends React.Component { {t('newFolder')} - @@ -116,6 +130,15 @@ class FileInput extends React.Component { webkitdirectory='true' ref={el => { this.folderInput = el }} onChange={this.onInputChange(this.folderInput)} /> + + { this.bulkCidInput = el }} + onChange={this.onBulkCidInputChange(this.bulkCidInput)} /> ) } @@ -125,7 +148,8 @@ FileInput.propTypes = { t: PropTypes.func.isRequired, onAddFiles: PropTypes.func.isRequired, onAddByPath: PropTypes.func.isRequired, - onNewFolder: PropTypes.func.isRequired + onNewFolder: PropTypes.func.isRequired, + onAddBulkCid: PropTypes.func.isRequired } export default connect( diff --git a/src/files/header/Header.js b/src/files/header/Header.js index ce9142109..a1df8f537 100644 --- a/src/files/header/Header.js +++ b/src/files/header/Header.js @@ -93,6 +93,7 @@ class Header extends React.Component { onNewFolder={this.props.onNewFolder} onAddFiles={this.props.onAddFiles} onAddByPath={this.props.onAddByPath} + onAddBulkCid={this.props.onAddBulkCid} onCliTutorMode={this.props.onCliTutorMode} /> :
{ this.dotsWrapper = el }}>