R2 layout¶
Cloudflare R2 — наше единственное S3-compatible хранилище для всех тяжёлых файлов: patch.zip'ов redux'ов, gunpack .rpf'ов, превью PNG, GLB-моделей, .exe инсталлеров.
Bucket: huntergraphics. Public access enabled на чтение (любой может GET по URL), write — только через S3-credentials.
Иерархия путей¶
huntergraphics/
├── redux/
│ └── <reduxId>/
│ ├── preview.png
│ ├── preview.glb
│ ├── v1/
│ │ ├── patch.zip
│ │ └── components/
│ │ ├── minimap.bin
│ │ ├── hud_reticle.bin
│ │ ├── tracers.bin
│ │ └── armor.zip
│ ├── v2/
│ │ ├── patch.zip
│ │ └── components/...
│ └── ...
├── gunpacks/
│ └── <gunpackId>/
│ ├── cover.png
│ ├── pack.rpf
│ ├── <internalName>.glb -- per-gun 3D
│ ├── <internalName>.png -- per-gun preview
│ └── ...
├── armor/
│ └── <armorPackId>/
│ ├── cover.png
│ ├── pack.rpf
│ ├── <pieceName>.glb
│ └── <pieceName>.png
├── dlc-imports/
│ └── <deviceName>/
│ └── dlc.rpf
├── library/
│ └── <componentId>/
│ ├── payload.bin
│ ├── preview.png
│ └── gallery/<n>.png
├── gta-presets/
│ └── <presetId>/
│ ├── settings.xml
│ └── preview.png
├── user-builds/
│ └── <buildId>/
│ └── cover.png
├── pro-players/
│ └── <playerId>/
│ ├── avatar.png
│ └── cover.png
└── releases/
├── MiamiGraphics_1.0.0.exe
├── MiamiGraphics_1.0.1.exe
├── MiamiGraphics_1.0.2.exe
├── MiamiGraphicsRenderer_1.0.0.zip -- portable Node+Chrome
└── MiamiGraphicsJre_1.0.0.zip -- on-demand JRE для minimap
Пути жёстко детерминированы по UUID + версии — это даёт:
- donor cache знает URL без DB-запроса (key =
<reduxId>_<versionId>_<component>). - Сборка
r2_gc.ps1может пройти всё и собрать orphan files (R2-пути не в БД).
Public URLs¶
Два домена:
https://cdn.miamigraphicsstorage.uk/<path>— основной CDN (через CloudFront Worker для cache).https://miamigraphicsstorage.uk/<path>— direct, без cache.
Лаунчер использует первый (с CDN) для всего. Direct domain — backup, если CDN упадёт.
Оба обслуживаются через FragmentingHttpHandler для bypass DPI в РФ.
Upload from launcher¶
public sealed class R2Uploader
{
private readonly IAmazonS3 _s3;
public R2Uploader(R2Config config)
{
_s3 = new AmazonS3Client(
new BasicAWSCredentials(config.AccessKey, config.SecretKey),
new AmazonS3Config
{
ServiceURL = $"https://{config.AccountId}.r2.cloudflarestorage.com",
ForcePathStyle = true,
});
}
public async Task<string> UploadAsync(string key, byte[] data, string contentType = "application/octet-stream")
{
await _s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "huntergraphics",
Key = key,
InputStream = new MemoryStream(data),
ContentType = contentType,
});
return $"https://cdn.miamigraphicsstorage.uk/{key}";
}
public async Task<string> UploadAsync(string key, string filePath, string contentType = null)
{
using var fs = File.OpenRead(filePath);
await _s3.PutObjectAsync(new PutObjectRequest
{
BucketName = "huntergraphics",
Key = key,
InputStream = fs,
ContentType = contentType ?? GuessContentType(filePath),
});
return $"https://cdn.miamigraphicsstorage.uk/{key}";
}
}
S3 credentials хранятся в admin-config.json (encrypted DPAPI на машине админа, не commit'ятся в git):
adminConfigSave / adminConfigTestR2 — handlers в Bridge для редактирования.
CORS¶
R2 bucket настроен на CORS «*» для всех origin'ов:
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
Это нужно потому что WebView2 React UI делает fetch() на CDN-домен. Без CORS — отказ. (Дополнительно мы шлём Access-Control-Allow-Origin: * через WebView2 bypass interceptor — двойная защита.)
Lifecycle / GC¶
R2 платно по объёму. Сейчас у нас ~30 GB (большая часть — patch.zip'ы redux'ов). Растёт ~5 GB/месяц.
При soft-delete redux row в Supabase R2 файлы остаются. Это намеренно (на случай юзеров с install-state ссылающихся на удалённый мод).
Раз в месяц вручную запускаем r2_gc.ps1:
# Читаем все живые ссылки из Supabase
$liveUrls = (Invoke-RestMethod "$supabaseUrl/rest/v1/redux_versions?select=patch_url").patch_url
# Список всех файлов в R2
$r2Files = rclone lsf --recursive r2:huntergraphics
# Diff: что в R2 нет в БД (> N дней по дате создания)
$orphans = $r2Files | Where-Object {
$url = "https://cdn.miamigraphicsstorage.uk/$_"
-not ($liveUrls -contains $url) -and (Get-Item ...).CreationTime -lt (Get-Date).AddDays(-30)
}
# Удаляем
$orphans | ForEach-Object { rclone delete "r2:huntergraphics/$_" }
Это полу-автомат — админ запускает, скрипт показывает dry-run, после approve удаляет. Ручной overhead, но раз в месяц приемлемо.
rclone access¶
С VPS / админ-машины используется rclone с remote r2: для batch-job'ов и backup'ов:
# Скачать всё (для backup'а)
rclone copy r2:huntergraphics /backup/r2/
# Глянуть размер
rclone size r2:huntergraphics
Remote config — в ~/.config/rclone/rclone.conf: