Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
19 changes: 14 additions & 5 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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
}),

/**
Expand Down
3 changes: 3 additions & 0 deletions src/bundles/files/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions src/bundles/files/utils.js
Original file line number Diff line number Diff line change
@@ -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
*/

/**
Expand Down Expand Up @@ -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)
})
}
}
4 changes: 4 additions & 0 deletions src/bundles/ipns.js
Original file line number Diff line number Diff line change
@@ -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: [],
Expand Down Expand Up @@ -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 }) => {
Expand Down
4 changes: 4 additions & 0 deletions src/bundles/pinning.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 })
Expand Down
4 changes: 3 additions & 1 deletion src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -416,6 +417,7 @@ export default connect(
'doFilesMove',
'doFilesMakeDir',
'doFilesShareLink',
'doFilesCopyCidProvide',
'doFilesDelete',
'doFilesAddPath',
'doAddCarFile',
Expand Down
10 changes: 8 additions & 2 deletions src/files/context-menu/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -65,7 +65,12 @@ class ContextMenu extends React.Component {
{t('actions.share')}
</Option>
}
<CopyToClipboard text={String(this.props.cid)} onCopy={this.props.handleClick}>
<CopyToClipboard text={String(this.props.cid)} onCopy={() => {
this.props.handleClick()
if (onCopyCid) {
onCopyCid(this.props.cid)
}
}}>
<Option>
<StrokeCopy className='w2 mr2 fill-aqua' />
{t('actions.copyHash')}
Expand Down Expand Up @@ -140,6 +145,7 @@ ContextMenu.propTypes = {
onInspect: PropTypes.func,
onShare: PropTypes.func,
onPublish: PropTypes.func,
onCopyCid: PropTypes.func,
className: PropTypes.string,
t: PropTypes.func.isRequired,
tReady: PropTypes.bool.isRequired,
Expand Down
44 changes: 42 additions & 2 deletions src/lib/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function getDownloadLink (files, gatewayUrl, ipfs) {
* @param {string} gatewayUrl - The URL of the default IPFS gateway.
* @param {string} subdomainGatewayUrl - The URL of the subdomain gateway.
* @param {IPFSService} ipfs - The IPFS service instance for interacting with the IPFS network.
* @returns {Promise<string>} - A promise that resolves to the shareable link for the provided files.
* @returns {Promise<{link: string, cid: CID}>} - A promise that resolves to an object containing the shareable link and root CID.
*/
export async function getShareableLink (files, gatewayUrl, subdomainGatewayUrl, ipfs) {
let cid
Expand Down Expand Up @@ -129,7 +129,7 @@ export async function getShareableLink (files, gatewayUrl, subdomainGatewayUrl,
shareableLink = `${gatewayUrl}/ipfs/${cid}${filename || ''}`
}

return shareableLink
return { link: shareableLink, cid }
}

/**
Expand All @@ -152,6 +152,46 @@ export async function getCarLink (files, gatewayUrl, ipfs) {
return `${gatewayUrl}/ipfs/${cid}?format=car&filename=${filename || cid}.car`
}

// Cache for tracking provide operations to avoid spamming the network
const provideCache = new Map()
const PROVIDE_DEBOUNCE_TIME = 15 * 60 * 1000 // 15 minutes in milliseconds

/**
* Debounced function to provide a CID to the IPFS DHT network.
*
* @param {CID} cid - The CID to provide to the network
* @param {IPFSService} ipfs - The IPFS service instance
*/
export async function debouncedProvide (cid, ipfs) {
const cidStr = cid.toString()
const now = Date.now()
const lastProvideTime = provideCache.get(cidStr)

if (lastProvideTime != null && (now - lastProvideTime) < PROVIDE_DEBOUNCE_TIME) {
return
}

try {
// @ts-expect-error - ipfs is actually a KuboRPCClient with routing API
const provideEvents = ipfs.routing.provide(cid, { recursive: false })

for await (const event of provideEvents) {
console.debug(`[PROVIDE] ${cidStr}:`, event)
}

// Clean up old cache entries
for (const [cachedCid, timestamp] of provideCache.entries()) {
if ((now - timestamp) > PROVIDE_DEBOUNCE_TIME) {
provideCache.delete(cachedCid)
}
}

provideCache.set(cidStr, now)
} catch (error) {
console.error(`[PROVIDE] Failed for CID ${cidStr}:`, error)
}
}

/**
* @param {number} size in bytes
* @param {object} opts format customization
Expand Down
6 changes: 4 additions & 2 deletions src/lib/files.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,11 @@ it('should get a subdomain gateway url', async () => {
const files = [file]

const url = new URL(DEFAULT_SUBDOMAIN_GATEWAY)
const shareableLink = await getShareableLink(files, DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY, ipfs)
const { link: shareableLink, cid } = await getShareableLink(files, DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY, ipfs)
const base32Cid = 'bafybeifffq3aeaymxejo37sn5fyaf7nn7hkfmzwdxyjculx3lw4tyhk7uy'
const rightShareableLink = `${url.protocol}//${base32Cid}.ipfs.${url.host}`
expect(shareableLink).toBe(rightShareableLink)
expect(cid).toBeDefined()
})

it('should get a path gateway url', async () => {
Expand All @@ -279,6 +280,7 @@ it('should get a path gateway url', async () => {
}
const files = [file]

const res = await getShareableLink(files, DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY, ipfs)
const { link: res, cid } = await getShareableLink(files, DEFAULT_PATH_GATEWAY, DEFAULT_SUBDOMAIN_GATEWAY, ipfs)
expect(res).toBe(DEFAULT_PATH_GATEWAY + '/ipfs/' + veryLongCidv1)
expect(cid).toBeDefined()
})