diff --git a/AsbCloudApp/Data/Manuals/CatalogItemManualDto.cs b/AsbCloudApp/Data/Manuals/CatalogItemManualDto.cs
new file mode 100644
index 00000000..d4a766d1
--- /dev/null
+++ b/AsbCloudApp/Data/Manuals/CatalogItemManualDto.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AsbCloudApp.Data.Manuals;
+
+///
+/// Элемент каталога инструкций
+///
+public class CatalogItemManualDto
+{
+ ///
+ /// DTO категории
+ ///
+ public FileCategoryDto Category { get; set; } = null!;
+
+ ///
+ /// DTO инструкций хранящиеся без папки
+ ///
+ public IEnumerable ManualsWithoutFolder { get; set; } = Enumerable.Empty();
+
+ ///
+ /// DTO папок с инструкциями
+ ///
+ public IEnumerable Folders { get; set; } = Enumerable.Empty();
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Data/Manuals/ManualDto.cs b/AsbCloudApp/Data/Manuals/ManualDto.cs
new file mode 100644
index 00000000..8c01f66c
--- /dev/null
+++ b/AsbCloudApp/Data/Manuals/ManualDto.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace AsbCloudApp.Data.Manuals;
+
+///
+/// DTO инструкции
+///
+public class ManualDto : IId
+{
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Название
+ ///
+ public string Name { get; set; } = null!;
+
+ ///
+ /// Дата загрузки
+ ///
+ public DateTime DateDownload { get; set; }
+
+ ///
+ /// Id папки
+ ///
+ public int? IdFolder { get; set; }
+
+ ///
+ /// Id категории файла
+ ///
+ public int? IdCategory { get; set; }
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Data/Manuals/ManualFolderDto.cs b/AsbCloudApp/Data/Manuals/ManualFolderDto.cs
new file mode 100644
index 00000000..8eef923d
--- /dev/null
+++ b/AsbCloudApp/Data/Manuals/ManualFolderDto.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AsbCloudApp.Data.Manuals;
+
+///
+/// DTO папки для хранения инструкций
+///
+public class ManualFolderDto : IId
+{
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Название
+ ///
+ public string Name { get; set; } = null!;
+
+ ///
+ /// Id родительской папки
+ ///
+ public int? IdParent { get; set; }
+
+ ///
+ /// Id категории
+ ///
+ public int IdCategory { get; set; }
+
+ ///
+ /// Вложенные папки
+ ///
+ public IEnumerable Children { get; set; } = Enumerable.Empty();
+
+ ///
+ /// Хранимые инструкции
+ ///
+ public IEnumerable Manuals { get; set; } = Enumerable.Empty();
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Repositories/IFileCategoryRepository.cs b/AsbCloudApp/Repositories/IFileCategoryRepository.cs
new file mode 100644
index 00000000..7e64b024
--- /dev/null
+++ b/AsbCloudApp/Repositories/IFileCategoryRepository.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Services;
+
+namespace AsbCloudApp.Repositories;
+
+///
+/// Репозиторий для работы с категориями файлов
+///
+public interface IFileCategoryRepository : ICrudRepository
+{
+ ///
+ /// Получение всех записей по идентификатору типа
+ ///
+ ///
+ ///
+ ///
+ Task> GetAllAsync(int idType, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Repositories/IFileStorageRepository.cs b/AsbCloudApp/Repositories/IFileStorageRepository.cs
index b8b7b482..37ebb125 100644
--- a/AsbCloudApp/Repositories/IFileStorageRepository.cs
+++ b/AsbCloudApp/Repositories/IFileStorageRepository.cs
@@ -47,6 +47,12 @@ namespace AsbCloudApp.Repositories
///
void DeleteFile(string fileName);
+ ///
+ /// Удаление директории
+ ///
+ ///
+ void DeleteDirectory(string path);
+
///
/// Удаление всех файлов с диска о которых нет информации в базе
///
diff --git a/AsbCloudApp/Repositories/IManualFolderRepository.cs b/AsbCloudApp/Repositories/IManualFolderRepository.cs
new file mode 100644
index 00000000..d7b86182
--- /dev/null
+++ b/AsbCloudApp/Repositories/IManualFolderRepository.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+using AsbCloudApp.Services;
+
+namespace AsbCloudApp.Repositories;
+
+///
+/// Репозиторий для работы с папки хранящими инструкциями
+///
+public interface IManualFolderRepository : ICrudRepository
+{
+ ///
+ /// Получение дерева каталога папок
+ ///
+ ///
+ ///
+ ///
+ Task> GetTreeAsync(int idCategory, CancellationToken cancellationToken);
+
+ ///
+ /// Получение одной папки по параметрам
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GetOrDefaultAsync(string name, int? idParent, int idCategory, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Repositories/IManualRepository.cs b/AsbCloudApp/Repositories/IManualRepository.cs
new file mode 100644
index 00000000..d5d98c04
--- /dev/null
+++ b/AsbCloudApp/Repositories/IManualRepository.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+using AsbCloudApp.Services;
+
+namespace AsbCloudApp.Repositories;
+
+///
+/// Репозиторий для инструкций
+///
+public interface IManualRepository : ICrudRepository
+{
+ ///
+ /// Получение инструкций, которые не добавлены в папку
+ ///
+ ///
+ ///
+ ///
+ Task> GetManualsWithoutFolderAsync(int idCategory, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudApp/Services/IManualCatalogService.cs b/AsbCloudApp/Services/IManualCatalogService.cs
new file mode 100644
index 00000000..598de63e
--- /dev/null
+++ b/AsbCloudApp/Services/IManualCatalogService.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+
+namespace AsbCloudApp.Services;
+
+///
+/// Сервис для работы c каталогом инструкций
+///
+public interface IManualCatalogService
+{
+ ///
+ /// Сохранение файла
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task SaveFileAsync(int? idCategory, int? idFolder, string name, Stream stream,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Добавление новой папки
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task AddFolderAsync(string name, int? idParent, int idCategory,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Обновление папки
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task UpdateFolderAsync(int id, string name, CancellationToken cancellationToken);
+
+ ///
+ /// Удаление папки
+ ///
+ ///
+ ///
+ ///
+ Task DeleteFolderAsync(int id, CancellationToken cancellationToken);
+
+ ///
+ /// Удаление файла
+ ///
+ ///
+ ///
+ ///
+ Task DeleteFileAsync(int id, CancellationToken cancellationToken);
+
+ ///
+ /// Получение файла
+ ///
+ ///
+ ///
+ ///
+ Task<(Stream stream, string fileName)?> GetFileAsync(int id, CancellationToken cancellationToken);
+
+ ///
+ /// Получение каталога
+ ///
+ ///
+ ///
+ Task> GetCatalogAsync(CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index 601d05e7..6d2a43be 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -221,6 +221,11 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
+
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
return services;
}
diff --git a/AsbCloudInfrastructure/Repository/FileCategoryRepository.cs b/AsbCloudInfrastructure/Repository/FileCategoryRepository.cs
new file mode 100644
index 00000000..703817e4
--- /dev/null
+++ b/AsbCloudInfrastructure/Repository/FileCategoryRepository.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudDb.Model;
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace AsbCloudInfrastructure.Repository;
+
+public class FileCategoryRepository : CrudCacheRepositoryBase, IFileCategoryRepository
+{
+ public FileCategoryRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache) : base(dbContext, memoryCache)
+ {
+ }
+
+ public FileCategoryRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache,
+ Func, IQueryable> makeQuery) : base(dbContext, memoryCache, makeQuery)
+ {
+ }
+
+ public async Task> GetAllAsync(int idType, CancellationToken cancellationToken)
+ {
+ return await dbContext.FileCategories.Where(f => f.IdType == idType)
+ .Select(f => f.Adapt())
+ .ToArrayAsync(cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/Repository/FileStorageRepository.cs b/AsbCloudInfrastructure/Repository/FileStorageRepository.cs
index 4af6aa09..ea6268eb 100644
--- a/AsbCloudInfrastructure/Repository/FileStorageRepository.cs
+++ b/AsbCloudInfrastructure/Repository/FileStorageRepository.cs
@@ -33,6 +33,21 @@ public class FileStorageRepository : IFileStorageRepository
DeleteFile(fileName);
}
}
+
+ public void DeleteDirectory(string path)
+ {
+ var fullPath = Path.Combine(RootPath, path);
+
+ if (!Directory.Exists(fullPath))
+ return;
+
+ foreach (var file in Directory.GetFiles(fullPath))
+ {
+ File.Delete(file);
+ }
+
+ Directory.Delete(fullPath, true);
+ }
public void DeleteFile(string fileName)
{
diff --git a/AsbCloudInfrastructure/Repository/ManualFolderRepository.cs b/AsbCloudInfrastructure/Repository/ManualFolderRepository.cs
new file mode 100644
index 00000000..a567195d
--- /dev/null
+++ b/AsbCloudInfrastructure/Repository/ManualFolderRepository.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+using AsbCloudApp.Repositories;
+using AsbCloudDb.Model;
+using AsbCloudDb.Model.Manuals;
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+
+namespace AsbCloudInfrastructure.Repository;
+
+public class ManualFolderRepository : CrudRepositoryBase, IManualFolderRepository
+{
+ public ManualFolderRepository(IAsbCloudDbContext context) : base(context)
+ {
+ }
+
+ public async Task> GetTreeAsync(int idCategory,
+ CancellationToken cancellationToken)
+ {
+ var folders = await dbContext.ManualFolders
+ .Where(m => m.IdCategory == idCategory)
+ .AsNoTracking()
+ .Include(m => m.Manuals)
+ .Include(m => m.Parent)
+ .ToArrayAsync(cancellationToken);
+
+ return BuildTree(folders).Select(x => x.Adapt());
+ }
+
+ public async Task GetOrDefaultAsync(string name, int? idParent, int idCategory,
+ CancellationToken cancellationToken)
+ {
+ var entity = await dbContext.ManualFolders
+ .FirstOrDefaultAsync(m => m.Name == name &&
+ m.IdCategory == idCategory &&
+ m.IdParent == idParent, cancellationToken);
+
+ if (entity is null)
+ return null;
+
+ return Convert(entity);
+ }
+
+ private IEnumerable BuildTree(IEnumerable folders)
+ {
+ var folderDict = folders.ToDictionary(f => f.Id);
+
+ foreach (var folder in folders)
+ {
+ if (folder.IdParent.HasValue && folderDict.TryGetValue(folder.IdParent.Value, out var parent))
+ {
+ parent.Children ??= new List();
+ parent.Children.Add(folder);
+ }
+ }
+
+ return folders.Where(f => f.IdParent == null);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/Repository/ManualRepository.cs b/AsbCloudInfrastructure/Repository/ManualRepository.cs
new file mode 100644
index 00000000..a23d8714
--- /dev/null
+++ b/AsbCloudInfrastructure/Repository/ManualRepository.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+using AsbCloudApp.Repositories;
+using AsbCloudDb.Model;
+using AsbCloudDb.Model.Manuals;
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+
+namespace AsbCloudInfrastructure.Repository;
+
+public class ManualRepository : CrudRepositoryBase, IManualRepository
+{
+ public ManualRepository(IAsbCloudDbContext context) : base(context)
+ {
+ }
+
+ public ManualRepository(IAsbCloudDbContext context, Func, IQueryable> makeQuery)
+ : base(context, makeQuery)
+ {
+ }
+
+ public async Task> GetManualsWithoutFolderAsync(int idCategory, CancellationToken cancellationToken)
+ {
+ return await dbContext.Manuals.Where(m => m.IdCategory == idCategory &&
+ m.IdFolder == null)
+ .Select(m => m.Adapt())
+ .ToArrayAsync(cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudInfrastructure/Services/ManualCatalogService.cs b/AsbCloudInfrastructure/Services/ManualCatalogService.cs
new file mode 100644
index 00000000..545a114e
--- /dev/null
+++ b/AsbCloudInfrastructure/Services/ManualCatalogService.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data.Manuals;
+using AsbCloudApp.Exceptions;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using AsbCloudDb.Model;
+using Microsoft.Extensions.Configuration;
+
+namespace AsbCloudInfrastructure.Services;
+
+public class ManualCatalogService : IManualCatalogService
+{
+ private readonly IEnumerable validExtensions = new[]
+ {
+ ".pdf",
+ ".mp4"
+ };
+
+ private readonly string directoryFiles;
+ private readonly IFileStorageRepository fileStorageRepository;
+ private readonly IManualFolderRepository manualFolderRepository;
+ private readonly IManualRepository manualRepository;
+ private readonly IFileCategoryRepository fileCategoryRepository;
+
+ public ManualCatalogService(IFileStorageRepository fileStorageRepository,
+ IManualFolderRepository manualFolderRepository,
+ IManualRepository manualRepository,
+ IFileCategoryRepository fileCategoryRepository,
+ IConfiguration configuration)
+ {
+ this.fileStorageRepository = fileStorageRepository;
+ this.manualFolderRepository = manualFolderRepository;
+ this.manualRepository = manualRepository;
+ this.fileCategoryRepository = fileCategoryRepository;
+ directoryFiles = configuration.GetValue("DirectoryManualFiles");
+
+ if (string.IsNullOrWhiteSpace(directoryFiles))
+ directoryFiles = "manuals";
+ }
+
+ public async Task SaveFileAsync(int? idCategory, int? idFolder, string name, Stream stream,
+ CancellationToken cancellationToken)
+ {
+ var extension = Path.GetExtension(name);
+
+ if (!validExtensions.Contains(extension))
+ throw new ArgumentInvalidException(
+ $"Невозможно загрузить файл с расширением '{extension}'. Допустимые форматы файлов: {string.Join(", ", validExtensions)}",
+ extension);
+
+ var path = await BuildFilePathAsync(idCategory, idFolder, name, cancellationToken);
+
+ await fileStorageRepository.SaveFileAsync(path,
+ stream,
+ cancellationToken);
+
+ var manual = new ManualDto
+ {
+ Name = name,
+ DateDownload = DateTime.UtcNow,
+ IdFolder = idFolder,
+ IdCategory = idCategory
+ };
+
+ return await manualRepository.InsertAsync(manual, cancellationToken);
+ }
+
+ public async Task AddFolderAsync(string name, int? idParent, int idCategory,
+ CancellationToken cancellationToken)
+ {
+ if (idParent.HasValue)
+ {
+ var parent = await manualFolderRepository.GetOrDefaultAsync(idParent.Value, cancellationToken)
+ ?? throw new ArgumentInvalidException("Родительской папки не существует", nameof(idParent));
+
+ if (parent.IdCategory != idCategory)
+ throw new ArgumentInvalidException("Категория родительской папки не соответствует текущей категории",
+ nameof(idCategory));
+ }
+
+ var manualFolder = new ManualFolderDto
+ {
+ Name = name,
+ IdParent = idParent,
+ IdCategory = idCategory,
+ };
+
+ if (await IsExistFolderAsync(manualFolder, cancellationToken))
+ throw new ArgumentInvalidException("Папка с таким названием уже существует", name);
+
+ return await manualFolderRepository.InsertAsync(manualFolder, cancellationToken);
+ }
+
+ public async Task UpdateFolderAsync(int id, string name, CancellationToken cancellationToken)
+ {
+ var folder = await manualFolderRepository.GetOrDefaultAsync(id, cancellationToken)
+ ?? throw new ArgumentInvalidException($"Папки с Id: {id} не сущесвует", nameof(id));
+
+ folder.Name = name;
+
+ if (await IsExistFolderAsync(folder, cancellationToken))
+ throw new ArgumentInvalidException("Папка с таким названием уже существует", name);
+
+ await manualFolderRepository.UpdateAsync(folder, cancellationToken);
+ }
+
+
+ public async Task DeleteFolderAsync(int id, CancellationToken cancellationToken)
+ {
+ var folder = await manualFolderRepository.GetOrDefaultAsync(id, cancellationToken);
+
+ if (folder is null)
+ return 0;
+
+ var path = Path.Combine(directoryFiles, folder.IdCategory.ToString(), folder.Id.ToString());
+ fileStorageRepository.DeleteDirectory(path);
+
+ return await manualFolderRepository.DeleteAsync(folder.Id, cancellationToken);
+ }
+
+ public async Task DeleteFileAsync(int id, CancellationToken cancellationToken)
+ {
+ var manual = await manualRepository.GetOrDefaultAsync(id, cancellationToken);
+
+ if (manual is null)
+ return 0;
+
+ var filePath = await BuildFilePathAsync(manual.IdCategory, manual.IdFolder, manual.Name,
+ cancellationToken);
+
+ fileStorageRepository.DeleteFile(filePath);
+
+ return await manualRepository.DeleteAsync(manual.Id, cancellationToken);
+ }
+
+ public async Task<(Stream stream, string fileName)?> GetFileAsync(int id, CancellationToken cancellationToken)
+ {
+ var manual = await manualRepository.GetOrDefaultAsync(id, cancellationToken);
+
+ if (manual is null)
+ return null;
+
+ var path = await BuildFilePathAsync(manual.IdCategory, manual.IdFolder, manual.Name, cancellationToken);
+
+ var fileStream = new FileStream(Path.GetFullPath(path), FileMode.Open);
+
+ return (fileStream, manual.Name);
+ }
+
+ public async Task> GetCatalogAsync(CancellationToken cancellationToken)
+ {
+ var catalogItems = new List();
+
+ var categories = await fileCategoryRepository.GetAllAsync(FileCategory.IdFileCategoryTypeManuals,
+ cancellationToken);
+
+ foreach (var category in categories)
+ {
+ catalogItems.Add(new CatalogItemManualDto()
+ {
+ Category = category,
+ ManualsWithoutFolder = await manualRepository.GetManualsWithoutFolderAsync(category.Id, cancellationToken),
+ Folders = await manualFolderRepository.GetTreeAsync(category.Id, cancellationToken)
+ });
+ }
+
+ return catalogItems;
+ }
+
+ private async Task IsExistFolderAsync(ManualFolderDto folder, CancellationToken cancellationToken)
+ {
+ var existingFolder = await manualFolderRepository.GetOrDefaultAsync(folder.Name, folder.IdParent,
+ folder.IdCategory,
+ cancellationToken);
+
+ return existingFolder is not null && folder.Id != existingFolder.Id;
+ }
+
+ private async Task BuildFilePathAsync(int? idCategory, int? idFolder, string name,
+ CancellationToken cancellationToken)
+ {
+ if (idFolder.HasValue)
+ {
+ var folder = await manualFolderRepository.GetOrDefaultAsync(idFolder.Value, cancellationToken)
+ ?? throw new ArgumentInvalidException($"Папки с Id: {idFolder} не сущесвует", nameof(idFolder));
+
+ return fileStorageRepository.MakeFilePath(directoryFiles, Path.Combine(folder.IdCategory.ToString(),
+ folder.IdParent.ToString() ?? string.Empty,
+ folder.Id.ToString()), name);
+ }
+
+ if (!idCategory.HasValue)
+ throw new ArgumentInvalidException("Не указан идентификатор категории", nameof(idCategory));
+
+ return fileStorageRepository.MakeFilePath(directoryFiles, idCategory.Value.ToString(), name);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Controllers/ManualController.cs b/AsbCloudWebApi/Controllers/ManualController.cs
new file mode 100644
index 00000000..1a05655d
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/ManualController.cs
@@ -0,0 +1,120 @@
+using System.ComponentModel.DataAnnotations;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AsbCloudWebApi.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class ManualController : ControllerBase
+{
+ private readonly IManualCatalogService manualCatalogService;
+ private readonly IUserRepository userRepository;
+
+ public ManualController(IManualCatalogService manualCatalogService,
+ IUserRepository userRepository)
+ {
+ this.manualCatalogService = manualCatalogService;
+ this.userRepository = userRepository;
+ }
+
+ ///
+ /// Сохранение файла
+ ///
+ /// Необязательный параметр. 30000 - АСУ ТП, 30001 - Технология бурения
+ /// Необязательный параметр. Id папки
+ /// Загружаемый файл
+ ///
+ ///
+ [HttpPost]
+ [Permission]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task SaveFileAsync(
+ [Range(minimum: 30000, maximum: 30001, ErrorMessage = "Категория файла недопустима. Допустимые: 30000, 30001")]
+ int? idCategory,
+ int? idFolder,
+ [Required] IFormFile file,
+ CancellationToken cancellationToken)
+ {
+ if(!CanUserAccess("Manual.edit"))
+ return Forbid();
+
+ using var fileStream = file.OpenReadStream();
+
+ var id = await manualCatalogService.SaveFileAsync(idCategory, idFolder, file.FileName, fileStream, cancellationToken);
+
+ return Ok(id);
+ }
+
+ ///
+ /// Получение файла
+ ///
+ /// Id инструкции
+ ///
+ ///
+ [HttpGet("{id:int}")]
+ [Permission]
+ [ProducesResponseType(typeof(PhysicalFileResult), StatusCodes.Status200OK, "application/octet-stream")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task GetFileAsync(int id, CancellationToken cancellationToken)
+ {
+ if(!CanUserAccess("Manual.view"))
+ return Forbid();
+
+ var file = await manualCatalogService.GetFileAsync(id, cancellationToken);
+
+ if (!file.HasValue)
+ return NoContent();
+
+ return File(file.Value.stream, "application/octet-stream", file.Value.fileName);
+ }
+
+ ///
+ /// Удаление файла
+ ///
+ /// Id инструкции
+ ///
+ ///
+ [HttpDelete]
+ [Permission]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task DeleteFileAsync(int id, CancellationToken cancellationToken)
+ {
+ if(!CanUserAccess("Manual.edit"))
+ return Forbid();
+
+ return Ok(await manualCatalogService.DeleteFileAsync(id, cancellationToken));
+ }
+
+ ///
+ /// Получение каталога с инструкциями
+ ///
+ ///
+ ///
+ [HttpGet]
+ [Permission]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task GetCatalogAsync(CancellationToken cancellationToken)
+ {
+ if(!CanUserAccess("Manual.view"))
+ return Forbid();
+
+ return Ok(await manualCatalogService.GetCatalogAsync(cancellationToken));
+ }
+
+ private bool CanUserAccess(string permission)
+ {
+ var idUser = User.GetUserId();
+
+ return idUser.HasValue && userRepository.HasPermission(idUser.Value, permission);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Controllers/ManualFolderController.cs b/AsbCloudWebApi/Controllers/ManualFolderController.cs
new file mode 100644
index 00000000..076abe3c
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/ManualFolderController.cs
@@ -0,0 +1,94 @@
+using System.ComponentModel.DataAnnotations;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+namespace AsbCloudWebApi.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class ManualFolderController : ControllerBase
+{
+ private readonly IManualCatalogService manualCatalogService;
+ private readonly IUserRepository userRepository;
+
+ public ManualFolderController(IManualCatalogService manualCatalogService,
+ IUserRepository userRepository)
+ {
+ this.manualCatalogService = manualCatalogService;
+ this.userRepository = userRepository;
+ }
+
+ ///
+ /// Создание папки
+ ///
+ /// Название
+ /// Необязательный параметр. Id родительской папки
+ /// Id категории. 30000 - АСУ ТП, 30001 - Технология бурения
+ ///
+ ///
+ [HttpPost]
+ [Permission]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task AddFolderAsync(string name, int? idParent,
+ [Required(ErrorMessage = "Обязательный параметр")]
+ [Range(minimum: 30000, maximum: 30001, ErrorMessage = "Категория файла недопустима. Допустимые: 30000, 30001")]
+ int idCategory,
+ CancellationToken cancellationToken)
+ {
+ if (!CanUserAccess())
+ Forbid();
+
+ return Ok(await manualCatalogService.AddFolderAsync(name, idParent, idCategory, cancellationToken));
+ }
+
+ ///
+ /// Обновление папки
+ ///
+ ///
+ /// Новое название папки
+ ///
+ ///
+ [HttpPut]
+ [Permission]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task UpdateFolderAsync(int id, string name, CancellationToken cancellationToken)
+ {
+ if (!CanUserAccess())
+ Forbid();
+
+ await manualCatalogService.UpdateFolderAsync(id, name, cancellationToken);
+
+ return Ok();
+ }
+
+ ///
+ /// Удаление папки
+ ///
+ ///
+ ///
+ ///
+ [HttpDelete]
+ [Permission]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task DeleteFolderAsync(int id, CancellationToken cancellationToken)
+ {
+ if (!CanUserAccess())
+ Forbid();
+
+ return Ok(await manualCatalogService.DeleteFolderAsync(id, cancellationToken));
+ }
+
+ private bool CanUserAccess()
+ {
+ var idUser = User.GetUserId();
+
+ return idUser.HasValue && userRepository.HasPermission(idUser.Value, "Manual.edit");
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/appsettings.json b/AsbCloudWebApi/appsettings.json
index 683f7929..6801ff38 100644
--- a/AsbCloudWebApi/appsettings.json
+++ b/AsbCloudWebApi/appsettings.json
@@ -27,6 +27,7 @@
"supportMail": "support@digitaldrilling.ru"
},
"DirectoryNameHelpPageFiles": "helpPages",
+ "DirectoryManualFiles": "manuals",
"Urls": "http://0.0.0.0:5000" //;https://0.0.0.0:5001" //,
// See https man: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-6.0
//"Kestrel": {