mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-20 22:52:46 -05:00
5f3eee5eaf
* chore: remove a package, move constants * chore: remove *
960 lines
28 KiB
TypeScript
960 lines
28 KiB
TypeScript
import assert from 'assert';
|
|
import UrlNode from 'url';
|
|
import _ from 'lodash';
|
|
import buildDebug from 'debug';
|
|
|
|
import { ErrorCode, isObject, getLatestVersion, tagVersion, validateName } from '@verdaccio/utils';
|
|
import { API_ERROR, DIST_TAGS, HTTP_STATUS, SUPPORT_ERRORS, USERS } from '@verdaccio/commons-api';
|
|
import { createTarballHash } from '@verdaccio/utils';
|
|
import { loadPlugin } from '@verdaccio/loaders';
|
|
import LocalDatabase from '@verdaccio/local-storage';
|
|
import { UploadTarball, ReadTarball } from '@verdaccio/streams';
|
|
import {
|
|
Token,
|
|
TokenFilter,
|
|
Package,
|
|
Config,
|
|
IUploadTarball,
|
|
IReadTarball,
|
|
MergeTags,
|
|
Version,
|
|
DistFile,
|
|
Callback,
|
|
Logger,
|
|
IPluginStorage,
|
|
IPackageStorage,
|
|
Author,
|
|
CallbackAction,
|
|
onSearchPackage,
|
|
StringValue,
|
|
onEndSearchPackage,
|
|
StorageUpdateCallback,
|
|
} from '@verdaccio/types';
|
|
import { VerdaccioError } from '@verdaccio/commons-api';
|
|
import { IStorage } from './storage';
|
|
|
|
import {
|
|
prepareSearchPackage,
|
|
generatePackageTemplate,
|
|
normalizePackage,
|
|
generateRevision,
|
|
getLatestReadme,
|
|
cleanUpReadme,
|
|
normalizeContributors,
|
|
STORAGE,
|
|
} from './storage-utils';
|
|
|
|
const debug = buildDebug('verdaccio:storage:local');
|
|
|
|
/**
|
|
* Implements Storage interface (same for storage.js, local-storage.js, up-storage.js).
|
|
*/
|
|
class LocalStorage implements IStorage {
|
|
public config: Config;
|
|
public storagePlugin: IPluginStorage<Config>;
|
|
public logger: Logger;
|
|
|
|
public constructor(config: Config, logger: Logger) {
|
|
debug('local storage created');
|
|
this.logger = logger.child({ sub: 'fs' });
|
|
this.config = config;
|
|
this.storagePlugin = this._loadStorage(config, logger);
|
|
}
|
|
|
|
public addPackage(name: string, pkg: Package, callback: Callback): void {
|
|
debug(`creating a package for`, name);
|
|
const storage: any = this._getLocalStorage(name);
|
|
|
|
if (_.isNil(storage)) {
|
|
debug(`storage is missing for %o package cannot be added`, name);
|
|
return callback(ErrorCode.getNotFound('this package cannot be added'));
|
|
}
|
|
|
|
storage.createPackage(name, generatePackageTemplate(name), (err) => {
|
|
if (
|
|
_.isNull(err) === false &&
|
|
(err.code === STORAGE.FILE_EXIST_ERROR || err.code === HTTP_STATUS.CONFLICT)
|
|
) {
|
|
debug(`error on creating a package for %o with error %o`, name, err.message);
|
|
return callback(ErrorCode.getConflict());
|
|
}
|
|
|
|
const latest = getLatestVersion(pkg);
|
|
if (_.isNil(latest) === false && pkg.versions[latest]) {
|
|
debug('latest version found %o for %o', latest, name);
|
|
return callback(null, pkg.versions[latest]);
|
|
}
|
|
|
|
debug('no latest version found for %o', name);
|
|
return callback();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove package.
|
|
* @param {*} name
|
|
* @param {*} callback
|
|
* @return {Function}
|
|
*/
|
|
public removePackage(name: string, callback: Callback): void {
|
|
const storage: any = this._getLocalStorage(name);
|
|
debug(`removing package for %o`, name);
|
|
|
|
if (_.isNil(storage)) {
|
|
return callback(ErrorCode.getNotFound());
|
|
}
|
|
|
|
storage.readPackage(name, (err, data: Package): void => {
|
|
if (_.isNil(err) === false) {
|
|
if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
|
|
debug(`error on not found %o with error %o`, name, err.message);
|
|
return callback(ErrorCode.getNotFound());
|
|
}
|
|
return callback(err);
|
|
}
|
|
|
|
data = normalizePackage(data);
|
|
|
|
this.storagePlugin.remove(name, (removeFailed: Error): void => {
|
|
if (removeFailed) {
|
|
// This will happen when database is locked
|
|
debug(`the database is locked, removed has failed for %o`, name);
|
|
|
|
return callback(ErrorCode.getBadData(removeFailed.message));
|
|
}
|
|
|
|
storage.deletePackage(STORAGE.PACKAGE_FILE_NAME, (err): void => {
|
|
if (err) {
|
|
debug(`error on delete a package %o with error %o`, name, err.message);
|
|
return callback(err);
|
|
}
|
|
const attachments = Object.keys(data._attachments);
|
|
|
|
this._deleteAttachments(storage, attachments, callback);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Synchronize remote package info with the local one
|
|
* @param {*} name
|
|
* @param {*} packageInfo
|
|
* @param {*} callback
|
|
*/
|
|
public updateVersions(name: string, packageInfo: Package, callback: Callback): void {
|
|
debug(`updating versions for package %o`, name);
|
|
this._readCreatePackage(name, (err, packageLocalJson): void => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
let change = false;
|
|
// updating readme
|
|
packageLocalJson.readme = getLatestReadme(packageInfo);
|
|
if (packageInfo.readme !== packageLocalJson.readme) {
|
|
change = true;
|
|
}
|
|
|
|
debug('update versions');
|
|
for (const versionId in packageInfo.versions) {
|
|
if (_.isNil(packageLocalJson.versions[versionId])) {
|
|
let version = packageInfo.versions[versionId];
|
|
|
|
// we don't keep readme for package versions,
|
|
// only one readme per package
|
|
version = cleanUpReadme(version);
|
|
debug('clean up readme for %o', versionId);
|
|
version.contributors = normalizeContributors(version.contributors as Author[]);
|
|
|
|
change = true;
|
|
packageLocalJson.versions[versionId] = version;
|
|
|
|
if (version?.dist?.tarball) {
|
|
const urlObject: any = UrlNode.parse(version.dist.tarball);
|
|
const filename = urlObject.pathname.replace(/^.*\//, '');
|
|
|
|
// we do NOT overwrite any existing records
|
|
if (_.isNil(packageLocalJson._distfiles[filename])) {
|
|
const hash: DistFile = (packageLocalJson._distfiles[filename] = {
|
|
url: version.dist.tarball,
|
|
sha: version.dist.shasum,
|
|
});
|
|
const upLink: string = version[Symbol.for('__verdaccio_uplink')];
|
|
|
|
if (_.isNil(upLink) === false) {
|
|
this._updateUplinkToRemoteProtocol(hash, upLink);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
debug('update dist-tags');
|
|
for (const tag in packageInfo[DIST_TAGS]) {
|
|
if (
|
|
!packageLocalJson[DIST_TAGS][tag] ||
|
|
packageLocalJson[DIST_TAGS][tag] !== packageInfo[DIST_TAGS][tag]
|
|
) {
|
|
change = true;
|
|
packageLocalJson[DIST_TAGS][tag] = packageInfo[DIST_TAGS][tag];
|
|
}
|
|
}
|
|
|
|
for (const up in packageInfo._uplinks) {
|
|
if (Object.prototype.hasOwnProperty.call(packageInfo._uplinks, up)) {
|
|
const need_change =
|
|
!isObject(packageLocalJson._uplinks[up]) ||
|
|
packageInfo._uplinks[up].etag !== packageLocalJson._uplinks[up].etag ||
|
|
packageInfo._uplinks[up].fetched !== packageLocalJson._uplinks[up].fetched;
|
|
|
|
if (need_change) {
|
|
change = true;
|
|
packageLocalJson._uplinks[up] = packageInfo._uplinks[up];
|
|
}
|
|
}
|
|
}
|
|
|
|
debug('update time');
|
|
if ('time' in packageInfo && !_.isEqual(packageLocalJson.time, packageInfo.time)) {
|
|
packageLocalJson.time = packageInfo.time;
|
|
change = true;
|
|
}
|
|
|
|
if (change) {
|
|
debug('updating package info %o', name);
|
|
this._writePackage(name, packageLocalJson, function (err): void {
|
|
callback(err, packageLocalJson);
|
|
});
|
|
} else {
|
|
callback(null, packageLocalJson);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a new version to a previous local package.
|
|
* @param {*} name
|
|
* @param {*} version
|
|
* @param {*} metadata
|
|
* @param {*} tag
|
|
* @param {*} callback
|
|
*/
|
|
public addVersion(
|
|
name: string,
|
|
version: string,
|
|
metadata: Version,
|
|
tag: StringValue,
|
|
callback: CallbackAction
|
|
): void {
|
|
debug(`add version package for`, name);
|
|
this._updatePackage(
|
|
name,
|
|
(data, cb: Callback): void => {
|
|
// keep only one readme per package
|
|
data.readme = metadata.readme;
|
|
|
|
// TODO: lodash remove
|
|
metadata = cleanUpReadme(metadata);
|
|
metadata.contributors = normalizeContributors(metadata.contributors as Author[]);
|
|
|
|
const hasVersion = data.versions[version] != null;
|
|
if (hasVersion) {
|
|
return cb(ErrorCode.getConflict());
|
|
}
|
|
|
|
// if uploaded tarball has a different shasum, it's very likely that we
|
|
// have some kind of error
|
|
if (isObject(metadata.dist) && _.isString(metadata.dist.tarball)) {
|
|
const tarball = metadata.dist.tarball.replace(/.*\//, '');
|
|
|
|
if (isObject(data._attachments[tarball])) {
|
|
if (
|
|
_.isNil(data._attachments[tarball].shasum) === false &&
|
|
_.isNil(metadata.dist.shasum) === false
|
|
) {
|
|
if (data._attachments[tarball].shasum != metadata.dist.shasum) {
|
|
const errorMessage =
|
|
`shasum error, ` +
|
|
`${data._attachments[tarball].shasum} != ${metadata.dist.shasum}`;
|
|
return cb(ErrorCode.getBadRequest(errorMessage));
|
|
}
|
|
}
|
|
|
|
const currentDate = new Date().toISOString();
|
|
|
|
// some old storage do not have this field #740
|
|
if (_.isNil(data.time)) {
|
|
data.time = {};
|
|
}
|
|
|
|
data.time['modified'] = currentDate;
|
|
|
|
if ('created' in data.time === false) {
|
|
data.time.created = currentDate;
|
|
}
|
|
|
|
data.time[version] = currentDate;
|
|
data._attachments[tarball].version = version;
|
|
}
|
|
}
|
|
|
|
data.versions[version] = metadata;
|
|
tagVersion(data, version, tag);
|
|
|
|
this.storagePlugin.add(name, (addFailed): void => {
|
|
if (addFailed) {
|
|
return cb(ErrorCode.getBadData(addFailed.message));
|
|
}
|
|
|
|
cb();
|
|
});
|
|
},
|
|
callback
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Merge a new list of tags for a local packages with the existing one.
|
|
* @param {*} pkgName
|
|
* @param {*} tags
|
|
* @param {*} callback
|
|
*/
|
|
public mergeTags(pkgName: string, tags: MergeTags, callback: CallbackAction): void {
|
|
debug(`merge tags for`, pkgName);
|
|
this._updatePackage(
|
|
pkgName,
|
|
(data, cb): void => {
|
|
/* eslint guard-for-in: 0 */
|
|
for (const tag in tags) {
|
|
// this handle dist-tag rm command
|
|
if (_.isNull(tags[tag])) {
|
|
delete data[DIST_TAGS][tag];
|
|
continue;
|
|
}
|
|
|
|
if (_.isNil(data.versions[tags[tag]])) {
|
|
return cb(this._getVersionNotFound());
|
|
}
|
|
const version: string = tags[tag];
|
|
tagVersion(data, version, tag);
|
|
}
|
|
cb(null);
|
|
},
|
|
callback
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return version not found
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
private _getVersionNotFound(): VerdaccioError {
|
|
return ErrorCode.getNotFound(API_ERROR.VERSION_NOT_EXIST);
|
|
}
|
|
|
|
/**
|
|
* Return file no available
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
private _getFileNotAvailable(): VerdaccioError {
|
|
return ErrorCode.getNotFound('no such file available');
|
|
}
|
|
|
|
/**
|
|
* Update the package metadata, tags and attachments (tarballs).
|
|
* Note: Currently supports unpublishing and deprecation.
|
|
* @param {*} name
|
|
* @param {*} incomingPkg
|
|
* @param {*} revision
|
|
* @param {*} callback
|
|
* @return {Function}
|
|
*/
|
|
public changePackage(
|
|
name: string,
|
|
incomingPkg: Package,
|
|
revision: string | void,
|
|
callback: Callback
|
|
): void {
|
|
debug(`change package tags for %o revision`, name);
|
|
if (!isObject(incomingPkg.versions) || !isObject(incomingPkg[DIST_TAGS])) {
|
|
debug(`change package bad data for %o`, name);
|
|
return callback(ErrorCode.getBadData());
|
|
}
|
|
|
|
debug(`change package udapting package for %o`, name);
|
|
this._updatePackage(
|
|
name,
|
|
(localData: Package, cb: CallbackAction): void => {
|
|
for (const version in localData.versions) {
|
|
const incomingVersion = incomingPkg.versions[version];
|
|
if (_.isNil(incomingVersion)) {
|
|
this.logger.info({ name: name, version: version }, 'unpublishing @{name}@@{version}');
|
|
|
|
// FIXME: I prefer return a new object rather mutate the metadata
|
|
delete localData.versions[version];
|
|
delete localData.time![version];
|
|
|
|
for (const file in localData._attachments) {
|
|
if (localData._attachments[file].version === version) {
|
|
delete localData._attachments[file].version;
|
|
}
|
|
}
|
|
} else if (Object.prototype.hasOwnProperty.call(incomingVersion, 'deprecated')) {
|
|
const incomingDeprecated = incomingVersion.deprecated;
|
|
if (incomingDeprecated != localData.versions[version].deprecated) {
|
|
if (!incomingDeprecated) {
|
|
this.logger.info(
|
|
{ name: name, version: version },
|
|
'undeprecating @{name}@@{version}'
|
|
);
|
|
delete localData.versions[version].deprecated;
|
|
} else {
|
|
this.logger.info(
|
|
{ name: name, version: version },
|
|
'deprecating @{name}@@{version}'
|
|
);
|
|
localData.versions[version].deprecated = incomingDeprecated;
|
|
}
|
|
localData.time!.modified = new Date().toISOString();
|
|
}
|
|
}
|
|
}
|
|
|
|
localData[USERS] = incomingPkg[USERS];
|
|
localData[DIST_TAGS] = incomingPkg[DIST_TAGS];
|
|
cb(null);
|
|
},
|
|
function (err): void {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
callback();
|
|
}
|
|
);
|
|
}
|
|
/**
|
|
* Remove a tarball.
|
|
* @param {*} name
|
|
* @param {*} filename
|
|
* @param {*} revision
|
|
* @param {*} callback
|
|
*/
|
|
public removeTarball(
|
|
name: string,
|
|
filename: string,
|
|
revision: string,
|
|
callback: CallbackAction
|
|
): void {
|
|
assert(validateName(filename));
|
|
|
|
this._updatePackage(
|
|
name,
|
|
(data, cb): void => {
|
|
if (data._attachments[filename]) {
|
|
delete data._attachments[filename];
|
|
cb(null);
|
|
} else {
|
|
cb(this._getFileNotAvailable());
|
|
}
|
|
},
|
|
(err: VerdaccioError): void => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
const storage = this._getLocalStorage(name);
|
|
|
|
if (storage) {
|
|
storage.deletePackage(filename, callback);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add a tarball.
|
|
* @param {String} name
|
|
* @param {String} filename
|
|
* @return {Stream}
|
|
*/
|
|
public addTarball(name: string, filename: string): IUploadTarball {
|
|
debug(`add a tarball for %o`, name);
|
|
assert(validateName(filename));
|
|
|
|
let length = 0;
|
|
const shaOneHash = createTarballHash();
|
|
const uploadStream: IUploadTarball = new UploadTarball({});
|
|
const _transform = uploadStream._transform;
|
|
const storage = this._getLocalStorage(name);
|
|
|
|
uploadStream.abort = function (): void {};
|
|
uploadStream.done = function (): void {};
|
|
|
|
uploadStream._transform = function (data, ...args): void {
|
|
shaOneHash.update(data);
|
|
// measure the length for validation reasons
|
|
length += data.length;
|
|
const appliedData = [data, ...args];
|
|
// FIXME: not sure about this approach, tsc complains
|
|
// @ts-ignore
|
|
_transform.apply(uploadStream, appliedData);
|
|
};
|
|
|
|
if (name === '__proto__') {
|
|
process.nextTick((): void => {
|
|
uploadStream.emit('error', ErrorCode.getForbidden());
|
|
});
|
|
return uploadStream;
|
|
}
|
|
|
|
if (!storage) {
|
|
process.nextTick((): void => {
|
|
uploadStream.emit('error', "can't upload this package");
|
|
});
|
|
return uploadStream;
|
|
}
|
|
|
|
const writeStream: IUploadTarball = storage.writeTarball(filename);
|
|
|
|
writeStream.on('error', (err) => {
|
|
// @ts-ignore
|
|
if (err.code === STORAGE.FILE_EXIST_ERROR || err.code === HTTP_STATUS.CONFLICT) {
|
|
uploadStream.emit('error', ErrorCode.getConflict());
|
|
uploadStream.abort();
|
|
// @ts-ignore
|
|
} else if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
|
|
// check if package exists to throw an appropriate message
|
|
this.getPackageMetadata(name, function (_err: VerdaccioError, _res: Package): void {
|
|
if (_err) {
|
|
uploadStream.emit('error', _err);
|
|
} else {
|
|
uploadStream.emit('error', err);
|
|
}
|
|
});
|
|
} else {
|
|
uploadStream.emit('error', err);
|
|
}
|
|
});
|
|
|
|
writeStream.on('open', function (): void {
|
|
// re-emitting open because it's handled in storage.js
|
|
uploadStream.emit('open');
|
|
});
|
|
|
|
writeStream.on('success', (): void => {
|
|
this._updatePackage(
|
|
name,
|
|
function updater(data, cb): void {
|
|
data._attachments[filename] = {
|
|
shasum: shaOneHash.digest('hex'),
|
|
};
|
|
cb(null);
|
|
},
|
|
function (err): void {
|
|
if (err) {
|
|
uploadStream.emit('error', err);
|
|
} else {
|
|
uploadStream.emit('success');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
uploadStream.abort = function (): void {
|
|
writeStream.abort();
|
|
};
|
|
|
|
uploadStream.done = function (): void {
|
|
if (!length) {
|
|
uploadStream.emit('error', ErrorCode.getBadData('refusing to accept zero-length file'));
|
|
writeStream.abort();
|
|
} else {
|
|
writeStream.done();
|
|
}
|
|
};
|
|
|
|
uploadStream.pipe(writeStream);
|
|
|
|
return uploadStream;
|
|
}
|
|
|
|
/**
|
|
* Get a tarball.
|
|
* @param {*} name
|
|
* @param {*} filename
|
|
* @return {ReadTarball}
|
|
*/
|
|
public getTarball(name: string, filename: string): IReadTarball {
|
|
assert(validateName(filename));
|
|
|
|
const storage: IPackageStorage = this._getLocalStorage(name);
|
|
|
|
if (_.isNil(storage)) {
|
|
return this._createFailureStreamResponse();
|
|
}
|
|
|
|
return this._streamSuccessReadTarBall(storage, filename);
|
|
}
|
|
|
|
/**
|
|
* Return a stream that emits a read failure.
|
|
* @private
|
|
* @return {ReadTarball}
|
|
*/
|
|
private _createFailureStreamResponse(): IReadTarball {
|
|
const stream: IReadTarball = new ReadTarball({});
|
|
|
|
process.nextTick((): void => {
|
|
stream.emit('error', this._getFileNotAvailable());
|
|
});
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Return a stream that emits the tarball data
|
|
* @param {Object} storage
|
|
* @param {String} filename
|
|
* @private
|
|
* @return {ReadTarball}
|
|
*/
|
|
private _streamSuccessReadTarBall(storage: any, filename: string): IReadTarball {
|
|
const stream: IReadTarball = new ReadTarball({});
|
|
const readTarballStream = storage.readTarball(filename);
|
|
const e404 = ErrorCode.getNotFound;
|
|
|
|
stream.abort = function (): void {
|
|
if (_.isNil(readTarballStream) === false) {
|
|
readTarballStream.abort();
|
|
}
|
|
};
|
|
|
|
readTarballStream.on('error', function (err) {
|
|
// @ts-ignore
|
|
if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
|
|
stream.emit('error', e404('no such file available'));
|
|
} else {
|
|
stream.emit('error', err);
|
|
}
|
|
});
|
|
|
|
readTarballStream.on('content-length', function (content): void {
|
|
stream.emit('content-length', content);
|
|
});
|
|
|
|
readTarballStream.on('open', function (): void {
|
|
// re-emitting open because it's handled in storage.js
|
|
stream.emit('open');
|
|
readTarballStream.pipe(stream);
|
|
});
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a package by name.
|
|
* @param {*} name
|
|
* @param {*} callback
|
|
* @return {Function}
|
|
*/
|
|
public getPackageMetadata(name: string, callback: Callback = (): void => {}): void {
|
|
const storage: IPackageStorage = this._getLocalStorage(name);
|
|
if (_.isNil(storage)) {
|
|
return callback(ErrorCode.getNotFound());
|
|
}
|
|
|
|
this._readPackage(name, storage, callback);
|
|
}
|
|
|
|
/**
|
|
* Search a local package.
|
|
* @param {*} startKey
|
|
* @param {*} options
|
|
* @return {Function}
|
|
*/
|
|
public search(startKey: string, options: any): IReadTarball {
|
|
const stream = new ReadTarball({ objectMode: true });
|
|
|
|
this._searchEachPackage(
|
|
(item: Package, cb: CallbackAction): void => {
|
|
// @ts-ignore
|
|
if (item.time > parseInt(startKey, 10)) {
|
|
this.getPackageMetadata(item.name, (err: VerdaccioError, data: Package): void => {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
|
|
// @ts-ignore
|
|
const time = new Date(item.time).toISOString();
|
|
const result = prepareSearchPackage(data, time);
|
|
if (_.isNil(result) === false) {
|
|
stream.push(result);
|
|
}
|
|
cb(null);
|
|
});
|
|
} else {
|
|
cb(null);
|
|
}
|
|
},
|
|
function onEnd(err): void {
|
|
if (err) {
|
|
stream.emit('error', err);
|
|
return;
|
|
}
|
|
stream.end();
|
|
}
|
|
);
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a wrapper that provide access to the package location.
|
|
* @param {Object} pkgName package name.
|
|
* @return {Object}
|
|
*/
|
|
private _getLocalStorage(pkgName: string): IPackageStorage {
|
|
return this.storagePlugin.getPackageStorage(pkgName);
|
|
}
|
|
|
|
/**
|
|
* Read a json file from storage.
|
|
* @param {Object} storage
|
|
* @param {Function} callback
|
|
*/
|
|
private _readPackage(name: string, storage: any, callback: Callback): void {
|
|
storage.readPackage(name, (err, result): void => {
|
|
if (err) {
|
|
if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
|
|
return callback(ErrorCode.getNotFound());
|
|
}
|
|
return callback(this._internalError(err, STORAGE.PACKAGE_FILE_NAME, 'error reading'));
|
|
}
|
|
|
|
callback(err, normalizePackage(result));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Walks through each package and calls `on_package` on them.
|
|
* @param {*} onPackage
|
|
* @param {*} onEnd
|
|
*/
|
|
private _searchEachPackage(onPackage: onSearchPackage, onEnd: onEndSearchPackage): void {
|
|
// save wait whether plugin still do not support search functionality
|
|
if (_.isNil(this.storagePlugin.search)) {
|
|
this.logger.warn('plugin search not implemented yet');
|
|
onEnd();
|
|
} else {
|
|
this.storagePlugin.search(onPackage, onEnd, validateName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve either a previous created local package or a boilerplate.
|
|
* @param {*} pkgName
|
|
* @param {*} callback
|
|
* @return {Function}
|
|
*/
|
|
private _readCreatePackage(pkgName: string, callback: Callback): void {
|
|
const storage: any = this._getLocalStorage(pkgName);
|
|
if (_.isNil(storage)) {
|
|
this._createNewPackage(pkgName, callback);
|
|
return;
|
|
}
|
|
|
|
storage.readPackage(pkgName, (err, data): void => {
|
|
// TODO: race condition
|
|
if (_.isNil(err) === false) {
|
|
if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
|
|
data = generatePackageTemplate(pkgName);
|
|
} else {
|
|
return callback(this._internalError(err, STORAGE.PACKAGE_FILE_NAME, 'error reading'));
|
|
}
|
|
}
|
|
|
|
callback(null, normalizePackage(data));
|
|
});
|
|
}
|
|
|
|
private _createNewPackage(name: string, callback: Callback): Callback {
|
|
return callback(null, normalizePackage(generatePackageTemplate(name)));
|
|
}
|
|
|
|
/**
|
|
* Handle internal error
|
|
* @param {*} err
|
|
* @param {*} file
|
|
* @param {*} message
|
|
* @return {Object} Error instance
|
|
*/
|
|
private _internalError(err: string, file: string, message: string): VerdaccioError {
|
|
this.logger.error({ err: err, file: file }, `${message} @{file}: @{!err.message}`);
|
|
|
|
return ErrorCode.getInternalError();
|
|
}
|
|
|
|
/**
|
|
* @param {*} name package name
|
|
* @param {*} updateHandler function(package, cb) - update function
|
|
* @param {*} callback callback that gets invoked after it's all updated
|
|
* @return {Function}
|
|
*/
|
|
private _updatePackage(
|
|
name: string,
|
|
updateHandler: StorageUpdateCallback,
|
|
callback: CallbackAction
|
|
): void {
|
|
const storage: IPackageStorage = this._getLocalStorage(name);
|
|
|
|
if (!storage) {
|
|
return callback(ErrorCode.getNotFound());
|
|
}
|
|
|
|
storage.updatePackage(
|
|
name,
|
|
updateHandler,
|
|
this._writePackage.bind(this),
|
|
normalizePackage,
|
|
callback
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update the revision (_rev) string for a package.
|
|
* @param {*} name
|
|
* @param {*} json
|
|
* @param {*} callback
|
|
* @return {Function}
|
|
*/
|
|
private _writePackage(name: string, json: Package, callback: Callback): void {
|
|
const storage: any = this._getLocalStorage(name);
|
|
if (_.isNil(storage)) {
|
|
return callback();
|
|
}
|
|
storage.savePackage(name, this._setDefaultRevision(json), callback);
|
|
}
|
|
|
|
private _setDefaultRevision(json: Package): Package {
|
|
// calculate revision from couch db
|
|
if (_.isString(json._rev) === false) {
|
|
json._rev = STORAGE.DEFAULT_REVISION;
|
|
}
|
|
|
|
// this is intended in debug mode we do not want modify the store revision
|
|
if (_.isNil(this.config._debug)) {
|
|
json._rev = generateRevision(json._rev);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
private _deleteAttachments(storage: any, attachments: string[], callback: Callback): void {
|
|
debug('deleting %o attachments total %o', attachments?.length);
|
|
const unlinkNext = function (cb): void {
|
|
if (_.isEmpty(attachments)) {
|
|
return cb();
|
|
}
|
|
|
|
const attachment = attachments.shift();
|
|
storage.deletePackage(attachment, function (): void {
|
|
unlinkNext(cb);
|
|
});
|
|
};
|
|
|
|
unlinkNext(function (): void {
|
|
// try to unlink the directory, but ignore errors because it can fail
|
|
storage.removePackage(function (err): void {
|
|
callback(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ensure the dist file remains as the same protocol
|
|
* @param {Object} hash metadata
|
|
* @param {String} upLinkKey registry key
|
|
* @private
|
|
*/
|
|
private _updateUplinkToRemoteProtocol(hash: DistFile, upLinkKey: string): void {
|
|
// if we got this information from a known registry,
|
|
// use the same protocol for the tarball
|
|
//
|
|
// see https://github.com/rlidwka/sinopia/issues/166
|
|
const tarballUrl: any = UrlNode.parse(hash.url);
|
|
const uplinkUrl: any = UrlNode.parse(this.config.uplinks[upLinkKey].url);
|
|
|
|
if (uplinkUrl.host === tarballUrl.host) {
|
|
tarballUrl.protocol = uplinkUrl.protocol;
|
|
hash.registry = upLinkKey;
|
|
hash.url = UrlNode.format(tarballUrl);
|
|
}
|
|
}
|
|
|
|
public async getSecret(config: Config): Promise<void> {
|
|
const secretKey = await this.storagePlugin.getSecret();
|
|
|
|
return this.storagePlugin.setSecret(config.checkSecretKey(secretKey));
|
|
}
|
|
|
|
private _loadStorage(config: Config, logger: Logger): IPluginStorage<Config> {
|
|
const Storage = this._loadStorePlugin();
|
|
|
|
if (_.isNil(Storage)) {
|
|
assert(this.config.storage, 'CONFIG: storage path not defined');
|
|
return new LocalDatabase(this.config, logger);
|
|
}
|
|
return Storage as IPluginStorage<Config>;
|
|
}
|
|
|
|
private _loadStorePlugin(): IPluginStorage<Config> | void {
|
|
const plugin_params = {
|
|
config: this.config,
|
|
logger: this.logger,
|
|
};
|
|
|
|
const plugins: IPluginStorage<Config>[] = loadPlugin<IPluginStorage<Config>>(
|
|
this.config,
|
|
this.config.store,
|
|
plugin_params,
|
|
(plugin): IPluginStorage<Config> => {
|
|
return plugin.getPackageStorage;
|
|
}
|
|
);
|
|
|
|
return _.head(plugins);
|
|
}
|
|
|
|
public saveToken(token: Token): Promise<any> {
|
|
if (_.isFunction(this.storagePlugin.saveToken) === false) {
|
|
return Promise.reject(
|
|
ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
|
|
);
|
|
}
|
|
|
|
return this.storagePlugin.saveToken(token);
|
|
}
|
|
|
|
public deleteToken(user: string, tokenKey: string): Promise<any> {
|
|
if (_.isFunction(this.storagePlugin.deleteToken) === false) {
|
|
return Promise.reject(
|
|
ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
|
|
);
|
|
}
|
|
|
|
return this.storagePlugin.deleteToken(user, tokenKey);
|
|
}
|
|
|
|
public readTokens(filter: TokenFilter): Promise<Token[]> {
|
|
if (_.isFunction(this.storagePlugin.readTokens) === false) {
|
|
return Promise.reject(
|
|
ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
|
|
);
|
|
}
|
|
|
|
return this.storagePlugin.readTokens(filter);
|
|
}
|
|
}
|
|
|
|
export { LocalStorage };
|