Begin work on new structure resolvers.

This commit is contained in:
Daniel Scalzi
2019-08-21 22:25:11 -04:00
parent b942f4df59
commit 2bbf732d8e
12 changed files with 413 additions and 87 deletions

View File

@@ -1,6 +1,8 @@
/* tslint:disable:no-shadowed-variable */
import { resolve } from 'path'
import { writeFile } from 'fs-extra'
import { resolve as resolvePath } from 'path'
import yargs from 'yargs'
import { DistributionStructure } from './model/struct/distribution.struct'
function rootOption(yargs: yargs.Argv) {
return yargs.option('root', {
@@ -10,7 +12,29 @@ function rootOption(yargs: yargs.Argv) {
global: true
})
.coerce({
root: resolve
root: resolvePath
})
}
function baseUrlOption(yargs: yargs.Argv) {
return yargs.option('baseUrl', {
describe: 'Base url of your file host.',
type: 'string',
demandOption: true,
global: true
})
.coerce({
baseUrl: (arg: string) => {
// Users must provide protocol in all other instances.
if (arg.indexOf('//') === -1) {
if (arg.toLowerCase().startsWith('localhost')) {
arg = 'http://' + arg
} else {
throw new TypeError('Please provide a URL protocol (ex. http:// or https://)')
}
}
return (new URL(arg)).toString()
}
})
}
@@ -32,9 +56,15 @@ const initRootCommand: yargs.CommandModule = {
yargs = rootOption(yargs)
return yargs
},
handler: (argv) => {
console.log(`Root set to ${argv.root}`)
console.log('Invoked init root.')
handler: async (argv) => {
console.debug(`Root set to ${argv.root}`)
console.debug('Invoked init root.')
try {
await new DistributionStructure(argv.root as string, '').init()
console.log(`Successfully created new root at ${argv.root}`)
} catch (error) {
console.error(`Failed to init new root at ${argv.root}`, error)
}
}
}
@@ -80,8 +110,8 @@ const generateServerCommand: yargs.CommandModule = {
})
},
handler: (argv) => {
console.log(`Root set to ${argv.root}`)
console.log(`Generating server ${argv.id} for Minecraft ${argv.version}.`,
console.debug(`Root set to ${argv.root}`)
console.debug(`Generating server ${argv.id} for Minecraft ${argv.version}.`,
`\n\t├ Include forge: ${argv.forge}`,
`\n\t└ Include liteloader: ${argv.liteloader}`)
}
@@ -92,12 +122,22 @@ const generateDistroCommand: yargs.CommandModule = {
describe: 'Generate a distribution index from the root file structure.',
builder: (yargs) => {
yargs = rootOption(yargs)
yargs = baseUrlOption(yargs)
yargs = namePositional(yargs)
return yargs
},
handler: (argv) => {
console.log(`Root set to ${argv.root}`)
console.log(`Invoked generate distro name ${argv.name}.json.`)
handler: async (argv) => {
console.debug(`Root set to ${argv.root}`)
console.debug(`Base Url set to ${argv.baseUrl}`)
console.debug(`Invoked generate distro name ${argv.name}.json.`)
try {
const distributionStruct = new DistributionStructure(argv.root as string, argv.baseUrl as string)
const distro = await distributionStruct.getSpecModel()
writeFile(resolvePath(argv.root as string, `${argv.name}.json`), JSON.stringify(distro, null, 2))
console.log(distro)
} catch (error) {
console.error(`Failed to generate distribution with root ${argv.root}.`, error)
}
}
}
@@ -122,7 +162,7 @@ const validateCommand: yargs.CommandModule = {
return namePositional(yargs)
},
handler: (argv) => {
console.log(`Invoked validate with name ${argv.name}.json`)
console.debug(`Invoked validate with name ${argv.name}.json`)
}
}

View File

@@ -1,45 +1,58 @@
export const Types: {[property: string]: Type} = {
export enum Type {
Library = 'Library',
ForgeHosted = 'ForgeHosted',
Forge = 'Forge',
LiteLoader = 'LiteLoader',
ForgeMod = 'ForgeMod',
LiteMod = 'LiteMod',
File = 'File',
VersionManifest = 'VersionManifest'
}
export interface TypeMetadata {
id: string
defaultExtension?: string
}
export const TypeMetadata: {[property: string]: TypeMetadata} = {
Library: {
id: 'Library',
id: Type.Library,
defaultExtension: 'jar'
},
/**
* @deprecated Will be replaced by Types.Forge.
*/
ForgeHosted: {
id: 'ForgeHosted',
id: Type.ForgeHosted,
defaultExtension: 'jar'
},
Forge: {
id: 'Forge',
id: Type.Forge,
defaultExtension: 'jar'
},
LiteLoader: {
id: 'LiteLoader',
id: Type.LiteLoader,
defaultExtension: 'jar'
},
ForgeMod: {
id: 'ForgeMod',
id: Type.ForgeMod,
defaultExtension: 'jar'
},
LiteMod: {
id: 'LiteMod',
id: Type.LiteMod,
defaultExtension: 'litemod'
},
File: {
id: 'File'
id: Type.File
},
VersionManifest: {
id: 'VersionManifest',
id: Type.VersionManifest,
defaultExtension: 'json'
}
}
export interface Type {
id: string
defaultExtension?: string
}

View File

@@ -0,0 +1,26 @@
import { mkdirs } from 'fs-extra'
import { join, resolve } from 'path'
import { ModelStructure } from './model.struct'
export abstract class BaseModelStructure<T> implements ModelStructure<T[]> {
protected resolvedModels: T[] | undefined
protected containerDirectory: string
constructor(
protected absoluteRoot: string,
protected relativeRoot: string,
protected structRoot: string,
protected baseUrl: string
) {
this.relativeRoot = join(relativeRoot, structRoot)
this.containerDirectory = resolve(absoluteRoot, structRoot)
}
public async init() {
mkdirs(this.containerDirectory)
}
public abstract async getSpecModel(): Promise<T[]>
}

View File

@@ -1,25 +1,32 @@
import { mkdirs } from 'fs-extra'
import { Distribution } from '../spec/distribution'
import { ModelStructure } from './model.struct'
import { ServerStructure } from './server.struct'
export class DistributionStructure implements ModelStructure<Distribution> {
private servers: ServerStructure[] | undefined
private serverStruct: ServerStructure
constructor(
private root: string
) {}
public getServers() {
return new ServerStructure(this.root).getSpecModel()
private absoluteRoot: string,
private baseUrl: string
) {
this.serverStruct = new ServerStructure(this.absoluteRoot, this.baseUrl)
}
public getSpecModel(): Distribution {
return {
version: '1.0.0',
rss: 'TODO',
servers: this.getServers()
}
public async init() {
await mkdirs(this.absoluteRoot)
await this.serverStruct.init()
}
public async getSpecModel() {
return new Promise(async (resolve) => {
resolve({
version: '1.0.0',
rss: '<FILL IN MANUALLY>',
servers: await this.serverStruct.getSpecModel()
})
}) as Promise<Distribution>
}
}

View File

@@ -1,5 +1,7 @@
export interface ModelStructure<T> {
getSpecModel(): T
init(): void
getSpecModel(): Promise<T>
}

View File

@@ -0,0 +1,28 @@
import { Stats } from 'fs'
import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct'
export class FileStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
) {
super(absoluteRoot, relativeRoot, 'files', baseUrl, Type.File)
}
protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
}

View File

@@ -0,0 +1,28 @@
import { Stats } from 'fs-extra'
import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct'
export class ForgeModStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
) {
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, Type.ForgeMod)
}
protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
}

View File

@@ -0,0 +1,29 @@
import { Stats } from 'fs-extra'
import { join } from 'path'
import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct'
export class LiteModStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
) {
super(absoluteRoot, relativeRoot, 'litemods', baseUrl, Type.LiteMod)
}
protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name
}
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return 'TODO'
}
}

View File

@@ -0,0 +1,67 @@
import { createHash } from 'crypto'
import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra'
import { resolve } from 'path'
import { Module } from '../../spec/module'
import { Type } from '../../spec/type'
import { BaseModelStructure } from '../basemodel.struct'
export abstract class ModuleStructure extends BaseModelStructure<Module> {
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string,
baseUrl: string,
protected type: Type
) {
super(absoluteRoot, relativeRoot, structRoot, baseUrl)
}
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doModuleRetrieval()
}
return this.resolvedModels
}
protected async abstract getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
protected async abstract getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
protected async abstract getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
protected async abstract getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
private async _doModuleRetrieval(): Promise<Module[]> {
const accumulator: Module[] = []
if (await pathExists(this.containerDirectory)) {
const files = await readdir(this.containerDirectory)
for (const file of files) {
const filePath = resolve(this.containerDirectory, file)
const stats = await lstat(filePath)
const buf = await readFile(filePath)
if (stats.isFile()) {
accumulator.push({
id: await this.getModuleId(file, filePath, stats, buf),
name: await this.getModuleName(file, filePath, stats, buf),
type: this.type,
required: {
value: false,
def: false
},
artifact: {
size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'),
url: await this.getModuleUrl(file, filePath, stats, buf),
path: await this.getModulePath(file, filePath, stats, buf)
}
})
}
}
}
return accumulator
}
}

View File

@@ -1,25 +1,100 @@
import { resolve } from 'path'
import { lstat, readdir } from 'fs-extra'
import { join, resolve as resolvePath } from 'path'
import { resolve as resolveUrl } from 'url'
import { Server } from '../spec/server'
import { ModelStructure } from './model.struct'
import { BaseModelStructure } from './basemodel.struct'
import { FileStructure } from './module/file.struct'
import { ForgeModStructure } from './module/forgemod.struct'
import { LiteModStructure } from './module/litemod.struct'
export class ServerStructure implements ModelStructure<Server[]> {
export class ServerStructure extends BaseModelStructure<Server> {
private servers: Server[] | undefined
private readonly ID_REGEX = /(.+-(.+)$)/
constructor(
private root: string
) {}
public getSpecModel(): Server[] {
if (this.servers == null) {
this.servers = this._doSeverRetrieval()
}
return this.servers
absoluteRoot: string,
baseUrl: string
) {
super(absoluteRoot, '', 'servers', baseUrl)
}
private _doSeverRetrieval(): Server[] {
const base = resolve(this.root, 'servers')
return [] // TODO
public async getSpecModel() {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doSeverRetrieval()
}
return this.resolvedModels
}
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) {
console.warn(`Server directory ${file} does not match the defined standard.`)
console.warn(`All server ids must end with -<minecraft version> (ex. -1.12.2)`)
continue
}
let iconUrl
// 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) {
console.warn(`No icon file found for server ${file}.`)
iconUrl = '<FILL IN MANUALLY>'
}
const forgeModStruct = new ForgeModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const forgeModModules = await forgeModStruct.getSpecModel()
const liteModStruct = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const liteModModules = await liteModStruct.getSpecModel()
const fileStruct = new FileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const fileModules = await fileStruct.getSpecModel()
const modules = [
...forgeModModules,
...liteModModules,
...fileModules
]
accumulator.push({
id: match[1],
name: '<FILL IN MANUALLY>',
description: '<FILL IN MANUALLY>',
icon: iconUrl,
version: '1.0.0',
address: '<FILL IN MANUALLY>',
minecraftVersion: match[2],
discord: {
shortId: '<FILL IN MANUALLY OR REMOVE>',
largeImageText: '<FILL IN MANUALLY OR REMOVE>',
largeImageKey: '<FILL IN MANUALLY OR REMOVE>'
},
mainServer: false,
autoconnect: false,
modules
})
} else {
console.warn(`Path ${file} in server directory is not a directory!`)
}
}
return accumulator
}
}