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 { AbstractVFS } from "../core/vfs/abstract";
import { LocalVFS } from "../core/vfs/local";
import { SteamVFS } from "../core/vfs/steam";
import { ZipVFS } from "../core/vfs/zip";
const steamPattern = /steam:(\d+)\/(\d+)/;
const zipPattern = /zip:(.+)/;
const localePattern = /local:"?(.+)"?/;
const source = Bun.argv
.find((arg) => arg.startsWith("--source="))
@@ -21,11 +23,14 @@ if (!source) {
assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD),
app,
depot
depot,
).init();
} else if (zipPattern.test(source)) {
const [, url] = source.match(zipPattern)!;
resolved = await new ZipVFS(url).init();
} else if (localePattern.test(source)) {
const [, path] = source.match(localePattern)!;
resolved = await new LocalVFS(path).init();
} else {
throw new Error("No valid source specified");
}
@@ -43,7 +43,7 @@ export class AssetUploader {
constructor(public message: string) {
[this.owner, this.repo] = assertSecret(
import.meta.env.PUBLIC_ASSET_REPO
import.meta.env.PUBLIC_ASSET_REPO,
).split("/");
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 response = await fetch(
`https://raw.githubusercontent.com/${blobPath}`,
{ headers: { "Accept-Encoding": "identity" } }
{ headers: { "Accept-Encoding": "identity" } },
);
let diff: { change: number; status: DiffStatus };
@@ -71,7 +71,7 @@ export class AssetUploader {
} else if (response.status === 200) {
if (
this.heuristicFormats.some((format) =>
change.path.endsWith(`.${format}`)
change.path.endsWith(`.${format}`),
)
) {
/**
@@ -94,7 +94,7 @@ export class AssetUploader {
}
} else {
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(
`${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${
diff.change > 0 ? "+" : ""
}${diff.change.toLocaleString()}B) ${change.path}`
}${diff.change.toLocaleString()}B) ${change.path}`,
);
// flush BEFORE going over limit
@@ -160,8 +160,8 @@ export class AssetUploader {
await new Promise((resolve) =>
setTimeout(
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" });
@@ -170,11 +170,11 @@ export class AssetUploader {
break;
} catch {
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) =>
setTimeout(resolve, this.time_per_blob)
setTimeout(resolve, this.time_per_blob),
);
}
}
@@ -209,7 +209,7 @@ export class AssetUploader {
[Symbol.dispose]() {
if (this.changes.length > 0) {
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 has(path: string): boolean;
abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>;
abstract paths(): string[];
resolve(path: string) {
async resolve(path: string) {
const normalized = normalize(path);
if (this.has(normalized)) return normalized;
if (await this.has(normalized)) return normalized;
const dvplPath = `${normalized}.dvpl`;
if (this.has(dvplPath)) return dvplPath;
if (await this.has(dvplPath)) return dvplPath;
return null;
}
assert(path: string) {
const resolved = this.resolve(path);
async assert(path: string) {
const resolved = await this.resolve(path);
if (resolved === null) throw new Error(`File not found: ${path}`);
@@ -36,7 +34,7 @@ export abstract class AbstractVFS {
}
async file(file: string) {
const resolved = this.assert(file);
const resolved = await this.assert(file);
const raw = await this.raw(resolved);
let buffer = raw;
@@ -47,21 +45,7 @@ export abstract class AbstractVFS {
return buffer;
}
dir(path: 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);
}
abstract dir(path: string): Promise<string[]>;
async text(path: string) {
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;
}
has(path: string) {
async has(path: string) {
return this.manifest.has(path);
}
@@ -89,8 +89,20 @@ export class SteamVFS extends AbstractVFS {
return new Uint8Array(downloaded.file);
}
paths() {
return Array.from(this.manifest.keys());
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() {
+19 -4
View File
@@ -5,7 +5,10 @@ import { AbstractVFS } from "./abstract";
export class ZipVFS extends AbstractVFS {
entries = new Map<string, ZipEntry>();
constructor(private url: string, private root = "app/") {
constructor(
private url: string,
private root = "app/",
) {
super();
}
@@ -21,7 +24,7 @@ export class ZipVFS extends AbstractVFS {
return this;
}
has(path: string) {
async has(path: string) {
return this.entries.has(path);
}
@@ -30,8 +33,20 @@ export class ZipVFS extends AbstractVFS {
return new Uint8Array(arrayBuffer);
}
paths() {
return Array.from(this.entries.keys());
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() {}