OfficeFighters’da bir karakterin sprite atlası, avatarı ve sesleri yalnızca dosya yükleme ekranının çıktısı değil; yayımlanmış karakter tanımının bağımlılıkları. Bir atlasın baytlarını aynı adreste değiştirmek, oyuncunun önbelleğindeki eski görselle sunucunun yeni animasyon tanımını bir araya getirebilir. Bu yüzden depolamayı içerik kimliği, sürüm, erişim kitlesi ve doğrulama akışıyla birlikte ele alıyorum. Bu yazı AssetService, S3ObjectStore, AssetContentReader ve taşıma komutunun kaynak incelemesine dayanıyor.
Metadata ve dosya baytları ayrı sorumluluklar
GameAsset kaydı dosyanın türünü, boyutlarını, uzunluğunu, SHA-256 özetini, grubunu ve içerik sürümünü tutuyor. Gerçek baytlar sağlayıcıya göre PostgreSQL’de game_asset_contents içinde ya da S3 uyumlu nesne deposunda bulunuyor. StorageOptions üzerinden Database ve S3 seçilebilmesi, okuma ve yazma servislerinin fiziksel konumu tek başına iş kuralına dönüştürmesini önlüyor. Bir varlığın kimliği ve hangi karakterde kullanıldığı ilişkisel modelde kalıyor.
S3ObjectStore, Supabase Storage’ın S3 uyumlu adresine sunucudan istek gönderiyor. ServiceUrl, region, bucket, key prefix ve erişim anahtarları bu adapterın ayarları. Supabase’in S3 belgesinde üretilen erişim anahtarlarının sunucu tarafında tutulması gerektiği belirtiliyor. Projede de tarayıcıya bu anahtarları vermek yerine içerik API üzerinden sunuluyor.
Yükleme doğrulaması dosya uzantısından fazlasını kontrol ediyor
UploadValidator önce uzunluğu sınırlıyor; dosyanın gerçek formatını başlık baytlarından belirliyor ve varlık türüyle eşleştiriyor. Sonraki aşamada kapsayıcı yapısı, animasyon, sona eklenmiş veri ve çözme hataları inceleniyor. Avatarlar yeniden kodlandığı için onlar için ayrı bir davranış var. Böylece örneğin dosya adının PNG olması, içeriğin doğrudan güvenilir sayılması anlamına gelmiyor. Sprite atlası için boyut ve hücre düzeni de kullanılabilirliğin parçası.
Önce nesne, sonra görünür kayıt
StoreCore yeni bir kimlik ürettikten sonra S3 yüklemesini metadata kaydından önce tamamlıyor. Aşağıdaki kod akışın kısaltılmış görünümü; gerçek metotta doğrulama, deduplication, denetim kaydı ve hata eşlemesi de var.
// AssetService.StoreCore: simplified storage/write order.
if (storage.Value.UsesS3)
await objects.PutAsync(id, content.Bytes, content.ContentType, ct);
else
db.GameAssetContents.Add(new GameAssetContent {
AssetId = id, Data = content.Bytes
});
// GameAsset metadata is tracked in the same DbContext.
var saved = await DbWrite.SaveAsync(db, ct);
Burada iki ayrı sistem arasında tek transaction bulunmuyor. Nesne yazıldıktan sonra PostgreSQL kaydı başarısız olursa depoda referanssız bir nesne kalabilir. Sıralamanın amacı dosyası hiç oluşmamış bir varlığı kullanıma açmamak. Referanssız nesneler ayrı temizlik konusu; kaynak kodda veritabanında hâlâ satırı bulunan varlıkların nesnesini silmeye karşı korumalı bir silme komutu var.
İçerik özeti, sürüm ve erişim kitlesi
Aynı türde aynı içerik özeti bulunduğunda mevcut varlık döndürülebiliyor. Yeni sürüm eklenirken arama aynı varlık grubuyla sınırlı; başka grubun kaydı yeni sürüm yerine geçmiyor. Avatarlarda yükleyen kullanıcı ve audience de eşleşiyor. Bu ayrıntı, bir hesabın avatar temizliğinin başka kullanıcının paylaştığı satırı etkilemesini önlemek için önemli.
Oyunculara açılan varlıklar GUID üzerinden public, bir yıllık immutable önbellek ve ETag ile sunuluyor. Editör kitlesine ait taslaklar yetkili XHR ile okunup blob URL’ye dönüştürülüyor. Buradaki public davranışı depolama bucket’ının herkese açılmasıyla eş anlamlı değil; API’nin belirli bir varlığı nasıl sunduğuyla ilgili.
Okuma sırasında eldeki değişmez kopyayı kullanmak
AssetContentReader S3 modunda önce nesne deposunu deniyor. Nesne yoksa veya depo hatası oluştuğunda saklanmış PostgreSQL kopyası varsa bu kopya kullanılabiliyor. Depo hatası sırasında kopya da bulunamazsa hata korunuyor; olay yanlışlıkla dosya bulunamadı sonucuna dönüşmüyor.
// AssetReadFallback: the retained copy must be immutable.
try {
if (await preferred() is { } stored) return stored;
}
catch (AssetStoreException) {
var copy = await retained();
if (copy is null) throw;
onOutageFallback();
return copy;
}
return await retained();
Bu geri dönüş, içeriğin aynı kimlik altında değişmemesine dayanıyor. Yeni S3 yüklemelerinin her biri için ayrıca veritabanı kopyası üretildiği varsayılmamalı; destek, elde hâlâ eski baytları bulunan varlıklar için anlamlı. Veritabanı akışı da okuyucu, komut ve bağlantıyı kapatan sahipli bir Stream ile dönüyor.
Taşıma kontrolünün neyi kanıtladığını bilmek
AssetStorageMigrator kayıtları tek tek ele alıyor. Depodaki nesnenin boyutu beklenen değerse zaten taşınmış sayılıyor. Yeniden yükleme gerektiğinde veritabanından okunan baytın SHA-256 özeti metadata ile karşılaştırılıyor; yüklemenin ardından nesne boyutu tekrar kontrol ediliyor. Boyut eşitliği tam bir uçtan uca içerik özeti kontrolü değildir; metrikleri yorumlarken bu sınır korunmalı.
Veritabanı baytlarının temizlenmesi açık purge seçeneğine bağlı. Aynı veritabanına bağlanan tüm uygulama örneklerinin S3 okuyabildiği doğrulanmadan bu adımı çalıştırmak eski örnekleri içeriksiz bırakabilir. Testte eksik nesne, Storage kesintisi, mevcut kopya, kopyasız hata, bozuk hash ve başarısız metadata kaydı ayrı senaryolar olmalı. Bunlar depolamanın yalnız başarılı yüklemeyle ölçülemeyeceğini gösteriyor.
Kaynaklar / References
OfficeFighters serisinin diğer yazıları
- OfficeFighters — SignalR ve WebSocket ile gerçek zamanlı oyun iletişimi
- OfficeFighters — Phaser, sabit adımlı simülasyon ve snapshot interpolasyonu
- OfficeFighters — MailKit ve outbox ile dayanıklı e-posta gönderimi
- OfficeFighters — JWT, refresh token rotasyonu ve çok sekmeli oturum yönetimi
- OfficeFighters — PostgreSQL, EF Core ve DB-first veri modeli