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

Commit

Permalink
fix: fix ipfs.ls() for a single file object (#3440)
Browse files Browse the repository at this point in the history
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 <alex@achingbrain.net>
  • Loading branch information
ikreymer and achingbrain committed Dec 11, 2020
1 parent c3c4607 commit f243dd1
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 31 deletions.
67 changes: 67 additions & 0 deletions packages/interface-ipfs-core/src/ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
}
3 changes: 2 additions & 1 deletion packages/ipfs-core/src/components/ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
78 changes: 50 additions & 28 deletions packages/ipfs-http-client/src/ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
})
Expand Down
19 changes: 18 additions & 1 deletion packages/ipfs-http-server/src/api/resources/files-regular.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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, {
Expand Down
46 changes: 45 additions & 1 deletion packages/ipfs-http-server/test/inject/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/ipfs/test/interface-http-go.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit f243dd1

Please sign in to comment.