2020-08-19 13:27:35 -05:00
|
|
|
import fs from 'fs';
|
|
|
|
import Path from 'path';
|
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';
|
2020-09-16 23:48:16 -05:00
|
|
|
import {
|
|
|
|
Callback,
|
|
|
|
Config,
|
|
|
|
IPackageStorage,
|
|
|
|
IPluginStorage,
|
|
|
|
LocalStorage,
|
|
|
|
Logger,
|
|
|
|
StorageList,
|
|
|
|
} from '@verdaccio/types';
|
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';
|
2021-01-02 02:11:32 -05:00
|
|
|
import TokenActions from './token';
|
2021-03-30 07:05:58 -05:00
|
|
|
import { _dbGenPath } from './utils';
|
2020-08-19 13:27:35 -05:00
|
|
|
|
|
|
|
const DB_NAME = '.verdaccio-db.json';
|
|
|
|
|
2020-08-21 00:51:12 -05:00
|
|
|
const debug = buildDebug('verdaccio:plugin:local-storage');
|
|
|
|
|
2021-01-02 02:11:32 -05:00
|
|
|
class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
2020-08-19 13:27:35 -05:00
|
|
|
public path: string;
|
|
|
|
public logger: Logger;
|
2021-03-30 07:05:58 -05:00
|
|
|
// @ts-ignore
|
2020-08-19 13:27:35 -05:00
|
|
|
public data: LocalStorage;
|
|
|
|
public config: Config;
|
|
|
|
public locked: boolean;
|
|
|
|
|
|
|
|
public constructor(config: Config, logger: Logger) {
|
2021-01-02 02:11:32 -05:00
|
|
|
super(config);
|
2020-08-19 13:27:35 -05:00
|
|
|
this.config = config;
|
|
|
|
this.logger = logger;
|
|
|
|
this.locked = false;
|
2021-03-30 07:05:58 -05:00
|
|
|
this.path = _dbGenPath(DB_NAME, config);
|
|
|
|
debug('plugin storage path %o', this.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async init(): Promise<void> {
|
|
|
|
debug('plugin init');
|
|
|
|
this.data = await this._fetchLocalPackages();
|
2020-08-19 13:27:35 -05:00
|
|
|
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());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-16 23:48:16 -05:00
|
|
|
public search(
|
|
|
|
onPackage: Callback,
|
|
|
|
onEnd: Callback,
|
|
|
|
validateName: (name: string) => boolean
|
|
|
|
): void {
|
2020-08-19 13:27:35 -05:00
|
|
|
const storages = this._getCustomPackageLocalStorages();
|
2020-08-21 00:51:12 -05:00
|
|
|
debug(`search custom local packages: %o`, JSON.stringify(storages));
|
2020-11-08 09:20:02 -05:00
|
|
|
const base = Path.dirname(this.config.config_path);
|
2020-08-19 13:27:35 -05:00
|
|
|
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,
|
2021-01-02 02:11:32 -05:00
|
|
|
time: self.getTime(stats.mtime.getTime(), stats.mtime),
|
2020-08-19 13:27:35 -05:00
|
|
|
},
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
cb
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
// @ts-ignore
|
|
|
|
onEnd
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public remove(name: string, cb: Callback): void {
|
|
|
|
this.get((err, data) => {
|
|
|
|
if (err) {
|
|
|
|
cb(getInternalError('error remove private package'));
|
2020-09-16 23:48:16 -05:00
|
|
|
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);
|
|
|
|
|
2020-09-16 23:48:16 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-09-16 23:48:16 -05:00
|
|
|
const packageStoragePath: string = Path.join(
|
2020-11-08 09:20:02 -05:00
|
|
|
Path.resolve(Path.dirname(this.config.config_path || ''), packagePath),
|
2020-09-16 23:48:16 -05:00
|
|
|
packageName
|
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-01-02 02:11:32 -05:00
|
|
|
private getTime(time: number, mtime: Date): number | Date {
|
2020-08-19 13:27:35 -05:00
|
|
|
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) {
|
2020-09-16 23:48:16 -05:00
|
|
|
this.logger.error(
|
2020-10-24 06:17:21 -05:00
|
|
|
'Database is locked, please check error message printed during startup to ' +
|
|
|
|
'prevent data loss.'
|
2020-09-16 23:48:16 -05:00
|
|
|
);
|
|
|
|
return new Error(
|
2020-10-24 06:17:21 -05:00
|
|
|
'Verdaccio database is locked, please contact your administrator to checkout ' +
|
|
|
|
'logs during verdaccio startup.'
|
2020-09-16 23:48:16 -05:00
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
}
|
|
|
|
// Uses sync to prevent ugly race condition
|
|
|
|
try {
|
|
|
|
const folderName = Path.dirname(this.path);
|
2021-03-30 07:05:58 -05:00
|
|
|
debug('creating folder %o', folderName);
|
2021-05-02 02:52:12 -05:00
|
|
|
fs.mkdirSync(folderName, { recursive: true });
|
2020-08-21 00:51:12 -05:00
|
|
|
debug('sync folder %o created succeed', folderName);
|
2021-08-30 01:19:08 -05:00
|
|
|
} catch (err: any) {
|
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;
|
2021-08-30 01:19:08 -05:00
|
|
|
} catch (err: any) {
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch local packages.
|
|
|
|
* @private
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
2021-03-30 07:05:58 -05:00
|
|
|
private async _fetchLocalPackages(): Promise<LocalStorage> {
|
2020-08-19 13:27:35 -05:00
|
|
|
const list: StorageList = [];
|
|
|
|
const emptyDatabase = { list, secret: '' };
|
|
|
|
|
|
|
|
try {
|
2021-03-30 07:05:58 -05:00
|
|
|
return await loadPrivatePackages(this.path, this.logger);
|
2021-08-30 01:19:08 -05:00
|
|
|
} catch (err: any) {
|
2020-08-19 13:27:35 -05:00
|
|
|
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
|
|
|
|
// Only recreate if file not found to prevent data loss
|
2021-03-30 07:05:58 -05:00
|
|
|
debug('error on fetch local packages %o', err);
|
2020-08-19 13:27:35 -05:00
|
|
|
if (err.code !== noSuchFile) {
|
|
|
|
this.locked = true;
|
2020-09-16 23:48:16 -05:00
|
|
|
this.logger.error(
|
|
|
|
'Failed to read package database file, please check the error printed below:\n',
|
|
|
|
`File Path: ${this.path}\n\n ${err.message}`
|
|
|
|
);
|
2020-08-19 13:27:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return emptyDatabase;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default LocalDatabase;
|