feat: add a function to download assets and its test

This commit is contained in:
HerozDotExe 2025-10-15 21:56:40 +02:00
parent fe7a2936ba
commit 1612d95357
6 changed files with 97 additions and 21 deletions

47
src/core/assets.ts Normal file
View file

@ -0,0 +1,47 @@
import path from "path";
import { ensureDir, exists } from "../utils/fs";
import { AssetIndex, PoolFile, Version } from "../utils/types";
import fs from "fs/promises";
import { downloadFile, DownloadPool } from "../utils/fetch";
export async function download(destination: string, versionManifest: Version) {
await ensureDir(path.join(destination, "indexes"));
await ensureDir(path.join(destination, "objects"));
const assetIndexPath = path.join(
destination,
"indexes",
`${versionManifest.assetIndex.id}.json`,
);
if (!await exists(assetIndexPath)) {
await downloadFile({
url: versionManifest.assetIndex.url,
path: assetIndexPath,
});
}
const assetIndex: AssetIndex = JSON.parse(
await fs.readFile(assetIndexPath, { encoding: "utf-8" }),
);
const files: PoolFile[] = [];
for (const key in assetIndex.objects) {
if (Object.prototype.hasOwnProperty.call(assetIndex.objects, key)) {
const asset = assetIndex.objects[key];
const subhash = asset.hash.substring(0, 2);
const assetPath = path.join(destination, "objects", subhash, asset.hash);
const assetURL = `https://resources.download.minecraft.net/${asset.hash.substring(0, 2)}/${asset.hash}`;
await ensureDir(path.dirname(assetPath))
files.push({ url: assetURL, path: assetPath });
}
}
const dPool = new DownloadPool(files, 5);
await dPool.run();
}

View file

@ -2,3 +2,4 @@ export * as java from "./java";
export * as version from "./version";
export * as natives from "./natives";
export * as libraries from "./libraries";
export * as assets from "./assets";

View file

@ -9,20 +9,17 @@ export async function fetchJson<T>(url: string): Promise<T> {
return await (await fetch(url)).json();
}
export async function downloadFile(
file: { url: string; path: string },
signal: AbortSignal,
) {
export async function downloadFile(file: { url: string; path: string }) {
try {
if (!file) return;
await ensureDir(path.dirname(file.path), true);
const res = await fetch(file.url, { signal });
const res = await fetch(file.url);
if (!res.ok)
throw new Error(
`Failed to download ${file.url}: ${res.status} ${res.statusText}`,
);
await pipeline(res.body, fs.createWriteStream(file.path), { signal });
await pipeline(res.body, fs.createWriteStream(file.path));
} catch (error) {
if (error.name !== "AbortError") throw error;
return;

View file

@ -10,26 +10,15 @@ export class Task<T> {
progressCallback: (done: number, total: number) => void;
constructor(concurrency: number) {
this.controller = new AbortController();
this.concurrency = concurrency;
this.queue = new pQueue({ concurrency: this.concurrency });
this.done = 0;
this.total = 0;
}
async _run(
f: (element: unknown, signal: AbortSignal) => Promise<void>,
data: T[],
) {
try {
for (const element of data) {
this.queue.add(() => {
f(element, this.controller.signal);
});
}
} catch (error) {
if (error.name !== "AbortError") throw error;
return;
async _run(f: (element: unknown) => Promise<void>, data: T[]) {
for (const element of data) {
this.queue.add(() => f(element));
}
this.queue.on("completed", () => {
@ -42,7 +31,6 @@ export class Task<T> {
cancel() {
this.queue.clear();
this.controller.abort();
}
}

View file

@ -149,3 +149,12 @@ export type Version = {
};
};
};
export type AssetIndex = {
objects: {
[key: string]: {
hash: string;
size: number;
};
};
};

34
tests/assets.test.ts Normal file
View file

@ -0,0 +1,34 @@
import { expect, test, vi } from "vitest";
import * as nlk from "../dist/index.js";
import path from "path";
import fs from "fs/promises";
const assetsPath = path.join(import.meta.dirname, "temp/assets");
await fs.rm(assetsPath, { recursive: true, force: true });
await fs.mkdir(assetsPath, { recursive: true });
async function exists(path: string) {
try {
await fs.access(path, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
// mock os to linux for testing
vi.stubGlobal("process", { platform: "linux" });
test(
"download assets for 1.21.8 correctly",
{ timeout: 60 * 1000 },
async () => {
const versionManifest = await nlk.core.version.getVersionManifest("1.21.8");
await nlk.core.assets.download(assetsPath, versionManifest);
expect(
await exists(path.join(assetsPath, "objects", "00", "00c9fa8115347fb0220aaf72a8d7d921f5354112")),
"objects/00/00c9fa8115347fb0220aaf72a8d7d921f5354112 exists",
).toBe(true);
},
);