diff --git a/src/cli/commands/file.js b/src/cli/commands/file.js new file mode 100644 index 0000000000..5bc4dcf93f --- /dev/null +++ b/src/cli/commands/file.js @@ -0,0 +1,15 @@ +'use strict' + +module.exports = { + command: 'file', + + description: 'Interact with IPFS objects representing Unix filesystems.', + + builder (yargs) { + return yargs + .commandDir('file') + }, + + handler (argv) { + } +} diff --git a/src/cli/commands/file/ls.js b/src/cli/commands/file/ls.js new file mode 100644 index 0000000000..c952087b98 --- /dev/null +++ b/src/cli/commands/file/ls.js @@ -0,0 +1,27 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'ls ', + + describe: 'List directory contents for Unix filesystem objects.', + + builder: {}, + + handler (argv) { + let path = argv.key + argv.ipfs.ls(path, (err, links) => { + if (err) { + throw err + } + + // Single file? Then print its hash + if (links.length === 0) { + links = [{hash: path}] + } + + links.forEach((file) => print(file.hash)) + }) + } +} diff --git a/src/http/api/resources/file.js b/src/http/api/resources/file.js new file mode 100644 index 0000000000..6c0b6b4c4c --- /dev/null +++ b/src/http/api/resources/file.js @@ -0,0 +1,111 @@ +'use strict' + +const mh = require('multihashes') +const debug = require('debug') +const log = debug('jsipfs:http-api:file') +log.error = debug('jsipfs:http-api:file:error') +const unixfsEngine = require('ipfs-unixfs-engine') +const exporter = unixfsEngine.exporter +const pull = require('pull-stream') +const toB58String = require('multihashes').toB58String + +exports = module.exports + +const fileTypeMap = { + file: 'File', + dir: 'Directory' +} + +function toFileObject (file) { + const fo = { + Hash: toB58String(file.hash), + Size: file.size, + Type: fileTypeMap[file.type] || file.type + } + if (fo.Hash !== file.name) { + fo.Name = file.name + } + return fo +} + +// common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args` +exports.parseKey = (request, reply) => { + if (!request.query.arg) { + return reply({ + Message: "Argument 'key' is required", + Code: 0 + }).code(400).takeover() + } + + let key = request.query.arg + if (key.indexOf('/ipfs/') === 0) { + key = key.substring(6) + } + + let hash = key + const slashIndex = hash.indexOf('/') + if (slashIndex > 0) { + hash = hash.substring(0, slashIndex) + } + + try { + mh.fromB58String(hash) + } catch (err) { + log.error(err) + return reply({ + Message: 'invalid ipfs ref path', + Code: 0 + }).code(500).takeover() + } + + const subpaths = key.split('/') + subpaths.shift() + reply({ + path: request.query.arg, + subpaths: subpaths, + key: key, + hash: hash + }) +} + +exports.ls = { + // 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 path = request.pre.args.path + const ipfs = request.server.app.ipfs + const subpaths = request.pre.args.subpaths + const rootDepth = subpaths.length + + pull( + exporter(path, ipfs._ipldResolver, { maxDepth: rootDepth + 1 }), + pull.collect((err, files) => { + if (err) { + return reply({ + Message: 'Failed to list dir: ' + err.message, + Code: 0 + }).code(500) + } + + let res = { + Arguments: {}, + Objects: {} + } + const links = [] + files.forEach((file) => { + if (file.depth === rootDepth) { + let id = toB58String(file.hash) + res.Arguments[path] = id + res.Objects[id] = toFileObject(file) + res.Objects[id].Links = file.type === 'file' ? null : links + } else { + links.push(toFileObject(file)) + } + }) + return reply(res) + }) + ) + } +} diff --git a/src/http/api/resources/index.js b/src/http/api/resources/index.js index 671a349952..37bb316cab 100644 --- a/src/http/api/resources/index.js +++ b/src/http/api/resources/index.js @@ -9,5 +9,6 @@ exports.config = require('./config') exports.block = require('./block') exports.swarm = require('./swarm') exports.bitswap = require('./bitswap') +exports.file = require('./file') exports.files = require('./files') exports.pubsub = require('./pubsub') diff --git a/src/http/api/routes/file.js b/src/http/api/routes/file.js new file mode 100644 index 0000000000..1abf7456b2 --- /dev/null +++ b/src/http/api/routes/file.js @@ -0,0 +1,19 @@ +'use strict' + +const resources = require('./../resources') + +module.exports = (server) => { + const api = server.select('API') + + api.route({ + // TODO fix method + method: '*', + path: '/api/v0/file/ls', + config: { + pre: [ + { method: resources.file.ls.parseArgs, assign: 'args' } + ], + handler: resources.file.ls.handler + } + }) +} diff --git a/src/http/api/routes/index.js b/src/http/api/routes/index.js index c317db6de7..3fa1d75e68 100644 --- a/src/http/api/routes/index.js +++ b/src/http/api/routes/index.js @@ -10,6 +10,7 @@ module.exports = (server) => { require('./config')(server) require('./swarm')(server) require('./bitswap')(server) + require('./file')(server) require('./files')(server) require('./pubsub')(server) require('./debug')(server) diff --git a/test/cli/commands.js b/test/cli/commands.js index e6d9059d36..dc681ecebd 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 = 57 +const commandCount = 58 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/file.js b/test/cli/file.js new file mode 100644 index 0000000000..59ab531f6d --- /dev/null +++ b/test/cli/file.js @@ -0,0 +1,33 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const runOnAndOff = require('../utils/on-and-off') +const file = 'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV' +const dir = 'QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2' + +describe('file ls', () => runOnAndOff((thing) => { + let ipfs + + before(function () { + this.timeout(50 * 1000) + ipfs = thing.ipfs + return ipfs('files add -r test/fixtures/test-data/recursive-get-dir') + }) + + it('prints a filename', () => { + return ipfs(`file ls ${file}`) + .then((out) => expect(out).to.eql(`${file}\n`)) + }) + + it('prints the filenames in a directory', () => { + return ipfs(`file ls ${dir}`) + .then((out) => expect(out).to.eql( + 'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9\n' + + 'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN\n' + + 'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz\n' + + 'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU\n' + + 'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV\n' + )) + }) +}))