From 35687cbfb17903c59fb8e29b5b3cbf374e999289 Mon Sep 17 00:00:00 2001 From: Pedro Teixeira Date: Tue, 14 Nov 2017 09:35:41 +0000 Subject: [PATCH] feat: ipfs.ls (#1073) --- package.json | 12 +++---- src/cli/commands/ls.js | 58 +++++++++++++++++++++++++++++++++ src/cli/utils.js | 8 +++++ src/core/components/files.js | 16 ++++++++- src/core/index.js | 3 ++ src/http/api/resources/files.js | 44 +++++++++++++++++++++++++ src/http/api/routes/files.js | 12 +++++++ test/cli/commands.js | 2 +- test/cli/files.js | 43 ++++++++++++++++++++++++ 9 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 src/cli/commands/ls.js diff --git a/package.json b/package.json index 0865299afd..ff2b7571a4 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "form-data": "^2.3.1", "gulp": "^3.9.1", "hat": "0.0.3", - "interface-ipfs-core": "~0.33.2", - "ipfsd-ctl": "~0.24.0", + "interface-ipfs-core": "~0.34.3", + "ipfsd-ctl": "~0.24.1", "left-pad": "^1.1.3", "lodash": "^4.17.4", "mocha": "^4.0.1", @@ -105,18 +105,18 @@ "hapi": "^16.6.2", "hapi-set-header": "^1.0.2", "hoek": "^5.0.2", - "ipfs-api": "^15.0.1", + "ipfs-api": "^15.1.0", "ipfs-bitswap": "~0.17.4", "ipfs-block": "~0.6.1", "ipfs-block-service": "~0.13.0", "ipfs-multipart": "~0.1.0", "ipfs-repo": "~0.18.3", "ipfs-unixfs": "~0.1.14", - "ipfs-unixfs-engine": "~0.23.1", + "ipfs-unixfs-engine": "~0.24.1", "ipld-resolver": "~0.14.1", "is-ipfs": "^0.3.2", "is-stream": "^1.1.0", - "joi": "^13.0.1", + "joi": "^13.0.2", "libp2p": "~0.13.1", "libp2p-circuit": "~0.1.4", "libp2p-floodsub": "~0.11.1", @@ -157,7 +157,7 @@ "readable-stream": "2.3.3", "safe-buffer": "^5.1.1", "stream-to-pull-stream": "^1.7.2", - "tar-stream": "^1.5.4", + "tar-stream": "^1.5.5", "temp": "~0.8.3", "through2": "^2.0.3", "update-notifier": "^2.3.0", diff --git a/src/cli/commands/ls.js b/src/cli/commands/ls.js new file mode 100644 index 0000000000..61a19e760c --- /dev/null +++ b/src/cli/commands/ls.js @@ -0,0 +1,58 @@ +'use strict' + +const utils = require('../utils') +const Unixfs = require('ipfs-unixfs') +const pull = require('pull-stream') + +module.exports = { + command: 'ls ', + + describe: 'List files for the given directory', + + builder: { + v: { + alias: 'headers', + desc: 'Print table headers (Hash, Size, Name).', + type: 'boolean', + default: false + }, + 'resolve-type': { + desc: 'Resolve linked objects to find out their types. (not implemented yet)', + type: 'boolean', + default: false // should be true when implemented + } + }, + + handler (argv) { + let path = argv.key + if (path.startsWith('/ipfs/')) { + path = path.replace('/ipfs/', '') + } + + argv.ipfs.ls(path, (err, links) => { + if (err) { + throw err + } + + if (argv.headers) { + links = [{hash: 'Hash', size: 'Size', name: 'Name'}].concat(links) + } + + links = links.filter((link) => link.path !== path) + links.forEach((link) => { + if (link.type === 'dir') { + // directory: add trailing "/" + link.name = (link.name || '') + '/' + } + }) + const multihashWidth = Math.max.apply(null, links.map((file) => file.hash.length)) + const sizeWidth = Math.max.apply(null, links.map((file) => String(file.size).length)) + + links.forEach((file) => { + utils.print(utils.rightpad(file.hash, multihashWidth + 1) + + utils.rightpad(file.size || '', sizeWidth + 1) + + file.name) + }) + }) + } +} diff --git a/src/cli/utils.js b/src/cli/utils.js index 4565fc316e..d44bf4b4fd 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -100,3 +100,11 @@ exports.createProgressBar = (totalBytes) => { total: totalBytes }) } + +exports.rightpad = (val, n) => { + let result = String(val) + for (let i = result.length; i < n; ++i) { + result += ' ' + } + return result +} diff --git a/src/core/components/files.js b/src/core/components/files.js index e2f6443350..0f7fa9509a 100644 --- a/src/core/components/files.js +++ b/src/core/components/files.js @@ -13,6 +13,7 @@ const waterfall = require('async/waterfall') const isStream = require('is-stream') const Duplex = require('stream').Duplex const CID = require('cids') +const toB58String = require('multihashes').toB58String module.exports = function files (self) { const createAddPullStream = (options) => { @@ -118,7 +119,20 @@ module.exports = function files (self) { getPull: promisify((ipfsPath, callback) => { callback(null, exporter(ipfsPath, self._ipldResolver)) - }) + }), + + immutableLs: promisify((ipfsPath, callback) => { + pull( + self.files.immutableLsPullStream(ipfsPath), + pull.collect(callback)) + }), + + immutableLsPullStream: (ipfsPath) => { + return pull( + exporter(ipfsPath, self._ipldResolver, { maxDepth: 1 }), + pull.filter((node) => node.depth === 1), + pull.map((node) => Object.assign({}, node, { hash: toB58String(node.hash) }))) + } } } diff --git a/src/core/index.js b/src/core/index.js index 1b85e38291..899f827ec6 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -112,6 +112,9 @@ class IPFS extends EventEmitter { this.state = require('./state')(this) + // ipfs.ls + this.ls = this.files.immutableLs + boot(this) } } diff --git a/src/http/api/resources/files.js b/src/http/api/resources/files.js index 1afbe2c6ef..ebb1353f35 100644 --- a/src/http/api/resources/files.js +++ b/src/http/api/resources/files.js @@ -274,3 +274,47 @@ exports.add = { ) } } + +exports.immutableLs = { + // uses common parseKey method that returns a `key` + parseArgs: exports.parseKey, + + // 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 ipfs = request.server.app.ipfs + + ipfs.ls(key, (err, files) => { + if (err) { + reply({ + Message: 'Failed to list dir: ' + err.message, + Code: 0 + }).code(500) + } + + reply({ + Objects: [{ + Hash: key, + Links: files.map((file) => ({ + Name: file.name, + Hash: file.hash, + Size: file.size, + Type: toTypeCode(file.type) + })) + }] + }) + }) + } +} + +function toTypeCode (type) { + switch (type) { + case 'dir': + return 1 + case 'file': + return 2 + default: + return 0 + } +} + diff --git a/src/http/api/routes/files.js b/src/http/api/routes/files.js index e7ce4f456d..2400f6012b 100644 --- a/src/http/api/routes/files.js +++ b/src/http/api/routes/files.js @@ -42,4 +42,16 @@ module.exports = (server) => { validate: resources.files.add.validate } }) + + api.route({ + // TODO fix method + method: '*', + path: '/api/v0/ls', + config: { + pre: [ + { method: resources.files.immutableLs.parseArgs, assign: 'args' } + ], + handler: resources.files.immutableLs.handler + } + }) } diff --git a/test/cli/commands.js b/test/cli/commands.js index 94ffb4aad4..e6d9059d36 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,7 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 56 +const commandCount = 57 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/files.js b/test/cli/files.js index c07762e1f3..c61750df82 100644 --- a/test/cli/files.js +++ b/test/cli/files.js @@ -254,6 +254,49 @@ describe('files', () => runOnAndOff((thing) => { }) }) + it('ls', () => { + return ipfs('ls QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2') + .then((out) => { + expect(out).to.eql( + 'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' + + 'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' + + 'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' + + 'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' + + 'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n') + }) + }) + + it('ls -v', () => { + return ipfs('ls /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2 -v') + .then((out) => { + expect(out).to.eql( + 'Hash Size Name\n' + + 'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' + + 'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' + + 'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' + + 'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' + + 'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n') + }) + }) + + it('ls --help', () => { + return ipfs('ls --help') + .then((out) => { + expect(out.split('\n').slice(1)).to.eql(['', + 'List files for the given directory', + '', + 'Options:', + ' -v, --version Show version number [boolean]', + ' --silent Show no output. [boolean]', + ' --help Show help [boolean]', + ' -v, --headers Print table headers (Hash, Size, Name).', + ' [boolean] [default: false]', + ' --resolve-type Resolve linked objects to find out their types. (not', + ' implemented yet) [boolean] [default: false]', + '', '']) + }) + }) + it('get', () => { return ipfs('files get QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB') .then((out) => {