+73
-34
@@ -1,4 +1,4 @@
|
||||
import type { FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWriteChunkType, Response } from "@sugoidogo/importable-types-web"
|
||||
import type { FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemRemoveOptions, FileSystemWriteChunkType, Response } from "@sugoidogo/importable-types-web"
|
||||
import type { Hash, HashFormat, PackFileMetaData, PackIndex, PackMetaData, Path, Side, Url } from "./types"
|
||||
import { parseTOML, stringifyTOML } from "confbox"
|
||||
import { forAsync } from "./forAsync.ts"
|
||||
@@ -9,7 +9,11 @@ import { ModrinthV2Client, type Project } from "@xmcl/modrinth"
|
||||
const pathSeperatorRegex = /[\\\/]/
|
||||
const minecrafCurseforgeGameID = 432
|
||||
|
||||
function encode(input: string) { return new TextEncoder().encode(input) }
|
||||
function encode(input: string) {
|
||||
// for some reason, every time we re-serialize a TOML file it gains an extra newline
|
||||
input = input.trimEnd() + "\n"
|
||||
return new TextEncoder().encode(input)
|
||||
}
|
||||
function decode(input: Uint8Array<ArrayBuffer>) { return new TextDecoder().decode(input) }
|
||||
|
||||
function assertOK(response: Response) {
|
||||
@@ -42,6 +46,14 @@ async function writeFile(fileHandle: FileSystemFileHandle, data: FileSystemWrite
|
||||
await writable.close()
|
||||
}
|
||||
|
||||
async function deleteFiles(directoryHandle: FileSystemDirectoryHandle, path: Path, options?: FileSystemRemoveOptions) {
|
||||
const pathSegments = path.split(pathSeperatorRegex)
|
||||
const basename = pathSegments.pop()
|
||||
const dirname = pathSegments.join('/')
|
||||
directoryHandle = await getDirectoryHandle(directoryHandle, dirname)
|
||||
return directoryHandle.removeEntry(basename, options)
|
||||
}
|
||||
|
||||
async function getHash(data: Uint8Array<ArrayBuffer> | string, format: HashFormat) {
|
||||
if (typeof data === 'string') data = new TextEncoder().encode(data)
|
||||
if (format.startsWith('sha')) { // copied from https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Non-cryptographic_uses_of_subtle_crypto#hashing_a_file
|
||||
@@ -98,6 +110,13 @@ export class PackwizPack {
|
||||
packIndex: PackIndex
|
||||
/** This object stores the contents of {@link PackFileMetaData} files referenced in the {@link PackIndex} */
|
||||
packFiles: { [key: Path]: PackFileMetaData | undefined }
|
||||
/**
|
||||
* This list informs the save function on what files need updates to save in-memory changes,
|
||||
* allowing you to make multiple changes before incurring writes to storage.
|
||||
* If you make any changes to the pack outside of the functions provided by this API,
|
||||
* you will need to update this list.
|
||||
*/
|
||||
packFilesModified: { [key: Path]: "write" | "delete" | undefined }
|
||||
/** Creates a new, empty modpack */
|
||||
constructor(packDirectoryHandle: FileSystemDirectoryHandle, packName: string, versions: PackMetaData["versions"]) {
|
||||
this.packDirectoryHandle = packDirectoryHandle
|
||||
@@ -116,6 +135,7 @@ export class PackwizPack {
|
||||
"files": [] as any
|
||||
}
|
||||
this.packFiles = {}
|
||||
this.packFilesModified = {}
|
||||
}
|
||||
/** Fetches a modpack from an HTTP/HTTPS url */
|
||||
static async fetchPack(packDirectoryHandle: FileSystemDirectoryHandle, packUrl: Url): Promise<PackwizPack> {
|
||||
@@ -265,9 +285,21 @@ export class PackwizPack {
|
||||
const packIndexPathSegments = this.packMetaData.index.file.split(pathSeperatorRegex)
|
||||
packIndexPathSegments.pop()
|
||||
const packIndexDirname = packIndexPathSegments.join("/")
|
||||
await forAsync(this.packIndex.files, async entry => {
|
||||
let deletedFileCount = 0
|
||||
await forAsync(Array(this.packIndex.files.length).keys(), async index => {
|
||||
index -= deletedFileCount
|
||||
const entry = this.packIndex.files[index]
|
||||
const fileOperation = this.packFilesModified[entry.file]
|
||||
delete this.packFilesModified[entry.file]
|
||||
if (!fileOperation) return
|
||||
const filePath = packIndexDirname + "/" + entry.file
|
||||
if (fileOperation === "delete") {
|
||||
this.packIndex.files.splice(index, 1)
|
||||
deletedFileCount++
|
||||
return deleteFiles(this.packDirectoryHandle, filePath)
|
||||
}
|
||||
const packFileMetaData = this.packFiles[entry.file]
|
||||
const fileHandle = await getFileHandle(this.packDirectoryHandle, packIndexDirname + "/" + entry.file, { "create": true })
|
||||
const fileHandle = await getFileHandle(this.packDirectoryHandle, filePath, { "create": true })
|
||||
let packFileBytes: Uint8Array<ArrayBuffer>
|
||||
if (packFileMetaData) {
|
||||
packFileBytes = encode(stringifyTOML(packFileMetaData))
|
||||
@@ -284,6 +316,18 @@ export class PackwizPack {
|
||||
delete entry.hash
|
||||
}
|
||||
})
|
||||
await forAsync(Object.keys(this.packFilesModified), async filePath => {
|
||||
const packFilePath = packIndexDirname + "/" + filePath
|
||||
const fileHandle = await getFileHandle(this.packDirectoryHandle, packFilePath, { 'create': true })
|
||||
const packFileBytes = encode(stringifyTOML(this.packFiles[filePath]))
|
||||
writeFile(fileHandle, packFileBytes)
|
||||
this.packIndex.files.push({
|
||||
"file": filePath,
|
||||
"metafile": true,
|
||||
"hash": hashes ? (await getHash(packFileBytes, this.packIndex["hash-format"])).toString() : undefined
|
||||
})
|
||||
delete this.packFilesModified[filePath]
|
||||
})
|
||||
await getFileHandle(this.packDirectoryHandle, this.packMetaData.index.file, { "create": true }).then(async packIndexFileHandle => {
|
||||
const packIndexBytes = encode(stringifyTOML(this.packIndex))
|
||||
if (hashes) {
|
||||
@@ -325,23 +369,24 @@ export class PackwizPack {
|
||||
console.log("requesting info on " + hashPaths.size + " files")
|
||||
const curseforge = new CurseforgeV1Client(apiKey)
|
||||
const result = await curseforge.getFingerprintsMatchesByGameId(minecrafCurseforgeGameID, [...hashPaths.keys()])
|
||||
console.debug("got " + result.exactMatches.length + " matches")
|
||||
const modIDs: number[] = []
|
||||
for (const match of result.exactMatches) modIDs.push(match.file.modId)
|
||||
const mods = new Map<number, Mod>()
|
||||
for (const mod of await curseforge.getMods(modIDs)) mods.set(mod.id, mod)
|
||||
const replacedFilePaths: string[] = []
|
||||
let replacedFileCount = 0
|
||||
await forAsync(result.exactMatches, async match => {
|
||||
const filePath = hashPaths.get(match.file.fileFingerprint)
|
||||
if (!filePath) return
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileBasename = filePathSegments.pop()
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const fileDirectoryHandle = await getDirectoryHandle(indexDirectoryHandle, fileDirname)
|
||||
await fileDirectoryHandle.removeEntry(fileBasename)
|
||||
const mod = mods.get(match.file.modId)
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const newFilePath = fileDirname + "/" + mod.slug + ".pw.toml"
|
||||
this.packFilesModified[filePath] = "delete"
|
||||
this.packFilesModified[newFilePath] = "write"
|
||||
for (const hash of match.file.hashes) {
|
||||
if (hash.algo === 1) {
|
||||
this.packFiles[fileDirname + "/" + mod.slug + ".pw.toml"] = {
|
||||
this.packFiles[newFilePath] = {
|
||||
"filename": match.file.fileName,
|
||||
"name": mod.name,
|
||||
"download": {
|
||||
@@ -360,17 +405,12 @@ export class PackwizPack {
|
||||
"description": mod.summary
|
||||
}
|
||||
}
|
||||
replacedFileCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
replacedFilePaths.push(filePath)
|
||||
})
|
||||
for (let i = 0; i < this.packIndex.files.length; i++) {
|
||||
if (replacedFilePaths.includes(this.packIndex.files[i].file)) {
|
||||
this.packIndex.files.splice(i, 1)
|
||||
}
|
||||
}
|
||||
console.log("replaced " + replacedFilePaths.length + " files")
|
||||
console.log("replaced " + replacedFileCount + " files")
|
||||
return this.savePack()
|
||||
}
|
||||
/**
|
||||
@@ -400,17 +440,18 @@ export class PackwizPack {
|
||||
for (const match of Object.values(matches)) projectIDs.push(match.project_id)
|
||||
const projects = new Map<string, Project>()
|
||||
for (const project of await modrinth.getProjects(projectIDs)) projects.set(project.id, project)
|
||||
const replacedFilePaths: string[] = []
|
||||
console.log("got " + projectIDs.length + " matches")
|
||||
let replacedFileCount = 0
|
||||
await forAsync(Object.entries(matches), async entry => {
|
||||
const [hash, match] = entry
|
||||
const filePath = hashPaths.get(hash)
|
||||
if (!filePath) return
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileBasename = filePathSegments.pop()
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const fileDirectoryHandle = await getDirectoryHandle(indexDirectoryHandle, fileDirname)
|
||||
await fileDirectoryHandle.removeEntry(fileBasename)
|
||||
const mod = projects.get(match.project_id)
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const newFilePath = fileDirname + "/" + mod.slug + ".pw.toml"
|
||||
this.packFilesModified[filePath] = "delete"
|
||||
this.packFilesModified[newFilePath] = "write"
|
||||
let side: Side = "both"
|
||||
if (mod.server_side === "unsupported") {
|
||||
side = "client"
|
||||
@@ -419,7 +460,7 @@ export class PackwizPack {
|
||||
}
|
||||
for (const file of match.files) {
|
||||
if (file.hashes["sha512"] === hash) {
|
||||
this.packFiles[fileDirname + "/" + mod.slug + ".pw.toml"] = {
|
||||
this.packFiles[newFilePath] = {
|
||||
"filename": file.filename,
|
||||
"name": mod.title,
|
||||
"download": {
|
||||
@@ -440,14 +481,9 @@ export class PackwizPack {
|
||||
}
|
||||
}
|
||||
}
|
||||
replacedFilePaths.push(filePath)
|
||||
replacedFileCount++
|
||||
})
|
||||
for (let i = 0; i < this.packIndex.files.length; i++) {
|
||||
if (replacedFilePaths.includes(this.packIndex.files[i].file)) {
|
||||
this.packIndex.files.splice(i, 1)
|
||||
}
|
||||
}
|
||||
console.log("replaced " + replacedFilePaths.length + " files")
|
||||
console.log("replaced " + replacedFileCount + " files")
|
||||
return this.savePack()
|
||||
}
|
||||
/**
|
||||
@@ -468,8 +504,10 @@ export class PackwizPack {
|
||||
for (const file of await curseforge.getFiles([...fileIDPaths.keys()])) {
|
||||
if (file.downloadUrl) {
|
||||
cachedURLcount++
|
||||
this.packFiles[fileIDPaths.get(file.id)].download.url = file.downloadUrl
|
||||
delete this.packFiles[fileIDPaths.get(file.id)].download.mode
|
||||
const filePath = fileIDPaths.get(file.id)
|
||||
this.packFiles[filePath].download.url = file.downloadUrl
|
||||
delete this.packFiles[filePath].download.mode
|
||||
this.packFilesModified[filePath] = "write"
|
||||
}
|
||||
}
|
||||
console.log("cached " + cachedURLcount + " URLs")
|
||||
@@ -521,6 +559,7 @@ export class PackwizPack {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.packFilesModified[filePath] = "write"
|
||||
}
|
||||
console.log("merged " + mergedFileCount + " files")
|
||||
return this
|
||||
|
||||
Reference in New Issue
Block a user