import path from 'path'; import { S3Client } from '@aws-sdk/client-s3'; import { S3SyncClient } from 's3-sync-client'; import mime from 'mime-types'; export default class S3Syncer { download(dist = true, storage = true) { this.log('Downloading files...'); return Promise.all([ dist && this.syncClient.sync(this.publicBucket, `${this.localPath}/dist`, { filters: this.downloadFilters, }), storage && this.syncClient.sync(this.privateBucket, `${this.localPath}/storage`, { filters: this.privateFilters, }), ]); } upload() { this.log('Uploading files...'); return Promise.all([ this.syncClient.sync(`${this.localPath}/dist`, this.publicBucket, { filters: this.filters, commandInput: input => ({ ACL: 'public-read', ContentType: mime.lookup(input.Key), CacheControl: input.Key.startsWith('data/') ? 'no-cache, stale-while-revalidate=5, stale-if-error=86400' : undefined, }), }), this.syncClient.sync(`${this.localPath}/storage`, this.privateBucket, { filters: this.privateFilters, }), ]); } 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, }, }); } /** @member {S3SyncClient} */ get syncClient() { return this._syncClient ??= new S3SyncClient({ client: this.s3Client }); } get publicBucket() { return `s3://${process.env.AWS_S3_BUCKET}`; } get privateBucket() { return `s3://${process.env.AWS_S3_PRIVATE_BUCKET}`; } get localPath() { return path.resolve('.'); } get filters() { return [ { exclude: () => true }, // Exclude everything by default { include: (key) => key.startsWith('assets/splatnet/') }, { include: (key) => key.startsWith('data/') }, { exclude: (key) => key.startsWith('data/archive/') }, { include: (key) => key.startsWith('status-screenshots/') }, ]; } // Download filters skip files that are handled by the VFS layer // (existence/mtime checks only, no content reads needed) get downloadFilters() { return [ { exclude: () => true }, // Exclude everything by default // assets/splatnet/ - handled by VFS (existence checks only) // status-screenshots/ - regenerated by social posting { include: (key) => key.startsWith('data/') }, { exclude: (key) => key.startsWith('data/archive/') }, { exclude: (key) => key.startsWith('data/xrank/') }, { exclude: (key) => key.startsWith('data/festivals.ranking.') }, ]; } get privateFilters() { return [ { exclude: (key) => key.startsWith('archive/') }, ]; } log(message) { console.log(`[S3] ${message}`); } }