Fix uploader not fulling completing with long uploads. Added smoother progress bar updates. Added upload rate text. Show selected file name for uploader.

dhcp-server
Pat Hartl 2023-08-28 01:25:03 -05:00
parent a6103a1f82
commit b6b6eb1e76
5 changed files with 177 additions and 129 deletions

View File

@ -63,24 +63,40 @@
</FormItem>
<FormItem>
<InputFile id="FileInput" OnChange="FileSelected" hidden />
<Upload Name="files" FileList="FileList">
<label class="ant-btn" for="FileInput">
<Icon Type="upload" />
Select Archive
</label>
</Upload>
<Space Direction="DirectionVHType.Horizontal">
<SpaceItem>
<InputFile id="FileInput" OnChange="FileSelected" hidden />
<Upload Name="files" FileList="FileList">
<label class="ant-btn" for="FileInput">
<Icon Type="upload" />
@if (File == null)
{
<Text>Select File</Text>
}
else
{
<Text>Change File</Text>
}
</label>
</Upload>
</SpaceItem>
<SpaceItem>
@if (File != null)
{
<Text>@File.Name (@ByteSizeLib.ByteSize.FromBytes(File.Size))</Text>
}
</SpaceItem>
</Space>
</FormItem>
<FormItem>
<Progress Percent="Progress" />
<Text>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</Text>
<Progress Percent="Progress" Status="@CurrentProgressStatus" Class="uploader-progress" />
<Text Class="uploader-progress-rate"></Text>
</FormItem>
</Form>
</Modal>
@code {
@code {
[Parameter] public Game Game { get; set; }
Archive Archive;
@ -98,11 +114,26 @@
int Progress = 0;
bool Uploading = false;
bool Finished = false;
double Speed = 0;
Stopwatch Watch;
long WatchBytesTransferred = 0;
string Filename;
ProgressStatus CurrentProgressStatus {
get
{
if (Finished)
return ProgressStatus.Success;
else if (Uploading)
return ProgressStatus.Active;
else
return ProgressStatus.Normal;
}
}
protected override async Task OnInitializedAsync()
{
if (Game.Archives == null)
@ -143,7 +174,7 @@
await MessageService.Success("Archive deleted!");
}
catch
catch (Exception ex)
{
await MessageService.Error("Archive could not be deleted.");
}
@ -169,37 +200,44 @@
{
Uploading = true;
var response = (await JS.InvokeAsync<string>("Uploader.Upload", "FileInput"));
var dotNetReference = DotNetObjectReference.Create(this);
if (Guid.TryParse(response, out var objectKey))
{
Uploading = false;
await UploadComplete(objectKey);
}
else
{
await MessageService.Error("Archive failed to upload!");
}
await JS.InvokeVoidAsync("Uploader.Upload", "FileInput", dotNetReference);
await InvokeAsync(StateHasChanged);
}
private async Task UploadComplete(Guid objectKey)
[JSInvokable]
public async void OnUploadComplete(string data)
{
Archive.ObjectKey = objectKey.ToString();
Archive.CompressedSize = File.Size;
var originalArchive = Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault();
Archive = await ArchiveService.Add(Archive);
ModalVisible = false;
await MessageService.Success("Archive uploaded!");
if (originalArchive != null)
if (Guid.TryParse(data, out var objectKey))
{
BackgroundJob.Enqueue<PatchArchiveBackgroundJob>(x => x.Execute(originalArchive.Id, Archive.Id));
Uploading = false;
Finished = true;
Archive.ObjectKey = objectKey.ToString();
Archive.CompressedSize = File.Size;
var originalArchive = Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault();
Archive = await ArchiveService.Add(Archive);
ModalVisible = false;
await InvokeAsync(StateHasChanged);
await MessageService.Success("Archive uploaded!");
if (originalArchive != null)
BackgroundJob.Enqueue<PatchArchiveBackgroundJob>(x => x.Execute(originalArchive.Id, Archive.Id));
}
else
{
ModalVisible = false;
await InvokeAsync(StateHasChanged);
await MessageService.Error("Archive failed to upload!");
}
}
}

View File

@ -8,4 +8,13 @@
.ant-card .ant-form > .ant-form-item:last-child {
margin-bottom: 0;
}
.uploader-progress .ant-progress-outer {
padding-right: 0;
margin-right: 0;
}
.uploader-progress .ant-progress-bg {
transition: none;
}

View File

@ -1,11 +1,14 @@
import Chunk from './Chunk';
import UploadInitResponse from './UploadInitResponse';
import axios from 'axios';
import axios, { AxiosProgressEvent } from 'axios';
export default class Uploader {
FileInput: HTMLInputElement | undefined;
UploadButton: HTMLButtonElement | undefined;
ObjectKeyInput: HTMLInputElement | undefined;
ProgressBar: HTMLElement | undefined;
ProgressText: HTMLElement | undefined;
ProgressRate: HTMLElement | undefined;
File: File | undefined;
@ -26,14 +29,13 @@ export default class Uploader {
this.ObjectKeyInput = document.getElementById(objectKeyInputId) as HTMLInputElement;
this.Chunks = [];
this.UploadButton.onclick = async (e) => {
await this.OnUploadButtonClicked(e);
}
}
async Upload(fileInputId: string) {
async Upload(fileInputId: string, dotNetObject: any) {
this.FileInput = document.getElementById(fileInputId) as HTMLInputElement;
this.ProgressBar = document.querySelector('.uploader-progress .ant-progress-bg');
this.ProgressText = document.querySelector('.uploader-progress .ant-progress-text');
this.ProgressRate = document.querySelector('.uploader-progress-rate');
this.Chunks = [];
this.File = this.FileInput.files.item(0);
@ -52,49 +54,14 @@ export default class Uploader {
}
}
catch (ex) {
this.OnError();
}
return this.Key;
} catch (ex) {
console.error(`Could not init upload: ${ex}`);
return null;
}
}
async OnUploadButtonClicked(e: MouseEvent) {
e.preventDefault();
this.OnStart();
this.File = this.FileInput.files.item(0);
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
try {
var resp = await axios.post<UploadInitResponse>(this.InitRoute);
this.Key = resp.data.key;
this.GetChunks();
try {
for (let chunk of this.Chunks) {
await this.UploadChunk(chunk);
}
this.ObjectKeyInput.value = this.Key;
var event = document.createEvent('HTMLEvents');
event.initEvent('change', false, true);
this.ObjectKeyInput.dispatchEvent(event);
this.OnComplete(this.Id, this.Key);
}
catch (ex) {
this.OnError();
if (this.OnError != null)
this.OnError();
}
} catch (ex) {
this.Key = null;
console.error(`Could not init upload: ${ex}`);
} finally {
dotNetObject.invokeMethodAsync('OnUploadComplete', this.Key);
}
}
@ -114,16 +81,16 @@ export default class Uploader {
method: "post",
url: this.ChunkRoute,
data: formData,
headers: { "Content-Type": "multipart/form-data" }
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
console.log(progressEvent);
this.UpdateProgressBar(chunk.Index, progressEvent);
}
});
} catch (ex) {
console.error(ex);
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
} finally {
var percent = Math.ceil((chunk.Index / this.TotalChunks) * 100);
let progress: HTMLElement = document.querySelector('.ant-progress-bg');
progress.style.width = percent + '%';
}
}
@ -139,6 +106,37 @@ export default class Uploader {
}
}
UpdateProgressBar(chunkIndex: number, progressEvent: AxiosProgressEvent) {
var percent = ((1 / this.TotalChunks) * progressEvent.progress) + ((chunkIndex - 1) / this.TotalChunks);
this.ProgressBar.style.width = (percent * 100) + '%';
this.ProgressText.innerText = Math.ceil(percent * 100) + '%';
if (progressEvent.rate > 0)
this.ProgressRate.innerText = this.GetHumanFileSize(progressEvent.rate, false, 1) + '/s';
}
GetHumanFileSize(bytes: number, si: boolean, dp: number) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
OnStart: () => void;
OnComplete: (id: string, key: string) => void;
OnProgress: (percent: number) => void;

View File

@ -62,13 +62,13 @@ class Uploader {
this.UploadButton = document.getElementById(uploadButtonId);
this.ObjectKeyInput = document.getElementById(objectKeyInputId);
this.Chunks = [];
this.UploadButton.onclick = (e) => __awaiter(this, void 0, void 0, function* () {
yield this.OnUploadButtonClicked(e);
});
}
Upload(fileInputId) {
Upload(fileInputId, dotNetObject) {
return __awaiter(this, void 0, void 0, function* () {
this.FileInput = document.getElementById(fileInputId);
this.ProgressBar = document.querySelector('.uploader-progress .ant-progress-bg');
this.ProgressText = document.querySelector('.uploader-progress .ant-progress-text');
this.ProgressRate = document.querySelector('.uploader-progress-rate');
this.Chunks = [];
this.File = this.FileInput.files.item(0);
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
@ -82,43 +82,17 @@ class Uploader {
}
}
catch (ex) {
this.OnError();
}
return this.Key;
}
catch (ex) {
console.error(`Could not init upload: ${ex}`);
return null;
}
});
}
OnUploadButtonClicked(e) {
return __awaiter(this, void 0, void 0, function* () {
e.preventDefault();
this.OnStart();
this.File = this.FileInput.files.item(0);
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
try {
var resp = yield axios__WEBPACK_IMPORTED_MODULE_1__["default"].post(this.InitRoute);
this.Key = resp.data.key;
this.GetChunks();
try {
for (let chunk of this.Chunks) {
yield this.UploadChunk(chunk);
}
this.ObjectKeyInput.value = this.Key;
var event = document.createEvent('HTMLEvents');
event.initEvent('change', false, true);
this.ObjectKeyInput.dispatchEvent(event);
this.OnComplete(this.Id, this.Key);
}
catch (ex) {
this.OnError();
if (this.OnError != null)
this.OnError();
}
}
catch (ex) {
this.Key = null;
console.error(`Could not init upload: ${ex}`);
}
finally {
dotNetObject.invokeMethodAsync('OnUploadComplete', this.Key);
}
});
}
UploadChunk(chunk) {
@ -135,16 +109,22 @@ class Uploader {
method: "post",
url: this.ChunkRoute,
data: formData,
headers: { "Content-Type": "multipart/form-data" }
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (progressEvent) => {
console.log(progressEvent);
this.UpdateProgressBar(chunk.Index, progressEvent);
}
});
}
catch (ex) {
console.error(ex);
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
}
finally {
console.log("Updating progress bar");
var percent = Math.ceil((chunk.Index / this.TotalChunks) * 100);
let progress = document.querySelector('.ant-progress-bg');
progress.style.width = percent + '%';
this.ProgressBar.style.width = percent + '%';
this.ProgressText.innerText = percent + '%';
}
});
}
@ -157,6 +137,29 @@ class Uploader {
this.Chunks.push(new _Chunk__WEBPACK_IMPORTED_MODULE_0__["default"](start, end, currentChunk));
}
}
UpdateProgressBar(chunkIndex, progressEvent) {
var percent = ((1 / this.TotalChunks) * progressEvent.progress) + ((chunkIndex - 1) / this.TotalChunks);
this.ProgressBar.style.width = (percent * 100) + '%';
this.ProgressText.innerText = Math.ceil(percent * 100) + '%';
if (progressEvent.rate > 0)
this.ProgressRate.innerText = this.GetHumanFileSize(progressEvent.rate, false, 1) + '/s';
}
GetHumanFileSize(bytes, si, dp) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = Math.pow(10, dp);
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
}

File diff suppressed because one or more lines are too long