Spaces:
Sleeping
Sleeping
const fs = require('fs/promises') | |
const fsm = require('fs-minipass') | |
const ssri = require('ssri') | |
const contentPath = require('./path') | |
const Pipeline = require('minipass-pipeline') | |
module.exports = read | |
const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024 | |
async function read (cache, integrity, opts = {}) { | |
const { size } = opts | |
const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => { | |
// get size | |
const stat = await fs.stat(cpath) | |
return { stat, cpath, sri } | |
}) | |
if (typeof size === 'number' && stat.size !== size) { | |
throw sizeError(size, stat.size) | |
} | |
if (stat.size > MAX_SINGLE_READ_SIZE) { | |
return readPipeline(cpath, stat.size, sri, new Pipeline()).concat() | |
} | |
const data = await fs.readFile(cpath, { encoding: null }) | |
if (!ssri.checkData(data, sri)) { | |
throw integrityError(sri, cpath) | |
} | |
return data | |
} | |
const readPipeline = (cpath, size, sri, stream) => { | |
stream.push( | |
new fsm.ReadStream(cpath, { | |
size, | |
readSize: MAX_SINGLE_READ_SIZE, | |
}), | |
ssri.integrityStream({ | |
integrity: sri, | |
size, | |
}) | |
) | |
return stream | |
} | |
module.exports.stream = readStream | |
module.exports.readStream = readStream | |
function readStream (cache, integrity, opts = {}) { | |
const { size } = opts | |
const stream = new Pipeline() | |
// Set all this up to run on the stream and then just return the stream | |
Promise.resolve().then(async () => { | |
const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => { | |
// just stat to ensure it exists | |
const stat = await fs.stat(cpath) | |
return { stat, cpath, sri } | |
}) | |
if (typeof size === 'number' && size !== stat.size) { | |
return stream.emit('error', sizeError(size, stat.size)) | |
} | |
return readPipeline(cpath, stat.size, sri, stream) | |
}).catch(err => stream.emit('error', err)) | |
return stream | |
} | |
module.exports.copy = copy | |
function copy (cache, integrity, dest) { | |
return withContentSri(cache, integrity, (cpath, sri) => { | |
return fs.copyFile(cpath, dest) | |
}) | |
} | |
module.exports.hasContent = hasContent | |
async function hasContent (cache, integrity) { | |
if (!integrity) { | |
return false | |
} | |
try { | |
return await withContentSri(cache, integrity, async (cpath, sri) => { | |
const stat = await fs.stat(cpath) | |
return { size: stat.size, sri, stat } | |
}) | |
} catch (err) { | |
if (err.code === 'ENOENT') { | |
return false | |
} | |
if (err.code === 'EPERM') { | |
/* istanbul ignore else */ | |
if (process.platform !== 'win32') { | |
throw err | |
} else { | |
return false | |
} | |
} | |
} | |
} | |
async function withContentSri (cache, integrity, fn) { | |
const sri = ssri.parse(integrity) | |
// If `integrity` has multiple entries, pick the first digest | |
// with available local data. | |
const algo = sri.pickAlgorithm() | |
const digests = sri[algo] | |
if (digests.length <= 1) { | |
const cpath = contentPath(cache, digests[0]) | |
return fn(cpath, digests[0]) | |
} else { | |
// Can't use race here because a generic error can happen before | |
// a ENOENT error, and can happen before a valid result | |
const results = await Promise.all(digests.map(async (meta) => { | |
try { | |
return await withContentSri(cache, meta, fn) | |
} catch (err) { | |
if (err.code === 'ENOENT') { | |
return Object.assign( | |
new Error('No matching content found for ' + sri.toString()), | |
{ code: 'ENOENT' } | |
) | |
} | |
return err | |
} | |
})) | |
// Return the first non error if it is found | |
const result = results.find((r) => !(r instanceof Error)) | |
if (result) { | |
return result | |
} | |
// Throw the No matching content found error | |
const enoentError = results.find((r) => r.code === 'ENOENT') | |
if (enoentError) { | |
throw enoentError | |
} | |
// Throw generic error | |
throw results.find((r) => r instanceof Error) | |
} | |
} | |
function sizeError (expected, found) { | |
/* eslint-disable-next-line max-len */ | |
const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`) | |
err.expected = expected | |
err.found = found | |
err.code = 'EBADSIZE' | |
return err | |
} | |
function integrityError (sri, path) { | |
const err = new Error(`Integrity verification failed for ${sri} (${path})`) | |
err.code = 'EINTEGRITY' | |
err.sri = sri | |
err.path = path | |
return err | |
} | |