Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 6db3fb8

Browse files
richardschneiderdaviddias
authored andcommitted
feat: implement "ipfs file ls" (#1078)
* feat: cli for "file ls" * fix: typo * chore: remove crlfs * chore: linting * chore: linting * feat: http-api for 'file ls' * fix: lint errors * fix: file type codes * refactor: talk directly to ipfs-unixfs-engine ipfs.ls does not provide enough information * fix: lint errors
1 parent 9de6f4c commit 6db3fb8

File tree

8 files changed

+208
-1
lines changed

8 files changed

+208
-1
lines changed

src/cli/commands/file.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict'
2+
3+
module.exports = {
4+
command: 'file',
5+
6+
description: 'Interact with IPFS objects representing Unix filesystems.',
7+
8+
builder (yargs) {
9+
return yargs
10+
.commandDir('file')
11+
},
12+
13+
handler (argv) {
14+
}
15+
}

src/cli/commands/file/ls.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict'
2+
3+
const print = require('../../utils').print
4+
5+
module.exports = {
6+
command: 'ls <key>',
7+
8+
describe: 'List directory contents for Unix filesystem objects.',
9+
10+
builder: {},
11+
12+
handler (argv) {
13+
let path = argv.key
14+
argv.ipfs.ls(path, (err, links) => {
15+
if (err) {
16+
throw err
17+
}
18+
19+
// Single file? Then print its hash
20+
if (links.length === 0) {
21+
links = [{hash: path}]
22+
}
23+
24+
links.forEach((file) => print(file.hash))
25+
})
26+
}
27+
}

src/http/api/resources/file.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict'
2+
3+
const mh = require('multihashes')
4+
const debug = require('debug')
5+
const log = debug('jsipfs:http-api:file')
6+
log.error = debug('jsipfs:http-api:file:error')
7+
const unixfsEngine = require('ipfs-unixfs-engine')
8+
const exporter = unixfsEngine.exporter
9+
const pull = require('pull-stream')
10+
const toB58String = require('multihashes').toB58String
11+
12+
exports = module.exports
13+
14+
const fileTypeMap = {
15+
file: 'File',
16+
dir: 'Directory'
17+
}
18+
19+
function toFileObject (file) {
20+
const fo = {
21+
Hash: toB58String(file.hash),
22+
Size: file.size,
23+
Type: fileTypeMap[file.type] || file.type
24+
}
25+
if (fo.Hash !== file.name) {
26+
fo.Name = file.name
27+
}
28+
return fo
29+
}
30+
31+
// common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args`
32+
exports.parseKey = (request, reply) => {
33+
if (!request.query.arg) {
34+
return reply({
35+
Message: "Argument 'key' is required",
36+
Code: 0
37+
}).code(400).takeover()
38+
}
39+
40+
let key = request.query.arg
41+
if (key.indexOf('/ipfs/') === 0) {
42+
key = key.substring(6)
43+
}
44+
45+
let hash = key
46+
const slashIndex = hash.indexOf('/')
47+
if (slashIndex > 0) {
48+
hash = hash.substring(0, slashIndex)
49+
}
50+
51+
try {
52+
mh.fromB58String(hash)
53+
} catch (err) {
54+
log.error(err)
55+
return reply({
56+
Message: 'invalid ipfs ref path',
57+
Code: 0
58+
}).code(500).takeover()
59+
}
60+
61+
const subpaths = key.split('/')
62+
subpaths.shift()
63+
reply({
64+
path: request.query.arg,
65+
subpaths: subpaths,
66+
key: key,
67+
hash: hash
68+
})
69+
}
70+
71+
exports.ls = {
72+
// uses common parseKey method that returns a `key`
73+
parseArgs: exports.parseKey,
74+
75+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
76+
handler: (request, reply) => {
77+
const path = request.pre.args.path
78+
const ipfs = request.server.app.ipfs
79+
const subpaths = request.pre.args.subpaths
80+
const rootDepth = subpaths.length
81+
82+
pull(
83+
exporter(path, ipfs._ipldResolver, { maxDepth: rootDepth + 1 }),
84+
pull.collect((err, files) => {
85+
if (err) {
86+
return reply({
87+
Message: 'Failed to list dir: ' + err.message,
88+
Code: 0
89+
}).code(500)
90+
}
91+
92+
let res = {
93+
Arguments: {},
94+
Objects: {}
95+
}
96+
const links = []
97+
files.forEach((file) => {
98+
if (file.depth === rootDepth) {
99+
let id = toB58String(file.hash)
100+
res.Arguments[path] = id
101+
res.Objects[id] = toFileObject(file)
102+
res.Objects[id].Links = file.type === 'file' ? null : links
103+
} else {
104+
links.push(toFileObject(file))
105+
}
106+
})
107+
return reply(res)
108+
})
109+
)
110+
}
111+
}

src/http/api/resources/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ exports.config = require('./config')
99
exports.block = require('./block')
1010
exports.swarm = require('./swarm')
1111
exports.bitswap = require('./bitswap')
12+
exports.file = require('./file')
1213
exports.files = require('./files')
1314
exports.pubsub = require('./pubsub')

src/http/api/routes/file.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
const resources = require('./../resources')
4+
5+
module.exports = (server) => {
6+
const api = server.select('API')
7+
8+
api.route({
9+
// TODO fix method
10+
method: '*',
11+
path: '/api/v0/file/ls',
12+
config: {
13+
pre: [
14+
{ method: resources.file.ls.parseArgs, assign: 'args' }
15+
],
16+
handler: resources.file.ls.handler
17+
}
18+
})
19+
}

src/http/api/routes/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = (server) => {
1010
require('./config')(server)
1111
require('./swarm')(server)
1212
require('./bitswap')(server)
13+
require('./file')(server)
1314
require('./files')(server)
1415
require('./pubsub')(server)
1516
require('./debug')(server)

test/cli/commands.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
const expect = require('chai').expect
55
const runOnAndOff = require('../utils/on-and-off')
66

7-
const commandCount = 57
7+
const commandCount = 58
88

99
describe('commands', () => runOnAndOff((thing) => {
1010
let ipfs

test/cli/file.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const expect = require('chai').expect
5+
const runOnAndOff = require('../utils/on-and-off')
6+
const file = 'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV'
7+
const dir = 'QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2'
8+
9+
describe('file ls', () => runOnAndOff((thing) => {
10+
let ipfs
11+
12+
before(function () {
13+
this.timeout(50 * 1000)
14+
ipfs = thing.ipfs
15+
return ipfs('files add -r test/fixtures/test-data/recursive-get-dir')
16+
})
17+
18+
it('prints a filename', () => {
19+
return ipfs(`file ls ${file}`)
20+
.then((out) => expect(out).to.eql(`${file}\n`))
21+
})
22+
23+
it('prints the filenames in a directory', () => {
24+
return ipfs(`file ls ${dir}`)
25+
.then((out) => expect(out).to.eql(
26+
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9\n' +
27+
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN\n' +
28+
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz\n' +
29+
'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU\n' +
30+
'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV\n'
31+
))
32+
})
33+
}))

0 commit comments

Comments
 (0)