diff --git a/@types/ipfs/index.d.ts b/@types/ipfs/index.d.ts new file mode 100644 index 000000000..b2888c4d8 --- /dev/null +++ b/@types/ipfs/index.d.ts @@ -0,0 +1,118 @@ +declare module "ipfs" { + import CID from "cids" + import Multiaddr from 'multiaddr' + + interface IPFSService extends CoreService { + pin: PinService + files: FileService + name: NameService + } + + interface CoreService { + cat(pathOrCID: string | CID, options?: CatOptions): AsyncIterable + ls(pathOrCID: string | CID, options?: ListOptions): AsyncIterable + } + + interface PinService { + add(cid: CID, options?: PinAddOptions): Promise + ls(options?: PinListOptions): AsyncIterable + rm(cid: CID, options?: PinRemoveOptions): Promise + } + + interface FileService { + stat(path: string, options?: FSStatOptions): Promise + } + + interface NameService { + resolve(value: string, options?: NameResloveOptions): AsyncIterable + } + + interface SwarmService { + connect(addr: Multiaddr, options?: TimeoutOptions): Promise + } + + + type Pin = { cid: CID } + + type TimeoutOptions = { + timeout?: number, + signal?: AbortSignal + } + + type PinAddOptions = TimeoutOptions & { + recursive?: boolean, + } + + type PinType = + | "recursive" + | "direct" + | "indirect" + + type PinEntry = { + cid: CID, + typ: PinType + } + + type PinListOptions = TimeoutOptions & { + paths?: string | CID | string[] | CID[], + type?: PinType + } + + type PinRemoveOptions = TimeoutOptions & { + recursive?: boolean + } + + + + type FSStatOptions = TimeoutOptions & { + hash?: boolean, + size?: boolean, + withLocal?: boolean + } + + type FileType = + | 'file' + | 'directory' + + interface FileStat { + cid: CID + size: number + cumulativeSize: number + type: FileType + blocks: number + withLocality: boolean + local: boolean + sizeLocal: number + } + + + type NameResloveOptions = TimeoutOptions & { + recursive?: boolean, + nocache?: boolean + } + + type CatOptions = TimeoutOptions & { + offset?: number + length?: number + } + + type ListOptions = TimeoutOptions & { + + } + + type ListEntry = { + depth: number, + name: string, + path: string, + size: number, + cid: CID, + type: FileType, + mode: number, + mtime: { secs: number, nsecs?: number } + } + + + export { IPFSService, CoreService, PinService, FileService, NameService, CID, Pin } + declare var ipfs: IPFSService + declare export default ipfs +} \ No newline at end of file diff --git a/@types/it-last/index.d.ts b/@types/it-last/index.d.ts new file mode 100644 index 000000000..9496fb2c8 --- /dev/null +++ b/@types/it-last/index.d.ts @@ -0,0 +1,5 @@ +declare module "it-last" { + declare function last(input: AsyncIterable): Promise + + export default last +} \ No newline at end of file diff --git a/@types/it-map/index.d.ts b/@types/it-map/index.d.ts new file mode 100644 index 000000000..eee7feab2 --- /dev/null +++ b/@types/it-map/index.d.ts @@ -0,0 +1,4 @@ +declare module "it-map" { + declare function map(input: AsyncIterable, f: (input: I) => O): AsyncIterable + declare export default map +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8ffe02bba..a58cd7575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37960,18 +37960,18 @@ } }, "multicodec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-1.0.1.tgz", - "integrity": "sha512-yrrU/K8zHyAH2B0slNVeq3AiwluflHpgQ3TAzwNJcuO2AoPyXgBT2EDkdbP1D8B/yFOY+S2hDYmFlI1vhVFkQw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-1.0.4.tgz", + "integrity": "sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg==", "requires": { - "buffer": "^5.5.0", + "buffer": "^5.6.0", "varint": "^5.0.0" }, "dependencies": { "buffer": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", - "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -37981,15 +37981,20 @@ } } }, - "ipfs-redux-bundle": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ipfs-redux-bundle/-/ipfs-redux-bundle-7.0.0.tgz", - "integrity": "sha512-sL6SR0PMYZbzhrI5J+An/gxj9l09DSRBByBOAHiF9sj7on7Gd/ArroSHL2EeFOFuH0gwcxP9BFjS18OyHU1jzg==", + "ipfs-provider": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ipfs-provider/-/ipfs-provider-1.0.0.tgz", + "integrity": "sha512-haFoGJYgpq6/V6dPoLlej8z7BVLlpTXgvhW0OHfSKGFhz8daavbfIr9z3MBKm/TdQEEq2lwsn/j6tm2NcMoLVA==", "requires": { - "ipfs-http-client": "^41.0.1", - "multiaddr": "^7.2.0", - "uri-to-multiaddr": "^3.0.1", - "window-or-global": "^1.0.1" + "iso-url": "~0.4.7", + "merge-options": "^2.0.0" + }, + "dependencies": { + "iso-url": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz", + "integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==" + } } }, "ipfs-unixfs": { @@ -38093,6 +38098,90 @@ "pump": "^3.0.0" } }, + "ipfs-http-client": { + "version": "41.0.1", + "resolved": "https://registry.npmjs.org/ipfs-http-client/-/ipfs-http-client-41.0.1.tgz", + "integrity": "sha512-oyH0hXoB+jfz4NqM+SZSpk6c9wR+9uvUO+/0eQFT6B9Ludz4zR8T5MHo7bnQoVerj9Qlv0x0wO1ckSoekaSPpw==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "async-iterator-to-pull-stream": "^1.3.0", + "bignumber.js": "^9.0.0", + "bl": "^4.0.0", + "bs58": "^4.0.1", + "buffer": "^5.4.2", + "callbackify": "^1.1.0", + "cids": "~0.7.1", + "debug": "^4.1.0", + "err-code": "^2.0.0", + "explain-error": "^1.0.4", + "form-data": "^3.0.0", + "ipfs-block": "~0.8.1", + "ipfs-utils": "^0.4.2", + "ipld-dag-cbor": "~0.15.0", + "ipld-dag-pb": "^0.18.1", + "ipld-raw": "^4.0.0", + "is-ipfs": "~0.6.1", + "it-all": "^1.0.1", + "it-glob": "0.0.7", + "it-tar": "^1.1.1", + "it-to-stream": "^0.1.1", + "iterable-ndjson": "^1.1.0", + "ky": "^0.15.0", + "ky-universal": "^0.3.0", + "merge-options": "^2.0.0", + "multiaddr": "^6.0.6", + "multiaddr-to-uri": "^5.0.0", + "multibase": "~0.6.0", + "multicodec": "^1.0.0", + "multihashes": "~0.4.14", + "parse-duration": "^0.1.1", + "peer-id": "~0.12.3", + "peer-info": "~0.15.1", + "promise-nodeify": "^3.0.1" + }, + "dependencies": { + "ipfs-utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-0.4.2.tgz", + "integrity": "sha512-k/uNOniniqg7uCnHvmujis8ASNefn0url8GS7HaNLAhL3RV3dHBiibtQFp8JZ/zfN+80FrYJt7cPEzRbGbmJUA==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "err-code": "^2.0.0", + "fs-extra": "^8.1.0", + "is-buffer": "^2.0.3", + "is-electron": "^2.2.0", + "is-pull-stream": "0.0.0", + "is-stream": "^2.0.0", + "it-glob": "0.0.7", + "kind-of": "^6.0.2", + "pull-stream-to-async-iterator": "^1.0.2", + "readable-stream": "^3.4.0" + } + }, + "ky": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.15.0.tgz", + "integrity": "sha512-6IlJRPFHq4ZKRRa9lyh6YqHqlmddAkfyXI9CYvZpLQtg7fQvwncPHyHrmtXAHKCqHOilINPMT88eW6FTA3HwkA==", + "dev": true + }, + "multiaddr": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-6.1.1.tgz", + "integrity": "sha512-Q1Ika0F9MNhMtCs62Ue+GWIJtRFEhZ3Xz8wH7/MZDVZTWhil1/H2bEGN02kUees3hkI3q1oHSjmXYDM0gxaFjQ==", + "dev": true, + "requires": { + "bs58": "^4.0.1", + "class-is": "^1.1.0", + "hi-base32": "~0.5.0", + "ip": "^1.1.5", + "is-ip": "^2.0.0", + "varint": "^5.0.0" + } + } + } + }, "ipfs-utils": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-0.7.2.tgz", @@ -38117,12 +38206,44 @@ } } }, + "it-glob": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/it-glob/-/it-glob-0.0.7.tgz", + "integrity": "sha512-XfbziJs4fi0MfdEGTLkZXeqo2EorF2baFXxFn1E2dGbgYMhFTZlZ2Yn/mx5CkpuLWVJvO1DwtTOVW2mzRyVK8w==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "minimatch": "^3.0.4" + } + }, "ky": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/ky/-/ky-0.16.2.tgz", "integrity": "sha512-4/Xcb0hqeueNX9sa+G2jREiam9yb+I2Y3p3J42lIeitAenHXUZwpyejEgeQcQsaGl+hbuA0s7c3u+nlcIYFtog==", "dev": true }, + "multicodec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-1.0.4.tgz", + "integrity": "sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg==", + "dev": true, + "requires": { + "buffer": "^5.6.0", + "varint": "^5.0.0" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -47209,12 +47330,13 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "stream-to-it": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stream-to-it/-/stream-to-it-0.2.2.tgz", - "integrity": "sha512-waULBmQpVdr6TkDzci6t1P7dIaSZ0bHC1TaPXDUeJC5PpSK7U3T0H0Zeo/LWUnd6mnhXOmGGDKAkjUCHw5IOng==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stream-to-it/-/stream-to-it-0.2.0.tgz", + "integrity": "sha512-bK/N8LPMc4FgNxXwIRBbJDWg2GYUfnVGH++hTM5SjCHzyPPWYp2ml+wnqaO86+y0SywZDxPAZSNAPP3Wii/QzQ==", "dev": true, "requires": { - "get-iterator": "^1.0.2" + "get-iterator": "^1.0.2", + "p-defer": "^3.0.0" } }, "strftime": { @@ -48574,30 +48696,6 @@ "punycode": "^2.1.0" } }, - "uri-to-multiaddr": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-to-multiaddr/-/uri-to-multiaddr-3.0.2.tgz", - "integrity": "sha512-I2AO1Y/3hUI7KfHiB6Py64lZ02jAB+hqlMVBzDRn4u6d85x+7tJhRwGzdKEYn8/1kDBtWFZVkHvgepF7Z+C1og==", - "requires": { - "is-ip": "^3.1.0", - "multiaddr": "^7.2.1" - }, - "dependencies": { - "ip-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", - "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" - }, - "is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "requires": { - "ip-regex": "^4.0.0" - } - } - } - }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", diff --git a/package.json b/package.json index 7ed3804c1..7949e2dca 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "@tableflip/react-dropdown": "^1.3.0", "@tableflip/react-inspector": "^2.3.0", "brace": "^0.11.1", - "change-case": "^3.1.0", + "change-case": "^4.1.1", "chart.js": "^2.9.3", - "cids": "^0.7.5", - "countly-sdk-web": "^19.8.0", + "cids": "^0.8.3", + "countly-sdk-web": "^20.4.0", "d3": "^5.16.0", "datatransfer-files-promise": "^1.3.1", "details-polyfill": "^1.1.0", @@ -39,17 +39,21 @@ "filesize": "^6.1.0", "hashlru": "^2.3.0", "i18next": "^19.6.2", - "i18next-browser-languagedetector": "^4.3.1", + "i18next-browser-languagedetector": "^5.0.0", "i18next-http-backend": "^1.0.17", "i18next-icu": "^1.4.2", "internal-nav-helper": "^3.1.0", "ip": "^1.1.5", "ipfs-css": "^1.2.0", "ipfs-geoip": "^4.1.0", - "ipfs-redux-bundle": "^7.0.0", + "ipfs-http-client": "^45.0.0", + "ipfs-provider": "^1.0.0", "ipld-explorer-components": "1.6.0", "is-binary": "^0.1.0", - "is-ipfs": "^0.6.3", + "is-ipfs": "^1.0.3", + "it-all": "1.0.2", + "it-last": "1.0.2", + "it-map": "1.0.2", "memoizee": "^0.4.14", "milliseconds": "^1.0.3", "money-clip": "^3.0.2", @@ -57,25 +61,24 @@ "multiaddr-to-uri": "^5.1.0", "p-queue": "^6.5.0", "prop-types": "^15.7.2", - "pull-file-reader": "^1.0.2", "react": "^16.13.1", - "react-ace": "^8.1.0", + "react-ace": "^9.1.1", "react-chartjs-2": "^2.9.0", "react-copy-to-clipboard": "^5.0.2", - "react-country-flag": "^1.1.0", - "react-debounce-render": "^5.0.0", - "react-dnd": "^10.0.2", - "react-dnd-html5-backend": "^10.0.2", + "react-country-flag": "^2.2.0", + "react-debounce-render": "^6.1.0", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", "react-dom": "^16.13.1", "react-faux-dom": "^4.5.0", - "react-helmet": "^5.2.1", + "react-helmet": "^6.1.0", "react-i18next": "^11.7.0", "react-identicons": "^1.2.4", "react-joyride": "^2.1.1", "react-loadable": "^5.5.0", - "react-overlays": "^2.1.1", + "react-overlays": "^4.1.0", "react-router-dom": "^5.2.0", - "react-scripts": "3.3.0", + "react-scripts": "3.4.1", "react-spring": "^8.0.27", "react-test-renderer": "^16.13.1", "react-virtualized": "^9.21.2", @@ -97,10 +100,14 @@ "@storybook/addons": "^5.3.19", "@storybook/react": "^5.3.19", "@svgr/cli": "^5.4.0", - "babel-eslint": "10.0.3", + "@types/ipfs": "file:./@types/ipfs", + "@types/it-all": "1.0.0", + "@types/it-last": "file:./@types/it-last", + "@types/it-map": "file:./@types/it-map", + "babel-eslint": "10.1.0", "big.js": "^5.2.2", "bundlesize": "^0.18.0", - "cross-env": "^6.0.3", + "cross-env": "^7.0.2", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "eslint-config-standard": "^14.1.1", @@ -110,15 +117,15 @@ "eslint-plugin-standard": "^4.0.1", "fake-indexeddb": "^3.1.1", "get-port": "^5.1.1", - "go-ipfs-dep": "0.4.22", + "go-ipfs-dep": "0.6.0", "http-server": "^0.12.3", - "ipfs": "0.40.0", - "ipfsd-ctl": "^1.0.7", + "ipfs": "0.48.1", + "ipfsd-ctl": "^5.0.0", "is-pull-stream": "0.0.0", "jest-puppeteer": "^4.4.0", - "multihashing-async": "^0.8.2", + "multihashing-async": "^1.0.0", "npm-run-all": "^4.1.5", - "puppeteer": "^3.3.0", + "puppeteer": "^5.2.1", "run-script-os": "^1.1.1", "shx": "^0.3.2", "webpack-bundle-analyzer": "^3.8.0" @@ -147,4 +154,4 @@ "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs" } } -} +} \ No newline at end of file diff --git a/src/bundles/analytics.js b/src/bundles/analytics.js index b7a4eff75..842131652 100644 --- a/src/bundles/analytics.js +++ b/src/bundles/analytics.js @@ -1,5 +1,5 @@ import root from 'window-or-global' -import changeCase from 'change-case' +import { constantCase } from 'change-case' import { createSelector } from 'redux-bundler' // Only record specific actions listed here. @@ -136,7 +136,7 @@ const createAnalyticsBundle = ({ ? 'EXPERIMENTS_' : 'DESKTOP_SETTING_' - key += changeCase.constantCase(action.payload.key) + key += constantCase(action.payload.key) if (state === 'FAILED') { key += '_FAILED' diff --git a/src/bundles/config.js b/src/bundles/config.js index 41d9721d5..fb31c5af1 100644 --- a/src/bundles/config.js +++ b/src/bundles/config.js @@ -4,8 +4,7 @@ import { createAsyncResourceBundle, createSelector } from 'redux-bundler' const bundle = createAsyncResourceBundle({ name: 'config', getPromise: async ({ getIpfs }) => { - // SEE: https://github.com/ipfs/js-ipfs-api/issues/822 - const rawConf = await getIpfs().config.get() + const rawConf = await getIpfs().config.getAll() let conf if (Buffer.isBuffer(rawConf)) { diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index 897b68da5..2af4ae32d 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -1,16 +1,51 @@ +// @ts-check + import { join, dirname, basename } from 'path' import { getDownloadLink, getShareableLink } from '../../lib/files' import countDirs from '../../lib/count-dirs' +import all from 'it-all' +import map from 'it-map' +import last from 'it-last' +import CID from 'cids' import { make, sortFiles, infoFromPath } from './utils' import { IGNORED_FILES, ACTIONS } from './consts' -const fileFromStats = ({ cumulativeSize, type, size, hash, name }, path, prefix = '/ipfs') => ({ +/** + * @typedef {import('ipfs').IPFSService} IPFSService + * @typedef {import('ipfs').Pin} Pin + */ + +/** + * @typedef {Object} FileStat + * @property {number|null} size + * @property {'directory'|'file'|'unknown'} type + * @property {CID} cid + * @property {string} name + * @property {string} path + * @property {boolean} pinned + * @property {boolean|void} isParent + * + * @param {Object} stat + * @param {'dir'|'directory'|'file'|'unknown'} stat.type + * @param {CID} stat.cid + * @param {string} stat.path + * @param {number} [stat.cumulativeSize] + * @param {number} [stat.size] + * @param {string|void} [stat.name] + * @param {boolean|void} [stat.pinned] + * @param {boolean|void} [stat.isParent] + * @param {string} [prefix] + * @returns {FileStat} + */ +const fileFromStats = ({ cumulativeSize, type, size, cid, name, path, pinned, isParent }, prefix = '/ipfs') => ({ size: cumulativeSize || size || null, type: (type === 'dir' || type === 'directory') ? 'directory' : (type === 'unknown') ? 'unknown' : 'file', - hash: hash, - name: name || path ? path.split('/').pop() : hash, - path: path || `${prefix}/${hash}` + cid, + name: name || path.split('/').pop() || cid.toString(), + path: path || `${prefix}/${cid.toString()}`, + pinned: Boolean(pinned), + isParent: isParent }) // TODO: use sth else @@ -22,49 +57,80 @@ const realMfsPath = (path) => { return path } -const stat = async (ipfs, hashOrPath) => { +/** + * @typedef {Object} Stat + * @property {string} path + * @property {'file'|'directory'|'unknown'} type + * @property {CID} cid + * @property {number|null} size + * + * @param {IPFSService} ipfs + * @param {string|CID} cidOrPath + * @returns {Promise} + */ +const stat = async (ipfs, cidOrPath) => { + const hashOrPath = cidOrPath.toString() const path = hashOrPath.startsWith('/') ? hashOrPath : `/ipfs/${hashOrPath}` try { const stats = await ipfs.files.stat(path) - return stats + return { path, ...stats } } catch (e) { // Discard error and mark DAG as 'unknown' to unblock listing other pins. // Clicking on 'unknown' entry will open it in Inspector. // No information is lost: if there is an error related // to specified hashOrPath user will read it in Inspector. + const [, , cid] = path.split('/') return { path: hashOrPath, - hash: hashOrPath, - type: 'unknown' + cid: new CID(cid), + type: 'unknown', + size: null } } } -const getRawPins = async (ipfs) => { - const recursive = await ipfs.pin.ls({ type: 'recursive' }) - const direct = await ipfs.pin.ls({ type: 'direct' }) - - return recursive.concat(direct) +/** + * + * @param {IPFSService} ipfs + * @returns {AsyncIterable} + */ +const getRawPins = async function * (ipfs) { + yield * ipfs.pin.ls({ type: 'recursive' }) + yield * ipfs.pin.ls({ type: 'direct' }) } -const getPins = async (ipfs) => { - const pins = await getRawPins(ipfs) - - const stats = await Promise.all( - pins.map(({ hash }) => stat(ipfs, hash)) - ) - - return stats.map(item => { - item = fileFromStats(item, null, '/pins') - item.pinned = true - return item - }) +/** + * @param {IPFSService} ipfs + * @returns {AsyncIterable} + */ +const getPinCIDs = (ipfs) => map(getRawPins(ipfs), (pin) => pin.cid) + +/** + * @param {IPFSService} ipfs + * @returns {AsyncIterable} + */ +const getPins = async function * (ipfs) { + for await (const cid of getPinCIDs(ipfs)) { + const info = await stat(ipfs, cid) + yield fileFromStats({ ...info, pinned: true }, '/pins') + } } -const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { +/** + * @typedef {Object} FetchFilesResult + * @property {string} path + * @property {Date} fetched + * @property {'directory'} type + * @property {FileStat[]} content + * + * @param {IPFSService} ipfs + * @param {*} id + * @param {*} options + */ +const fetchFilesFX = async (ipfs, id, { store }) => { let { path, realPath, isMfs, isPins, isRoot } = store.selectFilesPathInfo() if (isRoot && !isMfs && !isPins) { @@ -72,7 +138,7 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { } if (isRoot && isPins) { - const pins = await getPins(ipfs) // FIX: pins path + const pins = await all(getPins(ipfs)) // FIX: pins path return { path: '/pins', @@ -83,7 +149,7 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { } if (realPath.startsWith('/ipns')) { - realPath = await ipfs.name.resolve(realPath) + realPath = await last(ipfs.name.resolve(realPath)) } const stats = await stat(ipfs, realPath) @@ -94,18 +160,18 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { if (stats.type === 'file') { return { - ...fileFromStats(stats, path), + ...fileFromStats({ ...stats, path }), fetched: Date.now(), type: 'file', - read: () => ipfs.cat(stats.hash), + read: () => ipfs.cat(stats.cid), name: path.split('/').pop(), size: stats.size, - hash: stats.hash + cid: stats.cid } } // Otherwise get the directory info - const res = await ipfs.ls(stats.hash) || [] + const res = await all(ipfs.ls(stats.cid)) || [] const files = [] const showStats = res.length < 100 @@ -113,10 +179,10 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { const absPath = join(path, f.name) let file = null - if (showStats && f.type === 'dir') { - file = fileFromStats(await stat(ipfs, f.hash), absPath) + if (showStats && f.type === 'directory') { + file = fileFromStats({ ...await stat(ipfs, f.cid), path: absPath }) } else { - file = fileFromStats(f, absPath) + file = fileFromStats({ ...f, path: absPath }) } files.push(file) @@ -133,11 +199,12 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { parentInfo.realPath = await ipfs.name.resolve(parentInfo.realPath) } - parent = fileFromStats(await ipfs.files.stat(parentInfo.realPath)) - - parent.name = '..' - parent.path = parentInfo.path - parent.isParent = true + parent = fileFromStats({ + ...await ipfs.files.stat(parentInfo.realPath), + path: parentInfo.path, + name: '..', + isParent: true + }) } } @@ -145,15 +212,18 @@ const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { path: path, fetched: Date.now(), type: 'directory', - hash: stats.hash, + cid: stats.cid, upper: parent, content: sortFiles(files, store.selectFilesSorting()) } -}) +} + +const fetchFiles = make(ACTIONS.FETCH, fetchFilesFX) export default () => ({ doPinsFetch: make(ACTIONS.PIN_LIST, async (ipfs) => { - return { pins: (await getRawPins(ipfs)).map(f => f.hash) } + const cids = await all(getPinCIDs(ipfs)) + return { pins: cids } }), doFilesFetch: () => async ({ store, ...args }) => { @@ -215,10 +285,10 @@ export default () => ({ throw Object.assign(new Error('API returned a partial response.'), { code: 'ERR_API_RESPONSE' }) } - for (const { path, hash } of res) { + for (const { path, cid } of res) { // Only go for direct children if (path.indexOf('/') === -1 && path !== '') { - const src = `/ipfs/${hash}` + const src = `/ipfs/${cid}` const dst = join(realMfsPath(root || '/files'), path) try { @@ -260,9 +330,9 @@ export default () => ({ doFilesMakeDir: make(ACTIONS.MAKE_DIR, (ipfs, path) => ipfs.files.mkdir(realMfsPath(path), { parents: true }), { mfsOnly: true }), - doFilesPin: make(ACTIONS.PIN_ADD, (ipfs, hash) => ipfs.pin.add(hash)), + doFilesPin: make(ACTIONS.PIN_ADD, (ipfs, cid) => ipfs.pin.add(cid)), - doFilesUnpin: make(ACTIONS.PIN_REMOVE, (ipfs, hash) => ipfs.pin.rm(hash)), + doFilesUnpin: make(ACTIONS.PIN_REMOVE, (ipfs, cid) => ipfs.pin.rm(cid)), doFilesDismissErrors: () => async ({ dispatch }) => dispatch({ type: 'FILES_DISMISS_ERRORS' }), diff --git a/src/bundles/files/index.js b/src/bundles/files/index.js index b93623045..f50a92bcc 100644 --- a/src/bundles/files/index.js +++ b/src/bundles/files/index.js @@ -120,7 +120,7 @@ export default () => { } if (data.path === '/pins') { - additional.pins = data.content.map(f => f.hash) + additional.pins = data.content.map(f => f.cid) } } diff --git a/src/bundles/index.js b/src/bundles/index.js index b907043d1..4b8b15ecb 100644 --- a/src/bundles/index.js +++ b/src/bundles/index.js @@ -1,5 +1,5 @@ import { composeBundles, createCacheBundle } from 'redux-bundler' -import ipfsBundle from 'ipfs-redux-bundle' +import ipfsProvider from './ipfs-provider' import { exploreBundle } from 'ipld-explorer-components' import appIdle from './app-idle' import nodeBandwidthChartBundle from './node-bandwidth-chart' @@ -28,22 +28,7 @@ export default composeBundles( cacheFn: bundleCache.set }), appIdle({ idleTimeout: 5000 }), - ipfsBundle({ - tryWindow: false, - ipfsConnectionTest: async (ipfs) => { - // ipfs connection is working if can we fetch the bw stats. - // See: https://github.com/ipfs-shipyard/ipfs-webui/issues/835#issuecomment-466966884 - try { - await ipfs.stats.bw() - } catch (err) { - if (!/bandwidth reporter disabled in config/.test(err)) { - throw err - } - } - - return true - } - }), + ipfsProvider, identityBundle, navbarBundle, routesBundle, diff --git a/src/bundles/ipfs-provider.js b/src/bundles/ipfs-provider.js new file mode 100644 index 000000000..48b3b9a3f --- /dev/null +++ b/src/bundles/ipfs-provider.js @@ -0,0 +1,273 @@ +// @ts-check + +import multiaddr from 'multiaddr' +import HttpClient from 'ipfs-http-client' +import { getIpfs, providers } from 'ipfs-provider' +import last from 'it-last' + +/** + * @typedef {'httpClient'|'jsIpfs'|'windowIpfs'|'webExt'} ProviderName + * @typedef {Object} Model + * @property {void|string} apiAddress + * @property {void|ProviderName} provider + * @property {boolean} failed + * @property {boolean} ready + * @property {boolean} invalidAddress + * + * @typedef {Object} InitStarted + * @property {'IPFS_INIT_STARTED'} type + * + * @typedef {Object} InitFinished + * @property {'IPFS_INIT_FINISHED'} type + * @property {Object} payload + * @property {ProviderName} payload.provider + * @property {IPFSAPI} payload.ipfs + * @property {string} [payload.apiAddress] + * + * @typedef {Object} InitFailed + * @property {'IPFS_INIT_FAILED'} type + * + * @typedef {Object} Stopped + * @property {'IPFS_STOPPED'} type + * + * @typedef {Object} AddressUpdated + * @property {'IPFS_API_ADDRESS_UPDATED'} type + * @property {string} payload + * + * @typedef {Object} AddressInvalid + * @property {'IPFS_API_ADDRESS_INVALID'} type + * + * @typedef {Object} Dismiss + * @property {'IPFS_API_ADDRESS_INVALID_DISMISS'} type + * @typedef {InitStarted|InitFinished|InitFailed|Stopped|AddressUpdated|AddressInvalid|Dismiss} Message + */ + +/** + * @param {Model} state + * @param {Message} message + * @returns {Model} + */ +const update = (state, message) => { + switch (message.type) { + case 'IPFS_INIT_STARTED': { + return { ...state, ready: false } + } + case 'IPFS_INIT_FINISHED': { + ipfs = message.payload.ipfs + return { + ...state, + ready: true, + failed: false, + provider: message.payload.provider, + apiAddress: message.payload.apiAddress || state.apiAddress + } + } + case 'IPFS_STOPPED': { + return { ...state, ready: false, failed: false } + } + case 'IPFS_INIT_FAILED': { + return { ...state, ready: false, failed: true } + } + case 'IPFS_API_ADDRESS_UPDATED': { + return { ...state, apiAddress: message.payload, invalidAddress: false } + } + case 'IPFS_API_ADDRESS_INVALID': { + return { ...state, invalidAddress: true } + } + case 'IPFS_API_ADDRESS_INVALID_DISMISS': { + return { ...state, invalidAddress: true } + } + default: { + return state + } + } +} + +/** + * @returns {Model} + */ +const init = () => { + return { + apiAddress: readAPIAddressSetting(), + provider: null, + failed: false, + ready: false, + invalidAddress: false + } +} + +/** + * @returns {string|null} + */ +const readAPIAddressSetting = () => { + const setting = readSetting('ipfsApi') + return setting == null ? null : asAPIAddress(setting) +} + +const asAPIAddress = (value) => asMultiaddress(value) || asURL(value) + +/** + * Attempts to turn cast given value into `URL` instance. Return either `URL` + * instance or `null`. + * @param {any} value + * @returns {string|null} + */ +const asURL = (value) => { + try { + return new URL(value).toString() + } catch (_) { + return null + } +} + +/** + * Attempts to turn cast given value into `URL` instance. Return either `URL` + * instance or `null`. + */ +const asMultiaddress = (value) => { + if (value != null) { + try { + return multiaddr(value).toString() + } catch (_) {} + } + + return null +} + +/** + * Reads setting from the `localStorage` with a given `id` as JSON. If JSON + * parse is failed setting is interpreted as a string value. + * @param {string} id + * @returns {string|object|null} + */ +const readSetting = (id) => { + let setting = null + if (window.localStorage) { + try { + setting = window.localStorage.getItem(id) + } catch (error) { + console.log(`Error reading '${id}' value from localStorage`, error) + } + + try { + return JSON.parse(setting) + } catch (_) { + // res was probably a string, so pass it on. + return setting + } + } +} + +const writeSetting = (id, value) => { + try { + window.localStorage.setItem(id, JSON.stringify(value)) + } catch (error) { + console.log(`Error writing '${id}' value to localStorage`, error) + } +} + +/** + * @typedef {Object} IPFSAPI + * @property {(callback?:Function) => Promise} stop + */ + +/** @type {IPFSAPI|void} */ +let ipfs = null + +/** + * @typedef {Object} State + * @property {Model} ipfs + */ + +const bundle = { + name: 'ipfs', + reducer: (state, message) => update(state == null ? init() : state, message), + getExtraArgs () { + return { getIpfs: () => ipfs } + }, + /** + * @param {State} state + */ + selectIpfsReady: state => state.ipfs.ready, + /** + * @param {State} state + */ + selectIpfsProvider: state => state.ipfs.provider, + /** + * @param {State} state + */ + selectIpfsApiAddress: state => state.ipfs.apiAddress, + /** + * @param {State} state + */ + selectIpfsInvalidAddress: state => state.ipfs.invalidAddress, + /** + * @param {State} state + */ + selectIpfsInitFailed: state => state.ipfs.failed, + + doInitIpfs: () => async (store) => { + await initIPFS(store) + }, + + doStopIpfs: () => async (store) => { + if (ipfs) { + ipfs.stop(() => { + store.dispatch({ type: 'IPFS_STOPPED' }) + }) + } + }, + + doUpdateIpfsApiAddress: (address) => async (store) => { + const apiAddress = asAPIAddress(address) + if (apiAddress == null) { + store.dispatch({ type: 'IPFS_API_ADDRESS_INVALID' }) + } else { + await writeSetting('ipfsApi', apiAddress) + store.dispatch({ type: 'IPFS_API_ADDRESS_UPDATED', payload: apiAddress }) + + await initIPFS(store) + } + }, + + doDismissIpfsInvalidAddress: () => (store) => { + store.dispatch({ type: 'IPFS_API_ADDRESS_INVALID_DISMISS' }) + } +} + +const initIPFS = async (store) => { + store.dispatch({ type: 'IPFS_INIT_STARTED' }) + + /** @type {Model} */ + const { apiAddress } = store.getState().ipfs + + try { + const result = await getIpfs({ + // @ts-ignore - TS can't seem to infer connectionTest option + connectionTest: async (ipfs) => { + // ipfs connection is working if can we fetch the bw stats. + // See: https://github.com/ipfs-shipyard/ipfs-webui/issues/835#issuecomment-466966884 + try { + await last(ipfs.stats.bw()) + } catch (err) { + if (!/bandwidth reporter disabled in config/.test(err)) { + throw err + } + } + + return true + }, + loadHttpClientModule: () => HttpClient, + providers: [ + providers.webExt(), + providers.httpClient({ apiAddress }) + ] + }) + + store.dispatch({ type: 'IPFS_INIT_FINISHED', payload: result }) + } catch (error) { + store.dispatch({ type: 'IPFS_INIT_FAILED' }) + } +} + +export default bundle diff --git a/src/bundles/node-bandwidth.js b/src/bundles/node-bandwidth.js index afdf1edd9..174392bab 100644 --- a/src/bundles/node-bandwidth.js +++ b/src/bundles/node-bandwidth.js @@ -1,12 +1,13 @@ import { createAsyncResourceBundle, createSelector } from 'redux-bundler' import ms from 'milliseconds' +import last from 'it-last' const bundle = createAsyncResourceBundle({ name: 'nodeBandwidth', actionBaseType: 'NODE_BANDWIDTH', getPromise: async ({ getIpfs }) => { try { - const stats = await getIpfs().stats.bw() + const stats = await last(getIpfs().stats.bw()) return stats } catch (err) { if (/bandwidth reporter disabled in config/.test(err)) { diff --git a/src/bundles/peer-bandwidth.js b/src/bundles/peer-bandwidth.js index 7b891d962..4078ab000 100644 --- a/src/bundles/peer-bandwidth.js +++ b/src/bundles/peer-bandwidth.js @@ -1,4 +1,5 @@ import { createSelector } from 'redux-bundler' +import all from 'it-all' // Depends on ipfsBundle, peersBundle, routesBundle export default function (opts) { @@ -132,7 +133,7 @@ export default function (opts) { let bw try { - bw = await ipfs.stats.bw({ peer: peerId }) + bw = await all(ipfs.stats.bw({ peer: peerId })) } catch (err) { return dispatch({ type: 'UPDATE_PEER_BANDWIDTH_FAILED', @@ -162,7 +163,7 @@ export default function (opts) { if (routeInfo.url !== '/') return const peerIds = (peers || []).reduce((ids, p) => { - const id = p.peer.toB58String() + const id = p.peer if (ids.seen[id]) return ids ids.seen[id] = true ids.unique.push(id) diff --git a/src/bundles/peer-bandwidth.test.js b/src/bundles/peer-bandwidth.test.js index ad98fb3e5..09df9adb5 100644 --- a/src/bundles/peer-bandwidth.test.js +++ b/src/bundles/peer-bandwidth.test.js @@ -78,7 +78,7 @@ it('should sync added peers', async () => { expect(bwPeers.length).toBe(totalPeers) bwPeers.forEach(({ id }) => { - expect(nextPeers.some(p => p.peer.toB58String() === id)).toBe(true) + expect(nextPeers.some(p => p.peer === id)).toBe(true) }) }) @@ -105,7 +105,7 @@ it('should sync removed peers', async () => { expect(bwPeers.length).toBe(peers.length) bwPeers.forEach(({ id }) => { - expect(peers.some(p => p.peer.toB58String() === id)).toBe(true) + expect(peers.some(p => p.peer === id)).toBe(true) }) const nextTotalPeers = randomInt(1, totalPeers) @@ -119,7 +119,7 @@ it('should sync removed peers', async () => { expect(bwPeers.length).toBe(nextPeers.length) bwPeers.forEach(({ id }) => { - expect(nextPeers.some(p => p.peer.toB58String() === id)).toBe(true) + expect(nextPeers.some(p => p.peer === id)).toBe(true) }) }) @@ -146,7 +146,7 @@ it('should sync added and removed peers', async () => { expect(bwPeers.length).toBe(peers.length) bwPeers.forEach(({ id }) => { - expect(peers.some(p => p.peer.toB58String() === id)).toBe(true) + expect(peers.some(p => p.peer === id)).toBe(true) }) const totalAddedPeers = randomInt(1, 100) @@ -164,7 +164,7 @@ it('should sync added and removed peers', async () => { expect(bwPeers.length).toBe(nextPeers.length) bwPeers.forEach(({ id }) => { - expect(nextPeers.some(p => p.peer.toB58String() === id)).toBe(true) + expect(nextPeers.some(p => p.peer === id)).toBe(true) }) }) diff --git a/src/bundles/peer-locations.js b/src/bundles/peer-locations.js index ea964f096..ac2f1b12a 100644 --- a/src/bundles/peer-locations.js +++ b/src/bundles/peer-locations.js @@ -53,7 +53,7 @@ export default function (opts) { 'selectBootstrapPeers', 'selectIdentity', (peers, locations = {}, bootstrapPeers, identity) => peers && peers.map(peer => { - const peerId = peer.peer.toB58String() + const peerId = peer.peer const locationObj = locations ? locations[peerId] : null const location = toLocationString(locationObj) const flagCode = locationObj && locationObj.country_code @@ -193,7 +193,7 @@ const isPrivateAndNearby = (maddr, identity) => { } const parseNotes = (peer, bootstrapPeers) => { - const peerId = peer.peer.toB58String() + const peerId = peer.peer const addr = peer.addr const ipfsAddr = addr.encapsulate(`/ipfs/${peerId}`).toString() const p2pAddr = addr.encapsulate(`/p2p/${peerId}`).toString() @@ -234,7 +234,7 @@ class PeerLocationResolver { const res = {} for (const p of this.optimizedPeerSet(peers)) { - const peerId = p.peer.toB58String() + const peerId = p.peer const ipv4Tuple = p.addr.stringTuples().find(isNonHomeIPv4) if (!ipv4Tuple) { diff --git a/src/components/address/Address.js b/src/components/address/Address.js index d158ef9b6..5f466520f 100644 --- a/src/components/address/Address.js +++ b/src/components/address/Address.js @@ -6,7 +6,7 @@ const Address = ({ value }) => { const ma = Multiaddr(value) const protos = ma.protoNames().concat(['ipfs', 'p2p']) - const parts = value.split('/') + const parts = ma.toString().split('/') return (
diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index 4c04d44b2..393fb0dbe 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -85,8 +85,8 @@ class FilesPage extends React.Component { this.props.doFilesAddPath(this.props.files.path, path) } - onInspect = (hash) => { - this.props.doUpdateHash(`/explore/ipfs/${hash}`) + onInspect = (cid) => { + this.props.doUpdateHash(`/explore/ipfs/${cid}`) } showModal = (modal, files = null) => { @@ -226,14 +226,14 @@ class FilesPage extends React.Component { isMfs={filesPathInfo ? filesPathInfo.isMfs : false} isUnknown={!!(contextMenu.file && contextMenu.file.type === 'unknown')} pinned={contextMenu.file && contextMenu.file.pinned} - hash={contextMenu.file && contextMenu.file.hash} + cid={contextMenu.file && contextMenu.file.cid} onShare={() => this.showModal(SHARE, [contextMenu.file])} onDelete={() => this.showModal(DELETE, [contextMenu.file])} onRename={() => this.showModal(RENAME, [contextMenu.file])} - onInspect={() => this.onInspect(contextMenu.file.hash)} + onInspect={() => this.onInspect(contextMenu.file.cid)} onDownload={() => this.onDownload([contextMenu.file])} - onPin={() => this.props.doFilesPin(contextMenu.file.hash)} - onUnpin={() => this.props.doFilesUnpin(contextMenu.file.hash)} /> + onPin={() => this.props.doFilesPin(contextMenu.file.cid)} + onUnpin={() => this.props.doFilesUnpin(contextMenu.file.cid)} />
} - +
)) diff --git a/src/files/file-preview/FilePreview.js b/src/files/file-preview/FilePreview.js index 08ab1c868..04f283f5d 100644 --- a/src/files/file-preview/FilePreview.js +++ b/src/files/file-preview/FilePreview.js @@ -6,6 +6,7 @@ import { Trans, withTranslation } from 'react-i18next' import typeFromExt from '../type-from-ext' import ComponentLoader from '../../loader/ComponentLoader.js' import './FilePreview.css' +import CID from 'cids' class Preview extends React.Component { state = { @@ -18,10 +19,10 @@ class Preview extends React.Component { } render () { - const { t, name, hash, size, gatewayUrl } = this.props + const { t, name, cid, size, gatewayUrl } = this.props const type = typeFromExt(name) - const src = `${gatewayUrl}/ipfs/${hash}` + const src = `${gatewayUrl}/ipfs/${cid}` const className = 'mw-100 mt3 bg-snow-muted pa2 br2 border-box' switch (type) { @@ -85,7 +86,7 @@ class Preview extends React.Component { Preview.propTypes = { name: PropTypes.string.isRequired, - hash: PropTypes.string.isRequired, + hash: PropTypes.instanceOf(CID), size: PropTypes.number.isRequired, gatewayUrl: PropTypes.string.isRequired, read: PropTypes.func.isRequired, diff --git a/src/files/file/File.js b/src/files/file/File.js index 73a3b7b28..f30b022ff 100644 --- a/src/files/file/File.js +++ b/src/files/file/File.js @@ -14,6 +14,7 @@ import GlyphPin from '../../icons/GlyphPin' import Tooltip from '../../components/tooltip/Tooltip' import Checkbox from '../../components/checkbox/Checkbox' import FileIcon from '../file-icon/FileIcon' +import CID from 'cids' class File extends React.Component { static propTypes = { @@ -21,7 +22,7 @@ class File extends React.Component { type: PropTypes.string.isRequired, path: PropTypes.string.isRequired, size: PropTypes.number, - hash: PropTypes.string, + cid: PropTypes.instanceOf(CID), selected: PropTypes.bool, focused: PropTypes.bool, onSelect: PropTypes.func, @@ -47,19 +48,19 @@ class File extends React.Component { } handleCtxLeftClick = (ev) => { - const { name, type, size, hash, path, pinned } = this.props + const { name, type, size, cid, path, pinned } = this.props const pos = this.dotsWrapper.getBoundingClientRect() - this.props.handleContextMenuClick(ev, 'LEFT', { name, size, type, hash, path, pinned }, pos) + this.props.handleContextMenuClick(ev, 'LEFT', { name, size, type, cid, path, pinned }, pos) } handleCtxRightClick = (ev) => { - const { name, type, size, hash, path, pinned } = this.props - this.props.handleContextMenuClick(ev, 'RIGHT', { name, size, type, hash, path, pinned }) + const { name, type, size, cid, path, pinned } = this.props + this.props.handleContextMenuClick(ev, 'RIGHT', { name, size, type, cid, path, pinned }) } render () { let { - t, selected, focused, translucent, coloured, hash, name, type, size, pinned, onSelect, onNavigate, + t, selected, focused, translucent, coloured, cid, name, type, size, pinned, onSelect, onNavigate, isOver, canDrop, cantDrag, cantSelect, connectDropTarget, connectDragPreview, connectDragSource, styles = {} } = this.props @@ -87,7 +88,7 @@ class File extends React.Component { styles.overflow = 'hidden' size = size ? filesize(size, { round: 0 }) : '-' - hash = hash || t('hashUnavailable') + const hash = cid.toString() || t('hashUnavailable') const select = (select) => onSelect(name, select) diff --git a/src/files/files-list/FilesList.js b/src/files/files-list/FilesList.js index 5cd275863..b6494a7c7 100644 --- a/src/files/files-list/FilesList.js +++ b/src/files/files-list/FilesList.js @@ -106,7 +106,7 @@ export class FilesList extends React.Component { rename={() => this.props.onRename(this.selectedFiles)} share={() => this.props.onShare(this.selectedFiles)} download={() => this.props.onDownload(this.selectedFiles)} - inspect={() => this.props.onInspect(this.selectedFiles[0].hash)} + inspect={() => this.props.onInspect(this.selectedFiles[0].cid)} count={this.state.selected.length} isMfs={this.props.filesPathInfo.isMfs} downloadProgress={this.props.downloadProgress} @@ -341,14 +341,14 @@ export class FilesList extends React.Component {
{ this.filesRefs[files[index].name] = r }} isMfs={filesPathInfo.isMfs} name={files[index].name} onSelect={this.toggleOne} onNavigate={() => { if (files[index].type === 'unknown') { - onInspect(files[index].hash) + onInspect(files[index].cid) } else { onNavigate(files[index].path) } diff --git a/src/files/header/Header.js b/src/files/header/Header.js index e2f1faaf1..1674b63f0 100644 --- a/src/files/header/Header.js +++ b/src/files/header/Header.js @@ -36,7 +36,7 @@ class Header extends React.Component { const pos = this.dotsWrapper.getBoundingClientRect() this.props.handleContextMenu(ev, 'TOP', { ...this.props.files, - pinned: this.props.pins.includes(this.props.files.hash) + pinned: this.props.pins.includes(this.props.files.cid) }, pos) } diff --git a/src/lib/dnd-backend.js b/src/lib/dnd-backend.js index c4bf55599..1a385caa8 100644 --- a/src/lib/dnd-backend.js +++ b/src/lib/dnd-backend.js @@ -1,5 +1,5 @@ import { getFilesFromDataTransferItems } from 'datatransfer-files-promise' -import HTML5Backend from 'react-dnd-html5-backend' +import { HTML5Backend } from 'react-dnd-html5-backend' // If you drop a dir "foo" which contains "cat.jpg" & "dog.png" we receive a // single item in the `event.dataTransfer.items` for the directory. diff --git a/src/lib/files.js b/src/lib/files.js index 05e85ccda..36630fdf3 100644 --- a/src/lib/files.js +++ b/src/lib/files.js @@ -1,15 +1,17 @@ -import fileReader from 'pull-file-reader' -import CID from 'cids' +/** + * @typedef {import('ipfs').IPFSService} IPFSService + */ +/** + * @param {File[]} files + */ export async function filesToStreams (files) { const streams = [] for (const file of files) { - const stream = fileReader(file) - streams.push({ path: file.filepath || file.webkitRelativePath || file.name, - content: stream, + content: file, size: file.size }) } @@ -21,28 +23,28 @@ async function downloadSingle (file, gatewayUrl, apiUrl) { let url, filename if (file.type === 'directory') { - url = `${apiUrl}/api/v0/get?arg=${file.hash}&archive=true&compress=true` + url = `${apiUrl}/api/v0/get?arg=${file.cid}&archive=true&compress=true` filename = `${file.name}.tar.gz` } else { - url = `${gatewayUrl}/ipfs/${file.hash}` + url = `${gatewayUrl}/ipfs/${file.cid}` filename = file.name } return { url, filename } } -export async function makeHashFromFiles (files, ipfs) { +export async function makeCIDFromFiles (files, ipfs) { let cid = await ipfs.object.new('unixfs-dir') for (const file of files) { - cid = await ipfs.object.patch.addLink(cid.multihash, { + cid = await ipfs.object.patch.addLink(cid, { name: file.name, size: file.size, - cid: new CID(file.hash) + cid: file.cid }) } - return cid.toString() + return cid } async function downloadMultiple (files, apiUrl, ipfs) { @@ -51,11 +53,11 @@ async function downloadMultiple (files, apiUrl, ipfs) { return Promise.reject(e) } - const multihash = await makeHashFromFiles(files, ipfs) + const cid = await makeCIDFromFiles(files, ipfs) return { - url: `${apiUrl}/api/v0/get?arg=${multihash}&archive=true&compress=true`, - filename: `download_${multihash}.tar.gz` + url: `${apiUrl}/api/v0/get?arg=${cid}&archive=true&compress=true`, + filename: `download_${cid}.tar.gz` } } @@ -68,17 +70,17 @@ export async function getDownloadLink (files, gatewayUrl, apiUrl, ipfs) { } export async function getShareableLink (files, ipfs) { - let hash + let cid let filename if (files.length === 1) { - hash = files[0].hash + cid = files[0].cid if (files[0].type === 'file') { filename = `?filename=${encodeURIComponent(files[0].name)}` } } else { - hash = await makeHashFromFiles(files, ipfs) + cid = await makeCIDFromFiles(files, ipfs) } - return `https://ipfs.io/ipfs/${hash}${filename || ''}` + return `https://ipfs.io/ipfs/${cid}${filename || ''}` } diff --git a/src/peers/PeersTable/PeersTable.js b/src/peers/PeersTable/PeersTable.js index 3185ebe0d..6a287deb5 100644 --- a/src/peers/PeersTable/PeersTable.js +++ b/src/peers/PeersTable/PeersTable.js @@ -34,7 +34,7 @@ export class PeersTable extends React.Component { const isWindows = window.navigator.appVersion.indexOf('Win') !== -1 return ( - {isPrivate ? '🤝' : flagCode ? : '🌐'} + {isPrivate ? '🤝' : flagCode ? : '🌐'} ) }