UNPKG

@nilppm/npm

Version:

Node's internal lightweight private package manager

469 lines (468 loc) 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const nelts_1 = require("@nelts/nelts"); const request = require("request"); const path = require("path"); const crypto = require("crypto"); const uuid = require("uuid/v4"); const fse = require("fs-extra"); const fs = require("fs"); const sequelize_1 = require("sequelize"); class PackageService extends nelts_1.Component.Service { constructor(ctx) { super(ctx); this.configs = ctx.app.configs; } async searchFromDBO(keyword, size) { const WebService = new this.service.WebService(this.ctx); const result = await this.ctx.dbo.package.findAndCountAll({ attributes: ['pathname'], where: { name: { [sequelize_1.Op.like]: '%' + keyword + '%' } }, limit: 20, offset: size }); const total = result.count; const names = result.rows.map(x => { return this.getPackageInfo({ pathname: x.pathname }).then(data => WebService.fixUser(data).then(() => { return { name: data.name, description: data.description, maintainers: data.maintainers, version: data.version, _created: data._created }; })); }); const objects = await Promise.all(names); return { objects, total, time: new Date() }; } async searchFromNpm(keyword, size) { if (!keyword) throw new Error('search param need a string value'); if (!size) size = 0; const WebService = new this.service.WebService(this.ctx); return await new Promise((resolve, reject) => { let url = `http://registry.npmjs.com/-/v1/search?text=${encodeURIComponent(keyword)}&from=${size}`; request.get(url, (err, response, body) => { if (err) return reject(err); if (response.statusCode >= 300 || response.statusCode < 200) return reject(new Error(response.statusMessage)); const data = JSON.parse(body); if (data.error) return reject(new Error(data.error)); const objects = data.objects.map((x) => { const pkg = x.package; WebService.fixRemoteMaintainers(pkg); return { name: pkg.name, description: pkg.description, maintainers: pkg.maintainers, version: pkg.version, _created: pkg.date }; }); resolve({ objects, total: data.total, time: data.time }); }); }); } async unPublish(filepath, rev) { const MaintainerService = new this.service.MaintainerService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); const TagService = new this.service.TagService(this.ctx); const result = await VersionService.getSingleVersionByRev(rev, 'id', 'pid', 'name', 'package', 'ctime', 'tarball'); if (!result) throw new Error('cannot find the rev of ' + rev); const pack = await this.getSinglePackageById(result.pid, 'id', 'pathname'); if (!pack) throw new Error('cannot find the package of id: ' + result.pid); if (filepath.endsWith('.tgz')) { if (pack.pathname + '-' + result.name + '.tgz' !== filepath) throw new Error('invaild package receiver.'); } const maintainers = await MaintainerService.getMaintainersByPid(pack.id); if (!MaintainerService.checkMaintainerAllow(this.ctx.account, maintainers)) throw new Error('you cannot unpublish this package'); await VersionService.deleteVersion(result.id); const count = await VersionService.getCountOfPid(pack.id); if (count === 0) { await this.clearPackage(pack.id); await this.removePackageCache(pack.id, pack.pathname); } else { const tags = await TagService.getVidAndNameByPid(pack.id); let pool = []; for (let i = 0; i < tags.length; i++) { if (tags[i].vid === result.id) { pool.push(tags[i].name); } } if (pool.length) { const version = await VersionService.findLatestVersion(pack.id, new Date(result.ctime)); if (version) { await TagService.updateVidOnNamesByPid(pack.id, version.id, pool); } } await this.updateModifiedTime(pack.id); await this.updatePackageCache(pack.id); } const dfile = path.resolve(this.configs.nfs, result.tarball); if (fs.existsSync(dfile)) fs.unlinkSync(dfile); return JSON.parse(result.package); } async clearPackage(pid) { const MaintainerService = new this.service.MaintainerService(this.ctx); const TagService = new this.service.TagService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); await Promise.all([ MaintainerService.removeAllByPid(pid), TagService.removeAllByPid(pid), VersionService.removeAllByPid(pid), this.removeAllByPid(pid) ]); } async removeAllByPid(pid) { return await this.ctx.dbo.package.destroy({ where: { id: pid } }); } async removePackageCache(pid, pathname) { const TagServer = new this.service.TagService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); const MaintainerService = new this.service.MaintainerService(this.ctx); await MaintainerService.getMaintainersCache(pid).delete({ pid }); await TagServer.getTagsCache(pid).delete({ pid }); await VersionService.getVersionCache(pid).delete({ pid }); await this.ctx.redis.delete(':package:' + pathname); } async updatePackageCache(pid) { const TagServer = new this.service.TagService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); const MaintainerService = new this.service.MaintainerService(this.ctx); await MaintainerService.getMaintainersCache(pid).set({ pid }); await TagServer.getTagsCache(pid).set({ pid }); await VersionService.getVersionCache(pid).set({ pid }); const pack = await this.ctx.dbo.package.findAll({ attributes: ['pathname', 'ctime', 'mtime'], where: { id: pid } }); const pathname = pack[0].pathname; const ctime = pack[0].ctime; const mtime = pack[0].mtime; await this.ctx.redis.set(':package:' + pathname, { id: pid, ctime, mtime }); } async getUri(url, name, version) { return await new Promise((resolve, reject) => { url += '/' + name; if (version) url += '/' + version; request.get(url, (err, response, body) => { if (err) return reject(err); if (response.statusCode >= 300 || response.statusCode < 200) return reject(new Error(response.statusMessage)); resolve(body); }); }); } async getRemotePackageInformation(pathname, version) { const fetchPackageRegistriesOrder = this.configs.fetchPackageRegistriesOrder; for (let i = 0; i < fetchPackageRegistriesOrder.length; i++) { const text = await this.getUri(this.configs[fetchPackageRegistriesOrder[i]], pathname, version); try { const result = JSON.parse(text); if (!result.error) { if (!result.version) { if (!version) { result.version = result['dist-tags'].latest; } else { if (/\d+\.\d+\.\d+/.test(version)) { result.version = version; } else { result.version = result['dist-tags'][version]; } } } return result; } } catch (e) { } } throw new Error('not found'); } async getLocalPackageByPid(pid, ctime, mtime, version) { const TagServer = new this.service.TagService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); const MaintainerService = new this.service.MaintainerService(this.ctx); const UserService = new this.service.UserService(this.ctx); const [maintainers, tags, versions] = await Promise.all([ MaintainerService.getMaintainersCache(pid).get({ pid }), TagServer.getTagsCache(pid).get({ pid }), VersionService.getVersionCache(pid).get({ pid }) ]); if (!maintainers || !maintainers.length || !tags || !tags.latest || !versions || !Object.keys(versions).length) throw new Error('invaild cache data with package'); let chunk; const distTags = {}; const chunkVersions = {}; const times = {}; for (const i in tags) distTags[i] = versions[tags[i]].version; if (version && !/^\d+\.\d+\.\d+$/.test(version)) { if (!distTags[version]) throw new Error('cannot find tag in dist-tags:' + version); version = distTags[version]; } let _currentVersion; if (!version) { if (!versions[tags.latest]) throw new Error('cannot find the latest version'); chunk = versions[tags.latest]; _currentVersion = tags.latest; } else { for (const i in versions) { if (versions[i].version === version) { chunk = versions[i]; _currentVersion = version; break; } } } if (!chunk) throw new Error('invaild version data in cache'); chunk = JSON.parse(JSON.stringify(chunk)); if (!chunk.version) chunk.version = _currentVersion; chunk.maintainers = (await Promise.all(maintainers.map((maintainer) => UserService.userCache(maintainer).get({ account: maintainer })))).map((user) => { return { name: user.account, email: user.email, }; }); chunk['dist-tags'] = distTags; for (const i in versions) { times[versions[i].version] = versions[i]._created; chunkVersions[versions[i].version] = versions[i]; if (chunkVersions[versions[i].version].readme) delete chunkVersions[versions[i].version].readme; } chunk._nilppm = true; chunk.versions = chunkVersions; chunk.time = times; chunk.time.created = ctime; chunk.time.modified = mtime; if (chunk.main) delete chunk.main; if (chunk._nodeVersion) delete chunk._nodeVersion; if (chunk._npmUser) delete chunk._npmUser; if (chunk._npmVersion) delete chunk._npmVersion; return chunk; } async getPackageInfo(pkg) { const pck = await this.ctx.redis.get(':package:' + pkg.pathname); if (pck) { return await this.getLocalPackageByPid(pck.id, new Date(pck.ctime), new Date(pck.mtime), pkg.version); } const sp = pkg.pathname.split('/'); if (sp.length > 2) throw new Error('invaild package name'); if (sp.length === 2) { const scope = sp[0]; if (this.configs.scopes.indexOf(scope) > -1) { const pack = await this.getSinglePackageByPathname(pkg.pathname, 'id', 'ctime', 'mtime'); if (pack) { await this.updatePackageCache(pack.id); return await this.getLocalPackageByPid(pack.id, pack.ctime, pack.mtime, pkg.version); } } } return await this.getRemotePackageInformation(pkg.pathname, pkg.version); } createShasumCode(tarballBuffer) { const shasum = crypto.createHash('sha1'); shasum.update(tarballBuffer); return shasum.digest('hex'); } splitPackagePathname(pathname) { const sp = pathname.split('/'); return { scope: sp[0], alias: sp[1], }; } async getSinglePackageByPathname(pathname, ...attributes) { const res = await this.ctx.dbo.package.findAll({ attributes: attributes.length > 0 ? attributes : ['id'], where: { pathname } }); if (!res.length) return; return res[0]; } async getSinglePackageById(id, ...attributes) { const res = await this.ctx.dbo.package.findAll({ attributes: attributes.length > 0 ? attributes : ['id'], where: { id } }); if (!res.length) return; return res[0]; } async createNewPackage(scope, name, pathname) { return await this.ctx.dbo.package.create({ scope, name, pathname, }); } async updatPackage(pkg) { const pathname = pkg.name; const versions = pkg.versions; const pack = await this.getSinglePackageByPathname(pathname); const VersionService = new this.service.VersionService(this.ctx); const MaintainerService = new this.service.MaintainerService(this.ctx); if (!pack) throw new Error('cannot find the package of ' + pathname); const maintainers = await MaintainerService.getMaintainersByPid(pack.id); if (!MaintainerService.checkMaintainerAllow(this.ctx.account, maintainers)) throw new Error('you cannot update version metadata ' + pkg.name); const pid = pack.id; let updated = 0; for (const i in versions) { const version = versions[i]; updated += await VersionService.update(pid, version); } if (updated) { await this.updateModifiedTime(pid); await this.updatePackageCache(pid); } } async publish(account, pkg) { const name = pkg.name; const filename = Object.keys(pkg._attachments)[0]; const version = Object.keys(pkg.versions)[0]; const distTags = pkg['dist-tags'] || {}; const UserService = new this.service.UserService(this.ctx); const VersionService = new this.service.VersionService(this.ctx); const MaintainerService = new this.service.MaintainerService(this.ctx); const TagServer = new this.service.TagService(this.ctx); if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error('version is not a vaild version rule: ' + version); const tarballPath = path.resolve(this.configs.nfs, filename); if (!MaintainerService.checkMaintainerAllow(account, pkg.maintainers)) { throw new Error('You cannot publish this package or tell admins to add right for you'); } if (!Object.keys(distTags).length) { throw new Error('invalid: dist-tags should not be empty.'); } const { scope, alias } = this.splitPackagePathname(name); const cache = await UserService.userCache(account); const user = await cache.get({ account }); if (user.scopes.indexOf(scope) === -1) { throw new Error('forbidden: cannot publish package using ' + scope); } const attachment = pkg._attachments[filename]; const tarballBuffer = Buffer.from(attachment.data, 'base64'); if (tarballBuffer.length !== attachment.length) { throw new Error(`size_wrong: Attachment size ${attachment.length} not match download size ${tarballBuffer.length}`); } const shasum = this.createShasumCode(tarballBuffer); if (pkg.versions[version].dist) { pkg.versions[version].dist.tarball = this.configs.registryHost + '/download/' + filename; if (pkg.versions[version].dist.shasum !== shasum) { throw new Error(`shasum_wrong: Attachment shasum ${shasum} not match download size ${pkg.versions[version].dist.shasum}`); } } let packageId, firstTime = false; const packages = await this.getSinglePackageByPathname(name); if (!packages) { const packageModel = await this.createNewPackage(scope, alias, name); packageId = packageModel.id; firstTime = true; } else { packageId = packages.id; } const sysMaintainers = await MaintainerService.getMaintainersByPid(packageId); if (!firstTime) { if (!MaintainerService.checkMaintainerAllow(account, sysMaintainers)) { throw new Error('you have no right to publish package with ' + name); } } else { await MaintainerService.createNewMaintainer(account, packageId); } const _versions = await VersionService.getVersionsByPid(packageId); if (!VersionService.checkVersionAllow(version, _versions.map(ver => ver.name))) { throw new Error('forbidden: cannot publish pre-existing version: ' + version); } if (pkg.versions[version].dist) { pkg.versions[version].dist.size = attachment.length; } const _package = pkg.versions[version]; _package.author = account; if (_package.scripts) delete _package.scripts; if (_package.readmeFilename) delete _package.readmeFilename; const versionModel = await VersionService.createNewVersion({ pid: packageId, name: version, description: pkg.description, account, shasum, tarball: filename, size: attachment.length, package: JSON.stringify(_package), rev: uuid() }); const vid = versionModel.id; const tags = []; for (var t in distTags) tags.push([t, vid]); if (!distTags.latest) { const latest = await TagServer.getChunksByPidAndName(packageId, 'latest'); if (!latest.length) { tags.push(['latest', vid]); } } for (let i = 0; i < tags.length; i++) { await TagServer.createNewTag(packageId, tags[i][0], tags[i][1]); } await fse.ensureDir(path.dirname(tarballPath)); fs.writeFileSync(tarballPath, tarballBuffer); await this.updateModifiedTime(packageId); await this.updatePackageCache(packageId); } async updateModifiedTime(pid) { return await this.ctx.dbo.package.update({ mtime: new Date(), }, { where: { pid } }); } } exports.default = PackageService;