Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@
"noPinsInProgress": "All done, no remote pins in progress.",
"remotePinningInProgress": "Remote pinning in progress:",
"selectAllEntries": "Select all entries",
"syncFromPins": {
"title": "Sync from Pins",
"description": "Import your pinned files into the Files view so you can see and manage them alongside your other files.",
"note": "This will copy pinned files to MFS (Mutable File System) so they appear in your Files view. The original pins will remain unchanged.",
"syncSelected": "Sync {{count}} files"
},
"loadingPinDetails": "Loading pin details...",
"syncing": "Syncing...",
"pins": "pins",
"selectAll": "Select all",
"previewNotFound": {
"title": "IPFS can't find this item",
"helpTitle": "These are common troubleshooting steps might help:",
Expand Down
48 changes: 48 additions & 0 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,54 @@ const actions = () => ({
}
}),

/**
* Syncs selected pinned files to MFS at the given root path.
* @param {string[]} selectedPins - Array of CID strings to sync
* @param {string} root - Destination directory in MFS
*/
doSyncFromPins: (selectedPins, root) => perform(ACTIONS.SYNC_FROM_PINS, async (/** @type {IPFSService} */ ipfs, { store }) => {
ensureMFS(store)

const results = []
const errors = []

for (const pinCid of selectedPins) {
try {
const cid = CID.parse(pinCid)
const src = `/ipfs/${cid}`
const dst = realMfsPath(join(root || '/files', `pinned-${cid.toString().substring(0, 8)}`))

// Check if destination already exists
let dstExists = false
try {
await ipfs.files.stat(dst)
dstExists = true
} catch {
// Destination doesn't exist, we can proceed
}

if (dstExists) {
// Try with a different name
const timestamp = Date.now()
const newDst = realMfsPath(join(root || '/files', `pinned-${cid.toString().substring(0, 8)}-${timestamp}`))
await ipfs.files.cp(src, newDst)
results.push({ cid: pinCid, path: newDst, success: true })
} else {
await ipfs.files.cp(src, dst)
results.push({ cid: pinCid, path: dst, success: true })
}
} catch (error) {
console.error(`Error syncing pin ${pinCid}:`, error)
errors.push({ cid: pinCid, error: error instanceof Error ? error.message : String(error) })
}
}

// Refresh the files view
await store.doFilesFetch()

return { results, errors }
}),

/**
* Reads a text file containing CIDs and adds each one to IPFS at the given root path.
* @param {FileStream[]} source - The text file containing CIDs
Expand Down
2 changes: 2 additions & 0 deletions src/bundles/files/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const ACTIONS = {
ADD_CAR_FILE: ('FILES_ADD_CAR'),
/** @type {'FILES_BULK_CID_IMPORT'} */
BULK_CID_IMPORT: ('FILES_BULK_CID_IMPORT'),
/** @type {'FILES_SYNC_FROM_PINS'} */
SYNC_FROM_PINS: ('FILES_SYNC_FROM_PINS'),
/** @type {'FILES_PIN_ADD'} */
PIN_ADD: ('FILES_PIN_ADD'),
/** @type {'FILES_PIN_REMOVE'} */
Expand Down
1 change: 1 addition & 0 deletions src/bundles/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const createFilesBundle = () => {
case ACTIONS.MOVE:
case ACTIONS.COPY:
case ACTIONS.MAKE_DIR:
case ACTIONS.SYNC_FROM_PINS:
return updateJob(state, action.task, action.type)
case ACTIONS.PIN_ADD:
case ACTIONS.PIN_REMOVE: {
Expand Down
1 change: 1 addition & 0 deletions src/bundles/files/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type Message =
| Perform<'FILES_PIN_ADD', Error, Pin[], void>
| Perform<'FILES_PIN_REMOVE', Error, Pin[], void>
| Perform<'FILES_PIN_LIST', Error, { pins: CID[] }, void>
| Perform<'FILES_SYNC_FROM_PINS', Error, { results: Array<{ cid: string, path: string, success: boolean }>, errors: Array<{ cid: string, error: string }> }, void>
| Perform<'FILES_SIZE_GET', Error, { size: number }, void>
| Perform<'FILES_PINS_SIZE_GET', Error, { pinsSize: number, numberOfPins: number }, void>

Expand Down
12 changes: 12 additions & 0 deletions src/bundles/files/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ const selectors = () => ({
*/
selectFilesHasError: (state) => state.files.failed.length > 0,

/**
* @param {Model} state
*/
selectSyncFromPinsPending: (state) =>
state.files.pending.filter(s => s.type === ACTIONS.SYNC_FROM_PINS),

/**
* @param {Model} state
*/
selectSyncFromPinsFinished: (state) =>
state.files.finished.filter(s => s.type === ACTIONS.SYNC_FROM_PINS),

/**
* @param {Model} state
*/
Expand Down
15 changes: 12 additions & 3 deletions src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getJoyrideLocales } from '../helpers/i8n.js'
import SortDropdown from './sort-dropdown/SortDropdown.js'

// Icons
import Modals, { DELETE, NEW_FOLDER, SHARE, ADD_BY_CAR, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, SHORTCUTS, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
import Modals, { DELETE, NEW_FOLDER, SHARE, ADD_BY_CAR, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, SHORTCUTS, CLI_TUTOR_MODE, PINNING, PUBLISH, SYNC_FROM_PINS } from './modals/Modals.js'

import Header from './header/Header.js'
import FileImportStatus from './file-import-status/FileImportStatus.js'
Expand All @@ -31,8 +31,8 @@ import Checkbox from '../components/checkbox/Checkbox.js'
const FilesPage = ({
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doAddCarFile, doFilesBulkCidImport, doFilesAddPath, doUpdateHash,
doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins,
ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesCidProvide, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey,
files, filesPathInfo, filesIsFetching, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, filesSorting, t
ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesCopyCidProvide, doFilesCidProvide, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey,
files, filesPathInfo, filesSorting, filesIsFetching, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, pins, doSyncFromPins, t
}) => {
const { doExploreUserProvidedPath } = useExplore()
const contextMenuRef = useRef()
Expand Down Expand Up @@ -130,6 +130,10 @@ const FilesPage = ({
const onAddByCar = (file, name) => {
doAddCarFile(files.path, file, name)
}

const onSyncFromPins = (selectedPins) => {
doSyncFromPins(selectedPins, files.path)
}
const onInspect = (cid) => doUpdateHash(`/explore/${cid}`)
const onCheckRetrieval = (cid) => {
doFilesCidProvide(cid) // Trigger background provide
Expand Down Expand Up @@ -338,6 +342,7 @@ const FilesPage = ({
onAddByPath={(files) => showModal(ADD_BY_PATH, files)}
onAddByCar={(files) => showModal(ADD_BY_CAR, files)}
onBulkCidImport={(files) => showModal(BULK_CID_IMPORT, files)}
onSyncFromPins={() => showModal(SYNC_FROM_PINS)}
onNewFolder={(files) => showModal(NEW_FOLDER, files)}
onCliTutorMode={() => showModal(CLI_TUTOR_MODE)}
handleContextMenu={(...args) => handleContextMenu(...args, true)}
Expand Down Expand Up @@ -405,7 +410,9 @@ const FilesPage = ({
onAddByPath={onAddByPath}
onAddByCar={onAddByCar}
onBulkCidImport={onBulkCidImport}
onSyncFromPins={onSyncFromPins}
onPinningSet={doSetPinning}
pins={pins}
onPublish={doPublishIpnsKey}
cliOptions={cliOptions}
{ ...modals } />
Expand Down Expand Up @@ -470,5 +477,7 @@ export default connect(
'selectCliOptions',
'doSetPinning',
'doPublishIpnsKey',
'selectPins',
'doSyncFromPins',
withTour(withTranslation('files')(FilesPage))
)
13 changes: 12 additions & 1 deletion src/files/file-input/FileInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FolderIcon from '../../icons/StrokeFolder.js'
import NewFolderIcon from '../../icons/StrokeNewFolder.js'
import DecentralizationIcon from '../../icons/StrokeDecentralization.js'
import DataIcon from '../../icons/StrokeData.js'
import GlyphPinCloud from '../../icons/GlyphPinCloud.js'
// Components
import { Dropdown, DropdownMenu, Option } from '../dropdown/Dropdown.js'
import Button from '../../components/button/button.tsx'
Expand Down Expand Up @@ -61,6 +62,11 @@ class FileInput extends React.Component {
this.toggleDropdown()
}

onSyncFromPins = () => {
this.props.onSyncFromPins()
this.toggleDropdown()
}

onNewFolder = () => {
this.props.onNewFolder()
this.toggleDropdown()
Expand Down Expand Up @@ -112,6 +118,10 @@ class FileInput extends React.Component {
<DocumentIcon className='fill-aqua w2 mr1' />
{t('bulkImport')}
</Option>
<Option onClick={this.onSyncFromPins} id='sync-from-pins'>
<GlyphPinCloud className='fill-aqua w2 mr1' />
{t('syncFromPins.title')}
</Option>
</DropdownMenu>
</Dropdown>

Expand Down Expand Up @@ -142,7 +152,8 @@ FileInput.propTypes = {
onAddByPath: PropTypes.func.isRequired,
onAddByCar: PropTypes.func.isRequired,
onBulkCidImport: PropTypes.func.isRequired,
onNewFolder: PropTypes.func.isRequired
onNewFolder: PropTypes.func.isRequired,
onSyncFromPins: PropTypes.func.isRequired
}

export default connect(
Expand Down
1 change: 1 addition & 0 deletions src/files/header/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Header extends React.Component {
onAddByPath={this.props.onAddByPath}
onAddByCar={this.props.onAddByCar}
onBulkCidImport={this.props.onBulkCidImport}
onSyncFromPins={this.props.onSyncFromPins}
onCliTutorMode={this.props.onCliTutorMode}
/>
: <div ref={el => { this.dotsWrapper = el }}>
Expand Down
25 changes: 23 additions & 2 deletions src/files/modals/Modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import CliTutorMode from '../../components/cli-tutor-mode/CliTutorMode.js'
import { cliCommandList, cliCmdKeys } from '../../bundles/files/consts.js'
import { realMfsPath } from '../../bundles/files/actions.js'
import AddByCarModal from './add-by-car-modal/AddByCarModal.js'
import SyncFromPinsModal from './sync-from-pins-modal/SyncFromPinsModal.js'
// Constants
const NEW_FOLDER = 'new_folder'
const SHARE = 'share'
Expand All @@ -29,6 +30,7 @@ const CLI_TUTOR_MODE = 'cli_tutor_mode'
const PINNING = 'pinning'
const PUBLISH = 'publish'
const SHORTCUTS = 'shortcuts'
const SYNC_FROM_PINS = 'sync_from_pins'

export {
NEW_FOLDER,
Expand All @@ -41,7 +43,8 @@ export {
CLI_TUTOR_MODE,
PINNING,
PUBLISH,
SHORTCUTS
SHORTCUTS,
SYNC_FROM_PINS
}

class Modals extends React.Component {
Expand Down Expand Up @@ -82,6 +85,11 @@ class Modals extends React.Component {
this.leave()
}

onSyncFromPins = (selectedPins) => {
this.props.onSyncFromPins(selectedPins)
this.leave()
}

makeDir = (path) => {
this.props.onMakeDir(join(this.props.root, path))
this.leave()
Expand Down Expand Up @@ -199,6 +207,9 @@ class Modals extends React.Component {
case SHORTCUTS:
this.setState({ readyToShow: true })
break
case SYNC_FROM_PINS:
this.setState({ readyToShow: true })
break
default:
// do nothing
}
Expand Down Expand Up @@ -317,6 +328,14 @@ class Modals extends React.Component {
className='outline-0'
onLeave={this.leave} />
</Overlay>

<Overlay show={show === SYNC_FROM_PINS && readyToShow} onLeave={this.leave}>
<SyncFromPinsModal
className='outline-0'
pins={this.props.pins || []}
onCancel={this.leave}
onSync={this.onSyncFromPins} />
</Overlay>
</div>
)
}
Expand All @@ -326,13 +345,15 @@ Modals.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.string,
files: PropTypes.array,
pins: PropTypes.array,
onAddByPath: PropTypes.func.isRequired,
onAddByCar: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onMakeDir: PropTypes.func.isRequired,
onShareLink: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onPublish: PropTypes.func.isRequired
onPublish: PropTypes.func.isRequired,
onSyncFromPins: PropTypes.func.isRequired
}

export default withTranslation('files')(Modals)
Loading