Add Game Asset Preservation System Foundation

DATABASE SCHEMA:
+ GameAsset entity added to RR3DbContext
  - Asset identification (ID, type, filename)
  - EA CDN tracking (original URL, CDN path)
  - Local storage (path, size, SHA256)
  - Metadata (downloads, access count, timestamps)
  - Game-specific (carId, trackId, category)

FEATURES:
- Track all cached assets in database
- Store EA CDN URLs while available
- SHA256 integrity checking
- Access statistics
- Category organization
- Version tracking

PURPOSE:
Foundation for full asset caching system.
Next step: AssetsController with EA CDN proxying.

STORAGE STRUCTURE:
wwwroot/assets/
├── cars/
├── tracks/
├── textures/
├── audio/
├── models/
└── misc/

PRESERVATION STRATEGY:
Phase 1 (EA online): Proxy + cache assets
Phase 2 (EA offline): Serve from cache
Result: Complete game preservation! 🎮💾

Package added: Microsoft.Extensions.Http (for CDN proxying)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-17 22:50:04 -08:00
parent 3970ecd9a3
commit bfd37dc7c2
39 changed files with 741 additions and 392 deletions

View File

@@ -1,237 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("synergy/content")]
public class ContentController : ControllerBase
{
private readonly IWebHostEnvironment _env;
private readonly string _assetsPath;
public ContentController(IWebHostEnvironment env)
{
_env = env;
_assetsPath = Path.Combine(_env.ContentRootPath, "Assets");
// Create assets directory structure if not exists
Directory.CreateDirectory(Path.Combine(_assetsPath, "cars"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "tracks"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "textures"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "audio"));
}
/// <summary>
/// Get asset manifest - list of all available content
/// </summary>
[HttpGet("manifest")]
public IActionResult GetManifest()
{
var assets = GetAssetList();
return Ok(new
{
version = "1.0.0",
assetCount = assets.Count,
totalSize = assets.Sum(a => ((dynamic)a).size),
assets = assets
});
}
/// <summary>
/// Download specific asset file
/// </summary>
[HttpGet("download/{assetType}/{assetId}")]
public IActionResult DownloadAsset(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (!System.IO.File.Exists(assetPath))
{
return NotFound(new { error = $"Asset not found: {assetType}/{assetId}" });
}
var fileBytes = System.IO.File.ReadAllBytes(assetPath);
var fileName = $"{assetId}.pak";
return File(fileBytes, "application/octet-stream", fileName);
}
/// <summary>
/// Get asset metadata and info
/// </summary>
[HttpGet("info/{assetType}/{assetId}")]
public IActionResult GetAssetInfo(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (!System.IO.File.Exists(assetPath))
{
return NotFound(new { error = $"Asset not found: {assetType}/{assetId}" });
}
var fileInfo = new FileInfo(assetPath);
return Ok(new
{
assetId = assetId,
assetType = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
checksum = CalculateMD5(assetPath),
version = "1.0.0",
downloadUrl = $"/synergy/content/download/{assetType}/{assetId}",
lastModified = fileInfo.LastWriteTimeUtc
});
}
/// <summary>
/// Check if specific asset exists
/// </summary>
[HttpHead("download/{assetType}/{assetId}")]
public IActionResult CheckAssetExists(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (System.IO.File.Exists(assetPath))
{
var fileInfo = new FileInfo(assetPath);
Response.Headers["Content-Length"] = fileInfo.Length.ToString();
Response.Headers["X-Asset-Checksum"] = CalculateMD5(assetPath);
return Ok();
}
return NotFound();
}
/// <summary>
/// Get assets by type
/// </summary>
[HttpGet("list/{assetType}")]
public IActionResult GetAssetsByType(string assetType)
{
var typePath = Path.Combine(_assetsPath, assetType);
if (!Directory.Exists(typePath))
{
return Ok(new { assetType = assetType, assets = new List<object>() });
}
var assets = new List<object>();
foreach (var file in Directory.GetFiles(typePath, "*.pak"))
{
var assetId = Path.GetFileNameWithoutExtension(file);
var fileInfo = new FileInfo(file);
assets.Add(new
{
id = assetId,
type = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
url = $"/synergy/content/download/{assetType}/{assetId}",
checksum = CalculateMD5(file)
});
}
return Ok(new
{
assetType = assetType,
count = assets.Count,
totalSize = assets.Sum(a => ((dynamic)a).size),
assets = assets
});
}
/// <summary>
/// Health check for content service
/// </summary>
[HttpGet("health")]
public IActionResult HealthCheck()
{
var assetTypes = new[] { "cars", "tracks", "textures", "audio" };
var stats = new Dictionary<string, object>();
foreach (var type in assetTypes)
{
var typePath = Path.Combine(_assetsPath, type);
if (Directory.Exists(typePath))
{
var files = Directory.GetFiles(typePath, "*.pak");
var totalSize = files.Sum(f => new FileInfo(f).Length);
stats[type] = new
{
count = files.Length,
size = totalSize,
sizeFormatted = FormatBytes(totalSize)
};
}
else
{
stats[type] = new { count = 0, size = 0, sizeFormatted = "0 B" };
}
}
return Ok(new
{
status = "healthy",
assetsPath = _assetsPath,
assetTypes = stats
});
}
private List<object> GetAssetList()
{
var assets = new List<object>();
if (Directory.Exists(_assetsPath))
{
foreach (var typeDir in Directory.GetDirectories(_assetsPath))
{
var assetType = Path.GetFileName(typeDir);
foreach (var file in Directory.GetFiles(typeDir, "*.pak"))
{
var assetId = Path.GetFileNameWithoutExtension(file);
var fileInfo = new FileInfo(file);
assets.Add(new
{
id = assetId,
type = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
url = $"/synergy/content/download/{assetType}/{assetId}",
checksum = CalculateMD5(file)
});
}
}
}
return assets;
}
private string CalculateMD5(string filePath)
{
using var md5 = MD5.Create();
using var stream = System.IO.File.OpenRead(filePath);
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}