mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-25 09:51:01 -05:00
Made base class for uploaders
This commit is contained in:
parent
0dd215e0d7
commit
1a9e1f14d7
2 changed files with 197 additions and 134 deletions
197
src/platforms/generic-platform-uploader.ts
Normal file
197
src/platforms/generic-platform-uploader.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>;
|
|
||||||
}
|
|
Loading…
Reference in a new issue