Skip to content

Commit e66590f

Browse files
badgooooorlidel2colorSgtPooki
authored
feat(files): CAR Import (#2323)
* feat: add dropdown menu for import car * feat: add modal for import car * locale: add en locale * refactor,feat: change to functional component, add check file, rename file and validate file name * feat: wiring handling uploading car dag file * locale: add placeholder * feat: add import car file * refactor: clear unused params * update dependency * refactor: cleanup modal * feat: update types * feat: wip on dag import * feat: add util * feat: fix parse file for dag import * docs: Add AddByCarModal as storybook * fix: fix typecheck error * fix: revert back * feat: update types of IPFSService * feat: revert types to use modified type * refactor: remove unused * refactor: move definition from imports to local * refactor: change to pass stream to dag import * refactor: remove unused util * typo: Update wording * typo: Update wording in select file button * type: Update i18n, use wording for title in i18n * fix: fix logic on file name * fix: fix import to relative path within mfs * fix: icon, label, and styling making UI match bulk import feature * docs: cli tutor for cat import + unifying import/export language * chore: throw helpful error if name exists * fix: dont throw error when importing car if destination doesnt exist * fix: swallow error when file doesn't exist * chore: remove unnecessary changes * chore: use kubo-rpc-client types for IPFSService * chore: minor ux fixes * fix: dont error if path doesn't exist * fix: remove duplicate catch block --------- Co-authored-by: Marcin Rataj <[email protected]> Co-authored-by: Daniel N <[email protected]> Co-authored-by: Russell Dempsey <[email protected]>
1 parent 493223a commit e66590f

File tree

14 files changed

+536
-27
lines changed

14 files changed

+536
-27
lines changed

@types/ipfs/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ declare module 'ipfs' {
22
import type { CID } from 'multiformats/cid'
33
import type { Multiaddr } from '@multiformats/multiaddr'
44
import type { Buffer } from 'buffer'
5+
import type { KuboRPCClient } from 'kubo-rpc-client'
56

67
declare export interface IPFSService extends CoreService {
78
pin: PinService;
89
files: FileService;
910
name: NameService;
1011
object: ObjectService;
1112
config: ConfigService;
13+
dag: KuboRPCClient['dag'];
1214

1315
stop(options?: TimeoutOptions): Promise<void>
1416
}

package-lock.json

Lines changed: 299 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"it-first": "^1.0.6",
7474
"it-last": "^1.0.5",
7575
"it-map": "^1.0.5",
76-
"kubo-rpc-client": "^4.1.1",
76+
"kubo-rpc-client": "^5.0.2",
7777
"milliseconds": "^1.0.3",
7878
"money-clip": "^3.0.5",
7979
"multiformats": "^13.0.1",

public/locales/en/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"unselectAll": "Unselect all",
3131
"generate": "Generate",
3232
"publish": "Publish",
33-
"downloadCar": "Download as CAR",
33+
"downloadCar": "Export CAR",
3434
"done": "Done"
3535
},
3636
"cliModal": {

public/locales/en/files.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway</1> or <3>public gateway</3>.",
1515
"cantBePreviewed": "Sorry, this file can’t be previewed",
1616
"addByPath": "From IPFS",
17+
"addByCar": "From CAR",
1718
"bulkImport": "Bulk import",
1819
"newFolder": "New folder",
1920
"generating": "Generating…",
@@ -60,6 +61,13 @@
6061
"namePlaceholder": "Name (optional)",
6162
"examples": "Examples:"
6263
},
64+
"addByCarModal": {
65+
"title": "CAR Import",
66+
"description": "Choose a CAR file to import a DAG from and specify a name for it to be imported into the current directory.",
67+
"selectCARButtonText": "Select CAR…",
68+
"namePlaceholder": "Name",
69+
"renameImportPath": "Rename import"
70+
},
6371
"bulkImportModal": {
6472
"title": "Bulk import with text file",
6573
"description": "Upload a text file with a list of CIDs (names are optional). Example:",

src/bundles/files/actions.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,56 @@ const actions = () => ({
399399
}),
400400

401401
/**
402-
* Reads a text file containing CIDs and adds each one to IPFS at the given root path.
403-
* @param {FileStream[]} source - The text file containing CIDs
404-
* @param {string} root - Destination directory in IPFS
405-
*/
402+
* Adds CAR file. On completion will trigger `doFilesFetch` to update the state.
403+
* @param {string} root
404+
* @param {FileStream} carFile
405+
* @param {string} name
406+
*/
407+
doAddCarFile: (root, carFile, name = '') => perform(ACTIONS.ADD_CAR_FILE, async (/** @type {IPFSService} */ ipfs, { store }) => {
408+
ensureMFS(store)
409+
410+
const stream = carFile.content.stream()
411+
try {
412+
// @ts-expect-error - https://github.com/ipfs/js-kubo-rpc-client/issues/278
413+
const result = await all(ipfs.dag.import(stream, {
414+
pinRoots: true
415+
}))
416+
const cid = result[0].root.cid
417+
const src = `/ipfs/${cid}`
418+
const dst = realMfsPath(join(root, name))
419+
let dstExists = false
420+
421+
// Check if destination path already exists
422+
await ipfs.files.stat(dst).then(() => {
423+
dstExists = true
424+
}).catch(() => {
425+
// Swallow error. We can add the file to the dst path
426+
})
427+
428+
if (dstExists) {
429+
throw new Error(`The name "${name}" already exists in the current directory. Try importing with a different name.`)
430+
}
431+
432+
try {
433+
await ipfs.files.cp(src, dst)
434+
} catch (/** @type {any} */ err) {
435+
// TODO: Not sure why we do this. Perhaps a generic error is used
436+
// to avoid leaking private information via Countly?
437+
throw Object.assign(new Error('ipfs.files.cp call failed'), {
438+
code: 'ERR_FILES_CP_FAILED'
439+
})
440+
}
441+
return carFile
442+
} finally {
443+
await store.doFilesFetch()
444+
}
445+
}),
446+
447+
/**
448+
* Reads a text file containing CIDs and adds each one to IPFS at the given root path.
449+
* @param {FileStream[]} source - The text file containing CIDs
450+
* @param {string} root - Destination directory in IPFS
451+
*/
406452
doFilesBulkCidImport: (source, root) => perform(ACTIONS.BULK_CID_IMPORT, async function (ipfs, { store }) {
407453
ensureMFS(store)
408454

src/bundles/files/consts.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const ACTIONS = {
2323
SHARE_LINK: ('FILES_SHARE_LINK'),
2424
/** @type {'FILES_ADDBYPATH'} */
2525
ADD_BY_PATH: ('FILES_ADDBYPATH'),
26+
/** @type {'FILES_ADD_CAR'} */
27+
ADD_CAR_FILE: ('FILES_ADD_CAR'),
2628
/** @type {'FILES_BULK_CID_IMPORT'} */
2729
BULK_CID_IMPORT: ('FILES_BULK_CID_IMPORT'),
2830
/** @type {'FILES_PIN_ADD'} */
@@ -80,6 +82,7 @@ export const cliCmdKeys = {
8082
ADD_DIRECTORY: 'addNewDirectory',
8183
CREATE_NEW_DIRECTORY: 'createNewDirectory',
8284
FROM_IPFS: 'fromIpfs',
85+
FROM_CAR: 'fromCar',
8386
ADD_NEW_PEER: 'addNewPeer',
8487
PUBLISH_WITH_IPNS: 'publishWithIPNS',
8588
DOWNLOAD_CAR_COMMAND: 'downloadCarCommand'
@@ -128,6 +131,10 @@ export const cliCommandList = {
128131
* @param {string} path
129132
*/
130133
[cliCmdKeys.FROM_IPFS]: (path) => `ipfs files cp /ipfs/<cid> "${path}/<dest-name>"`,
134+
/**
135+
* @param {string} path
136+
*/
137+
[cliCmdKeys.FROM_CAR]: (path) => `ipfs dag import file.car && ipfs files cp /ipfs/<imported-cid> "${path}/<dest-name>"`,
131138
[cliCmdKeys.ADD_NEW_PEER]: () => 'ipfs swarm connect <peer-multiaddr>',
132139
/**
133140
* @param {string} ipfsPath

src/bundles/files/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Task from '../task.js'
44

55
/**
66
* @typedef {import('ipfs').IPFSService} IPFSService
7+
* @typedef {import('../../lib/files').FileStream} FileStream
78
* @typedef {import('./actions').Ext} Ext
89
* @typedef {import('./actions').Extra} Extra
910
*/

src/files/FilesPage.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import FilesList from './files-list/FilesList.js'
1515
import { getJoyrideLocales } from '../helpers/i8n.js'
1616

1717
// Icons
18-
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
18+
import Modals, { DELETE, NEW_FOLDER, ADD_BY_CAR, SHARE, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
1919
import Header from './header/Header.js'
2020
import FileImportStatus from './file-import-status/FileImportStatus.js'
2121
import { useExplore } from 'ipld-explorer-components/providers'
2222

2323
const FilesPage = ({
24-
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doFilesBulkCidImport, doFilesAddPath, doUpdateHash,
24+
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doAddCarFile, doFilesBulkCidImport, doFilesAddPath, doUpdateHash,
2525
doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins,
2626
ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey,
2727
files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t
@@ -79,6 +79,14 @@ const FilesPage = ({
7979
}
8080

8181
const onAddByPath = (path, name) => doFilesAddPath(files.path, path, name)
82+
/**
83+
*
84+
* @param {File} file
85+
* @param {string} name
86+
*/
87+
const onAddByCar = (file, name) => {
88+
doAddCarFile(files.path, file, name)
89+
}
8290
const onInspect = (cid) => doUpdateHash(`/explore/${cid}`)
8391
const showModal = (modal, files = null) => setModals({ show: modal, files })
8492
const hideModal = () => setModals({})
@@ -212,6 +220,7 @@ const FilesPage = ({
212220
onAddFiles={onAddFiles}
213221
onMove={doFilesMove}
214222
onAddByPath={(files) => showModal(ADD_BY_PATH, files)}
223+
onAddByCar={(files) => showModal(ADD_BY_CAR, files)}
215224
onBulkCidImport={(files) => showModal(BULK_CID_IMPORT, files)}
216225
onNewFolder={(files) => showModal(NEW_FOLDER, files)}
217226
onCliTutorMode={() => showModal(CLI_TUTOR_MODE)}
@@ -233,6 +242,7 @@ const FilesPage = ({
233242
onShareLink={doFilesShareLink}
234243
onRemove={doFilesDelete}
235244
onAddByPath={onAddByPath}
245+
onAddByCar={onAddByCar}
236246
onBulkCidImport={onBulkCidImport}
237247
onPinningSet={doSetPinning}
238248
onPublish={doPublishIpnsKey}
@@ -280,6 +290,7 @@ export default connect(
280290
'doFilesShareLink',
281291
'doFilesDelete',
282292
'doFilesAddPath',
293+
'doAddCarFile',
283294
'doFilesNavigateTo',
284295
'doFilesUpdateSorting',
285296
'selectFilesSorting',

src/files/file-input/FileInput.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import DocumentIcon from '../../icons/StrokeDocument.js'
88
import FolderIcon from '../../icons/StrokeFolder.js'
99
import NewFolderIcon from '../../icons/StrokeNewFolder.js'
1010
import DecentralizationIcon from '../../icons/StrokeDecentralization.js'
11+
import DataIcon from '../../icons/StrokeData.js'
1112
// Components
1213
import { Dropdown, DropdownMenu, Option } from '../dropdown/Dropdown.js'
1314
import Button from '../../components/button/button.tsx'
@@ -50,6 +51,11 @@ class FileInput extends React.Component {
5051
this.toggleDropdown()
5152
}
5253

54+
onAddByCar = () => {
55+
this.props.onAddByCar()
56+
this.toggleDropdown()
57+
}
58+
5359
onBulkCidImport = () => {
5460
this.props.onBulkCidImport()
5561
this.toggleDropdown()
@@ -87,15 +93,20 @@ class FileInput extends React.Component {
8793
<FolderIcon className='fill-aqua w2 mr1' />
8894
{t('app:terms.folder')}
8995
</Option>
96+
<Option onClick={this.onNewFolder} id='add-new-folder' onCliTutorMode={() => this.onCliTutorMode(cliCmdKeys.CREATE_NEW_DIRECTORY)}
97+
isCliTutorModeEnabled={isCliTutorModeEnabled}>
98+
<NewFolderIcon className='fill-aqua w2 h2 mr1' />
99+
{t('newFolder')}
100+
</Option>
90101
<Option onClick={this.onAddByPath} id='add-by-path' onCliTutorMode={() => this.onCliTutorMode(cliCmdKeys.FROM_IPFS)}
91102
isCliTutorModeEnabled={isCliTutorModeEnabled}>
92103
<DecentralizationIcon className='fill-aqua w2 mr1' />
93104
{t('addByPath')}
94105
</Option>
95-
<Option onClick={this.onNewFolder} id='add-new-folder' onCliTutorMode={() => this.onCliTutorMode(cliCmdKeys.CREATE_NEW_DIRECTORY)}
106+
<Option onClick={this.onAddByCar} id='add-by-car' onCliTutorMode={() => this.onCliTutorMode(cliCmdKeys.FROM_CAR)}
96107
isCliTutorModeEnabled={isCliTutorModeEnabled}>
97-
<NewFolderIcon className='fill-aqua w2 h2 mr1' />
98-
{t('newFolder')}
108+
<DataIcon className='fill-aqua w2 mr1' />
109+
{t('addByCar')}
99110
</Option>
100111
<Option onClick={this.onBulkCidImport} id='bulk-cid-import'>
101112
<DocumentIcon className='fill-aqua w2 mr1' />
@@ -129,8 +140,9 @@ FileInput.propTypes = {
129140
t: PropTypes.func.isRequired,
130141
onAddFiles: PropTypes.func.isRequired,
131142
onAddByPath: PropTypes.func.isRequired,
132-
onNewFolder: PropTypes.func.isRequired,
133-
onBulkCidImport: PropTypes.func.isRequired
143+
onAddByCar: PropTypes.func.isRequired,
144+
onBulkCidImport: PropTypes.func.isRequired,
145+
onNewFolder: PropTypes.func.isRequired
134146
}
135147

136148
export default connect(

0 commit comments

Comments
 (0)