diff --git a/AsbCloudApp/Data/HelpPageDto.cs b/AsbCloudApp/Data/HelpPageDto.cs
new file mode 100644
index 00000000..35f58700
--- /dev/null
+++ b/AsbCloudApp/Data/HelpPageDto.cs
@@ -0,0 +1,33 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace AsbCloudApp.Data;
+
+public class HelpPageDto : IId
+{
+ ///
+ /// Id записи
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Id категории файла
+ ///
+ [Range(1, int.MaxValue, ErrorMessage = "Id категории файла не может быть меньше 1")]
+ public int IdCategory { get; set; }
+
+ ///
+ /// Url страницы, которой пренадлежит справка
+ ///
+ public string UrlPage { get; set; } = null!;
+
+ ///
+ /// Имя файла
+ ///
+ [StringLength(260, MinimumLength = 1, ErrorMessage = "Допустимое имя файла от 1 до 260 символов")]
+ public string Name { get; set; } = null!;
+
+ ///
+ /// Размер файла
+ ///
+ public long Size { get; set; }
+}
diff --git a/AsbCloudApp/Repositories/IHelpPageRepository.cs b/AsbCloudApp/Repositories/IHelpPageRepository.cs
new file mode 100644
index 00000000..aa56a424
--- /dev/null
+++ b/AsbCloudApp/Repositories/IHelpPageRepository.cs
@@ -0,0 +1,34 @@
+using AsbCloudApp.Data;
+using AsbCloudApp.Services;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudApp.Repositories;
+
+///
+/// Интерфейс репозитория справок страниц
+///
+public interface IHelpPageRepository : ICrudRepository
+{
+ ///
+ /// Получение справки по url страницы и id категории
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage,
+ int idCategory,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Проверка на существование справки для определённой страницы в категории
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task IsCheckHelpPageWithUrlPageAndIdCategoryAsync(string urlPage,
+ int idCategory,
+ CancellationToken cancellationToken);
+}
diff --git a/AsbCloudApp/Services/IHelpPageService.cs b/AsbCloudApp/Services/IHelpPageService.cs
new file mode 100644
index 00000000..726f4566
--- /dev/null
+++ b/AsbCloudApp/Services/IHelpPageService.cs
@@ -0,0 +1,70 @@
+using AsbCloudApp.Data;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudApp.Services;
+
+///
+/// Интерфейс сервиса справок страниц
+///
+public interface IHelpPageService
+{
+ ///
+ /// Создание справки
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task CreateAsync(string urlPage,
+ int idCategory,
+ string fileName,
+ Stream fileStream,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Редактирование справки
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task UpdateAsync(HelpPageDto helpPage,
+ int idCategory,
+ string fileName,
+ Stream fileStream,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Получение справки по url страницы и id категории
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage,
+ int idCategory,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Получение справки по Id
+ ///
+ ///
+ ///
+ ///
+ Task GetOrDefaultByIdAsync(int id,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Получение файла справки по Id
+ ///
+ ///
+ ///
+ ///
+ Stream GetFileStream(HelpPageDto helpPage);
+}
\ No newline at end of file
diff --git a/AsbCloudDb/Model/HelpPage.cs b/AsbCloudDb/Model/HelpPage.cs
new file mode 100644
index 00000000..fe84254f
--- /dev/null
+++ b/AsbCloudDb/Model/HelpPage.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+
+namespace AsbCloudDb.Model;
+
+[Table("t_help_page"), Comment("Справки")]
+public class HelpPage : IId
+{
+ [Key]
+ [Column("id")]
+ public int Id { get; set; }
+
+ [Column("url_page"), Comment("Url страницы")]
+ public string UrlPage { get; set; } = null!;
+
+ [Column("id_category"), Comment("Id категории файла")]
+ public int IdCategory { get; set; }
+
+ [Column("name"), Comment("Название файла")]
+ public string Name { get; set; } = null!;
+
+ [Column("file_size"), Comment("Размер файла")]
+ public long Size { get; set; }
+
+ [JsonIgnore]
+ [ForeignKey(nameof(IdCategory))]
+ public virtual FileCategory FileCategory { get; set; } = null!;
+}
diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs
index aa4734bd..c3725e5f 100644
--- a/AsbCloudInfrastructure/DependencyInjection.cs
+++ b/AsbCloudInfrastructure/DependencyInjection.cs
@@ -136,6 +136,12 @@ namespace AsbCloudInfrastructure
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient(serviceProvider =>
+ new HelpPageService(
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ configuration.GetSection("HelpPageOptions:DirectoryNameHelpPageFiles")
+ .Get()));
services.AddTransient();
@@ -169,6 +175,7 @@ namespace AsbCloudInfrastructure
services.AddTransient, CrudCacheRepositoryBase>();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/AsbCloudInfrastructure/Repository/HelpPageRepository.cs b/AsbCloudInfrastructure/Repository/HelpPageRepository.cs
new file mode 100644
index 00000000..443b56db
--- /dev/null
+++ b/AsbCloudInfrastructure/Repository/HelpPageRepository.cs
@@ -0,0 +1,43 @@
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudDb.Model;
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsbCloudInfrastructure.Repository;
+
+public class HelpPageRepository : CrudRepositoryBase,
+ IHelpPageRepository
+{
+ public HelpPageRepository(IAsbCloudDbContext context)
+ : base(context)
+ {
+ }
+
+ public async Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage,
+ int idCategory,
+ CancellationToken cancellationToken)
+ {
+ var helpPage = await dbSet.AsNoTracking()
+ .SingleOrDefaultAsync(x =>
+ x.UrlPage == urlPage &&
+ x.IdCategory == idCategory,
+ cancellationToken);
+
+ if (helpPage is null)
+ return null;
+
+ return helpPage.Adapt();
+ }
+
+ public Task IsCheckHelpPageWithUrlPageAndIdCategoryAsync(string urlPage, int idCategory,
+ CancellationToken cancellationToken)
+ {
+ return dbSet.AnyAsync(x =>
+ x.UrlPage == urlPage &&
+ x.IdCategory == idCategory,
+ cancellationToken);
+ }
+}
diff --git a/AsbCloudInfrastructure/Services/HelpPageService.cs b/AsbCloudInfrastructure/Services/HelpPageService.cs
new file mode 100644
index 00000000..7be9e065
--- /dev/null
+++ b/AsbCloudInfrastructure/Services/HelpPageService.cs
@@ -0,0 +1,168 @@
+using System;
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Exceptions;
+
+namespace AsbCloudInfrastructure.Services;
+
+///
+/// Реализация сервиса справок страниц
+///
+public class HelpPageService : IHelpPageService
+{
+ private readonly string directoryNameHelpPageFiles;
+ private readonly IHelpPageRepository helpPageRepository;
+ private readonly IFileStorageRepository fileStorageRepository;
+
+ ///
+ /// Конструктор класса
+ ///
+ ///
+ ///
+ ///
+ public HelpPageService(IHelpPageRepository helpPageRepository,
+ IFileStorageRepository fileStorageRepository,
+ string directoryNameHelpPageFiles)
+ {
+ if (string.IsNullOrWhiteSpace(directoryNameHelpPageFiles))
+ throw new ArgumentException("Value cannot be null or whitespace", nameof(this.directoryNameHelpPageFiles));
+
+ this.helpPageRepository = helpPageRepository;
+ this.fileStorageRepository = fileStorageRepository;
+ this.directoryNameHelpPageFiles = directoryNameHelpPageFiles;
+ }
+
+ ///
+ /// Создание справки страницы
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task CreateAsync(string urlPage,
+ int idCategory,
+ string fileName,
+ Stream fileStream,
+ CancellationToken cancellationToken)
+ {
+ if (await helpPageRepository.IsCheckHelpPageWithUrlPageAndIdCategoryAsync(urlPage,
+ idCategory,
+ cancellationToken))
+ {
+ throw new ArgumentsInvalidException("Справка с такой категории файла для данной страницы уже существует",
+ new[] { nameof(urlPage), nameof(idCategory) });
+ }
+
+ HelpPageDto helpPage = new()
+ {
+ UrlPage = urlPage,
+ IdCategory = idCategory,
+ Name = Path.GetFileName(fileName),
+ Size = fileStream.Length,
+ };
+
+ int idFile = await helpPageRepository.InsertAsync(helpPage,
+ cancellationToken);
+
+ await SaveFileAsync(idCategory,
+ fileName,
+ fileStream,
+ idFile,
+ cancellationToken);
+
+ return idFile;
+ }
+
+ ///
+ /// Обновление справки страницы
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task UpdateAsync(HelpPageDto helpPage,
+ int idCategory,
+ string fileName,
+ Stream fileStream,
+ CancellationToken cancellationToken)
+ {
+ helpPage.Name = Path.GetFileName(fileName);
+ helpPage.IdCategory = idCategory;
+ helpPage.Size = fileStream.Length;
+
+ string fileFullName = fileStorageRepository.GetFilePath(directoryNameHelpPageFiles,
+ idCategory.ToString(),
+ helpPage.Id,
+ Path.GetExtension(helpPage.Name));
+
+ await helpPageRepository.UpdateAsync(helpPage,
+ cancellationToken);
+
+ fileStorageRepository.DeleteFile(fileFullName);
+
+ await SaveFileAsync(helpPage.IdCategory,
+ fileName,
+ fileStream,
+ helpPage.Id,
+ cancellationToken);
+ }
+
+ ///
+ /// Получение справки по url страницы и id категории
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage,
+ int idCategory,
+ CancellationToken cancellationToken) =>
+ helpPageRepository.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage,
+ idCategory,
+ cancellationToken);
+
+ public Task GetOrDefaultByIdAsync(int id, CancellationToken cancellationToken) =>
+ helpPageRepository.GetOrDefaultAsync(id,
+ cancellationToken);
+
+ ///
+ /// Получение файлового потока для файла справки
+ ///
+ ///
+ ///
+ ///
+ public Stream GetFileStream(HelpPageDto helpPage)
+ {
+ string filePath = fileStorageRepository.GetFilePath(directoryNameHelpPageFiles,
+ helpPage.IdCategory.ToString(),
+ helpPage.Id,
+ Path.GetExtension(helpPage.Name));;
+
+ var fileStream = new FileStream(Path.GetFullPath(filePath), FileMode.Open);
+
+ return fileStream;
+ }
+
+ private async Task SaveFileAsync(int idCategory,
+ string fileName,
+ Stream fileStream,
+ int fileId,
+ CancellationToken cancellationToken = default)
+ {
+ string filePath = fileStorageRepository.MakeFilePath(directoryNameHelpPageFiles,
+ idCategory.ToString(),
+ $"{fileId}" + $"{Path.GetExtension(fileName)}");
+
+ await fileStorageRepository.SaveFileAsync(filePath,
+ fileStream,
+ cancellationToken);
+ }
+}
diff --git a/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs
new file mode 100644
index 00000000..83fcf51c
--- /dev/null
+++ b/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Exceptions;
+using AsbCloudApp.Repositories;
+using AsbCloudApp.Services;
+using AsbCloudInfrastructure.Services;
+using Moq;
+using Xunit;
+
+namespace AsbCloudWebApi.Tests.ServicesTests;
+
+public class HelpPageServiceTest
+{
+ private const string directoryNameHelpPageFiles = "helpPages";
+
+ private static List HelpPages = new()
+ {
+ new()
+ {
+ Id = 123,
+ IdCategory = 20000,
+ Name = "1.pdf",
+ Size = 54000,
+ UrlPage = "test"
+ },
+ new()
+ {
+ Id = 134,
+ IdCategory = 20000,
+ Name = "2.pdf",
+ Size = 51000,
+ UrlPage = "test1"
+ },
+ new()
+ {
+ Id = 178,
+ IdCategory = 10000,
+ Name = "3.pdf",
+ Size = 49000,
+ UrlPage = "test2"
+ }
+ };
+
+ private readonly Mock helpPageRepository = new();
+ private readonly Mock fileStorageRepository = new();
+
+ private readonly IHelpPageService helpPageService;
+
+ public HelpPageServiceTest()
+ {
+ helpPageService = new HelpPageService(helpPageRepository.Object,
+ fileStorageRepository.Object,
+ directoryNameHelpPageFiles);
+ }
+
+ [Fact]
+ public async Task CreateAsync_ShouldReturn_PositiveId()
+ {
+ //arrange
+ int idHelpPage = new Random().Next(1, 100);
+ string urlPage = "test";
+ int idCategory = 20000;
+ string fullName = "test.pdf";
+ MemoryStream fileStream = new MemoryStream(Array.Empty());
+
+ helpPageRepository.Setup(x => x.InsertAsync(It.IsAny(),
+ It.IsAny()))
+ .Returns(() => Task.FromResult(idHelpPage));
+
+ fileStorageRepository.Setup(x => x.SaveFileAsync(It.IsAny(),
+ It.IsAny(),
+ It.IsAny()));
+
+ //act
+ int result = await helpPageService.CreateAsync(urlPage,
+ idCategory,
+ fullName,
+ fileStream,
+ CancellationToken.None);
+
+ //assert
+ Assert.True(result > 0);
+ }
+
+ [Fact]
+ public async Task CreateAsync_ShouldReturn_ArgumentsInvalidException()
+ {
+ //arrange
+ string urlPage = "test";
+ int idCategory = 20000;
+ string fullName = "test.pdf";
+ MemoryStream fileStream = new MemoryStream(Array.Empty());
+ bool isExistingHelpPage = true;
+
+ helpPageRepository.Setup(x => x.IsCheckHelpPageWithUrlPageAndIdCategoryAsync(It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(() => Task.FromResult(isExistingHelpPage));
+
+ //act
+ Task Result () => helpPageService.CreateAsync(urlPage,
+ idCategory,
+ fullName,
+ fileStream,
+ CancellationToken.None);
+
+ //assert
+ await Assert.ThrowsAsync(Result);
+ }
+
+ [Fact]
+ public async Task UpdateAsync_ShouldReturn_UpdatedHelpPage()
+ {
+ //arrange
+ HelpPageDto helpPage = new()
+ {
+ Id = 123,
+ IdCategory = 134,
+ UrlPage = "test",
+ Name = ".pdf",
+ Size = 54000
+ };
+
+ int newIdCategory = 451;
+ string newFileName = " .pdf";
+ MemoryStream newFileStream = new MemoryStream(Array.Empty());
+
+ //act
+ await helpPageService.UpdateAsync(helpPage,
+ newIdCategory,
+ newFileName,
+ newFileStream,
+ CancellationToken.None);
+
+ //assert
+ Assert.Equal(newFileName, helpPage.Name);
+ Assert.Equal(newIdCategory, helpPage.IdCategory);
+ Assert.Equal(newFileStream.Length, helpPage.Size);
+ }
+
+ [Theory]
+ [InlineData(20000, "test")]
+ [InlineData(20000, "test1")]
+ public async Task GetOrDefaultByUrlPageAndIdCategoryAsync_ShouldReturn_HelpPageDto(int idCategory,
+ string urlPage)
+ {
+ //arrange
+ helpPageRepository.Setup(x => x.GetOrDefaultByUrlPageAndIdCategoryAsync(It.IsAny(),
+ It.IsAny(), It.IsAny()))
+ .Returns(() =>
+ {
+ var helpPage = HelpPages.FirstOrDefault(x =>
+ x.UrlPage == urlPage &&
+ x.IdCategory == idCategory);
+
+ return Task.FromResult(helpPage);
+ });
+
+ //act
+ var result = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage,
+ idCategory,
+ CancellationToken.None);
+
+ //assert
+ Assert.NotNull(result);
+ }
+
+ [Theory]
+ [InlineData(123)]
+ [InlineData(178)]
+ public async Task GetOrDefaultByIdAsync_ShouldReturn_HelpPageDto(int id)
+ {
+ //arrange
+ helpPageRepository.Setup(x => x.GetOrDefaultAsync(It.IsAny(),
+ It.IsAny()))
+ .Returns(() =>
+ {
+ var helpPage = HelpPages.FirstOrDefault(x =>
+ x.Id == id);
+
+ return Task.FromResult(helpPage);
+ });
+
+ //act
+ var result = await helpPageService.GetOrDefaultByIdAsync(id,
+ CancellationToken.None);
+
+ //assert
+ Assert.NotNull(result);
+ }
+}
\ No newline at end of file
diff --git a/AsbCloudWebApi/Controllers/HelpPageController.cs b/AsbCloudWebApi/Controllers/HelpPageController.cs
new file mode 100644
index 00000000..d3bc0a48
--- /dev/null
+++ b/AsbCloudWebApi/Controllers/HelpPageController.cs
@@ -0,0 +1,125 @@
+using AsbCloudApp.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using AsbCloudApp.Data;
+using AsbCloudApp.Repositories;
+using Microsoft.AspNetCore.Authorization;
+
+namespace AsbCloudWebApi.Controllers;
+
+///
+/// Справки по страницам
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class HelpPageController : ControllerBase
+{
+ private readonly IHelpPageService helpPageService;
+ private readonly IUserRepository userRepository;
+
+ public HelpPageController(IHelpPageService helpPageService,
+ IUserRepository userRepository)
+ {
+ this.helpPageService = helpPageService;
+ this.userRepository = userRepository;
+ }
+
+ ///
+ /// Создание файла справки
+ ///
+ /// Url страницы для которой предназначена эта справка
+ /// Id катагории файла
+ /// Загружаемый файл
+ /// Id созданной справки
+ [HttpPost]
+ [Permission]
+ [Route("saveFile")]
+ [ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)]
+ public async Task CreateAsync(string urlPage,
+ int idCategory,
+ [Required] IFormFile file)
+ {
+ int? idUser = User.GetUserId();
+
+ if(!idUser.HasValue)
+ return Forbid();
+
+ if (!userRepository.HasPermission(idUser.Value, $"HelpPage.create"))
+ return Forbid();
+
+ var helpPage = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage,
+ idCategory,
+ CancellationToken.None);
+
+ using var fileStream = file.OpenReadStream();
+
+ if (helpPage is not null)
+ {
+ await helpPageService.UpdateAsync(helpPage,
+ idCategory,
+ file.FileName,
+ fileStream,
+ CancellationToken.None);
+
+ return Ok(helpPage.Id);
+ }
+
+ int helpPageId = await helpPageService.CreateAsync(urlPage,
+ idCategory,
+ file.FileName,
+ fileStream,
+ CancellationToken.None);
+
+ return Ok(helpPageId);
+ }
+
+ ///
+ /// Получение файла справки
+ ///
+ /// Id справки
+ /// Файл
+ [HttpGet]
+ [Route("getById/{id}")]
+ [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
+ public async Task GetFileAsync(int id)
+ {
+ var helpPage = await helpPageService.GetOrDefaultByIdAsync(id,
+ CancellationToken.None);
+
+ if (helpPage is null)
+ return NotFound();
+
+ using var fileStream = helpPageService.GetFileStream(helpPage);
+
+ var memoryStream = new MemoryStream();
+ await fileStream.CopyToAsync(memoryStream,
+ CancellationToken.None);
+ memoryStream.Position = 0;
+
+ return File(memoryStream, "application/octet-stream", helpPage.Name);
+ }
+
+ ///
+ /// Получение информации о справке
+ ///
+ /// Url страницы
+ /// Id категории
+ /// Dto справки
+ [HttpGet]
+ [Route("getByUrlPage/{urlPage}/{idCategory}")]
+ [ProducesResponseType(typeof(HelpPageDto), (int)System.Net.HttpStatusCode.OK)]
+ public async Task GetByUrlPageAsync(string urlPage,
+ int idCategory)
+ {
+ var helpPage = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage,
+ idCategory,
+ CancellationToken.None);
+
+ return Ok(helpPage);
+ }
+}