feat: local vfs

This commit is contained in:
TrèsAbhi committed 2026-04-01 21:34:44 -05:00
1 parent b9878a05a1
commit 34ee954502
6 files changed
+89 -42

No files matched your search

@@ -1,10 +1,12 @@
import { assertSecret } from "@blitzkit/core"; import { assertSecret } from "@blitzkit/core";
import { AbstractVFS } from "../core/vfs/abstract"; import { AbstractVFS } from "../core/vfs/abstract";
import { LocalVFS } from "../core/vfs/local";
import { SteamVFS } from "../core/vfs/steam"; import { SteamVFS } from "../core/vfs/steam";
import { ZipVFS } from "../core/vfs/zip"; import { ZipVFS } from "../core/vfs/zip";
const steamPattern = /steam:(\d+)\/(\d+)/; const steamPattern = /steam:(\d+)\/(\d+)/;
const zipPattern = /zip:(.+)/; const zipPattern = /zip:(.+)/;
const localePattern = /local:"?(.+)"?/;
const source = Bun.argv const source = Bun.argv
.find((arg) => arg.startsWith("--source=")) .find((arg) => arg.startsWith("--source="))
@@ -21,11 +23,14 @@ if (!source) {
assertSecret(import.meta.env.STEAM_USERNAME), assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD), assertSecret(import.meta.env.STEAM_PASSWORD),
app, app,
depot depot,
).init(); ).init();
} else if (zipPattern.test(source)) { } else if (zipPattern.test(source)) {
const [, url] = source.match(zipPattern)!; const [, url] = source.match(zipPattern)!;
resolved = await new ZipVFS(url).init(); resolved = await new ZipVFS(url).init();
} else if (localePattern.test(source)) {
const [, path] = source.match(localePattern)!;
resolved = await new LocalVFS(path).init();
} else { } else {
throw new Error("No valid source specified"); throw new Error("No valid source specified");
} }
@@ -43,7 +43,7 @@ export class AssetUploader {
constructor(public message: string) { constructor(public message: string) {
[this.owner, this.repo] = assertSecret( [this.owner, this.repo] = assertSecret(
import.meta.env.PUBLIC_ASSET_REPO import.meta.env.PUBLIC_ASSET_REPO,
).split("/"); ).split("/");
this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH); this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
} }
@@ -52,7 +52,7 @@ export class AssetUploader {
const blobPath = `${this.owner}/${this.repo}/${this.branch}/${change.path}`; const blobPath = `${this.owner}/${this.repo}/${this.branch}/${change.path}`;
const response = await fetch( const response = await fetch(
`https://raw.githubusercontent.com/${blobPath}`, `https://raw.githubusercontent.com/${blobPath}`,
{ headers: { "Accept-Encoding": "identity" } } { headers: { "Accept-Encoding": "identity" } },
); );
let diff: { change: number; status: DiffStatus }; let diff: { change: number; status: DiffStatus };
@@ -71,7 +71,7 @@ export class AssetUploader {
} else if (response.status === 200) { } else if (response.status === 200) {
if ( if (
this.heuristicFormats.some((format) => this.heuristicFormats.some((format) =>
change.path.endsWith(`.${format}`) change.path.endsWith(`.${format}`),
) )
) { ) {
/** /**
@@ -94,7 +94,7 @@ export class AssetUploader {
} }
} else { } else {
throw new Error( throw new Error(
`Unexpected status code ${response.status} for ${change.path}` `Unexpected status code ${response.status} for ${change.path}`,
); );
} }
@@ -104,7 +104,7 @@ export class AssetUploader {
console.log( console.log(
`${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${ `${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${
diff.change > 0 ? "+" : "" diff.change > 0 ? "+" : ""
}${diff.change.toLocaleString()}B) ${change.path}` }${diff.change.toLocaleString()}B) ${change.path}`,
); );
// flush BEFORE going over limit // flush BEFORE going over limit
@@ -160,8 +160,8 @@ export class AssetUploader {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout( setTimeout(
resolve, resolve,
Math.max(0, this.time_between_blobs + (start - end)) Math.max(0, this.time_between_blobs + (start - end)),
) ),
); );
blobs.push({ sha, path: change.path, mode: "100644", type: "blob" }); blobs.push({ sha, path: change.path, mode: "100644", type: "blob" });
@@ -170,11 +170,11 @@ export class AssetUploader {
break; break;
} catch { } catch {
console.warn( console.warn(
` failed ${change.path}; retrying in ${this.time_per_blob}ms...` ` failed ${change.path}; retrying in ${this.time_per_blob}ms...`,
); );
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, this.time_per_blob) setTimeout(resolve, this.time_per_blob),
); );
} }
} }
@@ -209,7 +209,7 @@ export class AssetUploader {
[Symbol.dispose]() { [Symbol.dispose]() {
if (this.changes.length > 0) { if (this.changes.length > 0) {
throw new Error( throw new Error(
`${this.changes.length} uncommitted changes. Did you forget to flush?` `${this.changes.length} uncommitted changes. Did you forget to flush?`,
); );
} }
} }
+8 -24
View File
@@ -11,24 +11,22 @@ export abstract class AbstractVFS {
abstract dispose(): void; abstract dispose(): void;
abstract has(path: string): boolean; abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>; abstract raw(path: string): Promise<Uint8Array>;
abstract paths(): string[]; async resolve(path: string) {
resolve(path: string) {
const normalized = normalize(path); const normalized = normalize(path);
if (this.has(normalized)) return normalized; if (await this.has(normalized)) return normalized;
const dvplPath = `${normalized}.dvpl`; const dvplPath = `${normalized}.dvpl`;
if (this.has(dvplPath)) return dvplPath; if (await this.has(dvplPath)) return dvplPath;
return null; return null;
} }
assert(path: string) { async assert(path: string) {
const resolved = this.resolve(path); const resolved = await this.resolve(path);
if (resolved === null) throw new Error(`File not found: ${path}`); if (resolved === null) throw new Error(`File not found: ${path}`);
@@ -36,7 +34,7 @@ export abstract class AbstractVFS {
} }
async file(file: string) { async file(file: string) {
const resolved = this.assert(file); const resolved = await this.assert(file);
const raw = await this.raw(resolved); const raw = await this.raw(resolved);
let buffer = raw; let buffer = raw;
@@ -47,21 +45,7 @@ export abstract class AbstractVFS {
return buffer; return buffer;
} }
dir(path: string) { abstract dir(path: string): Promise<string[]>;
const parentSegments = path.split("/").length;
const children = new Set<string>();
for (const child of this.paths()) {
if (!child.startsWith(path) || child === path) continue;
const childSegments = child.split("/");
const nextSegment = childSegments[parentSegments];
children.add(nextSegment);
}
return Array.from(children);
}
async text(path: string) { async text(path: string) {
const file = await this.file(path); const file = await this.file(path);
+31
View File
@@ -0,0 +1,31 @@
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) {
return await readdir(`${this.base}/${path}`);
}
dispose() {}
}
+15 -3
View File
@@ -76,7 +76,7 @@ export class SteamVFS extends AbstractVFS {
return this; return this;
} }
has(path: string) { async has(path: string) {
return this.manifest.has(path); return this.manifest.has(path);
} }
@@ -89,8 +89,20 @@ export class SteamVFS extends AbstractVFS {
return new Uint8Array(downloaded.file); return new Uint8Array(downloaded.file);
} }
paths() { async dir(path: string) {
return Array.from(this.manifest.keys()); 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() { dispose() {
+19 -4
View File
@@ -5,7 +5,10 @@ import { AbstractVFS } from "./abstract";
export class ZipVFS extends AbstractVFS { export class ZipVFS extends AbstractVFS {
entries = new Map<string, ZipEntry>(); entries = new Map<string, ZipEntry>();
constructor(private url: string, private root = "app/") { constructor(
private url: string,
private root = "app/",
) {
super(); super();
} }
@@ -21,7 +24,7 @@ export class ZipVFS extends AbstractVFS {
return this; return this;
} }
has(path: string) { async has(path: string) {
return this.entries.has(path); return this.entries.has(path);
} }
@@ -30,8 +33,20 @@ export class ZipVFS extends AbstractVFS {
return new Uint8Array(arrayBuffer); return new Uint8Array(arrayBuffer);
} }
paths() { async dir(path: string) {
return Array.from(this.entries.keys()); 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() {} dispose() {}