diff --git a/LANCommander/Jobs/Background/PatchArchiveBackgroundJob.cs b/LANCommander/Jobs/Background/PatchArchiveBackgroundJob.cs new file mode 100644 index 0000000..46f8d79 --- /dev/null +++ b/LANCommander/Jobs/Background/PatchArchiveBackgroundJob.cs @@ -0,0 +1,22 @@ +using LANCommander.Services; + +namespace LANCommander.Jobs.Background +{ + public class PatchArchiveBackgroundJob + { + private readonly ArchiveService ArchiveService; + + public PatchArchiveBackgroundJob(ArchiveService archiveService) + { + ArchiveService = archiveService; + } + + public async Task Execute(Guid originalArchiveId, Guid alteredArchiveId) + { + var originalArchive = await ArchiveService.Get(originalArchiveId); + var alteredArchive = await ArchiveService.Get(alteredArchiveId); + + await ArchiveService.PatchArchive(originalArchive, alteredArchive); + } + } +} diff --git a/LANCommander/Pages/Games/Components/ArchiveUploader.razor b/LANCommander/Pages/Games/Components/ArchiveUploader.razor index e8c10a0..e9def81 100644 --- a/LANCommander/Pages/Games/Components/ArchiveUploader.razor +++ b/LANCommander/Pages/Games/Components/ArchiveUploader.razor @@ -1,5 +1,7 @@ @using System.Net; @using System.Diagnostics; +@using Hangfire; +@using LANCommander.Jobs.Background; @inject HttpClient HttpClient @inject NavigationManager Navigator @inject ArchiveService ArchiveService @@ -177,10 +179,17 @@ Archive.ObjectKey = objectKey.ToString(); Archive.CompressedSize = File.Size; - await ArchiveService.Add(Archive); + var originalArchive = Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault(); + + Archive = await ArchiveService.Add(Archive); ModalVisible = false; await MessageService.Success("Archive uploaded!"); + + if (originalArchive != null) + { + BackgroundJob.Enqueue(x => x.Execute(originalArchive.Id, Archive.Id)); + } } } diff --git a/LANCommander/Services/ArchiveService.cs b/LANCommander/Services/ArchiveService.cs index 622ffcc..998266a 100644 --- a/LANCommander/Services/ArchiveService.cs +++ b/LANCommander/Services/ArchiveService.cs @@ -16,16 +16,26 @@ namespace LANCommander.Services { } - public override Task Delete(Archive entity) + public static string GetArchiveFileLocation(Archive archive) { - FileHelpers.DeleteIfExists($"Upload/{entity.ObjectKey}".ToPath()); + return GetArchiveFileLocation(archive.ObjectKey); + } - return base.Delete(entity); + public static string GetArchiveFileLocation(string objectKey) + { + return $"Upload/{objectKey}".ToPath(); + } + + public override Task Delete(Archive archive) + { + FileHelpers.DeleteIfExists(GetArchiveFileLocation(archive)); + + return base.Delete(archive); } public static GameManifest ReadManifest(string objectKey) { - var upload = $"Upload/{objectKey}".ToPath(); + var upload = GetArchiveFileLocation(objectKey); string manifestContents = String.Empty; @@ -57,7 +67,7 @@ namespace LANCommander.Services public static byte[] ReadFile(string objectKey, string path) { - var upload = $"Upload/{objectKey}".ToPath(); + var upload = GetArchiveFileLocation(objectKey); if (!File.Exists(upload)) throw new FileNotFoundException(upload); @@ -82,12 +92,77 @@ namespace LANCommander.Services { var archive = await Get(archiveId); - var upload = $"Upload/{archive.ObjectKey}".ToPath(); + var upload = GetArchiveFileLocation(archive); using (ZipArchive zip = ZipFile.OpenRead(upload)) { return zip.Entries; } } + + public async Task PatchArchive(Archive originalArchive, Archive alteredArchive, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + var alteredZipPath = GetArchiveFileLocation(alteredArchive); + var patchZipPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + ZipArchive originalZip = ZipFile.Open(GetArchiveFileLocation(originalArchive), ZipArchiveMode.Update); + ZipArchive alteredZip = ZipFile.OpenRead(alteredZipPath); + ZipArchive patchZip = ZipFile.Open(patchZipPath, ZipArchiveMode.Create); + + int i = 0; + + foreach (var entry in alteredZip.Entries) + { + var originalEntry = originalZip.GetEntry(entry.FullName); + + if (originalEntry == null || originalEntry.Crc32 != entry.Crc32) + { + originalEntry?.Delete(); + + var updatedEntry = originalZip.CreateEntry(entry.FullName, compressionLevel); + var patchEntry = patchZip.CreateEntry(entry.FullName, compressionLevel); + + // Copy the contents of the entry from the altered archive to the original archive + using (var updatedStream = updatedEntry.Open()) + using (var alteredStream = entry.Open()) + { + await alteredStream.CopyToAsync(updatedStream); + + Logger.Info("Added {EntryFullName} to base archive {ArchiveId} and new patch archive", entry.FullName, originalArchive.Id.ToString()); + } + + // Copy the contents of the entry from the altered archive to the patch archive + using (var patchStream = patchEntry.Open()) + using (var alteredStream = entry.Open()) + { + await alteredStream.CopyToAsync(patchStream); + + Logger.Info("Updated {EntryFullName} in base archive {ArchiveId} and added to new patch archive", entry.FullName, originalArchive.Id.ToString()); + } + } + + i++; + + Logger.Info("Finished processing entry {EntryIndex}/{TotalEntries} for original archive {ArchiveId}", i.ToString(), originalZip.Entries.Count.ToString(), originalArchive.Id.ToString()); + } + + originalZip.Dispose(); + alteredZip.Dispose(); + patchZip.Dispose(); + + // Replace the uploaded altered ZIP with the new patch ZIP + if (File.Exists(alteredZipPath)) + File.Delete(alteredZipPath); + + File.Move(patchZipPath, alteredZipPath); + + alteredArchive.CompressedSize = new FileInfo(GetArchiveFileLocation(alteredArchive)).Length; + originalArchive.CompressedSize = new FileInfo(GetArchiveFileLocation(originalArchive)).Length; + + await Update(alteredArchive); + await Update(originalArchive); + + Logger.Info("Finished merging original archive {ArchiveId} and rebuilt patch archive {PatchArchivePath}", originalArchive.Id.ToString(), alteredZipPath); + } } }