diff --git a/packages/ipfs-cli/src/commands/init.js b/packages/ipfs-cli/src/commands/init.js index 2a5f9fd0c1..a324d87c9f 100644 --- a/packages/ipfs-cli/src/commands/init.js +++ b/packages/ipfs-cli/src/commands/init.js @@ -69,22 +69,20 @@ module.exports = { const IPFS = require('ipfs-core') const Repo = require('ipfs-repo') - const node = await IPFS.create({ - repo: new Repo(repoPath), - init: false, - start: false, - config - }) - try { - await node.init({ - algorithm: argv.algorithm, - bits: argv.bits, - privateKey: argv.privateKey, - emptyRepo: argv.emptyRepo, - profiles: argv.profile, - pass: argv.pass, - log: print + await IPFS.create({ + repo: new Repo(repoPath), + init: { + algorithm: argv.algorithm, + bits: argv.bits, + privateKey: argv.privateKey, + emptyRepo: argv.emptyRepo, + profiles: argv.profile, + pass: argv.pass + }, + start: false, + // @ts-ignore - Expects more than {} + config }) } catch (err) { if (err.code === 'EACCES') { diff --git a/packages/ipfs-core/src/api-manager.js b/packages/ipfs-core/src/api-manager.js deleted file mode 100644 index 03a33b925d..0000000000 --- a/packages/ipfs-core/src/api-manager.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -module.exports = class ApiManager { - /** - * @callback UndefFn - * @param {PropertyKey} prop - */ - - /** - * @template API - * @typedef {{ cancel: () => any, api: API }} Updated - */ - - constructor () { - this._api = {} - /** - * @type {UndefFn} - * @returns {any} - */ - this._onUndef = () => undefined - this.api = new Proxy(this._api, { - get: (_, prop) => { - if (prop === 'then') return undefined // Not a promise! - return this._api[prop] === undefined ? this._onUndef(prop) : this._api[prop] - } - }) - } - - /** - * @template A - * @param {A} nextApi - * @param {UndefFn} [onUndef] - * @returns {Updated} - */ - update (nextApi, onUndef) { - const prevApi = { ...this._api } - const prevUndef = this._onUndef - Object.keys(this._api).forEach(k => { delete this._api[k] }) - const api = Object.assign(this._api, nextApi) - if (onUndef) this._onUndef = onUndef - return { cancel: () => this.update(prevApi, prevUndef), api } - } -} diff --git a/packages/ipfs-core/src/components/add-all/index.js b/packages/ipfs-core/src/components/add-all/index.js index c5c7502ddb..8b0f429428 100644 --- a/packages/ipfs-core/src/components/add-all/index.js +++ b/packages/ipfs-core/src/components/add-all/index.js @@ -13,10 +13,10 @@ const mergeOptions = require('merge-options').bind({ ignoreUndefined: true }) * @param {import('..').GCLock} config.gcLock * @param {import('..').Preload} config.preload * @param {import('..').Pin} config.pin - * @param {import('../init').ConstructorOptions} config.options + * @param {ShardingOptions} [config.options] */ -module.exports = ({ block, gcLock, preload, pin, options: constructorOptions }) => { - const isShardingEnabled = constructorOptions.EXPERIMENTAL && constructorOptions.EXPERIMENTAL.sharding +module.exports = ({ block, gcLock, preload, pin, options }) => { + const isShardingEnabled = options && options.sharding /** * Import multiple files and data into IPFS. * @@ -178,4 +178,7 @@ function pinFile (pin, opts) { * @typedef {import('../../utils').MTime} MTime * @typedef {import('../../utils').AbortOptions} AbortOptions * @typedef {import('..').CID} CID + * + * @typedef {Object} ShardingOptions + * @property {boolean} [sharding] */ diff --git a/packages/ipfs-core/src/components/bitswap/index.js b/packages/ipfs-core/src/components/bitswap/index.js new file mode 100644 index 0000000000..8209007a75 --- /dev/null +++ b/packages/ipfs-core/src/components/bitswap/index.js @@ -0,0 +1,27 @@ +'use strict' + +const createWantlist = require('./wantlist') +const createWantlistForPeer = require('./wantlist-for-peer') +const createUnwant = require('./unwant') +const createStat = require('./stat') + +class BitswapAPI { + /** + * @param {Object} config + * @param {NetworkService} config.network + */ + constructor ({ network }) { + this.wantlist = createWantlist({ network }) + this.wantlistForPeer = createWantlistForPeer({ network }) + this.unwant = createUnwant({ network }) + this.stat = createStat({ network }) + } +} +module.exports = BitswapAPI + +/** + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/bitswap/stat.js b/packages/ipfs-core/src/components/bitswap/stat.js index 237a54d80a..63ce7f5551 100644 --- a/packages/ipfs-core/src/components/bitswap/stat.js +++ b/packages/ipfs-core/src/components/bitswap/stat.js @@ -6,16 +6,13 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Show diagnostic information on the bitswap agent. * Note: `bitswap.stat` and `stats.bitswap` can be used interchangeably. * - * @param {import('../../utils').AbortOptions} [_options] - * @returns {Promise} - * * @example * ```js * const stats = await ipfs.bitswap.stat() @@ -35,8 +32,11 @@ module.exports = ({ bitswap }) => { * // dupDataReceived: 0 * // } * ``` + * @param {import('.').AbortOptions} [options] + * @returns {Promise} */ - async function stat (_options) { // eslint-disable-line require-await + async function stat (options) { + const { bitswap } = await network.use(options) const snapshot = bitswap.stat().snapshot return { @@ -59,13 +59,11 @@ module.exports = ({ bitswap }) => { * @typedef {object} BitswapStats - An object that contains information about the bitswap agent * @property {number} provideBufLen - an integer * @property {CID[]} wantlist - * @property {string[]} peers - array of peer IDs as Strings + * @property {CID[]} peers - array of peer IDs as Strings * @property {Big} blocksReceived * @property {Big} dataReceived * @property {Big} blocksSent * @property {Big} dataSent * @property {Big} dupBlksReceived * @property {Big} dupDataReceived - * - * @typedef {import('..').CID} CID */ diff --git a/packages/ipfs-core/src/components/bitswap/unwant.js b/packages/ipfs-core/src/components/bitswap/unwant.js index 455696b23c..a17cb3b355 100644 --- a/packages/ipfs-core/src/components/bitswap/unwant.js +++ b/packages/ipfs-core/src/components/bitswap/unwant.js @@ -6,15 +6,12 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Removes one or more CIDs from the wantlist * - * @param {CID | CID[]} cids - The CIDs to remove from the wantlist - * @param {AbortOptions} [options] - * @returns {Promise} - A promise that resolves once the request is complete * @example * ```JavaScript * let list = await ipfs.bitswap.wantlist() @@ -27,8 +24,14 @@ module.exports = ({ bitswap }) => { * console.log(list) * // [] * ``` + * + * @param {CID | CID[]} cids - The CIDs to remove from the wantlist + * @param {AbortOptions} [options] + * @returns {Promise} - A promise that resolves once the request is complete */ - async function unwant (cids, options) { // eslint-disable-line require-await + async function unwant (cids, options) { + const { bitswap } = await network.use(options) + if (!Array.isArray(cids)) { cids = [cids] } @@ -46,6 +49,5 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js b/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js index 6f79ab8c9d..e9778c6148 100644 --- a/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js +++ b/packages/ipfs-core/src/components/bitswap/wantlist-for-peer.js @@ -5,24 +5,26 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Returns the wantlist for a connected peer * - * @param {PeerId | CID | string | Uint8Array} peerId - A peer ID to return the wantlist for\ - * @param {AbortOptions} [options] - * @returns {Promise} - An array of CIDs currently in the wantlist - * * @example * ```js * const list = await ipfs.bitswap.wantlistForPeer(peerId) * console.log(list) * // [ CID('QmHash') ] * ``` + * + * @param {PeerId | CID | string | Uint8Array} peerId - A peer ID to return the wantlist for\ + * @param {AbortOptions} [options] + * @returns {Promise} - An array of CIDs currently in the wantlist + * */ - async function wantlistForPeer (peerId, options = {}) { // eslint-disable-line require-await + async function wantlistForPeer (peerId, options = {}) { + const { bitswap } = await network.use(options) const list = bitswap.wantlistForPeer(PeerId.createFromCID(peerId), options) return Array.from(list).map(e => e[1].cid) @@ -32,9 +34,9 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').PeerId} PeerId + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').PeerId} PeerId */ /** diff --git a/packages/ipfs-core/src/components/bitswap/wantlist.js b/packages/ipfs-core/src/components/bitswap/wantlist.js index 94fed01ebb..c25195c9a5 100644 --- a/packages/ipfs-core/src/components/bitswap/wantlist.js +++ b/packages/ipfs-core/src/components/bitswap/wantlist.js @@ -4,22 +4,24 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBitSwap} config.bitswap + * @param {import('.').NetworkService} config.network */ -module.exports = ({ bitswap }) => { +module.exports = ({ network }) => { /** * Returns the wantlist for your node * - * @param {AbortOptions} [options] - * @returns {Promise} - An array of CIDs currently in the wantlist. * @example * ```js * const list = await ipfs.bitswap.wantlist() * console.log(list) * // [ CID('QmHash') ] * ``` + * + * @param {AbortOptions} [options] + * @returns {Promise} - An array of CIDs currently in the wantlist. */ - async function wantlist (options = {}) { // eslint-disable-line require-await + async function wantlist (options = {}) { + const { bitswap } = await network.use(options) const list = bitswap.getWantlist(options) return Array.from(list).map(e => e[1].cid) @@ -29,6 +31,6 @@ module.exports = ({ bitswap }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/block/get.js b/packages/ipfs-core/src/components/block/get.js index 4f376fb4a5..225ab16e1b 100644 --- a/packages/ipfs-core/src/components/block/get.js +++ b/packages/ipfs-core/src/components/block/get.js @@ -5,10 +5,10 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Preload} config.preload */ -module.exports = ({ blockService, preload }) => { +module.exports = ({ preload, blockService }) => { /** * Get a raw IPFS block. * @@ -22,14 +22,14 @@ module.exports = ({ blockService, preload }) => { * console.log(block.data) * ``` */ - async function get (cid, options = {}) { // eslint-disable-line require-await + async function get (cid, options = {}) { cid = cleanCid(cid) if (options.preload !== false) { preload(cid) } - return blockService.get(cid, options) + return await blockService.get(cid, options) } return withTimeoutOption(get) @@ -39,7 +39,7 @@ module.exports = ({ blockService, preload }) => { * @typedef {Object} GetOptions * @property {boolean} [preload=true] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').IPLDBlock} IPLDBlock + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').IPLDBlock} IPLDBlock */ diff --git a/packages/ipfs-core/src/components/block/index.js b/packages/ipfs-core/src/components/block/index.js new file mode 100644 index 0000000000..252a49d115 --- /dev/null +++ b/packages/ipfs-core/src/components/block/index.js @@ -0,0 +1,36 @@ +'use strict' + +const createGet = require('./get') +const createPut = require('./put') +const createRm = require('./rm') +const createStat = require('./stat') + +class BlockAPI { + /** + * @param {Object} config + * @param {Preload} config.preload + * @param {BlockService} config.blockService + * @param {GCLock} config.gcLock + * @param {Pin} config.pin + * @param {PinManager} config.pinManager + */ + constructor ({ blockService, preload, gcLock, pinManager, pin }) { + this.get = createGet({ blockService, preload }) + this.put = createPut({ blockService, preload, gcLock, pin }) + this.rm = createRm({ blockService, gcLock, pinManager }) + this.stat = createStat({ blockService, preload }) + } +} + +module.exports = BlockAPI + +/** + * @typedef {import('..').Preload} Preload + * @typedef {import('..').BlockService} BlockService + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Pin} Pin + * @typedef {import('..').PinManager} PinManager + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + * @typedef {import('..').IPLDBlock} IPLDBlock + */ diff --git a/packages/ipfs-core/src/components/block/put.js b/packages/ipfs-core/src/components/block/put.js index 8303315b59..db395eb7ac 100644 --- a/packages/ipfs-core/src/components/block/put.js +++ b/packages/ipfs-core/src/components/block/put.js @@ -8,12 +8,12 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Pin} config.pin - * @param {import('..').GCLock} config.gcLock - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Pin} config.pin + * @param {import('.').GCLock} config.gcLock + * @param {import('.').Preload} config.preload */ -module.exports = ({ blockService, pin, gcLock, preload }) => { +module.exports = ({ blockService, preload, gcLock, pin }) => { /** * Stores input as an IPFS block. * @@ -21,7 +21,7 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { * don't need to pass options, as the block instance will carry the CID * value as a property. * - * @param {Uint8Array | IPLDBlock} block - The block or data to store + * @param {IPLDBlock} block - The block or data to store * @param {PutOptions & AbortOptions} [options] - **Note:** If you pass a `Block` instance as the block parameter, you don't need to pass options, as the block instance will carry the CID value as a property. * @returns {Promise} - A Block type object, containing both the data and the hash of the block * @example @@ -122,8 +122,7 @@ module.exports = ({ blockService, pin, gcLock, preload }) => { * @property {boolean} [pin=false] - If true, pin added blocks recursively (default: `false`) * @property {boolean} [preload] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').IPLDBlock} IPLDBlock + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').IPLDBlock} IPLDBlock * @typedef {0|1} CIDVersion */ diff --git a/packages/ipfs-core/src/components/block/rm.js b/packages/ipfs-core/src/components/block/rm.js index db7414e31d..bbfe56f8f0 100644 --- a/packages/ipfs-core/src/components/block/rm.js +++ b/packages/ipfs-core/src/components/block/rm.js @@ -12,11 +12,11 @@ const BLOCK_RM_CONCURRENCY = 8 /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('../pin/pin-manager')} config.pinManager - * @param {import('..').GCLock} config.gcLock + * @param {import('.').BlockService} config.blockService + * @param {import('.').PinManager} config.pinManager + * @param {import('.').GCLock} config.gcLock */ -module.exports = ({ blockService, gcLock, pinManager }) => { +module.exports = ({ gcLock, blockService, pinManager }) => { /** /** * Remove one or more IPFS block(s). @@ -65,6 +65,7 @@ module.exports = ({ blockService, gcLock, pinManager }) => { } // remove has check when https://github.com/ipfs/js-ipfs-block-service/pull/88 is merged + // @ts-ignore - this accesses some internals const has = await blockService._repo.blocks.has(cid) if (!has) { @@ -96,7 +97,7 @@ module.exports = ({ blockService, gcLock, pinManager }) => { * @property {boolean} [force=false] - Ignores nonexistent blocks * @property {boolean} [quiet=false] - Write minimal output * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * * @typedef {RmSucceess|RmFailure} RmResult * Note: If an error is present for a given object, the block with diff --git a/packages/ipfs-core/src/components/block/stat.js b/packages/ipfs-core/src/components/block/stat.js index 62f6c7ea3b..897a3b9263 100644 --- a/packages/ipfs-core/src/components/block/stat.js +++ b/packages/ipfs-core/src/components/block/stat.js @@ -5,9 +5,10 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPFSBlockService} config.blockService - * @param {import('..').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').Preload} config.preload */ + module.exports = ({ blockService, preload }) => { /** /** @@ -50,7 +51,6 @@ module.exports = ({ blockService, preload }) => { * @typedef {Object} StatOptions * @property {boolean} [preload] * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/bootstrap/add.js b/packages/ipfs-core/src/components/bootstrap/add.js index a9f22c43a1..d101c6e7fb 100644 --- a/packages/ipfs-core/src/components/bootstrap/add.js +++ b/packages/ipfs-core/src/components/bootstrap/add.js @@ -4,7 +4,8 @@ const { isValidMultiaddr } = require('./utils') const { withTimeoutOption } = require('../../utils') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -30,11 +31,13 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) + // @ts-ignore - May not have `Bootstrap` if (config.Bootstrap.indexOf(multiaddr.toString()) === -1) { + // @ts-ignore - May not have `Bootstrap` config.Bootstrap.push(multiaddr.toString()) } - await repo.config.set(config) + await repo.config.replace(config) return { Peers: [multiaddr] @@ -46,7 +49,7 @@ module.exports = ({ repo }) => { /** * @typedef {import('./utils').Peers} Peers - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/clear.js b/packages/ipfs-core/src/components/bootstrap/clear.js index 2637e4d345..9828372106 100644 --- a/packages/ipfs-core/src/components/bootstrap/clear.js +++ b/packages/ipfs-core/src/components/bootstrap/clear.js @@ -5,7 +5,7 @@ const Multiaddr = require('multiaddr') /** * @param {Object} config - * @param {import('..').IPFSRepo} config.repo + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -26,7 +26,7 @@ module.exports = ({ repo }) => { const removed = config.Bootstrap || [] config.Bootstrap = [] - await repo.config.set(config) + await repo.config.replace(config) return { Peers: removed.map(ma => new Multiaddr(ma)) } } @@ -35,8 +35,6 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/index.js b/packages/ipfs-core/src/components/bootstrap/index.js new file mode 100644 index 0000000000..3cb7c9dafa --- /dev/null +++ b/packages/ipfs-core/src/components/bootstrap/index.js @@ -0,0 +1,28 @@ +'use strict' + +const createAdd = require('./add') +const createClear = require('./clear') +const createList = require('./list') +const createReset = require('./reset') +const createRm = require('./rm') +class BootstrapAPI { + /** + * @param {Object} config + * @param {Repo} config.repo + */ + constructor ({ repo }) { + this.add = createAdd({ repo }) + this.list = createList({ repo }) + this.rm = createRm({ repo }) + this.clear = createClear({ repo }) + this.reset = createReset({ repo }) + } +} +module.exports = BootstrapAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + * @typedef {import('..').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/bootstrap/list.js b/packages/ipfs-core/src/components/bootstrap/list.js index 421f16792f..cb39d5b398 100644 --- a/packages/ipfs-core/src/components/bootstrap/list.js +++ b/packages/ipfs-core/src/components/bootstrap/list.js @@ -4,7 +4,8 @@ const { withTimeoutOption } = require('../../utils') const Multiaddr = require('multiaddr') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -21,7 +22,8 @@ module.exports = ({ repo }) => { * ``` */ async function list (options) { - const peers = await repo.config.get('Bootstrap', options) + /** @type {string[]|null} */ + const peers = (await repo.config.get('Bootstrap', options)) return { Peers: (peers || []).map(ma => new Multiaddr(ma)) } } @@ -29,8 +31,7 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/bootstrap/reset.js b/packages/ipfs-core/src/components/bootstrap/reset.js index 81a2dfc6bf..7593ae4004 100644 --- a/packages/ipfs-core/src/components/bootstrap/reset.js +++ b/packages/ipfs-core/src/components/bootstrap/reset.js @@ -5,7 +5,8 @@ const { withTimeoutOption } = require('../../utils') const Multiaddr = require('multiaddr') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -25,7 +26,7 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) config.Bootstrap = defaultConfig().Bootstrap - await repo.config.set(config) + await repo.config.replace(config) return { Peers: defaultConfig().Bootstrap.map(ma => new Multiaddr(ma)) @@ -36,8 +37,6 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/bootstrap/rm.js b/packages/ipfs-core/src/components/bootstrap/rm.js index 88300ea939..bca7eee8a1 100644 --- a/packages/ipfs-core/src/components/bootstrap/rm.js +++ b/packages/ipfs-core/src/components/bootstrap/rm.js @@ -4,7 +4,8 @@ const { isValidMultiaddr } = require('./utils') const { withTimeoutOption } = require('../../utils') /** - * @param {import('..').IPFSRepo} repo + * @param {Object} config + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { /** @@ -29,7 +30,7 @@ module.exports = ({ repo }) => { const config = await repo.config.getAll(options) config.Bootstrap = (config.Bootstrap || []).filter(ma => ma.toString() !== multiaddr.toString()) - await repo.config.set(config) + await repo.config.replace(config) return { Peers: [multiaddr] } } @@ -38,8 +39,7 @@ module.exports = ({ repo }) => { } /** - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').Multiaddr} Multiaddr * @typedef {import('./utils').Peers} Peers - * @typedef {import('..').CID} CID - * @typedef {import('..').Multiaddr} Multiaddr */ diff --git a/packages/ipfs-core/src/components/config.js b/packages/ipfs-core/src/components/config.js index b3df1c6f03..7584eb3bdf 100644 --- a/packages/ipfs-core/src/components/config.js +++ b/packages/ipfs-core/src/components/config.js @@ -6,27 +6,58 @@ const log = require('debug')('ipfs:core:config') /** * @param {Object} config - * @param {import('.').IPFSRepo} config.repo - * @returns {Config} + * @param {import('.').Repo} config.repo */ module.exports = ({ repo }) => { return { - getAll: withTimeoutOption(repo.config.getAll), - get: withTimeoutOption((key, options) => { - if (!key) { - return Promise.reject(new Error('key argument is required')) - } - - return repo.config.get(key, options) - }), - set: withTimeoutOption(repo.config.set), - replace: withTimeoutOption(repo.config.replace), + getAll: withTimeoutOption(getAll), + get: withTimeoutOption(get), + set: withTimeoutOption(set), + replace: withTimeoutOption(replace), profiles: { apply: withTimeoutOption(applyProfile), list: withTimeoutOption(listProfiles) } } + /** + * @param {AbortOptions} [options] + */ + async function getAll (options = {}) { + return await repo.config.getAll() + } + + /** + * + * @param {string} key + * @param {AbortOptions} [options] + */ + async function get (key, options) { + if (!key) { + return Promise.reject(new Error('key argument is required')) + } + + return await repo.config.get(key, options) + } + + /** + * + * @param {string} key + * @param {ToJSON} value + * @param {AbortOptions} [options] + */ + async function set (key, value, options) { + return await repo.config.set(key, value, options) + } + + /** + * @param {IPFSConfig} value + * @param {AbortOptions} [options] + */ + async function replace (value, options) { + return await repo.config.replace(value, options) + } + /** * @param {string} profileName * @param {*} options @@ -51,6 +82,7 @@ module.exports = ({ repo }) => { } // Scrub private key from output + // @ts-ignore `oldCfg.Identity` maybe undefined delete oldCfg.Identity.PrivKey delete newCfg.Identity.PrivKey @@ -190,10 +222,10 @@ module.exports.profiles = profiles * Returns the currently being used config. If the daemon is off, it returns * the stored config. * - * @param {string} [key] - The key of the value that should be fetched from the + * @param {string} key - The key of the value that should be fetched from the * config file. If no key is passed, then the whole config will be returned. * @param {AbortOptions} [options] - * @returns {Promise} - An object containing the configuration of the IPFS node + * @returns {Promise} - An object containing the configuration of the IPFS node * @example * const config = await ipfs.config.get('Addresses.Swarm') * console.log(config) @@ -216,7 +248,7 @@ module.exports.profiles = profiles * an effect. * * @param {string} key - The key of the value that should be added or replaced. - * @param {JSON} value - The value to be set. + * @param {ToJSON} value - The value to be set. * @param {AbortOptions} [options] * @returns {Promise} - Promise succeeds if config change succeeded, * otherwise fails with error. @@ -231,7 +263,7 @@ module.exports.profiles = profiles * i.e: if a config.replace changes the multiaddrs of the Swarm, Swarm will * have to be restarted manually for the changes to take an effect. * - * @param {Partial} value - A new configuration. + * @param {IPFSConfig} value - A new configuration. * @param {AbortOptions} [options] * @returns {Promise} * @example @@ -262,16 +294,13 @@ module.exports.profiles = profiles * @callback ApplyProfile * List available config profiles * @param {string} name - * @param {ApplyOptions} [options] + * @param {ApplyOptions & AbortOptions} [options] * @returns {Promise<{original: IPFSConfig, updated: IPFSConfig}>} * - * @typedef {Object} ApplyOptionsExt + * @typedef {Object} ApplyOptions * @property {boolean} [dryRun=false] - If true does not apply the profile - * @typedef {AbortOptions & ApplyOptionsExt} ApplyOptions * * - * @typedef {import('../utils').AbortOptions} AbortOptions - * * @typedef {Object} IPFSConfig * @property {AddressConfig} Addresses * @property {string} [Profiles] @@ -471,4 +500,7 @@ module.exports.profiles = profiles * exceeded, will trigger a connection GC operation. * * {{LowWater?:number, HighWater?:number}} ConnMgr + * + * @typedef {import('../interface/basic').ToJSON} ToJSON + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/core.js b/packages/ipfs-core/src/components/core.js new file mode 100644 index 0000000000..69734f94f6 --- /dev/null +++ b/packages/ipfs-core/src/components/core.js @@ -0,0 +1,45 @@ +'use strict' + +const createAddAPI = require('./add') +const createAddAllAPI = require('./add-all') +const createCatAPI = require('./cat') +const createGetAPI = require('./get') +const createLsAPI = require('./ls') + +class CoreAPI { + /** + * @param {Object} config + * @param {Block} config.block + * @param {Pin} config.pin + * @param {GCLock} config.gcLock + * @param {Preload} config.preload + * @param {IPLD} config.ipld + * @param {ShardingOptions} [config.options] + */ + constructor ({ preload, gcLock, pin, block, ipld, options }) { + const addAll = createAddAllAPI({ + preload, + gcLock, + block, + pin, + options + }) + + this.addAll = addAll + this.add = createAddAPI({ addAll }) + this.cat = createCatAPI({ ipld, preload }) + this.get = createGetAPI({ ipld, preload }) + this.ls = createLsAPI({ ipld, preload }) + } +} + +module.exports = CoreAPI + +/** + * @typedef {import('.').Block} Block + * @typedef {import('.').Pin} Pin + * @typedef {import('.').GCLock} GCLock + * @typedef {import('.').IPLD} IPLD + * @typedef {import('.').Preload} Preload + * @typedef {import('./add-all').ShardingOptions} ShardingOptions + */ diff --git a/packages/ipfs-core/src/components/dag/get.js b/packages/ipfs-core/src/components/dag/get.js index b0b305ea91..5aecec6188 100644 --- a/packages/ipfs-core/src/components/dag/get.js +++ b/packages/ipfs-core/src/components/dag/get.js @@ -14,20 +14,16 @@ module.exports = ({ ipld, preload }) => { /** * Retrieve an IPLD format node * - * @param {CID} ipfsPath - A DAG node that follows one of the supported IPLD formats - * @param {GetOptions & AbortOptions} [options] - An optional configration - * @returns {Promise} * @example * ```js - * ```JavaScript * // example obj * const obj = { - * a: 1, - * b: [1, 2, 3], - * c: { - * ca: [5, 6, 7], - * cb: 'foo' - * } + * a: 1, + * b: [1, 2, 3], + * c: { + * ca: [5, 6, 7], + * cb: 'foo' + * } * } * * const cid = await ipfs.dag.put(obj, { format: 'dag-cbor', hashAlg: 'sha2-256' }) @@ -35,8 +31,8 @@ module.exports = ({ ipld, preload }) => { * // zdpuAmtur968yprkhG9N5Zxn6MFVoqAWBbhUAkNLJs2UtkTq5 * * async function getAndLog(cid, path) { - * const result = await ipfs.dag.get(cid, { path }) - * console.log(result.value) + * const result = await ipfs.dag.get(cid, { path }) + * console.log(result.value) * } * * await getAndLog(cid, '/a') @@ -58,6 +54,11 @@ module.exports = ({ ipld, preload }) => { * // Logs: * // 6 * ``` + * + * @param {CID|string} ipfsPath - A DAG node that follows one of the supported IPLD formats + * @param ipfsPath + * @param {GetOptions & AbortOptions} [options] - An optional configration + * @returns {Promise} */ const get = async function get (ipfsPath, options = {}) { const { @@ -74,11 +75,11 @@ module.exports = ({ ipld, preload }) => { } if (options.path) { - const result = options.localResolve - /** @type {DagEntry} - first will return undefined if empty */ - ? (await first(ipld.resolve(cid, options.path))) - /** @type {DagEntry} - last will return undefined if empty */ - : (await last(ipld.resolve(cid, options.path))) + const entry = options.localResolve + ? await first(ipld.resolve(cid, options.path)) + : await last(ipld.resolve(cid, options.path)) + /** @type {DagEntry} - first and last will return undefined when empty */ + const result = (entry) return result } @@ -102,6 +103,6 @@ module.exports = ({ ipld, preload }) => { * @property {Object} value * @property {string} remainderPath * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/index.js b/packages/ipfs-core/src/components/dag/index.js new file mode 100644 index 0000000000..31e0c3b634 --- /dev/null +++ b/packages/ipfs-core/src/components/dag/index.js @@ -0,0 +1,69 @@ +'use strict' + +const createGet = require('./get') +const createResolve = require('./resolve') +const createTree = require('./tree') +const createPut = require('./put') + +class Reader { + /** + * @param {ReaderConfig} config + */ + constructor (config) { + this.get = createGet(config) + this.resolve = createResolve(config) + this.tree = createTree(config) + } +} + +class DagAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {Pin} config.pin + * @param {GCLock} config.gcLock + * @param {DagReader} config.dagReader + */ + constructor ({ ipld, pin, preload, gcLock, dagReader }) { + const { get, resolve, tree } = dagReader + const put = createPut({ ipld, preload, pin, gcLock }) + + this.get = get + this.resolve = resolve + this.tree = tree + this.put = put + } + + /** + * Creates a reader part of the DAG API. This allows other APIs that require + * reader parts of the DAG API to be instantiated before components required + * by writer end are. + * + * @param {ReaderConfig} config + * @returns {DagReader} + */ + static reader (config) { + return new Reader(config) + } +} + +module.exports = DagAPI + +/** + * @typedef {Object} DagReader + * @property {ReturnType} get + * @property {ReturnType} resolve + * @property {ReturnType} tree + * + * @typedef {Object} ReaderConfig + * @property {IPLD} config.ipld + * @property {Preload} config.preload + * + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').Pin} Pin + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/dag/put.js b/packages/ipfs-core/src/components/dag/put.js index dc2e786f1d..e15fbea195 100644 --- a/packages/ipfs-core/src/components/dag/put.js +++ b/packages/ipfs-core/src/components/dag/put.js @@ -11,12 +11,12 @@ const { withTimeoutOption } = require('../../utils') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import("..").Pin} config.pin - * @param {import("..").GCLock} config.gcLock - * @param {import("..").Preload} config.preload + * @param {import('.').Pin} config.pin + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock */ -module.exports = ({ ipld, pin, gcLock, preload }) => { +module.exports = ({ ipld, preload, pin, gcLock }) => { /** * Store an IPLD format node * @@ -32,49 +32,15 @@ module.exports = ({ ipld, pin, gcLock, preload }) => { * // zBwWX9ecx5F4X54WAjmFLErnBT6ByfNxStr5ovowTL7AhaUR98RWvXPS1V3HqV1qs3r5Ec5ocv7eCdbqYQREXNUfYNuKG * ``` */ - // eslint-disable-next-line complexity async function put (dagNode, options = {}) { - if (options.cid && (options.format || options.hashAlg)) { - throw new Error('Can\'t put dag node. Please provide either `cid` OR `format` and `hashAlg` options.') - } else if (((options.format && !options.hashAlg) || (!options.format && options.hashAlg))) { - throw new Error('Can\'t put dag node. Please provide `format` AND `hashAlg` options.') - } - - const optionDefaults = { - format: multicodec.DAG_CBOR, - hashAlg: multicodec.SHA2_256 - } + const { cidVersion, format, hashAlg } = readEncodingOptions(options) - // The IPLD expects the format and hashAlg as constants - if (options.format && typeof options.format === 'string') { - options.format = nameToCodec(options.format) - } - if (options.hashAlg && typeof options.hashAlg === 'string') { - options.hashAlg = nameToCodec(options.hashAlg) - } - - options = options.cid ? options : Object.assign({}, optionDefaults, options) - - // js-ipld defaults to verion 1 CIDs. Hence set version 0 explicitly for - // dag-pb nodes - if (options.version === undefined) { - if (options.format === multicodec.DAG_PB && options.hashAlg === multicodec.SHA2_256) { - options.version = 0 - } else { - options.version = 1 - } - } - - let release - - if (options.pin) { - release = await gcLock.readLock() - } + const release = options.pin ? await gcLock.readLock() : null try { - const cid = await ipld.put(dagNode, options.format, { - hashAlg: options.hashAlg, - cidVersion: options.version, + const cid = await ipld.put(dagNode, format, { + hashAlg, + cidVersion, signal: options.signal }) @@ -100,15 +66,79 @@ module.exports = ({ ipld, pin, gcLock, preload }) => { } /** - * @typedef {Object} PutOptions + * + * @param {PutOptions} options + */ +const readEncodingOptions = (options) => { + if (options.cid && (options.format || options.hashAlg)) { + throw new Error('Can\'t put dag node. Please provide either `cid` OR `format` and `hashAlg` options.') + } else if (((options.format && !options.hashAlg) || (!options.format && options.hashAlg))) { + throw new Error('Can\'t put dag node. Please provide `format` AND `hashAlg` options.') + } + + const cidVersion = readVersion(options) + + const { hashAlg, format } = options.cid != null + ? { format: options.cid.code, hashAlg: undefined } + : { ...defaultCIDOptions, ...options } + + return { + cidVersion, + format: typeof format === 'string' ? nameToCodec(format) : format, + hashAlg: typeof hashAlg === 'string' ? nameToCodec(hashAlg) : hashAlg + } +} + +/** + * Figures out what version of CID should be used given the options. + * + * @param {PutOptions} options + */ +const readVersion = ({ version, cid, format, hashAlg }) => { + // If version is passed just use that. + if (typeof version === 'number') { + return version + // If cid is provided use version field from it. + } else if (cid) { + return cid.version + // If it's dag-pb nodes use version 0 + } else if (format === multicodec.DAG_PB && hashAlg === multicodec.SHA2_256) { + return 0 + } else { + // Otherwise use version 1 + return 1 + } +} + +/** @type {WithCIDOptions} */ +const defaultCIDOptions = { + format: multicodec.DAG_CBOR, + hashAlg: multicodec.SHA2_256 +} + +/** + * @typedef {PutWith & OtherPutOptions} PutOptions + * @typedef {WithCID | WithCIDOptions} PutWith + * + * + * @typedef {Object} WithCID * @property {CID} [cid] - * @property {string|number} [format] - * @property {string|number} [hashAlg] + * // Note: We still stil need to reserve these fields otherwise it implies + * // that those fields can still be there and have very different types. + * @property {undefined} [format] + * @property {undefined} [hashAlg] + * @property {undefined} [version] + * + * @typedef {Object} WithCIDOptions + * @property {undefined} [cid] + * @property {string|number} format + * @property {string|number} hashAlg + * @property {0|1} [version] * + * @typedef {Object} OtherPutOptions * @property {boolean} [pin=false] - * @property {number} [version] * @property {boolean} [preload=false] * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/resolve.js b/packages/ipfs-core/src/components/dag/resolve.js index 30a7809e9f..eb7529bd1b 100644 --- a/packages/ipfs-core/src/components/dag/resolve.js +++ b/packages/ipfs-core/src/components/dag/resolve.js @@ -6,8 +6,8 @@ const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import('..').Preload} config.preload + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload */ module.exports = ({ ipld, preload }) => { /** @@ -103,6 +103,5 @@ module.exports = ({ ipld, preload }) => { * @property {string} remainderPath - The path to the end of the IPFS path * inside the node referenced by the CID * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dag/tree.js b/packages/ipfs-core/src/components/dag/tree.js index 3beedd768e..1c8fa3e41e 100644 --- a/packages/ipfs-core/src/components/dag/tree.js +++ b/packages/ipfs-core/src/components/dag/tree.js @@ -5,8 +5,8 @@ const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') /** * @param {Object} config - * @param {import('..').IPLD} config.ipld - * @param {import("..").Preload} config.preload + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload */ module.exports = ({ ipld, preload }) => { /** @@ -47,7 +47,7 @@ module.exports = ({ ipld, preload }) => { * // c/cb * ``` */ - async function * tree (ipfsPath, options = {}) { // eslint-disable-line require-await + async function * tree (ipfsPath, options = {}) { const { cid, path @@ -77,6 +77,6 @@ module.exports = ({ ipld, preload }) => { * @property {string} remainderPath - The path to the end of the IPFS path * inside the node referenced by the CID * - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/dht.js b/packages/ipfs-core/src/components/dht.js index f53865dabb..24180f3ffa 100644 --- a/packages/ipfs-core/src/components/dht.js +++ b/packages/ipfs-core/src/components/dht.js @@ -4,8 +4,15 @@ const PeerId = require('peer-id') const CID = require('cids') const errCode = require('err-code') const { withTimeoutOption } = require('../utils') +const { NotEnabledError } = require('../errors') +const get = require('dlv') -module.exports = ({ libp2p, repo }) => { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').Repo} config.repo + */ +module.exports = ({ network, repo }) => { const { get, put, findProvs, findPeer, provide, query } = { /** * Given a key, query the DHT for its best value. @@ -14,7 +21,8 @@ module.exports = ({ libp2p, repo }) => { * @param {AbortOptions} [options] - The key associated with the value to find * @returns {Promise} */ - async get (key, options = {}) { // eslint-disable-line require-await + async get (key, options = {}) { + const { libp2p } = await use(network, options) return libp2p._dht.get(normalizeCID(key), options) }, @@ -30,8 +38,9 @@ module.exports = ({ libp2p, repo }) => { * @param {AbortOptions} [options] * @returns {AsyncIterable} */ - put (key, value, options) { - return libp2p._dht.put(normalizeCID(key), value) + async * put (key, value, options) { + const { libp2p } = await use(network, options) + yield * libp2p._dht.put(normalizeCID(key), value) }, /** @@ -50,6 +59,7 @@ module.exports = ({ libp2p, repo }) => { * ``` */ async * findProvs (cid, options = {}) { + const { libp2p } = await use(network, options) if (options.numProviders) { options.maxNumProviders = options.numProviders } @@ -83,7 +93,8 @@ module.exports = ({ libp2p, repo }) => { * // '/ip4/147.75.94.115/tcp/4001' * ``` */ - async findPeer (peerId, options) { // eslint-disable-line require-await + async findPeer (peerId, options) { + const { libp2p } = await use(network, options) if (typeof peerId === 'string') { peerId = PeerId.createFromCID(peerId) } @@ -104,6 +115,7 @@ module.exports = ({ libp2p, repo }) => { * @returns {AsyncIterable} */ async * provide (cids, options = {}) { + const { libp2p } = await use(network, options) cids = Array.isArray(cids) ? cids : [cids] for (var i in cids) { @@ -142,6 +154,7 @@ module.exports = ({ libp2p, repo }) => { * @returns {AsyncIterable<{ id: CID, addrs: Multiaddr[] }>} */ async * query (peerId, options) { + const { libp2p } = await use(network, options) if (typeof peerId === 'string') { peerId = PeerId.createFromCID(peerId) } @@ -191,6 +204,18 @@ const parseCID = cid => { const normalizeCID = cid => cid instanceof Uint8Array ? cid : parseCID(cid) +/** + * @param {import('.').NetworkService} network + * @param {AbortOptions} [options] + */ +const use = async (network, options) => { + const net = await network.use(options) + if (get(net.libp2p, '_config.dht.enabled', false)) { + return net + } else { + throw new NotEnabledError('dht not enabled') + } +} /** * @typedef {Object} QueryEvent * @property {PeerId} id diff --git a/packages/ipfs-core/src/components/files/index.js b/packages/ipfs-core/src/components/files/index.js index e5f29180de..864590ed03 100644 --- a/packages/ipfs-core/src/components/files/index.js +++ b/packages/ipfs-core/src/components/files/index.js @@ -94,11 +94,11 @@ function createMfs (options) { /** * @param {Object} context * @param {import('..').IPLD} context.ipld - * @param {import('..').IPLDBlock} context.block - * @param {import('..').IPFSBlockService} context.blockService - * @param {import('..').IPFSRepo} context.repo + * @param {import('..').Block} context.block + * @param {import('..').BlockService} context.blockService + * @param {import('..').Repo} context.repo * @param {import('..').Preload} context.preload - * @param {import('../init').ConstructorOptions} context.options + * @param {import('..').Options} context.options * @returns {MFS} */ module.exports = ({ ipld, block, blockService, repo, preload, options: constructorOptions }) => { diff --git a/packages/ipfs-core/src/components/files/ls.js b/packages/ipfs-core/src/components/files/ls.js index 4610c34763..195002025f 100644 --- a/packages/ipfs-core/src/components/files/ls.js +++ b/packages/ipfs-core/src/components/files/ls.js @@ -3,6 +3,7 @@ const exporter = require('ipfs-unixfs-exporter') const toMfsPath = require('./utils/to-mfs-path') const { + MFS_FILE_TYPES, withTimeoutOption } = require('../../utils') @@ -11,20 +12,14 @@ const { * @returns {UnixFSEntry} */ const toOutput = (fsEntry) => { - /** @type FileType */ - let type = 'file' + let type = 0 let size = fsEntry.node.size || fsEntry.node.length let mode let mtime if (fsEntry.unixfs) { size = fsEntry.unixfs.fileSize() - type = fsEntry.unixfs.type - - if (fsEntry.unixfs.type === 'hamt-sharded-directory') { - type = 'directory' - } - + type = MFS_FILE_TYPES[fsEntry.unixfs.type] mode = fsEntry.unixfs.mode mtime = fsEntry.unixfs.mtime } @@ -94,13 +89,10 @@ module.exports = (context) => { * @property {number} [nsecs] - the number of nanoseconds since the last full * second. * - * @typedef {'file'|'directory'} FileType - * * @typedef {object} UnixFSEntry * @property {CID} cid - * @property {string} name * @property {number} [mode] * @property {UnixTimeObj} [mtime] * @property {number} size - * @property {FileType} type + * @property {number} type */ diff --git a/packages/ipfs-core/src/components/gc-lock.js b/packages/ipfs-core/src/components/gc-lock.js new file mode 100644 index 0000000000..9eed02de1c --- /dev/null +++ b/packages/ipfs-core/src/components/gc-lock.js @@ -0,0 +1,24 @@ +'use strict' + +const mortice = require('mortice') + +/** + * @param {Object} config + * @param {string} config.path + * @param {boolean} [config.repoOwner] + * @returns {GCLock} + */ +module.exports = ({ path, repoOwner }) => + mortice(path, { + singleProcess: repoOwner !== false + }) + +/** + * @typedef {RWLock} GCLock + * + * @typedef {Object} RWLock + * @property {() => Promise} readLock + * @property {() => Promise} writeLock + * + * @typedef {() => void} Lock + */ diff --git a/packages/ipfs-core/src/components/id.js b/packages/ipfs-core/src/components/id.js index 226dcc239e..62afd8d6e5 100644 --- a/packages/ipfs-core/src/components/id.js +++ b/packages/ipfs-core/src/components/id.js @@ -7,15 +7,15 @@ const uint8ArrayToString = require('uint8arrays/to-string') /** * @param {Object} config - * @param {import('peer-id')} config.peerId - * @param {import('libp2p')} [config.libp2p] + * @param {import('.').PeerId} config.peerId + * @param {import('.').NetworkService} config.network */ -module.exports = ({ peerId, libp2p }) => { +module.exports = ({ network, peerId }) => { /** * Returns the identity of the Peer * * @param {import('../utils').AbortOptions} [_options] - * @returns {Promise} + * @returns {Promise} * @example * ```js * const identity = await ipfs.id() @@ -27,7 +27,10 @@ module.exports = ({ peerId, libp2p }) => { let addresses = [] let protocols = [] - if (libp2p) { + const net = network.try() + + if (net) { + const { libp2p } = net // only available while the node is running addresses = libp2p.transportManager.getAddrs() protocols = Array.from(libp2p.upgrader.protocols.keys()) @@ -59,7 +62,7 @@ module.exports = ({ peerId, libp2p }) => { } /** - * @typedef {object} PeerId + * @typedef {object} ID * The Peer identity * @property {string} id - the Peer ID * @property {string} publicKey - the public key of the peer as a base64 encoded string diff --git a/packages/ipfs-core/src/components/index.js b/packages/ipfs-core/src/components/index.js index fd4f8c1794..b52fbc54a4 100644 --- a/packages/ipfs-core/src/components/index.js +++ b/packages/ipfs-core/src/components/index.js @@ -1,322 +1,377 @@ 'use strict' -/** - * @typedef {ReturnType} Add - */ -exports.add = require('./add') - -/** - * @typedef {ReturnType} AddAll - */ - -exports.addAll = require('./add-all') - -/** - * @typedef {Object} Block - * @property {ReturnType} get - * @property {ReturnType} put - * @property {ReturnType} rm - * @property {ReturnType} stat - */ -exports.block = { - get: require('./block/get'), - put: require('./block/put'), - rm: require('./block/rm'), - stat: require('./block/stat') +const { mergeOptions } = require('../utils') +const { isTest } = require('ipfs-utils/src/env') +const log = require('debug')('ipfs') + +const { DAGNode } = require('ipld-dag-pb') +const UnixFs = require('ipfs-unixfs') +const multicodec = require('multicodec') +const initAssets = require('../runtime/init-assets-nodejs') + +const createStartAPI = require('./start') +const createStopAPI = require('./stop') +const createDNSAPI = require('./dns') +const createIsOnlineAPI = require('./is-online') +const createResolveAPI = require('./resolve') +const PinAPI = require('./pin') +const IPNSAPI = require('./ipns') +const NameAPI = require('./name') +const createRefsAPI = require('./refs') +const createRefsLocalAPI = require('./refs/local') +const BitswapAPI = require('./bitswap') +const BootstrapAPI = require('./bootstrap') +const BlockAPI = require('./block') +const CoreAPI = require('./core') +const createVersionAPI = require('./version') +const createIDAPI = require('./id') +const createConfigAPI = require('./config') +const DagAPI = require('./dag') +const PinManagerAPI = require('./pin/pin-manager') +const createPreloadAPI = require('../preload') +const createMfsPreloadAPI = require('../mfs-preload') +const createFilesAPI = require('./files') +const KeyAPI = require('./key') +const ObjectAPI = require('./object') +const RepoAPI = require('./repo') +const StatsAPI = require('./stats') +const IPFSBlockService = require('ipfs-block-service') +const createIPLD = require('./ipld') +const Storage = require('./storage') +const Network = require('./network') +const Service = require('../utils/service') +const SwarmAPI = require('./swarm') +const createGCLockAPI = require('./gc-lock') +const createPingAPI = require('./ping') +const createDHTAPI = require('./dht') +const createPubSubAPI = require('./pubsub') + +class IPFS { + /** + * @param {Object} config + * @param {Print} config.print + * @param {StorageAPI} config.storage + * @param {Options} config.options + */ + constructor ({ print, storage, options }) { + const { peerId, repo, keychain, pass } = storage + const network = Service.create(Network) + + const preload = createPreloadAPI(options.preload) + + /** @type {BlockService} */ + const blockService = new IPFSBlockService(storage.repo) + const ipld = createIPLD({ blockService, print, options: options.ipld }) + + const gcLock = createGCLockAPI({ + path: repo.path, + repoOwner: options.repoOwner + }) + const dns = createDNSAPI() + const isOnline = createIsOnlineAPI({ network }) + const ipns = new IPNSAPI() + const dagReader = DagAPI.reader({ ipld, preload }) + + const name = new NameAPI({ + dns, + ipns, + dagReader, + peerId, + isOnline, + keychain, + options + }) + const resolve = createResolveAPI({ ipld, name }) + const pinManager = new PinManagerAPI({ repo, dagReader }) + const pin = new PinAPI({ gcLock, pinManager, dagReader }) + const block = new BlockAPI({ blockService, preload, gcLock, pinManager, pin }) + const dag = new DagAPI({ ipld, preload, gcLock, pin, dagReader }) + const refs = Object.assign(createRefsAPI({ ipld, resolve, preload }), { + local: createRefsLocalAPI({ repo: storage.repo }) + }) + const { add, addAll, cat, get, ls } = new CoreAPI({ + gcLock, + preload, + pin, + block, + ipld, + options: options.EXPERIMENTAL + }) + + const files = createFilesAPI({ + ipld, + block, + blockService, + repo, + preload, + options + }) + + const mfsPreload = createMfsPreloadAPI({ + files, + preload, + options: options.preload + }) + + this.preload = preload + this.name = name + this.ipld = ipld + this.ipns = ipns + this.pin = pin + this.resolve = resolve + this.block = block + this.refs = refs + + this.start = createStartAPI({ + network, + peerId, + repo, + blockService, + preload, + ipns, + mfsPreload, + print, + keychain, + pass + }) + + this.stop = createStopAPI({ + network, + preload, + mfsPreload, + blockService, + ipns, + repo + }) + + this.dht = createDHTAPI({ network, repo }) + this.pubsub = createPubSubAPI({ network, config: options.config }) + this.dns = dns + this.isOnline = isOnline + this.id = createIDAPI({ network, peerId }) + this.version = createVersionAPI({ repo }) + this.bitswap = new BitswapAPI({ network }) + this.bootstrap = new BootstrapAPI({ repo }) + this.config = createConfigAPI({ repo }) + this.ping = createPingAPI({ network }) + + this.add = add + this.addAll = addAll + this.cat = cat + this.get = get + this.ls = ls + + this.dag = dag + this.files = files + this.key = new KeyAPI({ keychain }) + this.object = new ObjectAPI({ ipld, preload, gcLock, dag }) + this.repo = new RepoAPI({ gcLock, pin, repo, refs }) + this.stats = new StatsAPI({ repo, network }) + this.swarm = new SwarmAPI({ network }) + + // For the backwards compatibility + Object.defineProperty(this, 'libp2p', () => { + const net = network.try() + return net ? net.libp2p : undefined + }) + } + + async init () { + // Just keep this around for backwards compatibility + } + + /** + * @param {Options} options + */ + static async create (options = {}) { + options = mergeOptions(getDefaultOptions(), options) + + // eslint-disable-next-line no-console + const print = options.silent ? log : console.log + + const init = { + ...mergeOptions(initOptions(options), options), + print + } + + const storage = await Storage.start(init) + const config = await storage.repo.config.getAll() + + const ipfs = new IPFS({ + storage, + print, + options: { ...options, config } + }) + + await ipfs.preload.start() + + ipfs.ipns.startOffline(storage) + if (!storage.isInitialized && !init.emptyRepo) { + // add empty unixfs dir object (go-ipfs assumes this exists) + const cid = await addEmptyDir(ipfs) + + log('adding default assets') + await initAssets({ addAll: ipfs.addAll, print }) + + log('initializing IPNS keyspace') + await ipfs.ipns.initializeKeyspace(storage.peerId.privKey, cid.toString()) + } + + if (options.start !== false) { + await ipfs.start() + } + + return ipfs + } } +module.exports = IPFS /** - * @typedef {Object} BitSwap - * @property {ReturnType} stat - * @property {ReturnType} unwant - * @property {ReturnType} wantlist + * @param {Options} options + * @returns {InitOptions} */ -exports.bitswap = { - stat: require('./bitswap/stat'), - unwant: require('./bitswap/unwant'), - wantlist: require('./bitswap/wantlist'), - wantlistForPeer: require('./bitswap/wantlist-for-peer') -} +const initOptions = ({ init }) => + init === 'object' ? init : {} /** - * @typedef {Object} Bootstrap - * @property {ReturnType} add - * @property {ReturnType} list - * @property {ReturnType} rm + * @param {IPFS} ipfs */ -exports.bootstrap = { - add: require('./bootstrap/add'), - clear: require('./bootstrap/clear'), - list: require('./bootstrap/list'), - reset: require('./bootstrap/reset'), - rm: require('./bootstrap/rm') +const addEmptyDir = async (ipfs) => { + const node = new DAGNode(new UnixFs('directory').marshal()) + const cid = await ipfs.dag.put(node, { + version: 0, + format: multicodec.DAG_PB, + hashAlg: multicodec.SHA2_256, + preload: false + }) + + await ipfs.pin.add(cid) + + return cid } /** - * @typedef {ReturnType} Cat + * @returns {Options} */ -exports.cat = require('./cat') +const getDefaultOptions = () => ({ + init: true, + start: true, + EXPERIMENTAL: {}, + preload: { + enabled: !isTest, // preload by default, unless in test env + addresses: [ + '/dns4/node0.preload.ipfs.io/https', + '/dns4/node1.preload.ipfs.io/https', + '/dns4/node2.preload.ipfs.io/https', + '/dns4/node3.preload.ipfs.io/https' + ] + } +}) /** - * @typedef {ReturnType} Config - */ -exports.config = require('./config') - -/** - * @typedef {Object} DAG - * @property {ReturnType} get - * @property {ReturnType} put - * @property {ReturnType} resolve - * @property {ReturnType} tree - */ -exports.dag = { - get: require('./dag/get'), - put: require('./dag/put'), - resolve: require('./dag/resolve'), - tree: require('./dag/tree') -} - -/** @typedef {ReturnType} DHT */ -exports.dht = require('./dht') - -/** @typedef {ReturnType} DNS */ -exports.dns = require('./dns') - -/** @typedef {ReturnType} Files */ -exports.files = require('./files') - -/** @typedef {ReturnType} Get */ -exports.get = require('./get') - -/** @typedef {ReturnType} ID */ -exports.id = require('./id') - -/** @typedef {ReturnType} Init */ -exports.init = require('./init') - -/** @typedef {ReturnType} IsOnline */ -exports.isOnline = require('./is-online') - -/** - * @typedef {Object} Key - * @property {ReturnType} export - * @property {ReturnType} gen - * @property {ReturnType} import - * @property {ReturnType} info - * @property {ReturnType} list - * @property {ReturnType} rename - * @property {ReturnType} rm - */ - -exports.key = { - export: require('./key/export'), - gen: require('./key/gen'), - import: require('./key/import'), - info: require('./key/info'), - list: require('./key/list'), - rename: require('./key/rename'), - rm: require('./key/rm') -} - -/** @typedef {ReturnType} LibP2P */ -exports.libp2p = require('./libp2p') - -/** @typedef {ReturnType} LS */ -exports.ls = require('./ls') - -/** - * @typedef {Object} Name - * @property {ReturnType} publish - * @property {ReturnType} resolve - * @property {NamePubSub} pubsub + * @typedef {StorageOptions & IPFSOptions} Options * - * @typedef {Object} NamePubSub - * @property {ReturnType} cancel - * @property {ReturnType} state - * @property {ReturnType} subs - */ - -exports.name = { - publish: require('./name/publish'), - pubsub: { - cancel: require('./name/pubsub/cancel'), - state: require('./name/pubsub/state'), - subs: require('./name/pubsub/subs') - }, - resolve: require('./name/resolve') -} - -/** - * @typedef {Object} ObjectAPI - * @property {ReturnType} data - * @property {ReturnType} get - * @property {ReturnType} links - * @property {ReturnType} new - * @property {ReturnType} put - * @property {ReturnType} stat - * @property {ObjectPath} patch + * @typedef {Object} IPFSOptions + * Options argument can be used to specify advanced configuration. + * @property {InitOptions|boolean} [init=true] - Perform repo initialization steps when creating + * the IPFS node. + * Note that *initializing* a repo is different from creating an instance of + * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor + * sets many special properties when initializing a repo, so you should usually + * not try and call `repoInstance.init()` yourself. + * @property {boolean} [start=true] - If `false`, do not automatically + * start the IPFS node. Instead, you’ll need to manually call + * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) + * yourself. + * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. + * @property {boolean} [silent=false] - Prevents all logging output from the + * IPFS node. (Default: `false`) + * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] + * - Configure circuit relay (see the [circuit relay tutorial] + * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) + * to learn more). + * @property {boolean} [offline=false] - Run ipfs node offline. The node does + * not connect to the rest of the network but provides a local API. + * @property {PreloadOptions} [preload] - Configure remote preload nodes. + * The remote will preload content added on this node, and also attempt to + * preload objects requested by this node. + * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure + * experimental features. + * @property {IPFSConfig} [config] - Modify the default IPFS node config. This + * object will be *merged* with the default config; it will not replace it. + * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) + * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) + * in browsers) + * @property {IPLDOptions} [ipld] - Modify the default IPLD config. This object + * will be *merged* with the default config; it will not replace it. Check IPLD + * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information + * on the available options. (Default: [`ipld-nodejs.js`] + * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld-nodejs.js) in Node.js, [`ipld-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld-browser.js) + * in browsers) + * @property {LibP2POptions|Function} [libp2p] - The libp2p option allows you to build + * your libp2p node by configuration, or via a bundle function. If you are + * looking to just modify the below options, using the object format is the + * quickest way to get the default features of libp2p. If you need to create a + * more customized libp2p node, such as with custom transports or peer/content + * routers that need some of the ipfs data on startup, a custom bundle is a + * great way to achieve this. + * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). + * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) + * for the list of options libp2p supports. + * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) + * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in + * browsers. + * @property {boolean} [repoOwner] * - * @typedef {Object} ObjectPath - * @property {ReturnType} addLink - * @property {ReturnType} rmLink - * @property {ReturnType} appendData - * @property {ReturnType} setData - */ -exports.object = { - data: require('./object/data'), - get: require('./object/get'), - links: require('./object/links'), - new: require('./object/new'), - patch: { - addLink: require('./object/patch/add-link'), - appendData: require('./object/patch/append-data'), - rmLink: require('./object/patch/rm-link'), - setData: require('./object/patch/set-data') - }, - put: require('./object/put'), - stat: require('./object/stat') -} - -/** - * @typedef Pin - * @property {ReturnType} add - * @property {ReturnType} addAll - * @property {ReturnType} ls - * @property {ReturnType} rm - */ -exports.pin = { - add: require('./pin/add'), - addAll: require('./pin/add-all'), - ls: require('./pin/ls'), - rm: require('./pin/rm'), - rmAll: require('./pin/rm-all') -} - -/** - * @typedef {ReturnType} Ping - */ -exports.ping = require('./ping') - -/** - * @typedef {ReturnType} PubSub - */ -exports.pubsub = require('./pubsub') - -/** - * @typedef {ReturnType} Refs - * @typedef {ReturnType} LocalRefs - * @typedef {Refs & {local:LocalRefs}} RefsWithLocal - */ -exports.refs = Object.assign(require('./refs'), { local: require('./refs/local') }) - -/** - * @typedef {Object} Repo - * @property {ReturnType} gc - * @property {ReturnType} stat - * @property {ReturnType} version - */ -exports.repo = { - gc: require('./repo/gc'), - stat: require('./repo/stat'), - version: require('./repo/version') -} - -/** @typedef {ReturnType} Resolve */ -exports.resolve = require('./resolve') - -/** @typedef {ReturnType} Start */ -exports.start = require('./start') - -/** - * @typedef {Object} Stats - * @property {ReturnType} bw - */ -exports.stats = { - bw: require('./stats/bw') -} - -/** @typedef {ReturnType} Stop */ -exports.stop = require('./stop') - -/** - * @typedef {Object} Swarm - * @property {ReturnType} addrs - * @property {ReturnType} connect - * @property {ReturnType} disconnect - * @property {ReturnType} localAddrs - * @property {ReturnType} peers - */ -exports.swarm = { - addrs: require('./swarm/addrs'), - connect: require('./swarm/connect'), - disconnect: require('./swarm/disconnect'), - localAddrs: require('./swarm/local-addrs'), - peers: require('./swarm/peers') -} - -/** - * @typedef {ReturnType} Version - */ -exports.version = require('./version') - -/** - * @typedef {ReturnType} Preload - * @typedef {RWLock} GCLock + * @typedef {object} ExperimentalOptions + * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) + * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) * - * @typedef {Object} RWLock - * @property {() => Promise} readLock - * @property {() => Promise} writeLock * - * @typedef {() => void} Lock + * @typedef {import('./storage').StorageOptions} StorageOptions + * @typedef {import('../preload').Options} PreloadOptions + * @typedef {import('./ipld').Options} IPLDOptions + * @typedef {import('./libp2p').Options} LibP2POptions * - * // External library types - * @typedef {import('cids')} CID - * @typedef {import('peer-id')} PeerId - * @typedef {import('multiaddr')} Multiaddr + * @typedef {object} RelayOptions + * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) + * @property {object} [hop] + * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) + * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destin * - * // Justs pretending these things are typed & hopefully in the future they - * // wil be. - * @typedef {import('ipld')} IPLD - * @typedef {import('ipld').Config} IPLDConfig - * @typedef {import('ipld-block')} IPLDBlock - * @typedef {import('ipfs-repo')} IPFSRepo - * @typedef {import('ipfs-block-service')} IPFSBlockService - * @typedef {import('ipfs-bitswap')} IPFSBitSwap - * @typedef {import('libp2p')} LibP2PService - * @typedef {import('libp2p').Config} LibP2PConfig - */ - -/** - * @typedef {Object} IPFSAPI - * @property {Add} add - * @property {BitSwap} bitswap - * @property {Block} block - * @property {Bootstrap} bootstrap - * @property {Cat} cat - * @property {Config} config - * @property {DAG} dag - * @property {DHT} dht - * @property {DNS} dns - * @property {Files} files - * @property {Get} get - * @property {ID} id - * @property {IsOnline} isOnline - * @property {Key} key - * @property {LibP2P} libp2p - * @property {LS} ls - * @property {Name} name - * @property {ObjectAPI} object - * @property {Pin} pin - * @property {Ping} ping - * @property {PubSub} pubsub - * @property {Refs} refs - * @property {Repo} repo - * @property {Resolve} resolve - * @property {Stats} stats - * @property {Swarm} swarm - * @property {Version} version + * @typedef {import('./storage').InitOptions} InitOptions * - * @property {Init} init - * @property {Start} start - * @property {Stop} stop + * @typedef {import('./storage')} StorageAPI + * + * @typedef {import('./network').Options} NetworkOptions + * @typedef {Service} NetworkService + * @typedef {import('./storage').Repo} Repo + * @typedef {(...args:any[]) => void} Print + * @typedef {import('./storage').Keychain} Keychain + * @typedef {import('./config').IPFSConfig} IPFSConfig + * + * @typedef {import('peer-id')} PeerId + * @typedef {import('./libp2p').LibP2P} LibP2P + * @typedef {import('./pin/pin-manager')} PinManager + * @typedef {import('../interface/block-service').BlockService} BlockService + * @typedef {import('../interface/bitswap').Bitswap} BitSwap + * @typedef {import('./ipld').IPLD} IPLD + * @typedef {import('./gc-lock').GCLock} GCLock + * @typedef {import('../preload').Preload} Preload + * @typedef {import('../mfs-preload').MFSPreload} MFSPreload + * @typedef {import('./ipns')} IPNS + * @typedef {import('./pin')} Pin + * @typedef {import('./block')} Block + * @typedef {import('./dag').DagReader} DagReader + * @typedef {import('./dag')} Dag + * @typedef {ReturnType} Files + * @typedef {ReturnType} IsOnline + * @typedef {ReturnType} Resolve + * @typedef {ReturnType} Refs + * @typedef {ReturnType} DNS + * @typedef {import('./name')} Name + * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('cids')} CID + * @typedef {import('multiaddr')} Multiaddr + * @typedef {import('./ipld').Block} IPLDBlock */ diff --git a/packages/ipfs-core/src/components/init.js b/packages/ipfs-core/src/components/init.js deleted file mode 100644 index 219d443e34..0000000000 --- a/packages/ipfs-core/src/components/init.js +++ /dev/null @@ -1,561 +0,0 @@ -'use strict' - -const log = require('debug')('ipfs:components:init') -const PeerId = require('peer-id') -const uint8ArrayFromString = require('uint8arrays/from-string') -const uint8ArrayToString = require('uint8arrays/to-string') - -const mergeOptions = require('merge-options') -const getDefaultConfig = require('../runtime/config-nodejs.js') -const createRepo = require('../runtime/repo-nodejs') -const mortice = require('mortice') -const { DAGNode } = require('ipld-dag-pb') -const UnixFs = require('ipfs-unixfs') -const multicodec = require('multicodec') -const { - AlreadyInitializingError, - AlreadyInitializedError, - NotStartedError, - NotEnabledError -} = require('../errors') -const BlockService = require('ipfs-block-service') - -/** - * @typedef {import('.').IPLD} IPLD - */ -const Ipld = require('ipld') -const getDefaultIpldOptions = require('../runtime/ipld') - -const createPreloader = require('../preload') -const { ERR_REPO_NOT_INITIALIZED } = require('ipfs-repo').errors -const IPNS = require('../ipns') -const OfflineDatastore = require('../ipns/routing/offline-datastore') -const initAssets = require('../runtime/init-assets-nodejs') -const PinManager = require('./pin/pin-manager') -const Components = require('./') - -/** - * @param {Object} config - * @param {import('../api-manager')} config.apiManager - * @param {(...args:any[]) => void} config.print - * @param {ConstructorOptions} config.options - */ -module.exports = ({ - apiManager, - print, - options: constructorOptions -}) => -/** - * @param {Object} options - */ - async function init (options = {}) { - const { cancel } = apiManager.update({ init: () => { throw new AlreadyInitializingError() } }) - - try { - if (typeof constructorOptions.init === 'object') { - options = mergeOptions(constructorOptions.init, options) - } - - options.pass = options.pass || constructorOptions.pass - - if (constructorOptions.config) { - options.config = mergeOptions(options.config, constructorOptions.config) - } - - options.repo = options.repo || constructorOptions.repo - options.repoAutoMigrate = options.repoAutoMigrate || constructorOptions.repoAutoMigrate - - const repo = typeof options.repo === 'string' || options.repo == null - ? createRepo({ path: options.repo, autoMigrate: options.repoAutoMigrate, silent: constructorOptions.silent }) - : options.repo - - let isInitialized = true - - if (repo.closed) { - try { - await repo.open() - } catch (err) { - if (err.code === ERR_REPO_NOT_INITIALIZED) { - isInitialized = false - } else { - throw err - } - } - } - - if (!isInitialized && options.allowNew === false) { - throw new NotEnabledError('new repo initialization is not enabled') - } - - const { peerId, keychain } = isInitialized - ? await initExistingRepo(repo, options) - : await initNewRepo(repo, { ...options, print }) - - log('peer created') - - const blockService = new BlockService(repo) - const ipld = new Ipld(getDefaultIpldOptions(blockService, constructorOptions.ipld, log)) - - const preload = createPreloader(constructorOptions.preload) - await preload.start() - - // Make sure GC lock is specific to repo, for tests where there are - // multiple instances of IPFS - const gcLock = mortice(repo.path, { singleProcess: constructorOptions.repoOwner !== false }) - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const pinManager = new PinManager(repo, dag) - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - - if (!isInitialized && !options.emptyRepo) { - // add empty unixfs dir object (go-ipfs assumes this exists) - const emptyDirCid = await addEmptyDir({ dag, pin }) - - log('adding default assets') - await initAssets({ addAll, print }) - - log('initializing IPNS keyspace') - // Setup the offline routing for IPNS. - // This is primarily used for offline ipns modifications, such as the initializeKeyspace feature. - const offlineDatastore = new OfflineDatastore(repo) - const ipns = new IPNS(offlineDatastore, repo.datastore, peerId, keychain, { pass: options.pass }) - await ipns.initializeKeyspace(peerId.privKey, emptyDirCid.toString()) - } - - const api = createApi({ - add: Components.add({ addAll }), - addAll, - apiManager, - constructorOptions, - block, - blockService, - dag, - gcLock, - initOptions: options, - ipld, - keychain, - object, - peerId, - pin, - pinManager, - preload, - print, - repo - }) - - return apiManager.update(api, () => { throw new NotStartedError() }).api - } catch (err) { - cancel() - throw err - } - } - -/** - * @param {IPFSRepo} repo - * @param {Object} options - * @param {PrivateKey} options.privateKey - * @param {boolean} [options.emptyRepo] - * @param {number} [options.bits=2048] - Number of bits to use in the generated key - * @param {string[]} options.profiles - * @param {IPFSConfig} options.config - * @param {string} [options.pass] - * @param {(...args:any[]) => void} options.print - * @param {KeyType} [options.algorithm='RSA'] - */ -async function initNewRepo (repo, { privateKey, emptyRepo, algorithm, bits, profiles, config, pass, print }) { - emptyRepo = emptyRepo || false - bits = bits == null ? 2048 : Number(bits) - - config = mergeOptions(applyProfiles(profiles, getDefaultConfig()), config) - - // Verify repo does not exist yet - const exists = await repo.exists() - log('repo exists?', exists) - - if (exists === true) { - throw new Error('repo already exists') - } - - const peerId = await createPeerId({ privateKey, algorithm, bits, print }) - - log('identity generated') - - config.Identity = { - PeerID: peerId.toB58String(), - PrivKey: uint8ArrayToString(peerId.privKey.bytes, 'base64pad') - } - - log('peer identity: %s', config.Identity.PeerID) - - await repo.init(config) - await repo.open() - - log('repo opened') - - // Create libp2p for Keychain creation - const libp2p = Components.libp2p({ - peerId, - repo, - config, - keychainConfig: { - pass - } - }) - - if (libp2p.keychain && libp2p.keychain.opts) { - await libp2p.loadKeychain() - - await repo.config.set('Keychain', { - dek: libp2p.keychain.opts.dek - }) - } - - return { peerId, keychain: libp2p.keychain } -} - -/** - * @param {IPFSRepo} repo - * @param {Object} options - * @param {IPFSConfig} [options.config] - * @param {string[]} [options.profiles] - * @param {string} [options.pass] - */ -async function initExistingRepo (repo, { config: newConfig, profiles, pass }) { - let config = await repo.config.getAll() - - if (newConfig || profiles) { - if (profiles) { - config = applyProfiles(profiles, config) - } - if (newConfig) { - config = mergeOptions(config, newConfig) - } - await repo.config.set(config) - } - - const peerId = await PeerId.createFromPrivKey(config.Identity.PrivKey) - - const libp2p = Components.libp2p({ - peerId, - repo, - config, - keychainConfig: { - pass, - ...config.Keychain - } - }) - - libp2p.keychain && await libp2p.loadKeychain() - - return { peerId, keychain: libp2p.keychain } -} - -/** - * @param {Object} options - * @param {KeyType} [options.algorithm='RSA'] - * @param {PrivateKey} options.privateKey - * @param {number} options.bits - * @param {(...args:any[]) => void} options.print - */ -function createPeerId ({ privateKey, algorithm = 'RSA', bits, print }) { - if (privateKey) { - log('using user-supplied private-key') - return typeof privateKey === 'object' - ? privateKey - : PeerId.createFromPrivKey(uint8ArrayFromString(privateKey, 'base64pad')) - } else { - // Generate peer identity keypair + transform to desired format + add to config. - print('generating %s-bit (rsa only) %s keypair...', bits, algorithm) - // @ts-ignore - expects "Ed25519" | "RSA" | "secp256k1" instoad of string - return PeerId.create({ keyType: algorithm, bits }) - } -} - -async function addEmptyDir ({ dag, pin }) { - const node = new DAGNode(new UnixFs('directory').marshal()) - const cid = await dag.put(node, { - version: 0, - format: multicodec.DAG_PB, - hashAlg: multicodec.SHA2_256, - preload: false - }) - await pin.add(cid) - - return cid -} - -// Apply profiles (e.g. ['server', 'lowpower']) to config -function applyProfiles (profiles, config) { - return (profiles || []).reduce((config, name) => { - const profile = require('./config').profiles[name] - if (!profile) { - throw new Error(`Could not find profile with name '${name}'`) - } - log('applying profile %s', name) - return profile.transform(config) - }, config) -} - -function createApi ({ - add, - addAll, - apiManager, - constructorOptions, - block, - blockService, - dag, - gcLock, - initOptions, - ipld, - keychain, - object, - peerId, - pin, - pinManager, - preload, - print, - repo -}) { - const notStarted = async () => { // eslint-disable-line require-await - throw new NotStartedError() - } - - const resolve = Components.resolve({ ipld }) - const refs = Object.assign(Components.refs({ ipld, resolve, preload }), { - local: Components.refs.local({ repo }) - }) - - const api = { - add, - addAll, - bitswap: { - stat: notStarted, - unwant: notStarted, - wantlist: notStarted, - wantlistForPeer: notStarted - }, - bootstrap: { - add: Components.bootstrap.add({ repo }), - list: Components.bootstrap.list({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - block, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dns: Components.dns(), - files: Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }), - get: Components.get({ ipld, preload }), - id: Components.id({ peerId }), - init: async () => { throw new AlreadyInitializedError() }, // eslint-disable-line require-await - isOnline: Components.isOnline({}), - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - ls: Components.ls({ ipld, preload }), - object, - pin, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: Components.start({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }), - stats: { - bitswap: notStarted, - bw: notStarted, - repo: Components.repo.stat({ repo }) - }, - stop: () => {}, - swarm: { - addrs: notStarted, - connect: notStarted, - disconnect: notStarted, - localAddrs: Components.swarm.localAddrs({ multiaddrs: [] }), - peers: notStarted - }, - version: Components.version({ repo }) - } - - return api -} - -/** - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * - * @typedef {Object} ConstructorOptions - * Options argument can be used to specify advanced configuration. - * @property {RepoOption} [repo='~/.jsipfs'] - * @property {boolean} [repoAutoMigrate=true] - `js-ipfs` comes bundled with a - * tool that automatically migrates your IPFS repository when a new version is - * available. - * @property {Init} [init=true] - Perform repo initialization steps when creating - * the IPFS node. - * Note that *initializing* a repo is different from creating an instance of - * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor - * sets many special properties when initializing a repo, so you should usually - * not try and call `repoInstance.init()` yourself. - * @property {Start} [start=true] - If `false`, do not automatically - * start the IPFS node. Instead, you’ll need to manually call - * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) - * yourself. - * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. - * @property {boolean} [silent=false] - Prevents all logging output from the - * IPFS node. (Default: `false`) - * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] - * - Configure circuit relay (see the [circuit relay tutorial] - * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) - * to learn more). - * @property {boolean} [offline=false] - Run ipfs node offline. The node does - * not connect to the rest of the network but provides a local API. - * @property {PreloadOptions} [preload] - Configure remote preload nodes. - * The remote will preload content added on this node, and also attempt to - * preload objects requested by this node. - * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure - * experimental features. - * @property {object} [config] - Modify the default IPFS node config. This - * object will be *merged* with the default config; it will not replace it. - * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) - * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) - * in browsers) - * @property {import('.').IPLDConfig} [ipld] - Modify the default IPLD config. This object - * will be *merged* with the default config; it will not replace it. Check IPLD - * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information - * on the available options. (Default: [`ipld.js`] - * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld.js) - * in browsers) - * @property {object|Function} [libp2p] - The libp2p option allows you to build - * your libp2p node by configuration, or via a bundle function. If you are - * looking to just modify the below options, using the object format is the - * quickest way to get the default features of libp2p. If you need to create a - * more customized libp2p node, such as with custom transports or peer/content - * routers that need some of the ipfs data on startup, a custom bundle is a - * great way to achieve this. - * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). - * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) - * for the list of options libp2p supports. - * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) - * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in - * browsers. - * - * @property {boolean} [repoOwner] - */ - -/** - * @typedef {IPFSRepo|string} RepoOption - * The file path at which to store the IPFS node’s data. Alternatively, you - * can set up a customized storage system by providing an `ipfs.Repo` instance. - * - * @example - * ```js - * // Store data outside your user directory - * const node = await IPFS.create({ repo: '/var/ipfs/data' }) - * ``` - * - * @typedef {object} RelayOptions - * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) - * @property {object} [hop] - * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) - * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destination peer even if that peer is not yet connected to the relay. (Default: `false`) - * - * @typedef {object} PreloadOptions - * @property {boolean} [enabled] - Enable content preloading (Default: `true`) - * @property {number} [interval] - * @property {string[]} [addresses] - Multiaddr API addresses of nodes that should preload content. - * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. - * - * @typedef {object} ExperimentalOptions - * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) - * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) - * - * @typedef {Object} InitOptions - * @property {boolean} [emptyRepo=false] - Whether to remove built-in assets, - * like the instructional tour and empty mutable file system, from the repo. - * @property {number} [bits=2048] - Number of bits to use in the generated key - * pair (rsa only). - * @property {PrivateKey} [privateKey] - A pre-generated private key to use. - * **NOTE: This overrides `bits`.** - * @property {string} [pass] - A passphrase to encrypt keys. You should - * generally use the top-level `pass` option instead of the `init.pass` - * option (this one will take its value from the top-level option if not set). - * @property {string[]} [profiles] - Apply profile settings to config. - * @property {boolean} [allowNew=true] - Set to `false` to disallow - * initialization if the repo does not already exist. - * @property {IPFSConfig} [config] - * - * @typedef {import('./config').IPFSConfig} IPFSConfig - * @typedef {import('.').IPFSRepo} IPFSRepo - * - * @typedef {'RSA' | 'ed25519' | 'secp256k1'} KeyType - * - * @typedef {string|PeerId} PrivateKey - * Can be either a base64 string or a [PeerId](https://github.com/libp2p/js-peer-id) - * instance. - * - * @typedef {import('libp2p').Keychain} Keychain - */ diff --git a/packages/ipfs-core/src/components/ipld.js b/packages/ipfs-core/src/components/ipld.js new file mode 100644 index 0000000000..a8b465bc08 --- /dev/null +++ b/packages/ipfs-core/src/components/ipld.js @@ -0,0 +1,23 @@ +'use strict' + +const getDefaultIpldOptions = require('../runtime/ipld') +const Ipld = require('ipld') + +/** + * @param {Object} config + * @param {BlockService} config.blockService + * @param {Print} config.print + * @param {Options} [config.options] + * @returns {IPLD} + */ +const createIPLD = ({ blockService, print, options }) => + new Ipld(getDefaultIpldOptions(blockService, options, print)) +module.exports = createIPLD + +/** + * @typedef {import('../interface/ipld').IPLD} IPLD + * @typedef {import('../interface/ipld').Options} Options + * @typedef {import('../interface/block-service').BlockService} BlockService + * @typedef {import('../interface/basic').Block} Block + * @typedef {import('.').Print} Print + */ diff --git a/packages/ipfs-core/src/components/ipns.js b/packages/ipfs-core/src/components/ipns.js new file mode 100644 index 0000000000..0f26670896 --- /dev/null +++ b/packages/ipfs-core/src/components/ipns.js @@ -0,0 +1,110 @@ +'use strict' + +const IPNS = require('../ipns') +const routingConfig = require('../ipns/routing/config') +const OfflineDatastore = require('../ipns/routing/offline-datastore') +const { NotInitializedError, AlreadyInitializedError } = require('../errors') +const log = require('debug')('ipfs:components:ipns') + +class IPNSAPI { + /** + * @param {Object} [options] + * @param {string} [options.pass] + * @param {boolean} [options.offline] + * @param {LibP2POptions} [options.libp2p] + * @param {ExperimentalOptions} [options.EXPERIMENTAL] + */ + constructor (options = {}) { + this.options = options + this.offline = null + this.online = null + } + + getIPNS () { + const ipns = this.online || this.offline + if (ipns) { + return ipns + } else { + throw new NotInitializedError() + } + } + + get routing () { + return this.getIPNS().routing + } + + /** + * Activates IPNS subsystem in an ofline mode. If it was started once already + * it will throw an exception. + * + * This is primarily used for offline ipns modifications, such as the + * initializeKeyspace feature. + * + * @param {Object} config + * @param {import('.').Repo} config.repo + * @param {import('.').PeerId} config.peerId + * @param {import('.').Keychain} config.keychain + */ + startOffline ({ repo, peerId, keychain }) { + if (this.offline != null) { + throw new AlreadyInitializedError() + } + + log('initializing IPNS keyspace') + + const routing = new OfflineDatastore(repo) + const ipns = new IPNS(routing, repo.datastore, peerId, keychain, this.options) + + this.offline = ipns + } + + /** + * @param {Object} config + * @param {import('.').LibP2P} config.libp2p + * @param {import('.').Repo} config.repo + * @param {import('.').PeerId} config.peerId + * @param {import('.').Keychain} config.keychain + */ + startOnline ({ libp2p, repo, peerId, keychain }) { + if (this.online != null) { + throw new AlreadyInitializedError() + } + + const routing = routingConfig({ libp2p, repo, peerId, options: this.options }) + const ipns = new IPNS(routing, repo.datastore, peerId, keychain, this.options) + this.online = ipns + } + + async stop () { + const ipns = this.online + if (ipns) { + await ipns.republisher.stop() + } + } + + publish (privKey, value, lifetime) { + return this.getIPNS().publish(privKey, value, lifetime) + } + + resolve (name, options) { + return this.getIPNS().resolve(name, options) + } + + initializeKeyspace (privKey, value) { + return this.getIPNS().initializeKeyspace(privKey, value) + } +} +module.exports = IPNSAPI + +/** + * @typedef {Object} ExperimentalOptions + * @property {boolean} [ipnsPubsub] + * + * @typedef {Object} LibP2POptions + * @property {DHTConfig} [config] + * + * @typedef {Object} DHTConfig + * @property {boolean} [enabled] + * + * @typedef {import('../ipns')} IPNS + */ diff --git a/packages/ipfs-core/src/components/is-online.js b/packages/ipfs-core/src/components/is-online.js index 450b62e153..1e9593a0f8 100644 --- a/packages/ipfs-core/src/components/is-online.js +++ b/packages/ipfs-core/src/components/is-online.js @@ -2,7 +2,13 @@ /** * @param {Object} config - * @param {import('libp2p')} [config.libp2p] + * @param {import('.').NetworkService} config.network */ -module.exports = ({ libp2p }) => () => - Boolean(libp2p && libp2p.isStarted()) +module.exports = ({ network }) => + /** + * @returns {boolean} + */ + () => { + const net = network.try() + return net != null && net.libp2p.isStarted() + } diff --git a/packages/ipfs-core/src/components/key/export.js b/packages/ipfs-core/src/components/key/export.js index 0e136c7f70..056ba49c15 100644 --- a/packages/ipfs-core/src/components/key/export.js +++ b/packages/ipfs-core/src/components/key/export.js @@ -2,6 +2,33 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, password, options = {}) => keychain.exportKey(name, password, options)) + /** + * Remove a key + * + * @example + * ```js + * const pem = await ipfs.key.export('self', 'password') + * + * console.log(pem) + * // -----BEGIN ENCRYPTED PRIVATE KEY----- + * // MIIFDTA/BgkqhkiG9w0BBQ0wMjAaBgkqhkiG9w0BBQwwDQQIpdO40RVyBwACAWQw + * // ... + * // YA== + * // -----END ENCRYPTED PRIVATE KEY----- + * ``` + * @param {string} name - The name of the key to export + * @param {string} password - Password to set on the PEM output + * @param {import('.').AbortOptions} options + * @returns {Promise} - The string representation of the key + */ + const exportKey = async (name, password, options) => { + return await keychain.exportKey(name, password, options) + } + + return withTimeoutOption(exportKey) } diff --git a/packages/ipfs-core/src/components/key/gen.js b/packages/ipfs-core/src/components/key/gen.js index e3eaea5ff0..e8d679d7f7 100644 --- a/packages/ipfs-core/src/components/key/gen.js +++ b/packages/ipfs-core/src/components/key/gen.js @@ -2,8 +2,45 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options = {}) => { - return keychain.createKey(name, options.type || 'rsa', options.size || 2048) - }) + /** + * Generate a new key + * + * @example + * ```js + * const key = await ipfs.key.gen('my-key', { + * type: 'rsa', + * size: 2048 + * }) + * + * console.log(key) + * // { id: 'QmYWqAFvLWb2G5A69JGXui2JJXzaHXiUEmQkQgor6kNNcJ', + * // name: 'my-key' } + * ``` + * + * @param {string} name - The name to give the key + * @param {GenOptions & AbortOptions} options + * @returns {Promise<>} + */ + const gen = async (name, options = {}) => { + return await keychain.createKey(name, options.type || 'rsa', options.size || 2048) + } + + return withTimeoutOption(gen) } + +/** + * @typedef {Object} GenOptions + * @property {import('libp2p-crypto').KeyType} [type='RSA'] - The key type + * @property {number} [size=2048] - The key size in bits + * + * @typedef {Object} Key + * @property {string} id + * @property {string} name + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/import.js b/packages/ipfs-core/src/components/key/import.js index 1aaba9559f..37b439d798 100644 --- a/packages/ipfs-core/src/components/key/import.js +++ b/packages/ipfs-core/src/components/key/import.js @@ -2,6 +2,37 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, pem, password, options) => keychain.importKey(name, pem, password, options)) + /** + * Remove a key + * + * @example + * ```js + * const key = await ipfs.key.import('clone', pem, 'password') + * + * console.log(key) + * // { id: 'QmQRiays958UM7norGRQUG3tmrLq8pJdmJarwYSk2eLthQ', + * // name: 'clone' } + * ``` + * @param {string} name - The name of the key to import + * @param {string} pem - The PEM encoded key + * @param {string} password - The password that protects the PEM key + * @param {import('.').AbortOptions} options + * @returns {Promise} - An object that describes the new key + */ + const importKey = async (name, pem, password, options) => { + return await keychain.importKey(name, pem, password, options) + } + + return withTimeoutOption(importKey) } + +/** + * @typedef {Object} ImportedKey + * @property {string} id + * @property {string} name + */ diff --git a/packages/ipfs-core/src/components/key/index.js b/packages/ipfs-core/src/components/key/index.js new file mode 100644 index 0000000000..586cdfd720 --- /dev/null +++ b/packages/ipfs-core/src/components/key/index.js @@ -0,0 +1,31 @@ +'use strict' + +const createExport = require('./export') +const createGen = require('./gen') +const createImport = require('./import') +const createInfo = require('./info') +const createList = require('./list') +const createRename = require('./rename') +const createRm = require('./rm') + +class KeyAPI { + /** + * @param {Object} config + * @param {Keychain} config.keychain + */ + constructor ({ keychain }) { + this.gen = createGen({ keychain }) + this.list = createList({ keychain }) + this.rm = createRm({ keychain }) + this.rename = createRename({ keychain }) + this.export = createExport({ keychain }) + this.import = createImport({ keychain }) + this.info = createInfo({ keychain }) + } +} +module.exports = KeyAPI + +/** + * @typedef {import('..').Keychain} Keychain + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/info.js b/packages/ipfs-core/src/components/key/info.js index 209ee27eba..4f396e443c 100644 --- a/packages/ipfs-core/src/components/key/info.js +++ b/packages/ipfs-core/src/components/key/info.js @@ -2,6 +2,24 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options) => keychain.findKeyByName(name, options)) + /** + * @param {string} name + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const info = async (name, options = {}) => { + return await keychain.findKeyByName(name, options) + } + + return withTimeoutOption(info) } + +/** + * @typedef {import('./gen').Key} Key + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/list.js b/packages/ipfs-core/src/components/key/list.js index 576132f7b6..2fcda87a30 100644 --- a/packages/ipfs-core/src/components/key/list.js +++ b/packages/ipfs-core/src/components/key/list.js @@ -2,6 +2,41 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((options) => keychain.listKeys(options)) + /** + * List all the keys + * + * @example + * ```js + * const keys = await ipfs.key.list() + * + * console.log(keys) + * // [ + * // { id: 'QmTe4tuceM2sAmuZiFsJ9tmAopA8au71NabBDdpPYDjxAb', + * // name: 'self' }, + * // { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W', + * // name: 'my-key' } + * // ] + * ``` + * + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const list = async (options = {}) => { + return await keychain.listKeys(options) + } + + return withTimeoutOption(list) } + +/** + * @typedef {Object} KeyEntry + * @property {string} name - The name of the key + * @property {string} hash - The hash of the key + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/rename.js b/packages/ipfs-core/src/components/key/rename.js index acd6a0c72f..7b4da5bedb 100644 --- a/packages/ipfs-core/src/components/key/rename.js +++ b/packages/ipfs-core/src/components/key/rename.js @@ -2,8 +2,30 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption(async (oldName, newName, options) => { + /** + * Rename a key + * + * @example + * ```js + * const key = await ipfs.key.rename('my-key', 'my-new-key') + * + * console.log(key) + * // { id: 'Qmd4xC46Um6s24MradViGLFtMitvrR4SVexKUgPgFjMNzg', + * // was: 'my-key', + * // now: 'my-new-key', + * // overwrite: false } + * ``` + * @param {string} oldName - The current key name + * @param {string} newName - The desired key name + * @param {AbortOptions} [options] + * @returns {Promise} + */ + const rename = async (oldName, newName, options = {}) => { const key = await keychain.renameKey(oldName, newName, options) return { was: oldName, @@ -11,5 +33,17 @@ module.exports = ({ keychain }) => { id: key.id, overwrite: false } - }) + } + + return withTimeoutOption(rename) } + +/** + * @typedef {Object} RenamedKey + * @property {string} was - The name of the key + * @property {string} now - The hash of the key + * @property {string} id + * @property {boolean} overwrite + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/key/rm.js b/packages/ipfs-core/src/components/key/rm.js index bd24943021..1b91de0466 100644 --- a/packages/ipfs-core/src/components/key/rm.js +++ b/packages/ipfs-core/src/components/key/rm.js @@ -2,6 +2,36 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Keychain} config.keychain + */ module.exports = ({ keychain }) => { - return withTimeoutOption((name, options) => keychain.removeKey(name, options)) + /** + * Remove a key + * + * @example + * ```js + * const key = await ipfs.key.rm('my-key') + * + * console.log(key) + * // { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W', + * // name: 'my-key' } + * ``` + * + * @param {string} name - The name of the key to remove + * @param {import('.').AbortOptions} options + * @returns {Promise} - An object that describes the removed key + */ + const rm = async (name, options) => { + return await keychain.removeKey(name, options) + } + + return withTimeoutOption(rm) } + +/** + * @typedef {Object} RemovedKey + * @property {string} name - The name of the key + * @property {string} id - The hash of the key + */ diff --git a/packages/ipfs-core/src/components/libp2p.js b/packages/ipfs-core/src/components/libp2p.js index 32faca1dd2..a181bc3924 100644 --- a/packages/ipfs-core/src/components/libp2p.js +++ b/packages/ipfs-core/src/components/libp2p.js @@ -7,13 +7,13 @@ const PubsubRouters = require('../runtime/libp2p-pubsub-routers-nodejs') /** * @param {Object} config - * @param {import('.').IPFSRepo} config.repo - * @param {Object} [config.options] - * @param {import('.').PeerId} [config.peerId] - * @param {string[]} [config.multiaddrs] + * @param {Repo} config.repo + * @param {Options} [config.options] + * @param {PeerId} [config.peerId] + * @param {string[]|Multiaddr[]} [config.multiaddrs] * @param {{pass?:string}} [config.keychainConfig] - * @param {import('.').LibP2PConfig} [config.config] - * @returns {import('.').LibP2PService} + * @param {Config} [config.config] + * @returns {LibP2P} */ module.exports = ({ options = {}, @@ -141,3 +141,12 @@ function getLibp2pOptions ({ options, config, datastore, keys, keychainConfig, p return libp2pConfig } + +/** + * @typedef {import('.').Repo} Repo + * @typedef {import('.').Multiaddr} Multiaddr + * @typedef {import('.').PeerId} PeerId + * @typedef {import('libp2p')} LibP2P + * @typedef {import('libp2p').Options} Options + * @typedef {import('libp2p').Config} Config + */ diff --git a/packages/ipfs-core/src/components/ls.js b/packages/ipfs-core/src/components/ls.js index e1cb13b2bd..88b2866993 100644 --- a/packages/ipfs-core/src/components/ls.js +++ b/packages/ipfs-core/src/components/ls.js @@ -6,8 +6,8 @@ const { normalizeCidPath, mapFile, withTimeoutOption } = require('../utils') /** * @param {Object} config - * @param {import('.').IPLD} config.ipld - * @param {import('.').Preload} config.preload + * @param {import('./core').IPLD} config.ipld + * @param {import('./core').Preload} config.preload */ module.exports = function ({ ipld, preload }) { /** diff --git a/packages/ipfs-core/src/components/name/index.js b/packages/ipfs-core/src/components/name/index.js new file mode 100644 index 0000000000..a8bb3b7faf --- /dev/null +++ b/packages/ipfs-core/src/components/name/index.js @@ -0,0 +1,41 @@ +'use strict' + +const createPublishAPI = require('./publish') +const createResolveAPI = require('./resolve') +const PubSubAPI = require('./pubsub') +class NameAPI { + /** + * @param {Object} config + * @param {IPNS} config.ipns + * @param {PeerId} config.peerId + * @param {Options} config.options + * @param {DagReader} config.dagReader + * @param {IsOnline} config.isOnline + * @param {Keychain} config.keychain + * @param {DNS} config.dns + */ + constructor ({ dns, ipns, dagReader, peerId, isOnline, keychain, options }) { + this.publish = createPublishAPI({ ipns, dagReader, peerId, isOnline, keychain }) + this.resolve = createResolveAPI({ dns, ipns, peerId, isOnline, options }) + this.pubsub = new PubSubAPI({ ipns, options: options.EXPERIMENTAL }) + } +} +module.exports = NameAPI + +/** + * @typedef {ResolveOptions & ExperimentalOptions} Options + * + * @typedef {Object} ExperimentalOptions + * @property {PubSubOptions} [EXPERIMENTAL] + * + * @typedef {import('./pubsub').Options} PubSubOptions + * @typedef {import('./resolve').ResolveOptions} ResolveOptions + * + * @typedef {import('..').IPNS} IPNS + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').DagReader} DagReader + * @typedef {import('..').Keychain} Keychain + * @typedef {import('..').IsOnline} IsOnline + * @typedef {import('..').DNS} DNS + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/name/publish.js b/packages/ipfs-core/src/components/name/publish.js index 217d2e41ca..8e2415e644 100644 --- a/packages/ipfs-core/src/components/name/publish.js +++ b/packages/ipfs-core/src/components/name/publish.js @@ -16,13 +16,13 @@ const { resolvePath } = require('./utils') * IPNS - Inter-Planetary Naming System * * @param {Object} config - * @param {import('../../ipns')} config.ipns - * @param {import('../index').DAG} config.dag - * @param {import('peer-id')} config.peerId - * @param {import('../index').IsOnline} config.isOnline - * @param {import('../init').Keychain} config.keychain + * @param {import('.').IPNS} config.ipns + * @param {import('.').DagReader} config.dagReader + * @param {import('.').PeerId} config.peerId + * @param {import('.').IsOnline} config.isOnline + * @param {import('.').Keychain} config.keychain */ -module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { +module.exports = ({ ipns, dagReader, peerId, isOnline, keychain }) => { const lookupKey = async keyName => { if (keyName === 'self') { return peerId.privKey @@ -93,7 +93,7 @@ module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { const results = await Promise.all([ // verify if the path exists, if not, an error will stop the execution lookupKey(key), - resolve ? resolvePath({ ipns, dag }, value) : Promise.resolve() + resolve ? resolvePath({ ipns, dagReader }, value) : Promise.resolve() ]) // Start publishing process @@ -123,5 +123,5 @@ module.exports = ({ ipns, dag, peerId, isOnline, keychain }) => { * @property {string} name * @property {string} value * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/name/pubsub/cancel.js b/packages/ipfs-core/src/components/name/pubsub/cancel.js index 1eb1b50985..288a0ec012 100644 --- a/packages/ipfs-core/src/components/name/pubsub/cancel.js +++ b/packages/ipfs-core/src/components/name/pubsub/cancel.js @@ -5,15 +5,15 @@ const { withTimeoutOption } = require('../../../utils') /** * @param {Object} config - * @param {import('../../../ipns')} config.ipns - * @param {import('../../init').ConstructorOptions} config.options + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] */ -module.exports = ({ ipns, options: constructorOptions }) => { +module.exports = ({ ipns, options: routingOptions }) => { /** * Cancel a name subscription. * * @param {string} name - The name of the subscription to cancel. - * @param {AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise<{ canceled: boolean }>} * @example * ```js @@ -24,13 +24,9 @@ module.exports = ({ ipns, options: constructorOptions }) => { * ``` */ async function cancel (name, options) { // eslint-disable-line require-await - const pubsub = getPubsubRouting(ipns, constructorOptions) + const pubsub = getPubsubRouting(ipns, routingOptions) return pubsub.cancel(name, options) } return withTimeoutOption(cancel) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/index.js b/packages/ipfs-core/src/components/name/pubsub/index.js new file mode 100644 index 0000000000..1ea174e082 --- /dev/null +++ b/packages/ipfs-core/src/components/name/pubsub/index.js @@ -0,0 +1,25 @@ +'use strict' + +const createCancelAPI = require('./cancel') +const createStateAPI = require('./state') +const createSubsAPI = require('./subs') + +class PubSubAPI { + /** + * @param {Object} config + * @param {IPNS} config.ipns + * @param {Options} [config.options] + */ + constructor ({ ipns, options }) { + this.cancel = createCancelAPI({ ipns, options }) + this.state = createStateAPI({ ipns, options }) + this.subs = createSubsAPI({ ipns, options }) + } +} +module.exports = PubSubAPI + +/** + * @typedef {import('..').IPNS} IPNS + * @typedef {import('./utils').PubSubRoutingOptions} Options + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/name/pubsub/state.js b/packages/ipfs-core/src/components/name/pubsub/state.js index f122da1396..3448f183b8 100644 --- a/packages/ipfs-core/src/components/name/pubsub/state.js +++ b/packages/ipfs-core/src/components/name/pubsub/state.js @@ -2,12 +2,16 @@ const { getPubsubRouting } = require('./utils') const { withTimeoutOption } = require('../../../utils') - -module.exports = ({ ipns, options: constructorOptions }) => { +/** + * @param {Object} config + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] + */ +module.exports = ({ ipns, options: routingOptions }) => { /** * Query the state of IPNS pubsub. * - * @param {AbortOptions} [_options] + * @param {import('.').AbortOptions} [_options] * @returns {Promise<{ enabled: boolean }>} * ```js * const result = await ipfs.name.pubsub.state() @@ -17,7 +21,7 @@ module.exports = ({ ipns, options: constructorOptions }) => { */ async function state (_options) { // eslint-disable-line require-await try { - return { enabled: Boolean(getPubsubRouting(ipns, constructorOptions)) } + return { enabled: Boolean(getPubsubRouting(ipns, routingOptions)) } } catch (err) { return { enabled: false } } @@ -25,7 +29,3 @@ module.exports = ({ ipns, options: constructorOptions }) => { return withTimeoutOption(state) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/subs.js b/packages/ipfs-core/src/components/name/pubsub/subs.js index 6e84f8ec3c..0a5543f8ca 100644 --- a/packages/ipfs-core/src/components/name/pubsub/subs.js +++ b/packages/ipfs-core/src/components/name/pubsub/subs.js @@ -3,11 +3,16 @@ const { getPubsubRouting } = require('./utils') const { withTimeoutOption } = require('../../../utils') -module.exports = ({ ipns, options: constructorOptions }) => { +/** + * @param {Object} config + * @param {import('.').IPNS} config.ipns + * @param {import('.').Options} [config.options] + */ +module.exports = ({ ipns, options: routingOptions }) => { /** * Show current name subscriptions. * - * @param {AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise} * @example * ```js @@ -17,13 +22,9 @@ module.exports = ({ ipns, options: constructorOptions }) => { * ``` */ async function subs (options) { // eslint-disable-line require-await - const pubsub = getPubsubRouting(ipns, constructorOptions) + const pubsub = getPubsubRouting(ipns, routingOptions) return pubsub.getSubscriptions(options) } return withTimeoutOption(subs) } - -/** - * @typedef {import('../../../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/name/pubsub/utils.js b/packages/ipfs-core/src/components/name/pubsub/utils.js index ee53a96f9c..43e7e69ae9 100644 --- a/packages/ipfs-core/src/components/name/pubsub/utils.js +++ b/packages/ipfs-core/src/components/name/pubsub/utils.js @@ -3,9 +3,14 @@ const IpnsPubsubDatastore = require('../../../ipns/routing/pubsub-datastore') const errcode = require('err-code') -// Get pubsub from IPNS routing +/** + * Get pubsub from IPNS routing + * + * @param {import('.').IPNS} ipns + * @param {PubSubRoutingOptions} [options] + */ exports.getPubsubRouting = (ipns, options) => { - if (!ipns || !(options.EXPERIMENTAL && options.EXPERIMENTAL.ipnsPubsub)) { + if (!ipns || !(options && options.ipnsPubsub)) { throw errcode(new Error('IPNS pubsub subsystem is not enabled'), 'ERR_IPNS_PUBSUB_NOT_ENABLED') } @@ -23,3 +28,8 @@ exports.getPubsubRouting = (ipns, options) => { return pubsub } + +/** + * @typedef {Object} PubSubRoutingOptions + * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) + */ diff --git a/packages/ipfs-core/src/components/name/resolve.js b/packages/ipfs-core/src/components/name/resolve.js index ad3eb704f5..59f3f03de5 100644 --- a/packages/ipfs-core/src/components/name/resolve.js +++ b/packages/ipfs-core/src/components/name/resolve.js @@ -2,8 +2,7 @@ const debug = require('debug') const errcode = require('err-code') -/** @type {typeof Object.assign} */ -const mergeOptions = require('merge-options') +const { mergeOptions } = require('../../utils') const CID = require('cids') const isDomain = require('is-domain-name') @@ -28,18 +27,18 @@ const appendRemainder = (result, remainder) => * IPNS - Inter-Planetary Naming System * * @param {Object} config - * @param {import('../index').DNS} config.dns - * @param {import('../../ipns')} config.ipns - * @param {import('peer-id')} config.peerId - * @param {import('../index').IsOnline} config.isOnline - * @param {{offline?:boolean}} config.options + * @param {import('.').DNS} config.dns + * @param {import('.').IPNS} config.ipns + * @param {import('.').PeerId} config.peerId + * @param {import('.').IsOnline} config.isOnline + * @param {ResolveOptions} config.options */ -module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) => { +module.exports = ({ dns, ipns, peerId, isOnline, options: { offline } }) => { /** * Given a key, query the DHT for its best value. * * @param {string} name - ipns name to resolve. Defaults to your node's peerID. - * @param {ResolveOptions} [options] + * @param {Options & AbortOptions} [options] * @returns {AsyncIterable} * @example * ```js @@ -58,8 +57,6 @@ module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) recursive: true }, options) - const { offline } = constructorOptions - // TODO: params related logic should be in the core implementation if (offline && options.nocache) { throw errcode(new Error('cannot specify both offline and nocache'), 'ERR_NOCACHE_AND_OFFLINE') @@ -104,11 +101,12 @@ module.exports = ({ dns, ipns, peerId, isOnline, options: constructorOptions }) /** * IPFS resolve options. * - * @typedef {ResolveSettings & AbortOptions} ResolveOptions - * - * @typedef {Object} ResolveSettings + * @typedef {Object} Options * @property {boolean} [options.nocache=false] - do not use cached entries. * @property {boolean} [options.recursive=true] - resolve until the result is not an IPNS name. * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {Object} ResolveOptions + * @property {boolean} [offline] + * + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/name/utils.js b/packages/ipfs-core/src/components/name/utils.js index acfb307fbd..b2b5970f78 100644 --- a/packages/ipfs-core/src/components/name/utils.js +++ b/packages/ipfs-core/src/components/name/utils.js @@ -2,14 +2,26 @@ const isIPFS = require('is-ipfs') -// resolves the given path by parsing out protocol-specific entries -// (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node -exports.resolvePath = ({ ipns, dag }, name) => { +/** + * resolves the given path by parsing out protocol-specific entries + * (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node + * + * @param {Object} context + * @param {IPNS} context.ipns + * @param {DagReader} context.dagReader + * @param {string} name + */ +exports.resolvePath = ({ ipns, dagReader }, name) => { // ipns path if (isIPFS.ipnsPath(name)) { return ipns.resolve(name) } // ipfs path - return dag.get(name.substring('/ipfs/'.length)) + return dagReader.get(name.substring('/ipfs/'.length)) } + +/** + * @typedef {import('.').DagReader} DagReader + * @typedef {import('.').IPNS} IPNS + */ diff --git a/packages/ipfs-core/src/components/network.js b/packages/ipfs-core/src/components/network.js new file mode 100644 index 0000000000..d8b7575aac --- /dev/null +++ b/packages/ipfs-core/src/components/network.js @@ -0,0 +1,129 @@ +'use strict' + +const IPFSBitswap = require('ipfs-bitswap') +const createLibP2P = require('./libp2p') +const Multiaddr = require('multiaddr') +const errCode = require('err-code') + +class Network { + /** + * @param {PeerId} peerId + * @param {LibP2P} libp2p + * @param {BitSwap} bitswap + */ + constructor (peerId, libp2p, bitswap) { + this.peerId = peerId + this.libp2p = libp2p + this.bitswap = bitswap + } + + /** + * @param {Options} options + */ + static async start ({ peerId, repo, print, pass }) { + // Need to ensure that repo is open as it could have been closed between + // `init` and `start`. + if (repo.closed) { + await repo.open() + } + + const config = await repo.config.getAll() + + const libp2p = createLibP2P({ + repo, + peerId, + multiaddrs: readAddrs(peerId, config), + config, + keychainConfig: { + pass, + ...config.Keychain + } + }) + + if (libp2p.keychain) { + await libp2p.loadKeychain() + + // If it is a new repo we don't have Keychain persisted yet. + if (libp2p.keychain.opts && !config.Keychain) { + await repo.config.set('Keychain', { + dek: libp2p.keychain.opts.dek + }) + } + } + + await libp2p.start() + + for (const ma of libp2p.transportManager.getAddrs()) { + print(`Swarm listening on ${ma}/p2p/${peerId.toB58String()}`) + } + + const bitswap = new IPFSBitswap(libp2p, repo.blocks, { statsEnabled: true }) + await bitswap.start() + + return new Network(peerId, libp2p, bitswap) + } + + /** + * @param {Network} network + */ + // eslint-disable-next-line require-await + static async stop (network) { + network.bitswap.stop() + } +} +module.exports = Network + +/** + * + * @param {PeerId} peerId + * @param {IPFSConfig} config + * @returns {Multiaddr[]} + */ +const readAddrs = (peerId, config) => { + const peerIdStr = peerId.toB58String() + /** @type {Multiaddr[]} */ + const addrs = [] + const swarm = (config.Addresses && config.Addresses.Swarm) || [] + for (const addr of swarm) { + let ma = Multiaddr(addr) + + // Temporary error for users migrating using websocket-star multiaddrs for listenning on libp2p + // websocket-star support was removed from ipfs and libp2p + if (ma.protoCodes().includes(WEBSOCKET_STAR_PROTO_CODE)) { + throw errCode(new Error('websocket-star swarm addresses are not supported. See https://github.com/ipfs/js-ipfs/issues/2779'), 'ERR_WEBSOCKET_STAR_SWARM_ADDR_NOT_SUPPORTED') + } + + // multiaddrs that go via a signalling server or other intermediary (e.g. stardust, + // webrtc-star) can have the intermediary's peer ID in the address, so append our + // peer ID to the end of it + const maId = ma.getPeerId() + if (maId && maId !== peerIdStr) { + ma = ma.encapsulate(`/p2p/${peerIdStr}`) + } + + addrs.push(ma) + } + + return addrs +} + +const WEBSOCKET_STAR_PROTO_CODE = 479 +/** + * @typedef {Object} Online + * @property {LibP2P} libp2p + * @property {BitSwap} bitswap + * + * @typedef {Object} Options + * @property {PeerId} options.peerId + * @property {Repo} options.repo + * @property {Print} options.print + * @property {string} [options.pass] + * + * @typedef {import('.').IPFSConfig} IPFSConfig + * @typedef {import('.').Repo} Repo + * @typedef {import('.').Print} Print + * @typedef {import('.').LibP2P} LibP2P + * @typedef {import('../interface/bitswap').Bitswap} BitSwap + * @typedef {import('.').PeerId} PeerId + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/data.js b/packages/ipfs-core/src/components/object/data.js index 7143daf308..a41d13ecc8 100644 --- a/packages/ipfs-core/src/components/object/data.js +++ b/packages/ipfs-core/src/components/object/data.js @@ -2,10 +2,29 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { const get = require('./get')({ ipld, preload }) - return withTimeoutOption(async function data (multihash, options) { + + /** + * @param {import('.').CID} multihash + * @param {GetOptions & AbortOptions} options + * @returns {Promise} + */ + async function data (multihash, options) { const node = await get(multihash, options) return node.Data - }) + } + + return withTimeoutOption(data) } + +/** + * @typedef {import('cids')} CID + * @typedef {import('./get').GetOptions} GetOptions + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/get.js b/packages/ipfs-core/src/components/object/get.js index 70b8d6785c..dc45835349 100644 --- a/packages/ipfs-core/src/components/object/get.js +++ b/packages/ipfs-core/src/components/object/get.js @@ -5,6 +5,11 @@ const errCode = require('err-code') const { withTimeoutOption } = require('../../utils') const uint8ArrayFromString = require('uint8arrays/from-string') +/** + * @param {string|Uint8Array|CID} multihash + * @param {string} [enc] + * @returns {string|Uint8Array} + */ function normalizeMultihash (multihash, enc) { if (typeof multihash === 'string') { if (enc === 'base58' || !enc) { @@ -19,8 +24,18 @@ function normalizeMultihash (multihash, enc) { throw new Error('unsupported multihash') } +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { - return withTimeoutOption(async function get (multihash, options = {}) { // eslint-disable-line require-await + /** + * + * @param {CID} multihash + * @param {GetOptions & AbortOptions} [options] + */ + async function get (multihash, options = {}) { // eslint-disable-line require-await let mh, cid try { @@ -44,5 +59,16 @@ module.exports = ({ ipld, preload }) => { } return ipld.get(cid, { signal: options.signal }) - }) + } + + return withTimeoutOption(get) } + +/** + * @typedef {Object} GetOptions + * @property {boolean} [preload] + * @property {number} [cidVersion] + * @property {string} [enc] + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/index.js b/packages/ipfs-core/src/components/object/index.js new file mode 100644 index 0000000000..210ab36f3d --- /dev/null +++ b/packages/ipfs-core/src/components/object/index.js @@ -0,0 +1,39 @@ +'use strict' + +const createData = require('./data') +const createGet = require('./get') +const createLinks = require('./links') +const createNew = require('./new') +const createPut = require('./put') +const createStat = require('./stat') +const ObjectPatchAPI = require('./patch') + +class ObjectAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {GCLock} config.gcLock + * @param {Dag} config.dag + */ + constructor ({ ipld, preload, dag, gcLock }) { + this.data = createData({ ipld, preload }) + this.get = createGet({ ipld, preload }) + this.links = createLinks({ dag }) + this.new = createNew({ ipld, preload }) + this.put = createPut({ ipld, preload, gcLock }) + this.stat = createStat({ ipld, preload }) + this.patch = new ObjectPatchAPI({ ipld, preload, gcLock }) + } +} + +module.exports = ObjectAPI + +/** + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Dag} Dag + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/links.js b/packages/ipfs-core/src/components/object/links.js index 8d9afb1929..6e3eef5548 100644 --- a/packages/ipfs-core/src/components/object/links.js +++ b/packages/ipfs-core/src/components/object/links.js @@ -5,6 +5,11 @@ const DAGLink = dagPB.DAGLink const CID = require('cids') const { withTimeoutOption } = require('../../utils') +/** + * @param {any} node + * @param {DAGLink[]} [links] + * @returns {DAGLink[]} + */ function findLinks (node, links = []) { for (const key in node) { const val = node[key] @@ -35,8 +40,17 @@ function findLinks (node, links = []) { return links } +/** + * @param {Object} config + * @param {import('.').Dag} config.dag + */ module.exports = ({ dag }) => { - return withTimeoutOption(async function links (multihash, options = {}) { + /** + * @param {CID} multihash + * @param {import('.').AbortOptions} options + * @returns {Promise} + */ + async function links (multihash, options = {}) { const cid = new CID(multihash) const result = await dag.get(cid, options) @@ -53,5 +67,7 @@ module.exports = ({ dag }) => { } throw new Error(`Cannot resolve links from codec ${cid.codec}`) - }) + } + + return withTimeoutOption(links) } diff --git a/packages/ipfs-core/src/components/object/new.js b/packages/ipfs-core/src/components/object/new.js index f7e24fdbbc..04ed5f56c3 100644 --- a/packages/ipfs-core/src/components/object/new.js +++ b/packages/ipfs-core/src/components/object/new.js @@ -6,8 +6,18 @@ const multicodec = require('multicodec') const Unixfs = require('ipfs-unixfs') const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { - return withTimeoutOption(async function _new (options = {}) { + /** + * + * @param {NewOptions & AbortOptions} options + * @returns {Promise} + */ + async function _new (options = {}) { let data if (options.template) { @@ -33,5 +43,19 @@ module.exports = ({ ipld, preload }) => { } return cid - }) + } + + return withTimeoutOption(_new) } + +/** + * @typedef {Object} NewOptions + * @property {string} [template] + * @property {boolean} [recursive] + * @property {boolean} [nocache] + * @property {boolean} [preload] + * @property {string} [enc] + * + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/patch/add-link.js b/packages/ipfs-core/src/components/object/patch/add-link.js index 77f0d26708..933967b245 100644 --- a/packages/ipfs-core/src/components/object/patch/add-link.js +++ b/packages/ipfs-core/src/components/object/patch/add-link.js @@ -2,13 +2,21 @@ const { withTimeoutOption } = require('../../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function addLink (multihash, link, options) { + async function addLink (multihash, link, options) { const node = await get(multihash, options) node.addLink(link) return put(node, options) - }) + } + + return withTimeoutOption(addLink) } diff --git a/packages/ipfs-core/src/components/object/patch/append-data.js b/packages/ipfs-core/src/components/object/patch/append-data.js index df31ca668c..9ab5126133 100644 --- a/packages/ipfs-core/src/components/object/patch/append-data.js +++ b/packages/ipfs-core/src/components/object/patch/append-data.js @@ -4,13 +4,20 @@ const { DAGNode } = require('ipld-dag-pb') const { withTimeoutOption } = require('../../../utils') const uint8ArrayConcat = require('uint8arrays/concat') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - - return withTimeoutOption(async function appendData (multihash, data, options) { + async function appendData (multihash, data, options) { const node = await get(multihash, options) const newData = uint8ArrayConcat([node.Data, data]) return put(new DAGNode(newData, node.Links), options) - }) + } + + return withTimeoutOption(appendData) } diff --git a/packages/ipfs-core/src/components/object/patch/index.js b/packages/ipfs-core/src/components/object/patch/index.js new file mode 100644 index 0000000000..5166c6b33e --- /dev/null +++ b/packages/ipfs-core/src/components/object/patch/index.js @@ -0,0 +1,30 @@ +'use strict' + +const createAddLink = require('./add-link') +const createAppendData = require('./append-data') +const createRmLink = require('./rm-link') +const createSetData = require('./set-data') + +class ObjectPatchAPI { + /** + * @param {Object} config + * @param {IPLD} config.ipld + * @param {Preload} config.preload + * @param {GCLock} config.gcLock + */ + constructor ({ ipld, preload, gcLock }) { + this.addLink = createAddLink({ ipld, preload, gcLock }) + this.appendData = createAppendData({ ipld, preload, gcLock }) + this.rmLink = createRmLink({ ipld, preload, gcLock }) + this.setData = createSetData({ ipld, preload, gcLock }) + } +} +module.exports = ObjectPatchAPI + +/** + * @typedef {import('..').IPLD} IPLD + * @typedef {import('..').Preload} Preload + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').CID} CID + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/patch/rm-link.js b/packages/ipfs-core/src/components/object/patch/rm-link.js index 9d028017d0..920632fcf1 100644 --- a/packages/ipfs-core/src/components/object/patch/rm-link.js +++ b/packages/ipfs-core/src/components/object/patch/rm-link.js @@ -2,13 +2,21 @@ const { withTimeoutOption } = require('../../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function rmLink (multihash, linkRef, options) { + async function rmLink (multihash, linkRef, options) { const node = await get(multihash, options) node.rmLink(linkRef.Name || linkRef.name) return put(node, options) - }) + } + + return withTimeoutOption(rmLink) } diff --git a/packages/ipfs-core/src/components/object/patch/set-data.js b/packages/ipfs-core/src/components/object/patch/set-data.js index 9a9e4b3a1f..94d272c693 100644 --- a/packages/ipfs-core/src/components/object/patch/set-data.js +++ b/packages/ipfs-core/src/components/object/patch/set-data.js @@ -3,12 +3,20 @@ const { DAGNode } = require('ipld-dag-pb') const { withTimeoutOption } = require('../../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { const get = require('../get')({ ipld, preload }) const put = require('../put')({ ipld, gcLock, preload }) - return withTimeoutOption(async function setData (multihash, data, options) { + async function setData (multihash, data, options) { const node = await get(multihash, options) return put(new DAGNode(data, node.Links), options) - }) + } + + return withTimeoutOption(setData) } diff --git a/packages/ipfs-core/src/components/object/put.js b/packages/ipfs-core/src/components/object/put.js index cbbe4f2e23..e56812f4f2 100644 --- a/packages/ipfs-core/src/components/object/put.js +++ b/packages/ipfs-core/src/components/object/put.js @@ -46,8 +46,20 @@ function parseProtoBuffer (buf) { return dagPB.util.deserialize(buf) } +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + * @param {import('.').GCLock} config.gcLock + */ module.exports = ({ ipld, gcLock, preload }) => { - return withTimeoutOption(async function put (obj, options = {}) { + /** + * + * @param {Uint8Array|DAGNode|{ Data: any, links: DAGLink[]}} obj + * @param {PutOptions & AbortOptions} options + * @returns {Promise} + */ + async function put (obj, options = {}) { const encoding = options.enc let node @@ -82,5 +94,16 @@ module.exports = ({ ipld, gcLock, preload }) => { } finally { release() } - }) + } + + return withTimeoutOption(put) } + +/** + * @typedef {Object} PutOptions + * @property {boolean} [preload] + * @property {string} [enc] + * + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/object/stat.js b/packages/ipfs-core/src/components/object/stat.js index 511415c4c7..d4dba4aeb8 100644 --- a/packages/ipfs-core/src/components/object/stat.js +++ b/packages/ipfs-core/src/components/object/stat.js @@ -3,9 +3,22 @@ const dagPB = require('ipld-dag-pb') const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').IPLD} config.ipld + * @param {import('.').Preload} config.preload + */ module.exports = ({ ipld, preload }) => { const get = require('./get')({ ipld, preload }) - return withTimeoutOption(async function stat (multihash, options = {}) { + + /** + * Returns stats about an Object + * + * @param {CID} multihash + * @param {StatOptions & AbortOptions} options + * @returns {Promise} + */ + async function stat (multihash, options = {}) { const node = await get(multihash, options) const serialized = dagPB.util.serialize(node) const cid = await dagPB.util.cid(serialized, { @@ -23,5 +36,20 @@ module.exports = ({ ipld, preload }) => { DataSize: node.Data.length, CumulativeSize: blockSize + linkLength } - }) + } + + return withTimeoutOption(stat) } +/** + * @typedef {Object} Stat + * @property {string} Hash + * @property {number} NumLinks + * @property {number} BlockSize + * @property {number} LinksSize + * @property {number} DataSize + * @property {number} CumulativeSize + * + * @typedef {import('./get').GetOptions} StatOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/pin/add-all.js b/packages/ipfs-core/src/components/pin/add-all.js index 24889323e3..2c23f4db1f 100644 --- a/packages/ipfs-core/src/components/pin/add-all.js +++ b/packages/ipfs-core/src/components/pin/add-all.js @@ -4,18 +4,15 @@ const { resolvePath, withTimeoutOption } = require('../../utils') const PinManager = require('./pin-manager') const { PinTypes } = PinManager - -/** @type {(source:Source) => AsyncIterable} */ const normaliseInput = require('ipfs-core-utils/src/pins/normalise-input') /** - * * @param {Object} config - * @param {import('..').GCLock} config.gcLock - * @param {import('..').DAG} config.dag - * @param {import('./pin-manager')} config.pinManager + * @param {import('.').GCLock} config.gcLock + * @param {import('.').DagReader} config.dagReader + * @param {import('.').PinManager} config.pinManager */ -module.exports = ({ pinManager, gcLock, dag }) => { +module.exports = ({ gcLock, dagReader, pinManager }) => { /** * Adds multiple IPFS objects to the pinset and also stores it to the IPFS * repo. pinset is the set of hashes currently pinned (not gc'able) @@ -39,7 +36,7 @@ module.exports = ({ pinManager, gcLock, dag }) => { */ const pinAdd = async function * () { for await (const { path, recursive, metadata } of normaliseInput(source)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) // verify that each hash can be pinned const { reason } = await pinManager.isPinnedWithType(cid, [PinTypes.recursive, PinTypes.direct]) @@ -89,9 +86,9 @@ module.exports = ({ pinManager, gcLock, dag }) => { * @typedef {Object} AddSettings * @property {boolean} [lock] * - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions * - * @typedef {import('..').CID} CID + * @typedef {import('.').CID} CID */ /** diff --git a/packages/ipfs-core/src/components/pin/index.js b/packages/ipfs-core/src/components/pin/index.js new file mode 100644 index 0000000000..2e3ee7c66b --- /dev/null +++ b/packages/ipfs-core/src/components/pin/index.js @@ -0,0 +1,35 @@ +'use strict' + +const createAdd = require('./add') +const createAddAll = require('./add-all') +const createLs = require('./ls') +const createRm = require('./rm') +const createRmAll = require('./rm-all') + +class PinAPI { + /** + * @param {Object} config + * @param {GCLock} config.gcLock + * @param {DagReader} config.dagReader + * @param {PinManager} config.pinManager + */ + constructor ({ gcLock, dagReader, pinManager }) { + const addAll = createAddAll({ gcLock, dagReader, pinManager }) + this.addAll = addAll + this.add = createAdd({ addAll }) + const rmAll = createRmAll({ gcLock, dagReader, pinManager }) + this.rmAll = rmAll + this.rm = createRm({ rmAll }) + this.ls = createLs({ dagReader, pinManager }) + } +} +module.exports = PinAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').DagReader} DagReader + * @typedef {import('..').PinManager} PinManager + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').CID} CID + */ diff --git a/packages/ipfs-core/src/components/pin/ls.js b/packages/ipfs-core/src/components/pin/ls.js index 179585d620..641acefda6 100644 --- a/packages/ipfs-core/src/components/pin/ls.js +++ b/packages/ipfs-core/src/components/pin/ls.js @@ -21,10 +21,10 @@ function toPin (type, cid, metadata) { /** * @param {Object} config - * @param {import('./pin-manager')} config.pinManager - * @param {import('../index').DAG} config.dag + * @param {import('.').PinManager} config.pinManager + * @param {import('.').DagReader} config.dagReader */ -module.exports = ({ pinManager, dag }) => { +module.exports = ({ pinManager, dagReader }) => { /** * List all the objects pinned to local storage * @@ -73,7 +73,7 @@ module.exports = ({ pinManager, dag }) => { let matched = false for await (const { path } of normaliseInput(options.paths)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) const { reason, pinned, parent, metadata } = await pinManager.isPinnedWithType(cid, type) if (!pinned) { @@ -137,6 +137,6 @@ module.exports = ({ pinManager, dag }) => { * @typedef {import('./pin-manager').PinType} PinType * @typedef {import('./pin-manager').PinQueryType} PinQueryType * - * @typedef {import('../../utils').AbortOptions} AbortOptions - * @typedef {import('..').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID */ diff --git a/packages/ipfs-core/src/components/pin/pin-manager.js b/packages/ipfs-core/src/components/pin/pin-manager.js index 9c99ee5d15..8155d35460 100644 --- a/packages/ipfs-core/src/components/pin/pin-manager.js +++ b/packages/ipfs-core/src/components/pin/pin-manager.js @@ -49,14 +49,25 @@ const PinTypes = { } class PinManager { - constructor (repo, dag) { + /** + * @param {Object} config + * @param {import('.').Repo} config.repo + * @param {import('.').DagReader} config.dagReader + */ + constructor ({ repo, dagReader }) { this.repo = repo - this.dag = dag + this.dag = dagReader this.log = debug('ipfs:pin') this.directPins = new Set() this.recursivePins = new Set() } + /** + * @private + * @param {CID} cid + * @param {Object} [options] + * @param {boolean} [options.preload] + */ async * _walkDag (cid, { preload = false }) { const { value: node } = await this.dag.get(cid, { preload }) @@ -73,6 +84,11 @@ class PinManager { } } + /** + * @param {CID} cid + * @param {PinOptions & AbortOptions} [options] + * @returns {Promise} + */ async pinDirectly (cid, options = {}) { await this.dag.get(cid, options) @@ -95,10 +111,21 @@ class PinManager { return this.repo.pins.put(cidToKey(cid), cbor.encode(pin)) } - async unpin (cid) { // eslint-disable-line require-await + /** + * @param {CID} cid + * @param {AbortOptions} [options] + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async unpin (cid, options) { return this.repo.pins.delete(cidToKey(cid)) } + /** + * @param {CID} cid + * @param {PreloadOptions & PinOptions & AbortOptions} [options] + * @returns {Promise} + */ async pinRecursively (cid, options = {}) { await this.fetchCompleteDag(cid, options) @@ -121,7 +148,11 @@ class PinManager { await this.repo.pins.put(cidToKey(cid), cbor.encode(pin)) } - async * directKeys () { + /** + * @param {AbortOptions} [options] + * @returns AsyncIterable<{ cid: CID, metadata: any }> + */ + async * directKeys (options) { for await (const entry of this.repo.pins.query({ filters: [(entry) => { const pin = cbor.decode(entry.value) @@ -141,7 +172,11 @@ class PinManager { } } - async * recursiveKeys () { + /** + * @param {AbortOptions} [options] + * @returns {AsyncIterable<{ cid: CID, metadata: any }>} + */ + async * recursiveKeys (options) { for await (const entry of this.repo.pins.query({ filters: [(entry) => { const pin = cbor.decode(entry.value) @@ -184,7 +219,12 @@ class PinManager { } } - async isPinnedWithType (cid, types) { + /** + * @param {CID} cid + * @param {PinQueryType|PinQueryType[]} types + * @param {AbortOptions} [options] + */ + async isPinnedWithType (cid, types, options) { if (!Array.isArray(types)) { types = [types] } @@ -256,18 +296,38 @@ class PinManager { } } + /** + * @param {CID} cid + * @param {PreloadOptions & AbortOptions} options + */ async fetchCompleteDag (cid, options) { await all(this._walkDag(cid, { preload: options.preload })) } - // Throws an error if the pin type is invalid + /** + * Throws an error if the pin type is invalid + * + * @param {any} type + * @returns {type is PinType} + */ static checkPinType (type) { if (typeof type !== 'string' || !Object.keys(PinTypes).includes(type)) { throw invalidPinTypeErr(type) } + return true } } PinManager.PinTypes = PinTypes module.exports = PinManager + +/** + * @typedef {Object} PinOptions + * @property {any} [metadata] + * + * @typedef {Object} PreloadOptions + * @property {boolean} [preload] + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/pin/rm-all.js b/packages/ipfs-core/src/components/pin/rm-all.js index 06f0887fe9..a7f7e117ad 100644 --- a/packages/ipfs-core/src/components/pin/rm-all.js +++ b/packages/ipfs-core/src/components/pin/rm-all.js @@ -6,16 +6,16 @@ const { PinTypes } = require('./pin-manager') /** * @param {Object} config - * @param {import('./pin-manager')} config.pinManager - * @param {import('..').GCLock} config.gcLock - * @param {import('..').DAG} config.dag + * @param {import('.').PinManager} config.pinManager + * @param {import('.').GCLock} config.gcLock + * @param {import('.').DagReader} config.dagReader */ -module.exports = ({ pinManager, gcLock, dag }) => { +module.exports = ({ pinManager, gcLock, dagReader }) => { /** * Unpin one or more blocks from your repo * * @param {Source} source - Unpin all pins from the source - * @param {AbortOptions} [_options] + * @param {AbortOptions} [options] * @returns {AsyncIterable} * @example * ```js @@ -29,13 +29,13 @@ module.exports = ({ pinManager, gcLock, dag }) => { * // CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u') * ``` */ - async function * rmAll (source, _options = {}) { + async function * rmAll (source, options = {}) { const release = await gcLock.readLock() try { // verify that each hash can be unpinned for await (const { path, recursive } of normaliseInput(source)) { - const cid = await resolvePath(dag, path) + const cid = await resolvePath(dagReader, path) const { pinned, reason } = await pinManager.isPinnedWithType(cid, PinTypes.all) if (!pinned) { @@ -72,7 +72,7 @@ module.exports = ({ pinManager, gcLock, dag }) => { } /** - * @typedef {import('..').CID} CID - * @typedef {import('../../utils').AbortOptions} AbortOptions + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions * @typedef {import('./add-all').Source} Source */ diff --git a/packages/ipfs-core/src/components/ping.js b/packages/ipfs-core/src/components/ping.js index 24ed95dbff..ca14d06ead 100644 --- a/packages/ipfs-core/src/components/ping.js +++ b/packages/ipfs-core/src/components/ping.js @@ -7,9 +7,9 @@ const { withTimeoutOption } = require('../utils') /** * @param {Object} config - * @param {import('libp2p')} config.libp2p + * @param {import('.').NetworkService} config.network */ -module.exports = ({ libp2p }) => { +module.exports = ({ network }) => { /** * Send echo request packets to IPFS hosts. * @@ -28,6 +28,7 @@ module.exports = ({ libp2p }) => { * ``` */ async function * ping (peerId, options = {}) { + const { libp2p } = await network.use() options.count = options.count || 10 if (!PeerId.isPeerId(peerId)) { @@ -92,5 +93,5 @@ module.exports = ({ libp2p }) => { * @typedef {Object} PingSettings * @property {number} [count=10] - The number of ping messages to send * - * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/pubsub.js b/packages/ipfs-core/src/components/pubsub.js index 531a85e359..cc50eed07a 100644 --- a/packages/ipfs-core/src/components/pubsub.js +++ b/packages/ipfs-core/src/components/pubsub.js @@ -2,18 +2,149 @@ const { withTimeoutOption } = require('../utils') const errCode = require('err-code') +const { NotEnabledError } = require('../errors') +const get = require('dlv') + +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').IPFSConfig} [config.config] + */ +module.exports = ({ network, config }) => { + const isEnabled = get(config, 'Pubsub.Enabled', true) -module.exports = ({ libp2p }) => { return { - subscribe: withTimeoutOption((...args) => libp2p.pubsub.subscribe(...args)), - unsubscribe: withTimeoutOption((...args) => libp2p.pubsub.unsubscribe(...args)), - publish: withTimeoutOption(async (topic, data, _options) => { - if (!data) { - throw errCode(new Error('argument "data" is required'), 'ERR_ARG_REQUIRED') - } - await libp2p.pubsub.publish(topic, data) - }), - ls: withTimeoutOption((...args) => libp2p.pubsub.getTopics(...args)), - peers: withTimeoutOption((...args) => libp2p.pubsub.getSubscribers(...args)) + subscribe: isEnabled ? withTimeoutOption(subscribe) : notEnabled, + unsubscribe: isEnabled ? withTimeoutOption(unsubscribe) : notEnabled, + publish: isEnabled ? withTimeoutOption(publish) : notEnabled, + ls: isEnabled ? withTimeoutOption(ls) : notEnabled, + peers: isEnabled ? withTimeoutOption(peers) : notEnabled + } + + /** + * Subscribe to a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.data.toString()) + * + * await ipfs.pubsub.subscribe(topic, receiveMsg) + * console.log(`subscribed to ${topic}`) + * ``` + * + * @param {string} topic - The topic name + * @param {(message:Message) => void} handler - Event handler which will be + * called with a message object everytime one is received. + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function subscribe (topic, handler, options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.subscribe(topic, handler, options) + } + + /** + * Unsubscribes from a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.toString()) + * + * await ipfs.pubsub.subscribe(topic, receiveMsg) + * console.log(`subscribed to ${topic}`) + * + * await ipfs.pubsub.unsubscribe(topic, receiveMsg) + * console.log(`unsubscribed from ${topic}`) + * + * // Or removing all listeners: + * + * const topic = 'fruit-of-the-day' + * const receiveMsg = (msg) => console.log(msg.toString()) + * await ipfs.pubsub.subscribe(topic, receiveMsg); + * // Will unsubscribe ALL handlers for the given topic + * await ipfs.pubsub.unsubscribe(topic); + * ``` + * + * @param {string} topic - The topic to unsubscribe from + * @param {(message:Message) => void} [handler] - The handler to remove. If + * not provided unsubscribes al handlers for the topic. + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function unsubscribe (topic, handler, options) { + const { libp2p } = await network.use(options) + libp2p.pubsub.unsubscribe(topic, handler, options) + } + + /** + * Publish a data message to a pubsub topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * const msg = new TextEncoder().encode('banana') + * + * await ipfs.pubsub.publish(topic, msg) + * // msg was broadcasted + * console.log(`published to ${topic}`) + * ``` + * + * @param {string} topic + * @param {Uint8Array} data + * @param {AbortOptions} options + * @returns {Promise} + */ + async function publish (topic, data, options) { + const { libp2p } = await network.use(options) + if (!data) { + throw errCode(new Error('argument "data" is required'), 'ERR_ARG_REQUIRED') + } + await libp2p.pubsub.publish(topic, data) + } + /** + * Returns the list of subscriptions the peer is subscribed to. + * + * @param {AbortOptions} [options] + * @returns {Promise} + */ + async function ls (options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.getTopics(options) + } + + /** + * Returns the peers that are subscribed to one topic. + * + * @example + * ```js + * const topic = 'fruit-of-the-day' + * + * const peerIds = await ipfs.pubsub.peers(topic) + * console.log(peerIds) + * ``` + * + * @param {string} topic + * @param {AbortOptions} [options] + * @returns {Promise} - An array of peer IDs subscribed to the topic + */ + async function peers (topic, options) { + const { libp2p } = await network.use(options) + return libp2p.pubsub.getSubscribers(topic, options) } } + +const notEnabled = async () => { // eslint-disable-line require-await + throw new NotEnabledError('pubsub not enabled') +} + +/** + * @typedef {Object} Message + * @property {string} from + * @property {Uint8Array} seqno + * @property {Uint8Array} data + * @property {string[]} topicIDs + * + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/refs/index.js b/packages/ipfs-core/src/components/refs/index.js index 669fd0cbe1..17557f67af 100644 --- a/packages/ipfs-core/src/components/refs/index.js +++ b/packages/ipfs-core/src/components/refs/index.js @@ -13,8 +13,21 @@ const Format = { edges: ' -> ' } +/** + * @param {Object} config + * @param {import('..').IPLD} config.ipld + * @param {import('..').Resolve} config.resolve + * @param {import('..').Preload} config.preload + */ module.exports = function ({ ipld, resolve, preload }) { - return withTimeoutOption(async function * refs (ipfsPath, options = {}) { // eslint-disable-line require-await + /** + * Get links (references) from an object + * + * @param {CID|string} ipfsPath - The object to search for references + * @param {RefsOptions & AbortOptions} [options] + * @returns {AsyncIterable} + */ + async function * refs (ipfsPath, options = {}) { if (options.maxDepth === 0) { return } @@ -30,12 +43,16 @@ module.exports = function ({ ipld, resolve, preload }) { } const rawPaths = Array.isArray(ipfsPath) ? ipfsPath : [ipfsPath] + + // @ts-ignore const paths = rawPaths.map(p => getFullPath(preload, p, options)) for (const path of paths) { yield * refsStream(resolve, ipld, path, options) } - }) + } + + return withTimeoutOption(refs) } module.exports.Format = Format @@ -160,3 +177,17 @@ function getNodeLinks (node, path = '') { } return links } + +/** + * @typedef {Object} RefsOptions + * @property {boolean} [recursive=false] - Recursively list references of child nodes + * @property {boolean} [unique=false] - Omit duplicate references from output + * @property {string} [format=''] - Output edges with given format. Available tokens: ``, ``, `` + * @property {boolean} [edges=false] - output references in edge format: `" -> "` + * @property {number} [maxDepth=1] - only for recursive refs, limits fetch and listing to the given depth + * + * @typedef {{ref:string, err?:null}|{ref?:undefined, err:Error}} RefResult + * + * @typedef {import('..').AbortOptions} AbortOptions + * @typedef {import('..').Repo} Repo + */ diff --git a/packages/ipfs-core/src/components/refs/local.js b/packages/ipfs-core/src/components/refs/local.js index 871c5440dd..e98a8733f8 100644 --- a/packages/ipfs-core/src/components/refs/local.js +++ b/packages/ipfs-core/src/components/refs/local.js @@ -2,10 +2,21 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = function ({ repo }) { - return withTimeoutOption(async function * refsLocal (options = {}) { + /** + * @param {import('.').AbortOptions} [options] + * @returns {AsyncIterable<{ref: string}>} + */ + async function * refsLocal (options = {}) { + // @ts-ignore - TS is not aware of keysOnly for await (const cid of repo.blocks.query({ keysOnly: true, signal: options.signal })) { yield { ref: cid.toString() } } - }) + } + + return withTimeoutOption(refsLocal) } diff --git a/packages/ipfs-core/src/components/repo/gc.js b/packages/ipfs-core/src/components/repo/gc.js index c2b8b26afa..bfa0d4e519 100644 --- a/packages/ipfs-core/src/components/repo/gc.js +++ b/packages/ipfs-core/src/components/repo/gc.js @@ -15,10 +15,10 @@ const BLOCK_RM_CONCURRENCY = 256 * Perform mark and sweep garbage collection * * @param {Object} config - * @param {import('..').GCLock} config.gcLock - * @param {import('..').Pin} config.pin - * @param {import('..').Refs} config.refs - * @param {import('..').IPFSRepo} config.repo + * @param {import('.').GCLock} config.gcLock + * @param {import('.').Pin} config.pin + * @param {import('.').Refs} config.refs + * @param {import('.').Repo} config.repo */ module.exports = ({ gcLock, pin, refs, repo }) => { /** @@ -35,6 +35,7 @@ module.exports = ({ gcLock, pin, refs, repo }) => { // Mark all blocks that are being used const markedSet = await createMarkedSet({ pin, refs, repo }) // Get all blocks keys from the blockstore + // @ts-ignore - TS is not aware of keysOnly overload const blockKeys = repo.blocks.query({ keysOnly: true }) // Delete blocks that are not being used diff --git a/packages/ipfs-core/src/components/repo/index.js b/packages/ipfs-core/src/components/repo/index.js new file mode 100644 index 0000000000..82e4e401d5 --- /dev/null +++ b/packages/ipfs-core/src/components/repo/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createGC = require('./gc') +const createStat = require('./stat') +const createVersion = require('./version') + +class RepoAPI { + /** + * @param {Object} config + * @param {GCLock} config.gcLock + * @param {Pin} config.pin + * @param {Repo} config.repo + * @param {Refs} config.refs + */ + constructor ({ gcLock, pin, repo, refs }) { + this.gc = createGC({ gcLock, pin, refs, repo }) + this.stat = createStat({ repo }) + this.version = createVersion({ repo }) + } +} +module.exports = RepoAPI + +/** + * @typedef {import('..').GCLock} GCLock + * @typedef {import('..').Pin} Pin + * @typedef {import('..').Repo} Repo + * @typedef {import('..').Refs} Refs + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/repo/stat.js b/packages/ipfs-core/src/components/repo/stat.js index 9194a00ded..afeff552dc 100644 --- a/packages/ipfs-core/src/components/repo/stat.js +++ b/packages/ipfs-core/src/components/repo/stat.js @@ -2,8 +2,15 @@ const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { - return withTimeoutOption(async function stat (options) { + /** + * @param {import('.').AbortOptions} [options] + */ + async function stat (options) { const stats = await repo.stat(options) return { @@ -13,5 +20,7 @@ module.exports = ({ repo }) => { version: stats.version.toString(), storageMax: stats.storageMax } - }) + } + + return withTimeoutOption(stat) } diff --git a/packages/ipfs-core/src/components/repo/version.js b/packages/ipfs-core/src/components/repo/version.js index 4622f667b7..e31390490a 100644 --- a/packages/ipfs-core/src/components/repo/version.js +++ b/packages/ipfs-core/src/components/repo/version.js @@ -3,15 +3,21 @@ const { repoVersion } = require('ipfs-repo') const { withTimeoutOption } = require('../../utils') +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { /** * If the repo has been initialized, report the current version. * Otherwise report the version that would be initialized. * - * @returns {number} + * @param options + * @returns {Promise} */ - return withTimeoutOption(async function version (options) { + async function version (options) { try { + // @ts-ignore - not a public API await repo._checkInitialized(options) } catch (err) { // TODO: (dryajov) This is really hacky, there must be a better way @@ -30,5 +36,7 @@ module.exports = ({ repo }) => { } return repo.version.get(options) - }) + } + + return withTimeoutOption(version) } diff --git a/packages/ipfs-core/src/components/resolve.js b/packages/ipfs-core/src/components/resolve.js index 9b14d4516c..2a5b890bd4 100644 --- a/packages/ipfs-core/src/components/resolve.js +++ b/packages/ipfs-core/src/components/resolve.js @@ -7,8 +7,8 @@ const { withTimeoutOption } = require('../utils') /** * @param {Object} config - * @param {import('.').IPLD} config.ipld - An instance of IPLD - * @param {import('.').Name} [config.name] - An IPFS core interface name API + * @param {import('.').IPLD} config.ipld + * @param {import('.').Name} config.name - An IPFS core interface name API */ module.exports = ({ ipld, name }) => { /** @@ -102,5 +102,5 @@ module.exports = ({ ipld, name }) => { * @property {boolean} [recursive=true] - Resolve until result is an IPFS name. * @property {string} [cidBase='base58btc'] - Multibase codec name the CID in the resolved path will be encoded with. * - * @typedef {import('../utils').AbortOptions} AbortOptions + * @typedef {import('.').AbortOptions} AbortOptions */ diff --git a/packages/ipfs-core/src/components/start.js b/packages/ipfs-core/src/components/start.js index 1f1f286eb2..63b6378443 100644 --- a/packages/ipfs-core/src/components/start.js +++ b/packages/ipfs-core/src/components/start.js @@ -1,404 +1,32 @@ 'use strict' -const log = require('debug')('ipfs:components:start') -const Bitswap = require('ipfs-bitswap') -const multiaddr = require('multiaddr') -const get = require('dlv') -const defer = require('p-defer') -const errCode = require('err-code') -const IPNS = require('../ipns') -const routingConfig = require('../ipns/routing/config') -const { AlreadyInitializedError, NotEnabledError } = require('../errors') -const Components = require('./') -const createMfsPreload = require('../mfs-preload') -const { withTimeoutOption } = require('../utils') - -const WEBSOCKET_STAR_PROTO_CODE = 479 +const Service = require('../utils/service') /** * @param {Object} config - * @param {APIManager} config.apiManager - * @param {StartOptions} config.options - * @param {IPFSBlockService} config.blockService - * @param {GCLock} config.gcLock - * @param {InitOptions} config.initOptions - * @param {IPLD} config.ipld - * @param {Keychain} config.keychain - * @param {PeerId} config.peerId - * @param {PinManager} config.pinManager - * @param {Preload} config.preload - * @param {Print} config.print - * @param {IPFSRepo} config.repo + * @param {import('.').NetworkService} config.network + * @param {import('.').PeerId} config.peerId + * @param {import('.').Repo} config.repo + * @param {import('.').BlockService} config.blockService + * @param {import('.').Print} config.print + * @param {import('.').Preload} config.preload + * @param {import('.').MFSPreload} config.mfsPreload + * @param {import('.').IPNS} config.ipns + * @param {import('.').Keychain} config.keychain + * @param {string} [config.pass] */ -module.exports = ({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo -}) => { - async function start () { - const startPromise = defer() - startPromise.promise.catch((err) => log(err)) - - const { cancel } = apiManager.update({ start: () => startPromise.promise }) - - try { - // The repo may be closed if previously stopped - if (repo.closed) { - await repo.open() - } - - const config = await repo.config.getAll() - const addrs = [] - - if (config.Addresses && config.Addresses.Swarm) { - config.Addresses.Swarm.forEach(addr => { - let ma = multiaddr(addr) - - // Temporary error for users migrating using websocket-star multiaddrs for listenning on libp2p - // websocket-star support was removed from ipfs and libp2p - if (ma.protoCodes().includes(WEBSOCKET_STAR_PROTO_CODE)) { - throw errCode(new Error('websocket-star swarm addresses are not supported. See https://github.com/ipfs/js-ipfs/issues/2779'), 'ERR_WEBSOCKET_STAR_SWARM_ADDR_NOT_SUPPORTED') - } - - // multiaddrs that go via a signalling server or other intermediary (e.g. stardust, - // webrtc-star) can have the intermediary's peer ID in the address, so append our - // peer ID to the end of it - const maId = ma.getPeerId() - if (maId && maId !== peerId.toB58String()) { - ma = ma.encapsulate(`/p2p/${peerId.toB58String()}`) - } - - addrs.push(ma) - }) - } - - const libp2p = Components.libp2p({ - options: constructorOptions, - repo, - peerId: peerId, - multiaddrs: addrs, - config - }) - - libp2p.keychain && await libp2p.loadKeychain() - - await libp2p.start() - - libp2p.transportManager.getAddrs().forEach(ma => print(`Swarm listening on ${ma}/p2p/${peerId.toB58String()}`)) - - const ipnsRouting = routingConfig({ libp2p, repo, peerId, options: constructorOptions }) - const ipns = new IPNS(ipnsRouting, repo.datastore, peerId, keychain, { pass: initOptions.pass }) - const bitswap = new Bitswap(libp2p, repo.blocks, { statsEnabled: true }) - - await bitswap.start() - - blockService.setExchange(bitswap) - - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const files = Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }) - const mfsPreload = createMfsPreload({ files, preload, options: constructorOptions.preload }) - - await Promise.all([ - ipns.republisher.start(), - preload.start(), - mfsPreload.start() - ]) - - const api = createApi({ - apiManager, - bitswap, - block, - blockService, - config, - constructorOptions, - dag, - files, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pin, - preload, - print, - repo - }) - - const { api: startedApi } = apiManager.update(api, () => undefined) - startPromise.resolve(startedApi) - return startedApi - } catch (err) { - cancel() - startPromise.reject(err) - throw err - } - } - return withTimeoutOption(start) -} - -/** - * @param {CreateAPIConfig} config - */ -function createApi ({ - apiManager, - bitswap, - block, - blockService, - config, - constructorOptions, - dag, - files, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pin, - preload, - print, - repo -}) { - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - const isOnline = Components.isOnline({ libp2p }) - - const dhtNotEnabled = async () => { // eslint-disable-line require-await - throw new NotEnabledError('dht not enabled') - } - - const dhtNotEnabledIterator = async function * () { // eslint-disable-line require-await,require-yield - throw new NotEnabledError('dht not enabled') - } - - const dht = get(libp2p, '_config.dht.enabled', false) ? Components.dht({ libp2p, repo }) : { - get: dhtNotEnabled, - put: dhtNotEnabled, - findProvs: dhtNotEnabledIterator, - findPeer: dhtNotEnabled, - provide: dhtNotEnabledIterator, - query: dhtNotEnabledIterator - } - - const dns = Components.dns() - const name = { - pubsub: { - cancel: Components.name.pubsub.cancel({ ipns, options: constructorOptions }), - state: Components.name.pubsub.state({ ipns, options: constructorOptions }), - subs: Components.name.pubsub.subs({ ipns, options: constructorOptions }) - }, - publish: Components.name.publish({ ipns, dag, peerId, isOnline, keychain }), - resolve: Components.name.resolve({ dns, ipns, peerId, isOnline, options: constructorOptions }) - } - const resolve = Components.resolve({ name, ipld }) - const refs = Object.assign( - Components.refs({ ipld, resolve, preload }), - { local: Components.refs.local({ repo }) } - ) - - const pubsubNotEnabled = async () => { // eslint-disable-line require-await - throw new NotEnabledError('pubsub not enabled') - } - - const pubsub = get(constructorOptions, 'config.Pubsub.Enabled', get(config, 'Pubsub.Enabled', true)) - ? Components.pubsub({ libp2p }) - : { - subscribe: pubsubNotEnabled, - unsubscribe: pubsubNotEnabled, - publish: pubsubNotEnabled, - ls: pubsubNotEnabled, - peers: pubsubNotEnabled - } - - const api = { - add: Components.add({ addAll }), - addAll, - bitswap: { - stat: Components.bitswap.stat({ bitswap }), - unwant: Components.bitswap.unwant({ bitswap }), - wantlist: Components.bitswap.wantlist({ bitswap }), - wantlistForPeer: Components.bitswap.wantlistForPeer({ bitswap }) - }, - block, - bootstrap: { - add: Components.bootstrap.add({ repo }), - clear: Components.bootstrap.clear({ repo }), - list: Components.bootstrap.list({ repo }), - reset: Components.bootstrap.reset({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dht, - dns, - files, - get: Components.get({ ipld, preload }), - id: Components.id({ peerId, libp2p }), - init: async () => { throw new AlreadyInitializedError() }, // eslint-disable-line require-await - isOnline, - ipld, - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - libp2p, - ls: Components.ls({ ipld, preload }), - name, - object, - pin, - ping: Components.ping({ libp2p }), - pubsub, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: () => apiManager.api, - stats: { - bitswap: Components.bitswap.stat({ bitswap }), - bw: libp2p.metrics - ? Components.stats.bw({ libp2p }) - : async () => { // eslint-disable-line require-await - throw new NotEnabledError('libp2p metrics not enabled') - }, - repo: Components.repo.stat({ repo }) - }, - stop: Components.stop({ - apiManager, - bitswap, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, +module.exports = ({ network, preload, peerId, keychain, repo, ipns, blockService, print, pass }) => { + const start = async () => { + const { bitswap, libp2p } = await Service.start(network, { peerId, - preload, + repo, print, - repo - }), - swarm: { - addrs: Components.swarm.addrs({ libp2p }), - connect: Components.swarm.connect({ libp2p }), - disconnect: Components.swarm.disconnect({ libp2p }), - localAddrs: Components.swarm.localAddrs({ multiaddrs: libp2p.multiaddrs }), - peers: Components.swarm.peers({ libp2p }) - }, - version: Components.version({ repo }) + pass + }) + ipns.startOnline({ keychain, libp2p, peerId, repo }) + preload.start() + blockService.setExchange(bitswap) } - return api + return start } - -/** - * @typedef {Object} CreateAPIConfig - * @property {APIManager} apiManager - * @property {Bitswap} [bitswap] - * @property {Block} block - * @property {IPFSBlockService} blockService - * @property {Config} config - * @property {StartOptions} constructorOptions - * @property {DAG} dag - * @property {Files} [files] - * @property {GCLock} gcLock - * @property {InitOptions} initOptions - * @property {IPLD} ipld - * @property {import('../ipns')} ipns - * @property {Keychain} keychain - * @property {LibP2P} libp2p - * @property {MFSPreload} mfsPreload - * @property {PeerId} peerId - * @property {Pin} pin - * @property {Preload} preload - * @property {Print} print - * @property {IPFSRepo} repo - * - * @typedef {(...args:any[]) => void} Print - * - * @typedef {import('./init').InitOptions} InitOptions - * @typedef {import('./init').ConstructorOptions} StartOptions - * @typedef {import('./init').Keychain} Keychain - * @typedef {import('../api-manager')} APIManager - * @typedef {import('./pin/pin-manager')} PinManager - * @typedef {import('../mfs-preload').MFSPreload} MFSPreload - * @typedef {import('.').IPFSBlockService} IPFSBlockService - * @typedef {import('.').GCLock} GCLock - * @typedef {import('.')} IPLD - * @typedef {import('.').PeerId} PeerId - * @typedef {import('.').Preload} Preload - * @typedef {import('.').IPFSRepo} IPFSRepo - * @typedef {import('.').LibP2P} LibP2P - * @typedef {import('.').Pin} Pin - * @typedef {import('.').Files} Files - * @typedef {import('.').DAG} DAG - * @typedef {import('.').Config} Config - * @typedef {import('.').Block} Block - */ diff --git a/packages/ipfs-core/src/components/stats/bw.js b/packages/ipfs-core/src/components/stats/bw.js index 3c2319187c..36351858db 100644 --- a/packages/ipfs-core/src/components/stats/bw.js +++ b/packages/ipfs-core/src/components/stats/bw.js @@ -5,6 +5,11 @@ const parseDuration = require('parse-duration').default const errCode = require('err-code') const { withTimeoutOption } = require('../../utils') +/** + * @param {LibP2P} libp2p + * @param {BWOptions} opts + * @returns {BandwidthInfo} + */ function getBandwidthStats (libp2p, opts) { let stats @@ -35,17 +40,30 @@ function getBandwidthStats (libp2p, opts) { } } -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function * (options = {}) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Get IPFS bandwidth information + * + * @param {BWOptions & AbortOptions} options + * @returns {AsyncIterable} + */ + const bw = async function * (options = {}) { + const { libp2p } = await network.use(options) + if (!options.poll) { yield getBandwidthStats(libp2p, options) return } - let interval = options.interval || 1000 + const interval = options.interval || 1000 + let ms = -1 try { - interval = typeof interval === 'string' ? parseDuration(interval) : interval - if (!interval || interval < 0) throw new Error('invalid duration') + ms = typeof interval === 'string' ? parseDuration(interval) || -1 : interval + if (!ms || ms < 0) throw new Error('invalid duration') } catch (err) { throw errCode(err, 'ERR_INVALID_POLL_INTERVAL') } @@ -55,10 +73,31 @@ module.exports = ({ libp2p }) => { while (true) { yield getBandwidthStats(libp2p, options) // eslint-disable-next-line no-loop-func - await new Promise(resolve => { timeoutId = setTimeout(resolve, interval) }) + await new Promise(resolve => { timeoutId = setTimeout(resolve, ms) }) } } finally { clearTimeout(timeoutId) } - }) + } + + return withTimeoutOption(bw) } + +/** + * @typedef {Object} BWOptions + * @property {PeerId|CID|string} [peer] - Specifies a peer to print bandwidth for + * @property {string} [proto] - Specifies a protocol to print bandwidth for + * @property {boolean} [poll] - Is used to yield bandwidth info at an interval + * @property {number|string} [interval=1000] - The time interval to wait between updating output, if `poll` is `true`. + * + * @typedef {Object} BandwidthInfo + * @property {Big} totalIn + * @property {Big} totalOut + * @property {Big} rateIn + * @property {Big} rateOut + * + * @typedef {import('.').LibP2P} LibP2P + * @typedef {import('.').PeerId} PeerId + * @typedef {import('.').CID} CID + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/stats/index.js b/packages/ipfs-core/src/components/stats/index.js new file mode 100644 index 0000000000..6760c21853 --- /dev/null +++ b/packages/ipfs-core/src/components/stats/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createBW = require('./bw') +const createRepo = require('../repo/stat') +const createBitswap = require('../bitswap/stat') + +class StatsAPI { + /** + * @param {Object} config + * @param {Repo} config.repo + * @param {NetworkService} config.network + */ + constructor ({ repo, network }) { + this.repo = createRepo({ repo }) + this.bw = createBW({ network }) + this.bitswap = createBitswap({ network }) + } +} + +module.exports = StatsAPI + +/** + * @typedef {import('..').Repo} Repo + * @typedef {import('..').PeerId} PeerId + * @typedef {import('..').LibP2P} LibP2P + * @typedef {import('..').CID} CID + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/stop.js b/packages/ipfs-core/src/components/stop.js index 88e320aaea..d5ccc44c2c 100644 --- a/packages/ipfs-core/src/components/stop.js +++ b/packages/ipfs-core/src/components/stop.js @@ -1,232 +1,27 @@ 'use strict' -const defer = require('p-defer') -const { NotStartedError, AlreadyInitializedError } = require('../errors') -const Components = require('./') -const { withTimeoutOption } = require('../utils') +const Service = require('../utils/service') -module.exports = ({ - apiManager, - options: constructorOptions, - bitswap, - blockService, - gcLock, - initOptions, - ipld, - ipns, - keychain, - libp2p, - mfsPreload, - peerId, - pinManager = {}, - preload, - print, - repo -}) => { - /** - * Stops the IPFS node and in case of talking with an IPFS Daemon, it stops - * the process. - * - * @param {AbortOptions} _options - * @returns {Promise} - * @example - * ```js - * await ipfs.stop() - * ``` - */ - async function stop (_options) { - const stopPromise = defer() - const { cancel } = apiManager.update({ stop: () => stopPromise.promise }) - - try { - blockService.unsetExchange() - bitswap.stop() - preload.stop() - - await Promise.all([ - ipns.republisher.stop(), - mfsPreload.stop(), - libp2p.stop(), - repo.close() - ]) - - const api = createApi({ - apiManager, - constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }) - - apiManager.update(api, () => { throw new NotStartedError() }) - } catch (err) { - cancel() - stopPromise.reject(err) - throw err - } - - stopPromise.resolve() - } - - return withTimeoutOption(stop) -} - -function createApi ({ - apiManager, - constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo -}) { - const dag = { - get: Components.dag.get({ ipld, preload }), - resolve: Components.dag.resolve({ ipld, preload }), - tree: Components.dag.tree({ ipld, preload }), - // FIXME: resolve this circular dependency - get put () { - const put = Components.dag.put({ ipld, pin, gcLock, preload }) - Object.defineProperty(this, 'put', { value: put }) - return put - } - } - const object = { - data: Components.object.data({ ipld, preload }), - get: Components.object.get({ ipld, preload }), - links: Components.object.links({ dag }), - new: Components.object.new({ ipld, preload }), - patch: { - addLink: Components.object.patch.addLink({ ipld, gcLock, preload }), - appendData: Components.object.patch.appendData({ ipld, gcLock, preload }), - rmLink: Components.object.patch.rmLink({ ipld, gcLock, preload }), - setData: Components.object.patch.setData({ ipld, gcLock, preload }) - }, - put: Components.object.put({ ipld, gcLock, preload }), - stat: Components.object.stat({ ipld, preload }) - } - - const pinAddAll = Components.pin.addAll({ pinManager, gcLock, dag }) - const pinRmAll = Components.pin.rmAll({ pinManager, gcLock, dag }) - - const pin = { - add: Components.pin.add({ addAll: pinAddAll }), - addAll: pinAddAll, - ls: Components.pin.ls({ pinManager, dag }), - rm: Components.pin.rm({ rmAll: pinRmAll }), - rmAll: pinRmAll - } - - const block = { - get: Components.block.get({ blockService, preload }), - put: Components.block.put({ blockService, pin, gcLock, preload }), - rm: Components.block.rm({ blockService, gcLock, pinManager }), - stat: Components.block.stat({ blockService, preload }) - } - - const addAll = Components.addAll({ block, preload, pin, gcLock, options: constructorOptions }) - const resolve = Components.resolve({ ipld }) - const refs = Object.assign( - Components.refs({ ipld, resolve, preload }), - { local: Components.refs.local({ repo }) } - ) - - const notStarted = async () => { // eslint-disable-line require-await - throw new NotStartedError() - } - - const api = { - add: Components.add({ addAll }), - addAll, - bitswap: { - stat: notStarted, - unwant: notStarted, - wantlist: notStarted, - wantlistForPeer: notStarted - }, - block, - bootstrap: { - add: Components.bootstrap.add({ repo }), - clear: Components.bootstrap.clear({ repo }), - list: Components.bootstrap.list({ repo }), - reset: Components.bootstrap.reset({ repo }), - rm: Components.bootstrap.rm({ repo }) - }, - cat: Components.cat({ ipld, preload }), - config: Components.config({ repo }), - dag, - dns: Components.dns(), - files: Components.files({ ipld, block, blockService, repo, preload, options: constructorOptions }), - get: Components.get({ ipld, preload }), - id: Components.id({ peerId }), - init: async () => { // eslint-disable-line require-await - throw new AlreadyInitializedError() - }, - isOnline: Components.isOnline({}), - key: { - export: Components.key.export({ keychain }), - gen: Components.key.gen({ keychain }), - import: Components.key.import({ keychain }), - info: Components.key.info({ keychain }), - list: Components.key.list({ keychain }), - rename: Components.key.rename({ keychain }), - rm: Components.key.rm({ keychain }) - }, - ls: Components.ls({ ipld, preload }), - object, - pin, - refs, - repo: { - gc: Components.repo.gc({ gcLock, pin, refs, repo }), - stat: Components.repo.stat({ repo }), - version: Components.repo.version({ repo }) - }, - resolve, - start: Components.start({ - apiManager, - options: constructorOptions, - blockService, - gcLock, - initOptions, - ipld, - keychain, - peerId, - pinManager, - preload, - print, - repo - }), - stats: { - bitswap: notStarted, - bw: notStarted, - repo: Components.repo.stat({ repo }) - }, - stop: () => {}, - swarm: { - addrs: notStarted, - connect: notStarted, - disconnect: notStarted, - localAddrs: Components.swarm.localAddrs({ multiaddrs: [] }), - peers: notStarted - }, - version: Components.version({ repo }) +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + * @param {import('.').Preload} config.preload + * @param {import('.').BlockService} config.blockService + * @param {import('.').IPNS} config.ipns + * @param {import('.').Repo} config.repo + * @param {import('.').MFSPreload} config.mfsPreload + */ +module.exports = ({ network, preload, blockService, ipns, repo, mfsPreload }) => { + const stop = async () => { + await Promise.all([ + blockService.unsetExchange(), + preload.stop(), + ipns.stop(), + mfsPreload.stop(), + Service.stop(network), + repo.close() + ]) } - return api + return stop } - -/** - * @typedef {import('../utils').AbortOptions} AbortOptions - */ diff --git a/packages/ipfs-core/src/components/storage.js b/packages/ipfs-core/src/components/storage.js new file mode 100644 index 0000000000..25c39b80e0 --- /dev/null +++ b/packages/ipfs-core/src/components/storage.js @@ -0,0 +1,295 @@ +'use strict' + +const log = require('debug')('ipfs:components:peer:storage') +const createRepo = require('../runtime/repo-nodejs') + +const { ERR_REPO_NOT_INITIALIZED } = require('ipfs-repo').errors +const uint8ArrayFromString = require('uint8arrays/from-string') +const uint8ArrayToString = require('uint8arrays/to-string') +const PeerId = require('peer-id') +const { mergeOptions } = require('../utils') +const configService = require('./config') +const { NotEnabledError } = require('../errors') +const createLibP2P = require('./libp2p') + +class Storage { + /** + * @private + * @param {PeerId} peerId + * @param {Keychain} keychain + * @param {Repo} repo + * @param {Print} print + * @param {string|undefined} pass + * @param {boolean} isInitialized + */ + constructor (peerId, keychain, repo, print, pass, isInitialized) { + this.print = print + this.peerId = peerId + this.keychain = keychain + this.repo = repo + this.print = print + this.pass = pass + this.isInitialized = isInitialized + } + + /** + * + * @param {Options} options + */ + static async start (options) { + const { repoAutoMigrate: autoMigrate, repo: repoInit, print, pass } = options + + const repo = (typeof repoInit === 'string' || repoInit == null) + ? createRepo({ path: repoInit, autoMigrate }) + : repoInit + + const { peerId, keychain, isInitialized } = await loadRepo(repo, options) + + return new Storage(peerId, keychain, repo, print, pass, isInitialized) + } +} +module.exports = Storage + +/** + * + * @param {Repo} repo + * @param {RepoOptions & InitOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain, isInitialized:boolean }>} + */ +const loadRepo = async (repo, options) => { + const openError = await openRepo(repo) + if (openError == null) { + // If opened succefully configure repo + return { ...await configureRepo(repo, options), isInitialized: true } + } else if (openError.code === ERR_REPO_NOT_INITIALIZED) { + if (options.allowNew === false) { + throw new NotEnabledError('new repo initialization is not enabled') + } else { + // If failed to open, because repo isn't initilaized and initalizing a + // new repo allowed, init repo: + return { ...await initRepo(repo, options), isInitialized: false } + } + } else { + throw openError + } +} + +/** + * Attempts to open given repo unless it is already open and returns result + * containing repo or an error if failed. + * + * @param {Repo} repo + * @returns {Promise<(Error & { code: number }) | null>} + */ +const openRepo = async (repo) => { + // If repo is closed attempt to open it. + if (!repo.closed) { + try { + await repo.open() + return null + } catch (error) { + return error + } + } else { + return null + } +} + +/** + * @param {Repo} repo + * @param {RepoOptions & InitOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain}>} + */ +const initRepo = async (repo, options) => { + // 1. Verify that repo does not exist yet (if it does and we could not + // open it we give up) + const exists = await repo.exists() + log('repo exists?', exists) + + if (exists === true) { + throw new Error('repo already exists') + } + + // 2. Restore `peerId` from a given `.privateKey` or init new using + // provide options. + const peerId = options.privateKey + ? await decodePeerId(options.privateKey) + : await initPeerId(options) + + const identity = peerIdToIdentity(peerId) + + log('peer identity: %s', identity.PeerID) + + // 3. Init new repo with provided `.config` and restored / initalized + // peerd identity. + const config = { + ...options.config, + Identity: identity + } + await repo.init(config) + + // 4. Open initalized repo. + await repo.open() + + log('repo opened') + + // Create libp2p for Keychain creation + const libp2p = createLibP2P({ + peerId, + repo, + config, + keychainConfig: { + pass: options.pass + } + }) + + if (libp2p.keychain && libp2p.keychain.opts) { + await libp2p.loadKeychain() + + await repo.config.set('Keychain', { + dek: libp2p.keychain.opts.dek + }) + } + + return { peerId, keychain: libp2p.keychain } +} + +/** + * Takes `peerId` either represented as a string serialized string or + * an instance and returns a `PeerId` instance. + * + * @param {PeerId|string} peerId + * @returns {Promise|PeerId} + */ +const decodePeerId = (peerId) => { + log('using user-supplied private-key') + return typeof peerId === 'object' + ? peerId + : PeerId.createFromPrivKey(uint8ArrayFromString(peerId, 'base64pad')) +} + +/** + * Initializes new PeerId by generting an underlying keypair. + * + * @param {Object} options + * @param {KeyType} [options.algorithm='RSA'] + * @param {number} [options.bits=2048] + * @param {Print} options.print + * @returns {Promise} + */ +const initPeerId = ({ print, algorithm = 'RSA', bits = 2048 }) => { + // Generate peer identity keypair + transform to desired format + add to config. + print('generating %s-bit (rsa only) %s keypair...', bits, algorithm) + return PeerId.create({ keyType: algorithm, bits }) +} + +/** + * @param {PeerId} peerId + */ +const peerIdToIdentity = (peerId) => ({ + PeerID: peerId.toB58String(), + /** @type {string} */ + PrivKey: uint8ArrayToString(peerId.privKey.bytes, 'base64pad') +}) + +/** + * Applies passed `profiles` and a `config` to an open repo. + * + * @param {Repo} repo + * @param {ConfigureOptions} options + * @returns {Promise<{peerId: PeerId, keychain:Keychain}>} + */ +const configureRepo = async (repo, { config, profiles, pass }) => { + const original = await repo.config.getAll() + const changed = mergeConfigs(applyProfiles(original, profiles), config) + + if (original !== changed) { + await repo.config.replace(changed) + } + + // @ts-ignore - Identity may not be present + const peerId = await PeerId.createFromPrivKey(changed.Identity.PrivKey) + const libp2p = createLibP2P({ + peerId, + repo, + config: changed, + keychainConfig: { + pass, + ...changed.Keychain + } + }) + libp2p.keychain && await libp2p.loadKeychain() + + return { peerId, keychain: libp2p.keychain } +} + +/** + * @param {IPFSConfig} config + * @param {Partial} [changes] + */ +const mergeConfigs = (config, changes) => + changes ? mergeOptions(config, changes) : config + +/** + * Apply profiles (e.g. ['server', 'lowpower']) to config + * + * @param {IPFSConfig} config + * @param {string[]} [profiles] + */ +const applyProfiles = (config, profiles) => { + return (profiles || []).reduce((config, name) => { + const profile = configService.profiles[name] + if (!profile) { + throw new Error(`Could not find profile with name '${name}'`) + } + log('applying profile %s', name) + return profile.transform(config) + }, config) +} + +/** + * @typedef {StorageOptions & RepoOptions & InitOptions} Options + * + * @typedef {Object} StorageOptions + * @property {Repo|string} [repo='~/.jsipfs'] - The file path at which to store the + * IPFS node’s data. Alternatively, you can set up a customized storage system + * by providing an Repo implementation. (In browser default is 'ipfs'). + * @property {boolean} [repoAutoMigrate=true] - js-ipfs comes bundled with a tool + * that automatically migrates your IPFS repository when a new version is + * available. + * @property {boolean} [repoOwner] + * @property {IPLDOptions} [ipld] + * + * + * @typedef {Object} RepoOptions + * @property {Print} print + * @property {IPFSConfig} [config] + * + * @typedef {Object} ConfigureOptions + * @property {IPFSConfig} [options.config] + * @property {string[]} [options.profiles] + * @property {string} [options.pass] + * + * @typedef {Object} InitOptions - On Frist run js-ipfs will initalize a repo + * which can be customized through this settings. + * @property {boolean} [emptyRepo=false] - Whether to remove built-in assets, + * like the instructional tour and empty mutable file system, from the repo. + * @property {KeyType} [algorithm='RSA'] - The type of key to use. + * @property {number} [bits=2048] - Number of bits to use in the generated key + * pair (rsa only). + * @property {PeerId|string} [privateKey] - A pre-generated private key to use. + * **NOTE: This overrides `bits`.** + * @property {string} [pass] - A passphrase to encrypt keys. You should + * generally use the top-level `pass` option instead of the `init.pass` + * option (this one will take its value from the top-level option if not set). + * @property {string[]} [profiles] - Apply profile settings to config. + * @property {boolean} [allowNew=true] - Set to `false` to disallow + * initialization if the repo does not already exist. + * + * @typedef {import('.').IPLDOptions} IPLDOptions + * @typedef {import('.').Print} Print + * @typedef {import('.').IPFSConfig} IPFSConfig + * @typedef {import('../interface/repo').Repo} Repo + * @typedef {import('libp2p-crypto').KeyType} KeyType + * @typedef {import('libp2p').LibP2PKeychain} Keychain + */ diff --git a/packages/ipfs-core/src/components/swarm/addrs.js b/packages/ipfs-core/src/components/swarm/addrs.js index cca0c83d32..657d889281 100644 --- a/packages/ipfs-core/src/components/swarm/addrs.js +++ b/packages/ipfs-core/src/components/swarm/addrs.js @@ -2,9 +2,20 @@ const { withTimeoutOption } = require('../../utils') -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function addrs (options) { // eslint-disable-line require-await +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * List of known addresses of each peer connected. + * + * @param {import('../../utils').AbortOptions} options + * @returns {Promise} + */ + async function addrs (options) { // eslint-disable-line require-await const peers = [] + const { libp2p } = await network.use(options) for (const [peerId, peer] of libp2p.peerStore.peers.entries(options)) { peers.push({ id: peerId, @@ -12,5 +23,15 @@ module.exports = ({ libp2p }) => { }) } return peers - }) + } + + return withTimeoutOption(addrs) } + +/** + * @typedef {Object} PeerInfo + * @property {string} id + * @property {Multiaddr[]} addrs + * + * @typedef {import('.').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/swarm/connect.js b/packages/ipfs-core/src/components/swarm/connect.js index aafa951742..541e4e4553 100644 --- a/packages/ipfs-core/src/components/swarm/connect.js +++ b/packages/ipfs-core/src/components/swarm/connect.js @@ -2,8 +2,22 @@ const { withTimeoutOption } = require('../../utils') -module.exports = ({ libp2p }) => { - return withTimeoutOption(function connect (addr, options) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Open a connection to a given address. + * + * @param {import('.').Multiaddr} addr + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function connect (addr, options) { + const { libp2p } = await network.use(options) return libp2p.dial(addr, options) - }) + } + + return withTimeoutOption(connect) } diff --git a/packages/ipfs-core/src/components/swarm/disconnect.js b/packages/ipfs-core/src/components/swarm/disconnect.js index 7d09d43edb..6c1c16bd0d 100644 --- a/packages/ipfs-core/src/components/swarm/disconnect.js +++ b/packages/ipfs-core/src/components/swarm/disconnect.js @@ -2,8 +2,22 @@ const { withTimeoutOption } = require('../../utils') -module.exports = ({ libp2p }) => { - return withTimeoutOption(function disconnect (addr, options) { +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Close a connection on a given address. + * + * @param {import('.').Multiaddr} addr + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function disconnect (addr, options) { + const { libp2p } = await network.use(options) return libp2p.hangUp(addr, options) - }) + } + + return withTimeoutOption(disconnect) } diff --git a/packages/ipfs-core/src/components/swarm/index.js b/packages/ipfs-core/src/components/swarm/index.js new file mode 100644 index 0000000000..29af23f83b --- /dev/null +++ b/packages/ipfs-core/src/components/swarm/index.js @@ -0,0 +1,29 @@ +'use strict' + +const createAddrsAPI = require('./addrs') +const createConnectAPI = require('./connect') +const createDisconnectAPI = require('./disconnect') +const createLocalAddrsAPI = require('./local-addrs') +const createPeersAPI = require('./peers') + +class SwarmAPI { + /** + * @param {Object} config + * @param {NetworkService} config.network + */ + constructor ({ network }) { + this.addrs = createAddrsAPI({ network }) + this.connect = createConnectAPI({ network }) + this.disconnect = createDisconnectAPI({ network }) + this.localAddrs = createLocalAddrsAPI({ network }) + this.peers = createPeersAPI({ network }) + } +} + +module.exports = SwarmAPI + +/** + * @typedef {import('..').NetworkService} NetworkService + * @typedef {import('..').Multiaddr} Multiaddr + * @typedef {import('..').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/swarm/local-addrs.js b/packages/ipfs-core/src/components/swarm/local-addrs.js index 10b7f04cc3..d679cee9c2 100644 --- a/packages/ipfs-core/src/components/swarm/local-addrs.js +++ b/packages/ipfs-core/src/components/swarm/local-addrs.js @@ -2,8 +2,25 @@ const { withTimeoutOption } = require('../../utils') -module.exports = ({ multiaddrs }) => { - return withTimeoutOption(async function localAddrs () { // eslint-disable-line require-await - return multiaddrs - }) +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Local addresses this node is listening on. + * + * @param {import('.').AbortOptions} [options] + * @returns {Promise} + */ + async function localAddrs (options) { + const { libp2p } = await network.use(options) + return libp2p.multiaddrs + } + + return withTimeoutOption(localAddrs) } + +/** + * @typedef {import('.').Multiaddr} Multiaddr + */ diff --git a/packages/ipfs-core/src/components/swarm/peers.js b/packages/ipfs-core/src/components/swarm/peers.js index 4a0654cad7..27377c6983 100644 --- a/packages/ipfs-core/src/components/swarm/peers.js +++ b/packages/ipfs-core/src/components/swarm/peers.js @@ -1,9 +1,19 @@ 'use strict' const { withTimeoutOption } = require('../../utils') - -module.exports = ({ libp2p }) => { - return withTimeoutOption(async function peers (options = {}) { // eslint-disable-line require-await +/** + * @param {Object} config + * @param {import('.').NetworkService} config.network + */ +module.exports = ({ network }) => { + /** + * Local addresses this node is listening on. + * + * @param {PeersOptions & AbortOptions} [options] + * @returns {Promise} + */ + async function peers (options = {}) { + const { libp2p } = await network.use(options) const verbose = options.v || options.verbose const peers = [] @@ -28,5 +38,26 @@ module.exports = ({ libp2p }) => { } return peers - }) + } + + return withTimeoutOption(peers) } + +/** + * @typedef {Object} PeerConnection + * @property {Multiaddr} addr + * @property {string} peer + * @property {string} [latency] + * @property {string} [muxer] + * @property {number} [direction] + * + * @typedef {Object} PeersOptions + * @property {boolean} [direction=false] + * @property {boolean} [streams=false] + * @property {boolean} [verbose=false] + * @property {boolean} [v=false] + * @property {boolean} [latency=false] + * + * @typedef {import('.').Multiaddr} Multiaddr + * @typedef {import('.').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/components/version.js b/packages/ipfs-core/src/components/version.js index 72b304a62f..20ab5cd9be 100644 --- a/packages/ipfs-core/src/components/version.js +++ b/packages/ipfs-core/src/components/version.js @@ -6,11 +6,15 @@ const { withTimeoutOption } = require('../utils') // gitHead is defined in published versions const meta = { gitHead: '', ...pkg } +/** + * @param {Object} config + * @param {import('.').Repo} config.repo + */ module.exports = ({ repo }) => { /** * Returns the implementation version * - * @param {import('../utils').AbortOptions} [options] + * @param {import('.').AbortOptions} [options] * @returns {Promise} * @example * ```js @@ -40,7 +44,7 @@ module.exports = ({ repo }) => { * supported by this node * * @property {string} version - * @property {string} repo + * @property {number} repo * @property {string} [commit] * @property {string} [interface-ipfs-core] * @property {string} [ipfs-http-client] diff --git a/packages/ipfs-core/src/errors.js b/packages/ipfs-core/src/errors.js index 465576b322..e4a434954a 100644 --- a/packages/ipfs-core/src/errors.js +++ b/packages/ipfs-core/src/errors.js @@ -44,6 +44,28 @@ class NotStartedError extends Error { NotStartedError.code = 'ERR_NOT_STARTED' exports.NotStartedError = NotStartedError +class AlreadyStartingError extends Error { + constructor (message = 'cannot start, already startin') { + super(message) + this.name = 'AlreadyStartingError' + this.code = AlreadyStartingError.code + } +} + +AlreadyStartingError.code = 'ERR_ALREADY_STARTING' +exports.AlreadyStartingError = AlreadyStartingError + +class AlreadyStartedError extends Error { + constructor (message = 'cannot start, already started') { + super(message) + this.name = 'AlreadyStartedError' + this.code = AlreadyStartedError.code + } +} + +AlreadyStartedError.code = 'ERR_ALREADY_STARTED' +exports.AlreadyStartedError = AlreadyStartedError + class NotEnabledError extends Error { constructor (message = 'not enabled') { super(message) diff --git a/packages/ipfs-core/src/index.js b/packages/ipfs-core/src/index.js index e718345207..bef8d923e3 100644 --- a/packages/ipfs-core/src/index.js +++ b/packages/ipfs-core/src/index.js @@ -1,10 +1,5 @@ 'use strict' -const log = require('debug')('ipfs') - -/** @type {typeof Object.assign} */ -const mergeOptions = require('merge-options') -const { isTest } = require('ipfs-utils/src/env') const globSource = require('ipfs-utils/src/files/glob-source') const urlSource = require('ipfs-utils/src/files/url-source') const PeerId = require('peer-id') @@ -16,70 +11,10 @@ const multicodec = require('multicodec') const multihashing = require('multihashing-async') const multihash = multihashing.multihash const CID = require('cids') -const { NotInitializedError } = require('./errors') -const Components = require('./components') -const ApiManager = require('./api-manager') - -const getDefaultOptions = () => ({ - init: true, - start: true, - EXPERIMENTAL: {}, - preload: { - enabled: !isTest, // preload by default, unless in test env - addresses: [ - '/dns4/node0.preload.ipfs.io/https', - '/dns4/node1.preload.ipfs.io/https', - '/dns4/node2.preload.ipfs.io/https', - '/dns4/node3.preload.ipfs.io/https' - ] - } -}) - -/** - * Creates and returns a ready to use instance of an IPFS node. - * - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * @param {CreateOptions} [options] - */ -async function create (options = {}) { - options = mergeOptions(getDefaultOptions(), options) - - // eslint-disable-next-line no-console - const print = options.silent ? log : console.log - - const apiManager = new ApiManager() - - const { api } = apiManager.update({ - init: Components.init({ apiManager, print, options }), - dns: Components.dns(), - isOnline: Components.isOnline({ libp2p: undefined }) - }, async () => { throw new NotInitializedError() }) // eslint-disable-line require-await - - const initializedApi = options.init && await api.init() - const startedApi = options.start && initializedApi && await initializedApi.start() - - /** - * create returns object that has different API set based on `options.init` - * and `options.start` values. If we just return `startedApi || initializedApi || api` - * TS will infer return type to be ` typeof startedAPI || typeof initializedApi || typeof api` - * which user would in practice act like `api` with all the extra APIs as optionals. - * - * Type trickery below attempts to affect inference by explicitly telling - * what the return type is and when. - * - * @typedef {typeof api} API - * @typedef {NonNullable} InitializedAPI - * @typedef {NonNullable} StartedAPI - * @type {If, API>} - */ - // @ts-ignore - const ipfs = startedApi || initializedApi || api - return ipfs -} +const IPFS = require('./components') module.exports = { - create, + create: IPFS.create, crypto, isIPFS, CID, @@ -92,100 +27,3 @@ module.exports = { globSource, urlSource } - -/** - * @template {boolean | InitOptions} Init - * @template {boolean} Start - * - * @typedef {Object} CreateOptions - * Options argument can be used to specify advanced configuration. - * @property {RepoOption} [repo='~/.jsipfs'] - * @property {boolean} [repoAutoMigrate=true] - `js-ipfs` comes bundled with a - * tool that automatically migrates your IPFS repository when a new version is - * available. - * @property {Init} [init=true] - Perform repo initialization steps when creating - * the IPFS node. - * Note that *initializing* a repo is different from creating an instance of - * [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor - * sets many special properties when initializing a repo, so you should usually - * not try and call `repoInstance.init()` yourself. - * @property {Start} [start=true] - If `false`, do not automatically - * start the IPFS node. Instead, you’ll need to manually call - * [`node.start()`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#nodestart) - * yourself. - * @property {string} [pass=null] - A passphrase to encrypt/decrypt your keys. - * @property {boolean} [silent=false] - Prevents all logging output from the - * IPFS node. (Default: `false`) - * @property {RelayOptions} [relay={ enabled: true, hop: { enabled: false, active: false } }] - * - Configure circuit relay (see the [circuit relay tutorial] - * (https://github.com/ipfs/js-ipfs/tree/master/examples/circuit-relaying) - * to learn more). - * @property {boolean} [offline=false] - Run ipfs node offline. The node does - * not connect to the rest of the network but provides a local API. - * @property {PreloadOptions} [preload] - Configure remote preload nodes. - * The remote will preload content added on this node, and also attempt to - * preload objects requested by this node. - * @property {ExperimentalOptions} [EXPERIMENTAL] - Enable and configure - * experimental features. - * @property {object} [config] - Modify the default IPFS node config. This - * object will be *merged* with the default config; it will not replace it. - * (Default: [`config-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-nodejs.js) - * in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/config-browser.js) - * in browsers) - * @property {import('./components').IPLDConfig} [ipld] - Modify the default IPLD config. This object - * will be *merged* with the default config; it will not replace it. Check IPLD - * [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information - * on the available options. (Default: [`ipld.js`] - * (https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core/runtime/ipld.js) - * in browsers) - * @property {object|Function} [libp2p] - The libp2p option allows you to build - * your libp2p node by configuration, or via a bundle function. If you are - * looking to just modify the below options, using the object format is the - * quickest way to get the default features of libp2p. If you need to create a - * more customized libp2p node, such as with custom transports or peer/content - * routers that need some of the ipfs data on startup, a custom bundle is a - * great way to achieve this. - * - You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs/js-ipfs/tree/master/examples/custom-libp2p). - * - Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) - * for the list of options libp2p supports. - * - Default: [`libp2p-nodejs.js`](../src/core/runtime/libp2p-nodejs.js) - * in Node.js, [`libp2p-browser.js`](../src/core/runtime/libp2p-browser.js) in - * browsers. - */ - -/** - * @typedef {IPFSRepo|string} RepoOption - * The file path at which to store the IPFS node’s data. Alternatively, you - * can set up a customized storage system by providing an `ipfs.Repo` instance. - * - * @example - * ```js - * // Store data outside your user directory - * const node = await IPFS.create({ repo: '/var/ipfs/data' }) - * ``` - * @typedef {import('./components/init').InitOptions} InitOptions - * - * @typedef {object} RelayOptions - * @property {boolean} [enabled] - Enable circuit relay dialer and listener. (Default: `true`) - * @property {object} [hop] - * @property {boolean} [hop.enabled] - Make this node a relay (other nodes can connect *through* it). (Default: `false`) - * @property {boolean} [hop.active] - Make this an *active* relay node. Active relay nodes will attempt to dial a destination peer even if that peer is not yet connected to the relay. (Default: `false`) - * - * @typedef {object} PreloadOptions - * @property {boolean} [enabled] - Enable content preloading (Default: `true`) - * @property {string[]} [addresses] - Multiaddr API addresses of nodes that should preload content. - * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. - * - * @typedef {object} ExperimentalOptions - * @property {boolean} [ipnsPubsub] - Enable pub-sub on IPNS. (Default: `false`) - * @property {boolean} [sharding] - Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`) - * - * @typedef {import('./components').IPFSRepo} IPFSRepo - */ - -/** - * Utility type to write type level conditionals - * - * @template Conditon, Then, Else - * @typedef {NonNullable extends false ? Else : Then } If - */ diff --git a/packages/ipfs-core/src/interface/basic.ts b/packages/ipfs-core/src/interface/basic.ts new file mode 100644 index 0000000000..41dea96426 --- /dev/null +++ b/packages/ipfs-core/src/interface/basic.ts @@ -0,0 +1,28 @@ +import CID from 'cids' +import PeerId from 'peer-id' +import BigInteger from 'bignumber.js' +export type Await = + | T + | Promise + +export type AwaitIterable = + | Iterable + | AsyncIterable + +export interface AbortOptions { + signal?: AbortSignal +} +export type ToJSON = + | null + | string + | number + | boolean + | ToJSON[] + | { toJSON?: () => ToJSON } & {[key:string]: ToJSON} + +export interface Block { + cid: CID + data: Uint8Array +} + +export type { CID, PeerId, BigInteger } diff --git a/packages/ipfs-core/src/interface/bitswap.ts b/packages/ipfs-core/src/interface/bitswap.ts new file mode 100644 index 0000000000..456a232adc --- /dev/null +++ b/packages/ipfs-core/src/interface/bitswap.ts @@ -0,0 +1,88 @@ +import { PeerId, CID, Block, Await, BigInteger, AbortOptions } from './basic' +import { MovingAverage } from './moving-avarage' +import { StoreReader, StoreExporter, StoreImporter } from './store' + +export interface Bitswap extends + StoreReader, + StoreExporter, + StoreImporter +{ + + readonly peerId: PeerId + + enableStats(): void + disableStats(): void + + wantlistForPeer(peerId: PeerId, options?:AbortOptions): Map + ledgerForPeer(peerId: PeerId): Ledger + + put(block: Block, options?: AbortOptions): Await + + unwant(cids: Iterable, options?: AbortOptions): void + cancelWants(cids: Iterable): void + getWantlist(options?: AbortOptions): Iterable<[string, WantListEntry]> + peers(): PeerId[] + stat(): Stats + start(): void + stop(): void +} + +export interface Ledger { + sentBytes(n:number):void + receivedBytes(n:number):void + + wants(cid: CID, priority: number, wantType: WantType):void + cancelWant(cid: CID): void + wantlistContains(cid:CID): WantListEntry|void + + debtRatio():number +} + +export interface WantListEntry { + readonly cid: CID + priority: number + inc(): void + dec(): void + hasRefs(): boolean + equals(other: WantListEntry): boolean +} + +export type WantList = { + entries: Entry[] + full?: boolean +} + +export type Entry = { + block: Uint8Array + priority: number + cancel: boolean + wantType?: WantType + sendDontHave?: boolean +} + +export type BlockPresence = { + cid: Uint8Array + type: BlockPresenceType +} + +export type Have = 0 +export type DontHave = 1 +export type BlockPresenceType = Have | DontHave + +export type WantBlock = 0 +export type HaveBlock = 1 +export type WantType = WantBlock | HaveBlock + +export type BlockData = { + prefix: Uint8Array + data: Uint8Array +} + +export interface Stats { + enable(): void + disable(): void + stop(): void + readonly snapshot: Record + readonly movingAverages: Record> + push(counter: number, inc: number): void +} diff --git a/packages/ipfs-core/src/interface/block-service.ts b/packages/ipfs-core/src/interface/block-service.ts new file mode 100644 index 0000000000..aa206edc4d --- /dev/null +++ b/packages/ipfs-core/src/interface/block-service.ts @@ -0,0 +1,20 @@ +import { Block, CID, Await, AbortOptions } from './basic' +import { StoreReader, StoreImporter, StoreExporter, StoreEraser } from './store' +import { Bitswap } from './bitswap' + +export interface BlockService extends + StoreReader, + StoreExporter, + StoreImporter, + StoreEraser +{ + setExchange(bitswap: Bitswap): void + + unsetExchange(): void + hasExchange(): boolean + + /** + * Put a block to the underlying datastore. + */ + put(block: Block, options?:AbortOptions): Await +} diff --git a/packages/ipfs-core/src/interface/datastore.ts b/packages/ipfs-core/src/interface/datastore.ts new file mode 100644 index 0000000000..418eadccf5 --- /dev/null +++ b/packages/ipfs-core/src/interface/datastore.ts @@ -0,0 +1,185 @@ +import { KeyValueStore, StoreBatch, StoreSelector, Resource } from './store' +export interface DataStore extends + KeyValueStore, + StoreSelector, + StoreBatch, + Resource +{ +} + +export interface Key { + /** + * Returns the "name" of this key (field of last namespace). + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.name() + * // 'JohnCleese' + * ``` + */ + name(): string + + /** + * Returns the "type" of this key (value of last namespace). + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * key.type() + * // 'Actor' + * ``` + */ + type(): string + + /** + * Returns the `namespaces` making up this `Key`. + */ + namespaces(): string[] + + /** + * Returns the "base" namespace of this key. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.baseNamespace() + * // 'Actor:JohnCleese' + */ + baseNamespace(): string + + /** + * Returns an "instance" of this type key (appends value to namespace). + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor' + * key.instance('JohnClesse').toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * ``` + */ + instance(): Key + + /** + * Returns the "path" of this key (parent + type). + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * key.path().toString() + * // '/Comedy/MontyPython/Actor' + * ``` + */ + path(): Key + + /** + * Returns the `parent` Key of this Key. + * + * @example + * ```js + * key.toString() + * "/Comedy/MontyPython/Actor:JohnCleese" + * key.parent().toString() + * // "/Comedy/MontyPython" + * ``` + */ + parent(): Key + + /** + * Returns the `child` Key of this Key. + * + * @example + * ```js + * key.toString() + * '/Comedy/MontyPython' + * child.toString() + * // 'Actor:JohnCleese' + * key.child(child).toString() + * '/Comedy/MontyPython/Actor:JohnCleese' + * ``` + */ + child(key: Key): Key + + /** + * Check if the given key is sorted lower than this. + */ + less(key: Key): boolean + + /** + * Returns whether this key is a prefix of `other` + * + * @example + * ```js + * comedy.toString() + * '/Comedy' + * monty.toString() + * '/Comedy/MontyPython' + * comedy.isAncestorOf(monty) + * // true + * ``` + */ + isAncestorOf(other: Key): boolean + + /** + * Returns whether this key is a contains `other` as prefix. + * ```js + * comedy.toString() + * '/Comedy' + * monty.toString() + * '/Comedy/MontyPython' + * monty.isDecendantOf(comedy) + * // true + * ``` + */ + isDecendantOf(other: Key): boolean + + /** + * Returns wether this key has only one namespace. + */ + isTopLevel(): boolean + + /** + * Returns the key with all parts in reversed order. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.reverse().toString() + * // /Actor:JohnCleese/MontyPython/Comedy + * new Key('/Comedy/MontyPython/Actor:JohnCleese').reverse() + * ``` + */ + reverse(): Key + + /** + * Concats one or more Keys into one new Key. + */ + concat(...keys: Key[]): Key + + /** + * Returns the array representation of this key. + * + * @example + * ```js + * key.toString() + * // '/Comedy/MontyPython/Actor:JohnCleese' + * key.list() + * // ['Comedy', 'MontyPythong', 'Actor:JohnCleese'] + * ``` + */ + list(): string[] + toString(): string +} + +export type Value = Uint8Array + +export interface Entry { + key: Key, + value: Value +} diff --git a/packages/ipfs-core/src/interface/format.ts b/packages/ipfs-core/src/interface/format.ts new file mode 100644 index 0000000000..4c0da87482 --- /dev/null +++ b/packages/ipfs-core/src/interface/format.ts @@ -0,0 +1,41 @@ + +import { Await, CID } from './basic' + +export interface Format { + util: Util + resolver: Resolver + + defaultHashArg: string | number + codec: string | number +} + +export interface Util { + /** + * Serialize an IPLD Node into a binary blob. + */ + serialize(node:T):Uint8Array + /** + * Deserialize a binary blob into an IPLD Node. + */ + deserialize(bytes: Uint8Array): T + + /** + * Calculate the CID of the binary blob. + */ + cid(bytes:Uint8Array, options?:CIDOptions): Await +} + +export interface CIDOptions { + cidVersion?: number + hashAlg?: number | string +} + +export interface Resolver { + resolve(bytes: Uint8Array, path: string): ResolveResult + tree(byte: Uint8Array): string[] +} + +export interface ResolveResult { + value: T + remainderPath: string +} diff --git a/packages/ipfs-core/src/interface/ipld.ts b/packages/ipfs-core/src/interface/ipld.ts new file mode 100644 index 0000000000..c168e38f61 --- /dev/null +++ b/packages/ipfs-core/src/interface/ipld.ts @@ -0,0 +1,43 @@ +import { BlockService } from './block-service' +import { Await, CID, AwaitIterable, AbortOptions } from './basic' +import { StoreReader, StoreExporter, StoreEraser } from './store' +import { ResolveResult, Format } from './format' + +export interface IPLD extends + StoreReader, + StoreExporter, + StoreEraser + +{ + put(value:T, format:FormatCode, options?:PutOptions & AbortOptions):Await + putMany(values: AwaitIterable, format: FormatCode, options?:PutOptions):AwaitIterable + + resolve(cid: CID, path: string, options?: AbortOptions): AwaitIterable> + tree(cid:CID, path?:string, options?:TreeOptions & AbortOptions):AwaitIterable + + addFormat(format:Format):IPLD + removeFormat(format:Format):IPLD + + defaultOptions: Options +} + +export type FormatCode = number +export type HashAlg = number + +export interface Options { + blockService?: BlockService + formats?: Record + + loadFormat?: (code:number|string) => Promise> +} + +export interface PutOptions { + hashAlg?: HashAlg, + cidVersion?: 0|1, + onlyHash?: boolean, + +} + +export interface TreeOptions { + recursive?: boolean +} diff --git a/packages/ipfs-core/src/interface/moving-avarage.ts b/packages/ipfs-core/src/interface/moving-avarage.ts new file mode 100644 index 0000000000..99045ae3f8 --- /dev/null +++ b/packages/ipfs-core/src/interface/moving-avarage.ts @@ -0,0 +1,9 @@ +export interface MovingAverage { + variance(): number + movingAverage(): number + + deviation(): number + forecast(): number + + push(time: number, value:number):void +} diff --git a/packages/ipfs-core/src/interface/repo.ts b/packages/ipfs-core/src/interface/repo.ts new file mode 100644 index 0000000000..efe003516a --- /dev/null +++ b/packages/ipfs-core/src/interface/repo.ts @@ -0,0 +1,104 @@ +import { CID, Block, ToJSON, Await, AbortOptions } from './basic' +import { DataStore, Key } from './datastore' +import { + ValueStore, StoreReader, Resource, StoreLookup, + StoreImporter, StoreExporter, StoreEraser, StoreSelector, + KeyValueStore +} from './store' + +export interface Repo extends Resource { + readonly path: string + closed: boolean + + /** + * Initializes necessary structures inside the repo + */ + init(config:Partial):Await + + /** + * Tells whether this repo exists or not. + */ + exists():Await + + /** + * Tells whether the repo has been initialized. + */ + isInitialized():Await + + /** + * Gets the repo status. + */ + stat(options?:AbortOptions):Await + + root: KeyValueStore + + blocks: BlockStore + datastore: DataStore + + pins: PinStore + config: ConfigStore + keys: KeyStore + + version: ValueStore + apiAddr: ValueStore +} + +export interface RepoStatus { + numObjects: number + repoPath: string + repoSize: number + version: number + storageMax: number +} + +interface BlockStore extends + StoreImporter, + StoreReader, + StoreLookup, + StoreExporter, + StoreEraser, + StoreSelector +{ + put(block: Block): Await +} + +export interface ConfigStore extends + StoreReader +{ + /** + * Set a config `value`, where `value` can be anything that is serializable + * to JSON. + */ + set(key: string, value: ToJSON, options?:AbortOptions): Await + + /** + * Set the whole `config` which can be a any value that is serializable to + * JSON. + * + * @param config + */ + replace(config: Config, options?: AbortOptions): Await + + /** + * Get the entire config value. + */ + getAll(options?:AbortOptions): Await + + /** + * Whether the config sub-repo exists. + */ + exists(): Await +} + +export interface PinStore extends + KeyValueStore, + Object +{ +} + +export interface KeyStore extends + KeyValueStore, + Object +{ + +} diff --git a/packages/ipfs-core/src/interface/store.ts b/packages/ipfs-core/src/interface/store.ts new file mode 100644 index 0000000000..332c573062 --- /dev/null +++ b/packages/ipfs-core/src/interface/store.ts @@ -0,0 +1,124 @@ + +import { Await, AwaitIterable, AbortOptions } from './basic' + +export interface ValueStore { + get(options?:AbortOptions): Await + set(value: T): Await +} + +export interface KeyValueStore extends + StoreReader, + StoreExporter, + StoreSelector, + StoreLookup, + StoreWriter, + StoreImporter, + StoreEraser { +} + +// Interface Datastore + +export interface StoreReader { + /** + * The key retrieve the value for. + */ + get(key: Key, options?: AbortOptions): Await +} + +export interface StoreLookup { + /** + * Check for the existence of a given key + */ + has(key: Key, options?: AbortOptions): Await +} + +export interface StoreExporter { + /** + * Retrieve a stream of values stored under the given keys. + */ + getMany(keys: AwaitIterable, options?: AbortOptions): AwaitIterable +} + +export interface StoreSelector { + /** + * Search the store for some values. + */ + query(query: Query, options?: AbortOptions): AwaitIterable +} + +export interface StoreWriter { + /** + * Store a value with the given key. + */ + put(key: Key, value: Value, options?: AbortOptions): Await +} + +export interface StoreImporter { + /** + * Store many key-value pairs. + */ + putMany(entries: AwaitIterable, options?: AbortOptions): AwaitIterable +} + +export interface StoreEraser { + /** + * Delete the content stored under the given key. + */ + delete(key: Key, options?: AbortOptions): Await + /** + * Delete the content stored under the given keys. + */ + deleteMany(keys: AwaitIterable, options?: AbortOptions): AwaitIterable + +} +export interface StoreBatch { + batch(): Batch +} + +export interface Batch { + put(key: Key, value: Value): void + delete(key: Key): void + + commit(options?: AbortOptions): Await +} + +export interface Resource { + /** + * Opens the datastore, this is only needed if the store was closed before, + * otherwise this is taken care of by the constructor. + */ + open(): Await + /** + * Close the datastore, this should always be called to ensure resources + * are cleaned up. + */ + close(): Await +} + +export interface Query { + /** + * Only return values where the key starts with this prefix + */ + prefix?: string + /** + * Filter the results according to the these functions + */ + filters?: Array<(resut: Entry) => boolean> + /** + * Order the results according to these functions + */ + orders?: Array<(results: Entry[]) => Entry[]> + /** + * Only return this many records + */ + limit?: number + /** + * An options object, all properties are optional + */ + options?: Options + /** + * A way to signal that the caller is no longer interested in the outcome of + * this operation + */ + signal?: AbortSignal +} diff --git a/packages/ipfs-core/src/mfs-preload.js b/packages/ipfs-core/src/mfs-preload.js index aabffee3bb..1716e1206c 100644 --- a/packages/ipfs-core/src/mfs-preload.js +++ b/packages/ipfs-core/src/mfs-preload.js @@ -8,9 +8,9 @@ const log = Object.assign(debug('ipfs:mfs-preload'), { /** * @param {Object} config - * @param {import('./components/index').Preload} config.preload - * @param {import('./components/index').Files} config.files - * @param {import('./components/init').PreloadOptions} [config.options] + * @param {import('./components').Preload} config.preload + * @param {import('./components').Files} config.files + * @param {import('./components').PreloadOptions} [config.options] */ module.exports = ({ preload, files, options = {} }) => { options.interval = options.interval || 30 * 1000 diff --git a/packages/ipfs-core/src/preload.js b/packages/ipfs-core/src/preload.js index 53425347d3..6168e6ff42 100644 --- a/packages/ipfs-core/src/preload.js +++ b/packages/ipfs-core/src/preload.js @@ -6,9 +6,6 @@ const CID = require('cids') const shuffle = require('array-shuffle') const AbortController = require('native-abort-controller') const preload = require('./runtime/preload-nodejs') -/** @type {typeof import('hashlru').default} */ -// @ts-ignore - hashlru has incorrect typedefs -const hashlru = require('hashlru') const log = Object.assign( debug('ipfs:preload'), @@ -16,15 +13,11 @@ const log = Object.assign( ) /** - * @param {Object} [options] - * @param {boolean} [options.enabled = false] - Whether to preload anything - * @param {string[]} [options.addresses = []] - Which preload servers to use - * @param {number} [options.cache = 1000] - How many CIDs to cache + * @param {Options & AbortOptions} [options] */ const createPreloader = (options = {}) => { options.enabled = Boolean(options.enabled) options.addresses = options.addresses || [] - options.cache = options.cache || 1000 if (!options.enabled || !options.addresses.length) { log('preload disabled') @@ -39,9 +32,6 @@ const createPreloader = (options = {}) => { let requests = [] const apiUris = options.addresses.map(toUri) - // Avoid preloading the same CID over and over again - const cache = hashlru(options.cache) - /** * @param {string|CID} path * @returns {Promise} @@ -54,14 +44,6 @@ const createPreloader = (options = {}) => { path = new CID(path).toString() } - if (cache.has(path)) { - // we've preloaded this recently, don't preload it again - return - } - - // make sure we don't preload this again any time soon - cache.set(path, true) - const fallbackApiUris = shuffle(apiUris) let success = false const now = Date.now() @@ -111,3 +93,15 @@ const createPreloader = (options = {}) => { } module.exports = createPreloader + +/** + * @typedef {ReturnType} Preload + * + * @typedef {object} Options + * @property {boolean} [enabled] - Enable content preloading (Default: `true`) + * @property {number} [interval] + * @property {string[]} [addresses] - Multiaddr API addresses of nodes that should preload content. + * **NOTE:** nodes specified here should also be added to your node's bootstrap address list at `config.Boostrap`. + * + * @typedef {import('./components').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-core/src/runtime/repo-nodejs.js b/packages/ipfs-core/src/runtime/repo-nodejs.js index e8ad5ee54c..a5f3ec99d8 100644 --- a/packages/ipfs-core/src/runtime/repo-nodejs.js +++ b/packages/ipfs-core/src/runtime/repo-nodejs.js @@ -9,6 +9,7 @@ const path = require('path') * @param {string} [options.path] * @param {boolean} [options.silent] * @param {boolean} [options.autoMigrate] + * @returns {Repo} */ module.exports = (options = {}) => { const repoPath = options.path || path.join(os.homedir(), '.jsipfs') @@ -29,3 +30,8 @@ module.exports = (options = {}) => { onMigrationProgress: options.silent ? null : onMigrationProgress }) } + +/** + * @typedef {import('../interface/repo').Repo} Repo + * @typedef {import('../components/config').IPFSConfig} IPFSConfig + */ diff --git a/packages/ipfs-core/src/utils.js b/packages/ipfs-core/src/utils.js index 18fcad2326..3c81077388 100644 --- a/packages/ipfs-core/src/utils.js +++ b/packages/ipfs-core/src/utils.js @@ -10,10 +10,20 @@ const Key = require('interface-datastore').Key const { TimeoutError } = require('./errors') const errCode = require('err-code') const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path') +/** @type {typeof Object.assign} */ +const mergeOptions = require('merge-options') + +exports.mergeOptions = mergeOptions const ERR_BAD_PATH = 'ERR_BAD_PATH' exports.OFFLINE_ERROR = 'This command must be run in online mode. Try running \'ipfs daemon\' first.' + +exports.MFS_FILE_TYPES = { + file: 0, + directory: 1, + 'hamt-sharded-directory': 1 +} exports.MFS_ROOT_KEY = new Key('/local/filesroot') exports.MFS_MAX_CHUNK_SIZE = 262144 exports.MFS_MAX_LINKS = 174 @@ -67,7 +77,7 @@ const normalizeCidPath = (path) => { * - /ipfs//link/to/pluto * - multihash Buffer * - * @param {import('./components').DAG} dag - The IPFS dag api + * @param {import('./components').DagReader} dag * @param {CID | string} ipfsPath - A CID or IPFS path * @param {Object} [options] - Optional options passed directly to dag.resolve * @returns {Promise} @@ -314,8 +324,19 @@ function withTimeoutOption (fn, optionsArgIndex) { } } +const withTimeout = withTimeoutOption( + /** + * @template T + * @param {Promise|T} promise + * @param {AbortOptions} [_options] + * @returns {Promise} + */ + async (promise, _options) => await promise +) + exports.normalizePath = normalizePath exports.normalizeCidPath = normalizeCidPath exports.resolvePath = resolvePath exports.mapFile = mapFile exports.withTimeoutOption = withTimeoutOption +exports.withTimeout = withTimeout diff --git a/packages/ipfs-core/src/utils/service.js b/packages/ipfs-core/src/utils/service.js new file mode 100644 index 0000000000..a8eea7ab4e --- /dev/null +++ b/packages/ipfs-core/src/utils/service.js @@ -0,0 +1,239 @@ +'use strict' + +const { NotStartedError, AlreadyStartingError, AlreadyStartedError } = require('../errors') +const { withTimeout } = require('../utils') + +/** + * @template Options, T + * + * Allows you to create a handle to service that can be started or + * stopped. It enables defining components that need to use service + * functionality before service is started. + * + */ +class Service { + /** + * Takes `activation` function that takes `options` and (async) returns + * an implementation. + * + * @template {(options:any) => Await} T + * + * @param {Object} config + * @param {T} config.start + * @param {(state:State) => Await} [config.stop] + * @returns {Service[0], State>} + */ + static create ({ start, stop }) { + return new Service(start, stop) + } + + /** + * Starts the service (by running actiavtion function). Will (async) throw + * unless service is stopped. + * + * @template Options, T + * @param {Service} service + * @param {Options} options + * @returns {Promise} + */ + static async start (service, options) { + const { state, activate } = service + switch (state.status) { + // If service is in 'stopped' state we activate and transition to + // to 'pending' state. Once activation is complete transition state to + // 'started' state. + // Note: This is the only code that does state transitions from + // - stopped + // - started + // Which ensures no race conditions can occur. + case 'stopped': { + try { + const promise = activate(options) + service.state = { status: 'starting', ready: promise } + // Note: MUST await after state transition above otherwise race + // condition may occur. + const result = await promise + service.state = { status: 'started', value: result } + return result + // If failed to start, transiton from 'starting' to 'stopped' + // state. + } catch (error) { + service.state = { status: 'stopped' } + throw error + } + } + case 'starting': { + throw new AlreadyStartingError() + } + case 'started': { + throw new AlreadyStartedError() + } + // If service is stopping we just wait for that to complete + // and try again. + case 'stopping': { + await state.ready + return await Service.start(service, options) + } + default: { + return Service.panic(service) + } + } + } + + /** + * Stops the service by executing deactivation. If service is stopped + * or is stopping this is noop. If service is starting up when called + * it will await for start to complete and then retry stop afterwards. + * This may (async) throw if `deactivate` does. + * + * @template T + * @param {Service} service + * @returns {Promise} + */ + static async stop (service) { + const { state, deactivate } = service + switch (state.status) { + // If stopped there's nothing to do. + case 'stopped': { + break + } + // If service is starting we await for it to complete + // and try again. That way + case 'starting': { + // We do not want to error stop if start failed. + try { await state.ready } catch (_) {} + return await Service.stop(service) + } + // if service is stopping we just await for it to complete. + case 'stopping': { + return await state.ready + } + case 'started': { + if (deactivate) { + await deactivate(state.value) + } + service.state = { status: 'stopped' } + break + } + default: { + Service.panic(state) + } + } + } + + /** + * @template T + * @param {Service} service + * @returns {T|null} + */ + static try ({ state }) { + switch (state.status) { + case 'started': + return state.value + default: + return null + } + } + + /** + * Unwraps state and returns underlying value. If state is in idle state it + * will throw an error. If state is pending it will wait and return the + * result or throw on failure. If state is ready returns result. + * + * @template T + * @param {Service} service + * @param {AbortOptions} [options] + * @returns {Promise} + */ + static async use ({ state }, options) { + switch (state.status) { + case 'started': + return state.value + case 'starting': + return await withTimeout(state.ready, options) + default: + throw new NotStartedError() + } + } + + // eslint-disable-next-line jsdoc/require-returns-check + /** + * @private + * @param {Service} service + * @returns {never} + */ + static panic ({ state }) { + const status = JSON.stringify({ status: state.status }) + throw RangeError(`Service in invalid state ${status}, should never happen if you see this please report a bug`) + } + + /** + * Takes `activation` function that takes `options` and (async) returns + * an implementation. + * + * @private + * @param {(options:Options) => Await} activate + * @param {(state:T) => Await} [deactivate] + */ + constructor (activate, deactivate) { + this.activate = activate + this.deactivate = deactivate + + /** + * A state machine for this service. + * + * @private + * @type {ServiceState} + */ + this.state = { status: 'stopped' } + } + + /** + * Allows you to asynchronously obtain service implementation. If service + * is starting it will await for completion. If service is stopped or stopping + * this will (async) throw exception. This allows components that need to use + * this service convinient API to do it. + * + * @param {AbortOptions} [options] - Abort options. + * @returns {Promise} + */ + async use (options) { + return await Service.use(this, options) + } + + /** + * @returns {T|null} + */ + try () { + return Service.try(this) + } +} +module.exports = Service + +/** + * @template T + * @typedef {import('../interface/basic').Await} Await + */ +/** + * @template {(options:any) => any} T + * @typedef {Parameters[0]} Options + */ +/** + * @template {(options:any) => any} T + * @typedef {ReturnType extends ? Promise ? U : ReturnType} State + */ +/** + * Reperests service state which can be not started in which case + * it is instaceof `Error`. Pending in which case it's promise or + * ready in which case it is the value itself. + * + * @template T + * @typedef {{ status: 'stopped' } + * | { status: 'starting', ready: Await } + * | { status: 'started', value: T } + * | { status: 'stopping', ready: Await } + * } ServiceState + */ +/** + * @typedef {import('../utils').AbortOptions} AbortOptions + */ diff --git a/packages/ipfs-http-client/src/block/put.js b/packages/ipfs-http-client/src/block/put.js index d64b747ff9..36be441477 100644 --- a/packages/ipfs-http-client/src/block/put.js +++ b/packages/ipfs-http-client/src/block/put.js @@ -23,6 +23,8 @@ module.exports = configure(api => { mhlen: length, version: data.cid.version } + // @ts-ignore - data is typed as block so TS complains about + // Uint8Array assignment. data = data.data } else if (options.cid) { const cid = new CID(options.cid) diff --git a/packages/ipfs-http-client/src/dag/put.js b/packages/ipfs-http-client/src/dag/put.js index 2a22176591..e235edac82 100644 --- a/packages/ipfs-http-client/src/dag/put.js +++ b/packages/ipfs-http-client/src/dag/put.js @@ -23,21 +23,24 @@ module.exports = configure((api, opts) => { throw new Error('Failed to put DAG node. Provide `format` AND `hashAlg` options') } + let encodingOptions if (options.cid) { const cid = new CID(options.cid) - options = { + encodingOptions = { ...options, format: multicodec.getName(cid.code), hashAlg: multihash.decode(cid.multihash).name } delete options.cid + } else { + encodingOptions = options } const settings = { format: 'dag-cbor', hashAlg: 'sha2-256', inputEnc: 'raw', - ...options + ...encodingOptions } const format = await load(settings.format)