2020-08-19 13:27:35 -05:00
|
|
|
import fs from 'fs';
|
|
|
|
import Path from 'path';
|
|
|
|
import stream from 'stream';
|
2020-08-21 00:51:12 -05:00
|
|
|
import buildDebug from 'debug';
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
import _ from 'lodash';
|
|
|
|
import async from 'async';
|
|
|
|
import mkdirp from 'mkdirp';
|
|
|
|
import { Callback, Config, IPackageStorage, IPluginStorage, LocalStorage, Logger, StorageList, Token, TokenFilter } from '@verdaccio/types';
|
|
|
|
import level from 'level';
|
2020-08-19 15:09:12 -05:00
|
|
|
import { getInternalError } from '@verdaccio/commons-api';
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
import LocalDriver, { noSuchFile } from './local-fs';
|
|
|
|
import { loadPrivatePackages } from './pkg-utils';
|
|
|
|
|
|
|
|
const DEPRECATED_DB_NAME = '.sinopia-db.json';
|
|
|
|
const DB_NAME = '.verdaccio-db.json';
|
|
|
|
const TOKEN_DB_NAME = '.token-db';
|
|
|
|
|
|
|
|
interface Level {
|
|
|
|
put(key: string, token, fn?: Function): void;
|
|
|
|
|
|
|
|
get(key: string, fn?: Function): void;
|
|
|
|
|
|
|
|
del(key: string, fn?: Function): void;
|
|
|
|
|
|
|
|
createReadStream(options?: object): stream.Readable;
|
|
|
|
}
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
const debug = buildDebug('verdaccio:plugin:local-storage');
|
|
|
|
|
2020-08-19 13:27:35 -05:00
|
|
|
/**
|
|
|
|
* Handle local database.
|
|
|
|
*/
|
|
|
|
class LocalDatabase implements IPluginStorage<{}> {
|
|
|
|
public path: string;
|
|
|
|
public logger: Logger;
|
|
|
|
public data: LocalStorage;
|
|
|
|
public config: Config;
|
|
|
|
public locked: boolean;
|
|
|
|
public tokenDb;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load an parse the local json database.
|
|
|
|
* @param {*} path the database path
|
|
|
|
*/
|
|
|
|
public constructor(config: Config, logger: Logger) {
|
|
|
|
this.config = config;
|
|
|
|
this.path = this._buildStoragePath(config);
|
|
|
|
this.logger = logger;
|
|
|
|
this.locked = false;
|
|
|
|
this.data = this._fetchLocalPackages();
|
|
|
|
this._sync();
|
|
|
|
}
|
|
|
|
|
|
|
|
public getSecret(): Promise<string> {
|
|
|
|
return Promise.resolve(this.data.secret);
|
|
|
|
}
|
|
|
|
|
|
|
|
public setSecret(secret: string): Promise<Error | null> {
|
|
|
|
return new Promise((resolve): void => {
|
|
|
|
this.data.secret = secret;
|
|
|
|
|
|
|
|
resolve(this._sync());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a new element.
|
|
|
|
* @param {*} name
|
|
|
|
* @return {Error|*}
|
|
|
|
*/
|
|
|
|
public add(name: string, cb: Callback): void {
|
|
|
|
if (this.data.list.indexOf(name) === -1) {
|
|
|
|
this.data.list.push(name);
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('the private package %o has been added', name);
|
2020-08-19 13:27:35 -05:00
|
|
|
cb(this._sync());
|
|
|
|
} else {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('the private package %o was not added', name);
|
2020-08-19 13:27:35 -05:00
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public search(onPackage: Callback, onEnd: Callback, validateName: (name: string) => boolean): void {
|
|
|
|
const storages = this._getCustomPackageLocalStorages();
|
2020-08-21 00:51:12 -05:00
|
|
|
debug(`search custom local packages: %o`, JSON.stringify(storages));
|
2020-08-19 13:27:35 -05:00
|
|
|
const base = Path.dirname(this.config.self_path);
|
|
|
|
const self = this;
|
|
|
|
const storageKeys = Object.keys(storages);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug(`search base: %o keys: %o`, base, storageKeys);
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
storageKeys,
|
|
|
|
function (storage, cb) {
|
|
|
|
const position = storageKeys.indexOf(storage);
|
|
|
|
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
|
|
|
|
const storagePath: string = Path.resolve(base, base2, storage);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('search path: %o : %o', storagePath, storage);
|
2020-08-19 13:27:35 -05:00
|
|
|
fs.readdir(storagePath, (err, files) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
files,
|
|
|
|
function (file, cb) {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('local-storage: [search] search file path: %o', file);
|
2020-08-19 13:27:35 -05:00
|
|
|
if (storageKeys.includes(file)) {
|
|
|
|
return cb();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (file.match(/^@/)) {
|
|
|
|
// scoped
|
|
|
|
const fileLocation = Path.resolve(base, storage, file);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('search scoped file location: %o', fileLocation);
|
2020-08-19 13:27:35 -05:00
|
|
|
fs.readdir(fileLocation, function (err, files) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
files,
|
|
|
|
(file2, cb) => {
|
|
|
|
if (validateName(file2)) {
|
|
|
|
const packagePath = Path.resolve(base, storage, file, file2);
|
|
|
|
|
|
|
|
fs.stat(packagePath, (err, stats) => {
|
|
|
|
if (_.isNil(err) === false) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
const item = {
|
|
|
|
name: `${file}/${file2}`,
|
|
|
|
path: packagePath,
|
|
|
|
time: stats.mtime.getTime(),
|
|
|
|
};
|
|
|
|
onPackage(item, cb);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else if (validateName(file)) {
|
|
|
|
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
|
|
|
|
const packagePath = Path.resolve(base, base2, storage, file);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('search file location: %o', packagePath);
|
2020-08-19 13:27:35 -05:00
|
|
|
fs.stat(packagePath, (err, stats) => {
|
|
|
|
if (_.isNil(err) === false) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
onPackage(
|
|
|
|
{
|
|
|
|
name: file,
|
|
|
|
path: packagePath,
|
|
|
|
time: self._getTime(stats.mtime.getTime(), stats.mtime),
|
|
|
|
},
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
// @ts-ignore
|
|
|
|
onEnd
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove an element from the database.
|
|
|
|
* @param {*} name
|
|
|
|
* @return {Error|*}
|
|
|
|
*/
|
|
|
|
public remove(name: string, cb: Callback): void {
|
|
|
|
this.get((err, data) => {
|
|
|
|
if (err) {
|
|
|
|
cb(getInternalError('error remove private package'));
|
|
|
|
this.logger.error({ err }, '[local-storage/remove]: remove the private package has failed @{err}');
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('error on remove package %o', name);
|
2020-08-19 13:27:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const pkgName = data.indexOf(name);
|
|
|
|
if (pkgName !== -1) {
|
|
|
|
this.data.list.splice(pkgName, 1);
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('remove package %o has been removed', name);
|
2020-08-19 13:27:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
cb(this._sync());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all database elements.
|
|
|
|
* @return {Array}
|
|
|
|
*/
|
|
|
|
public get(cb: Callback): void {
|
|
|
|
const list = this.data.list;
|
|
|
|
const totalItems = this.data.list.length;
|
|
|
|
|
|
|
|
cb(null, list);
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('get full list of packages (%o) has been fetched', totalItems);
|
2020-08-19 13:27:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public getPackageStorage(packageName: string): IPackageStorage {
|
|
|
|
const packageAccess = this.config.getMatchedPackagesSpec(packageName);
|
|
|
|
|
|
|
|
const packagePath: string = this._getLocalStoragePath(packageAccess ? packageAccess.storage : undefined);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('storage path selected: ', packagePath);
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
if (_.isString(packagePath) === false) {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('the package %o has no storage defined ', packageName);
|
2020-08-19 13:27:35 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const packageStoragePath: string = Path.join(Path.resolve(Path.dirname(this.config.self_path || ''), packagePath), packageName);
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('storage absolute path: ', packageStoragePath);
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
return new LocalDriver(packageStoragePath, this.logger);
|
|
|
|
}
|
|
|
|
|
|
|
|
public clean(): void {
|
|
|
|
this._sync();
|
|
|
|
}
|
|
|
|
|
|
|
|
public saveToken(token: Token): Promise<void> {
|
|
|
|
const key = this._getTokenKey(token);
|
|
|
|
const db = this.getTokenDb();
|
|
|
|
|
|
|
|
return new Promise((resolve, reject): void => {
|
|
|
|
db.put(key, token, (err) => {
|
|
|
|
if (err) {
|
|
|
|
reject(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public deleteToken(user: string, tokenKey: string): Promise<void> {
|
|
|
|
const key = this._compoundTokenKey(user, tokenKey);
|
|
|
|
const db = this.getTokenDb();
|
|
|
|
return new Promise((resolve, reject): void => {
|
|
|
|
db.del(key, (err) => {
|
|
|
|
if (err) {
|
|
|
|
reject(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public readTokens(filter: TokenFilter): Promise<Token[]> {
|
|
|
|
return new Promise((resolve, reject): void => {
|
|
|
|
const tokens: Token[] = [];
|
|
|
|
const key = filter.user + ':';
|
|
|
|
const db = this.getTokenDb();
|
|
|
|
const stream = db.createReadStream({
|
|
|
|
gte: key,
|
|
|
|
lte: String.fromCharCode(key.charCodeAt(0) + 1),
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.on('data', (data) => {
|
|
|
|
tokens.push(data.value);
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.once('end', () => resolve(tokens));
|
|
|
|
|
|
|
|
stream.once('error', (err) => reject(err));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private _getTime(time: number, mtime: Date): number | Date {
|
|
|
|
return time ? time : mtime;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _getCustomPackageLocalStorages(): object {
|
|
|
|
const storages = {};
|
|
|
|
|
|
|
|
// add custom storage if exist
|
|
|
|
if (this.config.storage) {
|
|
|
|
storages[this.config.storage] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { packages } = this.config;
|
|
|
|
|
|
|
|
if (packages) {
|
|
|
|
const listPackagesConf = Object.keys(packages || {});
|
|
|
|
|
|
|
|
listPackagesConf.map((pkg) => {
|
|
|
|
const storage = packages[pkg].storage;
|
|
|
|
if (storage) {
|
|
|
|
storages[storage] = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return storages;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Syncronize {create} database whether does not exist.
|
|
|
|
* @return {Error|*}
|
|
|
|
*/
|
|
|
|
private _sync(): Error | null {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync database started');
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
if (this.locked) {
|
|
|
|
this.logger.error('Database is locked, please check error message printed during startup to prevent data loss.');
|
|
|
|
return new Error('Verdaccio database is locked, please contact your administrator to checkout logs during verdaccio startup.');
|
|
|
|
}
|
|
|
|
// Uses sync to prevent ugly race condition
|
|
|
|
try {
|
|
|
|
// https://www.npmjs.com/package/mkdirp#mkdirpsyncdir-opts
|
|
|
|
const folderName = Path.dirname(this.path);
|
|
|
|
mkdirp.sync(folderName);
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync folder %o created succeed', folderName);
|
2020-08-19 13:27:35 -05:00
|
|
|
} catch (err) {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync create folder has failed with error: %o', err);
|
2020-08-19 13:27:35 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.writeFileSync(this.path, JSON.stringify(this.data));
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync write succeed');
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
return null;
|
|
|
|
} catch (err) {
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync failed %o', err);
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Verify the right local storage location.
|
|
|
|
* @param {String} path
|
|
|
|
* @return {String}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private _getLocalStoragePath(storage: string | void): string {
|
|
|
|
const globalConfigStorage = this.config ? this.config.storage : undefined;
|
|
|
|
if (_.isNil(globalConfigStorage)) {
|
|
|
|
throw new Error('global storage is required for this plugin');
|
|
|
|
} else {
|
|
|
|
if (_.isNil(storage) === false && _.isString(storage)) {
|
|
|
|
return Path.join(globalConfigStorage as string, storage as string);
|
|
|
|
}
|
|
|
|
|
|
|
|
return globalConfigStorage as string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build the local database path.
|
|
|
|
* @param {Object} config
|
|
|
|
* @return {string|String|*}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private _buildStoragePath(config: Config): string {
|
|
|
|
const sinopiadbPath: string = this._dbGenPath(DEPRECATED_DB_NAME, config);
|
|
|
|
try {
|
|
|
|
fs.accessSync(sinopiadbPath, fs.constants.F_OK);
|
|
|
|
return sinopiadbPath;
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code === noSuchFile) {
|
|
|
|
return this._dbGenPath(DB_NAME, config);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _dbGenPath(dbName: string, config: Config): string {
|
|
|
|
return Path.join(Path.resolve(Path.dirname(config.self_path || ''), config.storage as string, dbName));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch local packages.
|
|
|
|
* @private
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
|
|
|
private _fetchLocalPackages(): LocalStorage {
|
|
|
|
const list: StorageList = [];
|
|
|
|
const emptyDatabase = { list, secret: '' };
|
|
|
|
|
|
|
|
try {
|
|
|
|
const db = loadPrivatePackages(this.path, this.logger);
|
|
|
|
|
|
|
|
return db;
|
|
|
|
} catch (err) {
|
|
|
|
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
|
|
|
|
// Only recreate if file not found to prevent data loss
|
|
|
|
if (err.code !== noSuchFile) {
|
|
|
|
this.locked = true;
|
|
|
|
this.logger.error('Failed to read package database file, please check the error printed below:\n', `File Path: ${this.path}\n\n ${err.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return emptyDatabase;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getTokenDb(): Level {
|
|
|
|
if (!this.tokenDb) {
|
|
|
|
this.tokenDb = level(this._dbGenPath(TOKEN_DB_NAME, this.config), {
|
|
|
|
valueEncoding: 'json',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.tokenDb;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _getTokenKey(token: Token): string {
|
|
|
|
const { user, key } = token;
|
|
|
|
return this._compoundTokenKey(user, key);
|
|
|
|
}
|
|
|
|
|
|
|
|
private _compoundTokenKey(user: string, key: string): string {
|
|
|
|
return `${user}:${key}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default LocalDatabase;
|