feat: add a function to download assets and its test
This commit is contained in:
parent
fe7a2936ba
commit
1612d95357
6 changed files with 97 additions and 21 deletions
47
src/core/assets.ts
Normal file
47
src/core/assets.ts
Normal 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();
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,3 +149,12 @@ export type Version = {
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type AssetIndex = {
|
||||
objects: {
|
||||
[key: string]: {
|
||||
hash: string;
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
34
tests/assets.test.ts
Normal file
34
tests/assets.test.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue