From a3f73e6ec70323bfcbed7fbc94c697fdec66a1f0 Mon Sep 17 00:00:00 2001 From: Francisco Baio Dias Date: Sun, 27 Mar 2016 00:41:36 +0000 Subject: [PATCH] Add object patch endpoints and cli commands --- package.json | 2 +- src/cli/commands/object/patch/add-link.js | 66 +++ src/cli/commands/object/patch/append-data.js | 59 +++ src/cli/commands/object/patch/rm-link.js | 50 +++ src/cli/commands/object/patch/set-data.js | 59 +++ src/core/index.js | 17 +- src/http-api/resources/object.js | 225 ++++++++++ src/http-api/routes/object.js | 56 ++- tests/test-cli/test-commands.js | 2 +- tests/test-cli/test-object.js | 92 ++++ tests/test-core/test-object.js | 39 +- tests/test-http-api/test-object.js | 434 +++++++++++++++++++ 12 files changed, 1074 insertions(+), 27 deletions(-) create mode 100644 src/cli/commands/object/patch/add-link.js create mode 100644 src/cli/commands/object/patch/append-data.js create mode 100644 src/cli/commands/object/patch/rm-link.js create mode 100644 src/cli/commands/object/patch/set-data.js diff --git a/package.json b/package.json index 16ab38a9af..5351b0869d 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "bs58": "^3.0.0", "debug": "^2.2.0", "hapi": "^12.0.0", - "ipfs-api": "^2.13.1", + "ipfs-api": "github:ipfs/js-ipfs-api#1fd9749", "ipfs-blocks": "^0.1.0", "ipfs-data-importing": "^0.3.3", "ipfs-merkle-dag": "^0.2.1", diff --git a/src/cli/commands/object/patch/add-link.js b/src/cli/commands/object/patch/add-link.js new file mode 100644 index 0000000000..a9289c1b2d --- /dev/null +++ b/src/cli/commands/object/patch/add-link.js @@ -0,0 +1,66 @@ +'use strict' + +const Command = require('ronin').Command +const utils = require('../../../utils') +const bs58 = require('bs58') +const debug = require('debug') +const log = debug('cli:object') +const mDAG = require('ipfs-merkle-dag') +const DAGLink = mDAG.DAGLink +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: 'Add a link to a given object', + + options: {}, + + run: (root, name, ref) => { + if (!root) { + throw new Error("Argument 'root' is required") + } + if (!name) { + throw new Error("Argument 'name' is required") + } + if (!ref) { + throw new Error("Argument 'ref' is required") + } + + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + if (utils.isDaemonOn()) { + return ipfs.object.patch.addLink(root, name, ref, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + console.log(obj.Hash) + }) + } + + // when running locally we first need to get the ref object, + // so we can create the link with the correct size + const refMh = new Buffer(bs58.decode(ref)) + ipfs.object.get(refMh, (err, linkedObj) => { + if (err) { + log.error(err) + throw err + } + + const rootMh = new Buffer(bs58.decode(root)) + const link = new DAGLink(name, linkedObj.size(), linkedObj.multihash()) + ipfs.object.patch.addLink(rootMh, link, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + console.log(bs58.encode(obj.multihash()).toString()) + }) + }) + }) + } +}) diff --git a/src/cli/commands/object/patch/append-data.js b/src/cli/commands/object/patch/append-data.js new file mode 100644 index 0000000000..23b0e14f83 --- /dev/null +++ b/src/cli/commands/object/patch/append-data.js @@ -0,0 +1,59 @@ +'use strict' + +const Command = require('ronin').Command +const utils = require('../../../utils') +const bs58 = require('bs58') +const bl = require('bl') +const fs = require('fs') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +function appendData (keyStr, data) { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + const key = utils.isDaemonOn() ? keyStr : new Buffer(bs58.decode(keyStr)) + + ipfs.object.patch.appendData(key, data, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + if (typeof obj.multihash === 'function') { + console.log(bs58.encode(obj.multihash()).toString()) + return + } + + console.log(obj.Hash) + }) + }) +} + +module.exports = Command.extend({ + desc: 'Append data to the data segment of a dag node', + + options: {}, + + run: (key, filePath) => { + if (!key) { + throw new Error("Argument 'root' is required") + } + + if (filePath) { + return appendData(key, fs.readFileSync(filePath)) + } + + process.stdin.pipe(bl((err, input) => { + if (err) { + log.error(err) + throw err + } + + appendData(key, input) + })) + } +}) diff --git a/src/cli/commands/object/patch/rm-link.js b/src/cli/commands/object/patch/rm-link.js new file mode 100644 index 0000000000..818282b0b2 --- /dev/null +++ b/src/cli/commands/object/patch/rm-link.js @@ -0,0 +1,50 @@ +'use strict' + +const Command = require('ronin').Command +const utils = require('../../../utils') +const bs58 = require('bs58') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: 'Remove a link from an object', + + options: {}, + + run: (root, link) => { + if (!root) { + throw new Error("Argument 'root' is required") + } + if (!link) { + throw new Error("Argument 'link' is required") + } + + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + if (utils.isDaemonOn()) { + return ipfs.object.patch.rmLink(root, link, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + console.log(obj.Hash) + }) + } + + const mh = new Buffer(bs58.decode(root)) + ipfs.object.patch.rmLink(mh, link, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + console.log(bs58.encode(obj.multihash()).toString()) + }) + }) + } +}) diff --git a/src/cli/commands/object/patch/set-data.js b/src/cli/commands/object/patch/set-data.js new file mode 100644 index 0000000000..51ef181788 --- /dev/null +++ b/src/cli/commands/object/patch/set-data.js @@ -0,0 +1,59 @@ +'use strict' + +const Command = require('ronin').Command +const utils = require('../../../utils') +const bs58 = require('bs58') +const bl = require('bl') +const fs = require('fs') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +function parseAndAddNode (keyStr, data) { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + const key = utils.isDaemonOn() ? keyStr : new Buffer(bs58.decode(keyStr)) + + ipfs.object.patch.setData(key, data, (err, obj) => { + if (err) { + log.error(err) + throw err + } + + if (typeof obj.multihash === 'function') { + console.log(bs58.encode(obj.multihash()).toString()) + return + } + + console.log(obj.Hash) + }) + }) +} + +module.exports = Command.extend({ + desc: 'Set data field of an ipfs object', + + options: {}, + + run: (key, filePath) => { + if (!key) { + throw new Error("Argument 'root' is required") + } + + if (filePath) { + return parseAndAddNode(key, fs.readFileSync(filePath)) + } + + process.stdin.pipe(bl((err, input) => { + if (err) { + log.error(err) + throw err + } + + parseAndAddNode(key, input) + })) + } +}) diff --git a/src/core/index.js b/src/core/index.js index 682ca3ae05..0f26ce8e53 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -207,7 +207,7 @@ function IPFS (repo) { if (err) { return callback(err) } - callback(null, obj.multihash()) + callback(null, obj) }) }) }, @@ -219,24 +219,25 @@ function IPFS (repo) { if (err) { return callback(err) } - callback(null, obj.multihash()) + callback(null, obj) }) }) }, - rmLink: (multihash, multihashLink, callback) => { + rmLink: (multihash, linkRef, callback) => { this.object.get(multihash, (err, obj) => { if (err) { return callback(err) } obj.links = obj.links.filter((link) => { - if (link.hash.equals(multihashLink)) { - return false + // filter by name when linkRef is a string, or by hash otherwise + if (typeof linkRef === 'string') { + return link.name !== linkRef } - return true + return !link.hash.equals(linkRef) }) dagS.add(obj, (err) => { if (err) { return callback(err) } - callback(null, obj.multihash()) + callback(null, obj) }) }) }, @@ -248,7 +249,7 @@ function IPFS (repo) { if (err) { return callback(err) } - callback(null, obj.multihash()) + callback(null, obj) }) }) } diff --git a/src/http-api/resources/object.js b/src/http-api/resources/object.js index 4f8318a61f..40215b6099 100644 --- a/src/http-api/resources/object.js +++ b/src/http-api/resources/object.js @@ -4,6 +4,7 @@ const ipfs = require('./../index.js').ipfs const bs58 = require('bs58') const multipart = require('ipfs-multipart') const mDAG = require('ipfs-merkle-dag') +const DAGLink = mDAG.DAGLink const debug = require('debug') const log = debug('http-api:object') log.error = debug('http-api:object:error') @@ -243,3 +244,227 @@ exports.links = { }) } } + +// common pre request handler that parses the args and returns `data` & `key` which are assigned to `request.pre.args` +exports.parseKeyAndData = (request, reply) => { + if (!request.query.arg) { + return reply("Argument 'root' is required").code(400).takeover() + } + + if (!request.payload) { + return reply("File argument 'data' is required").code(400).takeover() + } + + const parser = multipart.reqParser(request.payload) + let file + + parser.on('file', (fileName, fileStream) => { + fileStream.on('data', (data) => { + file = data + }) + }) + + parser.on('end', () => { + if (!file) { + return reply("File argument 'data' is required").code(400).takeover() + } + + try { + return reply({ + data: file, + key: new Buffer(bs58.decode(request.query.arg)) // TODO: support ipfs paths: https://github.com/ipfs/http-api-spec/pull/68/files#diff-2625016b50d68d922257f74801cac29cR3880 + }) + } catch (err) { + return reply({ + Message: 'invalid ipfs ref path', + Code: 0 + }).code(500).takeover() + } + }) +} + +exports.patchAppendData = { + // uses common parseKeyAndData method that returns a `data` & `key` + parseArgs: exports.parseKeyAndData, + + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + const key = request.pre.args.key + const data = request.pre.args.data + + ipfs.object.patch.appendData(key, data, (err, obj) => { + if (err) { + log.error(err) + + return reply({ + Message: 'Failed to apend data to object: ' + err, + Code: 0 + }).code(500) + } + + return reply({ + Hash: bs58.encode(obj.multihash()).toString(), + Links: obj.links.map((link) => ({ + Name: link.name, + Hash: bs58.encode(link.hash).toString(), + Size: link.size + })) + }) + }) + } +} + +exports.patchSetData = { + // uses common parseKeyAndData method that returns a `data` & `key` + parseArgs: exports.parseKeyAndData, + + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + const key = request.pre.args.key + const data = request.pre.args.data + + ipfs.object.patch.setData(key, data, (err, obj) => { + if (err) { + log.error(err) + + return reply({ + Message: 'Failed to apend data to object: ' + err, + Code: 0 + }).code(500) + } + + return reply({ + Hash: bs58.encode(obj.multihash()).toString(), + Links: obj.links.map((link) => ({ + Name: link.name, + Hash: bs58.encode(link.hash).toString(), + Size: link.size + })) + }) + }) + } +} + +exports.patchAddLink = { + // pre request handler that parses the args and returns `root`, `name` & `ref` which is assigned to `request.pre.args` + parseArgs: (request, reply) => { + if (!(request.query.arg instanceof Array) || request.query.arg.length !== 3) { + return reply("Arguments 'root', 'name' & 'ref' are required").code(400).takeover() + } + + if (!request.query.arg[1]) { + return reply({ + Message: 'cannot create link with no name!', + Code: 0 + }).code(500).takeover() + } + + try { + return reply({ + root: new Buffer(bs58.decode(request.query.arg[0])), + name: request.query.arg[1], + ref: new Buffer(bs58.decode(request.query.arg[2])) + }) + } catch (err) { + log.error(err) + return reply({ + Message: 'invalid ipfs ref path', + Code: 0 + }).code(500).takeover() + } + }, + + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + const root = request.pre.args.root + const name = request.pre.args.name + const ref = request.pre.args.ref + + ipfs.object.get(ref, (err, linkedObj) => { + if (err) { + log.error(err) + return reply({ + Message: 'Failed to get linked object: ' + err, + Code: 0 + }).code(500) + } + + const link = new DAGLink(name, linkedObj.size(), linkedObj.multihash()) + + ipfs.object.patch.addLink(root, link, (err, obj) => { + if (err) { + log.error(err) + + return reply({ + Message: 'Failed to add link to object: ' + err, + Code: 0 + }).code(500) + } + + return reply({ + Hash: bs58.encode(obj.multihash()).toString(), + Links: obj.links.map((link) => ({ + Name: link.name, + Hash: bs58.encode(link.hash).toString(), + Size: link.size + })) + }) + }) + }) + } +} + +exports.patchRmLink = { + // pre request handler that parses the args and returns `root` & `link` which is assigned to `request.pre.args` + parseArgs: (request, reply) => { + if (!(request.query.arg instanceof Array) || request.query.arg.length !== 2) { + return reply("Arguments 'root' & 'link' are required").code(400).takeover() + } + + if (!request.query.arg[1]) { + return reply({ + Message: 'cannot create link with no name!', + Code: 0 + }).code(500).takeover() + } + + try { + return reply({ + root: new Buffer(bs58.decode(request.query.arg[0])), + link: request.query.arg[1] + }) + } catch (err) { + log.error(err) + return reply({ + Message: 'invalid ipfs ref path', + Code: 0 + }).code(500).takeover() + } + }, + + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + const root = request.pre.args.root + const link = request.pre.args.link + + ipfs.object.patch.rmLink(root, link, (err, obj) => { + if (err) { + log.error(err) + + return reply({ + Message: 'Failed to add link to object: ' + err, + Code: 0 + }).code(500) + } + + return reply({ + Hash: bs58.encode(obj.multihash()).toString(), + Links: obj.links.map((link) => ({ + Name: link.name, + Hash: bs58.encode(link.hash).toString(), + Size: link.size + })) + }) + }) + } +} diff --git a/src/http-api/routes/object.js b/src/http-api/routes/object.js index 502ead9f30..463e9c866a 100644 --- a/src/http-api/routes/object.js +++ b/src/http-api/routes/object.js @@ -1,9 +1,5 @@ const resources = require('./../resources') -// TODO: add `object patch` endpoints, once spec is finished, check -// https://github.com/ipfs/js-ipfs/issues/58 & https://github.com/ipfs/http-api-spec/pull/32 -// for more info - module.exports = (server) => { const api = server.select('API') @@ -76,4 +72,56 @@ module.exports = (server) => { handler: resources.object.links.handler } }) + + api.route({ + method: '*', + path: '/api/v0/object/patch/append-data', + config: { + payload: { + parse: false, + output: 'stream' + }, + pre: [ + { method: resources.object.patchAppendData.parseArgs, assign: 'args' } + ], + handler: resources.object.patchAppendData.handler + } + }) + + api.route({ + method: '*', + path: '/api/v0/object/patch/set-data', + config: { + payload: { + parse: false, + output: 'stream' + }, + pre: [ + { method: resources.object.patchSetData.parseArgs, assign: 'args' } + ], + handler: resources.object.patchSetData.handler + } + }) + + api.route({ + method: '*', + path: '/api/v0/object/patch/add-link', + config: { + pre: [ + { method: resources.object.patchAddLink.parseArgs, assign: 'args' } + ], + handler: resources.object.patchAddLink.handler + } + }) + + api.route({ + method: '*', + path: '/api/v0/object/patch/rm-link', + config: { + pre: [ + { method: resources.object.patchRmLink.parseArgs, assign: 'args' } + ], + handler: resources.object.patchRmLink.handler + } + }) } diff --git a/tests/test-cli/test-commands.js b/tests/test-cli/test-commands.js index 467c55cd16..f8924cc7c9 100644 --- a/tests/test-cli/test-commands.js +++ b/tests/test-cli/test-commands.js @@ -9,7 +9,7 @@ describe('commands', () => { .run((err, stdout, exitcode) => { expect(err).to.not.exist expect(exitcode).to.equal(0) - expect(stdout.length).to.equal(32) + expect(stdout.length).to.equal(36) done() }) }) diff --git a/tests/test-cli/test-object.js b/tests/test-cli/test-object.js index d701db9d42..b10c5befa8 100644 --- a/tests/test-cli/test-object.js +++ b/tests/test-cli/test-object.js @@ -78,6 +78,52 @@ describe('object', () => { done() }) }) + + describe('patch', () => { + it('append-data', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'append-data', 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n', process.cwd() + '/tests/badconfig']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6') + done() + }) + }) + + it('set-data', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'set-data', 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', process.cwd() + '/tests/badconfig']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6') + done() + }) + }) + + it('add-link', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'add-link', 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n', 'foo', 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK') + done() + }) + }) + + it('rm-link', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'rm-link', 'QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK', 'foo']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + done() + }) + }) + }) }) describe('api running', () => { @@ -167,5 +213,51 @@ describe('object', () => { done() }) }) + + describe('patch', () => { + it('append-data', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'append-data', 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n', process.cwd() + '/tests/badconfig']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6') + done() + }) + }) + + it('set-data', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'set-data', 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', process.cwd() + '/tests/badconfig']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6') + done() + }) + }) + + it('add-link', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'add-link', 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n', 'foo', 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK') + done() + }) + }) + + it('rm-link', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'object', 'patch', 'rm-link', 'QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK', 'foo']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout[0]) + .to.equal('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + done() + }) + }) + }) }) }) diff --git a/tests/test-core/test-object.js b/tests/test-core/test-object.js index c66c1a5e37..1be253b20b 100644 --- a/tests/test-core/test-object.js +++ b/tests/test-core/test-object.js @@ -32,9 +32,9 @@ describe('object', () => { it('patch append-data', (done) => { const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) - ipfs.object.patch.appendData(mh, new Buffer('data data'), (err, multihash) => { + ipfs.object.patch.appendData(mh, new Buffer('data data'), (err, obj) => { expect(err).to.not.exist - expect(mh).to.not.deep.equal(multihash) + expect(mh).to.not.deep.equal(obj.multihash()) done() }) }) @@ -42,30 +42,43 @@ describe('object', () => { it('patch add-link', (done) => { const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) - ipfs.object.patch.addLink(mh, new DAGLink('prev', 0, mh), (err, multihash) => { + ipfs.object.patch.addLink(mh, new DAGLink('prev', 0, mh), (err, obj) => { expect(err).to.not.exist - expect(mh).to.not.deep.equal(multihash) + expect(mh).to.not.deep.equal(obj.multihash()) done() }) }) - it('patch rm-link', (done) => { - const rmmh = new Buffer(bs58.decode('QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V')) - const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) + describe('patch rm-link', () => { + it('remove link by name', (done) => { + const name = 'about' + const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) - ipfs.object.patch.rmLink(mh, rmmh, (err, multihash) => { - expect(err).to.not.exist - expect(mh).to.not.deep.equal(multihash) - done() + ipfs.object.patch.rmLink(mh, name, (err, obj) => { + expect(err).to.not.exist + expect(mh).to.not.deep.equal(obj.multihash()) + done() + }) + }) + + it('remove link by multihash', (done) => { + const rmmh = new Buffer(bs58.decode('QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V')) + const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) + + ipfs.object.patch.rmLink(mh, rmmh, (err, obj) => { + expect(err).to.not.exist + expect(mh).to.not.deep.equal(obj.multihash()) + done() + }) }) }) it('patch set-data', (done) => { const mh = new Buffer(bs58.decode('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG')) - ipfs.object.patch.setData(mh, new Buffer('data data data'), (err, multihash) => { + ipfs.object.patch.setData(mh, new Buffer('data data data'), (err, obj) => { expect(err).to.not.exist - expect(mh).to.not.deep.equal(multihash) + expect(mh).to.not.deep.equal(obj.multihash()) done() }) }) diff --git a/tests/test-http-api/test-object.js b/tests/test-http-api/test-object.js index 64952b1608..9e21b64c20 100644 --- a/tests/test-http-api/test-object.js +++ b/tests/test-http-api/test-object.js @@ -266,6 +266,275 @@ describe('object', () => { }) }) }) + + describe('/object/patch/append-data', () => { + it('returns 400 for request without key', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/append-data' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 400 if no data is provided', (done) => { + const form = new FormData() + const headers = form.getHeaders() + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/append-data?arg=QmVLUHkjGg3duGb5w3dnwK5w2P9QWuJmtVNuDPLc9ZDjzk', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(400) + done() + }) + }) + }) + + it('returns 500 for request with invalid key', (done) => { + const form = new FormData() + const filePath = 'tests/badconfig' + form.append('file', fs.createReadStream(filePath)) + const headers = form.getHeaders() + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/append-data?arg=invalid', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(500) + done() + }) + }) + }) + + it('updates value', (done) => { + const form = new FormData() + const filePath = 'tests/badconfig' + form.append('data', fs.createReadStream(filePath)) + const headers = form.getHeaders() + const expectedResult = { + Hash: 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', + Links: [] + } + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/append-data?arg=QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result).to.deep.equal(expectedResult) + done() + }) + }) + }) + }) + + describe('/object/patch/set-data', () => { + it('returns 400 for request without key', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/set-data' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 400 if no data is provided', (done) => { + const form = new FormData() + const headers = form.getHeaders() + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/set-data?arg=QmVLUHkjGg3duGb5w3dnwK5w2P9QWuJmtVNuDPLc9ZDjzk', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(400) + done() + }) + }) + }) + + it('returns 500 for request with invalid key', (done) => { + const form = new FormData() + const filePath = 'tests/badconfig' + form.append('file', fs.createReadStream(filePath)) + const headers = form.getHeaders() + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/set-data?arg=invalid', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(500) + done() + }) + }) + }) + + it('updates value', (done) => { + const form = new FormData() + const filePath = 'tests/badconfig' + form.append('data', fs.createReadStream(filePath)) + const headers = form.getHeaders() + const expectedResult = { + Hash: 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', + Links: [] + } + + streamToPromise(form).then((payload) => { + api.inject({ + method: 'POST', + url: '/api/v0/object/patch/set-data?arg=QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', + headers: headers, + payload: payload + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result).to.deep.equal(expectedResult) + done() + }) + }) + }) + }) + + describe('/object/patch/add-link', () => { + it('returns 400 for request without arguments', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/add-link' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 400 for request with only one invalid argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/add-link?arg=invalid' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 500 for request with invalid first argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/add-link?arg=&arg=foo&arg=QmTz3oc4gdpRMKP2sdGUPZTAGRngqjsi99BPoztyP53JMM' + }, (res) => { + expect(res.statusCode).to.equal(500) + expect(res.result.Code).to.equal(0) + expect(res.result.Message).to.be.a('string') + done() + }) + }) + + it('returns 500 for request with empty second argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/add-link?arg=QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn&arg=&arg=QmTz3oc4gdpRMKP2sdGUPZTAGRngqjsi99BPoztyP53JMM' + }, (res) => { + expect(res.statusCode).to.equal(500) + expect(res.result.Code).to.equal(0) + expect(res.result.Message).to.be.a('string') + done() + }) + }) + + it('returns value', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/add-link?arg=QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n&arg=foo&arg=QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result.Hash).to.equal('QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK') + expect(res.result.Links[0]).to.deep.equal({ + Name: 'foo', + Hash: 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn', + Size: 4 + }) + done() + }) + }) + }) + + describe('/object/patch/rm-link', () => { + it('returns 400 for request without arguments', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/rm-link' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 400 for request with only one invalid argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/rm-link?arg=invalid' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('returns 500 for request with invalid first argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/rm-link?arg=invalid&arg=foo' + }, (res) => { + expect(res.statusCode).to.equal(500) + expect(res.result.Code).to.equal(0) + expect(res.result.Message).to.be.a('string') + done() + }) + }) + + it('returns 500 for request with invalid second argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/rm-link?arg=QmZKetgwm4o3LhNaoLSHv32wBhTwj9FBwAdSchDMKyFQEx&arg=' + }, (res) => { + expect(res.statusCode).to.equal(500) + expect(res.result.Code).to.equal(0) + expect(res.result.Message).to.be.a('string') + done() + }) + }) + + it('returns value', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/object/patch/rm-link?arg=QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK&arg=foo' + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result.Hash).to.equal('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + done() + }) + }) + }) }) describe('using js-ipfs-api', () => { @@ -425,5 +694,170 @@ describe('object', () => { }) }) }) + + describe('ipfs.object.patch.appendData', () => { + it('returns error for request without key & data', (done) => { + ctl.object.patch.appendData(null, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request without key', (done) => { + const key = 'QmVLUHkjGg3duGb5w3dnwK5w2P9QWuJmtVNuDPLc9ZDjzk' + + ctl.object.patch.appendData(key, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request without data', (done) => { + const filePath = 'tests/badnode.json' + + ctl.object.patch.appendData(null, filePath, (err) => { + expect(err).to.exist + done() + }) + }) + + it('updates value', (done) => { + const key = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' + const filePath = 'tests/badnode.json' + const expectedResult = { + Hash: 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', + Links: [] + } + + ctl.object.patch.appendData(key, filePath, (err, res) => { + expect(err).not.to.exist + expect(res).to.deep.equal(expectedResult) + done() + }) + }) + }) + + describe('ipfs.object.patch.setData', () => { + it('returns error for request without key & data', (done) => { + ctl.object.patch.setData(null, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request without key', (done) => { + const key = 'QmVLUHkjGg3duGb5w3dnwK5w2P9QWuJmtVNuDPLc9ZDjzk' + + ctl.object.patch.setData(key, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request without data', (done) => { + const filePath = 'tests/badnode.json' + + ctl.object.patch.setData(null, filePath, (err) => { + expect(err).to.exist + done() + }) + }) + + it('updates value', (done) => { + const key = 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6' + const filePath = 'tests/badnode.json' + const expectedResult = { + Hash: 'QmfY37rjbPCZRnhvvJuQ46htW3VCAWziVB991P79h6WSv6', + Links: [] + } + + ctl.object.patch.setData(key, filePath, (err, res) => { + expect(err).not.to.exist + expect(res).to.deep.equal(expectedResult) + done() + }) + }) + }) + + describe('ipfs.object.patch.addLink', () => { + it('returns error for request without arguments', (done) => { + ctl.object.patch.addLink(null, null, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request only one invalid argument', (done) => { + ctl.object.patch.addLink('invalid', null, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request without name', (done) => { + const root = 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' + const name = '' + const ref = 'QmTz3oc4gdpRMKP2sdGUPZTAGRngqjsi99BPoztyP53JMM' + + ctl.object.patch.addLink(root, name, ref, (err) => { + expect(err).to.exist + done() + }) + }) + + it('updates value', (done) => { + const root = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' + const name = 'foo' + const ref = 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' + + ctl.object.patch.addLink(root, name, ref, (err, res) => { + expect(err).not.to.exist + expect(res.Hash).to.equal('QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK') + expect(res.Links[0]).to.deep.equal({ + Name: 'foo', + Hash: 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn', + Size: 4 + }) + done() + }) + }) + }) + + describe('ipfs.object.patch.rmLink', () => { + it('returns error for request without arguments', (done) => { + ctl.object.patch.rmLink(null, null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request only one invalid argument', (done) => { + ctl.object.patch.rmLink('invalid', null, (err) => { + expect(err).to.exist + done() + }) + }) + + it('returns error for request with invalid first argument', (done) => { + const root = '' + const link = 'foo' + + ctl.object.patch.rmLink(root, link, (err) => { + expect(err).to.exist + done() + }) + }) + + it('updates value', (done) => { + const root = 'QmdVHE8fUD6FLNLugtNxqDFyhaCgdob372hs6BYEe75VAK' + const link = 'foo' + + ctl.object.patch.rmLink(root, link, (err, res) => { + expect(err).not.to.exist + expect(res.Hash).to.equal('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + done() + }) + }) + }) }) })