move UI logic from FileService to FileManager, add progressive retry logic, update file attributes if uploading a new version of a file, clean up temporary artifacts on failure, improve upload efficiency

This commit is contained in:
Shaun Walker
2022-11-09 21:11:02 -05:00
parent 6182b96d16
commit 7489d9d186
8 changed files with 122 additions and 154 deletions

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Controls
@using System.Threading
@inherits ModuleControlBase
@inject IFolderService FolderService
@inject IFileService FileService
@ -53,7 +54,7 @@
}
</div>
<div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1)
{
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
@ -304,17 +305,17 @@
}
}
private async Task UploadFile()
private async Task UploadFiles()
{
_message = string.Empty;
var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid);
if (upload.Length > 0)
var uploads = await interop.GetFiles(_fileinputid);
if (uploads.Length > 0)
{
string restricted = "";
foreach (var file in upload)
foreach (var upload in uploads)
{
var extension = (file.LastIndexOf(".") != -1) ? file.Substring(file.LastIndexOf(".") + 1) : "";
var extension = (upload.LastIndexOf(".") != -1) ? upload.Substring(upload.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
@ -324,48 +325,68 @@
{
try
{
string result;
if (Folder == Constants.PackagesFolder)
// upload the files
var posturl = Utilities.TenantUrl(PageState.Alias, "/api/file/upload");
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
// uploading is asynchronous so we need to wait for the uploads to complete
// note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
bool success = false;
int attempts = 0;
while (attempts < 5 && !success)
{
result = await FileService.UploadFilesAsync(Folder, upload, _guid);
}
else
{
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
success = true;
List<File> files = await FileService.GetFilesAsync(folder);
if (files.Count > 0)
{
foreach (string upload in uploads)
{
if (!files.Exists(item => item.Name == upload))
{
success = false;
}
}
}
}
if (result == string.Empty)
// reset progress indicators
await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
if (success)
{
await logger.LogInformation("File Upload Succeeded {Files}", upload);
await logger.LogInformation("File Upload Succeeded {Files}", uploads);
if (ShowSuccess)
{
_message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success;
}
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
StateHasChanged();
}
else
{
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value>
</data>
<data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed</value>
<value>File Upload Failed Or Is Still In Progress</value>
</data>
<data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value>

View File

@ -2,27 +2,17 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class FileService : ServiceBase, IFileService
{
private readonly SiteState _siteState;
private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{
_siteState = siteState;
_jsRuntime = jsRuntime;
}
public FileService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("File");
@ -75,54 +65,6 @@ namespace Oqtane.Services
return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}");
}
public async Task<string> UploadFilesAsync(int folderId, string[] files, string id)
{
return await UploadFilesAsync(folderId.ToString(), files, id);
}
public async Task<string> UploadFilesAsync(string folder, string[] files, string id)
{
string result = "";
var interop = new Interop(_jsRuntime);
await interop.UploadFiles($"{Apiurl}/upload", folder, id, _siteState.AntiForgeryToken);
// uploading files is asynchronous so we need to wait for the upload to complete
bool success = false;
int attempts = 0;
while (attempts < 5 && success == false)
{
Thread.Sleep(2000); // wait 2 seconds
result = "";
List<File> fileList = await GetFilesAsync(folder);
if (fileList.Count > 0)
{
success = true;
foreach (string file in files)
{
if (!fileList.Exists(item => item.Name == file))
{
success = false;
result += file + ",";
}
}
}
attempts += 1;
}
await interop.SetElementAttribute(id + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(id + "ProgressBar", "style", "display: none;");
if (!success)
{
result = result.Substring(0, result.Length - 1);
}
return result;
}
public async Task<byte[]> DownloadFileAsync(int fileId)
{
return await GetByteArrayAsync($"{Apiurl}/download/{fileId}");

View File

@ -66,27 +66,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<File> UploadFileAsync(string url, int folderId, string name);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folderId">Target <see cref="Folder"/></param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(int folderId, string[] files, string fileUploadName);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folder">Target <see cref="Folder"/>
/// TODO: todoc verify exactly from where the folder path must start
/// </param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(string folder, string[] files, string fileUploadName);
/// <summary>
/// Get / download a file (the body).
/// </summary>