Made base class for uploaders

This commit is contained in:
Kir_Antipov 2023-05-03 14:01:22 +00:00
parent 0dd215e0d7
commit 1a9e1f14d7
2 changed files with 197 additions and 134 deletions

View file

@ -0,0 +1,197 @@
import { Dependency, DependencyType } from "@/dependencies";
import { retry } from "@/utils/async-utils";
import { ArgumentNullError, isSoftError } from "@/utils/errors";
import { FileInfo } from "@/utils/io";
import { JavaVersion } from "@/utils/java";
import { Logger, LoggingStopwatch, NULL_LOGGER } from "@/utils/logging";
import { SecureString } from "@/utils/security";
import { VersionType } from "@/utils/versioning";
import { CurseForgeUploaderOptions } from "./curseforge/curseforge-uploader";
import { GitHubUploaderOptions } from "./github/github-uploader";
import { ModrinthUploaderOptions } from "./modrinth/modrinth-uploader";
import { PlatformType } from "./platform-type";
import { PlatformUploader } from "./platform-uploader";
/**
* Options for configuring a generic platform uploader.
*/
export interface GenericPlatformUploaderOptions {
/**
* An optional logger that can be used for recording log messages.
*/
logger?: Logger;
}
/**
* Represents all known options for a generic platform uploader.
*/
export type KnownPlatformUploaderOptions =
& ModrinthUploaderOptions
& GitHubUploaderOptions
& CurseForgeUploaderOptions;
/**
* Represents a request for uploading to a platform.
*/
export interface GenericPlatformUploadRequest {
/**
* The unique identifier of the project on the target platform.
*/
id?: string;
/**
* A secure token used for authenticating with the platform's API.
*/
token?: SecureString;
/**
* An array of files to be uploaded to the platform.
*/
files?: FileInfo[];
/**
* The name for the new version of the project to be uploaded.
*/
name?: string;
/**
* The new version identifier for the project.
*/
version?: string;
/**
* The specified type of the version (e.g., 'release', 'beta', 'alpha').
*/
versionType?: VersionType;
/**
* The changelog detailing the updates in the new version.
*/
changelog?: string;
/**
* An array of loaders that the project is compatible with.
*/
loaders?: string[];
/**
* An array of game versions that the project is compatible with.
*/
gameVersions?: string[];
/**
* An array of dependencies required by this version of the project.
*/
dependencies?: Dependency[];
/**
* An array of Java versions that the project supports.
*/
java?: JavaVersion[];
/**
* The maximum number of attempts to publish assets to the platform.
*/
retryAttempts?: number;
/**
* Time delay (in milliseconds) between each attempt to publish assets.
*/
retryDelay?: number;
}
/**
* The default number of retry attempts for a failed upload.
*/
const DEFAULT_RETRY_ATTEMPTS = 2;
/**
* The default delay time (in milliseconds) between retry attempts for a failed upload.
*/
const DEFAULT_RETRY_DELAY = 1000;
/**
* Base class for platform uploaders.
*
* @template TOptions - The type of options that the uploader can utilize.
* @template TRequest - The type of content that can be uploaded using the uploader.
* @template TReport - The type of report that is returned after the upload process.
*/
export abstract class GenericPlatformUploader<TOptions extends GenericPlatformUploaderOptions, TRequest extends GenericPlatformUploadRequest, TReport> implements PlatformUploader<TRequest, TReport> {
/**
* The logger used by the uploader.
*/
protected readonly _logger: Logger;
/**
* Constructs a new {@link PlatformUploader} instance.
*
* @param options - The options to use for the uploader.
*/
protected constructor(options?: TOptions) {
this._logger = options?.logger || NULL_LOGGER;
}
/**
* @inheritdoc
*/
abstract get platform(): PlatformType;
/**
* @inheritdoc
*/
async upload(request: TRequest): Promise<TReport> {
ArgumentNullError.throwIfNull(request, "request");
ArgumentNullError.throwIfNull(request.token, "request.token");
ArgumentNullError.throwIfNullOrEmpty(request.files, "request.files");
const platformName = PlatformType.friendlyNameOf(this.platform);
const maxAttempts = request.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
const delay = request.retryDelay ?? DEFAULT_RETRY_DELAY;
const stopwatch = LoggingStopwatch.startNew(this._logger,
() => `📤 Uploading assets to ${platformName}`,
ms => `✅ Successfully published assets to ${platformName} in ${ms} ms`
);
const onError = (error: Error) => {
if (isSoftError(error)) {
this._logger.error(error);
this._logger.info(`🔂 Facing difficulties, republishing assets to ${platformName} in ${delay} ms`);
return true;
}
return false;
};
const report = await retry(
() => this.uploadCore(request),
{ maxAttempts, delay, onError }
);
stopwatch.stop();
return report;
}
/**
* Processes the specified upload request.
*
* @param request - The request to process.
*
* @returns A report generated after the upload.
*/
protected abstract uploadCore(request: TRequest): Promise<TReport>;
/**
* Converts the specified dependencies to a simpler format.
*
* @param dependencies - The list of dependencies to convert.
* @param typeConverter - The function to use for converting dependency types.
*
* @returns An array of dependencies in a simplified format.
*/
protected convertToSimpleDependencies<T>(dependencies: Dependency[], typeConverter: (type: DependencyType) => T): [id: string, type: T][] {
return (dependencies || [])
.filter(x => x && !x.isIgnored(this.platform))
.map(x => [x.getProjectId(this.platform), typeConverter(x.type)] as [string, T])
.filter(([id, type]) => id && type);
}
}

View file

@ -1,134 +0,0 @@
import { context } from "@actions/github";
import { parseVersionName, parseVersionNameFromFileVersion } from "../utils/minecraft";
import File from "../utils/io/file";
import Publisher from "./publisher";
import PublisherTarget from "./publisher-target";
import MinecraftVersionResolver from "../utils/minecraft/minecraft-version-resolver";
import ModMetadataReader from "../metadata/mod-metadata-reader";
import Dependency from "../metadata/dependency";
import Version from "../utils/versioning/version";
import VersionType from "../utils/versioning/version-type";
import DependencyKind from "../metadata/dependency-kind";
import path from "path";
interface ModPublisherOptions {
id: string;
token: string;
versionType?: "alpha" | "beta" | "release";
loaders?: string | string[];
name?: string;
version?: string;
changelog?: string;
changelogFile?: string;
versionResolver?: string;
gameVersions?: string | string[];
java?: string | string[];
dependencies?: string | string[];
}
function processMultilineInput(input: string | string[], splitter?: RegExp): string[] {
if (!input) {
return [];
}
return (typeof input === "string" ? input.split(splitter || /(\r?\n)+/) : input).map(x => x.trim()).filter(x => x);
}
function processDependenciesInput(input: string | string[], inputSplitter?: RegExp, entrySplitter?: RegExp): Dependency[] {
return processMultilineInput(input, inputSplitter).map(x => {
const parts = x.split(entrySplitter || /\|/);
const id = parts[0].trim();
return Dependency.create({
id,
kind: parts[1] && DependencyKind.parse(parts[1].trim()) || DependencyKind.Depends,
version: parts[2]?.trim() || "*"
});
});
}
async function readChangelog(changelogPath: string): Promise<string | never> {
const file = (await File.getFiles(changelogPath))[0];
if (!file) {
throw new Error("Changelog file was not found");
}
return (await file.getBuffer()).toString("utf8");
}
export default abstract class ModPublisher extends Publisher<ModPublisherOptions> {
protected get requiresId(): boolean {
return true;
}
protected get requiresModLoaders(): boolean {
return true;
}
protected get requiresGameVersions(): boolean {
return true;
}
public async publish(files: File[], options: ModPublisherOptions): Promise<void> {
this.validateOptions(options);
const releaseInfo = <any>context.payload.release;
if (!Array.isArray(files) || !files.length) {
throw new Error("No upload files were specified");
}
const token = options.token;
if (!token) {
throw new Error(`Token is required to publish your assets to ${PublisherTarget.toString(this.target)}`);
}
const metadata = await ModMetadataReader.readMetadata(files[0].path);
const id = options.id || metadata?.getProjectId(this.target);
if (!id && this.requiresId) {
throw new Error(`Project id is required to publish your assets to ${PublisherTarget.toString(this.target)}`);
}
const filename = path.parse(files[0].path).name;
const version = (typeof options.version === "string" && options.version) || <string>releaseInfo?.tag_name || metadata?.version || Version.fromName(filename);
const versionType = options.versionType?.toLowerCase() || VersionType.fromName(metadata?.version || filename);
const name = typeof options.name === "string" ? options.name : (<string>releaseInfo?.name || version);
const changelog = typeof options.changelog === "string"
? options.changelog
: typeof options.changelogFile === "string"
? await readChangelog(options.changelogFile)
: <string>releaseInfo?.body || "";
const loaders = processMultilineInput(options.loaders, /\s+/);
if (!loaders.length && this.requiresModLoaders) {
if (metadata) {
loaders.push(...metadata.loaders);
}
if (!loaders.length) {
throw new Error("At least one mod loader should be specified");
}
}
const gameVersions = processMultilineInput(options.gameVersions);
if (!gameVersions.length && this.requiresGameVersions) {
const minecraftVersion =
metadata?.dependencies.filter(x => x.id === "minecraft").map(x => parseVersionName(x.version))[0] ||
parseVersionNameFromFileVersion(version);
if (minecraftVersion) {
const resolver = options.versionResolver && MinecraftVersionResolver.byName(options.versionResolver) || MinecraftVersionResolver.releasesIfAny;
gameVersions.push(...(await resolver.resolve(minecraftVersion)).map(x => x.id));
}
if (!gameVersions.length) {
throw new Error("At least one game version should be specified");
}
}
const java = processMultilineInput(options.java);
const dependencies = typeof options.dependencies === "string"
? processDependenciesInput(options.dependencies)
: metadata?.dependencies || [];
const uniqueDependencies = dependencies.filter((x, i, self) => !x.ignore && self.findIndex(y => y.id === x.id && y.kind === x.kind) === i);
await this.publishMod(id, token, name, version, versionType, loaders, gameVersions, java, changelog, files, uniqueDependencies, <Record<string, unknown>><unknown>options);
}
protected abstract publishMod(id: string, token: string, name: string, version: string, versionType: string, loaders: string[], gameVersions: string[], java: string[], changelog: string, files: File[], dependencies: Dependency[], options: Record<string, unknown>): Promise<void>;
}