From 50dd44d1d00edacc8f4dfab9f7e6f1d53b4994ba Mon Sep 17 00:00:00 2001 From: Halil ibrahim Kalkan Date: Thu, 21 Feb 2019 10:51:47 +0300 Subject: [PATCH] Resolved #810: Blog Module's FilesController should support N-tier deployment. --- .../BloggingTestAppModule.cs | 7 ++ .../Volo/Blogging/Files/BloggingWebConsts.cs | 14 ++++ .../Volo/Blogging/Files/FileUploadInputDto.cs | 13 ++++ .../Blogging/Files/FileUploadOutputDto.cs | 7 ++ .../Volo/Blogging/Files/IFileAppService.cs | 10 +++ .../Volo.Blogging.Application.csproj | 1 + .../Volo/Blogging/CommonOperations.cs | 5 +- .../Volo/Blogging/Files/BlogFileOptions.cs | 13 ++++ .../Volo/Blogging/Files/FileAppService.cs | 65 +++++++++++++++++ .../Volo/Blogging/Files/FileUploadConsts.cs | 20 ++++++ .../Volo/Blogging/Files}/ImageFormatHelper.cs | 0 .../Areas/Blog/Controllers/FilesController.cs | 36 ++++++++-- .../Volo.Blogging.Web/BloggingWebConsts.cs | 30 -------- .../Volo.Blogging.Web/Hosting/FileService.cs | 72 ------------------- .../Hosting/FormFileExtensions.cs | 42 ++--------- .../Volo.Blogging.Web/Hosting/IFileService.cs | 16 ----- .../Volo.Blogging.Web.csproj | 1 - 17 files changed, 185 insertions(+), 167 deletions(-) create mode 100644 modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs create mode 100644 modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs rename modules/blogging/src/{Volo.Blogging.Web/Areas/Blog/Helpers => Volo.Blogging.Application/Volo/Blogging/Files}/ImageFormatHelper.cs (100%) delete mode 100644 modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs delete mode 100644 modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs delete mode 100644 modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs diff --git a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs index 6a7ca9ec6d..1945c6ac31 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs +++ b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs @@ -31,6 +31,7 @@ using Volo.Abp.Threading; using Volo.Abp.UI; using Volo.Abp.VirtualFileSystem; using Volo.Blogging; +using Volo.Blogging.Files; using Volo.BloggingTestApp.EntityFrameworkCore; using Volo.BloggingTestApp.MongoDb; @@ -109,6 +110,12 @@ namespace Volo.BloggingTestApp { options.DefaultThemeName = BasicTheme.Name; }); + + Configure(options => + { + options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files"); + options.FileUploadUrlRoot = "/files/"; + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs new file mode 100644 index 0000000000..ae4eb32dff --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs @@ -0,0 +1,14 @@ +using System; + +namespace Volo.Blogging +{ + public class BloggingWebConsts + { + public class FileUploading + { + public const int MaxFileSize = 5242880; //5MB + + public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f); + } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs new file mode 100644 index 0000000000..c0686835fd --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Volo.Blogging.Files +{ + public class FileUploadInputDto + { + [Required] + public byte[] Bytes { get; set; } + + [Required] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs new file mode 100644 index 0000000000..1c3b1a1a94 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs @@ -0,0 +1,7 @@ +namespace Volo.Blogging.Files +{ + public class FileUploadOutputDto + { + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs new file mode 100644 index 0000000000..6fed8bc62f --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Volo.Blogging.Files +{ + public interface IFileAppService : IApplicationService + { + Task UploadAsync(FileUploadInputDto input); + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj index a769735b31..95682a2a9e 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj +++ b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj @@ -10,6 +10,7 @@ + diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs index 855b504ae4..01e3fefd36 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.AspNetCore.Authorization.Infrastructure; namespace Volo.Blogging { diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs new file mode 100644 index 0000000000..a06bdafc8d --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs @@ -0,0 +1,13 @@ +namespace Volo.Blogging.Files +{ + /* TODO: + * - It is not to have different options for all different modules. We should find a more generic way. + * - Actually, it is not good to assume to save to a local folder. Instead, use file storage once implemented. + */ + public class BlogFileOptions + { + public string FileUploadLocalFolder { get; set; } + + public string FileUploadUrlRoot { get; set; } + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs new file mode 100644 index 0000000000..8f18608080 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Validation; +using Volo.Blogging.Areas.Blog.Helpers; + +namespace Volo.Blogging.Files +{ + public class FileAppService : ApplicationService, IFileAppService + { + public BlogFileOptions Options { get; } + + public FileAppService(IOptions options) + { + Options = options.Value; + } + + public virtual Task UploadAsync(FileUploadInputDto input) + { + if (input.Bytes.IsNullOrEmpty()) + { + ThrowValidationException("Bytes can not be null or empty!", "Bytes"); + } + + if (input.Bytes.Length > BloggingWebConsts.FileUploading.MaxFileSize) + { + throw new UserFriendlyException($"File exceeds the maximum upload size ({BloggingWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!"); + } + + if (!ImageFormatHelper.IsValidImage(input.Bytes, FileUploadConsts.AllowedImageUploadFormats)) + { + throw new UserFriendlyException("Not a valid image format!"); + } + + var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(input.Name)); + var filePath = Path.Combine(Options.FileUploadLocalFolder, uniqueFileName); + + File.WriteAllBytes(filePath, input.Bytes); //TODO: Previously was using WriteAllBytesAsync, but it's only in .netcore. + + return Task.FromResult(new FileUploadOutputDto + { + Url = Options.FileUploadUrlRoot.EnsureEndsWith('/') + uniqueFileName + }); + } + + private static void ThrowValidationException(string message, string memberName) + { + throw new AbpValidationException(message, + new List + { + new ValidationResult(message, new[] {memberName}) + }); + } + + protected virtual string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null) + { + return prefix + GuidGenerator.Create().ToString("N") + postfix + extension; + } + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs new file mode 100644 index 0000000000..28cb3491f3 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Drawing.Imaging; +using System.Linq; + +namespace Volo.Blogging.Files +{ + public class FileUploadConsts + { + public static readonly ICollection AllowedImageUploadFormats = new Collection + { + ImageFormat.Jpeg, + ImageFormat.Png, + ImageFormat.Gif, + ImageFormat.Bmp + }; + + public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString())); + } +} diff --git a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Helpers/ImageFormatHelper.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs similarity index 100% rename from modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Helpers/ImageFormatHelper.cs rename to modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs diff --git a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs b/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs index 1f8b932a83..7d3e12f3cd 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs +++ b/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs @@ -1,31 +1,55 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Blogging.Areas.Blog.Models; +using Volo.Blogging.Files; using Volo.Blogging.Hosting; namespace Volo.Blogging.Areas.Blog.Controllers { + //TODO: This may be moved to HttpApi project since it may be needed by a SPA too. [Area("Blog")] [Route("Blog/[controller]/[action]")] public class FilesController : AbpController { - private readonly IFileService _fileService; + private readonly IFileAppService _fileAppService; - public FilesController(IFileService fileService) + public FilesController(IFileAppService fileAppService) { - _fileService = fileService; + _fileAppService = fileAppService; } [HttpPost] public async Task UploadImage(IFormFile file) { - file.ValidateImage(out var fileBytes); + //TODO: localize exception messages - var fileUrl = await _fileService.SaveFileAsync(fileBytes, file.FileName); + if (file == null) + { + throw new UserFriendlyException("No file found!"); + } - return Json(new FileUploadResult(fileUrl)); + if (file.Length <= 0) + { + throw new UserFriendlyException("File is empty!"); + } + + if (!file.ContentType.Contains("image")) + { + throw new UserFriendlyException("Not a valid image!"); + } + + var output = await _fileAppService.UploadAsync( + new FileUploadInputDto + { + Bytes = file.AsBytes(), + Name = file.FileName + } + ); + + return Json(new FileUploadResult(output.Url)); } } } \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs deleted file mode 100644 index 491ea9d67b..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Drawing.Imaging; -using System.Linq; - -namespace Volo.Blogging -{ - public class BloggingWebConsts - { - public class FileUploading - { - public const string DefaultFileUploadFolderName = "files"; - - public static readonly ICollection AllowedImageUploadFormats = new Collection - { - ImageFormat.Jpeg, - ImageFormat.Png, - ImageFormat.Gif, - ImageFormat.Bmp - }; - - public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString())); - - public const int MaxFileSize = 5242880; //5MB - - public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f); - } - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs deleted file mode 100644 index accb424215..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Volo.Abp; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Guids; - -namespace Volo.Blogging.Hosting -{ - public class FileService : IFileService, ITransientDependency - { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IGuidGenerator _guidGenerator; - - public FileService(IHostingEnvironment hostingEnvironment, IGuidGenerator guidGenerator) - { - _hostingEnvironment = hostingEnvironment; - _guidGenerator = guidGenerator; - } - - public string FileUploadDirectory - { - get - { - var uploadDirectory = Path.Combine(_hostingEnvironment.WebRootPath, BloggingWebConsts.FileUploading.DefaultFileUploadFolderName); - if (!Directory.Exists(uploadDirectory)) - { - Directory.CreateDirectory(uploadDirectory); - } - - return uploadDirectory; - } - } - - public string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null) - { - return prefix + _guidGenerator.Create().ToString("N") + postfix + extension; - } - - public async Task SaveFormFileAndGetUrlAsync(IFormFile file) - { - var uniqueFileName = await SaveFileInternalAsync(file.FileName, file.AsBytes()); - return GetFileUrl(uniqueFileName); - } - - public async Task SaveFileAsync(byte[] fileBytes, string originalFileName) - { - if (fileBytes == null || fileBytes.Length == 0) - { - throw new UserFriendlyException("File is empty!"); - } - - var uniqueFileName = await SaveFileInternalAsync(originalFileName, fileBytes); - return GetFileUrl(uniqueFileName); - } - - private static string GetFileUrl(string uniqueFileName) - { - return "/" + BloggingWebConsts.FileUploading.DefaultFileUploadFolderName + "/" + uniqueFileName; - } - - private async Task SaveFileInternalAsync(string originalFileName, byte[] fileBytes) - { - var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(originalFileName)); - var filePath = Path.Combine(FileUploadDirectory, uniqueFileName); - File.WriteAllBytes(filePath, fileBytes); //TODO: Previously was using WriteAllBytesAsync, but it's only in .netcore. - return uniqueFileName; - } - - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs index 669958d086..aab0832bbf 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs +++ b/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs @@ -2,56 +2,22 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using Volo.Abp; -using Volo.Blogging.Areas.Blog.Helpers; namespace Volo.Blogging.Hosting { public static class FormFileExtensions { - public static byte[] AsBytes(this IFormFile file) + public static byte[] AsBytes(this IFormFile file) //TODO: Move to the framework (rename to GetBytes) { - byte[] fileBytes; using (var stream = file.OpenReadStream()) { - fileBytes = stream.GetAllBytes(); + return stream.GetAllBytes(); } - - return fileBytes; } - public static void ValidateImage([CanBeNull] this IFormFile file, out byte[] fileBytes) + public static void ValidateImage([CanBeNull] this IFormFile file) { - fileBytes = null; - - if (file == null) - { - throw new UserFriendlyException("No file found!"); - } - - if (file.Length <= 0) - { - throw new UserFriendlyException("File is empty!"); - } - - if (!file.ContentType.Contains("image")) - { - throw new UserFriendlyException("Not a valid image!"); - } - - using (var stream = file.OpenReadStream()) - { - fileBytes = stream.GetAllBytes(); - } - - if (!ImageFormatHelper.IsValidImage(fileBytes, BloggingWebConsts.FileUploading.AllowedImageUploadFormats)) - { - throw new UserFriendlyException("Not a valid image format!"); - } - - if (file.Length > BloggingWebConsts.FileUploading.MaxFileSize) - { - throw new UserFriendlyException($"File exceeds the maximum upload size ({BloggingWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!"); - } + } } } \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs deleted file mode 100644 index 66516135bd..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Volo.Blogging.Hosting -{ - public interface IFileService - { - string FileUploadDirectory { get; } - - string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null); - - Task SaveFormFileAndGetUrlAsync(IFormFile file); - - Task SaveFileAsync(byte[] fileBytes, string originalFileName); - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj index 6000771e97..8feb2057d7 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj @@ -19,7 +19,6 @@ -