@shockpkg/core
Version:
shockpkg core
511 lines (470 loc) • 10.6 kB
JavaScript
import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { Package } from "./package.mjs";
import { TEMP_EXT } from "./constants.mjs";
/**
* Packages object.
*/
export class Packages {
/**
* Packages data.
*/
_packagesList = null;
/**
* Packages array.
*/
_packages = new Set();
/**
* Packages mapped by name.
*/
_packagesByName = new Map();
/**
* Packages mapped by sha256.
*/
_packagesBySha256 = new Map();
/**
* Packages mapped by sha1.
*/
_packagesBySha1 = new Map();
/**
* Packages mapped by md5.
*/
_packagesByMd5 = new Map();
/**
* Packages mapped by unique.
*/
_packagesByUnique = new Map();
/**
* The path to the packages file.
*/
/**
* Format version.
*/
static FORMAT = '1.2';
/**
* Packages constructor.
*
* @param path The path to the packages file.
*/
constructor(path) {
this._path = path;
}
/**
* Get path of the packages file.
*
* @returns The path.
*/
get path() {
return this._path;
}
/**
* Get if packages loaded.
*
* @returns Is loaded.
*/
get loaded() {
return !!this._packagesList;
}
/**
* Update packages.
*
* @param data Encoded data.
* @returns Update report.
*/
update(data) {
// Map out current list if any.
const map = new Map();
for (const pkg of this.packages()) {
const {
name,
file,
size,
sha256
} = pkg;
map.set(name, {
name,
file,
size,
sha256
});
}
// Actually set the data.
this._setPackagesList(this._parseData(data));
// List out the changes of significance.
const updated = [];
const added = [];
const removed = [];
for (const pkg of this.packages()) {
const {
name,
file,
size,
sha256
} = pkg;
const obj = {
name,
file,
size,
sha256
};
const before = map.get(name) || null;
map.delete(name);
if (!before) {
added.push(obj);
continue;
}
if (before.sha256 !== sha256 || before.size !== size || before.file !== file) {
updated.push(obj);
}
}
for (const [, obj] of map) {
removed.push(obj);
}
return {
updated,
added,
removed
};
}
/**
* Assert loaded.
*/
assertLoaded() {
if (!this.loaded) {
throw new Error('Packages list not loaded');
}
}
/**
* Check if the file path exists.
*
* @returns Does exist.
*/
async exists() {
return access(this.path).then(() => true, () => false);
}
/**
* Read the file path.
*/
async read() {
this._setPackagesList(this._castData(JSON.parse(await readFile(this.path, 'utf8'))));
}
/**
* Write packages to the file path.
*/
async write() {
if (!this._packagesList) {
throw new Error('Cannot write unloaded list');
}
const out = this.path;
const prt = `${out}${TEMP_EXT}`;
await mkdir(dirname(prt), {
recursive: true
});
await rm(prt, {
force: true
});
await writeFile(prt, JSON.stringify(this._packagesList, null, '\t'), {
flag: 'wx'
});
await rename(prt, out);
}
/**
* Read the file path if the file exists.
*
* @returns Did exist.
*/
async readIfExists() {
try {
await this.read();
} catch (err) {
if (err && err.code === 'ENOENT') {
return false;
}
throw err;
}
return true;
}
/**
* Itterate over the packages.
*
* @yields Package object.
*/
*packages() {
const packages = this._packages;
for (const entry of packages) {
// If the set changes, break loop.
if (packages !== this._packages) {
break;
}
yield entry;
}
}
/**
* Check if package is in this collection.
*
* @param pkg Package instance.
* @returns If the package instance is present.
*/
has(pkg) {
return this._packages.has(pkg);
}
/**
* Assert the package is in this collection.
*
* @param pkg Package instance.
*/
assertHas(pkg) {
if (!this.has(pkg)) {
throw new Error('Package not in collection');
}
}
/**
* Get package by the unique name.
*
* @param name Package name.
* @returns The package or null.
*/
byName(name) {
return this._packagesByName.get(name) || null;
}
/**
* Get package by the sha256 hash.
*
* @param sha256 Package sha256.
* @returns The package or null.
*/
bySha256(sha256) {
return this._packagesBySha256.get(sha256) || null;
}
/**
* Get package by the sha1 hash.
*
* @param sha1 Package sha1.
* @returns The package or null.
*/
bySha1(sha1) {
return this._packagesBySha1.get(sha1) || null;
}
/**
* Get package by the md5 hash.
*
* @param md5 Package md5.
* @returns The package or null.
*/
byMd5(md5) {
return this._packagesByMd5.get(md5) || null;
}
/**
* Get package by the unique value.
*
* @param unique Package unique.
* @returns The package or null.
*/
byUnique(unique) {
return this._packagesByUnique.get(unique) || null;
}
/**
* Create a package instance.
*
* @param info Package info.
* @returns Package instance.
*/
_createPackage(info) {
return new Package(info);
}
/**
* Set the packages list.
*
* @param packagesList Parsed list.
*/
_setPackagesList(packagesList) {
this._validateFormat(packagesList.format);
const parsed = this._parsePackages(packagesList.packages);
const packages = this._listPackages(parsed);
// Map out the names and hashes.
const byName = this._packagesMapName(packages);
const bySha256 = this._packagesMapSha256(packages);
const bySha1 = this._packagesMapSha1(packages);
const byMd5 = this._packagesMapMd5(packages);
const byUnique = this._packagesMapUnique(packages);
// If all parsed successfully, set properties.
this._packagesList = packagesList;
this._packages = packages;
this._packagesByName = byName;
this._packagesBySha256 = bySha256;
this._packagesBySha1 = bySha1;
this._packagesByMd5 = byMd5;
this._packagesByUnique = byUnique;
}
/**
* Validate format version string.
*
* @param format The format version string.
*/
_validateFormat(format) {
const Constructor = this.constructor;
const version = Constructor.FORMAT.split('.').map(Number);
const parts = format.split('.').map(Number);
if (parts.length !== 2) {
throw new Error(`Invalid format version value: ${format}`);
}
if (parts[0] !== version[0]) {
throw new Error(`Invalid format version major: ${format}`);
}
if (parts[1] < version[1]) {
throw new Error(`Invalid format version minor: ${format}`);
}
}
/**
* Parse the packages list.
*
* @param packages Packages list.
* @returns Parsed list.
*/
_parsePackages(packages) {
return packages.map(info => this._createPackage(info));
}
/**
* List all packages deep.
*
* @param packages A list of packages.
* @returns A set of all packages and their children.
*/
_listPackages(packages) {
const r = new Set();
const itter = [...packages];
for (;;) {
const entry = itter.shift();
if (!entry) {
break;
}
r.add(entry);
const chilren = entry.packages;
if (chilren) {
itter.unshift(...chilren);
}
}
return r;
}
/**
* Map out package list by name.
* Throws on any duplicates.
*
* @param packages Packages list.
* @returns Map from package name to package.
*/
_packagesMapName(packages) {
const r = new Map();
for (const entry of packages) {
const {
name
} = entry;
if (r.has(name)) {
throw new Error(`Duplicate package name: ${name}`);
}
r.set(name, entry);
}
return r;
}
/**
* Map out package list by sha256.
* Throws on any duplicates.
*
* @param packages Packages list.
* @returns Map from package sha256 to package.
*/
_packagesMapSha256(packages) {
const r = new Map();
for (const entry of packages) {
const {
sha256
} = entry;
if (r.has(sha256)) {
throw new Error(`Duplicate package sha256: ${sha256}`);
}
r.set(sha256, entry);
}
return r;
}
/**
* Map out package list by sha1.
* Throws on any duplicates.
*
* @param packages Packages list.
* @returns Map from package sha256 to package.
*/
_packagesMapSha1(packages) {
const r = new Map();
for (const entry of packages) {
const {
sha1
} = entry;
if (r.has(sha1)) {
throw new Error(`Duplicate package sha1: ${sha1}`);
}
r.set(sha1, entry);
}
return r;
}
/**
* Map out package list by md5.
* Throws on any duplicates.
*
* @param packages Packages list.
* @returns Map from package sha256 to package.
*/
_packagesMapMd5(packages) {
const r = new Map();
for (const entry of packages) {
const {
md5
} = entry;
if (r.has(md5)) {
throw new Error(`Duplicate package md5: ${md5}`);
}
r.set(md5, entry);
}
return r;
}
/**
* Map out package list by unique.
* Throws on any duplicates.
*
* @param packages Packages list.
* @returns Map from package unique to package.
*/
_packagesMapUnique(packages) {
const r = new Map();
for (const entry of packages) {
for (const unique of [entry.name, entry.sha256, entry.sha1, entry.md5]) {
if (r.has(unique)) {
throw new Error(`Duplicate package unique: ${unique}`);
}
r.set(unique, entry);
}
}
return r;
}
/**
* Parse and cast the encoded data.
*
* @param data Encoded data.
* @returns Parsed and cast data.
*/
_parseData(data) {
return this._castData(JSON.parse(data));
}
/**
* Cast the parsed data.
*
* @param packages Parsed data.
* @returns Cast data.
*/
_castData(packages) {
if (!packages || typeof packages !== 'object' || typeof packages.format !== 'string' || !Array.isArray(packages.packages)) {
throw new Error('Failed to validate packages');
}
return packages;
}
}
//# sourceMappingURL=packages.mjs.map