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

Commit b40925a

Browse files
Fix ipfs.ls() for a single file object (#3440)
This fixes a typo where `ipfs.ls()` returns instead of yielding from a generator. The result is that ipfs.ls() does not work when the path is a file. I think this should fix it, with `ls` returning a generator with a single file result. Co-authored-by: achingbrain <[email protected]>
1 parent c3c4607 commit b40925a

File tree

6 files changed

+190
-31
lines changed

6 files changed

+190
-31
lines changed

packages/interface-ipfs-core/src/ls.js

+67
Original file line numberDiff line numberDiff line change
@@ -218,5 +218,72 @@ module.exports = (common, options) => {
218218
expect(output).to.have.lengthOf(1)
219219
expect(output[0]).to.have.property('path', `${path}/${subfile}`)
220220
})
221+
222+
it('should ls single file', async () => {
223+
const dir = randomName('DIR')
224+
const file = randomName('F0')
225+
226+
const input = { path: `${dir}/${file}`, content: randomName('D1') }
227+
228+
const res = await ipfs.add(input)
229+
const path = `${res.cid}/${file}`
230+
const output = await all(ipfs.ls(path))
231+
232+
expect(output).to.have.lengthOf(1)
233+
expect(output[0]).to.have.property('path', path)
234+
})
235+
236+
it('should ls single file with metadata', async () => {
237+
const dir = randomName('DIR')
238+
const file = randomName('F0')
239+
240+
const input = {
241+
path: `${dir}/${file}`,
242+
content: randomName('D1'),
243+
mode: 0o631,
244+
mtime: {
245+
secs: 5000,
246+
nsecs: 100
247+
}
248+
}
249+
250+
const res = await ipfs.add(input)
251+
const path = `${res.cid}/${file}`
252+
const output = await all(ipfs.ls(res.cid))
253+
254+
expect(output).to.have.lengthOf(1)
255+
expect(output[0]).to.have.property('path', path)
256+
expect(output[0]).to.have.property('mode', input.mode)
257+
expect(output[0]).to.have.deep.property('mtime', input.mtime)
258+
})
259+
260+
it('should ls single file without containing directory', async () => {
261+
const input = { content: randomName('D1') }
262+
263+
const res = await ipfs.add(input)
264+
const output = await all(ipfs.ls(res.cid))
265+
266+
expect(output).to.have.lengthOf(1)
267+
expect(output[0]).to.have.property('path', res.cid.toString())
268+
})
269+
270+
it('should ls single file without containing directory with metadata', async () => {
271+
const input = {
272+
content: randomName('D1'),
273+
mode: 0o631,
274+
mtime: {
275+
secs: 5000,
276+
nsecs: 100
277+
}
278+
}
279+
280+
const res = await ipfs.add(input)
281+
const output = await all(ipfs.ls(res.cid))
282+
283+
expect(output).to.have.lengthOf(1)
284+
expect(output[0]).to.have.property('path', res.cid.toString())
285+
expect(output[0]).to.have.property('mode', input.mode)
286+
expect(output[0]).to.have.deep.property('mtime', input.mtime)
287+
})
221288
})
222289
}

packages/ipfs-core/src/components/ls.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ module.exports = function ({ ipld, preload }) {
3434
}
3535

3636
if (file.unixfs.type === 'file') {
37-
return mapFile(file, options)
37+
yield mapFile(file, options)
38+
return
3839
}
3940

4041
if (file.unixfs.type.includes('dir')) {

packages/ipfs-http-client/src/ls.js

+50-28
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,54 @@
33
const CID = require('cids')
44
const configure = require('./lib/configure')
55
const toUrlSearchParams = require('./lib/to-url-search-params')
6+
const stat = require('./files/stat')
67

7-
module.exports = configure(api => {
8+
module.exports = configure((api, opts) => {
89
return async function * ls (path, options = {}) {
10+
const pathStr = `${path instanceof Uint8Array ? new CID(path) : path}`
11+
12+
async function mapLink (link) {
13+
let hash = link.Hash
14+
15+
if (hash.includes('/')) {
16+
// the hash is a path, but we need the CID
17+
const ipfsPath = hash.startsWith('/ipfs/') ? hash : `/ipfs/${hash}`
18+
const stats = await stat(opts)(ipfsPath)
19+
20+
hash = stats.cid
21+
}
22+
23+
const entry = {
24+
name: link.Name,
25+
path: pathStr + (link.Name ? `/${link.Name}` : ''),
26+
size: link.Size,
27+
cid: new CID(hash),
28+
type: typeOf(link),
29+
depth: link.Depth || 1
30+
}
31+
32+
if (link.Mode) {
33+
entry.mode = parseInt(link.Mode, 8)
34+
}
35+
36+
if (link.Mtime !== undefined && link.Mtime !== null) {
37+
entry.mtime = {
38+
secs: link.Mtime
39+
}
40+
41+
if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) {
42+
entry.mtime.nsecs = link.MtimeNsecs
43+
}
44+
}
45+
46+
return entry
47+
}
48+
949
const res = await api.post('ls', {
1050
timeout: options.timeout,
1151
signal: options.signal,
1252
searchParams: toUrlSearchParams({
13-
arg: `${path instanceof Uint8Array ? new CID(path) : path}`,
53+
arg: pathStr,
1454
...options
1555
}),
1656
headers: options.headers
@@ -28,37 +68,19 @@ module.exports = configure(api => {
2868
throw new Error('expected one array in results.Objects')
2969
}
3070

31-
result = result.Links
32-
if (!Array.isArray(result)) {
71+
const links = result.Links
72+
if (!Array.isArray(links)) {
3373
throw new Error('expected one array in results.Objects[0].Links')
3474
}
3575

36-
for (const link of result) {
37-
const entry = {
38-
name: link.Name,
39-
path: path + '/' + link.Name,
40-
size: link.Size,
41-
cid: new CID(link.Hash),
42-
type: typeOf(link),
43-
depth: link.Depth || 1
44-
}
76+
if (!links.length) {
77+
// no links, this is a file, yield a single result
78+
yield mapLink(result)
4579

46-
if (link.Mode) {
47-
entry.mode = parseInt(link.Mode, 8)
48-
}
49-
50-
if (link.Mtime !== undefined && link.Mtime !== null) {
51-
entry.mtime = {
52-
secs: link.Mtime
53-
}
54-
55-
if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) {
56-
entry.mtime.nsecs = link.MtimeNsecs
57-
}
58-
}
59-
60-
yield entry
80+
return
6181
}
82+
83+
yield * links.map(mapLink)
6284
}
6385
}
6486
})

packages/ipfs-http-server/src/api/resources/files-regular.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,16 @@ exports.ls = {
401401

402402
const mapLink = link => {
403403
const output = {
404-
Name: link.name,
405404
Hash: cidToString(link.cid, { base: cidBase }),
406405
Size: link.size,
407406
Type: toTypeCode(link.type),
408407
Depth: link.depth
409408
}
410409

410+
if (link.name) {
411+
output.Name = link.name
412+
}
413+
411414
if (link.mode != null) {
412415
output.Mode = link.mode.toString(8).padStart(4, '0')
413416
}
@@ -423,6 +426,20 @@ exports.ls = {
423426
return output
424427
}
425428

429+
const stat = await ipfs.files.stat(path.startsWith('/ipfs/') ? path : `/ipfs/${path}`)
430+
431+
if (stat.type === 'file') {
432+
// return single object with metadata
433+
return h.response({
434+
Objects: [{
435+
...mapLink(stat),
436+
Hash: path,
437+
Depth: 1,
438+
Links: []
439+
}]
440+
})
441+
}
442+
426443
if (!stream) {
427444
try {
428445
const links = await all(ipfs.ls(path, {

packages/ipfs-http-server/test/inject/files.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ describe('/files', () => {
3030
cat: sinon.stub(),
3131
get: sinon.stub(),
3232
ls: sinon.stub(),
33-
refs: sinon.stub()
33+
refs: sinon.stub(),
34+
files: {
35+
stat: sinon.stub()
36+
}
3437
}
3538

3639
ipfs.refs.local = sinon.stub()
@@ -352,6 +355,9 @@ describe('/files', () => {
352355
})
353356

354357
it('should list directory contents', async () => {
358+
ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({
359+
type: 'directory'
360+
})
355361
ipfs.ls.withArgs(`${cid}`, defaultOptions).returns([{
356362
name: 'link',
357363
cid,
@@ -380,7 +386,36 @@ describe('/files', () => {
380386
})
381387
})
382388

389+
it('should list a file', async () => {
390+
ipfs.files.stat.withArgs(`/ipfs/${cid}/derp`).returns({
391+
cid,
392+
size: 10,
393+
type: 'file',
394+
depth: 1,
395+
mode: 0o420
396+
})
397+
398+
const res = await http({
399+
method: 'POST',
400+
url: `/api/v0/ls?arg=${cid}/derp`
401+
}, { ipfs })
402+
403+
expect(res).to.have.property('statusCode', 200)
404+
expect(res).to.have.deep.nested.property('result.Objects[0]', {
405+
Hash: `${cid}/derp`,
406+
Depth: 1,
407+
Mode: '0420',
408+
Size: 10,
409+
Type: 2,
410+
Links: []
411+
})
412+
expect(ipfs.ls.called).to.be.false()
413+
})
414+
383415
it('should list directory contents without unixfs v1.5 fields', async () => {
416+
ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({
417+
type: 'directory'
418+
})
384419
ipfs.ls.withArgs(`${cid}`, defaultOptions).returns([{
385420
name: 'link',
386421
cid,
@@ -408,6 +443,9 @@ describe('/files', () => {
408443
})
409444

410445
it('should list directory contents recursively', async () => {
446+
ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({
447+
type: 'directory'
448+
})
411449
ipfs.ls.withArgs(`${cid}`, {
412450
...defaultOptions,
413451
recursive: true
@@ -456,6 +494,9 @@ describe('/files', () => {
456494
})
457495

458496
it('accepts a timeout', async () => {
497+
ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({
498+
type: 'directory'
499+
})
459500
ipfs.ls.withArgs(`${cid}`, {
460501
...defaultOptions,
461502
timeout: 1000
@@ -477,6 +518,9 @@ describe('/files', () => {
477518
})
478519

479520
it('accepts a timeout when streaming', async () => {
521+
ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({
522+
type: 'directory'
523+
})
480524
ipfs.ls.withArgs(`${cid}`, {
481525
...defaultOptions,
482526
timeout: 1000

packages/ipfs/test/interface-http-go.js

+8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ describe('interface-ipfs-core over ipfs-http-client tests against go-ipfs', () =
4646
name: 'should ls with metadata',
4747
reason: 'TODO not implemented in go-ipfs yet'
4848
},
49+
{
50+
name: 'should ls single file with metadata',
51+
reason: 'TODO not implemented in go-ipfs yet'
52+
},
53+
{
54+
name: 'should ls single file without containing directory with metadata',
55+
reason: 'TODO not implemented in go-ipfs yet'
56+
},
4957
{
5058
name: 'should override raw leaves when file is smaller than one block and metadata is present',
5159
reason: 'TODO not implemented in go-ipfs yet'

0 commit comments

Comments
 (0)