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:
		| @ -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; | ||||
| 				} | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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}"); | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Shaun Walker
					Shaun Walker