diff --git a/public/locales/en/files.json b/public/locales/en/files.json
index c44340915..2e6f6c4ab 100644
--- a/public/locales/en/files.json
+++ b/public/locales/en/files.json
@@ -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:",
diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js
index 91e991067..c506e3a8f 100644
--- a/src/bundles/files/actions.js
+++ b/src/bundles/files/actions.js
@@ -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
diff --git a/src/bundles/files/consts.js b/src/bundles/files/consts.js
index 7d91a787b..857c08d58 100644
--- a/src/bundles/files/consts.js
+++ b/src/bundles/files/consts.js
@@ -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'} */
diff --git a/src/bundles/files/index.js b/src/bundles/files/index.js
index 6d073c472..17aa1b8a2 100644
--- a/src/bundles/files/index.js
+++ b/src/bundles/files/index.js
@@ -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: {
diff --git a/src/bundles/files/protocol.ts b/src/bundles/files/protocol.ts
index 90c9f450b..15d430a49 100644
--- a/src/bundles/files/protocol.ts
+++ b/src/bundles/files/protocol.ts
@@ -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>
diff --git a/src/bundles/files/selectors.js b/src/bundles/files/selectors.js
index 2806c6384..2c7ad4540 100644
--- a/src/bundles/files/selectors.js
+++ b/src/bundles/files/selectors.js
@@ -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
*/
diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js
index 64d70973c..f42a380d2 100644
--- a/src/files/FilesPage.js
+++ b/src/files/FilesPage.js
@@ -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'
@@ -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()
@@ -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
@@ -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)}
@@ -405,7 +410,9 @@ const FilesPage = ({
onAddByPath={onAddByPath}
onAddByCar={onAddByCar}
onBulkCidImport={onBulkCidImport}
+ onSyncFromPins={onSyncFromPins}
onPinningSet={doSetPinning}
+ pins={pins}
onPublish={doPublishIpnsKey}
cliOptions={cliOptions}
{ ...modals } />
@@ -470,5 +477,7 @@ export default connect(
'selectCliOptions',
'doSetPinning',
'doPublishIpnsKey',
+ 'selectPins',
+ 'doSyncFromPins',
withTour(withTranslation('files')(FilesPage))
)
diff --git a/src/files/file-input/FileInput.js b/src/files/file-input/FileInput.js
index 5f654740a..9e43248b5 100644
--- a/src/files/file-input/FileInput.js
+++ b/src/files/file-input/FileInput.js
@@ -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'
@@ -61,6 +62,11 @@ class FileInput extends React.Component {
this.toggleDropdown()
}
+ onSyncFromPins = () => {
+ this.props.onSyncFromPins()
+ this.toggleDropdown()
+ }
+
onNewFolder = () => {
this.props.onNewFolder()
this.toggleDropdown()
@@ -112,6 +118,10 @@ class FileInput extends React.Component {
{t('syncFromPins.description')}
+{t('syncFromPins.note')}
+