LANCommander/LANCommander.Server/UI/Components/ArchiveUploader.razor
2026-05-28 22:26:24 -05:00

256 lines
9.2 KiB
Text

@using Hangfire;
@using LANCommander.SDK.Enums
@using LANCommander.Server.Jobs.Background;
@using LANCommander.UI.Services
@using Microsoft.Extensions.Options
@inject ArchiveService ArchiveService
@inject IOptions<Settings.Settings> Settings
@inject IMessageService MessageService
@inject ConfirmService ConfirmService
@inject IJSRuntime JS
@inject ILogger<ArchiveUploader> Logger
@{
RenderFragment Footer =
@<Template>
<FilePickerButton EntrySelectable="@(entry => !String.IsNullOrWhiteSpace(entry.Name) && entry.Name.ToLower().EndsWith(".zip"))"
OnSelected="OnLocalFileSelected"
Root="@RootPath"
Disabled="@(Status != "" || String.IsNullOrWhiteSpace(Archive.Version))">
Use Local File
</FilePickerButton>
<Button OnClick="Start" Disabled="@(File == null || Status != "" || String.IsNullOrWhiteSpace(Archive.Version))" Type="@ButtonType.Primary">Upload</Button>
<Button OnClick="Cancel">Cancel</Button>
</Template>;
}
<Modal Visible="@Visible" Title="Upload Archive" OnOk="Start" OnCancel="Cancel" Footer="@Footer" Closable="true" MaskClosable="false">
<Form Model="@Archive" Layout="@FormLayout.Vertical">
<FormItem Label="Version">
<Input @bind-Value="@context.Version" />
</FormItem>
<FormItem Label="Changelog">
<TextArea @bind-Value="@context.Changelog" MaxLength=500 ShowCount />
</FormItem>
<FormItem Label="Path">
<StorageLocationSelector Type="StorageLocationType.Archive" @bind-Value="@context.StorageLocation" />
</FormItem>
<FormItem>
<ChunkUploader @ref="ChunkUploader" Accept=".zip" @bind-File="File" @bind-Status="Status" @bind-StorageLocationId="@(context.StorageLocation.Id)">
<Text>
<p>Drag and Drop</p>
<p>or</p>
<p>
<Button Type="@ButtonType.Primary" Style="margin-top: 8px;">Browse</Button>
</p>
</Text>
<Hint>Only ZIP files are supported as game archives</Hint>
</ChunkUploader>
</FormItem>
</Form>
</Modal>
@code {
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public Guid ToolId { get; set; }
[Parameter] public EventCallback<Guid> OnArchiveUploaded { get; set; }
Archive Archive;
string RootPath = Path.GetPathRoot(Directory.GetCurrentDirectory());
IBrowserFile File;
ChunkUploader ChunkUploader;
Guid StorageLocationId;
StorageLocation StorageLocation;
bool Visible = false;
bool Uploading = false;
string Filename;
string Status = "";
private async Task Start()
{
Uploading = true;
// Capture values NOW (at the moment user clicks Upload)
// so the callback has correct data regardless of component lifecycle.
var gameId = Archive.GameId;
var redistributableId = Archive.RedistributableId;
var toolId = Archive.ToolId;
var version = Archive.Version;
var changelog = Archive.Changelog;
await ChunkUploader.Start(async (string objectKeyStr) =>
{
try
{
if (!Guid.TryParse(objectKeyStr, out var objectKey))
{
Logger.LogError("Archive failed to upload - invalid object key");
return;
}
var uploadedArchive = await ArchiveService.FirstOrDefaultAsync(a => a.ObjectKey == objectKey.ToString());
if (uploadedArchive == null)
{
Logger.LogError("Archive record not found for object key {ObjectKey}", objectKeyStr);
return;
}
uploadedArchive.GameId = gameId;
uploadedArchive.RedistributableId = redistributableId;
uploadedArchive.ToolId = toolId;
uploadedArchive.Version = version;
uploadedArchive.Changelog = changelog;
uploadedArchive.CompressedSize = await ArchiveService.GetCompressedSizeAsync(uploadedArchive);
uploadedArchive.UncompressedSize = await ArchiveService.GetUncompressedSizeAsync(uploadedArchive);
await ArchiveService.UpdateAsync(uploadedArchive);
if (Settings.Value.Server.Archives.EnablePatching)
{
Archive? lastArchive = null;
if (gameId != Guid.Empty)
lastArchive = await ArchiveService.GetLatestArchiveAsync(a => a.Id != uploadedArchive.Id && a.GameId == gameId);
else if (redistributableId != Guid.Empty)
lastArchive = await ArchiveService.GetLatestArchiveAsync(a => a.Id != uploadedArchive.Id && a.RedistributableId == redistributableId);
else if (toolId != Guid.Empty)
lastArchive = await ArchiveService.GetLatestArchiveAsync(a => a.Id != uploadedArchive.Id && a.ToolId == toolId);
if (lastArchive != null)
BackgroundJob.Enqueue<PatchArchiveBackgroundJob>(x => x.Execute(lastArchive.Id, uploadedArchive.Id));
}
if (OnArchiveUploaded.HasDelegate)
await OnArchiveUploaded.InvokeAsync(uploadedArchive.Id);
Uploading = false;
Visible = false;
try { await InvokeAsync(StateHasChanged); } catch { }
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to process uploaded archive");
}
});
}
private async Task Clear()
{
if (ChunkUploader != null)
await ChunkUploader.Clear();
}
private async Task Cancel()
{
if (Uploading && ChunkUploader?.IsUploading == true)
{
var result = await ConfirmService.Show(
"The upload will continue in the background. You can track its progress in the sidebar.",
"Continue in Background?",
ConfirmButtons.OKCancel,
ConfirmIcon.Info);
if (result == ConfirmResult.OK)
{
Visible = false;
await InvokeAsync(StateHasChanged);
}
}
else
{
if (ChunkUploader != null)
await ChunkUploader.Clear();
Visible = false;
}
}
public async Task Open(Guid? archiveId = null)
{
if (archiveId.HasValue && archiveId != Guid.Empty)
{
Archive = await ArchiveService.GetAsync(archiveId.Value);
}
else
{
Archive = new Archive();
if (GameId != Guid.Empty)
Archive.GameId = GameId;
else if (RedistributableId != Guid.Empty)
Archive.RedistributableId = RedistributableId;
else if (ToolId != Guid.Empty)
Archive.ToolId = ToolId;
}
Uploading = false;
Visible = true;
await InvokeAsync(StateHasChanged);
}
public async Task OnUploadError(string message)
{
Uploading = false;
Visible = false;
await InvokeAsync(StateHasChanged);
MessageService.Error("Archive failed to upload!");
Logger.LogError($"Archive failed to upload: {message}");
}
public async Task OnLocalFileSelected(string path)
{
try
{
var objectKey = await ArchiveService.CopyFromLocalFileAsync(path, Archive.StorageLocation.Id);
// For local files, run the completion logic directly.
var gameId = Archive.GameId;
var redistributableId = Archive.RedistributableId;
var toolId = Archive.ToolId;
var version = Archive.Version;
var changelog = Archive.Changelog;
var uploadedArchive = await ArchiveService.FirstOrDefaultAsync(a => a.ObjectKey == objectKey.ToString());
if (uploadedArchive != null)
{
uploadedArchive.GameId = gameId;
uploadedArchive.RedistributableId = redistributableId;
uploadedArchive.ToolId = toolId;
uploadedArchive.Version = version;
uploadedArchive.Changelog = changelog;
uploadedArchive.CompressedSize = await ArchiveService.GetCompressedSizeAsync(uploadedArchive);
uploadedArchive.UncompressedSize = await ArchiveService.GetUncompressedSizeAsync(uploadedArchive);
await ArchiveService.UpdateAsync(uploadedArchive);
if (OnArchiveUploaded.HasDelegate)
await OnArchiveUploaded.InvokeAsync(uploadedArchive.Id);
}
Visible = false;
await InvokeAsync(StateHasChanged);
MessageService.Success("Archive uploaded!");
}
catch (Exception ex)
{
MessageService.Error("An unknown error occurred while trying to use a local file");
Logger.LogError(ex, "An unknown error occurred while trying to use a local file");
}
}
}