Fix uploader not fulling completing with long uploads. Added smoother progress bar updates. Added upload rate text. Show selected file name for uploader.
parent
a6103a1f82
commit
b6b6eb1e76
|
@ -63,24 +63,40 @@
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<InputFile id="FileInput" OnChange="FileSelected" hidden />
|
<Space Direction="DirectionVHType.Horizontal">
|
||||||
|
<SpaceItem>
|
||||||
<Upload Name="files" FileList="FileList">
|
<InputFile id="FileInput" OnChange="FileSelected" hidden />
|
||||||
<label class="ant-btn" for="FileInput">
|
<Upload Name="files" FileList="FileList">
|
||||||
<Icon Type="upload" />
|
<label class="ant-btn" for="FileInput">
|
||||||
Select Archive
|
<Icon Type="upload" />
|
||||||
</label>
|
@if (File == null)
|
||||||
</Upload>
|
{
|
||||||
|
<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>
|
||||||
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Progress Percent="Progress" />
|
<Progress Percent="Progress" Status="@CurrentProgressStatus" Class="uploader-progress" />
|
||||||
<Text>@ByteSizeLib.ByteSize.FromBytes(Speed)/s</Text>
|
<Text Class="uploader-progress-rate"></Text>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public Game Game { get; set; }
|
[Parameter] public Game Game { get; set; }
|
||||||
|
|
||||||
Archive Archive;
|
Archive Archive;
|
||||||
|
@ -98,11 +114,26 @@
|
||||||
|
|
||||||
int Progress = 0;
|
int Progress = 0;
|
||||||
bool Uploading = false;
|
bool Uploading = false;
|
||||||
|
bool Finished = false;
|
||||||
double Speed = 0;
|
double Speed = 0;
|
||||||
|
|
||||||
Stopwatch Watch;
|
Stopwatch Watch;
|
||||||
long WatchBytesTransferred = 0;
|
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()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (Game.Archives == null)
|
if (Game.Archives == null)
|
||||||
|
@ -143,7 +174,7 @@
|
||||||
|
|
||||||
await MessageService.Success("Archive deleted!");
|
await MessageService.Success("Archive deleted!");
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await MessageService.Error("Archive could not be deleted.");
|
await MessageService.Error("Archive could not be deleted.");
|
||||||
}
|
}
|
||||||
|
@ -169,37 +200,44 @@
|
||||||
{
|
{
|
||||||
Uploading = true;
|
Uploading = true;
|
||||||
|
|
||||||
var response = (await JS.InvokeAsync<string>("Uploader.Upload", "FileInput"));
|
var dotNetReference = DotNetObjectReference.Create(this);
|
||||||
|
|
||||||
if (Guid.TryParse(response, out var objectKey))
|
await JS.InvokeVoidAsync("Uploader.Upload", "FileInput", dotNetReference);
|
||||||
{
|
|
||||||
Uploading = false;
|
|
||||||
await UploadComplete(objectKey);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await MessageService.Error("Archive failed to upload!");
|
|
||||||
}
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UploadComplete(Guid objectKey)
|
[JSInvokable]
|
||||||
|
public async void OnUploadComplete(string data)
|
||||||
{
|
{
|
||||||
Archive.ObjectKey = objectKey.ToString();
|
if (Guid.TryParse(data, out var objectKey))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,13 @@
|
||||||
|
|
||||||
.ant-card .ant-form > .ant-form-item:last-child {
|
.ant-card .ant-form > .ant-form-item:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-progress .ant-progress-outer {
|
||||||
|
padding-right: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-progress .ant-progress-bg {
|
||||||
|
transition: none;
|
||||||
}
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
import Chunk from './Chunk';
|
import Chunk from './Chunk';
|
||||||
import UploadInitResponse from './UploadInitResponse';
|
import UploadInitResponse from './UploadInitResponse';
|
||||||
import axios from 'axios';
|
import axios, { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
export default class Uploader {
|
export default class Uploader {
|
||||||
FileInput: HTMLInputElement | undefined;
|
FileInput: HTMLInputElement | undefined;
|
||||||
UploadButton: HTMLButtonElement | undefined;
|
UploadButton: HTMLButtonElement | undefined;
|
||||||
ObjectKeyInput: HTMLInputElement | undefined;
|
ObjectKeyInput: HTMLInputElement | undefined;
|
||||||
|
ProgressBar: HTMLElement | undefined;
|
||||||
|
ProgressText: HTMLElement | undefined;
|
||||||
|
ProgressRate: HTMLElement | undefined;
|
||||||
|
|
||||||
File: File | undefined;
|
File: File | undefined;
|
||||||
|
|
||||||
|
@ -26,14 +29,13 @@ export default class Uploader {
|
||||||
this.ObjectKeyInput = document.getElementById(objectKeyInputId) as HTMLInputElement;
|
this.ObjectKeyInput = document.getElementById(objectKeyInputId) as HTMLInputElement;
|
||||||
|
|
||||||
this.Chunks = [];
|
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.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.Chunks = [];
|
||||||
|
|
||||||
this.File = this.FileInput.files.item(0);
|
this.File = this.FileInput.files.item(0);
|
||||||
|
@ -52,49 +54,14 @@ export default class Uploader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
this.OnError();
|
if (this.OnError != null)
|
||||||
}
|
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();
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
this.Key = null;
|
||||||
console.error(`Could not init upload: ${ex}`);
|
console.error(`Could not init upload: ${ex}`);
|
||||||
|
} finally {
|
||||||
|
dotNetObject.invokeMethodAsync('OnUploadComplete', this.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,16 +81,16 @@ export default class Uploader {
|
||||||
method: "post",
|
method: "post",
|
||||||
url: this.ChunkRoute,
|
url: this.ChunkRoute,
|
||||||
data: formData,
|
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) {
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
|
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;
|
OnStart: () => void;
|
||||||
OnComplete: (id: string, key: string) => void;
|
OnComplete: (id: string, key: string) => void;
|
||||||
OnProgress: (percent: number) => void;
|
OnProgress: (percent: number) => void;
|
||||||
|
|
|
@ -62,13 +62,13 @@ class Uploader {
|
||||||
this.UploadButton = document.getElementById(uploadButtonId);
|
this.UploadButton = document.getElementById(uploadButtonId);
|
||||||
this.ObjectKeyInput = document.getElementById(objectKeyInputId);
|
this.ObjectKeyInput = document.getElementById(objectKeyInputId);
|
||||||
this.Chunks = [];
|
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* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
this.FileInput = document.getElementById(fileInputId);
|
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.Chunks = [];
|
||||||
this.File = this.FileInput.files.item(0);
|
this.File = this.FileInput.files.item(0);
|
||||||
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
|
this.TotalChunks = Math.ceil(this.File.size / this.MaxChunkSize);
|
||||||
|
@ -82,43 +82,17 @@ class Uploader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
this.OnError();
|
if (this.OnError != null)
|
||||||
}
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
this.Key = null;
|
||||||
console.error(`Could not init upload: ${ex}`);
|
console.error(`Could not init upload: ${ex}`);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
dotNetObject.invokeMethodAsync('OnUploadComplete', this.Key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
UploadChunk(chunk) {
|
UploadChunk(chunk) {
|
||||||
|
@ -135,16 +109,22 @@ class Uploader {
|
||||||
method: "post",
|
method: "post",
|
||||||
url: this.ChunkRoute,
|
url: this.ChunkRoute,
|
||||||
data: formData,
|
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) {
|
catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
|
throw `Error uploading chunk ${chunk.Index}/${this.TotalChunks}`;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
console.log("Updating progress bar");
|
||||||
var percent = Math.ceil((chunk.Index / this.TotalChunks) * 100);
|
var percent = Math.ceil((chunk.Index / this.TotalChunks) * 100);
|
||||||
let progress = document.querySelector('.ant-progress-bg');
|
this.ProgressBar.style.width = percent + '%';
|
||||||
progress.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));
|
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
Loading…
Reference in New Issue