Move struct out of model folder.

This commit is contained in:
Daniel Scalzi
2020-09-12 20:14:18 -04:00
parent cd7b4d8abc
commit 1a19df0e93
25 changed files with 194 additions and 269 deletions

View File

@@ -0,0 +1,28 @@
import { mkdirs } from 'fs-extra'
import { join, resolve } from 'path'
import { FileStructure } from './FileStructure'
import { Logger } from 'winston'
import { LoggerUtil } from '../util/LoggerUtil'
export abstract class BaseFileStructure implements FileStructure {
protected logger: Logger
protected containerDirectory: string
constructor(
protected absoluteRoot: string,
protected relativeRoot: string,
protected structRoot: string
) {
this.relativeRoot = join(relativeRoot, structRoot)
this.containerDirectory = resolve(absoluteRoot, structRoot)
this.logger = LoggerUtil.getLogger(this.getLoggerName())
}
public async init(): Promise<void> {
mkdirs(this.containerDirectory)
}
public abstract getLoggerName(): string
}

View File

@@ -0,0 +1,5 @@
export interface FileStructure {
init(): void
}

View File

@@ -0,0 +1,99 @@
import got from 'got'
import { createWriteStream, mkdirs, pathExists } from 'fs-extra'
import { dirname, join, resolve } from 'path'
import { resolve as resolveURL } from 'url'
import { MavenUtil } from '../../util/maven'
import { BaseFileStructure } from '../BaseFileStructure'
import { LoggerUtil } from '../../util/LoggerUtil'
export abstract class BaseMavenRepo extends BaseFileStructure {
private static readonly logger = LoggerUtil.getLogger('BaseMavenRepo')
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string
) {
super(absoluteRoot, relativeRoot, structRoot)
}
public getArtifactById(mavenIdentifier: string, extension?: string): string {
return resolve(this.containerDirectory, MavenUtil.mavenIdentifierToString(mavenIdentifier, extension))
}
public getArtifactByComponents(
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
): string {
return resolve(this.containerDirectory,
MavenUtil.mavenComponentsToString(group, artifact, version, classifier, extension))
}
public getArtifactUrlByComponents(
baseURL: string, group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
): string {
return resolveURL(baseURL, join(this.relativeRoot,
MavenUtil.mavenComponentsToString(group, artifact, version, classifier, extension)))
}
public async artifactExists(path: string): Promise<boolean> {
return pathExists(path)
}
public async downloadArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise<void> {
return this.downloadArtifactBase(url, MavenUtil.mavenIdentifierToString(mavenIdentifier, extension) as string)
}
public async downloadArtifactByComponents(
url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string
): Promise<void> {
return this.downloadArtifactBase(url,
MavenUtil.mavenComponentsToString(group, artifact, version, classifier, extension))
}
private async downloadArtifactBase(url: string, relative: string): Promise<void> {
const resolvedURL = resolveURL(url, relative).toString()
return this.downloadArtifactDirect(resolvedURL, relative)
}
public async downloadArtifactDirect(url: string, path: string): Promise<void> {
BaseMavenRepo.logger.debug(`Downloading ${url}..`)
const request = await got.stream.get({ url })
const localPath = resolve(this.containerDirectory, path)
await mkdirs(dirname(localPath))
const writer = createWriteStream(localPath)
request.pipe(writer)
// tslint:disable-next-line: no-shadowed-variable
return new Promise((resolve, reject) => {
writer.on('finish', () => {
BaseMavenRepo.logger.debug(`Completed download of ${url}.`)
resolve()
})
writer.on('error', reject)
})
}
public async headArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise<boolean> {
return this.headArtifactBase(url, MavenUtil.mavenIdentifierToString(mavenIdentifier, extension) as string)
}
public async headArtifactByComponents(
url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string
): Promise<boolean> {
return this.headArtifactBase(url,
MavenUtil.mavenComponentsToString(group, artifact, version, classifier, extension))
}
private async headArtifactBase(url: string, relative: string): Promise<boolean> {
const resolvedURL = resolveURL(url, relative).toString()
try {
const response = await got.head({
url: resolvedURL
})
return response.statusCode === 200
} catch (ignored) {
return false
}
}
}

View File

@@ -0,0 +1,39 @@
import { BaseMavenRepo } from './BaseMavenRepo'
export class LibRepoStructure extends BaseMavenRepo {
public static readonly MINECRAFT_GROUP = 'net.minecraft'
public static readonly MINECRAFT_CLIENT_ARTIFACT = 'client'
public static readonly FORGE_GROUP = 'net.minecraftforge'
public static readonly FORGE_ARTIFACT = 'forge'
public static readonly LITELOADER_GROUP = 'com.mumfrey'
public static readonly LITELOADER_ARTIFACT = 'liteloader'
constructor(
absoluteRoot: string,
relativeRoot: string
) {
super(absoluteRoot, relativeRoot, 'lib')
}
public getLoggerName(): string {
return 'LibRepoStructure'
}
public getLocalForge(version: string, classifier?: string): string {
return this.getArtifactByComponents(
LibRepoStructure.FORGE_GROUP,
LibRepoStructure.FORGE_ARTIFACT,
version, classifier, 'jar')
}
public getLocalLiteLoader(version: string, classifier?: string): string {
return this.getArtifactByComponents(
LibRepoStructure.LITELOADER_GROUP,
LibRepoStructure.LITELOADER_ARTIFACT,
version, classifier, 'jar')
}
}

View File

@@ -0,0 +1,46 @@
import { join } from 'path'
import { BaseFileStructure } from '../BaseFileStructure'
import { LibRepoStructure } from './LibRepo.struct'
import { VersionRepoStructure } from './VersionRepo.struct'
export class RepoStructure extends BaseFileStructure {
private libRepoStruct: LibRepoStructure
private versionRepoStruct: VersionRepoStructure
constructor(
absoluteRoot: string,
relativeRoot: string
) {
super(absoluteRoot, relativeRoot, 'repo')
this.libRepoStruct = new LibRepoStructure(this.containerDirectory, this.relativeRoot)
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot)
}
public getLoggerName(): string {
return 'RepoStructure'
}
public async init(): Promise<void> {
super.init()
await this.libRepoStruct.init()
await this.versionRepoStruct.init()
}
public getLibRepoStruct(): LibRepoStructure {
return this.libRepoStruct
}
public getVersionRepoStruct(): VersionRepoStructure {
return this.versionRepoStruct
}
public getTempDirectory(): string {
return join(this.absoluteRoot, 'temp')
}
public getWorkDirectory(): string {
return join(this.absoluteRoot, 'work')
}
}

View File

@@ -0,0 +1,33 @@
import { join } from 'path'
import { resolve as resolveURL } from 'url'
import { BaseFileStructure } from '../BaseFileStructure'
import { MinecraftVersion } from '../../util/MinecraftVersion'
export class VersionRepoStructure extends BaseFileStructure {
constructor(
absoluteRoot: string,
relativeRoot: string
) {
super(absoluteRoot, relativeRoot, 'versions')
}
public getLoggerName(): string {
return 'VersionRepoStructure'
}
public getFileName(minecraftVersion: MinecraftVersion, forgeVersion: string): string {
return `${minecraftVersion}-forge-${forgeVersion}`
}
public getVersionManifest(minecraftVersion: MinecraftVersion, forgeVersion: string): string {
const fileName = this.getFileName(minecraftVersion, forgeVersion)
return join(this.containerDirectory, fileName, `${fileName}.json`)
}
public getVersionManifestURL(url: string, minecraftVersion: MinecraftVersion, forgeVersion: string): string {
const fileName = this.getFileName(minecraftVersion, forgeVersion)
return resolveURL(url, join(this.relativeRoot, fileName, `${fileName}.json`))
}
}

View File

@@ -0,0 +1,19 @@
import { BaseFileStructure } from '../BaseFileStructure'
import { SpecModelStructure } from './SpecModelStructure'
export abstract class BaseModelStructure<T> extends BaseFileStructure implements SpecModelStructure<T[]> {
protected resolvedModels: T[] | undefined
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string,
protected baseUrl: string
) {
super(absoluteRoot, relativeRoot, structRoot)
}
public abstract async getSpecModel(): Promise<T[]>
}

View File

@@ -0,0 +1,45 @@
import { mkdirs, writeFile, readFile } from 'fs-extra'
import { Distribution } from 'helios-distribution-types'
import { SpecModelStructure } from './SpecModelStructure'
import { ServerStructure } from './Server.struct'
import { join, resolve } from 'path'
import { DistroMeta, getDefaultDistroMeta } from '../../model/nebula/distrometa'
export class DistributionStructure implements SpecModelStructure<Distribution> {
private readonly DISTRO_META_FILE = 'distrometa.json'
private serverStruct: ServerStructure
private metaPath: string
constructor(
private absoluteRoot: string,
private baseUrl: string
) {
this.serverStruct = new ServerStructure(this.absoluteRoot, this.baseUrl)
this.metaPath = join(this.absoluteRoot, 'meta')
}
public async init(): Promise<void> {
await mkdirs(this.absoluteRoot)
await mkdirs(this.metaPath)
const distroMeta: DistroMeta = getDefaultDistroMeta()
await writeFile(resolve(this.metaPath, this.DISTRO_META_FILE), JSON.stringify(distroMeta, null, 2))
await this.serverStruct.init()
}
public async getSpecModel(): Promise<Distribution> {
const distroMeta: DistroMeta = JSON.parse(await readFile(resolve(this.metaPath, this.DISTRO_META_FILE), 'utf-8'))
return {
version: '1.0.0',
rss: distroMeta.meta.rss,
...(distroMeta.meta.discord ? {discord: distroMeta.meta.discord} : {}),
servers: await this.serverStruct.getSpecModel()
}
}
}

View File

@@ -0,0 +1,184 @@
import { lstat, mkdirs, pathExists, readdir, readFile, writeFile } from 'fs-extra'
import { Server, Module } from 'helios-distribution-types'
import { dirname, join, resolve as resolvePath } from 'path'
import { resolve as resolveUrl } from 'url'
import { VersionSegmentedRegistry } from '../../util/VersionSegmentedRegistry'
import { ServerMeta, getDefaultServerMeta, ServerMetaOptions } from '../../model/nebula/servermeta'
import { BaseModelStructure } from './BaseModel.struct'
import { MiscFileStructure } from './module/File.struct'
import { LiteModStructure } from './module/LiteMod.struct'
import { LibraryStructure } from './module/Library.struct'
import { MinecraftVersion } from '../../util/MinecraftVersion'
export class ServerStructure extends BaseModelStructure<Server> {
private readonly ID_REGEX = /(.+-(.+)$)/
private readonly SERVER_META_FILE = 'servermeta.json'
constructor(
absoluteRoot: string,
baseUrl: string
) {
super(absoluteRoot, '', 'servers', baseUrl)
}
public getLoggerName(): string {
return 'ServerStructure'
}
public async getSpecModel(): Promise<Server[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doSeverRetrieval()
}
return this.resolvedModels
}
public async createServer(
id: string,
minecraftVersion: MinecraftVersion,
options: {
forgeVersion?: string
liteloaderVersion?: string
}
): Promise<void> {
const effectiveId = `${id}-${minecraftVersion}`
const absoluteServerRoot = resolvePath(this.containerDirectory, effectiveId)
const relativeServerRoot = join(this.relativeRoot, effectiveId)
if (await pathExists(absoluteServerRoot)) {
this.logger.error('Server already exists! Aborting.')
return
}
await mkdirs(absoluteServerRoot)
const serverMetaOpts: ServerMetaOptions = {}
if (options.forgeVersion != null) {
const fms = VersionSegmentedRegistry.getForgeModStruct(
minecraftVersion,
options.forgeVersion,
absoluteServerRoot,
relativeServerRoot,
this.baseUrl
)
await fms.init()
serverMetaOpts.forgeVersion = options.forgeVersion
}
if (options.liteloaderVersion != null) {
const lms = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await lms.init()
serverMetaOpts.liteloaderVersion = options.liteloaderVersion
}
const serverMeta: ServerMeta = getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts)
await writeFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), JSON.stringify(serverMeta, null, 2))
const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await libS.init()
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await mfs.init()
}
private async _doSeverRetrieval(): Promise<Server[]> {
const accumulator: Server[] = []
const files = await readdir(this.containerDirectory)
for (const file of files) {
const absoluteServerRoot = resolvePath(this.containerDirectory, file)
const relativeServerRoot = join(this.relativeRoot, file)
if ((await lstat(absoluteServerRoot)).isDirectory()) {
const match = this.ID_REGEX.exec(file)
if (match == null) {
this.logger.warn(`Server directory ${file} does not match the defined standard.`)
this.logger.warn('All server ids must end with -<minecraft version> (ex. -1.12.2)')
continue
}
let iconUrl: string = null!
// Resolve server icon
const subFiles = await readdir(absoluteServerRoot)
for (const subFile of subFiles) {
const caseInsensitive = subFile.toLowerCase()
if (caseInsensitive.endsWith('.jpg') || caseInsensitive.endsWith('.png')) {
iconUrl = resolveUrl(this.baseUrl, join(relativeServerRoot, subFile))
}
}
if (!iconUrl) {
this.logger.warn(`No icon file found for server ${file}.`)
}
// Read server meta
const serverMeta: ServerMeta = JSON.parse(await readFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), 'utf-8'))
const minecraftVersion = new MinecraftVersion(match[2])
const modules: Module[] = []
if(serverMeta.forge) {
const forgeResolver = VersionSegmentedRegistry.getForgeResolver(
minecraftVersion,
serverMeta.forge.version,
dirname(this.containerDirectory),
'',
this.baseUrl
)
// Resolve forge
const forgeItselfModule = await forgeResolver.getModule()
modules.push(forgeItselfModule)
const forgeModStruct = VersionSegmentedRegistry.getForgeModStruct(
minecraftVersion,
serverMeta.forge.version,
absoluteServerRoot,
relativeServerRoot,
this.baseUrl
)
const forgeModModules = await forgeModStruct.getSpecModel()
modules.push(...forgeModModules)
}
if(serverMeta.liteloader) {
const liteModStruct = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const liteModModules = await liteModStruct.getSpecModel()
modules.push(...liteModModules)
}
const libraryStruct = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const libraryModules = await libraryStruct.getSpecModel()
modules.push(...libraryModules)
const fileStruct = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const fileModules = await fileStruct.getSpecModel()
modules.push(...fileModules)
accumulator.push({
id: match[1],
name: serverMeta.meta.name,
description: serverMeta.meta.description,
icon: iconUrl,
version: serverMeta.meta.version,
address: serverMeta.meta.address,
minecraftVersion: match[2],
...(serverMeta.meta.discord ? {discord: serverMeta.meta.discord} : {}),
mainServer: serverMeta.meta.mainServer,
autoconnect: serverMeta.meta.autoconnect,
modules
})
} else {
this.logger.warn(`Path ${file} in server directory is not a directory!`)
}
}
return accumulator
}
}

View File

@@ -0,0 +1,7 @@
import { FileStructure } from '../FileStructure'
export interface SpecModelStructure<T> extends FileStructure {
getSpecModel(): Promise<T>
}

View File

@@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Stats } from 'fs'
import { Type, Module } from 'helios-distribution-types'
import { resolve as resolveURL } from 'url'
import { ModuleStructure } from './Module.struct'
import { readdir, stat } from 'fs-extra'
import { join, resolve, sep } from 'path'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
export class MiscFileStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'files', baseUrl, minecraftVersion, Type.File)
}
public getLoggerName(): string {
return 'MiscFileStructure'
}
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this.recursiveModuleScan(this.containerDirectory)
}
return this.resolvedModels
}
protected async recursiveModuleScan(dir: string): Promise<Module[]> {
let acc: Module[] = []
const subdirs = await readdir(dir)
for (const file of subdirs) {
const filePath = resolve(dir, file)
const stats = await stat(filePath)
if (stats.isDirectory()) {
acc = acc.concat(await this.recursiveModuleScan(filePath))
} else {
acc.push(await this.parseModule(file, filePath, stats))
}
}
return acc
}
protected async getModuleId(name: string, path: string): Promise<string> {
return name
}
protected async getModuleName(name: string, path: string): Promise<string> {
return name
}
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return resolveURL(this.baseUrl, join(this.relativeRoot, ...path.substr(this.containerDirectory.length+1).split(sep)))
}
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return path.substr(this.containerDirectory.length+1).replace(/\\/g, '/')
}
}

View File

@@ -0,0 +1,61 @@
import { Stats } from 'fs-extra'
import { Type, Module } from 'helios-distribution-types'
import { join } from 'path'
import { resolve } from 'url'
import { VersionSegmented } from '../../../util/VersionSegmented'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
import { ToggleableModuleStructure } from './ToggleableModule.struct'
import { LibraryType } from '../../../model/claritas/ClaritasLibraryType'
import { ClaritasException } from './Module.struct'
export abstract class BaseForgeModStructure extends ToggleableModuleStructure implements VersionSegmented {
protected readonly EXAMPLE_MOD_ID = 'examplemod'
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, minecraftVersion, Type.ForgeMod)
}
public async getSpecModel(): Promise<Module[]> {
// Sort by file name to allow control of load order.
return (await super.getSpecModel()).sort((a, b) => {
const aFileName = a.artifact.url.substring(a.artifact.url.lastIndexOf('/')+1)
const bFileName = b.artifact.url.substring(b.artifact.url.lastIndexOf('/')+1)
return aFileName.localeCompare(bFileName)
})
}
public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return resolve(this.baseUrl, join(this.relativeRoot, this.getActiveNamespace(), name))
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return null
}
protected getClaritasExceptions(): ClaritasException[] {
return [{
exceptionName: 'optifine',
proxyMetadata: {
group: 'net.optifine'
}
}]
}
protected getClaritasType(): LibraryType {
return LibraryType.FORGE
}
protected discernResult(claritasValue: string | undefined, crudeInference: string): string {
return (claritasValue == null || claritasValue == '') ? crudeInference : claritasValue
}
}

View File

@@ -0,0 +1,44 @@
import { ModuleStructure } from './Module.struct'
import { Type, TypeMetadata } from 'helios-distribution-types'
import { Stats } from 'fs-extra'
import { join } from 'path'
import { resolve } from 'url'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
export class LibraryStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'libraries', baseUrl, minecraftVersion, Type.Library, (name: string) => {
return name.toLowerCase().endsWith(TypeMetadata[this.type].defaultExtension!)
})
}
public getLoggerName(): string {
return 'LibraryStructure'
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleId(name: string, path: string): Promise<string> {
const inference = this.attemptCrudeInference(name)
return this.generateMavenIdentifier(this.getDefaultGroup(), inference.name, inference.version)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleName(name: string, path: string): Promise<string> {
const inference = this.attemptCrudeInference(name)
return inference.name
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return resolve(this.baseUrl, join(this.relativeRoot, name))
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return null
}
}

View File

@@ -0,0 +1,101 @@
import StreamZip from 'node-stream-zip'
import { Stats } from 'fs-extra'
import { Type } from 'helios-distribution-types'
import { join } from 'path'
import { resolve } from 'url'
import { capitalize } from '../../../util/stringutils'
import { LiteMod } from '../../../model/liteloader/litemod'
import { ToggleableModuleStructure } from './ToggleableModule.struct'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
import { LibraryType } from '../../../model/claritas/ClaritasLibraryType'
import { MetadataUtil } from '../../../util/MetadataUtil'
export class LiteModStructure extends ToggleableModuleStructure {
private liteModMetadata: {[property: string]: LiteMod | undefined} = {}
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'litemods', baseUrl, minecraftVersion, Type.LiteMod)
}
public getLoggerName(): string {
return 'LiteModStructure'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const liteModData = await this.getLiteModMetadata(name, path)
return this.generateMavenIdentifier(
MetadataUtil.completeGroupInference(this.getClaritasGroup(path), liteModData.name), liteModData.name, `${liteModData.version}-${liteModData.mcversion}`)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getLiteModMetadata(name, path)).name)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return resolve(this.baseUrl, join(this.relativeRoot, this.getActiveNamespace(), name))
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return null
}
protected getClaritasType(): LibraryType {
return LibraryType.LITELOADER
}
private getLiteModMetadata(name: string, path: string): Promise<LiteMod> {
return new Promise((resolve, reject) => {
if (!Object.prototype.hasOwnProperty.call(this.liteModMetadata, name)) {
const zip = new StreamZip({
file: path,
storeEntries: true
})
zip.on('error', err => reject(err))
zip.on('ready', () => {
try {
const res = this.processZip(zip, name)
zip.close()
resolve(res)
return
} catch(err) {
zip.close()
reject(err)
return
}
})
} else {
resolve(this.liteModMetadata[name] as LiteMod)
return
}
})
}
private processZip(zip: StreamZip, name: string): LiteMod {
let raw: Buffer | undefined
try {
raw = zip.entryDataSync('litemod.json')
} catch(err) {
// ignored
}
if (raw) {
this.liteModMetadata[name] = JSON.parse(raw.toString()) as LiteMod
} else {
throw new Error(`Litemod ${name} does not contain litemod.json file.`)
}
return this.liteModMetadata[name] as LiteMod
}
}

View File

@@ -0,0 +1,185 @@
import { createHash } from 'crypto'
import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra'
import { Module, Type, TypeMetadata } from 'helios-distribution-types'
import { resolve } from 'path'
import { BaseModelStructure } from '../BaseModel.struct'
import { LibraryType } from '../../../model/claritas/ClaritasLibraryType'
import { ClaritasResult, ClaritasModuleMetadata } from '../../../model/claritas/ClaritasResult'
import { ClaritasWrapper } from '../../../util/java/ClaritasWrapper'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
export interface ModuleCandidate {
file: string
filePath: string
stats: Stats
}
export interface ClaritasException {
exceptionName: string
proxyMetadata: ClaritasModuleMetadata
}
export abstract class ModuleStructure extends BaseModelStructure<Module> {
private readonly crudeRegex = /(.+?)-(.+).[jJ][aA][rR]/
protected readonly DEFAULT_VERSION = '0.0.0'
protected claritasResult!: ClaritasResult
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string,
baseUrl: string,
protected minecraftVersion: MinecraftVersion,
protected type: Type,
protected filter?: ((name: string, path: string, stats: Stats) => boolean)
) {
super(absoluteRoot, relativeRoot, structRoot, baseUrl)
}
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doModuleRetrieval(await this._doModuleDiscovery(this.containerDirectory))
}
return this.resolvedModels
}
protected getDefaultGroup(): string {
return `generated.${this.type.toLowerCase()}`
}
protected generateMavenIdentifier(group: string, id: string, version: string): string {
return `${group}:${id}:${version}@${TypeMetadata[this.type].defaultExtension}`
}
protected attemptCrudeInference(name: string): { name: string, version: string } {
const result = this.crudeRegex.exec(name)
if(result != null) {
return {
name: result[1],
version: result[2]
}
} else {
return {
name: name.substring(0, name.lastIndexOf('.')),
version: this.DEFAULT_VERSION
}
}
}
protected getClaritasGroup(path: string): string {
return this.claritasResult[path]?.group || this.getDefaultGroup()
}
protected getClaritasExceptions(): ClaritasException[] {
return []
}
protected getClaritasType(): LibraryType | null {
return null
}
protected async abstract getModuleId(name: string, path: string): Promise<string>
protected async abstract getModuleName(name: string, path: string): Promise<string>
protected async abstract getModuleUrl(name: string, path: string, stats: Stats): Promise<string>
protected async abstract getModulePath(name: string, path: string, stats: Stats): Promise<string | null>
protected async parseModule(file: string, filePath: string, stats: Stats): Promise<Module> {
const buf = await readFile(filePath)
const mdl: Module = {
id: await this.getModuleId(file, filePath),
name: await this.getModuleName(file, filePath),
type: this.type,
artifact: {
size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'),
url: await this.getModuleUrl(file, filePath, stats)
}
}
const pth = await this.getModulePath(file, filePath, stats)
if (pth) {
mdl.artifact.path = pth
}
return mdl
}
protected async _doModuleDiscovery(scanDirectory: string): Promise<ModuleCandidate[]> {
const moduleCandidates: ModuleCandidate[] = []
if (await pathExists(scanDirectory)) {
const files = await readdir(scanDirectory)
for (const file of files) {
const filePath = resolve(scanDirectory, file)
const stats = await lstat(filePath)
if (stats.isFile()) {
if(this.filter == null || this.filter(file, filePath, stats)) {
moduleCandidates.push({file, filePath, stats})
}
}
}
}
return moduleCandidates
}
protected async invokeClaritas(moduleCandidates: ModuleCandidate[]): Promise<void> {
if(this.getClaritasType() != null) {
const claritasExecutor = new ClaritasWrapper(this.absoluteRoot)
let claritasCandidates = moduleCandidates
const exceptionCandidates: [ModuleCandidate, ClaritasException][] = []
for(const exception of this.getClaritasExceptions()) {
const exceptionCandidate = moduleCandidates.find((value) => value.file.toLowerCase().indexOf(exception.exceptionName) > -1)
if(exceptionCandidate != null) {
exceptionCandidates.push([exceptionCandidate, exception])
claritasCandidates = claritasCandidates.filter((value) => value.file.toLowerCase().indexOf(exception.exceptionName) === -1)
}
}
this.claritasResult = await claritasExecutor.execute(
this.getClaritasType()!,
this.minecraftVersion,
claritasCandidates.map(entry => entry.filePath)
)
if(this.claritasResult == null) {
this.logger.error('Failed to process Claritas result!')
} else {
for(const [candidate, exception] of exceptionCandidates) {
this.claritasResult[candidate.filePath] = exception.proxyMetadata
}
}
}
}
protected async _doModuleRetrieval(moduleCandidates: ModuleCandidate[], options?: {
preProcess?: (candidate: ModuleCandidate) => void
postProcess?: (module: Module) => void
}): Promise<Module[]> {
const accumulator: Module[] = []
if(moduleCandidates.length > 0) {
// Invoke Claritas and attach result to class.
await this.invokeClaritas(moduleCandidates)
// Process Modules
for(const candidate of moduleCandidates) {
options?.preProcess?.(candidate)
const mdl = await this.parseModule(candidate.file, candidate.filePath, candidate.stats)
options?.postProcess?.(mdl)
accumulator.push(mdl)
}
}
return accumulator
}
}

View File

@@ -0,0 +1,82 @@
import { ModuleStructure, ModuleCandidate } from './Module.struct'
import { Type, Module } from 'helios-distribution-types'
import { Stats, mkdirs } from 'fs-extra'
import { resolve } from 'path'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
export enum ToggleableNamespace {
REQUIRED = 'required',
OPTIONAL_ON = 'optionalon',
OPTIONAL_OFF = 'optionaloff'
}
export interface ToggleableModuleCandidate extends ModuleCandidate {
namespace: ToggleableNamespace
}
export abstract class ToggleableModuleStructure extends ModuleStructure {
private activeNamespace: string | undefined
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion,
type: Type,
filter?: ((name: string, path: string, stats: Stats) => boolean)
) {
super(absoluteRoot, relativeRoot, structRoot, baseUrl, minecraftVersion, type, filter)
}
public async init(): Promise<void> {
await super.init()
for(const namespace of Object.values(ToggleableNamespace)) {
await mkdirs(resolve(this.containerDirectory, namespace))
}
}
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
const moduleCandidates: ToggleableModuleCandidate[] = []
for(const value of Object.values(ToggleableNamespace)) {
moduleCandidates.push(...(await super._doModuleDiscovery(resolve(this.containerDirectory, value))).map(val => ({...val, namespace: value})))
}
this.resolvedModels = await this._doModuleRetrieval(moduleCandidates, {
preProcess: (candidate) => {
this.activeNamespace = (candidate as ToggleableModuleCandidate).namespace
},
postProcess: (module) => {
this.getNamespaceMapper(this.activeNamespace as ToggleableNamespace)(module)
}
})
// Cleanup
this.activeNamespace = undefined
}
return this.resolvedModels
}
protected getActiveNamespace(): string {
return this.activeNamespace || ''
}
protected getNamespaceMapper(namespace: ToggleableNamespace): (x: Module) => void {
switch(namespace) {
case ToggleableNamespace.REQUIRED:
return () => { /* do nothing */ }
case ToggleableNamespace.OPTIONAL_ON:
return (x) => x.required = { value: false }
case ToggleableNamespace.OPTIONAL_OFF:
return (x) => x.required = { value: false, def: false }
}
}
}

View File

@@ -0,0 +1,184 @@
import StreamZip from 'node-stream-zip'
import toml from 'toml'
import { capitalize } from '../../../../util/stringutils'
import { VersionUtil } from '../../../../util/versionutil'
import { ModsToml } from '../../../../model/forge/modstoml'
import { BaseForgeModStructure } from '../ForgeMod.struct'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
export class ForgeModStructure113 extends BaseForgeModStructure {
public static readonly IMPLEMENTATION_VERSION_REGEX = /^Implementation-Version: (.+)[\r\n]/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return VersionUtil.isVersionAcceptable(version, [13, 14, 15, 16])
}
private forgeModMetadata: {[property: string]: ModsToml | undefined} = {}
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion)
}
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return ForgeModStructure113.isForVersion(version, libraryVersion)
}
public getLoggerName(): string {
return 'ForgeModStructure (1.13)'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const fmData = await this.getForgeModMetadata(name, path)
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.mods[0].modId, fmData.mods[0].version)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getForgeModMetadata(name, path)).mods[0].displayName)
}
private getForgeModMetadata(name: string, path: string): Promise<ModsToml> {
return new Promise((resolve, reject) => {
if (!Object.prototype.hasOwnProperty.call(this.forgeModMetadata, name)) {
const zip = new StreamZip({
file: path,
storeEntries: true
})
zip.on('error', err => reject(err))
zip.on('ready', () => {
try {
const res = this.processZip(zip, name, path)
zip.close()
resolve(res)
return
} catch(err) {
zip.close()
reject(err)
return
}
})
} else {
resolve(this.forgeModMetadata[name] as ModsToml)
return
}
})
}
private processZip(zip: StreamZip, name: string, path: string): ModsToml {
// Optifine is a tweak that can be loaded as a forge mod. It does not
// appear to contain a mcmod.info class. This a special case we will
// account for.
if (name.toLowerCase().indexOf('optifine') > -1) {
// Read zip for changelog.txt
let changelogBuf: Buffer
try {
changelogBuf = zip.entryDataSync('changelog.txt')
} catch(err) {
throw new Error('Failed to read OptiFine changelog.')
}
const info = changelogBuf.toString().split('\n')[0].trim()
const version = info.split(' ')[1]
this.forgeModMetadata[name] = ({
modLoader: 'javafml',
loaderVersion: '',
mods: [{
modId: 'optifine',
version,
displayName: 'OptiFine',
description: `OptiFine is a Minecraft optimization mod.
It allows Minecraft to run faster and look better with full support for shaders, HD textures and many configuration options.`
}]
})
return this.forgeModMetadata[name] as ModsToml
}
let raw: Buffer | undefined
try {
raw = zip.entryDataSync('META-INF/mods.toml')
} catch(err) {
// ignored
}
if (raw) {
try {
const parsed = toml.parse(raw.toString()) as ModsToml
this.forgeModMetadata[name] = parsed
} catch (err) {
this.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`)
}
} else {
this.logger.error(`ForgeMod ${name} does not contain mods.toml file.`)
}
const cRes = this.claritasResult?.[path]
if(cRes == null) {
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
this.logger.error('Is this mod malformated or does Claritas need an update?')
}
const claritasId = cRes?.id
const crudeInference = this.attemptCrudeInference(name)
if(this.forgeModMetadata[name] != null) {
const x = this.forgeModMetadata[name]!
for(const entry of x.mods) {
if(entry.modId === this.EXAMPLE_MOD_ID) {
entry.modId = this.discernResult(claritasId, crudeInference.name.toLowerCase())
entry.displayName = crudeInference.name
}
if (entry.version === '${file.jarVersion}') {
let version = crudeInference.version
try {
const manifest = zip.entryDataSync('META-INF/MANIFEST.MF')
const keys = manifest.toString().split('\n')
this.logger.debug(keys)
for (const key of keys) {
const match = ForgeModStructure113.IMPLEMENTATION_VERSION_REGEX.exec(key)
if (match != null) {
version = match[1]
}
}
this.logger.debug(`ForgeMod ${name} contains a version wildcard, inferring ${version}`)
} catch {
this.logger.debug(`ForgeMod ${name} contains a version wildcard yet no MANIFEST.MF.. Defaulting to ${version}`)
}
entry.version = version
}
}
} else {
this.forgeModMetadata[name] = ({
modLoader: 'javafml',
loaderVersion: '',
mods: [{
modId: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
version: crudeInference.version,
displayName: crudeInference.name,
description: ''
}]
})
}
return this.forgeModMetadata[name] as ModsToml
}
}

View File

@@ -0,0 +1,185 @@
import StreamZip from 'node-stream-zip'
import { capitalize } from '../../../../util/stringutils'
import { VersionUtil } from '../../../../util/versionutil'
import { McModInfo } from '../../../../model/forge/mcmodinfo'
import { McModInfoList } from '../../../../model/forge/mcmodinfolist'
import { BaseForgeModStructure } from '../ForgeMod.struct'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
import { ForgeModType_1_7 } from '../../../../model/claritas/ClaritasResult'
export class ForgeModStructure17 extends BaseForgeModStructure {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
}
private forgeModMetadata: {[property: string]: McModInfo | undefined} = {}
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion)
}
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return ForgeModStructure17.isForVersion(version, libraryVersion)
}
public getLoggerName(): string {
return 'ForgeModStructure (1.7)'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const fmData = await this.getForgeModMetadata(name, path)
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.modid, fmData.version)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getForgeModMetadata(name, path)).name)
}
private getForgeModMetadata(name: string, path: string): Promise<McModInfo> {
return new Promise((resolve, reject) => {
if (!Object.prototype.hasOwnProperty.call(this.forgeModMetadata, name)) {
const zip = new StreamZip({
file: path,
storeEntries: true
})
zip.on('error', err => reject(err))
zip.on('ready', () => {
try {
const res = this.processZip(zip, name, path)
zip.close()
resolve(res)
return
} catch(err) {
zip.close()
reject(err)
return
}
})
} else {
resolve(this.forgeModMetadata[name] as McModInfo)
return
}
})
}
private isMalformedVersion(version: string): boolean {
// Ex. empty, @VERSION@, ${version}
return version.trim().length === 0 || version.indexOf('@') > -1 || version.indexOf('$') > -1
}
private processZip(zip: StreamZip, name: string, path: string): McModInfo {
// Optifine is a tweak that can be loaded as a forge mod. It does not
// appear to contain a mcmod.info class. This a special case we will
// account for.
if (name.toLowerCase().indexOf('optifine') > -1) {
// Read zip for changelog.txt
let changelogBuf: Buffer
try {
changelogBuf = zip.entryDataSync('changelog.txt')
} catch(err) {
throw new Error('Failed to read OptiFine changelog.')
}
const info = changelogBuf.toString().split('\n')[0].trim()
const version = info.split(' ')[1]
this.forgeModMetadata[name] = ({
modid: 'optifine',
name: info,
version,
mcversion: version.substring(0, version.indexOf('_'))
}) as McModInfo
return this.forgeModMetadata[name] as McModInfo
}
let raw: Buffer | undefined
try {
raw = zip.entryDataSync('mcmod.info')
} catch(err) {
// ignored
}
if (raw) {
// Assuming the main mod will be the first entry in this file.
try {
const resolved = JSON.parse(raw.toString()) as (McModInfoList | McModInfo[])
if (Object.prototype.hasOwnProperty.call(resolved, 'modListVersion')) {
this.forgeModMetadata[name] = (resolved as McModInfoList).modList[0]
} else {
this.forgeModMetadata[name] = (resolved as McModInfo[])[0]
}
} catch (err) {
this.logger.error(`ForgeMod ${name} contains an invalid mcmod.info file.`)
}
} else {
this.logger.warn(`ForgeMod ${name} does not contain mcmod.info file.`)
}
const cRes = this.claritasResult[path]
if(cRes == null) {
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
this.logger.error('Is this mod malformated or does Claritas need an update?')
} else {
switch(cRes.modType!) {
case ForgeModType_1_7.CORE_MOD:
this.logger.info(`CORE_MOD Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities are limited.`)
break
case ForgeModType_1_7.TWEAKER:
this.logger.info(`TWEAKER Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities may be limited.`)
break
case ForgeModType_1_7.UNKNOWN:
this.logger.error(`Jar file ${name} is not a ForgeMod. Is it a library?`)
break
}
}
const claritasId = cRes?.id
const claritasVersion = cRes?.version
const claritasName = cRes?.name
// Validate
const crudeInference = this.attemptCrudeInference(name)
if(this.forgeModMetadata[name] != null) {
const x = this.forgeModMetadata[name]!
if(x.modid == null || x.modid === '' || x.modid === this.EXAMPLE_MOD_ID) {
x.modid = this.discernResult(claritasId, crudeInference.name.toLowerCase())
x.name = this.discernResult(claritasName, crudeInference.name)
}
if(this.forgeModMetadata[name]!.version != null) {
const isMalformedVersion = this.isMalformedVersion(this.forgeModMetadata[name]!.version)
if(isMalformedVersion) {
x.version = this.discernResult(claritasVersion, crudeInference.version)
}
} else {
x.version = this.discernResult(claritasVersion, crudeInference.version)
}
} else {
this.forgeModMetadata[name] = ({
modid: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
name: this.discernResult(claritasName, crudeInference.name),
version: this.discernResult(claritasVersion, crudeInference.version)
}) as McModInfo
}
return this.forgeModMetadata[name] as McModInfo
}
}