2024-08-04 18:44:33 -05:00
using LANCommander.Server.Data ;
using LANCommander.Server.Data.Models ;
2023-01-09 01:38:41 -06:00
using LANCommander.Helpers ;
2023-01-08 01:34:38 -06:00
using System.IO.Compression ;
2024-05-07 23:08:22 -05:00
using System.Linq.Expressions ;
2025-02-01 02:21:34 -06:00
using AutoMapper ;
2025-09-06 12:57:15 -05:00
using LANCommander.SDK.Services ;
2025-01-23 00:25:23 -06:00
using LANCommander.Server.Services.Extensions ;
2023-01-08 01:34:38 -06:00
using YamlDotNet.Serialization ;
2024-05-07 23:08:22 -05:00
using ZiggyCreatures.Caching.Fusion ;
2024-10-07 18:04:26 -05:00
using LANCommander.Server.Services.Models ;
2025-02-09 18:53:40 -06:00
using Microsoft.AspNetCore.Http ;
2024-10-07 18:04:26 -05:00
using Microsoft.Extensions.Logging ;
2024-10-11 01:09:12 -05:00
using Microsoft.EntityFrameworkCore ;
2025-02-01 02:21:34 -06:00
using PascalCaseNamingConvention = YamlDotNet . Serialization . NamingConventions . PascalCaseNamingConvention ;
2025-12-17 22:52:26 +11:00
using LANCommander.SDK ;
2023-01-08 01:34:38 -06:00
2024-08-04 18:44:33 -05:00
namespace LANCommander.Server.Services
2023-01-08 01:34:38 -06:00
{
2025-11-17 22:09:03 -06:00
public sealed class ArchiveService (
ILogger < ArchiveService > logger ,
2025-11-16 12:31:25 -06:00
SettingsProvider < Settings . Settings > settingsProvider ,
2025-02-01 02:21:34 -06:00
IFusionCache cache ,
IMapper mapper ,
2025-02-09 18:53:40 -06:00
IHttpContextAccessor httpContextAccessor ,
2025-02-01 02:21:34 -06:00
IDbContextFactory < DatabaseContext > dbContextFactory ,
2025-11-16 12:31:25 -06:00
StorageLocationService storageLocationService ) : BaseDatabaseService < Archive > ( logger , settingsProvider , cache , mapper , httpContextAccessor , dbContextFactory ) , IArchiveClient
2023-01-08 01:34:38 -06:00
{
2025-11-20 22:04:20 -06:00
public async Task < Archive > GetLatestArchiveAsync ( Expression < Func < Archive , bool > > predicate )
2023-01-09 01:05:40 -06:00
{
2025-02-01 02:21:34 -06:00
return await Query ( q = >
{
return q . OrderByDescending ( a = > a . CreatedOn ) ;
} ) . FirstOrDefaultAsync ( predicate ) ;
2023-01-09 01:05:40 -06:00
}
2025-02-01 02:21:34 -06:00
2025-01-23 00:25:23 -06:00
public string GetArchiveFileLocation ( Archive archive , StorageLocation storageLocation )
{
return Path . Combine ( storageLocation . Path , archive . ObjectKey ) ;
}
2025-01-08 17:10:03 -06:00
public async Task < string > GetArchiveFileLocationAsync ( Archive archive )
2023-01-09 01:38:41 -06:00
{
2025-01-08 17:10:03 -06:00
string storageLocationPath ;
if ( archive . StorageLocation ! = null )
storageLocationPath = archive . StorageLocation . Path ;
else
{
2025-02-01 02:21:34 -06:00
var storageLocation = await storageLocationService . GetAsync ( archive . StorageLocationId ) ;
2025-01-08 17:10:03 -06:00
storageLocationPath = storageLocation . Path ;
}
2025-12-19 14:51:06 +11:00
return Path . IsPathRooted ( storageLocationPath ) ?
Path . Combine ( storageLocationPath , archive . ObjectKey ) :
Path . Combine ( AppPaths . GetConfigDirectory ( ) , storageLocationPath , archive . ObjectKey ) ;
2023-04-20 00:26:08 -05:00
}
2024-10-11 01:09:12 -05:00
public async Task < string > GetArchiveFileLocationAsync ( string objectKey )
2023-04-20 00:26:08 -05:00
{
2025-01-08 17:10:03 -06:00
var archive = await Include ( a = > a . StorageLocation ) . FirstOrDefaultAsync ( a = > a . ObjectKey = = objectKey ) ;
2023-09-12 20:23:06 -05:00
2025-01-08 17:10:03 -06:00
return await GetArchiveFileLocationAsync ( archive ) ;
2023-04-20 00:26:08 -05:00
}
2023-01-09 01:38:41 -06:00
2024-11-12 18:32:46 -06:00
public override async Task < Archive > AddAsync ( Archive entity )
2024-05-07 23:08:22 -05:00
{
2025-02-01 02:21:34 -06:00
await cache . ExpireGameCacheAsync ( entity . GameId ) ;
2024-05-07 23:08:22 -05:00
2025-02-25 21:22:33 -06:00
return await base . AddAsync ( entity , async context = >
2025-02-23 08:49:43 -06:00
{
await context . UpdateRelationshipAsync ( a = > a . Game ) ;
await context . UpdateRelationshipAsync ( a = > a . Redistributable ) ;
await context . UpdateRelationshipAsync ( a = > a . StorageLocation ) ;
} ) ;
2024-05-07 23:08:22 -05:00
}
2024-11-12 18:32:46 -06:00
public override async Task < ExistingEntityResult < Archive > > AddMissingAsync ( Expression < Func < Archive , bool > > predicate , Archive entity )
2024-05-07 23:08:22 -05:00
{
2025-02-01 02:21:34 -06:00
await cache . ExpireGameCacheAsync ( entity . GameId ) ;
2024-05-07 23:08:22 -05:00
2024-11-12 18:32:46 -06:00
return await base . AddMissingAsync ( predicate , entity ) ;
2024-05-07 23:08:22 -05:00
}
2025-02-01 02:21:34 -06:00
public override async Task < Archive > UpdateAsync ( Archive updatedArchive )
2024-05-07 23:08:22 -05:00
{
2025-02-04 02:31:23 -06:00
await cache . ExpireGameCacheAsync ( updatedArchive . GameId ) ;
2025-05-17 06:38:14 +02:00
await cache . ExpireArchiveCacheAsync ( updatedArchive . Id ) ;
2025-02-04 02:31:23 -06:00
2025-02-02 14:58:07 -06:00
return await base . UpdateAsync ( updatedArchive , async context = >
{
await context . UpdateRelationshipAsync ( a = > a . Game ) ;
await context . UpdateRelationshipAsync ( a = > a . Redistributable ) ;
await context . UpdateRelationshipAsync ( a = > a . StorageLocation ) ;
} ) ;
2024-05-07 23:08:22 -05:00
}
2025-01-23 00:25:11 -06:00
2024-11-12 18:32:46 -06:00
public override async Task DeleteAsync ( Archive archive )
2023-04-20 00:26:08 -05:00
{
2025-01-08 17:10:03 -06:00
FileHelpers . DeleteIfExists ( await GetArchiveFileLocationAsync ( archive ) ) ;
2023-04-20 00:26:08 -05:00
2025-02-01 02:21:34 -06:00
await cache . ExpireGameCacheAsync ( archive . GameId ) ;
2025-05-17 06:38:14 +02:00
await cache . ExpireArchiveCacheAsync ( archive . Id ) ;
2025-01-23 00:25:23 -06:00
await base . DeleteAsync ( archive ) ;
}
public async Task DeleteAsync ( Archive archive , StorageLocation storageLocation = null )
{
if ( storageLocation = = null )
FileHelpers . DeleteIfExists ( await GetArchiveFileLocationAsync ( archive ) ) ;
else
FileHelpers . DeleteIfExists ( GetArchiveFileLocation ( archive , storageLocation ) ) ;
2025-02-01 02:21:34 -06:00
await cache . ExpireGameCacheAsync ( archive . GameId ) ;
2024-05-07 23:08:22 -05:00
2024-11-12 18:32:46 -06:00
await base . DeleteAsync ( archive ) ;
2023-01-09 01:38:41 -06:00
}
2025-11-29 18:24:27 -06:00
public async Task < SDK . Models . Manifest . Game > ReadManifestAsync ( string objectKey )
2023-01-08 01:34:38 -06:00
{
2024-10-11 01:09:12 -05:00
var upload = await GetArchiveFileLocationAsync ( objectKey ) ;
2023-01-08 01:34:38 -06:00
string manifestContents = String . Empty ;
2023-01-09 01:38:41 -06:00
if ( ! File . Exists ( upload ) )
2023-01-08 01:34:38 -06:00
throw new FileNotFoundException ( upload ) ;
using ( ZipArchive zip = ZipFile . OpenRead ( upload ) )
{
var entry = zip . Entries . FirstOrDefault ( e = > e . FullName = = "_manifest.yml" ) ;
if ( entry = = null )
throw new FileNotFoundException ( "Manifest not found" ) ;
using ( StreamReader sr = new StreamReader ( entry . Open ( ) ) )
{
manifestContents = sr . ReadToEnd ( ) ;
}
}
var deserializer = new DeserializerBuilder ( )
2023-01-08 12:10:20 -06:00
. IgnoreUnmatchedProperties ( )
2023-12-20 20:25:25 -06:00
. WithNamingConvention ( new PascalCaseNamingConvention ( ) )
2023-01-08 01:34:38 -06:00
. Build ( ) ;
2025-11-29 18:24:27 -06:00
var manifest = deserializer . Deserialize < SDK . Models . Manifest . Game > ( manifestContents ) ;
2023-01-08 01:34:38 -06:00
return manifest ;
}
2024-11-12 18:32:46 -06:00
public async Task < byte [ ] > ReadFileAsync ( string objectKey , string path )
2023-01-08 01:34:38 -06:00
{
2024-10-11 01:09:12 -05:00
var upload = await GetArchiveFileLocationAsync ( objectKey ) ;
2023-01-08 01:34:38 -06:00
2023-01-09 01:38:41 -06:00
if ( ! File . Exists ( upload ) )
2023-01-08 01:34:38 -06:00
throw new FileNotFoundException ( upload ) ;
using ( ZipArchive zip = ZipFile . OpenRead ( upload ) )
{
var entry = zip . Entries . FirstOrDefault ( e = > e . FullName = = path ) ;
if ( entry = = null )
throw new FileNotFoundException ( path ) ;
using ( var ms = new MemoryStream ( ) )
{
entry . Open ( ) . CopyTo ( ms ) ;
return ms . ToArray ( ) ;
}
}
}
2023-01-28 21:18:36 -06:00
2025-12-03 23:41:51 -06:00
public async Task < bool > FileExistsAsync ( Guid archiveId )
2023-08-02 18:54:57 -05:00
{
2025-01-08 17:10:03 -06:00
var archive = await Include ( a = > a . StorageLocation ) . GetAsync ( archiveId ) ;
2023-08-02 18:54:57 -05:00
2025-12-03 19:33:27 -06:00
if ( archive = = null )
return false ;
2025-01-08 17:10:03 -06:00
var path = await GetArchiveFileLocationAsync ( archive ) ;
2023-08-02 18:54:57 -05:00
return File . Exists ( path ) ;
}
2024-11-12 18:32:46 -06:00
public async Task < Guid > CopyFromLocalFileAsync ( string path )
2024-03-30 14:55:16 -05:00
{
Guid objectKey = Guid . NewGuid ( ) ;
2024-10-11 01:09:12 -05:00
var importArchivePath = await GetArchiveFileLocationAsync ( objectKey . ToString ( ) ) ;
2024-03-30 14:55:16 -05:00
File . Copy ( path , importArchivePath , true ) ;
return objectKey ;
}
2024-11-12 18:32:46 -06:00
public async Task < IEnumerable < ZipArchiveEntry > > GetContentsAsync ( Guid archiveId )
2023-01-28 21:18:36 -06:00
{
2025-01-08 17:10:03 -06:00
var archive = await Include ( a = > a . StorageLocation ) . GetAsync ( archiveId ) ;
2023-01-28 21:18:36 -06:00
2025-01-08 17:10:03 -06:00
var upload = await GetArchiveFileLocationAsync ( archive ) ;
2023-01-28 21:18:36 -06:00
using ( ZipArchive zip = ZipFile . OpenRead ( upload ) )
{
return zip . Entries ;
}
}
2023-04-20 00:26:08 -05:00
2025-01-08 17:10:03 -06:00
public async Task < long > GetCompressedSizeAsync ( Archive archive )
2024-10-11 01:09:12 -05:00
{
long size = 0 ;
try
{
2025-01-08 17:10:03 -06:00
size = new FileInfo ( await GetArchiveFileLocationAsync ( archive ) ) . Length ;
2024-10-11 01:09:12 -05:00
}
catch { }
return size ;
}
2025-01-08 17:10:03 -06:00
public async Task < long > GetUncompressedSizeAsync ( Archive archive )
2024-10-11 01:09:12 -05:00
{
long size = 0 ;
2025-01-08 17:10:03 -06:00
using ( ZipArchive zip = ZipFile . OpenRead ( await GetArchiveFileLocationAsync ( archive ) ) )
2024-10-11 01:09:12 -05:00
{
foreach ( ZipArchiveEntry entry in zip . Entries )
{
size + = entry . Length ;
}
}
return size ;
}
2025-05-23 13:29:54 +02:00
public async Task < bool > RecalculateFileSizeArchiveAsync ( Archive archive )
{
try
{
var path = await GetArchiveFileLocationAsync ( archive ) ;
if ( File . Exists ( path ) )
{
archive . CompressedSize = await GetCompressedSizeAsync ( archive ) ;
archive . UncompressedSize = await GetUncompressedSizeAsync ( archive ) ;
await UpdateAsync ( archive ) ;
return true ;
}
else
{
_logger ? . LogWarning ( "No local file found for archive {ArchiveId} on path: {path}" , archive . Id , path ) ;
}
}
catch ( Exception ex )
{
_logger ? . LogError ( ex , "Could not recalculate file size for archive {ArchiveId}" , archive . Id ) ;
}
return false ;
}
2025-08-03 14:11:34 -05:00
2025-03-28 19:16:51 -05:00
public async Task < Archive > WriteToFileAsync ( Archive archive , Stream stream , bool overwrite = true )
{
if ( archive . StorageLocation = = null )
archive = await Include ( a = > a . StorageLocation ) . GetAsync ( archive . Id ) ;
if ( archive . StorageLocation = = null )
throw new ArgumentException ( "Archive has no storage location" , nameof ( archive ) ) ;
var path = GetArchiveFileLocation ( archive , archive . StorageLocation ) ;
if ( overwrite & & File . Exists ( path ) )
File . Delete ( path ) ;
using ( var fs = new FileStream ( path , FileMode . Create , FileAccess . Write ) )
{
await stream . CopyToAsync ( fs ) ;
}
archive . CompressedSize = await GetCompressedSizeAsync ( archive ) ;
archive . UncompressedSize = await GetUncompressedSizeAsync ( archive ) ;
await UpdateAsync ( archive ) ;
return archive ;
}
2024-11-12 18:32:46 -06:00
public async Task PatchArchiveAsync ( Archive originalArchive , Archive alteredArchive , CompressionLevel compressionLevel = CompressionLevel . Optimal )
2023-04-20 00:26:08 -05:00
{
2025-01-08 17:10:03 -06:00
var alteredZipPath = await GetArchiveFileLocationAsync ( alteredArchive ) ;
2023-04-20 00:26:08 -05:00
var patchZipPath = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
2025-01-08 17:10:03 -06:00
ZipArchive originalZip = ZipFile . Open ( await GetArchiveFileLocationAsync ( originalArchive ) , ZipArchiveMode . Update ) ;
2023-04-20 00:26:08 -05:00
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 ) ;
2025-02-01 02:21:34 -06:00
_logger ? . LogInformation ( "Added {EntryFullName} to base archive {ArchiveId} and new patch archive" , entry . FullName , originalArchive . Id . ToString ( ) ) ;
2023-04-20 00:26:08 -05:00
}
// 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 ) ;
2025-02-01 02:21:34 -06:00
_logger ? . LogInformation ( "Updated {EntryFullName} in base archive {ArchiveId} and added to new patch archive" , entry . FullName , originalArchive . Id . ToString ( ) ) ;
2023-04-20 00:26:08 -05:00
}
}
i + + ;
2025-02-01 02:21:34 -06:00
_logger ? . LogInformation ( "Finished processing entry {EntryIndex}/{TotalEntries} for original archive {ArchiveId}" , i . ToString ( ) , originalZip . Entries . Count . ToString ( ) , originalArchive . Id . ToString ( ) ) ;
2023-04-20 00:26:08 -05:00
}
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 ) ;
2025-01-08 17:10:03 -06:00
alteredArchive . CompressedSize = new FileInfo ( await GetArchiveFileLocationAsync ( alteredArchive ) ) . Length ;
originalArchive . CompressedSize = new FileInfo ( await GetArchiveFileLocationAsync ( originalArchive ) ) . Length ;
2023-04-20 00:26:08 -05:00
2024-11-12 18:32:46 -06:00
await UpdateAsync ( alteredArchive ) ;
await UpdateAsync ( originalArchive ) ;
2023-04-20 00:26:08 -05:00
2025-02-01 02:21:34 -06:00
_logger ? . LogInformation ( "Finished merging original archive {ArchiveId} and rebuilt patch archive {PatchArchivePath}" , originalArchive . Id . ToString ( ) , alteredZipPath ) ;
2023-04-20 00:26:08 -05:00
}
2023-01-08 01:34:38 -06:00
}
}