diff --git a/packages/interface-ipfs-core/src/ls.js b/packages/interface-ipfs-core/src/ls.js index 533c229faf..ae670e5852 100644 --- a/packages/interface-ipfs-core/src/ls.js +++ b/packages/interface-ipfs-core/src/ls.js @@ -218,5 +218,72 @@ module.exports = (common, options) => { expect(output).to.have.lengthOf(1) expect(output[0]).to.have.property('path', `${path}/${subfile}`) }) + + it('should ls single file', async () => { + const dir = randomName('DIR') + const file = randomName('F0') + + const input = { path: `${dir}/${file}`, content: randomName('D1') } + + const res = await ipfs.add(input) + const path = `${res.cid}/${file}` + const output = await all(ipfs.ls(path)) + + expect(output).to.have.lengthOf(1) + expect(output[0]).to.have.property('path', path) + }) + + it('should ls single file with metadata', async () => { + const dir = randomName('DIR') + const file = randomName('F0') + + const input = { + path: `${dir}/${file}`, + content: randomName('D1'), + mode: 0o631, + mtime: { + secs: 5000, + nsecs: 100 + } + } + + const res = await ipfs.add(input) + const path = `${res.cid}/${file}` + const output = await all(ipfs.ls(res.cid)) + + expect(output).to.have.lengthOf(1) + expect(output[0]).to.have.property('path', path) + expect(output[0]).to.have.property('mode', input.mode) + expect(output[0]).to.have.deep.property('mtime', input.mtime) + }) + + it('should ls single file without containing directory', async () => { + const input = { content: randomName('D1') } + + const res = await ipfs.add(input) + const output = await all(ipfs.ls(res.cid)) + + expect(output).to.have.lengthOf(1) + expect(output[0]).to.have.property('path', res.cid.toString()) + }) + + it('should ls single file without containing directory with metadata', async () => { + const input = { + content: randomName('D1'), + mode: 0o631, + mtime: { + secs: 5000, + nsecs: 100 + } + } + + const res = await ipfs.add(input) + const output = await all(ipfs.ls(res.cid)) + + expect(output).to.have.lengthOf(1) + expect(output[0]).to.have.property('path', res.cid.toString()) + expect(output[0]).to.have.property('mode', input.mode) + expect(output[0]).to.have.deep.property('mtime', input.mtime) + }) }) } diff --git a/packages/ipfs-core/src/components/ls.js b/packages/ipfs-core/src/components/ls.js index ae34e1aee9..1fe0234f2c 100644 --- a/packages/ipfs-core/src/components/ls.js +++ b/packages/ipfs-core/src/components/ls.js @@ -34,7 +34,8 @@ module.exports = function ({ ipld, preload }) { } if (file.unixfs.type === 'file') { - return mapFile(file, options) + yield mapFile(file, options) + return } if (file.unixfs.type.includes('dir')) { diff --git a/packages/ipfs-http-client/src/ls.js b/packages/ipfs-http-client/src/ls.js index ff9cdbb571..f51a9f640e 100644 --- a/packages/ipfs-http-client/src/ls.js +++ b/packages/ipfs-http-client/src/ls.js @@ -3,14 +3,54 @@ const CID = require('cids') const configure = require('./lib/configure') const toUrlSearchParams = require('./lib/to-url-search-params') +const stat = require('./files/stat') -module.exports = configure(api => { +module.exports = configure((api, opts) => { return async function * ls (path, options = {}) { + const pathStr = `${path instanceof Uint8Array ? new CID(path) : path}` + + async function mapLink (link) { + let hash = link.Hash + + if (hash.includes('/')) { + // the hash is a path, but we need the CID + const ipfsPath = hash.startsWith('/ipfs/') ? hash : `/ipfs/${hash}` + const stats = await stat(opts)(ipfsPath) + + hash = stats.cid + } + + const entry = { + name: link.Name, + path: pathStr + (link.Name ? `/${link.Name}` : ''), + size: link.Size, + cid: new CID(hash), + type: typeOf(link), + depth: link.Depth || 1 + } + + if (link.Mode) { + entry.mode = parseInt(link.Mode, 8) + } + + if (link.Mtime !== undefined && link.Mtime !== null) { + entry.mtime = { + secs: link.Mtime + } + + if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) { + entry.mtime.nsecs = link.MtimeNsecs + } + } + + return entry + } + const res = await api.post('ls', { timeout: options.timeout, signal: options.signal, searchParams: toUrlSearchParams({ - arg: `${path instanceof Uint8Array ? new CID(path) : path}`, + arg: pathStr, ...options }), headers: options.headers @@ -28,37 +68,19 @@ module.exports = configure(api => { throw new Error('expected one array in results.Objects') } - result = result.Links - if (!Array.isArray(result)) { + const links = result.Links + if (!Array.isArray(links)) { throw new Error('expected one array in results.Objects[0].Links') } - for (const link of result) { - const entry = { - name: link.Name, - path: path + '/' + link.Name, - size: link.Size, - cid: new CID(link.Hash), - type: typeOf(link), - depth: link.Depth || 1 - } + if (!links.length) { + // no links, this is a file, yield a single result + yield mapLink(result) - if (link.Mode) { - entry.mode = parseInt(link.Mode, 8) - } - - if (link.Mtime !== undefined && link.Mtime !== null) { - entry.mtime = { - secs: link.Mtime - } - - if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) { - entry.mtime.nsecs = link.MtimeNsecs - } - } - - yield entry + return } + + yield * links.map(mapLink) } } }) diff --git a/packages/ipfs-http-server/src/api/resources/files-regular.js b/packages/ipfs-http-server/src/api/resources/files-regular.js index b3d27bc688..c3d9725bd1 100644 --- a/packages/ipfs-http-server/src/api/resources/files-regular.js +++ b/packages/ipfs-http-server/src/api/resources/files-regular.js @@ -401,13 +401,16 @@ exports.ls = { const mapLink = link => { const output = { - Name: link.name, Hash: cidToString(link.cid, { base: cidBase }), Size: link.size, Type: toTypeCode(link.type), Depth: link.depth } + if (link.name) { + output.Name = link.name + } + if (link.mode != null) { output.Mode = link.mode.toString(8).padStart(4, '0') } @@ -423,6 +426,20 @@ exports.ls = { return output } + const stat = await ipfs.files.stat(path.startsWith('/ipfs/') ? path : `/ipfs/${path}`) + + if (stat.type === 'file') { + // return single object with metadata + return h.response({ + Objects: [{ + ...mapLink(stat), + Hash: path, + Depth: 1, + Links: [] + }] + }) + } + if (!stream) { try { const links = await all(ipfs.ls(path, { diff --git a/packages/ipfs-http-server/test/inject/files.js b/packages/ipfs-http-server/test/inject/files.js index 8cf18c32c4..9b73c2ed7f 100644 --- a/packages/ipfs-http-server/test/inject/files.js +++ b/packages/ipfs-http-server/test/inject/files.js @@ -30,7 +30,10 @@ describe('/files', () => { cat: sinon.stub(), get: sinon.stub(), ls: sinon.stub(), - refs: sinon.stub() + refs: sinon.stub(), + files: { + stat: sinon.stub() + } } ipfs.refs.local = sinon.stub() @@ -352,6 +355,9 @@ describe('/files', () => { }) it('should list directory contents', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({ + type: 'directory' + }) ipfs.ls.withArgs(`${cid}`, defaultOptions).returns([{ name: 'link', cid, @@ -380,7 +386,36 @@ describe('/files', () => { }) }) + it('should list a file', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}/derp`).returns({ + cid, + size: 10, + type: 'file', + depth: 1, + mode: 0o420 + }) + + const res = await http({ + method: 'POST', + url: `/api/v0/ls?arg=${cid}/derp` + }, { ipfs }) + + expect(res).to.have.property('statusCode', 200) + expect(res).to.have.deep.nested.property('result.Objects[0]', { + Hash: `${cid}/derp`, + Depth: 1, + Mode: '0420', + Size: 10, + Type: 2, + Links: [] + }) + expect(ipfs.ls.called).to.be.false() + }) + it('should list directory contents without unixfs v1.5 fields', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({ + type: 'directory' + }) ipfs.ls.withArgs(`${cid}`, defaultOptions).returns([{ name: 'link', cid, @@ -408,6 +443,9 @@ describe('/files', () => { }) it('should list directory contents recursively', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({ + type: 'directory' + }) ipfs.ls.withArgs(`${cid}`, { ...defaultOptions, recursive: true @@ -456,6 +494,9 @@ describe('/files', () => { }) it('accepts a timeout', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({ + type: 'directory' + }) ipfs.ls.withArgs(`${cid}`, { ...defaultOptions, timeout: 1000 @@ -477,6 +518,9 @@ describe('/files', () => { }) it('accepts a timeout when streaming', async () => { + ipfs.files.stat.withArgs(`/ipfs/${cid}`).returns({ + type: 'directory' + }) ipfs.ls.withArgs(`${cid}`, { ...defaultOptions, timeout: 1000 diff --git a/packages/ipfs/test/interface-http-go.js b/packages/ipfs/test/interface-http-go.js index c3a60d6d0d..6d56c3800b 100644 --- a/packages/ipfs/test/interface-http-go.js +++ b/packages/ipfs/test/interface-http-go.js @@ -46,6 +46,14 @@ describe('interface-ipfs-core over ipfs-http-client tests against go-ipfs', () = name: 'should ls with metadata', reason: 'TODO not implemented in go-ipfs yet' }, + { + name: 'should ls single file with metadata', + reason: 'TODO not implemented in go-ipfs yet' + }, + { + name: 'should ls single file without containing directory with metadata', + reason: 'TODO not implemented in go-ipfs yet' + }, { name: 'should override raw leaves when file is smaller than one block and metadata is present', reason: 'TODO not implemented in go-ipfs yet'