From 23421f397e4ae80ac9e9264ee6662b2cc0e34791 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Wed, 11 Dec 2019 19:29:12 +0000 Subject: [PATCH 1/6] refactor: use new ipfsd-ctl setup (#1127) * fix: use new ipfsd-ctl setup * chore: add correct branches * fix: support new ctl * fix: update to the latest ctl api * fix: use proper module * fix: override properly * chore: bump ipfsd-ctl, interface-core --- package.json | 4 +- test/commands.spec.js | 16 +-- test/constructor.spec.js | 12 +- test/dag.spec.js | 16 +-- test/diag.spec.js | 16 +-- test/files-mfs.spec.js | 16 +-- test/get.spec.js | 17 +-- test/interface.spec.js | 146 ++++++++++++------------- test/key.spec.js | 17 +-- test/log.spec.js | 16 +-- test/ping.spec.js | 30 +---- test/repo.spec.js | 16 +-- test/stats.spec.js | 16 +-- test/utils/factory.js | 14 ++- test/utils/interface-common-factory.js | 92 ---------------- 15 files changed, 106 insertions(+), 338 deletions(-) delete mode 100644 test/utils/interface-common-factory.js diff --git a/package.json b/package.json index abaf35d69..f0253c933 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,8 @@ "cross-env": "^6.0.0", "detect-node": "^2.0.4", "go-ipfs-dep": "^0.4.22", - "interface-ipfs-core": "^0.124.0", - "ipfsd-ctl": "^0.47.1", + "interface-ipfs-core": "~0.125.0", + "ipfsd-ctl": "^1.0.0", "ndjson": "^1.5.0", "nock": "^11.4.0", "pull-stream": "^3.6.14", diff --git a/test/commands.spec.js b/test/commands.spec.js index 2f7b1f5e3..9e68a1e01 100644 --- a/test/commands.spec.js +++ b/test/commands.spec.js @@ -2,30 +2,18 @@ 'use strict' const { expect } = require('interface-ipfs-core/src/utils/mocha') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.commands', function () { this.timeout(60 * 1000) - let ipfsd let ipfs before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('lists commands', async () => { const res = await ipfs.commands() diff --git a/test/constructor.spec.js b/test/constructor.spec.js index 60bfc9f33..a1a000c84 100644 --- a/test/constructor.spec.js +++ b/test/constructor.spec.js @@ -93,24 +93,18 @@ describe('ipfs-http-client constructor tests', () => { }) describe('integration', () => { - let apiAddr let ipfsd before(async function () { this.timeout(60 * 1000) // slow CI - ipfsd = await f.spawn({ initOptions: { bits: 1024, profile: 'test' } }) - apiAddr = ipfsd.apiAddr.toString() + ipfsd = await f.spawn() }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('can connect to an ipfs http api', async () => { - await clientWorks(ipfsClient(apiAddr)) + await clientWorks(ipfsClient(ipfsd.apiAddr)) }) }) }) diff --git a/test/dag.spec.js b/test/dag.spec.js index cd58002ad..de9217d13 100644 --- a/test/dag.spec.js +++ b/test/dag.spec.js @@ -6,29 +6,17 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha') const { DAGNode } = require('ipld-dag-pb') const CID = require('cids') -const ipfsClient = require('../src') const f = require('./utils/factory') -let ipfsd let ipfs describe('.dag', function () { this.timeout(20 * 1000) before(async function () { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('should be able to put and get a DAG node with format dag-pb', async () => { const data = Buffer.from('some data') diff --git a/test/diag.spec.js b/test/diag.spec.js index e42716214..37a1911e7 100644 --- a/test/diag.spec.js +++ b/test/diag.spec.js @@ -3,7 +3,6 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha') const platform = require('browser-process-platform') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.diag', function () { @@ -12,24 +11,13 @@ describe('.diag', function () { // go-ipfs does not support these on Windows if (platform === 'win32') { return } - let ipfsd let ipfs before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) describe('api API', () => { // Disabled in go-ipfs 0.4.10 diff --git a/test/files-mfs.spec.js b/test/files-mfs.spec.js index 6ae508d1d..0e3d6b1af 100644 --- a/test/files-mfs.spec.js +++ b/test/files-mfs.spec.js @@ -10,7 +10,6 @@ const values = require('pull-stream/sources/values') const pull = require('pull-stream/pull') const collect = require('pull-stream/sinks/collect') -const ipfsClient = require('../src') const f = require('./utils/factory') const expectTimeout = require('./utils/expect-timeout') @@ -31,26 +30,15 @@ const HASH_ALGS = [ describe('.files (the MFS API part)', function () { this.timeout(20 * 1000) - let ipfsd let ipfs const expectedMultihash = 'Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP' before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('.add file for testing', async () => { const res = await ipfs.add(testfile) diff --git a/test/get.spec.js b/test/get.spec.js index 7c1a63e74..a6fb3aaa4 100644 --- a/test/get.spec.js +++ b/test/get.spec.js @@ -6,7 +6,6 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha') const loadFixture = require('aegir/fixtures') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.get (specific go-ipfs features)', function () { @@ -21,26 +20,14 @@ describe('.get (specific go-ipfs features)', function () { data: fixture('test/fixtures/testfile.txt') } - let ipfsd let ipfs before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) - + ipfs = (await f.spawn()).api await ipfs.add(smallFile.data) }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('no compression args', async () => { const files = await ipfs.get(smallFile.cid) diff --git a/test/interface.spec.js b/test/interface.spec.js index dc41ec325..5435518cf 100644 --- a/test/interface.spec.js +++ b/test/interface.spec.js @@ -2,47 +2,41 @@ 'use strict' const tests = require('interface-ipfs-core') -const isNode = require('detect-node') -const CommonFactory = require('./utils/interface-common-factory') +const merge = require('merge-options') +const { isNode } = require('ipfs-utils/src/env') +const { createFactory } = require('ipfsd-ctl') +const { findBin } = require('ipfsd-ctl/src/utils') const isWindows = process.platform && process.platform === 'win32' -describe('interface-ipfs-core tests', () => { - const defaultCommonFactory = CommonFactory.createAsync() - - tests.bitswap(defaultCommonFactory, { - skip: [ - // bitswap.stat - { - name: 'should not get bitswap stats when offline', - reason: 'FIXME go-ipfs returns an error https://github.com/ipfs/go-ipfs/issues/4078' - }, - // bitswap.wantlist - { - name: 'should not get the wantlist when offline', - reason: 'FIXME go-ipfs returns an error https://github.com/ipfs/go-ipfs/issues/4078' - }, - // bitswap.unwant - { - name: 'should remove a key from the wantlist', - reason: 'FIXME why is this skipped?' - }, - { - name: 'should not remove a key from the wantlist when offline', - reason: 'FIXME go-ipfs returns an error https://github.com/ipfs/go-ipfs/issues/4078' - } - ] - }) +/** @typedef {import("ipfsd-ctl").ControllerOptions} ControllerOptions */ - tests.block(defaultCommonFactory, { +describe('interface-ipfs-core tests', () => { + /** @type ControllerOptions */ + const commonOptions = { + test: true, + ipfsHttpModule: { + path: require.resolve('../src'), + ref: require('../src') + }, + ipfsOptions: { + pass: 'ipfs-is-awesome-software' + }, + ipfsBin: findBin('go') + } + const commonFactory = createFactory(commonOptions) + + tests.bitswap(commonFactory) + + tests.block(commonFactory, { skip: [{ name: 'should get a block added as CIDv1 with a CIDv0', reason: 'go-ipfs does not support the `version` param' }] }) - tests.bootstrap(defaultCommonFactory) + tests.bootstrap(commonFactory) - tests.config(defaultCommonFactory, { + tests.config(commonFactory, { skip: [ // config.replace { @@ -60,7 +54,7 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.dag(defaultCommonFactory, { + tests.dag(commonFactory, { skip: [ // dag.tree { @@ -87,7 +81,7 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.dht(defaultCommonFactory, { + tests.dht(commonFactory, { skip: [ // dht.findpeer { @@ -107,47 +101,47 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.filesRegular(defaultCommonFactory, { + tests.filesMFS(commonFactory, { skip: [ - // .addFromFs - isNode ? null : { - name: 'addFromFs', - reason: 'Not designed to run in the browser' - }, - // .catPullStream { - name: 'should export a chunk of a file', - reason: 'TODO not implemented in go-ipfs yet' + name: 'should ls directory with long option', + reason: 'TODO unskip when go-ipfs supports --long https://github.com/ipfs/go-ipfs/pull/6528' }, { - name: 'should export a chunk of a file in a Pull Stream', + name: 'should read from outside of mfs', reason: 'TODO not implemented in go-ipfs yet' }, { - name: 'should export a chunk of a file in a Readable Stream', + name: 'should ls from outside of mfs', reason: 'TODO not implemented in go-ipfs yet' } ] }) - tests.filesMFS(defaultCommonFactory, { + tests.filesRegular(commonFactory, { skip: [ + // .addFromFs + isNode ? null : { + name: 'addFromFs', + reason: 'Not designed to run in the browser' + }, + // .catPullStream { - name: 'should ls directory with long option', - reason: 'TODO unskip when go-ipfs supports --long https://github.com/ipfs/go-ipfs/pull/6528' + name: 'should export a chunk of a file', + reason: 'TODO not implemented in go-ipfs yet' }, { - name: 'should read from outside of mfs', + name: 'should export a chunk of a file in a Pull Stream', reason: 'TODO not implemented in go-ipfs yet' }, { - name: 'should ls from outside of mfs', + name: 'should export a chunk of a file in a Readable Stream', reason: 'TODO not implemented in go-ipfs yet' } ] }) - tests.key(defaultCommonFactory, { + tests.key(commonFactory, { skip: [ // key.export { @@ -162,21 +156,15 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.miscellaneous(defaultCommonFactory, { - skip: [ - // stop - { - name: 'should stop the node', - reason: 'FIXME go-ipfs returns an error https://github.com/ipfs/go-ipfs/issues/4078' - } - ] - }) + tests.miscellaneous(commonFactory) - tests.name(CommonFactory.createAsync({ - spawnOptions: { - args: ['--offline'] + tests.name(createFactory(merge(commonOptions, + { + ipfsOptions: { + offline: true + } } - }), { + )), { skip: [ // stop { @@ -186,12 +174,15 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.namePubsub(CommonFactory.createAsync({ - spawnOptions: { - args: ['--enable-namesys-pubsub'], - initOptions: { bits: 1024, profile: 'test' } + tests.namePubsub(createFactory(merge(commonOptions, + { + ipfsOptions: { + EXPERIMENTAL: { + ipnsPubsub: true + } + } } - }), { + )), { skip: [ // name.pubsub.cancel { @@ -206,11 +197,11 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.object(defaultCommonFactory) + tests.object(commonFactory) - tests.pin(defaultCommonFactory) + tests.pin(commonFactory) - tests.ping(defaultCommonFactory, { + tests.ping(commonFactory, { skip: [ { name: 'should fail when pinging an unknown peer over pull stream', @@ -227,10 +218,9 @@ describe('interface-ipfs-core tests', () => { ] }) - tests.pubsub(CommonFactory.createAsync({ - spawnOptions: { - args: ['--enable-pubsub-experiment'], - initOptions: { bits: 1024, profile: 'test' } + tests.pubsub(createFactory(commonOptions, { + go: { + args: ['--enable-pubsub-experiment'] } }), { skip: isWindows ? [ @@ -246,9 +236,9 @@ describe('interface-ipfs-core tests', () => { ] : null }) - tests.repo(defaultCommonFactory) + tests.repo(commonFactory) - tests.stats(defaultCommonFactory) + tests.stats(commonFactory) - tests.swarm(defaultCommonFactory) + tests.swarm(commonFactory) }) diff --git a/test/key.spec.js b/test/key.spec.js index 2e4e15714..180cb795b 100644 --- a/test/key.spec.js +++ b/test/key.spec.js @@ -1,32 +1,19 @@ /* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ 'use strict' const { expect } = require('interface-ipfs-core/src/utils/mocha') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.key', function () { this.timeout(50 * 1000) - let ipfsd let ipfs before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) describe('.gen', () => { it('create a new rsa key', async () => { diff --git a/test/log.spec.js b/test/log.spec.js index 7f8e2c608..1a885028d 100644 --- a/test/log.spec.js +++ b/test/log.spec.js @@ -3,30 +3,18 @@ 'use strict' const { expect } = require('interface-ipfs-core/src/utils/mocha') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.log', function () { this.timeout(100 * 1000) - let ipfsd let ipfs before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('.log.tail', async () => { const i = setInterval(async () => { diff --git a/test/ping.spec.js b/test/ping.spec.js index 10c131b75..01636ed60 100644 --- a/test/ping.spec.js +++ b/test/ping.spec.js @@ -5,7 +5,6 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha') const pull = require('pull-stream/pull') const collect = require('pull-stream/sinks/collect') -const ipfsClient = require('../src') const f = require('./utils/factory') // Determine if a ping response object is a pong, or something else, like a status message @@ -17,29 +16,14 @@ describe('.ping', function () { this.timeout(20 * 1000) let ipfs - let ipfsd let other - let otherd let otherId before(async function () { this.timeout(30 * 1000) // slow CI - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) - - otherd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - other = otherd.api + ipfs = (await f.spawn()).api + other = (await f.spawn()).api const ma = (await ipfs.id()).addresses[0] await other.swarm.connect(ma) @@ -47,15 +31,7 @@ describe('.ping', function () { otherId = (await other.id()).id }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - - if (otherd) { - await otherd.stop() - } - }) + after(() => f.clean()) it('.ping with default count', async () => { const res = await ipfs.ping(otherId) diff --git a/test/repo.spec.js b/test/repo.spec.js index 84b5a42c3..21482135d 100644 --- a/test/repo.spec.js +++ b/test/repo.spec.js @@ -2,30 +2,18 @@ 'use strict' const { expect } = require('interface-ipfs-core/src/utils/mocha') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('.repo', function () { this.timeout(50 * 1000) // slow CI let ipfs - let ipfsd before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('.repo.gc', async () => { const res = await ipfs.repo.gc() diff --git a/test/stats.spec.js b/test/stats.spec.js index 0b7084d0a..d60aaa330 100644 --- a/test/stats.spec.js +++ b/test/stats.spec.js @@ -2,30 +2,18 @@ 'use strict' const { expect } = require('interface-ipfs-core/src/utils/mocha') -const ipfsClient = require('../src') const f = require('./utils/factory') describe('stats', function () { this.timeout(50 * 1000) // slow CI let ipfs - let ipfsd before(async () => { - ipfsd = await f.spawn({ - initOptions: { - bits: 1024, - profile: 'test' - } - }) - ipfs = ipfsClient(ipfsd.apiAddr) + ipfs = (await f.spawn()).api }) - after(async () => { - if (ipfsd) { - await ipfsd.stop() - } - }) + after(() => f.clean()) it('.stats.bitswap', async () => { const res = await ipfs.stats.bitswap() diff --git a/test/utils/factory.js b/test/utils/factory.js index aab6296fe..d6ac161ea 100644 --- a/test/utils/factory.js +++ b/test/utils/factory.js @@ -1,5 +1,15 @@ 'use strict' +const { createFactory } = require('ipfsd-ctl') +const { findBin } = require('ipfsd-ctl/src/utils') -const IPFSFactory = require('ipfsd-ctl') +const factory = createFactory({ + test: 'true', + type: 'go', + ipfsBin: findBin('go'), + ipfsHttpModule: { + path: require.resolve('../../src'), + ref: require('../../src') + } +}) -module.exports = IPFSFactory.create() +module.exports = factory diff --git a/test/utils/interface-common-factory.js b/test/utils/interface-common-factory.js deleted file mode 100644 index bd4720b6e..000000000 --- a/test/utils/interface-common-factory.js +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const each = require('async/each') -const IPFSFactory = require('ipfsd-ctl') -const ipfsClient = require('../../src') -const merge = require('merge-options') - -const DEFAULT_FACTORY_OPTIONS = { - IpfsClient: ipfsClient -} - -function createFactory (options) { - options = options || {} - - options.factoryOptions = options.factoryOptions || { ...DEFAULT_FACTORY_OPTIONS } - options.spawnOptions = options.spawnOptions || { initOptions: { bits: 1024, profile: 'test' } } - - const ipfsFactory = IPFSFactory.create(options.factoryOptions) - - return function createCommon () { - const nodes = [] - let setup, teardown - - if (options.createSetup) { - setup = options.createSetup({ ipfsFactory, nodes }, options) - } else { - setup = (callback) => { - callback(null, { - spawnNode (cb) { - ipfsFactory.spawn(options.spawnOptions) - .then((ipfsd) => { - nodes.push(ipfsd) - setImmediate(() => cb(null, ipfsd.api)) - }) - .catch(err => { - setImmediate(() => cb(err)) - }) - } - }) - } - } - - if (options.createTeardown) { - teardown = options.createTeardown({ ipfsFactory, nodes }, options) - } else { - teardown = callback => each(nodes, (node, cb) => { - node - .stop() - .then(() => setImmediate(() => cb())) - .catch(err => setImmediate(() => cb(err))) - }, callback) - } - - return { setup, teardown } - } -} - -function createAsync (options = {}) { - return () => { - const nodes = [] - const setup = async (setupOptions = {}) => { - const ipfsFactory = IPFSFactory.create(merge( - options.factoryOptions ? {} : { ...DEFAULT_FACTORY_OPTIONS }, - setupOptions.factoryOptions, - options.factoryOptions - )) - const node = await ipfsFactory.spawn(merge( - setupOptions.spawnOptions, - options.spawnOptions || { initOptions: { profile: 'test' } } - )) - nodes.push(node) - - const id = await node.api.id() - node.api.peerId = id - - return node.api - } - - const teardown = () => { - return Promise.all(nodes.map(n => n.stop())) - } - return { - setup, - teardown - } - } -} -module.exports = { - createAsync, - create: createFactory -} From 8b9fc28253c46960a4dc1142a47d47c23985b080 Mon Sep 17 00:00:00 2001 From: Jim Pick Date: Wed, 18 Dec 2019 12:14:40 -0800 Subject: [PATCH 2/6] docs: change "IPN" to "IPNS" in README (#1204) I'm assuming that's what it was supposed to be. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c6a00070..9c6d3a3c7 100644 --- a/README.md +++ b/README.md @@ -413,7 +413,7 @@ The js-ipfs-http-client is a work in progress. As such, there's a few things you - **[Check out the existing issues](https://github.com/ipfs/js-ipfs-http-client/issues)**! - **Perform code reviews**. More eyes will help a) speed the project along b) ensure quality and c) reduce possible future bugs. - **Add tests**. There can never be enough tests. Note that interface tests exist inside [`interface-ipfs-core`](https://github.com/ipfs/interface-ipfs-core/tree/master/js/src). -- **Contribute to the [FAQ repository](https://github.com/ipfs/faq/issues)** with any questions you have about IPFS or any of the relevant technology. A good example would be asking, 'What is a merkledag tree?'. If you don't know a term, odds are, someone else doesn't either. Eventually, we should have a good understanding of where we need to improve communications and teaching together to make IPFS and IPN better. +- **Contribute to the [FAQ repository](https://github.com/ipfs/faq/issues)** with any questions you have about IPFS or any of the relevant technology. A good example would be asking, 'What is a merkledag tree?'. If you don't know a term, odds are, someone else doesn't either. Eventually, we should have a good understanding of where we need to improve communications and teaching together to make IPFS and IPNS better. **Want to hack on IPFS?** From 4cd7858de6a2812ee941eaeb4bd48f6ea6cc121c Mon Sep 17 00:00:00 2001 From: Paul Cowgill Date: Tue, 31 Dec 2019 06:02:21 -0600 Subject: [PATCH 3/6] docs: grammar fix (#1208) * Grammar fix * Same grammar fix in a different spot --- examples/bundle-browserify/README.md | 2 +- examples/bundle-webpack/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/bundle-browserify/README.md b/examples/bundle-browserify/README.md index a185527e0..def887200 100644 --- a/examples/bundle-browserify/README.md +++ b/examples/bundle-browserify/README.md @@ -11,7 +11,7 @@ As for any js-ipfs-http-client example, **you need a running IPFS daemon**, you **Note:** If you load your app from a different domain than the one the daemon is running (most probably), you will need to set up CORS, see https://github.com/ipfs/js-ipfs-http-client#cors to learn how to do that. -A quick (and dirty way to get it done) is: +A quick (and dirty) way to get it done is: ```bash > ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]" diff --git a/examples/bundle-webpack/README.md b/examples/bundle-webpack/README.md index 3b894b7f5..c85543240 100644 --- a/examples/bundle-webpack/README.md +++ b/examples/bundle-webpack/README.md @@ -11,7 +11,7 @@ As for any js-ipfs-http-client example, **you need a running IPFS daemon**, you **Note:** If you load your app from a different domain than the one the daemon is running (most probably), you will need to set up CORS, see https://github.com/ipfs/js-ipfs-http-client#cors to learn how to do that. -A quick (and dirty way to get it done) is: +A quick (and dirty) way to get it done is: ```bash > ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]" From da9d17a38ce09d299e7180d489a56c1e276b4fb9 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 9 Jan 2020 11:04:33 +0000 Subject: [PATCH 4/6] feat: support UnixFSv1.5 metadata (#1186) * feat: support UnixFSv1.5 metadata * fix: expose new mfs functions * refactor: send mtime and mode as headers instead of message parts * fix: include headers for directories * chore: update ipfs utils dep version * chore: update ipfs-utils dep * fix: stringify mode in browser * test: add tests for unixfs metadata * fix: fix up tests etc for optional mtime --- package.json | 4 +- src/add/form-data.browser.js | 24 +++- src/add/form-data.js | 22 +++- src/add/index.js | 14 +- src/files/chmod.js | 25 ++++ src/files/index.js | 2 + src/files/ls.js | 7 +- src/files/mkdir.js | 11 ++ src/files/stat.js | 5 +- src/files/touch.js | 29 +++++ src/files/write.js | 16 ++- src/lib/buffer-to-form-data.js | 20 ++- src/lib/mode-to-string.js | 13 ++ src/lib/mtime-to-object.js | 56 ++++++++ src/lib/object-to-camel-with-metadata.js | 24 ++++ src/ls.js | 18 ++- test/interface.spec.js | 156 +++++++++++++++++++++++ 17 files changed, 429 insertions(+), 17 deletions(-) create mode 100644 src/files/chmod.js create mode 100644 src/files/touch.js create mode 100644 src/lib/mode-to-string.js create mode 100644 src/lib/mtime-to-object.js create mode 100644 src/lib/object-to-camel-with-metadata.js diff --git a/package.json b/package.json index f0253c933..261b888c9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "explain-error": "^1.0.4", "form-data": "^3.0.0", "ipfs-block": "~0.8.1", - "ipfs-utils": "^0.4.0", + "ipfs-utils": "^0.4.2", "ipld-dag-cbor": "~0.15.0", "ipld-dag-pb": "^0.18.1", "ipld-raw": "^4.0.0", @@ -84,7 +84,7 @@ "cross-env": "^6.0.0", "detect-node": "^2.0.4", "go-ipfs-dep": "^0.4.22", - "interface-ipfs-core": "~0.125.0", + "interface-ipfs-core": "^0.126.0", "ipfsd-ctl": "^1.0.0", "ndjson": "^1.5.0", "nock": "^11.4.0", diff --git a/src/add/form-data.browser.js b/src/add/form-data.browser.js index 247396c42..484f67c6c 100644 --- a/src/add/form-data.browser.js +++ b/src/add/form-data.browser.js @@ -2,6 +2,7 @@ /* eslint-env browser */ const normaliseInput = require('ipfs-utils/src/files/normalise-input') +const mtimeToObject = require('../lib/mtime-to-object') exports.toFormData = async input => { const files = normaliseInput(input) @@ -9,6 +10,21 @@ exports.toFormData = async input => { let i = 0 for await (const file of files) { + const headers = {} + + if (file.mtime !== undefined && file.mtime !== null) { + const mtime = mtimeToObject(file.mtime) + + if (mtime) { + headers.mtime = mtime.secs + headers['mtime-nsecs'] = mtime.nsecs + } + } + + if (file.mode !== undefined && file.mode !== null) { + headers.mode = file.mode.toString(8).padStart(4, '0') + } + if (file.content) { // In the browser there's _currently_ no streaming upload, buffer up our // async iterator chunks and append a big Blob :( @@ -18,9 +34,13 @@ exports.toFormData = async input => { bufs.push(chunk) } - formData.append(`file-${i}`, new Blob(bufs, { type: 'application/octet-stream' }), encodeURIComponent(file.path)) + formData.append(`file-${i}`, new Blob(bufs, { type: 'application/octet-stream' }), encodeURIComponent(file.path), { + header: headers + }) } else { - formData.append(`dir-${i}`, new Blob([], { type: 'application/x-directory' }), encodeURIComponent(file.path)) + formData.append(`dir-${i}`, new Blob([], { type: 'application/x-directory' }), encodeURIComponent(file.path), { + header: headers + }) } i++ diff --git a/src/add/form-data.js b/src/add/form-data.js index 96d55f672..1ce5050da 100644 --- a/src/add/form-data.js +++ b/src/add/form-data.js @@ -5,6 +5,7 @@ const { Buffer } = require('buffer') const toStream = require('it-to-stream') const normaliseInput = require('ipfs-utils/src/files/normalise-input') const { isElectronRenderer } = require('ipfs-utils/src/env') +const mtimeToObject = require('../lib/mtime-to-object') exports.toFormData = async input => { const files = normaliseInput(input) @@ -12,6 +13,21 @@ exports.toFormData = async input => { let i = 0 for await (const file of files) { + const headers = {} + + if (file.mtime !== undefined && file.mtime !== null) { + const mtime = mtimeToObject(file.mtime) + + if (mtime) { + headers.mtime = mtime.secs + headers['mtime-nsecs'] = mtime.nsecs + } + } + + if (file.mode !== undefined && file.mode !== null) { + headers.mode = file.mode.toString(8).padStart(4, '0') + } + if (file.content) { // In Node.js, FormData can be passed a stream so no need to buffer formData.append( @@ -26,13 +42,15 @@ exports.toFormData = async input => { { filepath: encodeURIComponent(file.path), contentType: 'application/octet-stream', - knownLength: file.content.length // Send Content-Length header if known + knownLength: file.content.length, // Send Content-Length header if known + header: headers } ) } else { formData.append(`dir-${i}`, Buffer.alloc(0), { filepath: encodeURIComponent(file.path), - contentType: 'application/x-directory' + contentType: 'application/x-directory', + header: headers }) } diff --git a/src/add/index.js b/src/add/index.js index df6afbad3..546e5428d 100644 --- a/src/add/index.js +++ b/src/add/index.js @@ -52,6 +52,16 @@ module.exports = configure(({ ky }) => { } }) -function toCoreInterface ({ name, hash, size }) { - return { path: name, hash, size: parseInt(size) } +function toCoreInterface ({ name, hash, size, mode, mtime }) { + const output = { + path: name, + hash, + size: parseInt(size) + } + + if (mode !== undefined) { + output.mode = parseInt(mode, 8) + } + + return output } diff --git a/src/files/chmod.js b/src/files/chmod.js new file mode 100644 index 000000000..b4c0a11dd --- /dev/null +++ b/src/files/chmod.js @@ -0,0 +1,25 @@ +'use strict' + +const configure = require('../lib/configure') +const modeToString = require('../lib/mode-to-string') + +module.exports = configure(({ ky }) => { + return function chmod (path, mode, options) { + options = options || {} + + const searchParams = new URLSearchParams(options.searchParams) + searchParams.append('arg', path) + searchParams.append('mode', modeToString(mode)) + if (options.format) searchParams.set('format', options.format) + if (options.flush != null) searchParams.set('flush', options.flush) + if (options.hashAlg) searchParams.set('hash', options.hashAlg) + if (options.parents != null) searchParams.set('parents', options.parents) + + return ky.post('files/chmod', { + timeout: options.timeout, + signal: options.signal, + headers: options.headers, + searchParams + }).text() + } +}) diff --git a/src/files/index.js b/src/files/index.js index 25e79fcab..ee6b7d8d8 100644 --- a/src/files/index.js +++ b/src/files/index.js @@ -8,6 +8,7 @@ module.exports = config => { const read = require('./read')(config) return { + chmod: callbackify.variadic(require('./chmod')(config)), cp: callbackify.variadic(require('./cp')(config)), mkdir: callbackify.variadic(require('./mkdir')(config)), flush: callbackify.variadic(require('./flush')(config)), @@ -19,6 +20,7 @@ module.exports = config => { read: callbackify.variadic(concatify(read)), readReadableStream: streamify.readable(read), readPullStream: pullify.source(read), + touch: callbackify.variadic(require('./touch')(config)), write: callbackify.variadic(require('./write')(config)), mv: callbackify.variadic(require('./mv')(config)) } diff --git a/src/files/ls.js b/src/files/ls.js index 1baa3f656..51ee33912 100644 --- a/src/files/ls.js +++ b/src/files/ls.js @@ -4,7 +4,7 @@ const CID = require('cids') const ndjson = require('iterable-ndjson') const toIterable = require('../lib/stream-to-iterable') const configure = require('../lib/configure') -const toCamel = require('../lib/object-to-camel') +const toCamelWithMetadata = require('../lib/object-to-camel-with-metadata') module.exports = configure(({ ky }) => { return async function * ls (path, options) { @@ -32,11 +32,12 @@ module.exports = configure(({ ky }) => { // go-ipfs does not yet support the "stream" option if ('Entries' in result) { for (const entry of result.Entries || []) { - yield toCamel(entry) + yield toCamelWithMetadata(entry) } return } - yield toCamel(result) + + yield toCamelWithMetadata(result) } } }) diff --git a/src/files/mkdir.js b/src/files/mkdir.js index 0fc3c238d..3a50c7728 100644 --- a/src/files/mkdir.js +++ b/src/files/mkdir.js @@ -1,10 +1,13 @@ 'use strict' const configure = require('../lib/configure') +const modeToString = require('../lib/mode-to-string') +const mtimeToObject = require('../lib/mtime-to-object') module.exports = configure(({ ky }) => { return (path, options) => { options = options || {} + const mtime = mtimeToObject(options.mtime) const searchParams = new URLSearchParams(options.searchParams) searchParams.append('arg', path) @@ -13,6 +16,14 @@ module.exports = configure(({ ky }) => { if (options.flush != null) searchParams.set('flush', options.flush) if (options.hashAlg) searchParams.set('hash', options.hashAlg) if (options.parents != null) searchParams.set('parents', options.parents) + if (mtime) { + searchParams.set('mtime', mtime.secs) + + if (mtime.nsecs != null) { + searchParams.set('mtimeNsecs', mtime.nsecs) + } + } + if (options.mode != null) searchParams.set('mode', modeToString(options.mode)) return ky.post('files/mkdir', { timeout: options.timeout, diff --git a/src/files/stat.js b/src/files/stat.js index 98026283e..1b4af061b 100644 --- a/src/files/stat.js +++ b/src/files/stat.js @@ -1,7 +1,7 @@ 'use strict' const configure = require('../lib/configure') -const toCamel = require('../lib/object-to-camel') +const toCamelWithMetadata = require('../lib/object-to-camel-with-metadata') module.exports = configure(({ ky }) => { return async (path, options) => { @@ -27,6 +27,7 @@ module.exports = configure(({ ky }) => { }).json() res.WithLocality = res.WithLocality || false - return toCamel(res) + + return toCamelWithMetadata(res) } }) diff --git a/src/files/touch.js b/src/files/touch.js new file mode 100644 index 000000000..b38aca905 --- /dev/null +++ b/src/files/touch.js @@ -0,0 +1,29 @@ +'use strict' + +const configure = require('../lib/configure') +const mtimeToObject = require('../lib/mtime-to-object') + +module.exports = configure(({ ky }) => { + return function touch (path, options) { + options = options || {} + const mtime = mtimeToObject(options.mtime) + + const searchParams = new URLSearchParams(options.searchParams) + searchParams.append('arg', path) + if (mtime) { + searchParams.set('mtime', mtime.secs) + searchParams.set('mtimeNsecs', mtime.nsecs) + } + if (options.format) searchParams.set('format', options.format) + if (options.flush != null) searchParams.set('flush', options.flush) + if (options.hashAlg) searchParams.set('hash', options.hashAlg) + if (options.parents != null) searchParams.set('parents', options.parents) + + return ky.post('files/touch', { + timeout: options.timeout, + signal: options.signal, + headers: options.headers, + searchParams + }).text() + } +}) diff --git a/src/files/write.js b/src/files/write.js index 77a772ea6..e31f0a8a2 100644 --- a/src/files/write.js +++ b/src/files/write.js @@ -2,10 +2,13 @@ const configure = require('../lib/configure') const toFormData = require('../lib/buffer-to-form-data') +const modeToString = require('../lib/mode-to-string') +const mtimeToObject = require('../lib/mtime-to-object') module.exports = configure(({ ky }) => { return async (path, input, options) => { options = options || {} + const mtime = mtimeToObject(options.mtime) const searchParams = new URLSearchParams(options.searchParams) searchParams.set('arg', path) @@ -18,13 +21,24 @@ module.exports = configure(({ ky }) => { if (options.parents != null) searchParams.set('parents', options.parents) if (options.rawLeaves != null) searchParams.set('raw-leaves', options.rawLeaves) if (options.truncate != null) searchParams.set('truncate', options.truncate) + if (mtime) { + searchParams.set('mtime', mtime.secs) + + if (mtime.nsecs != null) { + searchParams.set('mtimeNsecs', mtime.nsecs) + } + } const res = await ky.post('files/write', { timeout: options.timeout, signal: options.signal, headers: options.headers, searchParams, - body: toFormData(input) // TODO: support inputs other than buffer as per spec + body: toFormData(input, { + mode: options.mode != null ? modeToString(options.mode) : undefined, + mtime: mtime ? mtime.secs : undefined, + mtimeNsecs: mtime ? mtime.nsecs : undefined + }) // TODO: support inputs other than buffer as per spec }) return res.text() diff --git a/src/lib/buffer-to-form-data.js b/src/lib/buffer-to-form-data.js index 41f03383e..1a4830361 100644 --- a/src/lib/buffer-to-form-data.js +++ b/src/lib/buffer-to-form-data.js @@ -3,9 +3,25 @@ const FormData = require('form-data') const { isElectronRenderer } = require('ipfs-utils/src/env') -module.exports = buf => { +module.exports = (buf, { mode, mtime, mtimeNsecs } = {}) => { + const headers = {} + + if (mode != null) { + headers.mode = mode + } + + if (mtime != null) { + headers.mtime = mtime + + if (mtimeNsecs != null) { + headers['mtime-nsecs'] = mtimeNsecs + } + } + const formData = new FormData() - formData.append('file', buf) + formData.append('file', buf, { + header: headers + }) return formData } diff --git a/src/lib/mode-to-string.js b/src/lib/mode-to-string.js new file mode 100644 index 000000000..ee2742b9a --- /dev/null +++ b/src/lib/mode-to-string.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = (mode) => { + if (mode === undefined || mode === null) { + return undefined + } + + if (typeof mode === 'string' || mode instanceof String) { + return mode + } + + return mode.toString(8).padStart(4, '0') +} diff --git a/src/lib/mtime-to-object.js b/src/lib/mtime-to-object.js new file mode 100644 index 000000000..be89148f6 --- /dev/null +++ b/src/lib/mtime-to-object.js @@ -0,0 +1,56 @@ +'use strict' + +module.exports = function parseMtime (mtime) { + if (mtime == null) { + return undefined + } + + // Javascript Date + if (mtime instanceof Date) { + const ms = mtime.getTime() + const secs = Math.floor(ms / 1000) + + return { + secs: secs, + nsecs: (ms - (secs * 1000)) * 1000 + } + } + + // { secs, nsecs } + if (Object.prototype.hasOwnProperty.call(mtime, 'secs')) { + return { + secs: mtime.secs, + nsecs: mtime.nsecs + } + } + + // UnixFS TimeSpec + if (Object.prototype.hasOwnProperty.call(mtime, 'Seconds')) { + return { + secs: mtime.Seconds, + nsecs: mtime.FractionalNanoseconds + } + } + + // process.hrtime() + if (Array.isArray(mtime)) { + return { + secs: mtime[0], + nsecs: mtime[1] + } + } + /* + TODO: https://github.com/ipfs/aegir/issues/487 + + // process.hrtime.bigint() + if (typeof mtime === 'bigint') { + const secs = mtime / BigInt(1e9) + const nsecs = mtime - (secs * BigInt(1e9)) + + return { + secs: parseInt(secs), + nsecs: parseInt(nsecs) + } + } + */ +} diff --git a/src/lib/object-to-camel-with-metadata.js b/src/lib/object-to-camel-with-metadata.js new file mode 100644 index 000000000..55f16d0bb --- /dev/null +++ b/src/lib/object-to-camel-with-metadata.js @@ -0,0 +1,24 @@ +'use strict' + +const toCamel = require('./object-to-camel') + +function toCamelWithMetadata (entry) { + const file = toCamel(entry) + + if (Object.prototype.hasOwnProperty.call(file, 'mode')) { + file.mode = parseInt(file.mode, 8) + } + + if (Object.prototype.hasOwnProperty.call(file, 'mtime')) { + file.mtime = { + secs: file.mtime, + nsecs: file.mtimeNsecs || 0 + } + + delete file.mtimeNsecs + } + + return file +} + +module.exports = toCamelWithMetadata diff --git a/src/ls.js b/src/ls.js index a9cd476f9..43e92a54a 100644 --- a/src/ls.js +++ b/src/ls.js @@ -48,7 +48,7 @@ module.exports = configure(({ ky }) => { } for (const link of result) { - yield { + const entry = { name: link.Name, path: path + '/' + link.Name, size: link.Size, @@ -56,6 +56,22 @@ module.exports = configure(({ ky }) => { type: typeOf(link), depth: link.Depth || 1 } + + if (link.Mode) { + entry.mode = parseInt(link.Mode, 8) + } + + if (link.Mtime !== undefined && link.Mtime !== null) { + entry.mtime = { + secs: link.Mtime + } + + if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) { + entry.mtime.nsecs = link.MtimeNsecs + } + } + + yield entry } } }) diff --git a/test/interface.spec.js b/test/interface.spec.js index 5435518cf..e09cac5a6 100644 --- a/test/interface.spec.js +++ b/test/interface.spec.js @@ -114,6 +114,134 @@ describe('interface-ipfs-core tests', () => { { name: 'should ls from outside of mfs', reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should change file mode', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should change directory mode', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should change file mode as string', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should change file mode to 0', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should update file mtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should update directory mtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mode', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mode', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should respect metadata when copying files', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should respect metadata when copying directories', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should respect metadata when copying from outside of mfs', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'ls directory with long option should include metadata', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should have default mtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should set mtime as Date', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should set mtime as { nsecs, secs }', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should set mtime as timespec', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should set mtime as hrtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and have default mode', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mode as string', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mode as number', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mtime as Date', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mtime as { nsecs, secs }', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mtime as timespec', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should make directory and specify mtime as hrtime', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mode as a string', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mode as a number', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mtime as Date', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mtime as { nsecs, secs }', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mtime as timespec', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should write file and specify mtime as hrtime', + reason: 'TODO not implemented in go-ipfs yet' } ] }) @@ -125,6 +253,30 @@ describe('interface-ipfs-core tests', () => { name: 'addFromFs', reason: 'Not designed to run in the browser' }, + { + name: 'should add with mode as string', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should add with mode as number', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should add with mtime as Date', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should add with mtime as { nsecs, secs }', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should add with mtime as timespec', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should add with mtime as hrtime', + reason: 'TODO not implemented in go-ipfs yet' + }, // .catPullStream { name: 'should export a chunk of a file', @@ -137,6 +289,10 @@ describe('interface-ipfs-core tests', () => { { name: 'should export a chunk of a file in a Readable Stream', reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should ls with metadata', + reason: 'TODO not implemented in go-ipfs yet' } ] }) From 0c679de76c96770e50a5813f246514e142149756 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 9 Jan 2020 11:42:35 +0000 Subject: [PATCH 5/6] chore: update contributors --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 261b888c9..d0b0d7783 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipfs-http-client", - "version": "40.1.0", + "version": "40.2.0", "description": "A client library for the IPFS HTTP API", "keywords": [ "ipfs" @@ -137,6 +137,7 @@ "Jeff Downie ", "Jeromy ", "Jeromy ", + "Jim Pick ", "Joe Turgeon ", "Jonathan ", "Juan Batiz-Benet ", @@ -158,6 +159,7 @@ "Níckolas Goline ", "Oli Evans ", "Orie Steele ", + "Paul Cowgill ", "Pedro Santos ", "Pedro Santos ", "Pedro Teixeira ", From 7042ea190c032b940763975b6553853258919dd6 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 9 Jan 2020 11:42:35 +0000 Subject: [PATCH 6/6] chore: release version v40.2.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af8c75cd7..c41311373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +# [40.2.0](https://github.com/ipfs/js-ipfs-http-client/compare/v40.1.0...v40.2.0) (2020-01-09) + + +### Features + +* support UnixFSv1.5 metadata ([#1186](https://github.com/ipfs/js-ipfs-http-client/issues/1186)) ([da9d17a](https://github.com/ipfs/js-ipfs-http-client/commit/da9d17a)) + + + # [40.1.0](https://github.com/ipfs/js-ipfs-http-client/compare/v40.0.1...v40.1.0) (2019-12-10)