mirror of
https://github.com/misenhower/splatoon3.ink.git
synced 2026-10-03 12:26:58 -05:00
198 lines
6.5 KiB
JavaScript
198 lines
6.5 KiB
JavaScript
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import fs from 'node:fs/promises';
|
|
import { ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
import { S3SyncClient } from 's3-sync-client';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import R2Syncer from './R2Syncer.mjs';
|
|
|
|
class FakeSyncClient
|
|
{
|
|
calls = [];
|
|
|
|
async sync(source, target, options) {
|
|
this.calls.push({ options, source, target });
|
|
return { created: [], deleted: [], updated: [] };
|
|
}
|
|
}
|
|
|
|
class FakeS3Client
|
|
{
|
|
uploads = [];
|
|
|
|
async send(command) {
|
|
if (command instanceof ListObjectsV2Command) {
|
|
return { Contents: [], IsTruncated: false };
|
|
}
|
|
|
|
if (command instanceof PutObjectCommand) {
|
|
this.uploads.push({
|
|
...command.input,
|
|
Body: await streamText(command.input.Body),
|
|
});
|
|
return {};
|
|
}
|
|
|
|
throw new Error(`Unexpected command: ${command.constructor.name}`);
|
|
}
|
|
}
|
|
|
|
function isIncluded(filters, key) {
|
|
let excluded = false;
|
|
|
|
for (let filter of filters) {
|
|
if (!excluded && filter.exclude) {
|
|
excluded = filter.exclude(key);
|
|
}
|
|
if (excluded && filter.include) {
|
|
excluded = !filter.include(key);
|
|
}
|
|
}
|
|
|
|
return !excluded;
|
|
}
|
|
|
|
async function streamText(stream) {
|
|
if (Buffer.isBuffer(stream)) {
|
|
return stream.toString();
|
|
}
|
|
|
|
let chunks = [];
|
|
for await (let chunk of stream) {
|
|
chunks.push(Buffer.from(chunk));
|
|
}
|
|
return Buffer.concat(chunks).toString();
|
|
}
|
|
|
|
describe('R2Syncer', () => {
|
|
let temporaryDirectories = [];
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
temporaryDirectories = [];
|
|
config = {
|
|
bucket: 'splatoon3-ink-assets',
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
return Promise.all(temporaryDirectories.map(directory => fs.rm(directory, {
|
|
force: true,
|
|
recursive: true,
|
|
})));
|
|
});
|
|
|
|
it('uploads only public data and generated images with canonical R2 keys', async () => {
|
|
let syncClient = new FakeSyncClient;
|
|
let syncer = new R2Syncer({ config, syncClient });
|
|
|
|
await syncer.upload();
|
|
|
|
expect(syncClient.calls).toHaveLength(1);
|
|
let call = syncClient.calls[0];
|
|
expect(call.source).toBe(path.resolve('dist'));
|
|
expect(call.target).toBe('s3://splatoon3-ink-assets');
|
|
expect(call.options).not.toHaveProperty('partSize');
|
|
expect(isIncluded(call.options.filters, 'data/schedules.json')).toBe(true);
|
|
expect(isIncluded(call.options.filters, 'data/festivals.US.ics')).toBe(true);
|
|
expect(isIncluded(call.options.filters, 'data/archive/old.json')).toBe(false);
|
|
expect(isIncluded(call.options.filters, 'data/index.html')).toBe(false);
|
|
expect(isIncluded(call.options.filters, 'assets/splatnet/v3/stage.png')).toBe(true);
|
|
expect(isIncluded(call.options.filters, 'assets/splatnet/.DS_Store')).toBe(false);
|
|
expect(isIncluded(call.options.filters, 'assets/main.js')).toBe(false);
|
|
expect(isIncluded(call.options.filters, 'status-screenshots/schedules.png')).toBe(true);
|
|
expect(isIncluded(call.options.filters, 'status-screenshots/index.html')).toBe(false);
|
|
expect(call.options.relocations[0]('assets/splatnet/v3/stage.png'))
|
|
.toBe('splatnet/v3/stage.png');
|
|
expect(call.options.relocations[0]('data/schedules.json'))
|
|
.toBe('data/schedules.json');
|
|
});
|
|
|
|
it('does not transform public data upload bodies', async () => {
|
|
let localPath = await fs.mkdtemp(path.join(os.tmpdir(), 'r2-syncer-'));
|
|
temporaryDirectories.push(localPath);
|
|
let jsonPath = path.join(localPath, 'schedules.json');
|
|
await fs.writeFile(
|
|
jsonPath,
|
|
'{"image":"https://assets.splatoon3.ink/splatnet/one.png"}',
|
|
);
|
|
let syncClient = new FakeSyncClient;
|
|
let syncer = new R2Syncer({ config, syncClient });
|
|
await syncer.upload();
|
|
let commandInput = syncClient.calls[0].options.commandInput;
|
|
let input = {
|
|
Body: { path: jsonPath },
|
|
ContentLength: 61,
|
|
Key: 'data/schedules.json',
|
|
};
|
|
|
|
let result = commandInput(input);
|
|
|
|
expect(result.Body).toBeUndefined();
|
|
expect(result.ContentLength).toBeUndefined();
|
|
expect(result).toMatchObject({
|
|
CacheControl: 'no-cache, stale-while-revalidate=5, stale-if-error=86400',
|
|
ContentType: 'application/json',
|
|
});
|
|
expect(result).not.toHaveProperty('ACL');
|
|
});
|
|
|
|
it('does not rewrite image upload bodies', async () => {
|
|
let syncClient = new FakeSyncClient;
|
|
let syncer = new R2Syncer({ config, syncClient });
|
|
await syncer.upload();
|
|
let commandInput = syncClient.calls[0].options.commandInput;
|
|
let body = { path: '/unused/image.png' };
|
|
|
|
let result = commandInput({
|
|
Body: body,
|
|
ContentLength: 5,
|
|
Key: 'splatnet/stage.png',
|
|
});
|
|
|
|
expect(result.Body).toBeUndefined();
|
|
expect(result.ContentType).toBe('image/png');
|
|
});
|
|
|
|
it('uploads canonical data and relocated images through the real sync client', async () => {
|
|
let localPath = await fs.mkdtemp(path.join(os.tmpdir(), 'r2-syncer-'));
|
|
temporaryDirectories.push(localPath);
|
|
await fs.mkdir(path.join(localPath, 'data'), { recursive: true });
|
|
await fs.mkdir(path.join(localPath, 'assets/splatnet'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(localPath, 'data/schedules.json'),
|
|
'{"image":"https://assets.splatoon3.ink/splatnet/stage.png"}',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(localPath, 'data/festivals.US.ics'),
|
|
'URL:https://splatoon3.ink\r\n'
|
|
+ 'ATTACH:https://assets.splatoon3.ink/splatnet/fest.png\r\n',
|
|
);
|
|
await fs.writeFile(path.join(localPath, 'assets/splatnet/stage.png'), 'image');
|
|
await fs.writeFile(path.join(localPath, 'assets/main.js'), 'static site');
|
|
let s3Client = new FakeS3Client;
|
|
let syncClient = new S3SyncClient({ client: s3Client });
|
|
let syncer = new R2Syncer({ config, localPath, s3Client, syncClient });
|
|
|
|
await syncer.upload();
|
|
|
|
expect(s3Client.uploads.map(upload => upload.Key).sort()).toEqual([
|
|
'data/festivals.US.ics',
|
|
'data/schedules.json',
|
|
'splatnet/stage.png',
|
|
]);
|
|
let json = s3Client.uploads.find(upload => upload.Key === 'data/schedules.json');
|
|
expect(json.Body).toBe(
|
|
'{"image":"https://assets.splatoon3.ink/splatnet/stage.png"}',
|
|
);
|
|
expect(json.ContentLength).toBe(Buffer.byteLength(json.Body));
|
|
let calendar = s3Client.uploads.find(upload => upload.Key === 'data/festivals.US.ics');
|
|
expect(calendar.Body).toBe(
|
|
'URL:https://splatoon3.ink\r\n'
|
|
+ 'ATTACH:https://assets.splatoon3.ink/splatnet/fest.png\r\n',
|
|
);
|
|
expect(calendar.ContentLength).toBe(Buffer.byteLength(calendar.Body));
|
|
expect(calendar.ContentType).toBe('text/calendar');
|
|
});
|
|
});
|