Перейти к содержанию

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

HunterGraphics.Shell/Services/R2Uploader.cs (упрощено)
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):

{
  "r2": {
    "accountId": "abc123def456",
    "accessKey": "...",
    "secretKey": "..."
  }
}

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:

scripts/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:

[r2]
type = s3
provider = Cloudflare
access_key_id = ...
secret_access_key = ...
endpoint = https://<accountId>.r2.cloudflarestorage.com

Дальше: VPS и домены →