@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
231 lines • 9.83 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
/**
* SPDX-License-Identifier: Apache-2.0
*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import { pipeline as streamPipeline } from 'node:stream/promises';
import got from 'got';
import path from 'path';
import { DataValidationError, SoloError, IllegalArgumentError, MissingArgumentError, ResourceNotFoundError, } from './errors.js';
import * as https from 'https';
import * as http from 'http';
import { Templates } from './templates.js';
import * as constants from './constants.js';
import { StatusCodes } from 'http-status-codes';
import { inject, injectable } from 'tsyringe-neo';
import { patchInject } from './dependency_injection/container_helper.js';
import { InjectTokens } from './dependency_injection/inject_tokens.js';
let PackageDownloader = class PackageDownloader {
logger;
constructor(logger) {
this.logger = logger;
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
}
isValidURL(url) {
try {
// attempt to parse to check URL format
const out = new URL(url);
return out.href !== undefined;
}
catch {
return false;
}
}
urlExists(url) {
const self = this;
return new Promise(resolve => {
try {
self.logger.debug(`Checking URL: ${url}`);
// attempt to send a HEAD request to check URL exists
const req = url.startsWith('http://')
? http.request(url, { method: 'HEAD', timeout: 100, headers: { Connection: 'close' } })
: https.request(url, { method: 'HEAD', timeout: 100, headers: { Connection: 'close' } });
req.on('response', r => {
const statusCode = r.statusCode;
self.logger.debug({
response: {
// @ts-ignore
connectOptions: r['connect-options'],
statusCode: r.statusCode,
headers: r.headers,
},
});
req.destroy();
if ([StatusCodes.OK, StatusCodes.MOVED_TEMPORARILY].includes(statusCode)) {
resolve(true);
}
resolve(false);
});
req.on('error', err => {
self.logger.error(err);
resolve(false);
req.destroy();
});
req.end(); // make the request
}
catch (e) {
self.logger.error(e);
resolve(false);
}
});
}
/**
* Fetch data from a URL and save the output to a file
*
* @param url - source file URL
* @param destPath - destination path for the downloaded file
*/
async fetchFile(url, destPath) {
if (!url) {
throw new IllegalArgumentError('package URL is required', url);
}
if (!destPath) {
throw new IllegalArgumentError('destination path is required', destPath);
}
if (!this.isValidURL(url)) {
throw new IllegalArgumentError(`package URL '${url}' is invalid`, url);
}
if (!(await this.urlExists(url))) {
throw new ResourceNotFoundError(`package URL '${url}' does not exist`, url);
}
try {
await streamPipeline(got.stream(url, { followRedirect: true }), fs.createWriteStream(destPath));
return destPath;
}
catch (e) {
throw new SoloError(`Error fetching file ${url}: ${e.message}`, e);
}
}
/**
* Compute hash of the file contents
* @param filePath - path of the file
* @param [algo] - hash algorithm
* @returns hex digest of the computed hash
* @throws {Error} - if the file cannot be read
*/
computeFileHash(filePath, algo = 'sha384') {
const self = this;
return new Promise((resolve, reject) => {
try {
self.logger.debug(`Computing checksum for '${filePath}' using algo '${algo}'`);
const checksum = crypto.createHash(algo);
const s = fs.createReadStream(filePath);
s.on('data', d => {
checksum.update(d);
});
s.on('end', () => {
const d = checksum.digest('hex');
self.logger.debug(`Computed checksum '${d}' for '${filePath}' using algo '${algo}'`);
resolve(d);
});
s.on('error', e => {
reject(e);
});
}
catch (e) {
reject(new SoloError('failed to compute checksum', e, { filePath, algo }));
}
});
}
/**
* Verifies that the checksum of the sourceFile matches with the contents of the checksumFile
*
* It throws error if the checksum doesn't match.
*
* @param sourceFile - path to the file for which checksum to be computed
* @param checksum - expected checksum
* @param [algo] - hash algorithm to be used to compute checksum
* @returns
* @throws {DataValidationError} - if the checksum doesn't match
*/
async verifyChecksum(sourceFile, checksum, algo = 'sha256') {
const computed = await this.computeFileHash(sourceFile, algo);
if (checksum !== computed)
throw new DataValidationError('checksum', checksum, computed);
}
/**
* Fetch a remote package
* @param packageURL
* @param checksumURL - package checksum URL
* @param destDir - a directory where the files should be downloaded to
* @param [algo] - checksum algo
* @param [force] - force download even if the file exists in the destDir
*/
async fetchPackage(packageURL, checksumURL, destDir, algo = 'sha256', force = false) {
if (!packageURL)
throw new Error('package URL is required');
if (!checksumURL)
throw new Error('checksum URL is required');
if (!destDir)
throw new Error('destination directory path is required');
this.logger.debug(`Downloading package: ${packageURL}, checksum: ${checksumURL}`);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const packageFile = `${destDir}/${path.basename(packageURL)}`;
const checksumFile = `${destDir}/${path.basename(checksumURL)}`;
try {
if (fs.existsSync(packageFile) && !force) {
return packageFile;
}
await this.fetchFile(checksumURL, checksumFile);
const checksumData = fs.readFileSync(checksumFile).toString();
if (!checksumData)
throw new SoloError(`unable to read checksum file: ${checksumFile}`);
const checksum = checksumData.split(' ')[0];
await this.fetchFile(packageURL, packageFile);
await this.verifyChecksum(packageFile, checksum, algo);
return packageFile;
}
catch (e) {
if (fs.existsSync(checksumFile)) {
fs.rmSync(checksumFile);
}
if (fs.existsSync(packageFile)) {
fs.rmSync(packageFile);
}
throw new SoloError(e.message, e);
}
}
/**
* Fetch Hedera platform release artifact
*
* It fetches the build.zip file containing the release from a URL like: https://builds.hedera.com/node/software/v0.40/build-v0.40.4.zip
*
* @param tag - full semantic version e.g. v0.40.4
* @param destDir - directory where the artifact needs to be saved
* @param [force] - whether to download even if the file exists
* @returns full path to the downloaded file
*/
async fetchPlatform(tag, destDir, force = false) {
if (!tag)
throw new MissingArgumentError('tag is required');
if (!destDir) {
throw new MissingArgumentError('destination directory path is required');
}
const releaseDir = Templates.prepareReleasePrefix(tag);
const downloadDir = `${destDir}/${releaseDir}`;
const packageURL = `${constants.HEDERA_BUILDS_URL}/node/software/${releaseDir}/build-${tag}.zip`;
const checksumURL = `${constants.HEDERA_BUILDS_URL}/node/software/${releaseDir}/build-${tag}.sha384`;
return await this.fetchPackage(packageURL, checksumURL, downloadDir, 'sha384', force);
}
};
PackageDownloader = __decorate([
injectable(),
__param(0, inject(InjectTokens.SoloLogger)),
__metadata("design:paramtypes", [Function])
], PackageDownloader);
export { PackageDownloader };
//# sourceMappingURL=package_downloader.js.map