diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2288c5a35..bab8bb1eb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,7 +11,8 @@ module.exports = { // ignore .ts files because it fails to parse it. ignorePatterns: 'src/**/*.ts', rules: { - 'react/prop-types': [0, { ignore: ['className'], customValidators: [], skipUndeclared: true }] // TODO: set this rule to error when all issues are resolved. + 'react/prop-types': [0, { ignore: ['className'], customValidators: [], skipUndeclared: true }], // TODO: set this rule to error when all issues are resolved. + 'no-void': 'off' }, overrides: [ { diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index 17135de0f..e82cc7ceb 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -9,7 +9,7 @@ import map from 'it-map' import last from 'it-last' import { CID } from 'multiformats/cid' -import { spawn, perform, send, ensureMFS, Channel, sortFiles, infoFromPath } from './utils.js' +import { spawn, perform, send, ensureMFS, Channel, sortFiles, infoFromPath, dispatchAsyncProvide } from './utils.js' import { IGNORED_FILES, ACTIONS } from './consts.js' /** @@ -523,14 +523,23 @@ const actions = () => ({ }), /** - * Generates sharable link for the provided files. - * @param {FileStat[]} files + * Triggers provide operation for a copied CID. + * @param {import('multiformats/cid').CID} cid */ - doFilesShareLink: (files) => perform(ACTIONS.SHARE_LINK, async (ipfs, { store }) => { + doFilesCopyCidProvide: (cid) => perform('FILES_COPY_CID_PROVIDE', async (ipfs) => { + dispatchAsyncProvide(cid, ipfs, 'COPY') + }), + + doFilesShareLink: (/** @type {FileStat[]} */ files) => perform(ACTIONS.SHARE_LINK, async (ipfs, { store }) => { // ensureMFS deliberately omitted here, see https://github.com/ipfs/ipfs-webui/issues/1744 for context. const publicGateway = store.selectPublicGateway() const publicSubdomainGateway = store.selectPublicSubdomainGateway() - return getShareableLink(files, publicGateway, publicSubdomainGateway, ipfs) + const { link: shareableLink, cid } = await getShareableLink(files, publicGateway, publicSubdomainGateway, ipfs) + + // Trigger background provide operation with the CID from getShareableLink + dispatchAsyncProvide(cid, ipfs, 'SHARE') + + return shareableLink }), /** diff --git a/src/bundles/files/consts.js b/src/bundles/files/consts.js index 49597aa06..a087a737b 100644 --- a/src/bundles/files/consts.js +++ b/src/bundles/files/consts.js @@ -60,6 +60,9 @@ export const IGNORED_FILES = [ 'desktop.ini' ] +// Maximum length for DNS labels (used for subdomain gateway CID validation) +export const DNS_LABEL_MAX_LENGTH = 63 + /** @type {Model} */ export const DEFAULT_STATE = { pageContent: null, diff --git a/src/bundles/files/utils.js b/src/bundles/files/utils.js index a980cc020..a466586e4 100644 --- a/src/bundles/files/utils.js +++ b/src/bundles/files/utils.js @@ -1,12 +1,14 @@ import { sortByName, sortBySize } from '../../lib/sort.js' import { IS_MAC, SORTING } from './consts.js' import * as Task from '../task.js' +import { debouncedProvide } from '../../lib/files.js' /** * @typedef {import('ipfs').IPFSService} IPFSService * @typedef {import('../../lib/files').FileStream} FileStream * @typedef {import('./actions').Ext} Ext * @typedef {import('./actions').Extra} Extra + * @typedef {import('multiformats/cid').CID} CID */ /** @@ -316,3 +318,19 @@ export const ensureMFS = (store) => { throw new Error('Unable to perform task if not in MFS') } } + +/** + * Dispatches an async provide operation for a CID. + * + * @param {CID|null|undefined} cid - The CID to provide + * @param {IPFSService} ipfs - The IPFS service instance + * @param {string} context - Context for logging + */ +export const dispatchAsyncProvide = (cid, ipfs, context) => { + if (cid != null) { + console.debug(`[${context}] Dispatching one-time ad-hoc provide for root CID ${cid.toString()} (non-recursive) for improved performance when sharing today`) + void debouncedProvide(cid, ipfs).catch((error) => { + console.error(`[${context}] debouncedProvide failed:`, error) + }) + } +} diff --git a/src/bundles/ipns.js b/src/bundles/ipns.js index d05b8bd1a..86c3459b8 100644 --- a/src/bundles/ipns.js +++ b/src/bundles/ipns.js @@ -1,5 +1,6 @@ import all from 'it-all' import { readSetting, writeSetting } from './local-storage.js' +import { dispatchAsyncProvide } from './files/utils.js' const init = () => ({ keys: [], @@ -61,6 +62,9 @@ const ipnsBundle = { doPublishIpnsKey: (cid, key) => async ({ getIpfs, store }) => { const ipfs = getIpfs() await ipfs.name.publish(cid, { key }) + + // Trigger background provide operation for the published CID + dispatchAsyncProvide(cid, ipfs, 'IPNS') }, doUpdateExpectedPublishTime: (time) => async ({ store, dispatch }) => { diff --git a/src/bundles/pinning.js b/src/bundles/pinning.js index 313455f20..6f323f0f2 100644 --- a/src/bundles/pinning.js +++ b/src/bundles/pinning.js @@ -5,6 +5,7 @@ import { CID } from 'multiformats/cid' import all from 'it-all' import { readSetting, writeSetting } from './local-storage.js' +import { dispatchAsyncProvide } from './files/utils.js' // This bundle leverages createCacheBundle and persistActions for // the persistence layer that keeps pins in IndexDB store @@ -361,6 +362,9 @@ const pinningBundle = { if (pinLocally) { await ipfs.pin.add(cid) dispatch({ type: 'IPFS_PIN_SUCCEED', msgArgs }) + + // Trigger background provide operation for pinned CID + dispatchAsyncProvide(cid, ipfs, 'PIN') } else { await ipfs.pin.rm(cid) dispatch({ type: 'IPFS_UNPIN_SUCCEED', msgArgs }) diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index 5aa344f1b..0ceb47ab4 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -28,7 +28,7 @@ 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, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey, + ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesCopyCidProvide, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey, files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t }) => { const { doExploreUserProvidedPath } = useExplore() @@ -291,6 +291,7 @@ const FilesPage = ({ onDownloadCar={() => onDownloadCar([contextMenu.file])} onPinning={() => showModal(PINNING, [contextMenu.file])} onPublish={() => showModal(PUBLISH, [contextMenu.file])} + onCopyCid={(cid) => doFilesCopyCidProvide(cid)} isCliTutorModeEnabled={isCliTutorModeEnabled} onCliTutorMode={() => showModal(CLI_TUTOR_MODE, [contextMenu.file])} doSetCliOptions={doSetCliOptions} @@ -416,6 +417,7 @@ export default connect( 'doFilesMove', 'doFilesMakeDir', 'doFilesShareLink', + 'doFilesCopyCidProvide', 'doFilesDelete', 'doFilesAddPath', 'doAddCarFile', diff --git a/src/files/context-menu/ContextMenu.js b/src/files/context-menu/ContextMenu.js index 0d3b3634b..7aafd6295 100644 --- a/src/files/context-menu/ContextMenu.js +++ b/src/files/context-menu/ContextMenu.js @@ -45,7 +45,7 @@ class ContextMenu extends React.Component { render () { const { - t, onRename, onRemove, onDownload, onInspect, onShare, onDownloadCar, onPublish, + t, onRename, onRemove, onDownload, onInspect, onShare, onDownloadCar, onPublish, onCopyCid, translateX, translateY, className, isMfs, isUnknown, isCliTutorModeEnabled } = this.props return ( @@ -65,7 +65,12 @@ class ContextMenu extends React.Component { {t('actions.share')} } - + { + this.props.handleClick() + if (onCopyCid) { + onCopyCid(this.props.cid) + } + }}>