cacache 16.0.7 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/lib/content/read.js +74 -89
- package/lib/content/rm.js +9 -9
- package/lib/content/write.js +30 -32
- package/lib/entry-index.js +91 -104
- package/lib/get.js +67 -73
- package/lib/put.js +14 -21
- package/lib/util/fix-owner.js +36 -39
- package/lib/util/move-file.js +35 -48
- package/lib/util/tmp.js +5 -7
- package/lib/verify.js +156 -192
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -413,6 +413,19 @@ with an `EINTEGRITY` error.
|
|
|
413
413
|
|
|
414
414
|
`algorithms` has no effect if this option is present.
|
|
415
415
|
|
|
416
|
+
##### `opts.integrityEmitter`
|
|
417
|
+
|
|
418
|
+
*Streaming only* If present, uses the provided event emitter as a source of
|
|
419
|
+
truth for both integrity and size. This allows use cases where integrity is
|
|
420
|
+
already being calculated outside of cacache to reuse that data instead of
|
|
421
|
+
calculating it a second time.
|
|
422
|
+
|
|
423
|
+
The emitter must emit both the `'integrity'` and `'size'` events.
|
|
424
|
+
|
|
425
|
+
NOTE: If this option is provided, you must verify that you receive the correct
|
|
426
|
+
integrity value yourself and emit an `'error'` event if there is a mismatch.
|
|
427
|
+
[ssri Integrity Streams](https://github.com/npm/ssri#integrity-stream) do this for you when given an expected integrity.
|
|
428
|
+
|
|
416
429
|
##### `opts.algorithms`
|
|
417
430
|
|
|
418
431
|
Default: ['sha512']
|
package/lib/content/read.js
CHANGED
|
@@ -1,42 +1,35 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const fs = require('fs')
|
|
3
|
+
const fs = require('@npmcli/fs')
|
|
6
4
|
const fsm = require('fs-minipass')
|
|
7
5
|
const ssri = require('ssri')
|
|
8
6
|
const contentPath = require('./path')
|
|
9
7
|
const Pipeline = require('minipass-pipeline')
|
|
10
8
|
|
|
11
|
-
const lstat = util.promisify(fs.lstat)
|
|
12
|
-
const readFile = util.promisify(fs.readFile)
|
|
13
|
-
const copyFile = util.promisify(fs.copyFile)
|
|
14
|
-
|
|
15
9
|
module.exports = read
|
|
16
10
|
|
|
17
11
|
const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024
|
|
18
|
-
function read (cache, integrity, opts = {}) {
|
|
12
|
+
async function read (cache, integrity, opts = {}) {
|
|
19
13
|
const { size } = opts
|
|
20
|
-
|
|
14
|
+
const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => {
|
|
21
15
|
// get size
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
const stat = await fs.lstat(cpath)
|
|
17
|
+
return { stat, cpath, sri }
|
|
18
|
+
})
|
|
19
|
+
if (typeof size === 'number' && stat.size !== size) {
|
|
20
|
+
throw sizeError(size, stat.size)
|
|
21
|
+
}
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
if (stat.size > MAX_SINGLE_READ_SIZE) {
|
|
24
|
+
return readPipeline(cpath, stat.size, sri, new Pipeline()).concat()
|
|
25
|
+
}
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
const data = await fs.readFile(cpath, { encoding: null })
|
|
28
|
+
if (!ssri.checkData(data, sri)) {
|
|
29
|
+
throw integrityError(sri, cpath)
|
|
30
|
+
}
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
})
|
|
39
|
-
})
|
|
32
|
+
return data
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
const readPipeline = (cpath, size, sri, stream) => {
|
|
@@ -58,7 +51,7 @@ module.exports.sync = readSync
|
|
|
58
51
|
function readSync (cache, integrity, opts = {}) {
|
|
59
52
|
const { size } = opts
|
|
60
53
|
return withContentSriSync(cache, integrity, (cpath, sri) => {
|
|
61
|
-
const data = fs.readFileSync(cpath)
|
|
54
|
+
const data = fs.readFileSync(cpath, { encoding: null })
|
|
62
55
|
if (typeof size === 'number' && size !== data.length) {
|
|
63
56
|
throw sizeError(size, data.length)
|
|
64
57
|
}
|
|
@@ -77,16 +70,19 @@ module.exports.readStream = readStream
|
|
|
77
70
|
function readStream (cache, integrity, opts = {}) {
|
|
78
71
|
const { size } = opts
|
|
79
72
|
const stream = new Pipeline()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
// Set all this up to run on the stream and then just return the stream
|
|
74
|
+
Promise.resolve().then(async () => {
|
|
75
|
+
const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => {
|
|
76
|
+
// just lstat to ensure it exists
|
|
77
|
+
const stat = await fs.lstat(cpath)
|
|
78
|
+
return { stat, cpath, sri }
|
|
79
|
+
})
|
|
84
80
|
if (typeof size === 'number' && size !== stat.size) {
|
|
85
81
|
return stream.emit('error', sizeError(size, stat.size))
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
readPipeline(cpath, stat.size, sri, stream)
|
|
89
|
-
}
|
|
85
|
+
}).catch(err => stream.emit('error', err))
|
|
90
86
|
|
|
91
87
|
return stream
|
|
92
88
|
}
|
|
@@ -96,7 +92,7 @@ module.exports.copy.sync = copySync
|
|
|
96
92
|
|
|
97
93
|
function copy (cache, integrity, dest) {
|
|
98
94
|
return withContentSri(cache, integrity, (cpath, sri) => {
|
|
99
|
-
return copyFile(cpath, dest)
|
|
95
|
+
return fs.copyFile(cpath, dest)
|
|
100
96
|
})
|
|
101
97
|
}
|
|
102
98
|
|
|
@@ -108,14 +104,17 @@ function copySync (cache, integrity, dest) {
|
|
|
108
104
|
|
|
109
105
|
module.exports.hasContent = hasContent
|
|
110
106
|
|
|
111
|
-
function hasContent (cache, integrity) {
|
|
107
|
+
async function hasContent (cache, integrity) {
|
|
112
108
|
if (!integrity) {
|
|
113
|
-
return
|
|
109
|
+
return false
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
|
|
112
|
+
try {
|
|
113
|
+
return await withContentSri(cache, integrity, async (cpath, sri) => {
|
|
114
|
+
const stat = await fs.lstat(cpath)
|
|
115
|
+
return { size: stat.size, sri, stat }
|
|
116
|
+
})
|
|
117
|
+
} catch (err) {
|
|
119
118
|
if (err.code === 'ENOENT') {
|
|
120
119
|
return false
|
|
121
120
|
}
|
|
@@ -128,7 +127,7 @@ function hasContent (cache, integrity) {
|
|
|
128
127
|
return false
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
|
-
}
|
|
130
|
+
}
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
module.exports.hasContent.sync = hasContentSync
|
|
@@ -159,61 +158,47 @@ function hasContentSync (cache, integrity) {
|
|
|
159
158
|
})
|
|
160
159
|
}
|
|
161
160
|
|
|
162
|
-
function withContentSri (cache, integrity, fn) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (result) {
|
|
193
|
-
return result
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Throw the No matching content found error
|
|
197
|
-
const enoentError = results.find((r) => r.code === 'ENOENT')
|
|
198
|
-
if (enoentError) {
|
|
199
|
-
throw enoentError
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Throw generic error
|
|
203
|
-
throw results.find((r) => r instanceof Error)
|
|
204
|
-
})
|
|
161
|
+
async function withContentSri (cache, integrity, fn) {
|
|
162
|
+
const sri = ssri.parse(integrity)
|
|
163
|
+
// If `integrity` has multiple entries, pick the first digest
|
|
164
|
+
// with available local data.
|
|
165
|
+
const algo = sri.pickAlgorithm()
|
|
166
|
+
const digests = sri[algo]
|
|
167
|
+
|
|
168
|
+
if (digests.length <= 1) {
|
|
169
|
+
const cpath = contentPath(cache, digests[0])
|
|
170
|
+
return fn(cpath, digests[0])
|
|
171
|
+
} else {
|
|
172
|
+
// Can't use race here because a generic error can happen before
|
|
173
|
+
// a ENOENT error, and can happen before a valid result
|
|
174
|
+
const results = await Promise.all(digests.map(async (meta) => {
|
|
175
|
+
try {
|
|
176
|
+
return await withContentSri(cache, meta, fn)
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err.code === 'ENOENT') {
|
|
179
|
+
return Object.assign(
|
|
180
|
+
new Error('No matching content found for ' + sri.toString()),
|
|
181
|
+
{ code: 'ENOENT' }
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
return err
|
|
185
|
+
}
|
|
186
|
+
}))
|
|
187
|
+
// Return the first non error if it is found
|
|
188
|
+
const result = results.find((r) => !(r instanceof Error))
|
|
189
|
+
if (result) {
|
|
190
|
+
return result
|
|
205
191
|
}
|
|
206
|
-
}
|
|
207
192
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.catch(reject)
|
|
213
|
-
} catch (err) {
|
|
214
|
-
reject(err)
|
|
193
|
+
// Throw the No matching content found error
|
|
194
|
+
const enoentError = results.find((r) => r.code === 'ENOENT')
|
|
195
|
+
if (enoentError) {
|
|
196
|
+
throw enoentError
|
|
215
197
|
}
|
|
216
|
-
|
|
198
|
+
|
|
199
|
+
// Throw generic error
|
|
200
|
+
throw results.find((r) => r instanceof Error)
|
|
201
|
+
}
|
|
217
202
|
}
|
|
218
203
|
|
|
219
204
|
function withContentSriSync (cache, integrity, fn) {
|
package/lib/content/rm.js
CHANGED
|
@@ -8,13 +8,13 @@ const rimraf = util.promisify(require('rimraf'))
|
|
|
8
8
|
|
|
9
9
|
module.exports = rm
|
|
10
10
|
|
|
11
|
-
function rm (cache, integrity) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
11
|
+
async function rm (cache, integrity) {
|
|
12
|
+
const content = await hasContent(cache, integrity)
|
|
13
|
+
// ~pretty~ sure we can't end up with a content lacking sri, but be safe
|
|
14
|
+
if (content && content.sri) {
|
|
15
|
+
await rimraf(contentPath(cache, content.sri))
|
|
16
|
+
return true
|
|
17
|
+
} else {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
20
|
}
|
package/lib/content/write.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const events = require('events')
|
|
3
4
|
const util = require('util')
|
|
4
5
|
|
|
5
6
|
const contentPath = require('./path')
|
|
6
7
|
const fixOwner = require('../util/fix-owner')
|
|
7
|
-
const fs = require('fs')
|
|
8
|
+
const fs = require('@npmcli/fs')
|
|
8
9
|
const moveFile = require('../util/move-file')
|
|
9
10
|
const Minipass = require('minipass')
|
|
10
11
|
const Pipeline = require('minipass-pipeline')
|
|
@@ -15,8 +16,6 @@ const ssri = require('ssri')
|
|
|
15
16
|
const uniqueFilename = require('unique-filename')
|
|
16
17
|
const fsm = require('fs-minipass')
|
|
17
18
|
|
|
18
|
-
const writeFile = util.promisify(fs.writeFile)
|
|
19
|
-
|
|
20
19
|
module.exports = write
|
|
21
20
|
|
|
22
21
|
async function write (cache, data, opts = {}) {
|
|
@@ -36,7 +35,7 @@ async function write (cache, data, opts = {}) {
|
|
|
36
35
|
|
|
37
36
|
const tmp = await makeTmp(cache, opts)
|
|
38
37
|
try {
|
|
39
|
-
await writeFile(tmp.target, data, { flag: 'wx' })
|
|
38
|
+
await fs.writeFile(tmp.target, data, { flag: 'wx' })
|
|
40
39
|
await moveToDestination(tmp, cache, sri, opts)
|
|
41
40
|
return { integrity: sri, size: data.length }
|
|
42
41
|
} finally {
|
|
@@ -115,7 +114,21 @@ async function handleContent (inputStream, cache, opts) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
function pipeToTmp (inputStream, cache, tmpTarget, opts) {
|
|
117
|
+
async function pipeToTmp (inputStream, cache, tmpTarget, opts) {
|
|
118
|
+
const outStream = new fsm.WriteStream(tmpTarget, {
|
|
119
|
+
flags: 'wx',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (opts.integrityEmitter) {
|
|
123
|
+
// we need to create these all simultaneously since they can fire in any order
|
|
124
|
+
const [integrity, size] = await Promise.all([
|
|
125
|
+
events.once(opts.integrityEmitter, 'integrity').then(res => res[0]),
|
|
126
|
+
events.once(opts.integrityEmitter, 'size').then(res => res[0]),
|
|
127
|
+
new Pipeline(inputStream, outStream).promise(),
|
|
128
|
+
])
|
|
129
|
+
return { integrity, size }
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
let integrity
|
|
120
133
|
let size
|
|
121
134
|
const hashStream = ssri.integrityStream({
|
|
@@ -130,43 +143,28 @@ function pipeToTmp (inputStream, cache, tmpTarget, opts) {
|
|
|
130
143
|
size = s
|
|
131
144
|
})
|
|
132
145
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// NB: this can throw if the hashStream has a problem with
|
|
138
|
-
// it, and the data is fully written. but pipeToTmp is only
|
|
139
|
-
// called in promisory contexts where that is handled.
|
|
140
|
-
const pipeline = new Pipeline(
|
|
141
|
-
inputStream,
|
|
142
|
-
hashStream,
|
|
143
|
-
outStream
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return pipeline.promise().then(() => ({ integrity, size }))
|
|
146
|
+
const pipeline = new Pipeline(inputStream, hashStream, outStream)
|
|
147
|
+
await pipeline.promise()
|
|
148
|
+
return { integrity, size }
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
function makeTmp (cache, opts) {
|
|
151
|
+
async function makeTmp (cache, opts) {
|
|
150
152
|
const tmpTarget = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
|
|
151
|
-
|
|
153
|
+
await fixOwner.mkdirfix(cache, path.dirname(tmpTarget))
|
|
154
|
+
return {
|
|
152
155
|
target: tmpTarget,
|
|
153
156
|
moved: false,
|
|
154
|
-
}
|
|
157
|
+
}
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
function moveToDestination (tmp, cache, sri, opts) {
|
|
160
|
+
async function moveToDestination (tmp, cache, sri, opts) {
|
|
158
161
|
const destination = contentPath(cache, sri)
|
|
159
162
|
const destDir = path.dirname(destination)
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
})
|
|
166
|
-
.then(() => {
|
|
167
|
-
tmp.moved = true
|
|
168
|
-
return fixOwner.chownr(cache, destination)
|
|
169
|
-
})
|
|
164
|
+
await fixOwner.mkdirfix(cache, destDir)
|
|
165
|
+
await moveFile(tmp.target, destination)
|
|
166
|
+
tmp.moved = true
|
|
167
|
+
await fixOwner.chownr(cache, destination)
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
function sizeError (expected, found) {
|
package/lib/entry-index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const util = require('util')
|
|
4
4
|
const crypto = require('crypto')
|
|
5
|
-
const fs = require('fs')
|
|
5
|
+
const fs = require('@npmcli/fs')
|
|
6
6
|
const Minipass = require('minipass')
|
|
7
7
|
const path = require('path')
|
|
8
8
|
const ssri = require('ssri')
|
|
@@ -17,11 +17,6 @@ const _rimraf = require('rimraf')
|
|
|
17
17
|
const rimraf = util.promisify(_rimraf)
|
|
18
18
|
rimraf.sync = _rimraf.sync
|
|
19
19
|
|
|
20
|
-
const appendFile = util.promisify(fs.appendFile)
|
|
21
|
-
const readFile = util.promisify(fs.readFile)
|
|
22
|
-
const readdir = util.promisify(fs.readdir)
|
|
23
|
-
const writeFile = util.promisify(fs.writeFile)
|
|
24
|
-
|
|
25
20
|
module.exports.NotFoundError = class NotFoundError extends Error {
|
|
26
21
|
constructor (cache, key) {
|
|
27
22
|
super(`No cache entry for ${key} found in ${cache}`)
|
|
@@ -85,7 +80,7 @@ async function compact (cache, key, matchFn, opts = {}) {
|
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
const write = async (tmp) => {
|
|
88
|
-
await writeFile(tmp.target, newIndex, { flag: 'wx' })
|
|
83
|
+
await fs.writeFile(tmp.target, newIndex, { flag: 'wx' })
|
|
89
84
|
await fixOwner.mkdirfix(cache, path.dirname(bucket))
|
|
90
85
|
// we use @npmcli/move-file directly here because we
|
|
91
86
|
// want to overwrite the existing file
|
|
@@ -118,7 +113,7 @@ async function compact (cache, key, matchFn, opts = {}) {
|
|
|
118
113
|
|
|
119
114
|
module.exports.insert = insert
|
|
120
115
|
|
|
121
|
-
function insert (cache, key, integrity, opts = {}) {
|
|
116
|
+
async function insert (cache, key, integrity, opts = {}) {
|
|
122
117
|
const { metadata, size } = opts
|
|
123
118
|
const bucket = bucketPath(cache, key)
|
|
124
119
|
const entry = {
|
|
@@ -128,36 +123,32 @@ function insert (cache, key, integrity, opts = {}) {
|
|
|
128
123
|
size,
|
|
129
124
|
metadata,
|
|
130
125
|
}
|
|
131
|
-
|
|
132
|
-
.mkdirfix(cache, path.dirname(bucket))
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return undefined
|
|
149
|
-
}
|
|
126
|
+
try {
|
|
127
|
+
await fixOwner.mkdirfix(cache, path.dirname(bucket))
|
|
128
|
+
const stringified = JSON.stringify(entry)
|
|
129
|
+
// NOTE - Cleverness ahoy!
|
|
130
|
+
//
|
|
131
|
+
// This works because it's tremendously unlikely for an entry to corrupt
|
|
132
|
+
// another while still preserving the string length of the JSON in
|
|
133
|
+
// question. So, we just slap the length in there and verify it on read.
|
|
134
|
+
//
|
|
135
|
+
// Thanks to @isaacs for the whiteboarding session that ended up with
|
|
136
|
+
// this.
|
|
137
|
+
await fs.appendFile(bucket, `\n${hashEntry(stringified)}\t${stringified}`)
|
|
138
|
+
await fixOwner.chownr(cache, bucket)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err.code === 'ENOENT') {
|
|
141
|
+
return undefined
|
|
142
|
+
}
|
|
150
143
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return formatEntry(cache, entry)
|
|
160
|
-
})
|
|
144
|
+
throw err
|
|
145
|
+
// There's a class of race conditions that happen when things get deleted
|
|
146
|
+
// during fixOwner, or between the two mkdirfix/chownr calls.
|
|
147
|
+
//
|
|
148
|
+
// It's perfectly fine to just not bother in those cases and lie
|
|
149
|
+
// that the index entry was written. Because it's a cache.
|
|
150
|
+
}
|
|
151
|
+
return formatEntry(cache, entry)
|
|
161
152
|
}
|
|
162
153
|
|
|
163
154
|
module.exports.insert.sync = insertSync
|
|
@@ -187,25 +178,24 @@ function insertSync (cache, key, integrity, opts = {}) {
|
|
|
187
178
|
|
|
188
179
|
module.exports.find = find
|
|
189
180
|
|
|
190
|
-
function find (cache, key) {
|
|
181
|
+
async function find (cache, key) {
|
|
191
182
|
const bucket = bucketPath(cache, key)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
} else {
|
|
198
|
-
return latest
|
|
199
|
-
}
|
|
200
|
-
}, null)
|
|
201
|
-
})
|
|
202
|
-
.catch((err) => {
|
|
203
|
-
if (err.code === 'ENOENT') {
|
|
204
|
-
return null
|
|
183
|
+
try {
|
|
184
|
+
const entries = await bucketEntries(bucket)
|
|
185
|
+
return entries.reduce((latest, next) => {
|
|
186
|
+
if (next && next.key === key) {
|
|
187
|
+
return formatEntry(cache, next)
|
|
205
188
|
} else {
|
|
206
|
-
|
|
189
|
+
return latest
|
|
207
190
|
}
|
|
208
|
-
})
|
|
191
|
+
}, null)
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.code === 'ENOENT') {
|
|
194
|
+
return null
|
|
195
|
+
} else {
|
|
196
|
+
throw err
|
|
197
|
+
}
|
|
198
|
+
}
|
|
209
199
|
}
|
|
210
200
|
|
|
211
201
|
module.exports.find.sync = findSync
|
|
@@ -257,67 +247,64 @@ function lsStream (cache) {
|
|
|
257
247
|
const indexDir = bucketDir(cache)
|
|
258
248
|
const stream = new Minipass({ objectMode: true })
|
|
259
249
|
|
|
260
|
-
|
|
261
|
-
|
|
250
|
+
// Set all this up to run on the stream and then just return the stream
|
|
251
|
+
Promise.resolve().then(async () => {
|
|
252
|
+
const buckets = await readdirOrEmpty(indexDir)
|
|
253
|
+
await Promise.all(buckets.map(async (bucket) => {
|
|
262
254
|
const bucketPath = path.join(indexDir, bucket)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
))
|
|
298
|
-
.then(
|
|
299
|
-
() => stream.end(),
|
|
300
|
-
err => stream.emit('error', err)
|
|
301
|
-
)
|
|
255
|
+
const subbuckets = await readdirOrEmpty(bucketPath)
|
|
256
|
+
await Promise.all(subbuckets.map(async (subbucket) => {
|
|
257
|
+
const subbucketPath = path.join(bucketPath, subbucket)
|
|
258
|
+
|
|
259
|
+
// "/cachename/<bucket 0xFF>/<bucket 0xFF>./*"
|
|
260
|
+
const subbucketEntries = await readdirOrEmpty(subbucketPath)
|
|
261
|
+
await Promise.all(subbucketEntries.map(async (entry) => {
|
|
262
|
+
const entryPath = path.join(subbucketPath, entry)
|
|
263
|
+
try {
|
|
264
|
+
const entries = await bucketEntries(entryPath)
|
|
265
|
+
// using a Map here prevents duplicate keys from showing up
|
|
266
|
+
// twice, I guess?
|
|
267
|
+
const reduced = entries.reduce((acc, entry) => {
|
|
268
|
+
acc.set(entry.key, entry)
|
|
269
|
+
return acc
|
|
270
|
+
}, new Map())
|
|
271
|
+
// reduced is a map of key => entry
|
|
272
|
+
for (const entry of reduced.values()) {
|
|
273
|
+
const formatted = formatEntry(cache, entry)
|
|
274
|
+
if (formatted) {
|
|
275
|
+
stream.write(formatted)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err.code === 'ENOENT') {
|
|
280
|
+
return undefined
|
|
281
|
+
}
|
|
282
|
+
throw err
|
|
283
|
+
}
|
|
284
|
+
}))
|
|
285
|
+
}))
|
|
286
|
+
}))
|
|
287
|
+
stream.end()
|
|
288
|
+
}).catch(err => stream.emit('error', err))
|
|
302
289
|
|
|
303
290
|
return stream
|
|
304
291
|
}
|
|
305
292
|
|
|
306
293
|
module.exports.ls = ls
|
|
307
294
|
|
|
308
|
-
function ls (cache) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
295
|
+
async function ls (cache) {
|
|
296
|
+
const entries = await lsStream(cache).collect()
|
|
297
|
+
return entries.reduce((acc, xs) => {
|
|
298
|
+
acc[xs.key] = xs
|
|
299
|
+
return acc
|
|
300
|
+
}, {})
|
|
315
301
|
}
|
|
316
302
|
|
|
317
303
|
module.exports.bucketEntries = bucketEntries
|
|
318
304
|
|
|
319
|
-
function bucketEntries (bucket, filter) {
|
|
320
|
-
|
|
305
|
+
async function bucketEntries (bucket, filter) {
|
|
306
|
+
const data = await fs.readFile(bucket, 'utf8')
|
|
307
|
+
return _bucketEntries(data, filter)
|
|
321
308
|
}
|
|
322
309
|
|
|
323
310
|
module.exports.bucketEntries.sync = bucketEntriesSync
|
|
@@ -406,7 +393,7 @@ function formatEntry (cache, entry, keepAll) {
|
|
|
406
393
|
}
|
|
407
394
|
|
|
408
395
|
function readdirOrEmpty (dir) {
|
|
409
|
-
return readdir(dir).catch((err) => {
|
|
396
|
+
return fs.readdir(dir).catch((err) => {
|
|
410
397
|
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
|
|
411
398
|
return []
|
|
412
399
|
}
|