Skip to content

Commit 02dd3f2

Browse files
committed
feat: jsipfs ls -r (Recursive list directory) (ipfs#1222)
1 parent 40cf00f commit 02dd3f2

File tree

9 files changed

+150
-96
lines changed

9 files changed

+150
-96
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@
111111
"ipfs-api": "^18.0.0",
112112
"ipfs-bitswap": "~0.19.0",
113113
"human-to-milliseconds": "^1.0.0",
114+
"ipfs-api": "^18.1.1",
115+
"ipfs-bitswap": "~0.19.0",
114116
"ipfs-block": "~0.6.1",
115117
"ipfs-block-service": "~0.13.0",
116118
"ipfs-multipart": "~0.1.0",

src/cli/commands/file/ls.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ module.exports = {
1111

1212
handler (argv) {
1313
let path = argv.key
14+
// `ipfs file ls` is deprecated. See https://ipfs.io/docs/commands/#ipfs-file-ls
15+
print(`This functionality is deprecated, and will be removed in future versions. If possible, please use 'ipfs ls' instead.`)
1416
argv.ipfs.ls(path, (err, links) => {
1517
if (err) {
1618
throw err

src/cli/commands/files.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict'
22

3+
const print = require('../utils').print
4+
const lsCmd = require('./ls')
5+
36
module.exports = {
47
command: 'files <command>',
58

@@ -8,9 +11,10 @@ module.exports = {
811
builder (yargs) {
912
return yargs
1013
.commandDir('files')
14+
.command(lsCmd)
1115
},
1216

1317
handler (argv) {
14-
console.log('Type `jsipfs bitswap --help` for more instructions')
18+
print('Type `jsipfs files --help` for more instructions')
1519
}
1620
}

src/cli/commands/ls.js

+15-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ module.exports = {
1414
type: 'boolean',
1515
default: false
1616
},
17+
r: {
18+
alias: 'recursive',
19+
desc: 'List subdirectories recursively',
20+
type: 'boolean',
21+
default: false
22+
},
1723
'resolve-type': {
1824
desc: 'Resolve linked objects to find out their types. (not implemented yet)',
1925
type: 'boolean',
@@ -27,7 +33,7 @@ module.exports = {
2733
path = path.replace('/ipfs/', '')
2834
}
2935

30-
argv.ipfs.ls(path, (err, links) => {
36+
argv.ipfs.ls(path, { recursive: argv.recursive }, (err, links) => {
3137
if (err) {
3238
throw err
3339
}
@@ -36,20 +42,17 @@ module.exports = {
3642
links = [{hash: 'Hash', size: 'Size', name: 'Name'}].concat(links)
3743
}
3844

39-
links = links.filter((link) => link.path !== path)
40-
links.forEach((link) => {
41-
if (link.type === 'dir') {
42-
// directory: add trailing "/"
43-
link.name = (link.name || '') + '/'
44-
}
45-
})
4645
const multihashWidth = Math.max.apply(null, links.map((file) => file.hash.length))
4746
const sizeWidth = Math.max.apply(null, links.map((file) => String(file.size).length))
4847

49-
links.forEach((file) => {
50-
utils.print(utils.rightpad(file.hash, multihashWidth + 1) +
51-
utils.rightpad(file.size || '', sizeWidth + 1) +
52-
file.name)
48+
links.forEach(link => {
49+
const fileName = link.type === 'dir' ? `${link.name || ''}/` : link.name
50+
const padding = link.depth - path.split('/').length
51+
utils.print(
52+
utils.rightpad(link.hash, multihashWidth + 1) +
53+
utils.rightpad(link.size || '', sizeWidth + 1) +
54+
' '.repeat(padding) + fileName
55+
)
5356
})
5457
})
5558
}

src/core/components/files.js

+23-11
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,20 @@ module.exports = function files (self) {
176176
return d
177177
}
178178

179-
function _lsPullStreamImmutable (ipfsPath) {
179+
function _lsPullStreamImmutable (ipfsPath, options) {
180180
const path = normalizePath(ipfsPath)
181-
const depth = path.split('/').length
181+
const recursive = options && options.recursive
182+
const pathDepth = path.split('/').length
183+
const maxDepth = recursive ? global.Infinity : pathDepth
184+
182185
return pull(
183-
exporter(ipfsPath, self._ipldResolver, { maxDepth: depth }),
184-
pull.filter((node) => node.depth === depth),
185-
pull.map((node) => {
186-
node = Object.assign({}, node, { hash: toB58String(node.hash) })
186+
exporter(ipfsPath, self._ipldResolver, { maxDepth: maxDepth }),
187+
pull.filter(node =>
188+
recursive ? node.depth >= pathDepth : node.depth === pathDepth
189+
),
190+
pull.map(node => {
191+
const cid = new CID(node.hash)
192+
node = Object.assign({}, node, { hash: cid.toBaseEncodedString() })
187193
delete node.content
188194
return node
189195
})
@@ -293,20 +299,26 @@ module.exports = function files (self) {
293299
return exporter(ipfsPath, self._ipldResolver)
294300
},
295301

296-
lsImmutable: promisify((ipfsPath, callback) => {
302+
lsImmutable: promisify((ipfsPath, options, callback) => {
303+
if (typeof options === 'function') {
304+
callback = options
305+
options = {}
306+
}
307+
297308
pull(
298-
_lsPullStreamImmutable(ipfsPath),
309+
_lsPullStreamImmutable(ipfsPath, options),
299310
pull.collect((err, values) => {
300311
if (err) {
301-
return callback(err)
312+
callback(err)
313+
return
302314
}
303315
callback(null, values)
304316
})
305317
)
306318
}),
307319

308-
lsReadableStreamImmutable: (ipfsPath) => {
309-
return toStream.source(_lsPullStreamImmutable(ipfsPath))
320+
lsReadableStreamImmutable: (ipfsPath, options) => {
321+
return toStream.source(_lsPullStreamImmutable(ipfsPath, options))
310322
},
311323

312324
lsPullStreamImmutable: _lsPullStreamImmutable

src/http/api/resources/files.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,14 @@ exports.immutableLs = {
272272
handler: (request, reply) => {
273273
const key = request.pre.args.key
274274
const ipfs = request.server.app.ipfs
275+
const recursive = request.query && request.query.recursive === 'true'
275276

276-
ipfs.ls(key, (err, files) => {
277+
ipfs.ls(key, { recursive: recursive }, (err, files) => {
277278
if (err) {
278-
reply({
279+
return reply({
279280
Message: 'Failed to list dir: ' + err.message,
280281
Code: 0
281-
}).code(500)
282+
}).code(500).takeover()
282283
}
283284

284285
reply({
@@ -288,7 +289,8 @@ exports.immutableLs = {
288289
Name: file.name,
289290
Hash: file.hash,
290291
Size: file.size,
291-
Type: toTypeCode(file.type)
292+
Type: toTypeCode(file.type),
293+
Depth: file.depth
292294
}))
293295
}]
294296
})

test/cli/file.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ describe('file ls', () => runOnAndOff((thing) => {
1717

1818
it('prints a filename', () => {
1919
return ipfs(`file ls ${file}`)
20-
.then((out) => expect(out).to.eql(`${file}\n`))
20+
.then((out) => expect(out).to.eql(
21+
`This functionality is deprecated, and will be removed in future versions. If possible, please use 'ipfs ls' instead.\n` +
22+
`${file}\n`
23+
))
2124
})
2225

2326
it('prints the filenames in a directory', () => {
2427
return ipfs(`file ls ${dir}`)
2528
.then((out) => expect(out).to.eql(
29+
`This functionality is deprecated, and will be removed in future versions. If possible, please use 'ipfs ls' instead.\n` +
2630
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9\n' +
2731
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN\n' +
2832
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz\n' +

test/cli/files.js

-67
Original file line numberDiff line numberDiff line change
@@ -296,73 +296,6 @@ describe('files', () => runOnAndOff((thing) => {
296296
})
297297
})
298298

299-
it('ls', function () {
300-
this.timeout(20 * 1000)
301-
302-
return ipfs('ls QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2')
303-
.then((out) => {
304-
expect(out).to.eql(
305-
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' +
306-
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' +
307-
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' +
308-
'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' +
309-
'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n')
310-
})
311-
})
312-
313-
it('ls -v', function () {
314-
this.timeout(20 * 1000)
315-
316-
return ipfs('ls /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2 -v')
317-
.then((out) => {
318-
expect(out).to.eql(
319-
'Hash Size Name\n' +
320-
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' +
321-
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' +
322-
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' +
323-
'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' +
324-
'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n')
325-
})
326-
})
327-
328-
it('ls <subdir>', function () {
329-
this.timeout(20 * 1000)
330-
331-
return ipfs('ls /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2/init-docs')
332-
.then((out) => {
333-
expect(out).to.eql(
334-
'QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V 1688 about\n' +
335-
'QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y 200 contact\n' +
336-
'QmegvLXxpVKiZ4b57Xs1syfBVRd8CbucVHAp7KpLQdGieC 65 docs/\n' +
337-
'QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7 322 help\n' +
338-
'QmdncfsVm2h5Kqq9hPmU7oAVX2zTSVP3L869tgTbPYnsha 1728 quick-start\n' +
339-
'QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB 1102 readme\n' +
340-
'QmTumTjvcYCAvRRwQ8sDRxh8ezmrcr88YFU7iYNroGGTBZ 1027 security-notes\n' +
341-
'QmciSU8hfpAXKjvK5YLUSwApomGSWN5gFbP4EpDAEzu2Te 863 tour/\n')
342-
})
343-
})
344-
345-
it('ls --help', function () {
346-
this.timeout(20 * 1000)
347-
348-
return ipfs('ls --help')
349-
.then((out) => {
350-
expect(out.split('\n').slice(1)).to.eql(['',
351-
'List files for the given directory',
352-
'',
353-
'Options:',
354-
' --version Show version number [boolean]',
355-
' --silent Write no output [boolean] [default: false]',
356-
' --pass Pass phrase for the keys [string] [default: ""]',
357-
' --help Show help [boolean]',
358-
' -v, --headers Print table headers (Hash, Size, Name).',
359-
' [boolean] [default: false]',
360-
' --resolve-type Resolve linked objects to find out their types. (not',
361-
' implemented yet) [boolean] [default: false]',
362-
'', ''])
363-
})
364-
})
365-
366299
it('get', function () {
367300
this.timeout(20 * 1000)
368301

test/cli/ls.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const expect = require('chai').expect
5+
const runOnAndOff = require('../utils/on-and-off')
6+
7+
describe('ls', () => runOnAndOff((thing) => {
8+
let ipfs
9+
10+
before(() => {
11+
ipfs = thing.ipfs
12+
return ipfs('files add -r test/fixtures/test-data/recursive-get-dir')
13+
})
14+
15+
it('prints added files', function () {
16+
this.timeout(20 * 1000)
17+
return ipfs('ls QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2')
18+
.then((out) => {
19+
expect(out).to.eql(
20+
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' +
21+
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' +
22+
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' +
23+
'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' +
24+
'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n'
25+
)
26+
})
27+
})
28+
29+
it('prints nothing for non-existant hashes', function () {
30+
// If the daemon is off, ls should fail
31+
// If the daemon is on, ls should search until it hits a timeout
32+
return Promise.race([
33+
ipfs.fail('ls QmYmW4HiZhotsoSqnv2o1oSssvkRM8b9RweBoH7ao5nki2'),
34+
new Promise((res, rej) => setTimeout(res, 4000))
35+
])
36+
.catch(() => expect.fail(0, 1, 'Should have thrown or timedout'))
37+
})
38+
39+
it('adds a header, -v', function () {
40+
this.timeout(20 * 1000)
41+
return ipfs('ls /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2 -v')
42+
.then((out) => {
43+
expect(out).to.eql(
44+
'Hash Size Name\n' +
45+
'QmQQHYDwAQms78fPcvx1uFFsfho23YJNoewfLbi9AtdyJ9 123530 blocks/\n' +
46+
'QmPkWYfSLCEBLZu7BZt4kigGDMe3cpogMbeVf97gN2xJDN 3939 config\n' +
47+
'Qma13ZrhKG52MWnwtZ6fMD8jGj8d4Q9sJgn5xtKgeZw5uz 5503 datastore/\n' +
48+
'QmUhUuiTKkkK8J6JZ9zmj8iNHPuNfGYcszgRumzhHBxEEU 7397 init-docs/\n' +
49+
'QmR56UJmAaZLXLdTT1ALrE9vVqV8soUEekm9BMd4FnuYqV 10 version\n'
50+
)
51+
})
52+
})
53+
54+
it('follows a path, <hash>/<subdir>', function () {
55+
this.timeout(20 * 1000)
56+
57+
return ipfs('ls /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2/init-docs')
58+
.then((out) => {
59+
expect(out).to.eql(
60+
'QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V 1688 about\n' +
61+
'QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y 200 contact\n' +
62+
'QmegvLXxpVKiZ4b57Xs1syfBVRd8CbucVHAp7KpLQdGieC 65 docs/\n' +
63+
'QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7 322 help\n' +
64+
'QmdncfsVm2h5Kqq9hPmU7oAVX2zTSVP3L869tgTbPYnsha 1728 quick-start\n' +
65+
'QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB 1102 readme\n' +
66+
'QmTumTjvcYCAvRRwQ8sDRxh8ezmrcr88YFU7iYNroGGTBZ 1027 security-notes\n' +
67+
'QmciSU8hfpAXKjvK5YLUSwApomGSWN5gFbP4EpDAEzu2Te 863 tour/\n'
68+
)
69+
})
70+
})
71+
72+
it('recursively follows folders, -r', function () {
73+
this.slow(2000)
74+
this.timeout(20 * 1000)
75+
76+
return ipfs('ls -r /ipfs/QmYmW4HiZhotsoSqnv2o1oUusvkRM8b9RweBoH7ao5nki2/init-docs')
77+
.then(out => {
78+
expect(out).to.eql(
79+
'QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V 1688 about\n' +
80+
'QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y 200 contact\n' +
81+
'QmegvLXxpVKiZ4b57Xs1syfBVRd8CbucVHAp7KpLQdGieC 65 docs/\n' +
82+
'QmQN88TEidd3RY2u3dpib49fERTDfKtDpvxnvczATNsfKT 14 index\n' +
83+
'QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7 322 help\n' +
84+
'QmdncfsVm2h5Kqq9hPmU7oAVX2zTSVP3L869tgTbPYnsha 1728 quick-start\n' +
85+
'QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB 1102 readme\n' +
86+
'QmTumTjvcYCAvRRwQ8sDRxh8ezmrcr88YFU7iYNroGGTBZ 1027 security-notes\n' +
87+
'QmciSU8hfpAXKjvK5YLUSwApomGSWN5gFbP4EpDAEzu2Te 863 tour/\n' +
88+
'QmYE7xo6NxbHEVEHej1yzxijYaNY51BaeKxjXxn6Ssa6Bs 807 0.0-intro\n'
89+
)
90+
})
91+
})
92+
}))

0 commit comments

Comments
 (0)