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.
302 lines
10 KiB
Text
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.");
|
|
}
|
|
}
|
|
}
|