mirror of
https://github.com/misenhower/splatoon3.ink.git
synced 2026-04-25 07:18:35 -05:00
Lazily initialized to avoid console-stamp issues in test environment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
3.4 KiB
JavaScript
132 lines
3.4 KiB
JavaScript
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
import prefixedConsole from './prefixedConsole.mjs';
|
|
|
|
const prefixes = ['assets/splatnet/', 'data/'];
|
|
|
|
class VirtualFileSystem {
|
|
// Map of S3 key to { lastModified: Date, size: number }
|
|
_listing = new Map();
|
|
_loadPromise = null;
|
|
_localPrefix = 'dist';
|
|
|
|
get _console() {
|
|
return this.__console ??= prefixedConsole('VFS');
|
|
}
|
|
|
|
get _canUseS3() {
|
|
return !!(
|
|
process.env.AWS_ACCESS_KEY_ID &&
|
|
process.env.AWS_SECRET_ACCESS_KEY &&
|
|
process.env.AWS_S3_BUCKET
|
|
);
|
|
}
|
|
|
|
get _s3Client() {
|
|
return this.__s3Client ??= new S3Client({
|
|
endpoint: process.env.AWS_S3_ENDPOINT,
|
|
region: process.env.AWS_REGION,
|
|
credentials: {
|
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ensure the S3 listing is loaded. Loads once on first call,
|
|
* subsequent calls return the same promise.
|
|
*/
|
|
async _ensureLoaded() {
|
|
if (!this._canUseS3) return false;
|
|
|
|
this._loadPromise ??= this._loadFromS3();
|
|
await this._loadPromise;
|
|
return true;
|
|
}
|
|
|
|
async _loadFromS3() {
|
|
const bucket = process.env.AWS_S3_BUCKET;
|
|
|
|
this._console.info('Loading S3 listing...');
|
|
|
|
for (const prefix of prefixes) {
|
|
let continuationToken;
|
|
let count = 0;
|
|
|
|
do {
|
|
const response = await this._s3Client.send(new ListObjectsV2Command({
|
|
Bucket: bucket,
|
|
Prefix: prefix,
|
|
ContinuationToken: continuationToken,
|
|
}));
|
|
|
|
for (const obj of response.Contents ?? []) {
|
|
this._listing.set(obj.Key, {
|
|
lastModified: obj.LastModified,
|
|
size: obj.Size,
|
|
});
|
|
count++;
|
|
}
|
|
|
|
continuationToken = response.IsTruncated
|
|
? response.NextContinuationToken
|
|
: undefined;
|
|
} while (continuationToken);
|
|
|
|
this._console.info(`Loaded ${count} entries for prefix "${prefix}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a local file path is known to exist in the VFS listing.
|
|
* Returns true/false if the path is within a tracked prefix,
|
|
* or null if VFS is not available.
|
|
* @param {string} localPath
|
|
*/
|
|
async has(localPath) {
|
|
if (!(await this._ensureLoaded())) return null;
|
|
|
|
const key = this._localPathToKey(localPath);
|
|
if (key === null) return null;
|
|
if (!this._isTrackedKey(key)) return null;
|
|
|
|
return this._listing.has(key);
|
|
}
|
|
|
|
/**
|
|
* Get the last modified time for a file from the S3 listing.
|
|
* Returns Date if found, null if not tracked or VFS not available.
|
|
* @param {string} localPath
|
|
*/
|
|
async getMtime(localPath) {
|
|
if (!(await this._ensureLoaded())) return null;
|
|
|
|
const key = this._localPathToKey(localPath);
|
|
if (key === null) return null;
|
|
if (!this._isTrackedKey(key)) return null;
|
|
|
|
const entry = this._listing.get(key);
|
|
return entry ? entry.lastModified : null;
|
|
}
|
|
|
|
/**
|
|
* Convert a local path (e.g. 'dist/assets/splatnet/foo.png')
|
|
* to an S3 key (e.g. 'assets/splatnet/foo.png').
|
|
* @param {string} localPath
|
|
*/
|
|
_localPathToKey(localPath) {
|
|
const prefix = this._localPrefix + '/';
|
|
if (localPath.startsWith(prefix)) {
|
|
return localPath.slice(prefix.length);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_isTrackedKey(key) {
|
|
return prefixes.some(prefix => key.startsWith(prefix));
|
|
}
|
|
}
|
|
|
|
const vfs = new VirtualFileSystem();
|
|
export default vfs;
|