|
|
|
@ -2,12 +2,17 @@ import path from 'path';
|
|
|
|
|
import gunzip from 'gunzip-maybe';
|
|
|
|
|
import tar from 'tar-stream';
|
|
|
|
|
|
|
|
|
|
import addLeadingSlash from '../utils/addLeadingSlash.js';
|
|
|
|
|
import createPackageURL from '../utils/createPackageURL.js';
|
|
|
|
|
import createSearch from '../utils/createSearch.js';
|
|
|
|
|
import { getPackage } from '../utils/npm.js';
|
|
|
|
|
import getIntegrity from '../utils/getIntegrity.js';
|
|
|
|
|
import getContentType from '../utils/getContentType.js';
|
|
|
|
|
import bufferStream from '../utils/bufferStream.js';
|
|
|
|
|
|
|
|
|
|
const leadingSlashes = /^\/*/;
|
|
|
|
|
const multipleSlashes = /\/*/;
|
|
|
|
|
const trailingSlashes = /\/*$/;
|
|
|
|
|
const leadingSegment = /^[^/]+\/?/;
|
|
|
|
|
|
|
|
|
|
function fileRedirect(req, res, entry) {
|
|
|
|
|
// Redirect to the file with the extension so it's more
|
|
|
|
@ -22,7 +27,7 @@ function fileRedirect(req, res, entry) {
|
|
|
|
|
createPackageURL(
|
|
|
|
|
req.packageName,
|
|
|
|
|
req.packageVersion,
|
|
|
|
|
addLeadingSlash(entry.name),
|
|
|
|
|
entry.name.replace(leadingSlashes, '/'),
|
|
|
|
|
createSearch(req.query)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
@ -41,23 +46,19 @@ function indexRedirect(req, res, entry) {
|
|
|
|
|
createPackageURL(
|
|
|
|
|
req.packageName,
|
|
|
|
|
req.packageVersion,
|
|
|
|
|
addLeadingSlash(entry.name),
|
|
|
|
|
entry.name.replace(leadingSlashes, '/'),
|
|
|
|
|
createSearch(req.query)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stripLeadingSegment(name) {
|
|
|
|
|
return name.replace(/^[^/]+\/?/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Search the given tarball for entries that match the given name.
|
|
|
|
|
* Follows node's resolution algorithm.
|
|
|
|
|
* https://nodejs.org/api/modules.html#modules_all_together
|
|
|
|
|
*/
|
|
|
|
|
function searchEntries(stream, entryName, wantsIndex) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
return new Promise((accept, reject) => {
|
|
|
|
|
const jsEntryName = `${entryName}.js`;
|
|
|
|
|
const jsonEntryName = `${entryName}.json`;
|
|
|
|
|
const entries = {};
|
|
|
|
@ -72,13 +73,13 @@ function searchEntries(stream, entryName, wantsIndex) {
|
|
|
|
|
.pipe(gunzip())
|
|
|
|
|
.pipe(tar.extract())
|
|
|
|
|
.on('error', reject)
|
|
|
|
|
.on('entry', (header, stream, next) => {
|
|
|
|
|
.on('entry', async (header, stream, next) => {
|
|
|
|
|
const entry = {
|
|
|
|
|
// Most packages have header names that look like `package/index.js`
|
|
|
|
|
// so we shorten that to just `index.js` here. A few packages use a
|
|
|
|
|
// prefix other than `package/`. e.g. the firebase package uses the
|
|
|
|
|
// `firebase_npm/` prefix. So we just strip the first dir name.
|
|
|
|
|
name: stripLeadingSegment(header.name),
|
|
|
|
|
name: header.name.replace(leadingSegment, ''),
|
|
|
|
|
type: header.type
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -124,33 +125,25 @@ function searchEntries(stream, entryName, wantsIndex) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chunks = [];
|
|
|
|
|
const content = await bufferStream(stream);
|
|
|
|
|
|
|
|
|
|
stream
|
|
|
|
|
.on('data', chunk => {
|
|
|
|
|
chunks.push(chunk);
|
|
|
|
|
})
|
|
|
|
|
.on('end', () => {
|
|
|
|
|
const content = Buffer.concat(chunks);
|
|
|
|
|
// Set some extra properties for files that we will
|
|
|
|
|
// need to serve them and for ?meta listings.
|
|
|
|
|
entry.contentType = getContentType(entry.name);
|
|
|
|
|
entry.integrity = getIntegrity(content);
|
|
|
|
|
entry.lastModified = header.mtime.toUTCString();
|
|
|
|
|
entry.size = content.length;
|
|
|
|
|
|
|
|
|
|
// Set some extra properties for files that we will
|
|
|
|
|
// need to serve them and for ?meta listings.
|
|
|
|
|
entry.contentType = getContentType(entry.name);
|
|
|
|
|
entry.integrity = getIntegrity(content);
|
|
|
|
|
entry.lastModified = header.mtime.toUTCString();
|
|
|
|
|
entry.size = content.length;
|
|
|
|
|
// Set the content only for the foundEntry and
|
|
|
|
|
// discard the buffer for all others.
|
|
|
|
|
if (entry === foundEntry) {
|
|
|
|
|
entry.content = content;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the content only for the foundEntry and
|
|
|
|
|
// discard the buffer for all others.
|
|
|
|
|
if (entry === foundEntry) {
|
|
|
|
|
entry.content = content;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
})
|
|
|
|
|
.on('finish', () => {
|
|
|
|
|
resolve({
|
|
|
|
|
accept({
|
|
|
|
|
entries,
|
|
|
|
|
// If we didn't find a matching file entry,
|
|
|
|
|
// try a directory entry with the same name.
|
|
|
|
@ -160,22 +153,18 @@ function searchEntries(stream, entryName, wantsIndex) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const leadingSlash = /^\//;
|
|
|
|
|
const multipleSlash = /\/\/+/;
|
|
|
|
|
const trailingSlash = /\/$/;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch and search the archive to try and find the requested file.
|
|
|
|
|
* Redirect to the "index" file if a directory was requested.
|
|
|
|
|
*/
|
|
|
|
|
export default async function findFile(req, res, next) {
|
|
|
|
|
const wantsIndex = trailingSlash.test(req.filename);
|
|
|
|
|
const wantsIndex = req.filename.endsWith('/');
|
|
|
|
|
|
|
|
|
|
// The name of the file/directory we're looking for.
|
|
|
|
|
const entryName = req.filename
|
|
|
|
|
.replace(multipleSlash, '/')
|
|
|
|
|
.replace(trailingSlash, '')
|
|
|
|
|
.replace(leadingSlash, '');
|
|
|
|
|
.replace(multipleSlashes, '/')
|
|
|
|
|
.replace(trailingSlashes, '')
|
|
|
|
|
.replace(leadingSlashes, '');
|
|
|
|
|
|
|
|
|
|
const stream = await getPackage(req.packageName, req.packageVersion);
|
|
|
|
|
const { entries, foundEntry } = await searchEntries(
|
|
|
|
|