LANCommander/LANCommander.Server.Services/GameService.cs
Pat Hartl b28e8a51ee Fix auditing
The way updates are being handled means that the auditing interceptor doesn't work as the entities are not showing in the change tracker at that stage as modified. Added the auditing to BaseDatabaseService instead.
2025-02-09 18:53:40 -06:00

718 lines
30 KiB
C#

using AutoMapper;
using LANCommander.Server.Data;
using LANCommander.Server.Data.Models;
using LANCommander.Server.Services.Extensions;
using LANCommander.Server.Models;
using LANCommander.SDK;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Helpers;
using System.IO;
using System.IO.Compression;
using System.Linq.Expressions;
using ZiggyCreatures.Caching.Fusion;
using LANCommander.Server.Services.Models;
using Microsoft.Extensions.Logging;
using System.Data.SqlClient;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using SharpCompress.Common;
namespace LANCommander.Server.Services
{
public class GameService(
ILogger<GameService> logger,
IFusionCache cache,
IMapper mapper,
IHttpContextAccessor httpContextAccessor,
IDbContextFactory<DatabaseContext> contextFactory,
ArchiveService archiveService,
MediaService mediaService,
EngineService engineService,
TagService tagService,
CompanyService companyService,
GenreService genreService,
StorageLocationService storageLocationService) : BaseDatabaseService<Game>(logger, cache, mapper, httpContextAccessor, contextFactory)
{
public override async Task<Game> AddAsync(Game entity)
{
await cache.ExpireGameCacheAsync(entity.Id);
return await base.AddAsync(entity);
}
public override async Task<ExistingEntityResult<Game>> AddMissingAsync(Expression<Func<Game, bool>> predicate, Game entity)
{
await cache.ExpireGameCacheAsync(entity.Id);
return await base.AddMissingAsync(predicate, entity);
}
public override async Task<Game> UpdateAsync(Game entity)
{
await cache.ExpireGameCacheAsync(entity.Id);
if (entity.Media != null)
foreach (var media in entity.Media.Where(m => m.Id == Guid.Empty && String.IsNullOrWhiteSpace(m.Crc32)).ToList())
entity.Media.Remove(media);
var update = await base.UpdateAsync(entity, async context =>
{
await context.UpdateRelationshipAsync(g => g.Actions);
await context.UpdateRelationshipAsync(g => g.Archives);
await context.UpdateRelationshipAsync(g => g.BaseGame);
await context.UpdateRelationshipAsync(g => g.Categories);
await context.UpdateRelationshipAsync(g => g.Collections);
await context.UpdateRelationshipAsync(g => g.CustomFields);
await context.UpdateRelationshipAsync(g => g.Developers);
await context.UpdateRelationshipAsync(g => g.Engine);
await context.UpdateRelationshipAsync(g => g.Genres);
await context.UpdateRelationshipAsync(g => g.Keys);
await context.UpdateRelationshipAsync(g => g.Libraries);
await context.UpdateRelationshipAsync(g => g.Media);
await context.UpdateRelationshipAsync(g => g.MultiplayerModes);
await context.UpdateRelationshipAsync(g => g.Pages);
await context.UpdateRelationshipAsync(g => g.Platforms);
await context.UpdateRelationshipAsync(g => g.Publishers);
await context.UpdateRelationshipAsync(g => g.Redistributables);
await context.UpdateRelationshipAsync(g => g.SavePaths);
await context.UpdateRelationshipAsync(g => g.Scripts);
await context.UpdateRelationshipAsync(g => g.Tags);
});
return update;
}
public override async Task DeleteAsync(Game game)
{
game = await Include(
g => g.Archives,
g => g.Media)
.GetAsync(game.Id);
if (game.Archives != null)
foreach (var archive in game.Archives.ToList())
await archiveService.DeleteAsync(archive);
if (game.Media != null)
foreach (var media in game.Media.ToList())
await mediaService.DeleteAsync(media);
await cache.ExpireGameCacheAsync(game.Id);
await base.DeleteAsync(game);
}
public async Task<GameManifest> GetManifestAsync(Guid id)
{
var game = await Query(q =>
{
return q
.AsNoTracking()
.AsSplitQuery()
.Include(g => g.Actions)
.Include(g => g.Archives)
.Include(g => g.BaseGame)
.Include(g => g.Categories)
.Include(g => g.Collections)
.Include(g => g.CustomFields)
.Include(g => g.DependentGames)
.Include(g => g.Developers)
.Include(g => g.Engine)
.Include(g => g.Genres)
.Include(g => g.Media)
.Include(g => g.MultiplayerModes)
.Include(g => g.Platforms)
.Include(g => g.Publishers)
.Include(g => g.Redistributables)
.Include(g => g.Tags);
}).GetAsync(id);
return GetManifest(game);
}
public GameManifest GetManifest(Game game)
{
if (game == null)
return null;
var manifest = new GameManifest()
{
Id = game.Id,
Title = game.Title,
SortTitle = game.SortTitle,
Description = game.Description,
Notes = game.Notes,
ReleasedOn = game.ReleasedOn.GetValueOrDefault(),
Singleplayer = game.Singleplayer,
Type = (SDK.Enums.GameType)(int)game.Type,
};
if (game.Engine != null)
manifest.Engine = game.Engine.Name;
if (game.Genres != null && game.Genres.Count > 0)
manifest.Genre = game.Genres.Select(g => g.Name).ToArray();
if (game.Tags != null && game.Tags.Count > 0)
manifest.Tags = game.Tags.Select(g => g.Name).ToArray();
if (game.Publishers != null && game.Publishers.Count > 0)
manifest.Publishers = game.Publishers.Select(g => g.Name).ToArray();
if (game.Developers != null && game.Developers.Count > 0)
manifest.Developers = game.Developers.Select(g => g.Name).ToArray();
if (game.Collections != null && game.Collections.Count > 0)
manifest.Collections = game.Collections.Select(c => c.Name).ToArray();
if (game.Archives != null && game.Archives.Count > 0)
manifest.Version = game.Archives.OrderByDescending(a => a.CreatedOn).First().Version;
if (game.Media != null && game.Media.Count > 0)
manifest.Media = mapper.Map<IEnumerable<SDK.Models.Media>>(game.Media);
if (game.Actions != null && game.Actions.Count > 0)
{
manifest.Actions = game.Actions.Select(a => new SDK.Models.Action()
{
Name = a.Name,
Arguments = a.Arguments,
Path = a.Path,
WorkingDirectory = a.WorkingDirectory,
IsPrimaryAction = a.PrimaryAction,
SortOrder = a.SortOrder,
}).ToArray();
}
if (game.MultiplayerModes != null && game.MultiplayerModes.Count > 0)
{
var local = game.MultiplayerModes.FirstOrDefault(m => m.Type == MultiplayerType.Local);
var lan = game.MultiplayerModes.FirstOrDefault(m => m.Type == MultiplayerType.LAN);
var online = game.MultiplayerModes.FirstOrDefault(m => m.Type == MultiplayerType.Online);
if (local != null)
manifest.LocalMultiplayer = new MultiplayerInfo()
{
MinPlayers = local.MinPlayers,
MaxPlayers = local.MaxPlayers,
Description = local.Description,
NetworkProtocol = local.NetworkProtocol,
};
if (lan != null)
manifest.LanMultiplayer = new MultiplayerInfo()
{
MinPlayers = lan.MinPlayers,
MaxPlayers = lan.MaxPlayers,
Description = lan.Description,
NetworkProtocol = lan.NetworkProtocol,
};
if (online != null)
manifest.OnlineMultiplayer = new MultiplayerInfo()
{
MinPlayers = online.MinPlayers,
MaxPlayers = online.MaxPlayers,
Description = online.Description,
NetworkProtocol = online.NetworkProtocol,
};
}
if (game.SavePaths != null && game.SavePaths.Count > 0)
{
manifest.SavePaths = game.SavePaths.Select(p => new SDK.Models.SavePath()
{
Id = p.Id,
Path = p.Path,
IsRegex = p.IsRegex,
WorkingDirectory = p.WorkingDirectory,
Type = p.Type
});
}
if (game.DependentGames != null && game.DependentGames.Count > 0)
{
manifest.DependentGames = game.DependentGames.Select(g => g.Id);
}
if (game.CustomFields != null && game.CustomFields.Count > 0)
{
manifest.CustomFields = game.CustomFields.Select(cf => new SDK.Models.GameCustomField(cf.Name, cf.Value)).ToArray();
}
return manifest;
}
public async Task<GameManifest> ExportAsync(Guid id)
{
var game = await Query(q =>
{
return q
.AsNoTracking()
.AsSplitQuery()
.Include(g => g.Actions)
.Include(g => g.Archives)
.Include(g => g.BaseGame)
.Include(g => g.Categories)
.Include(g => g.Collections)
.Include(g => g.CustomFields)
.Include(g => g.DependentGames)
.Include(g => g.Developers)
.Include(g => g.Engine)
.Include(g => g.Genres)
.Include(g => g.Media)
.Include(g => g.MultiplayerModes)
.Include(g => g.Platforms)
.Include(g => g.Publishers)
.Include(g => g.Redistributables)
.Include(g => g.Scripts)
.Include(g => g.Tags);
}).GetAsync(id);
var manifest = await GetManifestAsync(id);
if (game.Media != null && game.Media.Count > 0)
{
manifest.Media = game.Media.Select(m => new SDK.Models.Media()
{
Id = m.Id,
FileId = m.FileId,
MimeType = m.MimeType,
SourceUrl = m.SourceUrl,
Type = (SDK.Enums.MediaType)(int)m.Type,
CreatedOn = m.CreatedOn,
}).ToList();
}
if (game.Scripts != null && game.Scripts.Count > 0)
{
manifest.Scripts = game.Scripts.Select(s => new SDK.Models.Script()
{
Id = s.Id,
Name = s.Name,
Description = s.Description,
RequiresAdmin = s.RequiresAdmin,
Type = (SDK.Enums.ScriptType)(int)s.Type,
CreatedOn = s.CreatedOn,
UpdatedOn = s.UpdatedOn,
});
}
if (game.Archives != null && game.Archives.Count > 0)
{
manifest.Archives = game.Archives.Select(a => new SDK.Models.Archive()
{
Id = a.Id,
Changelog = a.Changelog,
Version = a.Version,
CreatedOn = a.CreatedOn,
ObjectKey = a.ObjectKey,
}).ToList();
}
if (game.Keys != null && game.Keys.Count > 0)
manifest.Keys = game.Keys.Select(k => k.Value).ToList();
return manifest;
}
public async Task<Game> ImportLocalFileAsync(string path)
{
Guid objectKey = Guid.NewGuid();
var importArchivePath = await archiveService.GetArchiveFileLocationAsync(objectKey.ToString());
File.Copy(path, importArchivePath, true);
return await ImportAsync(objectKey);
}
public async Task<Game> ImportAsync(Guid objectKey)
{
var importArchive = await archiveService.FirstOrDefaultAsync(a => a.ObjectKey == objectKey.ToString());
var importArchivePath = await archiveService.GetArchiveFileLocationAsync(importArchive);
var storageLocation = await storageLocationService.GetAsync(importArchive.StorageLocationId);
Game game;
using (var importZip = ZipFile.OpenRead(importArchivePath))
{
// Read manifest
GameManifest manifest = ManifestHelper.Deserialize<GameManifest>(await importZip.ReadAllTextAsync(ManifestHelper.ManifestFilename));
game = await GetAsync(manifest.Id);
var exists = game != null;
if (!exists)
game = new Game();
else
{
game = await Include(g => g.Actions)
.Include(g => g.Archives)
.Include(g => g.BaseGame)
.Include(g => g.Categories)
.Include(g => g.Collections)
.Include(g => g.DependentGames)
.Include(g => g.Developers)
.Include(g => g.Engine)
.Include(g => g.Genres)
.Include(g => g.Media)
.Include(g => g.MultiplayerModes)
.Include(g => g.Platforms)
.Include(g => g.Publishers)
.Include(g => g.Redistributables)
.Include(g => g.Tags)
.FirstOrDefaultAsync(g => g.Id == manifest.Id);
}
game.Id = manifest.Id;
game.Description = manifest.Description;
game.Notes = manifest.Notes;
game.ReleasedOn = manifest.ReleasedOn;
game.Singleplayer = manifest.Singleplayer;
game.SortTitle = manifest.SortTitle;
game.Title = manifest.Title;
if (!exists)
game = await AddAsync(game);
#region Actions
game.Actions = new List<Data.Models.Action>();
if (manifest.Actions != null && manifest.Actions.Count() > 0)
foreach (var action in manifest.Actions)
{
game.Actions.Add(new Data.Models.Action()
{
Name = action.Name,
Arguments = action.Arguments,
Path = action.Path,
WorkingDirectory = action.WorkingDirectory,
PrimaryAction = action.IsPrimaryAction,
SortOrder = action.SortOrder,
});
}
#endregion
#region Engine
if (game.Engine != null)
{
var engine = await engineService.AddMissingAsync(e => e.Name == manifest.Engine, new Engine { Name = manifest.Engine });
game.Engine = engine.Value;
}
#endregion
#region Tags
if (game.Tags == null)
game.Tags = new List<Data.Models.Tag>();
if (manifest.Tags != null)
foreach (var tag in manifest.Tags.Where(mt => !game.Tags.Any(t => t.Name == mt)))
{
game.Tags.Add((await tagService.AddMissingAsync(t => t.Name == tag, new Tag()
{
Name = tag
})).Value);
}
foreach (var tag in game.Tags.Where(c => !manifest.Tags.Any(t => c.Name == t)))
game.Tags.Remove(tag);
#endregion
#region Genres
if (game.Genres == null)
game.Genres = new List<Data.Models.Genre>();
if (manifest.Genre != null)
foreach (var genre in manifest.Genre.Where(mg => !game.Genres.Any(g => g.Name == mg)))
{
game.Genres.Add((await genreService.AddMissingAsync(g => g.Name == genre, new Genre()
{
Name = genre
})).Value);
}
foreach (var genre in game.Genres.Where(c => !manifest.Genre.Any(g => c.Name == g)))
game.Genres.Remove(genre);
#endregion
#region Developers
if (game.Developers == null)
game.Developers = new List<Data.Models.Company>();
if (manifest.Developers != null)
foreach (var developer in manifest.Developers.Where(md => !game.Developers.Any(c => c.Name == md)))
{
game.Developers.Add((await companyService.AddMissingAsync(c => c.Name == developer, new Company()
{
Name = developer
})).Value);
}
foreach (var developer in game.Developers.Where(c => !manifest.Developers.Any(d => c.Name == d)))
game.Developers.Remove(developer);
#endregion
#region Publishers
if (game.Publishers == null)
game.Publishers = new List<Data.Models.Company>();
if (manifest.Publishers != null)
foreach (var publisher in manifest.Publishers.Where(mp => !game.Publishers.Any(c => c.Name == mp)))
{
game.Publishers.Add((await companyService.AddMissingAsync(c => c.Name == publisher, new Company()
{
Name = publisher
})).Value);
}
foreach (var publisher in game.Publishers.Where(c => !manifest.Publishers.Any(p => c.Name == p)))
game.Publishers.Remove(publisher);
#endregion
#region Multiplayer Modes
if (game.MultiplayerModes == null)
game.MultiplayerModes = new List<Data.Models.MultiplayerMode>();
else
game.MultiplayerModes.Clear();
if (manifest.LanMultiplayer != null)
game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.LAN,
MinPlayers = manifest.LanMultiplayer.MinPlayers,
MaxPlayers = manifest.LanMultiplayer.MaxPlayers,
Description = manifest.LanMultiplayer.Description,
NetworkProtocol = manifest.LanMultiplayer.NetworkProtocol,
});
if (manifest.LocalMultiplayer != null)
game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.Local,
MinPlayers = manifest.LocalMultiplayer.MinPlayers,
MaxPlayers = manifest.LocalMultiplayer.MaxPlayers,
Description = manifest.LocalMultiplayer.Description,
NetworkProtocol = manifest.LocalMultiplayer.NetworkProtocol,
});
if (manifest.OnlineMultiplayer != null)
game.MultiplayerModes.Add(new MultiplayerMode()
{
Type = MultiplayerType.Online,
MinPlayers = manifest.OnlineMultiplayer.MinPlayers,
MaxPlayers = manifest.OnlineMultiplayer.MaxPlayers,
Description = manifest.OnlineMultiplayer.Description,
NetworkProtocol = manifest.OnlineMultiplayer.NetworkProtocol,
});
#endregion
#region Save Paths
if (game.SavePaths == null)
game.SavePaths = new List<SavePath>();
foreach (var path in game.SavePaths)
{
var manifestSavePath = manifest.SavePaths.FirstOrDefault(sp => sp.Id == path.Id);
if (manifestSavePath != null)
{
path.Path = manifestSavePath.Path;
path.WorkingDirectory = manifestSavePath.WorkingDirectory;
path.IsRegex = manifestSavePath.IsRegex;
path.Type = manifestSavePath.Type;
}
else
game.SavePaths.Remove(path);
}
if (manifest.SavePaths != null)
foreach (var manifestSavePath in manifest.SavePaths.Where(msp => !game.SavePaths.Any(sp => sp.Id == msp.Id)))
{
game.SavePaths.Add(new SavePath()
{
Path = manifestSavePath.Path,
WorkingDirectory = manifestSavePath.WorkingDirectory,
IsRegex = manifestSavePath.IsRegex,
Type = manifestSavePath.Type
});
}
#endregion
#region Keys
if (game.Keys == null)
game.Keys = new List<Data.Models.Key>();
foreach (var key in game.Keys)
{
if (!manifest.Keys.Contains(key.Value))
game.Keys.Remove(key);
}
if (manifest.Keys != null)
foreach (var key in manifest.Keys)
{
if (!game.Keys.Any(k => k.Value == key))
game.Keys.Add(new Key()
{
Value = key
});
}
#endregion
#region Scripts
if (game.Scripts == null)
game.Scripts = new List<Data.Models.Script>();
foreach (var script in game.Scripts)
{
var manifestScript = manifest.Scripts.FirstOrDefault(s => s.Id == script.Id);
if (manifestScript != null)
{
script.Contents = await importZip.ReadAllTextAsync($"Scripts/{script.Id}");
script.Description = manifestScript.Description;
script.Name = manifestScript.Name;
script.RequiresAdmin = manifestScript.RequiresAdmin;
script.Type = (ScriptType)(int)manifestScript.Type;
}
else
game.Scripts.Remove(script);
}
if (manifest.Scripts != null)
foreach (var manifestScript in manifest.Scripts.Where(ms => !game.Scripts.Any(s => s.Id == ms.Id)))
{
game.Scripts.Add(new Script()
{
Id = manifestScript.Id,
Contents = await importZip.ReadAllTextAsync($"Scripts/{manifestScript.Id}"),
Description = manifestScript.Description,
Name = manifestScript.Name,
RequiresAdmin = manifestScript.RequiresAdmin,
Type = (ScriptType)(int)manifestScript.Type,
CreatedOn = manifestScript.CreatedOn,
});
}
#endregion
#region Media
var mediaStorageLocation =
await storageLocationService.FirstOrDefaultAsync(l => l.Type == StorageLocationType.Media && l.Default);
if (game.Media == null)
game.Media = new List<Data.Models.Media>();
foreach (var media in game.Media)
{
var manifestMedia = manifest.Media.FirstOrDefault(s => s.Id == media.Id);
if (manifestMedia != null)
{
media.SourceUrl = manifestMedia.SourceUrl;
media.FileId = manifestMedia.FileId;
media.Type = manifestMedia.Type;
media.MimeType = manifestMedia.MimeType;
media.CreatedOn = manifestMedia.CreatedOn;
importZip.ExtractEntry($"Media/{media.FileId}", MediaService.GetMediaPath(media), true);
media.Crc32 = SDK.Services.MediaService.CalculateChecksum(MediaService.GetMediaPath(media));
await mediaService.UpdateAsync(media);
}
}
if (manifest.Media != null)
foreach (var manifestMedia in manifest.Media.Where(mm => !game.Media.Any(m => m.Id == mm.Id)))
{
var media = new Media()
{
FileId = Guid.NewGuid(),
MimeType = manifestMedia.MimeType,
SourceUrl = manifestMedia.SourceUrl,
Type = manifestMedia.Type,
CreatedOn = manifestMedia.CreatedOn,
StorageLocationId = mediaStorageLocation.Id,
};
var mediaPath = MediaService.GetMediaPath(media, mediaStorageLocation);
importZip.ExtractEntry($"Media/{manifestMedia.FileId}", mediaPath, true);
media.Crc32 = SDK.Services.MediaService.CalculateChecksum(mediaPath);
media = await mediaService.AddAsync(media);
game.Media.Add(media);
}
#endregion
#region Archives
if (game.Archives == null)
game.Archives = new List<Data.Models.Archive>();
foreach (var archive in game.Archives)
{
var manifestArchive = manifest.Archives.FirstOrDefault(a => a.Id == archive.Id);
if (manifestArchive != null)
{
archive.Changelog = manifestArchive.Changelog;
archive.ObjectKey = manifestArchive.ObjectKey;
archive.Version = manifestArchive.Version;
archive.CreatedOn = manifestArchive.CreatedOn;
var extractionLocation = await archiveService.GetArchiveFileLocationAsync(archive);
importZip.ExtractEntry($"Archives/{archive.ObjectKey}", extractionLocation, true);
var archiveFile = ZipFile.Open(extractionLocation, ZipArchiveMode.Read);
archive.CompressedSize = new FileInfo(extractionLocation).Length;
archive.UncompressedSize = archiveFile.Entries.Sum(e => e.Length);
await archiveService.UpdateAsync(archive);
}
}
if (manifest.Archives != null)
foreach (var manifestArchive in manifest.Archives.Where(ma => !game.Archives.Any(a => a.Id == ma.Id)))
{
var archive = new Archive()
{
ObjectKey = Guid.NewGuid().ToString(),
Changelog = manifestArchive.Changelog,
Version = manifestArchive.Version,
CreatedOn = manifestArchive.CreatedOn,
StorageLocationId = storageLocation.Id,
};
var extractionLocation = await archiveService.GetArchiveFileLocationAsync(archive);
importZip.ExtractEntry($"Archives/{manifestArchive.ObjectKey}", extractionLocation, true);
var archiveFile = ZipFile.Open(extractionLocation, ZipArchiveMode.Read);
archive.CompressedSize = new FileInfo(extractionLocation).Length;
archive.UncompressedSize = archiveFile.Entries.Sum(e => e.Length);
archive = await archiveService.AddAsync(archive);
game.Archives.Add(archive);
}
#endregion
game = await UpdateAsync(game);
}
await archiveService.DeleteAsync(importArchive, storageLocation);
await cache.ExpireGameCacheAsync(game.Id);
return game;
}
}
}