LANCommander/LANCommander.Server/UI/Components/ArchiveEditor.razor
Pat Hartl 24eaa79885 Add tool to repack non-streamable ZIP archives
Adds a tool that checks archives to see if they use data descriptors instead of local entry headers for tracking entry size. It will also repack these archives if required.
2026-06-22 19:18:11 -05:00

302 lines
10 KiB
Text

@inject ArchiveService ArchiveService
@inject ScriptService ScriptService
@inject GameService GameService
@inject ToolService ToolService
@inject RedistributableService RedistributableService
@inject HttpClient HttpClient
@inject NavigationManager Navigator
@inject ModalService ModalService
@inject IMessageService MessageService
@inject ConfirmService ConfirmService
@inject IJSRuntime JS
@inject ILogger<ArchiveEditor> Logger
@using Hangfire
@using LANCommander.Server.Jobs.Background
<DataTable
@ref="_table"
TItem="Archive"
HidePagination
Responsive
Query="a => (GameId != Guid.Empty && a.GameId == GameId) || (RedistributableId != Guid.Empty && a.RedistributableId == RedistributableId) || (ToolId != Guid.Empty && a.ToolId == ToolId)">
<RightToolbar>
@if (_hasPackageScript)
{
<Button OnClick="Package" Loading="_packaging">Package</Button>
}
<Button OnClick="RecalculateFileSizes" Type="@ButtonType.Default">Recalculate File Sizes</Button>
<Button OnClick="UploadArchive" Type="@ButtonType.Primary">Upload Archive</Button>
</RightToolbar>
<Columns>
<BoundDataColumn Property="a => a.Version">
<Input Type="InputType.Text" Bordered="false" @bind-Value="context.Version" OnBlur="() => Update(context)"/>
</BoundDataColumn>
<BoundDataColumn Property="a => a.CompressedSize">
<ByteSize Value="context.CompressedSize" />
</BoundDataColumn>
<BoundDataColumn Property="a => a.UncompressedSize">
<ByteSize Value="context.UncompressedSize" />
</BoundDataColumn>
<BoundDataColumn Property="a => a.CreatedBy != null ? a.CreatedBy.UserName : String.Empty" Include="CreatedBy"/>
<BoundDataColumn Property="s => s.CreatedOn" DefaultSortOrder="SortDirection.Descending">
<LocalTime Value="context.CreatedOn" />
</BoundDataColumn>
<DataActions TData="string">
<Tooltip Title="Recalculate File Size">
<Button Icon="@IconType.Outline.Sync" Type="@ButtonType.Text" OnClick="() => RecalculateFileSize(context)"/>
</Tooltip>
<Tooltip Title="Check Streaming Compatibility">
<Button Icon="@IconType.Outline.Safety" Type="@ButtonType.Text" OnClick="() => CheckStreamingCompatibility(context)" Loading="_checking"/>
</Tooltip>
<Tooltip Title="Download">
<a href="/Download/Archive/@context.Id" target="_blank" class="ant-btn ant-btn-text ant-btn-icon-only">
<Icon Type="@IconType.Outline.Download"/>
</a>
</Tooltip>
<Tooltip Title="Browse">
<Button Icon="@IconType.Outline.FolderOpen" Type="@ButtonType.Text" OnClick="() => BrowseArchive(context)" Loading="_browsing"/>
</Tooltip>
<Popconfirm Title="Are you sure you want to delete this archive?" OnConfirm="() => Delete(context)">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger/>
</Popconfirm>
</DataActions>
</Columns>
</DataTable>
<ArchiveUploader @ref="_uploader" GameId="GameId" RedistributableId="RedistributableId" ToolId="ToolId" OnArchiveUploaded="ArchiveUploaded" />
@code {
[CascadingParameter(Name = "GameId")] public Guid GameId { get; set; }
[CascadingParameter(Name = "RedistributableId")] public Guid RedistributableId { get; set; }
[CascadingParameter(Name = "ToolId")] public Guid ToolId { get; set; }
DataTable<Archive> _table;
ArchiveUploader _uploader;
bool _browsing;
bool _packaging;
bool _checking;
bool _hasPackageScript;
protected override async Task OnInitializedAsync()
{
HttpClient.BaseAddress = new Uri(Navigator.BaseUri);
if (GameId != Guid.Empty)
_hasPackageScript = (await ScriptService.GetAsync(s => s.GameId == GameId && s.Type == SDK.Enums.ScriptType.Package)).Any();
else if (RedistributableId != Guid.Empty)
_hasPackageScript = (await ScriptService.GetAsync(s => s.RedistributableId == RedistributableId && s.Type == SDK.Enums.ScriptType.Package)).Any();
else if (ToolId != Guid.Empty)
_hasPackageScript = (await ScriptService.GetAsync(s => s.ToolId == ToolId && s.Type == SDK.Enums.ScriptType.Package)).Any();
}
private async Task Package()
{
_packaging = true;
await InvokeAsync(StateHasChanged);
await Task.Yield();
string title = "Package";
if (GameId != Guid.Empty)
{
var game = await GameService.GetAsync(GameId);
title = $"Package {game.Title}";
}
else if (ToolId != Guid.Empty)
{
var tool = await ToolService.GetAsync(ToolId);
title = $"Package {tool.Name}";
}
else if (RedistributableId != Guid.Empty)
{
var redistributable = await RedistributableService.GetAsync(RedistributableId);
title = $"Package {redistributable.Name}";
}
var modalOptions = new ModalOptions()
{
Title = title,
Maximizable = true,
DefaultMaximized = false,
Closable = true,
Footer = null,
Width = 800,
};
var options = new PackagingDialogOptions
{
GameId = GameId,
ToolId = ToolId,
RedistributableId = RedistributableId,
};
var modalRef = await ModalService.CreateModalAsync<PackagingDialog, PackagingDialogOptions>(modalOptions, options);
modalRef.OnCancel = async () =>
{
await _table.ReloadAsync();
};
_packaging = false;
await InvokeAsync(StateHasChanged);
await Task.Yield();
}
private async Task RecalculateFileSizes()
{
MessageService.Info("Recalculating File Sizes...");
await Task.Yield();
await InvokeAsync(StateHasChanged);
var archives = _table.DataSource;
var results = new List<bool>();
foreach (var archive in archives)
{
var result = await ArchiveService.RecalculateFileSizeArchiveAsync(archive);
results.Add(result);
}
if (results.All(x => x))
MessageService.Success("File sizes recalculated!");
else
MessageService.Warning("File sizes recalculated but some failed!");
await Task.Yield();
await InvokeAsync(StateHasChanged);
await _table.ReloadAsync();
}
private async Task RecalculateFileSize(Archive archive)
{
MessageService.Info("Recalculating File Sizes...");
archive = await ArchiveService.AsNoTracking().GetAsync(archive.Id);
var result = await ArchiveService.RecalculateFileSizeArchiveAsync(archive);
await ArchiveUploaded(archive.Id);
if (result)
MessageService.Success("File sizes recalculated!");
else
MessageService.Error("Recalculating file sizes failed!");
}
private async Task CheckStreamingCompatibility(Archive archive)
{
_checking = true;
await InvokeAsync(StateHasChanged);
try
{
var report = await ArchiveService.InspectStreamingCompatibilityAsync(archive.Id);
if (report.IsStreamingSafe)
{
MessageService.Success("This archive is streaming-safe and can be installed by launchers.");
return;
}
var count = report.ProblemEntries.Count;
var result = await ConfirmService.Show(
$"This archive has {count} entr{(count == 1 ? "y" : "ies")} that launchers cannot extract reliably because they are stored uncompressed with a streaming data descriptor. Repack it into a streaming-safe layout? This runs in the background and may take a while for large archives.",
"Repack Archive?",
ConfirmButtons.YesNo,
ConfirmIcon.Warning);
if (result == ConfirmResult.Yes)
{
BackgroundJob.Enqueue<RepackArchiveBackgroundJob>(x => x.Execute(archive.Id));
MessageService.Info("Repack queued. The archive will be rewritten in the background.");
}
}
catch (Exception ex)
{
MessageService.Error("Could not inspect the archive.");
Logger.LogError(ex, "Could not inspect archive {ArchiveId} for streaming compatibility", archive.Id);
}
finally
{
_checking = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task ArchiveUploaded(Guid archiveId)
{
await _table.ReloadAsync();
}
private async Task UploadArchive()
{
await _uploader.Open();
}
private async Task BrowseArchive(Archive archive)
{
_browsing = true;
await InvokeAsync(StateHasChanged);
await Task.Yield();
var modalOptions = new ModalOptions()
{
Title = "Browse Archive",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
WrapClassName = "file-picker-dialog",
};
var modalRef = await ModalService.CreateModalAsync<ArchiveBrowserDialog, Guid, Guid>(modalOptions, archive.Id);
_browsing = false;
await InvokeAsync(StateHasChanged);
await Task.Yield();
}
private async Task Update(Archive archive)
{
try
{
await ArchiveService.UpdateAsync(archive);
await _table.ReloadAsync();
MessageService.Success("Archive updated!");
}
catch (Exception ex)
{
MessageService.Error("Archive could not be updated.");
Logger.LogError(ex, "Archive could not be updated.");
}
}
private async Task Delete(Archive archive)
{
try
{
await ArchiveService.DeleteAsync(archive);
await _table.ReloadAsync();
MessageService.Success("Archive deleted!");
}
catch (Exception ex)
{
MessageService.Error("Archive could not be deleted.");
Logger.LogError(ex, "Archive could not be deleted.");
}
}
}