Merge pull request 'feature/manuals' (#95) from feature/manuals into dev

Reviewed-on: http://test.digitaldrilling.ru:8080/DDrilling/AsbCloudServer/pulls/95
This commit is contained in:
Никита Фролов 2023-09-14 12:54:02 +05:00
commit 3befaf3617
21 changed files with 9586 additions and 2 deletions

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
namespace AsbCloudApp.Data.Manuals;
/// <summary>
/// Директория для хранения инструкций
/// </summary>
public class ManualDirectoryDto : IId
{
/// <inheritdoc/>
public int Id { get; set; }
/// <summary>
/// Название
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Id родительской директории
/// </summary>
public int? IdParent { get; set; }
/// <summary>
/// Вложенные директории
/// </summary>
public IEnumerable<ManualDirectoryDto> Children { get; set; } = Enumerable.Empty<ManualDirectoryDto>();
/// <summary>
/// Хранимые инструкции
/// </summary>
public IEnumerable<ManualDto> Manuals { get; set; } = Enumerable.Empty<ManualDto>();
}

View File

@ -0,0 +1,37 @@
using System;
namespace AsbCloudApp.Data.Manuals;
/// <summary>
/// Инструкция
/// </summary>
public class ManualDto : IId
{
/// <inheritdoc/>
public int Id { get; set; }
/// <summary>
/// Название
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Дата загрузки
/// </summary>
public DateTime DateDownload { get; set; }
/// <summary>
/// Id автора
/// </summary>
public int IdAuthor { get; set; }
/// <summary>
/// Id директории
/// </summary>
public int IdDirectory { get; set; }
/// <summary>
/// Id категории файла
/// </summary>
public int IdCategory { get; set; }
}

View File

@ -47,6 +47,13 @@ namespace AsbCloudApp.Repositories
/// <param name="fileName"></param> /// <param name="fileName"></param>
void DeleteFile(string fileName); void DeleteFile(string fileName);
/// <summary>
/// Удаление директории
/// </summary>
/// <param name="path"></param>
/// <param name="isRecursive"></param>
void DeleteDirectory(string path, bool isRecursive);
/// <summary> /// <summary>
/// Удаление всех файлов с диска о которых нет информации в базе /// Удаление всех файлов с диска о которых нет информации в базе
/// </summary> /// </summary>

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.Manuals;
using AsbCloudApp.Services;
namespace AsbCloudApp.Repositories;
/// <summary>
/// Репозиторий для работы с директориямиы хранящими инструкциями
/// </summary>
public interface IManualDirectoryRepository : ICrudRepository<ManualDirectoryDto>
{
/// <summary>
/// Получение дерева директорий
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<ManualDirectoryDto>> GetTreeAsync(CancellationToken cancellationToken);
/// <summary>
/// Получение одной директории по параметрам
/// </summary>
/// <param name="name"></param>
/// <param name="idParent"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<ManualDirectoryDto?> GetOrDefaultAsync(string name, int? idParent, CancellationToken cancellationToken);
/// <summary>
/// Проверка директории на существование
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> IsExistsAsync(int id, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,64 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudApp.Services;
/// <summary>
/// Сервис для работы c каталогом инструкций
/// </summary>
public interface IManualCatalogService
{
/// <summary>
/// Сохранение файла
/// </summary>
/// <param name="idDirectory"></param>
/// <param name="idAuthor"></param>
/// <param name="name"></param>
/// <param name="stream"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> SaveFileAsync(int idDirectory, int idAuthor, string name, Stream stream, CancellationToken cancellationToken);
/// <summary>
/// Добавление директории
/// </summary>
/// <param name="name"></param>
/// <param name="idParent"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> AddDirectoryAsync(string name, int? idParent, CancellationToken cancellationToken);
/// <summary>
/// Обновление директории
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task UpdateDirectoryAsync(int id, string name, CancellationToken cancellationToken);
/// <summary>
/// Удаление директории
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> DeleteDirectoryAsync(int id, CancellationToken cancellationToken);
/// <summary>
/// Удаление файла
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> DeleteFileAsync(int id, CancellationToken cancellationToken);
/// <summary>
/// Получение файла
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<(Stream stream, string fileName)?> GetFileAsync(int id, CancellationToken cancellationToken);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AsbCloudDb.Migrations
{
public partial class Add_Manuals : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "t_manual_directory",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "text", nullable: false, comment: "Название"),
id_parent = table.Column<int>(type: "integer", nullable: true, comment: "Id родительской директории")
},
constraints: table =>
{
table.PrimaryKey("PK_t_manual_directory", x => x.id);
table.ForeignKey(
name: "FK_t_manual_directory_t_manual_directory_id_parent",
column: x => x.id_parent,
principalTable: "t_manual_directory",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
},
comment: "Директория для инструкций");
migrationBuilder.CreateTable(
name: "t_manual",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "text", nullable: false, comment: "Название"),
date_download = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "Дата загрузки"),
id_directory = table.Column<int>(type: "integer", nullable: false, comment: "Id директории"),
id_category = table.Column<int>(type: "integer", nullable: false, comment: "Id категории файла"),
id_author = table.Column<int>(type: "integer", nullable: false, comment: "Id автора")
},
constraints: table =>
{
table.PrimaryKey("PK_t_manual", x => x.id);
table.ForeignKey(
name: "FK_t_manual_t_file_category_id_category",
column: x => x.id_category,
principalTable: "t_file_category",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_t_manual_t_manual_directory_id_directory",
column: x => x.id_directory,
principalTable: "t_manual_directory",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_t_manual_t_user_id_author",
column: x => x.id_author,
principalTable: "t_user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
},
comment: "Инструкции");
migrationBuilder.InsertData(
table: "t_file_category",
columns: new[] { "id", "name", "short_name" },
values: new object[] { 30000, "Инструкции", null });
migrationBuilder.InsertData(
table: "t_permission",
columns: new[] { "id", "description", "name" },
values: new object[,]
{
{ 523, "Разрешить редактирование инструкций", "Manual.edit" },
{ 524, "Разрешить получение инструкций", "Manual.get" }
});
migrationBuilder.InsertData(
table: "t_relation_user_role_permission",
columns: new[] { "id_permission", "id_user_role" },
values: new object[,]
{
{ 523, 1 },
{ 524, 1 }
});
migrationBuilder.CreateIndex(
name: "IX_t_manual_id_author",
table: "t_manual",
column: "id_author");
migrationBuilder.CreateIndex(
name: "IX_t_manual_id_category",
table: "t_manual",
column: "id_category");
migrationBuilder.CreateIndex(
name: "IX_t_manual_id_directory",
table: "t_manual",
column: "id_directory");
migrationBuilder.CreateIndex(
name: "IX_t_manual_directory_id_parent",
table: "t_manual_directory",
column: "id_parent");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "t_manual");
migrationBuilder.DropTable(
name: "t_manual_directory");
migrationBuilder.DeleteData(
table: "t_file_category",
keyColumn: "id",
keyValue: 30000);
migrationBuilder.DeleteData(
table: "t_relation_user_role_permission",
keyColumns: new[] { "id_permission", "id_user_role" },
keyValues: new object[] { 523, 1 });
migrationBuilder.DeleteData(
table: "t_relation_user_role_permission",
keyColumns: new[] { "id_permission", "id_user_role" },
keyValues: new object[] { 524, 1 });
migrationBuilder.DeleteData(
table: "t_permission",
keyColumn: "id",
keyValue: 523);
migrationBuilder.DeleteData(
table: "t_permission",
keyColumn: "id",
keyValue: 524);
}
}
}

View File

@ -785,6 +785,11 @@ namespace AsbCloudDb.Migrations
{ {
Id = 20000, Id = 20000,
Name = "Справки по страницам" Name = "Справки по страницам"
},
new
{
Id = 30000,
Name = "Инструкции"
}); });
}); });
@ -1066,6 +1071,83 @@ namespace AsbCloudDb.Migrations
b.HasComment("Ограничения по параметрам телеметрии"); b.HasComment("Ограничения по параметрам телеметрии");
}); });
modelBuilder.Entity("AsbCloudDb.Model.Manuals.Manual", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("DateDownload")
.HasColumnType("timestamp with time zone")
.HasColumnName("date_download")
.HasComment("Дата загрузки");
b.Property<int>("IdAuthor")
.HasColumnType("integer")
.HasColumnName("id_author")
.HasComment("Id автора");
b.Property<int>("IdCategory")
.HasColumnType("integer")
.HasColumnName("id_category")
.HasComment("Id категории файла");
b.Property<int>("IdDirectory")
.HasColumnType("integer")
.HasColumnName("id_directory")
.HasComment("Id директории");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name")
.HasComment("Название");
b.HasKey("Id");
b.HasIndex("IdAuthor");
b.HasIndex("IdCategory");
b.HasIndex("IdDirectory");
b.ToTable("t_manual");
b.HasComment("Инструкции");
});
modelBuilder.Entity("AsbCloudDb.Model.Manuals.ManualDirectory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("IdParent")
.HasColumnType("integer")
.HasColumnName("id_parent")
.HasComment("Id родительской директории");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name")
.HasComment("Название");
b.HasKey("Id");
b.HasIndex("IdParent");
b.ToTable("t_manual_directory");
b.HasComment("Директория для инструкций");
});
modelBuilder.Entity("AsbCloudDb.Model.Measure", b => modelBuilder.Entity("AsbCloudDb.Model.Measure", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -2165,6 +2247,18 @@ namespace AsbCloudDb.Migrations
Name = "UserSettings.delete" Name = "UserSettings.delete"
}, },
new new
{
Id = 523,
Description = "Разрешить редактирование инструкций",
Name = "Manual.edit"
},
new
{
Id = 524,
Description = "Разрешить получение инструкций",
Name = "Manual.get"
},
new
{ {
Id = 525, Id = 525,
Description = "Разрешение на редактирование РТК у завершенной скважины", Description = "Разрешение на редактирование РТК у завершенной скважины",
@ -3719,6 +3813,16 @@ namespace AsbCloudDb.Migrations
IdPermission = 522 IdPermission = 522
}, },
new new
{
IdUserRole = 1,
IdPermission = 523
},
new
{
IdUserRole = 1,
IdPermission = 524
},
new
{ {
IdUserRole = 1, IdUserRole = 1,
IdPermission = 525 IdPermission = 525
@ -7659,6 +7763,43 @@ namespace AsbCloudDb.Migrations
b.Navigation("Telemetry"); b.Navigation("Telemetry");
}); });
modelBuilder.Entity("AsbCloudDb.Model.Manuals.Manual", b =>
{
b.HasOne("AsbCloudDb.Model.User", "Author")
.WithMany()
.HasForeignKey("IdAuthor")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AsbCloudDb.Model.FileCategory", "Category")
.WithMany()
.HasForeignKey("IdCategory")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AsbCloudDb.Model.Manuals.ManualDirectory", "Directory")
.WithMany("Manuals")
.HasForeignKey("IdDirectory")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
b.Navigation("Category");
b.Navigation("Directory");
});
modelBuilder.Entity("AsbCloudDb.Model.Manuals.ManualDirectory", b =>
{
b.HasOne("AsbCloudDb.Model.Manuals.ManualDirectory", "Parent")
.WithMany("Children")
.HasForeignKey("IdParent")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Parent");
});
modelBuilder.Entity("AsbCloudDb.Model.Measure", b => modelBuilder.Entity("AsbCloudDb.Model.Measure", b =>
{ {
b.HasOne("AsbCloudDb.Model.MeasureCategory", "Category") b.HasOne("AsbCloudDb.Model.MeasureCategory", "Category")
@ -8284,6 +8425,13 @@ namespace AsbCloudDb.Migrations
b.Navigation("FileMarks"); b.Navigation("FileMarks");
}); });
modelBuilder.Entity("AsbCloudDb.Model.Manuals.ManualDirectory", b =>
{
b.Navigation("Children");
b.Navigation("Manuals");
});
modelBuilder.Entity("AsbCloudDb.Model.MeasureCategory", b => modelBuilder.Entity("AsbCloudDb.Model.MeasureCategory", b =>
{ {
b.Navigation("Measures"); b.Navigation("Measures");

View File

@ -3,6 +3,7 @@ using AsbCloudDb.Model.Subsystems;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudDb.Model.Manuals;
namespace AsbCloudDb.Model namespace AsbCloudDb.Model
{ {
@ -77,6 +78,9 @@ namespace AsbCloudDb.Model
public DbSet<HelpPage> HelpPages => Set<HelpPage>(); public DbSet<HelpPage> HelpPages => Set<HelpPage>();
public DbSet<Notification> Notifications => Set<Notification>(); public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<NotificationCategory> NotificationCategories => Set<NotificationCategory>(); public DbSet<NotificationCategory> NotificationCategories => Set<NotificationCategory>();
public DbSet<Manual> Manuals => Set<Manual>();
public DbSet<ManualDirectory> ManualDirectories => Set<ManualDirectory>();
public AsbCloudDbContext() : base() public AsbCloudDbContext() : base()
{ {
@ -388,6 +392,18 @@ namespace AsbCloudDb.Model
entity.HasKey(x => new { x.IdWell, x.IdUser }); entity.HasKey(x => new { x.IdWell, x.IdUser });
}); });
modelBuilder.Entity<ManualDirectory>()
.HasOne(mf => mf.Parent)
.WithMany(mf => mf.Children)
.HasForeignKey(mf => mf.IdParent)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Manual>()
.HasOne(m => m.Directory)
.WithMany(f => f.Manuals)
.HasForeignKey(m => m.IdDirectory)
.OnDelete(DeleteBehavior.Cascade);
DefaultData.DefaultContextData.Fill(modelBuilder); DefaultData.DefaultContextData.Fill(modelBuilder);
} }

View File

@ -73,7 +73,9 @@
new () {Id = 10042, Name = "Паспорт скважины (заполняется геологами)"}, new () {Id = 10042, Name = "Паспорт скважины (заполняется геологами)"},
new () {Id = 10043, Name = "Фактические данные бурения (вставляются в паспорт скважины)"}, new () {Id = 10043, Name = "Фактические данные бурения (вставляются в паспорт скважины)"},
new () {Id = 20000, Name = "Справки по страницам"} new () {Id = 20000, Name = "Справки по страницам"},
new() { Id = 30000, Name = "Инструкции"},
}; };
} }
} }

View File

@ -156,6 +156,9 @@
new() { Id = 521, Name = "HelpPage.edit", Description = "Разрешить создание справок по страницам"}, new() { Id = 521, Name = "HelpPage.edit", Description = "Разрешить создание справок по страницам"},
new() { Id = 522, Name = "UserSettings.delete", Description = "Разрешить удаление всех настроек пользователя"}, new() { Id = 522, Name = "UserSettings.delete", Description = "Разрешить удаление всех настроек пользователя"},
new() { Id = 523, Name = "Manual.edit", Description = "Разрешить редактирование инструкций" },
new() { Id = 524, Name = "Manual.get", Description = "Разрешить получение инструкций"},
new (){ Id = 525, Name = "ProcessMap.editCompletedWell", Description = "Разрешение на редактирование РТК у завершенной скважины"}, new (){ Id = 525, Name = "ProcessMap.editCompletedWell", Description = "Разрешение на редактирование РТК у завершенной скважины"},
new (){ Id = 526, Name = "WellOperation.editCompletedWell", Description = "Разрешение на редактирование операций у завершенной скважины"} new (){ Id = 526, Name = "WellOperation.editCompletedWell", Description = "Разрешение на редактирование операций у завершенной скважины"}

View File

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudDb.Model.Manuals;
namespace AsbCloudDb.Model namespace AsbCloudDb.Model
{ {
@ -70,6 +71,8 @@ namespace AsbCloudDb.Model
DbSet<HelpPage> HelpPages { get; } DbSet<HelpPage> HelpPages { get; }
DbSet<Notification> Notifications { get; } DbSet<Notification> Notifications { get; }
DbSet<NotificationCategory> NotificationCategories { get; } DbSet<NotificationCategory> NotificationCategories { get; }
DbSet<Manual> Manuals { get; }
DbSet<ManualDirectory> ManualDirectories { get; }
DatabaseFacade Database { get; } DatabaseFacade Database { get; }
Task<int> RefreshMaterializedViewAsync(string mwName, CancellationToken token); Task<int> RefreshMaterializedViewAsync(string mwName, CancellationToken token);

View File

@ -0,0 +1,38 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace AsbCloudDb.Model.Manuals;
[Table("t_manual"), Comment("Инструкции")]
public class Manual : IId
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("name"), Comment("Название")]
public string Name { get; set; } = null!;
[Column("date_download"), Comment("Дата загрузки")]
public DateTime DateDownload { get; set; }
[Column("id_directory"), Comment("Id директории")]
public int IdDirectory { get; set; }
[Column("id_category"), Comment("Id категории файла")]
public int IdCategory { get; set; }
[Column("id_author"), Comment("Id автора")]
public int IdAuthor { get; set; }
[ForeignKey(nameof(IdDirectory))]
public virtual ManualDirectory Directory { get; set; } = null!;
[ForeignKey(nameof(IdCategory))]
public virtual FileCategory Category { get; set; } = null!;
[ForeignKey(nameof(IdAuthor))]
public virtual User Author { get; set; } = null!;
}

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace AsbCloudDb.Model.Manuals;
[Table("t_manual_directory"), Comment("Директория для инструкций")]
public class ManualDirectory : IId
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("name"), Comment("Название")]
public string Name { get; set; } = null!;
[Column("id_parent"), Comment("Id родительской директории")]
public int? IdParent { get; set; }
[ForeignKey(nameof(IdParent))]
public virtual ManualDirectory? Parent { get; set; }
public virtual ICollection<ManualDirectory>? Children { get; set; }
public virtual ICollection<Manual>? Manuals { get; set; }
}

View File

@ -23,8 +23,10 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using AsbCloudApp.Data.Manuals;
using AsbCloudApp.Services.AutoGeneratedDailyReports; using AsbCloudApp.Services.AutoGeneratedDailyReports;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudDb.Model.Manuals;
using AsbCloudInfrastructure.Services.AutoGeneratedDailyReports; using AsbCloudInfrastructure.Services.AutoGeneratedDailyReports;
namespace AsbCloudInfrastructure namespace AsbCloudInfrastructure
@ -221,6 +223,10 @@ namespace AsbCloudInfrastructure
services.AddTransient<IAutoGeneratedDailyReportService, AutoGeneratedDailyReportService>(); services.AddTransient<IAutoGeneratedDailyReportService, AutoGeneratedDailyReportService>();
services.AddTransient<IAutoGeneratedDailyReportMakerService, AutoGeneratedDailyReportMakerService>(); services.AddTransient<IAutoGeneratedDailyReportMakerService, AutoGeneratedDailyReportMakerService>();
services.AddTransient<IManualDirectoryRepository, ManualDirectoryRepository>();
services.AddTransient<IManualCatalogService, ManualCatalogService>();
services.AddTransient<ICrudRepository<ManualDto>, CrudRepositoryBase<ManualDto, Manual>>();
services.AddTransient<IWellboreService, WellboreService>(); services.AddTransient<IWellboreService, WellboreService>();

View File

@ -1,4 +1,5 @@
using AsbCloudApp.Data; using System;
using AsbCloudApp.Data;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -33,6 +34,23 @@ public class FileStorageRepository : IFileStorageRepository
DeleteFile(fileName); DeleteFile(fileName);
} }
} }
public void DeleteDirectory(string path, bool isRecursive)
{
if (!Directory.Exists(path))
return;
if (!isRecursive)
{
var files = Directory.GetFiles(path);
var directories = Directory.GetDirectories(path);
if (files.Length != 0 || directories.Length != 0)
throw new InvalidOperationException("Директория не пуста и не может быть удалена");
}
Directory.Delete(path, isRecursive);
}
public void DeleteFile(string fileName) public void DeleteFile(string fileName)
{ {

View File

@ -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 ManualDirectoryRepository : CrudRepositoryBase<ManualDirectoryDto, ManualDirectory>, IManualDirectoryRepository
{
public ManualDirectoryRepository(IAsbCloudDbContext context) : base(context)
{
}
public async Task<IEnumerable<ManualDirectoryDto>> GetTreeAsync(CancellationToken cancellationToken)
{
var directories = await dbContext.ManualDirectories
.Include(m => m.Manuals)
.Include(m => m.Parent)
.AsNoTracking()
.ToArrayAsync(cancellationToken);
return BuildTree(directories).Select(x => x.Adapt<ManualDirectoryDto>());
}
public async Task<ManualDirectoryDto?> GetOrDefaultAsync(string name, int? idParent, CancellationToken cancellationToken)
{
var entity = await dbContext.ManualDirectories
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Name == name &&
m.IdParent == idParent, cancellationToken);
if (entity is null)
return null;
return Convert(entity);
}
public Task<bool> IsExistsAsync(int id, CancellationToken cancellationToken) =>
dbContext.ManualDirectories.AnyAsync(d => d.Id == id, cancellationToken);
private static IEnumerable<ManualDirectory> BuildTree(IEnumerable<ManualDirectory> directories)
{
var directoryDict = directories.ToDictionary(f => f.Id);
foreach (var directory in directories)
{
if (directory.IdParent.HasValue && directoryDict.TryGetValue(directory.IdParent.Value, out var parent))
{
parent.Children ??= new List<ManualDirectory>();
parent.Children.Add(directory);
}
}
return directories.Where(f => f.IdParent == null);
}
}

View File

@ -0,0 +1,180 @@
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 Microsoft.Extensions.Configuration;
namespace AsbCloudInfrastructure.Services;
public class ManualCatalogService : IManualCatalogService
{
private const int IdFileCategory = 30000;
private readonly IEnumerable<string> validExtensions = new[]
{
".pdf",
".mp4"
};
private readonly string directoryFiles;
private readonly IFileStorageRepository fileStorageRepository;
private readonly IManualDirectoryRepository manualDirectoryRepository;
private readonly ICrudRepository<ManualDto> manualRepository;
public ManualCatalogService(IFileStorageRepository fileStorageRepository,
IManualDirectoryRepository manualDirectoryRepository,
ICrudRepository<ManualDto> manualRepository,
IConfiguration configuration)
{
this.fileStorageRepository = fileStorageRepository;
this.manualDirectoryRepository = manualDirectoryRepository;
this.manualRepository = manualRepository;
directoryFiles = configuration.GetValue<string>("DirectoryManualFiles");
if (string.IsNullOrWhiteSpace(directoryFiles))
directoryFiles = "manuals";
}
public async Task<int> SaveFileAsync(int idDirectory, int idAuthor, 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(idDirectory, name, cancellationToken);
await fileStorageRepository.SaveFileAsync(path, stream, cancellationToken);
var manual = new ManualDto
{
Name = name,
DateDownload = DateTime.UtcNow,
IdDirectory = idDirectory,
IdCategory = IdFileCategory,
IdAuthor = idAuthor
};
return await manualRepository.InsertAsync(manual, cancellationToken);
}
public async Task<int> AddDirectoryAsync(string name, int? idParent, CancellationToken cancellationToken)
{
if (idParent.HasValue && !await manualDirectoryRepository.IsExistsAsync(idParent.Value, cancellationToken))
throw new ArgumentInvalidException("Родительской директории не существует", nameof(idParent));
var directory = new ManualDirectoryDto
{
Name = name,
IdParent = idParent,
};
if (await IsExistDirectoryAsync(directory, cancellationToken))
throw new ArgumentInvalidException("Директория с таким названием уже существует", name);
return await manualDirectoryRepository.InsertAsync(directory, cancellationToken);
}
public async Task UpdateDirectoryAsync(int id, string name, CancellationToken cancellationToken)
{
var directory = await manualDirectoryRepository.GetOrDefaultAsync(id, cancellationToken)
?? throw new ArgumentInvalidException($"Директории с Id: {id} не существует", nameof(id));
directory.Name = name;
if (await IsExistDirectoryAsync(directory, cancellationToken))
throw new ArgumentInvalidException("Директория с таким названием уже существует", name);
await manualDirectoryRepository.UpdateAsync(directory, cancellationToken);
}
public async Task<int> DeleteDirectoryAsync(int id, CancellationToken cancellationToken)
{
var directory = await manualDirectoryRepository.GetOrDefaultAsync(id, cancellationToken);
if (directory is null)
return 0;
var path = fileStorageRepository.MakeFilePath(directoryFiles, IdFileCategory.ToString(),
await BuildDirectoryPathAsync(id, cancellationToken));
try
{
fileStorageRepository.DeleteDirectory(path, true);
}
catch (InvalidOperationException ex)
{
throw new ArgumentInvalidException(ex.Message, nameof(id));
}
return await manualDirectoryRepository.DeleteAsync(directory.Id, cancellationToken);
}
public async Task<int> DeleteFileAsync(int id, CancellationToken cancellationToken)
{
var manual = await manualRepository.GetOrDefaultAsync(id, cancellationToken);
if (manual is null)
return 0;
var filePath = await BuildFilePathAsync(manual.IdDirectory, 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.IdDirectory, manual.Name, cancellationToken);
var fileStream = new FileStream(path, FileMode.Open);
return (fileStream, manual.Name);
}
private async Task<bool> IsExistDirectoryAsync(ManualDirectoryDto directory, CancellationToken cancellationToken)
{
var existingDirectory = await manualDirectoryRepository.GetOrDefaultAsync(directory.Name, directory.IdParent,
cancellationToken);
return existingDirectory is not null && directory.Id != existingDirectory.Id;
}
private async Task<string> BuildDirectoryPathAsync(int idDirectory, CancellationToken cancellationToken)
{
var directiories = await manualDirectoryRepository.GetAllAsync(cancellationToken);
var directory = directiories.FirstOrDefault(d => d.Id == idDirectory)
?? throw new ArgumentInvalidException($"Директории с Id: {idDirectory} не существует", nameof(idDirectory));
var pathSegments = new List<int> { directory.Id };
while (directory.IdParent.HasValue)
{
directory = directiories.FirstOrDefault(d => d.Id == directory.IdParent.Value);
pathSegments.Insert(0, directory.Id);
}
return string.Join("/", pathSegments);
}
private async Task<string> BuildFilePathAsync(int idDirectory, string name, CancellationToken cancellationToken)
{
var directoryPath = await BuildDirectoryPathAsync(idDirectory, cancellationToken);
return fileStorageRepository.MakeFilePath(directoryFiles, IdFileCategory.ToString(), Path.Combine(directoryPath, name));
}
}

View File

@ -0,0 +1,105 @@
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace AsbCloudWebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ManualController : ControllerBase
{
private readonly IManualCatalogService manualCatalogService;
private readonly IUserRepository userRepository;
public ManualController(IManualCatalogService manualCatalogService,
IUserRepository userRepository)
{
this.manualCatalogService = manualCatalogService;
this.userRepository = userRepository;
}
/// <summary>
/// Сохранение файла
/// </summary>
/// <param name="idDirectory">Id директории</param>
/// <param name="file">Загружаемый файл</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost]
[Permission]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> SaveFileAsync(int idDirectory,
[Required] IFormFile file,
CancellationToken cancellationToken)
{
var idUser = User.GetUserId();
if(!idUser.HasValue)
throw new ForbidException("Не удается вас опознать");
AssertUserHasAccessToManual("Manual.edit");
using var fileStream = file.OpenReadStream();
var id = await manualCatalogService.SaveFileAsync(idDirectory, idUser.Value, file.FileName, fileStream, cancellationToken);
return Ok(id);
}
/// <summary>
/// Получение файла
/// </summary>
/// <param name="id">Id инструкции</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("{id:int}")]
[Permission]
[ProducesResponseType(typeof(PhysicalFileResult), StatusCodes.Status200OK, "application/octet-stream")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetFileAsync(int id, CancellationToken cancellationToken)
{
AssertUserHasAccessToManual("Manual.get");
var file = await manualCatalogService.GetFileAsync(id, cancellationToken);
if (!file.HasValue)
return NoContent();
return File(file.Value.stream, "application/octet-stream", file.Value.fileName);
}
/// <summary>
/// Удаление файла
/// </summary>
/// <param name="id">Id инструкции</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpDelete]
[Permission]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> DeleteFileAsync(int id, CancellationToken cancellationToken)
{
AssertUserHasAccessToManual("Manual.edit");
return Ok(await manualCatalogService.DeleteFileAsync(id, cancellationToken));
}
private void AssertUserHasAccessToManual(string permissionName)
{
var idUser = User.GetUserId();
if (!idUser.HasValue || !userRepository.HasPermission(idUser.Value, permissionName))
throw new ForbidException("У вас недостаточно прав");
}
}

View File

@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.Manuals;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace AsbCloudWebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ManualDirectoryController : ControllerBase
{
private readonly IManualDirectoryRepository manualDirectoryRepository;
private readonly IManualCatalogService manualCatalogService;
private readonly IUserRepository userRepository;
public ManualDirectoryController(IManualDirectoryRepository manualDirectoryRepository,
IManualCatalogService manualCatalogService,
IUserRepository userRepository)
{
this.manualDirectoryRepository = manualDirectoryRepository;
this.manualCatalogService = manualCatalogService;
this.userRepository = userRepository;
}
/// <summary>
/// Создание директории
/// </summary>
/// <param name="name">Название</param>
/// <param name="idParent">Необязательный параметр. Id родительской директории</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost]
[Permission]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> AddDirectoryAsync(string name, int? idParent, CancellationToken cancellationToken)
{
AssertUserHasAccessToManualDirectory("Manual.edit");
return Ok(await manualCatalogService.AddDirectoryAsync(name, idParent, cancellationToken));
}
/// <summary>
/// Обновление директории
/// </summary>
/// <param name="id"></param>
/// <param name="name">Новое название директории</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPut]
[Permission]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> UpdateDirectoryAsync(int id, string name, CancellationToken cancellationToken)
{
AssertUserHasAccessToManualDirectory("Manual.edit");
await manualCatalogService.UpdateDirectoryAsync(id, name, cancellationToken);
return Ok();
}
/// <summary>
/// Удаление директории
/// </summary>
/// <param name="id">Идентификатор директории</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpDelete]
[Permission]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> DeleteDirectoryAsync(int id, CancellationToken cancellationToken)
{
AssertUserHasAccessToManualDirectory("Manual.edit");
return Ok(await manualCatalogService.DeleteDirectoryAsync(id, cancellationToken));
}
/// <summary>
/// Получение дерева категорий
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Permission]
[ProducesResponseType(typeof(IEnumerable<ManualDirectoryDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
{
AssertUserHasAccessToManualDirectory("Manual.get");
return Ok(await manualDirectoryRepository.GetTreeAsync(cancellationToken));
}
private void AssertUserHasAccessToManualDirectory(string permissionName)
{
var idUser = User.GetUserId();
if (!idUser.HasValue || !userRepository.HasPermission(idUser.Value, permissionName))
throw new ForbidException("У вас недостаточно прав");
}
}

View File

@ -27,6 +27,7 @@
"supportMail": "support@digitaldrilling.ru" "supportMail": "support@digitaldrilling.ru"
}, },
"DirectoryNameHelpPageFiles": "helpPages", "DirectoryNameHelpPageFiles": "helpPages",
"DirectoryManualFiles": "manuals",
"Urls": "http://0.0.0.0:5000" //;https://0.0.0.0:5001" //, "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 // See https man: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-6.0
//"Kestrel": { //"Kestrel": {