mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
infrastructure for inline vfs and clients
This commit is contained in:
1 parent
d81d885767
commit
39745dd475
19 files changed
+133
-64
No files matched your search
@@ -1,79 +0,0 @@
|
||||
import { readDVPL } from "@blitzkit/core";
|
||||
import { Options, parse as parseCSV } from "csv-parse/sync";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import { normalize } from "path/posix";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
export abstract class AbstractVFS {
|
||||
textDecoder = new TextDecoder();
|
||||
private xmlParser = new XMLParser();
|
||||
|
||||
abstract init(): Promise<typeof this>;
|
||||
|
||||
abstract dispose(): void;
|
||||
|
||||
abstract has(path: string): Promise<boolean>;
|
||||
|
||||
abstract raw(path: string): Promise<Uint8Array>;
|
||||
|
||||
async resolve(path: string) {
|
||||
const normalized = normalize(path);
|
||||
if (await this.has(normalized)) return normalized;
|
||||
|
||||
const dvplPath = `${normalized}.dvpl`;
|
||||
if (await this.has(dvplPath)) return dvplPath;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async assert(path: string) {
|
||||
const resolved = await this.resolve(path);
|
||||
|
||||
if (resolved === null) throw new Error(`File not found: ${path}`);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async file(file: string) {
|
||||
const resolved = await this.assert(file);
|
||||
const raw = await this.raw(resolved);
|
||||
let buffer = raw;
|
||||
|
||||
if (resolved.endsWith(".dvpl")) {
|
||||
buffer = new Uint8Array(readDVPL(Buffer.from(raw)));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
abstract dir(path: string): Promise<string[]>;
|
||||
|
||||
async text(path: string) {
|
||||
const file = await this.file(path);
|
||||
return this.textDecoder.decode(file);
|
||||
}
|
||||
|
||||
async yaml<Type>(path: string) {
|
||||
const file = await this.text(path);
|
||||
return parseYaml(file) as Type;
|
||||
}
|
||||
|
||||
async xml<Type>(path: string) {
|
||||
const file = await this.text(path);
|
||||
return this.xmlParser.parse(file) as Type;
|
||||
}
|
||||
|
||||
async _csv(text: string, options: Options = {}) {
|
||||
const parsed = parseCSV(text, options);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async csv(path: string, options: Options = {}) {
|
||||
const text = await this.text(path);
|
||||
return await this._csv(text, options);
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
export class LocalVFS extends AbstractVFS {
|
||||
constructor(private base: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
const file = Bun.file(`${this.base}/${path}`);
|
||||
return await file.exists();
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
const file = Bun.file(`${this.base}/${path}`);
|
||||
const buffer = await file.arrayBuffer();
|
||||
const array = new Uint8Array(buffer);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
try {
|
||||
return await readdir(`${this.base}/${path}`);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
export class MixedVFS extends AbstractVFS {
|
||||
constructor(private vfses: AbstractVFS[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
for (const vfs of this.vfses) {
|
||||
await vfs.init();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const vfs of this.vfses) {
|
||||
vfs.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
for (const vfs of this.vfses) {
|
||||
if (await vfs.has(path)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
for (const vfs of this.vfses) {
|
||||
if (await vfs.has(path)) return await vfs.raw(path);
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
const files = new Set<string>();
|
||||
|
||||
for (const vfs of this.vfses) {
|
||||
for (const file of await vfs.dir(path)) {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(files);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import SteamUser, { EConnectionProtocol } from "steam-user";
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
interface SteamManifestFile {
|
||||
chunks: {
|
||||
sha: string;
|
||||
crc: number;
|
||||
offset: string;
|
||||
cb_original: number;
|
||||
cb_compressed: number;
|
||||
}[];
|
||||
filename: string;
|
||||
size: string;
|
||||
flags: number;
|
||||
sha_filename: string;
|
||||
sha_content: string;
|
||||
linktarget: unknown;
|
||||
}
|
||||
|
||||
interface SteamManifest {
|
||||
files: SteamManifestFile[];
|
||||
depot_id: number;
|
||||
gid_manifest: string;
|
||||
creation_time: number;
|
||||
filenames_encrypted: boolean;
|
||||
cb_disk_original: string;
|
||||
cb_disk_compressed: string;
|
||||
unique_chunks: number;
|
||||
crc_encrypted: number;
|
||||
crc_clear: number;
|
||||
}
|
||||
|
||||
export class SteamVFS extends AbstractVFS {
|
||||
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
|
||||
private manifest: Map<string, SteamManifestFile> = new Map();
|
||||
|
||||
constructor(
|
||||
private username: string,
|
||||
private password: string,
|
||||
private app: number,
|
||||
private depot: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.steam.logOn({
|
||||
accountName: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
|
||||
|
||||
const productInfo = await this.steam.getProductInfo([this.app], []);
|
||||
const manifest: SteamManifest = await new Promise((resolve) => {
|
||||
// @ts-expect-error
|
||||
this.steam.getManifest(
|
||||
this.app,
|
||||
this.depot,
|
||||
// @ts-expect-error
|
||||
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
|
||||
.gid,
|
||||
"public",
|
||||
// @ts-expect-error
|
||||
(_, response) => {
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
for (const file of manifest.files) {
|
||||
const path = file.filename.replaceAll("\\", "/");
|
||||
this.manifest.set(path, file);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
return this.manifest.has(path);
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
const fileManifest = this.manifest.get(path)!;
|
||||
const downloaded: { type: "complete"; file: Buffer } =
|
||||
// @ts-expect-error
|
||||
await this.steam.downloadFile(this.app, this.depot, fileManifest);
|
||||
|
||||
return new Uint8Array(downloaded.file);
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
const parentSegments = path.split("/").length;
|
||||
const children = new Set<string>();
|
||||
|
||||
for (const child of this.manifest.keys()) {
|
||||
if (!child.startsWith(path) || child === path) continue;
|
||||
|
||||
const childSegments = child.split("/");
|
||||
const nextSegment = childSegments[parentSegments];
|
||||
|
||||
children.add(nextSegment);
|
||||
}
|
||||
|
||||
return Array.from(children);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.steam.logOff();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { normalize } from "path/posix";
|
||||
import { HTTPRangeReader, unzip, ZipEntry } from "unzipit";
|
||||
import { AbstractVFS } from "./abstract";
|
||||
|
||||
export class ZipVFS extends AbstractVFS {
|
||||
entries = new Map<string, ZipEntry>();
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private root = "app/",
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async init() {
|
||||
const reader = new HTTPRangeReader(this.url);
|
||||
const { entries } = await unzip(reader);
|
||||
|
||||
for (const entry of Object.values(entries)) {
|
||||
const normalized = normalize(entry.name.slice(this.root.length));
|
||||
this.entries.set(normalized, entry);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async has(path: string) {
|
||||
return this.entries.has(path);
|
||||
}
|
||||
|
||||
async raw(path: string) {
|
||||
const arrayBuffer = await this.entries.get(path)!.arrayBuffer();
|
||||
return new Uint8Array(arrayBuffer);
|
||||
}
|
||||
|
||||
async dir(path: string) {
|
||||
const parentSegments = path.split("/").length;
|
||||
const children = new Set<string>();
|
||||
|
||||
for (const child of this.entries.keys()) {
|
||||
if (!child.startsWith(path) || child === path) continue;
|
||||
|
||||
const childSegments = child.split("/");
|
||||
const nextSegment = childSegments[parentSegments];
|
||||
|
||||
children.add(nextSegment);
|
||||
}
|
||||
|
||||
return Array.from(children);
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
}
|
||||
Reference in new issue
Block a user