Add ZIP bulk upload to asset manager
Features: - Upload ZIP files with folder structure - Automatic extraction and MD5/SHA256 calculation - Preserve folder paths as EA CDN paths - Auto-categorize based on file extensions - Update existing assets automatically - Bootstrap tabs for single/bulk upload UI - Progress feedback (X new, Y updated) Example: cars/porsche.dat → /cars/porsche.dat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
@@ -150,6 +151,172 @@ public class AssetsModel : PageModel
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUploadZipAsync(
|
||||
IFormFile zipFile,
|
||||
string baseCategory,
|
||||
bool isRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (zipFile == null || zipFile.Length == 0)
|
||||
{
|
||||
Message = "No ZIP file selected.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!zipFile.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Message = "Please upload a ZIP file.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Ensure assets directory exists
|
||||
if (!Directory.Exists(_assetsBasePath))
|
||||
{
|
||||
Directory.CreateDirectory(_assetsBasePath);
|
||||
}
|
||||
|
||||
// Save ZIP to temp location
|
||||
var tempZipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
|
||||
using (var stream = new FileStream(tempZipPath, FileMode.Create))
|
||||
{
|
||||
await zipFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
int extractedCount = 0;
|
||||
int skippedCount = 0;
|
||||
var errors = new List<string>();
|
||||
|
||||
// Extract ZIP and process each file
|
||||
using (var archive = ZipFile.OpenRead(tempZipPath))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip directories
|
||||
if (string.IsNullOrEmpty(entry.Name) || entry.FullName.EndsWith("/"))
|
||||
continue;
|
||||
|
||||
// Determine category from path in ZIP
|
||||
var pathParts = entry.FullName.Split('/', '\\');
|
||||
var category = baseCategory;
|
||||
|
||||
// If ZIP has folders, use first folder as subcategory
|
||||
if (pathParts.Length > 1)
|
||||
{
|
||||
category = Path.Combine(baseCategory, pathParts[0]);
|
||||
}
|
||||
|
||||
// Create category subdirectory
|
||||
var categoryPath = Path.Combine(_assetsBasePath, category);
|
||||
if (!Directory.Exists(categoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(categoryPath);
|
||||
}
|
||||
|
||||
// Extract file
|
||||
var fileName = entry.Name;
|
||||
var localPath = Path.Combine(categoryPath, fileName);
|
||||
|
||||
// Extract to disk
|
||||
entry.ExtractToFile(localPath, overwrite: true);
|
||||
|
||||
// Calculate hashes
|
||||
var md5Hash = await CalculateMd5Async(localPath);
|
||||
var sha256Hash = await CalculateSha256Async(localPath);
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
|
||||
// Build EA CDN path from ZIP structure
|
||||
var eaCdnPath = "/" + entry.FullName.Replace("\\", "/");
|
||||
|
||||
// Check if asset already exists
|
||||
var existingAsset = await _context.GameAssets
|
||||
.FirstOrDefaultAsync(a => a.EaCdnPath == eaCdnPath);
|
||||
|
||||
if (existingAsset != null)
|
||||
{
|
||||
// Update existing
|
||||
existingAsset.FileName = fileName;
|
||||
existingAsset.LocalPath = localPath;
|
||||
existingAsset.FileSize = fileInfo.Length;
|
||||
existingAsset.Md5Hash = md5Hash;
|
||||
existingAsset.FileSha256 = sha256Hash;
|
||||
existingAsset.Category = category;
|
||||
existingAsset.IsRequired = isRequired;
|
||||
existingAsset.ContentType = GetContentType(fileName);
|
||||
existingAsset.UploadedAt = DateTime.UtcNow;
|
||||
skippedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new asset
|
||||
var asset = new GameAsset
|
||||
{
|
||||
FileName = fileName,
|
||||
EaCdnPath = eaCdnPath,
|
||||
LocalPath = localPath,
|
||||
FileSize = fileInfo.Length,
|
||||
Md5Hash = md5Hash,
|
||||
FileSha256 = sha256Hash,
|
||||
Category = category,
|
||||
AssetType = DetermineAssetType(fileName),
|
||||
IsRequired = isRequired,
|
||||
Description = $"Extracted from {zipFile.FileName}",
|
||||
ContentType = GetContentType(fileName),
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.GameAssets.Add(asset);
|
||||
extractedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"{entry.FullName}: {ex.Message}");
|
||||
_logger.LogError(ex, "Error extracting file from ZIP: {FileName}", entry.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Clean up temp ZIP
|
||||
if (System.IO.File.Exists(tempZipPath))
|
||||
{
|
||||
System.IO.File.Delete(tempZipPath);
|
||||
}
|
||||
|
||||
// Build message
|
||||
if (errors.Any())
|
||||
{
|
||||
Message = $"ZIP processed: {extractedCount} new, {skippedCount} updated. Errors: {errors.Count}";
|
||||
IsError = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Message = $"ZIP extracted successfully! {extractedCount} new files, {skippedCount} updated.";
|
||||
}
|
||||
|
||||
_logger.LogInformation("ZIP uploaded: {FileName} -> {ExtractedCount} files", zipFile.FileName, extractedCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing ZIP file");
|
||||
Message = $"Error processing ZIP: {ex.Message}";
|
||||
IsError = true;
|
||||
}
|
||||
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
@@ -278,6 +445,19 @@ public class AssetsModel : PageModel
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
private string DetermineAssetType(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".png" or ".jpg" or ".jpeg" or ".pvr" or ".atlas" => "Texture",
|
||||
".mp3" or ".ogg" or ".wav" => "Audio",
|
||||
".json" or ".xml" => "Config",
|
||||
".pak" or ".dat" or ".nct" => "Data",
|
||||
_ => "Data"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AssetStats
|
||||
|
||||
Reference in New Issue
Block a user