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

Commit 0af9bf4

Browse files
authored
Merge pull request #213 from ipfs/improve-consistency-with-go
chore: use same hash for empty node as go
2 parents 4eb417c + 1a70af6 commit 0af9bf4

File tree

7 files changed

+252
-22
lines changed

7 files changed

+252
-22
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@
5959
"bs58": "^4.0.1",
6060
"cids": "~0.5.3",
6161
"deep-extend": "~0.6.0",
62-
"ipfs-unixfs": "~0.1.14",
63-
"ipld": "^0.17.2",
62+
"ipfs-unixfs": "~0.1.15",
63+
"ipld": "~0.17.2",
6464
"ipld-dag-pb": "~0.14.4",
6565
"left-pad": "^1.3.0",
6666
"lodash": "^4.17.10",

src/exporter/file.js

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ module.exports = (node, name, path, pathRest, resolve, size, dag, parent, depth,
3030
}
3131

3232
if (length === 0) {
33-
return pull.empty()
33+
return pull.once(Buffer.alloc(0))
3434
}
3535

3636
if (!offset) {
@@ -56,36 +56,37 @@ module.exports = (node, name, path, pathRest, resolve, size, dag, parent, depth,
5656

5757
function streamBytes (dag, node, fileSize, offset, length) {
5858
if (offset === fileSize || length === 0) {
59-
return pull.empty()
59+
return pull.once(Buffer.alloc(0))
6060
}
6161

6262
const end = offset + length
63-
let streamPosition = 0
6463

6564
function getData ({ node, start }) {
66-
if (!node || !node.data) {
67-
return
68-
}
69-
7065
try {
7166
const file = UnixFS.unmarshal(node.data)
7267

7368
if (!file.data) {
74-
return
69+
return Buffer.alloc(0)
7570
}
7671

77-
const block = extractDataFromBlock(file.data, start, offset, end)
78-
79-
streamPosition += block.length
80-
81-
return block
72+
return extractDataFromBlock(file.data, start, offset, end)
8273
} catch (error) {
8374
throw new Error(`Failed to unmarshal node - ${error.message}`)
8475
}
8576
}
8677

78+
// as we step through the children, keep track of where we are in the stream
79+
// so we can filter out nodes we're not interested in
80+
let streamPosition = 0
81+
8782
function visitor ({ node }) {
8883
const file = UnixFS.unmarshal(node.data)
84+
const nodeHasData = Boolean(file.data && file.data.length)
85+
86+
// handle case where data is present on leaf nodes and internal nodes
87+
if (nodeHasData && node.links.length) {
88+
streamPosition += file.data.length
89+
}
8990

9091
// work out which child nodes contain the requested data
9192
const filteredLinks = node.links
@@ -100,7 +101,7 @@ function streamBytes (dag, node, fileSize, offset, length) {
100101

101102
return child
102103
})
103-
.filter((child, index) => {
104+
.filter((child) => {
104105
return (offset >= child.start && offset < child.end) || // child has offset byte
105106
(end > child.start && end <= child.end) || // child has end byte
106107
(offset < child.start && end > child.end) // child is between offset and end bytes
@@ -138,6 +139,12 @@ function streamBytes (dag, node, fileSize, offset, length) {
138139
function extractDataFromBlock (block, streamPosition, begin, end) {
139140
const blockLength = block.length
140141

142+
if (begin >= streamPosition + blockLength) {
143+
// If begin is after the start of the block, return an empty block
144+
// This can happen when internal nodes contain data
145+
return Buffer.alloc(0)
146+
}
147+
141148
if (end - streamPosition < blockLength) {
142149
// If the end byte is in the current block, truncate the block to the end byte
143150
block = block.slice(0, end - streamPosition)

test/exporter.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ const CID = require('cids')
1414
const loadFixture = require('aegir/fixtures')
1515
const doUntil = require('async/doUntil')
1616
const waterfall = require('async/waterfall')
17+
const parallel = require('async/parallel')
18+
const series = require('async/series')
19+
const fs = require('fs')
20+
const path = require('path')
21+
const push = require('pull-pushable')
22+
const toPull = require('stream-to-pull-stream')
23+
const toStream = require('pull-stream-to-stream')
24+
const {
25+
DAGNode,
26+
DAGLink
27+
} = require('ipld-dag-pb')
1728

1829
const unixFSEngine = require('./../src')
1930
const exporter = unixFSEngine.exporter
@@ -64,6 +75,52 @@ module.exports = (repo) => {
6475
})
6576
}
6677

78+
function addTestDirectory ({directory, strategy = 'balanced', maxChunkSize}, callback) {
79+
const input = push()
80+
const dirName = path.basename(directory)
81+
82+
pull(
83+
input,
84+
pull.map((file) => {
85+
return {
86+
path: path.join(dirName, path.basename(file)),
87+
content: toPull.source(fs.createReadStream(file))
88+
}
89+
}),
90+
importer(ipld, {
91+
strategy,
92+
maxChunkSize
93+
}),
94+
pull.collect(callback)
95+
)
96+
97+
const listFiles = (directory, depth, stream, cb) => {
98+
waterfall([
99+
(done) => fs.stat(directory, done),
100+
(stats, done) => {
101+
if (stats.isDirectory()) {
102+
return waterfall([
103+
(done) => fs.readdir(directory, done),
104+
(children, done) => {
105+
series(
106+
children.map(child => (next) => listFiles(path.join(directory, child), depth + 1, stream, next)),
107+
done
108+
)
109+
}
110+
], done)
111+
}
112+
113+
stream.push(directory)
114+
done()
115+
}
116+
], cb)
117+
}
118+
119+
listFiles(directory, 0, input, () => {
120+
input.end()
121+
})
122+
}
123+
67124
function checkBytesThatSpanBlocks (strategy, cb) {
68125
const bytesInABlock = 262144
69126
const bytes = Buffer.alloc(bytesInABlock + 100, 0)
@@ -517,6 +574,55 @@ module.exports = (repo) => {
517574
checkBytesThatSpanBlocks('trickle', done)
518575
})
519576

577+
it('exports a directory containing an empty file whose content gets turned into a ReadableStream', function (done) {
578+
// replicates the behaviour of ipfs.files.get
579+
waterfall([
580+
(cb) => addTestDirectory({
581+
directory: path.join(__dirname, 'fixtures', 'dir-with-empty-files')
582+
}, cb),
583+
(result, cb) => {
584+
const dir = result.pop()
585+
586+
pull(
587+
exporter(dir.multihash, ipld),
588+
pull.map((file) => {
589+
if (file.content) {
590+
file.content = toStream.source(file.content)
591+
file.content.pause()
592+
}
593+
594+
return file
595+
}),
596+
pull.collect((error, files) => {
597+
if (error) {
598+
return cb(error)
599+
}
600+
601+
series(
602+
files
603+
.filter(file => Boolean(file.content))
604+
.map(file => {
605+
return (done) => {
606+
if (file.content) {
607+
file.content
608+
.pipe(toStream.sink(pull.collect((error, bufs) => {
609+
expect(error).to.not.exist()
610+
expect(bufs.length).to.equal(1)
611+
expect(bufs[0].length).to.equal(0)
612+
613+
done()
614+
})))
615+
}
616+
}
617+
}),
618+
cb
619+
)
620+
})
621+
)
622+
}
623+
], done)
624+
})
625+
520626
// TODO: This needs for the stores to have timeouts,
521627
// otherwise it is impossible to predict if a file doesn't
522628
// really exist
@@ -532,6 +638,100 @@ module.exports = (repo) => {
532638
})
533639
)
534640
})
641+
642+
it('exports file with data on internal and leaf nodes', function (done) {
643+
waterfall([
644+
(cb) => createAndPersistNode(ipld, 'raw', [0x04, 0x05, 0x06, 0x07], [], cb),
645+
(leaf, cb) => createAndPersistNode(ipld, 'file', [0x00, 0x01, 0x02, 0x03], [
646+
leaf
647+
], cb),
648+
(file, cb) => {
649+
pull(
650+
exporter(file.multihash, ipld),
651+
pull.asyncMap((file, cb) => readFile(file, cb)),
652+
pull.through(buffer => {
653+
expect(buffer).to.deep.equal(Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]))
654+
}),
655+
pull.collect(cb)
656+
)
657+
}
658+
], done)
659+
})
660+
661+
it('exports file with data on some internal and leaf nodes', function (done) {
662+
// create a file node with three children:
663+
// where:
664+
// i = internal node without data
665+
// d = internal node with data
666+
// l = leaf node with data
667+
// i
668+
// / | \
669+
// l d i
670+
// | \
671+
// l l
672+
waterfall([
673+
(cb) => {
674+
// create leaves
675+
parallel([
676+
(next) => createAndPersistNode(ipld, 'raw', [0x00, 0x01, 0x02, 0x03], [], next),
677+
(next) => createAndPersistNode(ipld, 'raw', [0x08, 0x09, 0x10, 0x11], [], next),
678+
(next) => createAndPersistNode(ipld, 'raw', [0x12, 0x13, 0x14, 0x15], [], next)
679+
], cb)
680+
},
681+
(leaves, cb) => {
682+
parallel([
683+
(next) => createAndPersistNode(ipld, 'raw', [0x04, 0x05, 0x06, 0x07], [leaves[1]], next),
684+
(next) => createAndPersistNode(ipld, 'raw', null, [leaves[2]], next)
685+
], (error, internalNodes) => {
686+
if (error) {
687+
return cb(error)
688+
}
689+
690+
createAndPersistNode(ipld, 'file', null, [
691+
leaves[0],
692+
internalNodes[0],
693+
internalNodes[1]
694+
], cb)
695+
})
696+
},
697+
(file, cb) => {
698+
pull(
699+
exporter(file.multihash, ipld),
700+
pull.asyncMap((file, cb) => readFile(file, cb)),
701+
pull.through(buffer => {
702+
expect(buffer).to.deep.equal(
703+
Buffer.from([
704+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
705+
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15
706+
])
707+
)
708+
}),
709+
pull.collect(cb)
710+
)
711+
}
712+
], done)
713+
})
714+
715+
it('exports file with data on internal and leaf nodes with an offset that only fetches data from leaf nodes', function (done) {
716+
waterfall([
717+
(cb) => createAndPersistNode(ipld, 'raw', [0x04, 0x05, 0x06, 0x07], [], cb),
718+
(leaf, cb) => createAndPersistNode(ipld, 'file', [0x00, 0x01, 0x02, 0x03], [
719+
leaf
720+
], cb),
721+
(file, cb) => {
722+
pull(
723+
exporter(file.multihash, ipld, {
724+
offset: 4
725+
}),
726+
pull.asyncMap((file, cb) => readFile(file, cb)),
727+
pull.through(buffer => {
728+
expect(buffer).to.deep.equal(Buffer.from([0x04, 0x05, 0x06, 0x07]))
729+
}),
730+
pull.collect(cb)
731+
)
732+
}
733+
], done)
734+
})
535735
})
536736
}
537737

@@ -567,3 +767,26 @@ function readFile (file, done) {
567767
})
568768
)
569769
}
770+
771+
function createAndPersistNode (ipld, type, data, children, callback) {
772+
const file = new UnixFS(type, data ? Buffer.from(data) : undefined)
773+
const links = []
774+
775+
children.forEach(child => {
776+
const leaf = UnixFS.unmarshal(child.data)
777+
778+
file.addBlockSize(leaf.fileSize())
779+
780+
links.push(new DAGLink('', child.size, child.multihash))
781+
})
782+
783+
DAGNode.create(file.marshal(), links, (error, node) => {
784+
if (error) {
785+
return callback(error)
786+
}
787+
788+
ipld.put(node, {
789+
cid: new CID(node.multihash)
790+
}, (error) => callback(error, node))
791+
})
792+
}

test/fixtures/dir-with-empty-files/empty-file.txt

Whitespace-only changes.

test/importer.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const extend = require('deep-extend')
77
const chai = require('chai')
88
chai.use(require('dirty-chai'))
99
const expect = chai.expect
10-
const sinon = require('sinon')
10+
const spy = require('sinon/lib/sinon/spy')
1111
const BlockService = require('ipfs-block-service')
1212
const pull = require('pull-stream')
1313
const mh = require('multihashes')
@@ -214,7 +214,7 @@ module.exports = (repo) => {
214214
expect(err).to.not.exist()
215215
expect(nodes.length).to.be.eql(1)
216216
// always yield empty node
217-
expect(mh.toB58String(nodes[0].multihash)).to.be.eql('QmfJMCvenrj4SKKRc48DYPxwVdS44qCUCqqtbqhJuSTWXP')
217+
expect(mh.toB58String(nodes[0].multihash)).to.be.eql('QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH')
218218
done()
219219
}))
220220
})
@@ -456,7 +456,7 @@ module.exports = (repo) => {
456456
})
457457

458458
it('will call an optional progress function', (done) => {
459-
options.progress = sinon.spy()
459+
options.progress = spy()
460460

461461
pull(
462462
pull.values([{

test/node.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const mkdirp = require('mkdirp')
1010
const series = require('async/series')
1111

1212
describe('IPFS UnixFS Engine', () => {
13-
const repoExample = path.join(process.cwd(), '/test/test-repo')
14-
const repoTests = path.join(os.tmpdir(), '/unixfs-tests-' + Date.now())
13+
const repoExample = path.join(process.cwd(), 'test', 'test-repo')
14+
const repoTests = path.join(os.tmpdir(), 'unixfs-tests-' + Date.now())
1515

1616
const repo = new IPFSRepo(repoTests)
1717

test/with-dag-api.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ describe('with dag-api', function () {
225225
expect(err).to.not.exist()
226226
expect(nodes.length).to.be.eql(1)
227227
// always yield empty node
228-
expect(mh.toB58String(nodes[0].multihash)).to.be.eql('QmfJMCvenrj4SKKRc48DYPxwVdS44qCUCqqtbqhJuSTWXP')
228+
expect(mh.toB58String(nodes[0].multihash)).to.be.eql('QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH')
229229
done()
230230
}))
231231
})

0 commit comments

Comments
 (0)