using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using RR3CommunityServer.Data; using RR3CommunityServer.Models; using System.Security.Cryptography; namespace RR3CommunityServer.Pages; [Authorize] public class AssetsModel : PageModel { private readonly RR3DbContext _context; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _assetsBasePath; public AssetsModel(RR3DbContext context, IConfiguration configuration, ILogger logger) { _context = context; _configuration = configuration; _logger = logger; _assetsBasePath = configuration.GetValue("AssetsBasePath") ?? Path.Combine(Directory.GetCurrentDirectory(), "Assets", "downloaded"); } public List Assets { get; set; } = new(); public AssetStats Stats { get; set; } = new(); public string? Message { get; set; } public bool IsError { get; set; } public async Task OnGetAsync() { Assets = await _context.GameAssets .OrderByDescending(a => a.UploadedAt) .ToListAsync(); await CalculateStatsAsync(); } public async Task OnPostUploadAsync( IFormFile assetFile, string eaCdnPath, string category, string assetType, bool isRequired, string? description) { try { if (assetFile == null || assetFile.Length == 0) { Message = "No file selected."; IsError = true; await OnGetAsync(); return Page(); } // Ensure assets directory exists if (!Directory.Exists(_assetsBasePath)) { Directory.CreateDirectory(_assetsBasePath); } // Create category subdirectory var categoryPath = Path.Combine(_assetsBasePath, category); if (!Directory.Exists(categoryPath)) { Directory.CreateDirectory(categoryPath); } // Save file to disk var fileName = Path.GetFileName(assetFile.FileName); var localPath = Path.Combine(categoryPath, fileName); using (var stream = new FileStream(localPath, FileMode.Create)) { await assetFile.CopyToAsync(stream); } // Calculate MD5 and SHA256 var md5Hash = await CalculateMd5Async(localPath); var sha256Hash = await CalculateSha256Async(localPath); var fileInfo = new FileInfo(localPath); // Normalize EA CDN path if (!eaCdnPath.StartsWith("/")) { eaCdnPath = "/" + eaCdnPath; } // Check if asset already exists var existingAsset = await _context.GameAssets .FirstOrDefaultAsync(a => a.EaCdnPath == eaCdnPath); if (existingAsset != null) { // Update existing asset existingAsset.FileName = fileName; existingAsset.LocalPath = localPath; existingAsset.FileSize = fileInfo.Length; existingAsset.Md5Hash = md5Hash; existingAsset.FileSha256 = sha256Hash; existingAsset.Category = category; existingAsset.AssetType = assetType; existingAsset.IsRequired = isRequired; existingAsset.Description = description; existingAsset.ContentType = GetContentType(fileName); existingAsset.UploadedAt = DateTime.UtcNow; Message = $"Asset '{fileName}' updated successfully!"; } else { // Create new asset var asset = new GameAsset { FileName = fileName, EaCdnPath = eaCdnPath, LocalPath = localPath, FileSize = fileInfo.Length, Md5Hash = md5Hash, FileSha256 = sha256Hash, Category = category, AssetType = assetType, IsRequired = isRequired, Description = description, ContentType = GetContentType(fileName), UploadedAt = DateTime.UtcNow, DownloadedAt = DateTime.UtcNow }; _context.GameAssets.Add(asset); Message = $"Asset '{fileName}' uploaded successfully!"; } await _context.SaveChangesAsync(); _logger.LogInformation("Asset uploaded: {FileName} -> {CdnPath}", fileName, eaCdnPath); } catch (Exception ex) { _logger.LogError(ex, "Error uploading asset"); Message = $"Error uploading asset: {ex.Message}"; IsError = true; } await OnGetAsync(); return Page(); } public async Task OnPostDeleteAsync(int id) { try { var asset = await _context.GameAssets.FindAsync(id); if (asset == null) { Message = "Asset not found."; IsError = true; await OnGetAsync(); return Page(); } // Delete file from disk if (!string.IsNullOrEmpty(asset.LocalPath) && System.IO.File.Exists(asset.LocalPath)) { System.IO.File.Delete(asset.LocalPath); } _context.GameAssets.Remove(asset); await _context.SaveChangesAsync(); Message = $"Asset '{asset.FileName}' deleted successfully!"; _logger.LogInformation("Asset deleted: {FileName}", asset.FileName); } catch (Exception ex) { _logger.LogError(ex, "Error deleting asset"); Message = $"Error deleting asset: {ex.Message}"; IsError = true; } await OnGetAsync(); return Page(); } public async Task OnPostGenerateManifestAsync() { try { var assets = await _context.GameAssets.ToListAsync(); // Generate manifest in RR3 format (tab-separated) var manifestContent = new System.Text.StringBuilder(); foreach (var asset in assets) { // Format: /path/to/file.ext md5hash compressedSize uncompressedSize manifestContent.AppendLine($"{asset.EaCdnPath}\t{asset.Md5Hash}\t{asset.CompressedSize ?? asset.FileSize}\t{asset.FileSize}"); } // Save to Assets directory var manifestPath = Path.Combine(_assetsBasePath, "asset_list_community.txt"); await System.IO.File.WriteAllTextAsync(manifestPath, manifestContent.ToString()); // Also generate JSON manifest for API var jsonManifest = assets.Select(a => new { path = a.EaCdnPath, md5 = a.Md5Hash, compressedSize = a.CompressedSize ?? a.FileSize, uncompressedSize = a.FileSize, category = a.Category, required = a.IsRequired }); var jsonPath = Path.Combine(_assetsBasePath, "asset_manifest_community.json"); await System.IO.File.WriteAllTextAsync(jsonPath, System.Text.Json.JsonSerializer.Serialize(jsonManifest, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); Message = $"Manifest generated successfully! ({assets.Count} assets)"; _logger.LogInformation("Asset manifest generated with {Count} assets", assets.Count); } catch (Exception ex) { _logger.LogError(ex, "Error generating manifest"); Message = $"Error generating manifest: {ex.Message}"; IsError = true; } await OnGetAsync(); return Page(); } private async Task CalculateStatsAsync() { Stats.TotalAssets = Assets.Count; Stats.AvailableAssets = Assets.Count(a => !string.IsNullOrEmpty(a.LocalPath) && System.IO.File.Exists(a.LocalPath)); Stats.TotalSizeMB = (long)(Assets.Sum(a => a.FileSize) / 1024.0 / 1024.0); Stats.TotalDownloads = Assets.Sum(a => a.AccessCount); } private async Task CalculateMd5Async(string filePath) { using var md5 = MD5.Create(); using var stream = System.IO.File.OpenRead(filePath); var hash = await md5.ComputeHashAsync(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } private async Task CalculateSha256Async(string filePath) { using var sha256 = SHA256.Create(); using var stream = System.IO.File.OpenRead(filePath); var hash = await sha256.ComputeHashAsync(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } private string GetContentType(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); return extension switch { ".pak" => "application/octet-stream", ".dat" => "application/octet-stream", ".nct" => "application/octet-stream", ".z" => "application/x-compress", ".json" => "application/json", ".xml" => "application/xml", ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", ".pvr" => "image/pvr", ".atlas" => "application/octet-stream", ".mp3" => "audio/mpeg", ".ogg" => "audio/ogg", ".wav" => "audio/wav", _ => "application/octet-stream" }; } } public class AssetStats { public int TotalAssets { get; set; } public int AvailableAssets { get; set; } public long TotalSizeMB { get; set; } public int TotalDownloads { get; set; } }