don't make uneccesary file changes
/ release (push) Failing after 57s

This commit is contained in:
2026-02-18 19:00:20 -08:00
parent 8be47829c5
commit d4a429e0b0
+73 -34
View File
@@ -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