diff --git a/PARR.API/Contracts/V1/ApiRoutes.cs b/PARR.API/Contracts/V1/ApiRoutes.cs index 5c0482dc..90ec6db7 100644 --- a/PARR.API/Contracts/V1/ApiRoutes.cs +++ b/PARR.API/Contracts/V1/ApiRoutes.cs @@ -41,6 +41,12 @@ public const string getParam = "{id}"; } + public static class EsppData + { + public const string UploadTemplates = Base + "/espp-data/templates"; + + } + public static class Test { public const string GetMyIp = Base + "/tests/my-ip"; diff --git a/PARR.API/Controllers/V1/EsppDataController.cs b/PARR.API/Controllers/V1/EsppDataController.cs new file mode 100644 index 00000000..81fb4597 --- /dev/null +++ b/PARR.API/Controllers/V1/EsppDataController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using PARR.API.Contracts.V1; +using PARR.API.Controllers.V1.Base; +using PARR.API.Services.Interfaces; + +namespace PARR.API.Controllers.V1 +{ + public class EsppDataController : BaseApiController + { + private readonly IFileService fileService; + + public EsppDataController(ILogger logger, IFileService fileService) + { + Logger = logger; + this.fileService = fileService; + } + + public ILogger Logger { get; } + + /// + /// Загрузить файл списка шаблонов полученных из ЕСПП в формате csv + /// + /// + [HttpPost(ApiRoutes.EsppData.UploadTemplates)] + public IActionResult UploadTemplates([BindRequired] IFormFile file) + { + return Ok(); + } + } +} diff --git a/PARR.API/Domain/UploadResult.cs b/PARR.API/Domain/UploadResult.cs new file mode 100644 index 00000000..13d66801 --- /dev/null +++ b/PARR.API/Domain/UploadResult.cs @@ -0,0 +1,8 @@ +namespace PARR.API.Domain +{ + public class UploadResult + { + public required string FileName { get; set; } + public required string Path { get; set; } + } +} diff --git a/PARR.API/Installers/ApiServicesInstaller.cs b/PARR.API/Installers/ApiServicesInstaller.cs index 86ef8a54..28f21d57 100644 --- a/PARR.API/Installers/ApiServicesInstaller.cs +++ b/PARR.API/Installers/ApiServicesInstaller.cs @@ -20,7 +20,7 @@ namespace PARR.API.Installers }); services.AddTransient(); - //services.AddTransient(); + services.AddTransient(); } diff --git a/PARR.API/Installers/SettingsInstaller.cs b/PARR.API/Installers/SettingsInstaller.cs index 29117a34..0a878fa6 100644 --- a/PARR.API/Installers/SettingsInstaller.cs +++ b/PARR.API/Installers/SettingsInstaller.cs @@ -1,4 +1,6 @@ -namespace PARR.API.Installers +using PARR.API.Settings; + +namespace PARR.API.Installers { /// /// Биндинги из конфига appsettings @@ -7,9 +9,9 @@ { public static void InstallSettings(this IServiceCollection services, IConfiguration configuration) { - //var storageSettings = new StorageSettings(); - //configuration.GetSection(nameof(StorageSettings)).Bind(storageSettings); - //services.AddSingleton(storageSettings); + var storageSettings = new StorageSettings(); + configuration.GetSection(nameof(StorageSettings)).Bind(storageSettings); + services.AddSingleton(storageSettings); //TODO: add other } diff --git a/PARR.API/Services/Implementations/FileService.cs b/PARR.API/Services/Implementations/FileService.cs new file mode 100644 index 00000000..c8c74a8a --- /dev/null +++ b/PARR.API/Services/Implementations/FileService.cs @@ -0,0 +1,101 @@ +using PARR.API.Domain; +using PARR.API.Services.Interfaces; +using PARR.API.Settings; + +namespace PARR.API.Services.Implementations +{ + public class FileService : IFileService + { + private readonly StorageSettings storageSettings; + private readonly ILogger logger; + + public FileService(StorageSettings storageSettings, ILogger logger) + { + this.storageSettings = storageSettings; + this.logger = logger; + } + + public bool IsExtensionAllowed(IFormFile formFile, string[] allowedExtensions) + { + var extension = Path.GetExtension(formFile.FileName).ToLower(); + + return !string.IsNullOrEmpty(extension) && allowedExtensions.Contains(extension); + } + + public bool IsNotExceededLimit(IFormFile formFile, int limitMb) + { + var fileSizeByte = formFile.Length; + var fileSizeMByte = fileSizeByte / 1024.0 / 1024.0; + + return fileSizeMByte < limitMb; + } + + public async Task UploadAsync(IFormFile formFile, string[] rootFolderWithoutStorage) + { + InitRootFolders(rootFolderWithoutStorage); + + //Генерим уникальное имя файла: старое имя + рандом в середину + var newFileName = $"{Path.GetFileNameWithoutExtension(formFile.FileName)}-{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}{Path.GetExtension(formFile.FileName)}"; + + + var relativePath = Path.Combine(Path.Combine(rootFolderWithoutStorage), newFileName); + var absPath = Path.Combine(storageSettings.StoragePath, relativePath); + + if (File.Exists(absPath)) + { + logger.LogError($"Не могу сохранить файл, такой файл уже существует: {absPath}"); + return null; + } + + try + { + var origFileName = Path.GetFileName(formFile.FileName); + + using (var stream = new FileStream(absPath, FileMode.Create)) + { + await formFile.CopyToAsync(stream); + logger.LogInformation($"Файл сохранен {origFileName}, {absPath}"); + } + + return new UploadResult { FileName = origFileName, Path = relativePath }; + } + catch (Exception ex) + { + logger.LogError(ex, $"Ошибка при сохранении файла {absPath}"); + return null; + } + } + + private void InitRootFolders(string[] rootFolderWithoutStorage) + { + if (!rootFolderWithoutStorage.Any()) + return; + + var path = string.Empty; + foreach (var item in rootFolderWithoutStorage) + { + path = Path.Combine(path, item); + CreateDirectory(path); + } + } + + private void CreateDirectory(string relativePath) + { + var absolutePath = Path.Combine(storageSettings.StoragePath, relativePath); + + try + { + if (!Directory.Exists(absolutePath)) + Directory.CreateDirectory(absolutePath); + + // не нужно ничего возвращать, при сохраненнии файла будет ошибка и ее обработаем + //return true; + } + catch (Exception e) + { + logger.LogError(e, $"Не могу создать директорию: {absolutePath}"); + //return false; + } + } + } +} diff --git a/PARR.API/Services/Interfaces/IFileService.cs b/PARR.API/Services/Interfaces/IFileService.cs new file mode 100644 index 00000000..cd54edd2 --- /dev/null +++ b/PARR.API/Services/Interfaces/IFileService.cs @@ -0,0 +1,25 @@ +using PARR.API.Domain; + +namespace PARR.API.Services.Interfaces +{ + public interface IFileService + { + Task UploadAsync(IFormFile formFile, string[] rootFolderWithoutStorage); + + /// + /// Размер файла не превышает лимит? + /// + /// + /// + /// + bool IsNotExceededLimit(IFormFile formFile, int limitMb); + + /// + /// Разрешено это расширение? + /// + /// + /// + /// + bool IsExtensionAllowed(IFormFile formFile, string[] allowedExtensions); + } +} diff --git a/PARR.API/Settings/StorageSettings.cs b/PARR.API/Settings/StorageSettings.cs new file mode 100644 index 00000000..87fca1c8 --- /dev/null +++ b/PARR.API/Settings/StorageSettings.cs @@ -0,0 +1,24 @@ +namespace PARR.API.Settings +{ + public class StorageSettings + { + /// + /// Путь к хранилищу + /// + public string StoragePath { get; set; } = string.Empty; + + /// + /// Настройки хранилища загрузки шаблонов ЕСПП + /// + public StorageSettingsEsspTemplates? EsppTemplates { get; set; } + } + + public class StorageSettingsEsspTemplates + { + public string[] TemplatePath => new string[] { "espp-templates" }; + + public string[] AllowedExtensions { get; set; } = Array.Empty(); + + public int MaxFileSizeMb { get; set; } + } +} diff --git a/PARR.API/appsettings.Development.json b/PARR.API/appsettings.Development.json index 0c208ae9..8121164b 100644 --- a/PARR.API/appsettings.Development.json +++ b/PARR.API/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "StorageSettings": { + "StoragePath": "W:\\" } } diff --git a/PARR.API/appsettings.json b/PARR.API/appsettings.json index 1dfce196..82b28a51 100644 --- a/PARR.API/appsettings.json +++ b/PARR.API/appsettings.json @@ -29,5 +29,12 @@ "AllowedHosts": "*", "CorsSettings": { "AllowedHosts": "*" + }, + "StorageSettings": { + "StoragePath": "Data", + "EsppTemplates": { + "AllowedExtensions": [ ".csv" ], + "MaxFileSizeMb": 100 + } } }