diff --git a/AsbCloudApp/Data/FileInfoDto.cs b/AsbCloudApp/Data/FileInfoDto.cs index 63abba8f..badcd5a4 100644 --- a/AsbCloudApp/Data/FileInfoDto.cs +++ b/AsbCloudApp/Data/FileInfoDto.cs @@ -39,6 +39,11 @@ namespace AsbCloudApp.Data /// public long Size { get; set; } + /// + /// Помечен как удаленный + /// + public bool IsDeleted { get; set; } + /// /// DTO автора /// diff --git a/AsbCloudApp/Repositories/IFileRepository.cs b/AsbCloudApp/Repositories/IFileRepository.cs index e36d1001..a05cf909 100644 --- a/AsbCloudApp/Repositories/IFileRepository.cs +++ b/AsbCloudApp/Repositories/IFileRepository.cs @@ -1,42 +1,33 @@ -using AsbCloudApp.Data; +using AsbCloudApp.Requests; +using AsbCloudApp.Data; using AsbCloudApp.Services; -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace AsbCloudApp.Repositories { +#nullable enable /// /// Сервис доступа к файлам /// public interface IFileRepository : ICrudService { /// - /// Получить файлы определенной категории + /// Получение файлов по скважине /// - /// - /// + /// /// /// - Task> GetInfosByCategoryAsync(int idWell, int idCategory, CancellationToken token); + Task> GetInfosAsync(FileRequest request, CancellationToken token); /// /// Получить список файлов в контейнере /// - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// - Task> GetInfosAsync(int idWell, - int idCategory, string companyName = default, string fileName = default, DateTime begin = default, - DateTime end = default, int skip = 0, int take = 32, CancellationToken token = default); + Task> GetInfosPaginatedAsync(FileRequest request, CancellationToken token = default); /// /// Пометить файл как удаленный @@ -87,12 +78,6 @@ namespace AsbCloudApp.Repositories /// Task MarkFileMarkAsDeletedAsync(IEnumerable idsMarks, CancellationToken token); - /// - /// Получение файлов по скважине - /// - /// - /// - /// - Task> GetInfosByWellIdAsync(int idWell, CancellationToken token); } +#nullable disable } diff --git a/AsbCloudApp/Repositories/IFileStorageRepository.cs b/AsbCloudApp/Repositories/IFileStorageRepository.cs index 9d821861..0d867db0 100644 --- a/AsbCloudApp/Repositories/IFileStorageRepository.cs +++ b/AsbCloudApp/Repositories/IFileStorageRepository.cs @@ -1,25 +1,23 @@ -using System.IO; +using AsbCloudApp.Data; +using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; namespace AsbCloudApp.Repositories { +#nullable enable /// /// Репозиторий хранения фалов /// public interface IFileStorageRepository { - /// - /// Директория хранения файлов - /// - string RootPath { get; } - /// /// Получение длинны фала и проверка его наличия, если отсутствует падает исключение /// /// /// - long GetLengthFile(string srcFilePath); + long GetFileLength(string srcFilePath); /// /// Перемещение файла @@ -31,21 +29,52 @@ namespace AsbCloudApp.Repositories /// /// Копирование файла /// + /// + /// + /// /// - Task CopyFileAsync(string filePath, Stream fileStream, CancellationToken token); + Task SaveFileAsync(string filePathRec, Stream fileStreamSrc, CancellationToken token); /// /// Удаление файла /// - /// - void DeleteFile(string fileName); + /// + void DeleteFile(IEnumerable filesName); /// - /// Проверка наличия файла + /// Удаление всех файлов с диска о которых нет информации в базе /// - /// - /// + /// + /// + int DeleteFilesNotInList(int idWell, IEnumerable idsFiles); + + /// + /// Вывод списка всех файлов из базы, для которых нет файла на диске + /// + /// + /// /// - bool FileExists(string fullPath, string fileName); + IEnumerable GetListFilesNotDisc(IEnumerable files); + + /// + /// Получение пути к файлу + /// + /// + /// + /// + /// + /// + string MakeFilePath(int idWell, int idCategory, string fileFullName, int fileId); + + /// + /// Получить путь для скачивания + /// + /// + /// + /// + /// + /// + string GetUrl(int idWell, int idCategory, int idFile, string dotExtention); } +#nullable disable } diff --git a/AsbCloudApp/Requests/FileRequest.cs b/AsbCloudApp/Requests/FileRequest.cs new file mode 100644 index 00000000..14a799e0 --- /dev/null +++ b/AsbCloudApp/Requests/FileRequest.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace AsbCloudApp.Requests +{ +#nullable enable + /// + /// Параметры запроса для файлового сервиса + /// + public class FileRequest : RequestBase + { + /// + /// Идентификатор скважины + /// + [Required] + public int IdWell { get; set; } + + /// + /// Идентификатор категории файла + /// + [Required] + public int? IdCategory { get; set; } + + /// + /// Наименование компании + /// + public string? CompanyNamePart { get; set; } + + /// + /// Имя файла + /// + public string? FileNamePart { get; set; } + + /// + /// Дата начала периода + /// + public DateTime? Begin { get; set; } + + /// + /// Дата окончания периода + /// + public DateTime? End { get; set; } + + /// + /// Признак удаления + /// + public bool? IsDeleted { get; set; } + } +#nullable disable +} diff --git a/AsbCloudApp/Services/FileService.cs b/AsbCloudApp/Services/FileService.cs index b27c4383..3ba250aa 100644 --- a/AsbCloudApp/Services/FileService.cs +++ b/AsbCloudApp/Services/FileService.cs @@ -1,6 +1,6 @@ using AsbCloudApp.Data; using AsbCloudApp.Repositories; -using System; +using AsbCloudApp.Requests; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,6 +9,7 @@ using System.Threading.Tasks; namespace AsbCloudApp.Services { +#nullable enable /// /// Сервис доступа к файлам /// @@ -38,12 +39,12 @@ namespace AsbCloudApp.Services /// /// /// - public async Task MoveAsync(int idWell, int? idUser, int idCategory, + public async Task MoveAsync(int idWell, int? idUser, int idCategory, string destinationFileName, string srcFilePath, CancellationToken token = default) { destinationFileName = Path.GetFileName(destinationFileName); srcFilePath = Path.GetFullPath(srcFilePath); - var fileSize = fileStorageRepository.GetLengthFile(srcFilePath); + var fileSize = fileStorageRepository.GetFileLength(srcFilePath); //save info to db var dto = new FileInfoDto { @@ -56,10 +57,10 @@ namespace AsbCloudApp.Services var fileId = await fileRepository.InsertAsync(dto, token) .ConfigureAwait(false); - string filePath = MakeFilePath(idWell, idCategory, destinationFileName, fileId); + string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, destinationFileName, fileId); fileStorageRepository.MoveFile(srcFilePath, filePath); - - return await GetInfoAsync(fileId, token); + + return await GetOrDefaultAsync(fileId, token); } /// @@ -72,7 +73,7 @@ namespace AsbCloudApp.Services /// /// /// - public async Task SaveAsync(int idWell, int? idUser, int idCategory, + public async Task SaveAsync(int idWell, int? idUser, int idCategory, string fileFullName, Stream fileStream, CancellationToken token) { //save info to db @@ -82,37 +83,17 @@ namespace AsbCloudApp.Services IdAuthor = idUser, IdCategory = idCategory, Name = Path.GetFileName(fileFullName), - Size = fileStream?.Length ?? 0 + Size = fileStream.Length }; var fileId = await fileRepository.InsertAsync(dto, token) .ConfigureAwait(false); //save stream to disk - string filePath = MakeFilePath(idWell, idCategory, fileFullName, fileId); - await fileStorageRepository.CopyFileAsync(filePath, fileStream, token); + string filePath = fileStorageRepository.MakeFilePath(idWell, idCategory, fileFullName, fileId); + await fileStorageRepository.SaveFileAsync(filePath, fileStream, token); - return await GetInfoAsync(fileId, token); - } - - /// - /// Инфо о файле - /// - /// - /// - /// - public async Task GetInfoAsync(int idFile, - CancellationToken token) - { - var dto = await fileRepository.GetOrDefaultAsync(idFile, token).ConfigureAwait(false); - - var ext = Path.GetExtension(dto.Name); - - var relativePath = GetUrl(dto.IdWell, dto.IdCategory, dto.Id, ext); - var fullPath = Path.GetFullPath(relativePath); - fileStorageRepository.FileExists(fullPath, relativePath); - - return dto; + return await GetOrDefaultAsync(fileId, token); } /// @@ -140,27 +121,12 @@ namespace AsbCloudApp.Services if (files is null || !files.Any()) return 0; - foreach (var file in files) - { - var fileName = GetUrl(file.IdWell, file.IdCategory, file.Id, Path.GetExtension(file.Name)); - fileStorageRepository.DeleteFile(fileName); - } + var filesName = files.Select(x => GetUrl(x.IdWell, x.IdCategory, x.Id, Path.GetExtension(x.Name))); + fileStorageRepository.DeleteFile(filesName); return files.Any() ? 1 : 0; } - /// - /// получить путь для скачивания - /// - /// - /// - public async Task GetUrl(int idFile) - { - var fileInfo = await fileRepository.GetOrDefaultAsync(idFile, CancellationToken.None).ConfigureAwait(false); - - return GetUrl(fileInfo.IdWell, fileInfo.IdCategory, fileInfo.Id, Path.GetExtension(fileInfo.Name)); - } - /// /// получить путь для скачивания /// @@ -178,7 +144,7 @@ namespace AsbCloudApp.Services /// /// public string GetUrl(int idWell, int idCategory, int idFile, string dotExtention) => - Path.Combine(fileStorageRepository.RootPath, idWell.ToString(), idCategory.ToString(), $"{idFile}{dotExtention}"); + fileStorageRepository.GetUrl(idWell, idCategory, idFile, dotExtention); /// /// пометить метку файла как удаленную @@ -206,30 +172,28 @@ namespace AsbCloudApp.Services var ext = Path.GetExtension(entity.Name); var relativePath = GetUrl(entity.IdWell, entity.IdCategory, entity.Id, ext); var fullPath = Path.GetFullPath(relativePath); - fileStorageRepository.FileExists(fullPath, relativePath); } return result; } /// - /// Получить список файлов в контейнере + /// Получить файлы определенной категории /// - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// - public async Task> GetInfosAsync(int idWell, - int idCategory, string companyName = default, string fileName = default, DateTime begin = default, - DateTime end = default, int skip = 0, int take = 32, CancellationToken token = default) - => await fileRepository.GetInfosAsync(idWell, idCategory, companyName, fileName, begin, end, skip, take, token) - .ConfigureAwait(false); + public Task> GetInfosAsync(FileRequest request, CancellationToken token) + => fileRepository.GetInfosAsync(request, token); + + /// + /// Получить список файлов в контейнере + /// + /// + /// + /// + public Task> GetInfosPaginatedAsync(FileRequest request, CancellationToken token) + => fileRepository.GetInfosPaginatedAsync(request, token); /// /// Пометить файл как удаленный @@ -237,9 +201,8 @@ namespace AsbCloudApp.Services /// /// /// - public async Task MarkAsDeletedAsync(int idFile, CancellationToken token = default) - => await fileRepository.MarkAsDeletedAsync(idFile, token) - .ConfigureAwait(false); + public Task MarkAsDeletedAsync(int idFile, CancellationToken token = default) + => fileRepository.MarkAsDeletedAsync(idFile, token); /// /// добавить метку на файл @@ -248,9 +211,8 @@ namespace AsbCloudApp.Services /// /// /// - public async Task CreateFileMarkAsync(FileMarkDto fileMarkDto, int idUser, CancellationToken token) - => await fileRepository.CreateFileMarkAsync(fileMarkDto, idUser, token) - .ConfigureAwait(false); + public Task CreateFileMarkAsync(FileMarkDto fileMarkDto, int idUser, CancellationToken token) + => fileRepository.CreateFileMarkAsync(fileMarkDto, idUser, token); /// /// Получить запись по id @@ -258,9 +220,8 @@ namespace AsbCloudApp.Services /// /// /// - public async Task GetOrDefaultAsync(int id, CancellationToken token) - => await fileRepository.GetOrDefaultAsync(id, token) - .ConfigureAwait(false); + public Task GetOrDefaultAsync(int id, CancellationToken token) + => fileRepository.GetOrDefaultAsync(id, token); /// /// получить инфо о файле по метке @@ -268,45 +229,84 @@ namespace AsbCloudApp.Services /// /// /// - public async Task GetByMarkId(int idMark, CancellationToken token) - => await fileRepository.GetByMarkId(idMark, token) - .ConfigureAwait(false); + public Task GetByMarkId(int idMark, CancellationToken token) + => fileRepository.GetByMarkId(idMark, token); /// /// получить инфо о файле по метке /// - /// + /// /// /// - public async Task MarkFileMarkAsDeletedAsync(IEnumerable idsMarks, CancellationToken token) - => await fileRepository.MarkFileMarkAsDeletedAsync(idsMarks, token) - .ConfigureAwait(false); + public Task MarkFileMarkAsDeletedAsync(IEnumerable idsMarks, CancellationToken token) + => fileRepository.MarkFileMarkAsDeletedAsync(idsMarks, token); /// - /// Получение файлов по скважине + /// Удаление всех файлов по скважине помеченных как удаленные /// /// /// /// - public async Task> GetInfosByWellIdAsync(int idWell, CancellationToken token) - => await fileRepository.GetInfosByWellIdAsync(idWell, token) - .ConfigureAwait(false); - - /// - /// Получить файлы определенной категории - /// - /// - /// - /// - /// - public async Task> GetInfosByCategoryAsync(int idWell, int idCategory, CancellationToken token) - => await fileRepository.GetInfosByCategoryAsync(idWell, idCategory, token) - .ConfigureAwait(false); - - private string MakeFilePath(int idWell, int idCategory, string fileFullName, int fileId) + public async Task DeleteFilesFromDbMarkedDeletionByIdWell(int idWell, CancellationToken token) { - return Path.Combine(fileStorageRepository.RootPath, $"{idWell}", - $"{idCategory}", $"{fileId}" + $"{Path.GetExtension(fileFullName)}"); + var files = await fileRepository.GetInfosAsync( + new FileRequest + { + IdWell = idWell, + IsDeleted = true + }, + token); + var result = await DeleteAsync(files.Select(x => x.Id), token); + return result; + } + + /// + /// Удаление всех файлов с диска о которых нет информации в базе + /// + /// + /// + public async Task DeleteFilesNotExistStorage(int idWell, CancellationToken token) + { + var files = await fileRepository.GetInfosAsync( + new FileRequest + { + IdWell = idWell + }, + token); + var result = await Task.FromResult(fileStorageRepository.DeleteFilesNotInList(idWell, files.Select(x => x.Id))); + return result; + } + + /// + /// Вывод списка всех файлов из базы, для которых нет файла на диске + /// + /// + /// + /// + public async Task> GetListFilesNotDisc(int idWell, CancellationToken token) + { + var files = await fileRepository.GetInfosAsync( + new FileRequest + { + IdWell = idWell + }, + token); + var result = fileStorageRepository.GetListFilesNotDisc(files); + return result; + } + + /// + /// Получить файловый поток по идентификатору файла + /// + /// + /// + public Stream GetFileStream(FileInfoDto fileInfo) + { + var relativePath = GetUrl(fileInfo); + var fileStream = new FileStream(Path.GetFullPath(relativePath), FileMode.Open); + + return fileStream; } } +#nullable disable } diff --git a/AsbCloudDb/EFExtentions.cs b/AsbCloudDb/EFExtentions.cs index f0523294..3cbf3509 100644 --- a/AsbCloudDb/EFExtentions.cs +++ b/AsbCloudDb/EFExtentions.cs @@ -117,6 +117,17 @@ namespace AsbCloudDb } return stat; } + + public static IQueryable SkipTake(this IQueryable query, int? skip, int? take) + { + if (skip > 0) + query = query.Skip((int)skip); + + if (take > 0) + query = query.Take((int)take); + + return query; + } } interface IQueryStringFactory { } diff --git a/AsbCloudInfrastructure/Repository/FileRepository.cs b/AsbCloudInfrastructure/Repository/FileRepository.cs index 0cca5b8b..deb43445 100644 --- a/AsbCloudInfrastructure/Repository/FileRepository.cs +++ b/AsbCloudInfrastructure/Repository/FileRepository.cs @@ -1,20 +1,27 @@ using AsbCloudApp.Data; using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudApp.Services; +using AsbCloudDb; using AsbCloudDb.Model; +using AsbCloudInfrastructure.Services; +using DocumentFormat.OpenXml.Drawing.Charts; +using DocumentFormat.OpenXml.Wordprocessing; using Mapster; using Microsoft.EntityFrameworkCore; +using Org.BouncyCastle.Asn1.Ocsp; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository { +#nullable enable public class FileRepository : IFileRepository { - private readonly IQueryable dbSetConfigured; + private readonly IQueryable dbSetConfigured; private readonly IAsbCloudDbContext db; public FileRepository(IAsbCloudDbContext db) @@ -29,10 +36,55 @@ namespace AsbCloudInfrastructure.Repository .Include(f => f.Well); } - public async Task> GetInfosByCategoryAsync(int idWell, int idCategory, CancellationToken token) + private IQueryable BuildQuery(FileRequest request) { - var entities = await dbSetConfigured - .Where(e => e.IdWell == idWell && e.IdCategory == idCategory && e.IsDeleted == false) + var query = dbSetConfigured + .Where(e => e.IdWell == request.IdWell); + + double timezoneOffsetHours = query.FirstOrDefault() + ?.Well.Timezone.Hours ?? 5d; + + if (request.IdCategory is not null) + query = query.Where(x => x.IdCategory == request.IdCategory); + + if (request.IsDeleted is not null) + query = query.Where(x => x.IsDeleted == request.IsDeleted); + + if (request.CompanyNamePart is not null) + query = query.Where(e => e.Author.Company.Caption.ToLower().Contains(request.CompanyNamePart.ToLower())); + + if (request.FileNamePart is not null) + query = query.Where(e => e.Name.ToLower().Contains(request.FileNamePart.ToLower())); + + if (request.Begin is not null) + { + var beginUtc = request.Begin.Value.ToUtcDateTimeOffset(timezoneOffsetHours); + query = query.Where(e => e.UploadDate >= beginUtc); + } + + if (request.End is not null) + { + var endUtc = request.End.Value.ToUtcDateTimeOffset(timezoneOffsetHours); + query = query.Where(e => e.UploadDate <= endUtc); + } + + if (request?.SortFields?.Any() == true) + { + query = query.SortBy(request.SortFields); + } + else + query = query + .OrderBy(o => o.UploadDate); + + return query; + } + + public async Task> GetInfosAsync(FileRequest request, CancellationToken token) + { + var query = BuildQuery(request); + + var entities = await query + .SkipTake(request.Skip, request.Take) .AsNoTracking() .ToListAsync(token) .ConfigureAwait(false); @@ -41,76 +93,37 @@ namespace AsbCloudInfrastructure.Repository return dtos; } - public async Task> GetInfosAsync(int idWell, - int idCategory, string companyName = default, string fileName = default, DateTime begin = default, - DateTime end = default, int skip = 0, int take = 32, CancellationToken token = default) + public async Task> GetInfosPaginatedAsync(FileRequest request, CancellationToken token = default) { - var query = dbSetConfigured - .Where(e => e.IdWell == idWell && - e.IdCategory == idCategory && - !e.IsDeleted); + var skip = request.Skip ?? 0; + var take = request.Take ?? 32; - if (!string.IsNullOrEmpty(companyName)) - query = query.Where(e => (e.Author == null) || - (e.Author.Company == null) || - e.Author.Company.Caption.Contains(companyName)); + var query = BuildQuery(request); - if (!string.IsNullOrEmpty(fileName)) - query = query.Where(e => e.Name.ToLower().Contains(fileName.ToLower())); - - var firstFile = await query.FirstOrDefaultAsync(token); - if (firstFile is null) - return new PaginationContainer() - { - Skip = skip, - Take = take, - Count = 0, - }; - - var timezoneOffset = firstFile.Well.Timezone?.Hours ?? 5; - - if (begin != default) - { - var beginUtc = begin.ToUtcDateTimeOffset(timezoneOffset); - query = query.Where(e => e.UploadDate >= beginUtc); - } - - if (end != default) - { - var endUtc = end.ToUtcDateTimeOffset(timezoneOffset); - query = query.Where(e => e.UploadDate <= endUtc); - } - - var count = await query.CountAsync(token).ConfigureAwait(false); - - var result = new PaginationContainer(count) + var result = new PaginationContainer() { Skip = skip, Take = take, - Count = count, + Count = await query.CountAsync(token), }; - if (count <= skip) + if (result.Count == 0) return result; - query = query.OrderBy(e => e.UploadDate); - - if (skip > 0) - query = query.Skip(skip); - query = query.Take(take); - var entities = await query - .Take(take).AsNoTracking().ToListAsync(token) + .SkipTake(skip, take) + .AsNoTracking() + .ToListAsync(token) .ConfigureAwait(false); - var dtos = entities.Select(e => Convert(e, timezoneOffset)); - result.Items.AddRange(dtos); + result.Items = entities.Select(e => Convert(e)).ToList(); + return result; } public async Task> GetInfoByIdsAsync(IEnumerable idsFile, CancellationToken token) { - var result = new List(); + var result = Enumerable.Empty(); var entities = await dbSetConfigured .AsNoTracking() @@ -118,14 +131,8 @@ namespace AsbCloudInfrastructure.Repository .ToListAsync(token) .ConfigureAwait(false); - foreach (var entity in entities) - { - if (entity is null) - { - throw new FileNotFoundException($"fileId:{entity.Id} not found"); - } - result.Add(Convert(entity)); - } + if (entities is not null) + result = entities.Select(entity => Convert(entity)); return result; } @@ -163,7 +170,7 @@ namespace AsbCloudInfrastructure.Repository .FirstOrDefaultAsync(f => f.FileMarks.Any(m => m.Id == idMark), token) .ConfigureAwait(false); - FileInfoDto dto = Convert(entity); + FileInfoDto dto = Convert(entity!); return dto; } @@ -201,44 +208,13 @@ namespace AsbCloudInfrastructure.Repository return await db.SaveChangesAsync(token); } - public async Task> GetInfosByWellIdAsync(int idWell, CancellationToken token) - { - var entities = await dbSetConfigured - .Where(e => e.IdWell == idWell && e.IsDeleted == false) - .AsNoTracking() - .ToListAsync(token) - .ConfigureAwait(false); - - var dtos = entities.Select(e => Convert(e)); - return dtos; - } - - private static FileInfoDto Convert(AsbCloudDb.Model.FileInfo entity) - { - var timezoneOffset = entity.Well.Timezone?.Hours ?? 5; - return Convert(entity, timezoneOffset); - } - - private static FileInfoDto Convert(AsbCloudDb.Model.FileInfo entity, double timezoneOffset) - { - var dto = entity.Adapt(); - dto.UploadDate = entity.UploadDate.ToRemoteDateTime(timezoneOffset); - dto.FileMarks = entity.FileMarks.Select(m => - { - var mark = m.Adapt(); - mark.DateCreated = m.DateCreated.ToRemoteDateTime(timezoneOffset); - return mark; - }); - return dto; - } - public async Task> GetAllAsync(CancellationToken token) => await dbSetConfigured.AsNoTracking() .Select(x => Convert(x)) .ToListAsync(token) .ConfigureAwait(false); - public async Task GetOrDefaultAsync(int id, CancellationToken token) + public async Task GetOrDefaultAsync(int id, CancellationToken token) { var entity = await dbSetConfigured .AsNoTracking() @@ -246,24 +222,20 @@ namespace AsbCloudInfrastructure.Repository .ConfigureAwait(false); if (entity is null) - { - throw new FileNotFoundException($"fileId:{id} not found"); - } + return null; var dto = Convert(entity); return dto; } - public FileInfoDto GetOrDefault(int id) + public FileInfoDto? GetOrDefault(int id) { var entity = dbSetConfigured .AsNoTracking() .FirstOrDefault(f => f.Id == id); if (entity is null) - { - throw new FileNotFoundException($"fileId:{id} not found"); - } + return null; var dto = Convert(entity); return dto; @@ -271,7 +243,7 @@ namespace AsbCloudInfrastructure.Repository public async Task InsertAsync(FileInfoDto newItem, CancellationToken token) { - var fileInfo = new AsbCloudDb.Model.FileInfo() + var fileInfo = new FileInfo() { IdWell = newItem.IdWell, IdAuthor = newItem.IdAuthor, @@ -301,5 +273,25 @@ namespace AsbCloudInfrastructure.Repository { throw new NotImplementedException(); } + + private static FileInfoDto Convert(FileInfo entity) + { + var timezoneOffset = entity.Well.Timezone?.Hours ?? 5; + return Convert(entity, timezoneOffset); + } + + private static FileInfoDto Convert(FileInfo entity, double timezoneOffset) + { + var dto = entity.Adapt(); + dto.UploadDate = entity.UploadDate.ToRemoteDateTime(timezoneOffset); + dto.FileMarks = entity.FileMarks.Select(m => + { + var mark = m.Adapt(); + mark.DateCreated = m.DateCreated.ToRemoteDateTime(timezoneOffset); + return mark; + }); + return dto; + } } +#nullable disable } diff --git a/AsbCloudInfrastructure/Repository/FileStorageRepository.cs b/AsbCloudInfrastructure/Repository/FileStorageRepository.cs index 7466f3a3..8bd61e2f 100644 --- a/AsbCloudInfrastructure/Repository/FileStorageRepository.cs +++ b/AsbCloudInfrastructure/Repository/FileStorageRepository.cs @@ -1,38 +1,43 @@ -using AsbCloudApp.Exceptions; +using AsbCloudApp.Data; using AsbCloudApp.Repositories; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository { +#nullable enable public class FileStorageRepository : IFileStorageRepository { - public string RootPath { get; private set; } + /// + /// Директория хранения файлов + /// + private readonly string RootPath = "files"; public FileStorageRepository() { - RootPath = "files"; } - public async Task CopyFileAsync(string filePath, Stream fileStream, CancellationToken token) + public async Task SaveFileAsync(string filePathRec, Stream fileStreamSrc, CancellationToken token) { - CreateDirectory(filePath); - using var newfileStream = new FileStream(filePath, FileMode.Create); - await fileStream.CopyToAsync(newfileStream, token).ConfigureAwait(false); + CreateDirectory(filePathRec); + using var newfileStream = new FileStream(filePathRec, FileMode.Create); + await fileStreamSrc.CopyToAsync(newfileStream, token).ConfigureAwait(false); } - public void DeleteFile(string fileName) + public void DeleteFile(IEnumerable filesName) { - if (File.Exists(fileName)) - File.Delete(fileName); + foreach (var fileName in filesName) + { + if (File.Exists(fileName)) + File.Delete(fileName); + } } - public long GetLengthFile(string srcFilePath) + public long GetFileLength(string srcFilePath) { - if (!File.Exists(srcFilePath)) - throw new ArgumentInvalidException($"file {srcFilePath} doesn't exist", nameof(srcFilePath)); - var sysFileInfo = new FileInfo(srcFilePath); return sysFileInfo.Length; } @@ -43,17 +48,74 @@ namespace AsbCloudInfrastructure.Repository File.Move(srcFilePath, filePath); } - public bool FileExists(string fullPath, string fileName) + public string MakeFilePath(int idWell, int idCategory, string fileFullName, int fileId) { - if (!File.Exists(fullPath)) - throw new FileNotFoundException("not found", fileName); - - return true; + return Path.Combine(RootPath, $"{idWell}", + $"{idCategory}", $"{fileId}" + $"{Path.GetExtension(fileFullName)}"); } - private void CreateDirectory(string filePath) + public int DeleteFilesNotInList(int idWell, IEnumerable idsFilesList) { - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + var allFilesPath = GetFilesPath(idWell); + var result = 0; + + foreach (var filePath in allFilesPath) + { + if (int.TryParse(Path.GetFileNameWithoutExtension(filePath), out int idFile) + || !idsFilesList.Any(x => x == idFile)) + { + File.Delete(filePath); + result++; + } + } + + return result; + } + + public IEnumerable GetListFilesNotDisc(IEnumerable files) + { + var resutl = new List(); + var groupFiles = files.GroupBy(x => x.IdWell); + + foreach (var itemGroupFiles in groupFiles) + { + var idsFilesStorage = GetIdsFiles(itemGroupFiles.Key); + foreach (var file in files) + { + if (!idsFilesStorage.Any(x => x == file.Id)) + resutl.Add(file); + } + } + + return resutl; + } + + public string GetUrl(int idWell, int idCategory, int idFile, string dotExtention) => + Path.Combine(RootPath, idWell.ToString(), idCategory.ToString(), $"{idFile}{dotExtention}"); + + private IEnumerable GetIdsFiles(int idWell) + { + var result = new List(); + var allFilesPath = GetFilesPath(idWell); + + foreach (var filePath in allFilesPath) + if(int.TryParse(Path.GetFileNameWithoutExtension(filePath), out int idFile)) + result.Add(idFile); + + return result; + } + + private IEnumerable GetFilesPath(int idWell) + { + var path = Path.Combine(RootPath, $"{idWell}"); + return Directory.GetFiles(path, "*.*", SearchOption.AllDirectories); + } + + private static void CreateDirectory(string filePath) + { + var directoryName = Path.GetDirectoryName(filePath)!; + Directory.CreateDirectory(directoryName); } } +#nullable disable } diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index ca0827b0..e926a886 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -282,7 +282,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram fileMarkDto.IdMarkType != idMarkTypeReject) throw new ArgumentInvalidException($"В этом методе допустимы только отметки о принятии или отклонении.", nameof(fileMarkDto)); - var fileInfo = await fileService.GetInfoAsync(fileMarkDto.IdFile, token) + var fileInfo = await fileService.GetOrDefaultAsync(fileMarkDto.IdFile, token) .ConfigureAwait(false); if (fileInfo is null) @@ -357,7 +357,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task NotifyPublisherOnFullAccepAsync(FileMarkDto fileMark, CancellationToken token) { - var file = await fileService.GetInfoAsync(fileMark.IdFile, token); + var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, token); var well = await wellService.GetOrDefaultAsync(file.IdWell, token); var user = file.Author; var factory = new DrillingMailBodyFactory(configuration); @@ -369,7 +369,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task NotifyPublisherOnRejectAsync(FileMarkDto fileMark, CancellationToken token) { - var file = await fileService.GetInfoAsync(fileMark.IdFile, token); + var file = await fileService.GetOrDefaultAsync(fileMark.IdFile, token); var well = await wellService.GetOrDefaultAsync(file.IdWell, token); var user = file.Author; var factory = new DrillingMailBodyFactory(configuration); @@ -473,7 +473,6 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram var well = await wellService.GetOrDefaultAsync(idWell, token); var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx"; var tempResultFilePath = Path.Combine(Path.GetTempPath(), "drillingProgram", resultFileName); - var mailService = new EmailService(backgroundWorker, configuration); async Task funcProgramMake(string id, CancellationToken token) { var contextOptions = new DbContextOptionsBuilder() diff --git a/AsbCloudInfrastructure/Services/WellFinalDocumentsService.cs b/AsbCloudInfrastructure/Services/WellFinalDocumentsService.cs index 62d82522..4d44f38c 100644 --- a/AsbCloudInfrastructure/Services/WellFinalDocumentsService.cs +++ b/AsbCloudInfrastructure/Services/WellFinalDocumentsService.cs @@ -1,5 +1,6 @@ using AsbCloudApp.Data; using AsbCloudApp.Exceptions; +using AsbCloudApp.Requests; using AsbCloudApp.Services; using AsbCloudDb.Model; using AsbCloudInfrastructure.Repository; @@ -96,7 +97,7 @@ namespace AsbCloudInfrastructure.Services .Select(g => g.Key); var files = (await fileService - .GetInfosByWellIdAsync(idWell, token) + .GetInfosAsync(new FileRequest { IdWell = idWell}, token) .ConfigureAwait(false)) .Where(f => categoriesIds.Contains(f.IdCategory)) .ToArray(); @@ -162,7 +163,12 @@ namespace AsbCloudInfrastructure.Services public async Task GetFilesHistoryByIdCategory(int idWell, int idCategory, CancellationToken token) { - var files = await fileService.GetInfosByCategoryAsync(idWell, idCategory, token).ConfigureAwait(false); + var request = new FileRequest + { + IdWell = idWell, + IdCategory = idCategory, + }; + var files = await fileService.GetInfosAsync(request, token).ConfigureAwait(false); return new WellFinalDocumentsHistoryDto { IdWell = idWell, diff --git a/AsbCloudInfrastructure/Services/_Readme.md b/AsbCloudInfrastructure/Services/_Readme.md new file mode 100644 index 00000000..d05ae29e --- /dev/null +++ b/AsbCloudInfrastructure/Services/_Readme.md @@ -0,0 +1,9 @@ +# Создание репозитория для сервися + +1. Создать интерфейс репозитория в AsbCloudApp.Services +2. Создать репозиторий в AsbCloudInfrastructure.Repository, наследоваться от созданного интерфейса, в нем добавить работу с БД +3. Добавить репозиторий в AsbCloudInfrastructure.DependencyInjection +4. Добавить в конструктор сервиса новый репозиторий и использовать его методы +5. Перенести сервис из AsbCloudInfrastructure.Services в AsbCloudApp.Services +6. Добавить или поправить тесты на изменяемый сервис используя AsbCloudWebApi.Tests.RepositoryFactory +7. В тестах сделать мок данных для репозитория \ No newline at end of file diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs index fa72df35..a1e275b9 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs @@ -220,7 +220,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests { ConfigureNotApproved(); fileServiceMock - .Setup(s => s.GetInfoAsync(It.IsAny(), It.IsAny())) + .Setup(s => s.GetOrDefaultAsync(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(file1002.Adapt())); fileServiceMock @@ -251,7 +251,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests { ConfigureNotApproved(); fileServiceMock - .Setup(s => s.GetInfoAsync(It.IsAny(), It.IsAny())) + .Setup(s => s.GetOrDefaultAsync(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(file1002.Adapt())); fileServiceMock diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs index 025cc7bb..698f83cd 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs @@ -1,21 +1,15 @@ using AsbCloudApp.Data; using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; using AsbCloudApp.Services; -using AsbCloudDb.Model; -using AsbCloudInfrastructure.Repository; -using AsbCloudInfrastructure.Services; -using DocumentFormat.OpenXml.Spreadsheet; -using DocumentFormat.OpenXml.Wordprocessing; using Moq; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; -using static AsbCloudWebApi.Tests.TestHelpter; namespace AsbCloudWebApi.Tests.ServicesTests { @@ -115,15 +109,9 @@ namespace AsbCloudWebApi.Tests.ServicesTests return Task.FromResult(result); }); - repositoryMock.Setup(x => x.GetInfosByWellIdAsync(It.IsAny(), It.IsAny())) - .Returns((int idWell, CancellationToken token) => { - var data = Files.Where(x => x.IdWell == idWell); - return Task.FromResult(data); - }); - - repositoryMock.Setup(x => x.GetInfosByCategoryAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((int idWell, int idCategory, CancellationToken token) => { - var data = Files.Where(x => x.IdWell == idWell && x.IdCategory == idCategory); + repositoryMock.Setup(x => x.GetInfosAsync(It.IsAny(), It.IsAny())) + .Returns((FileRequest request, CancellationToken token) => { + var data = Files.Where(x => x.IdWell == request.IdWell); return Task.FromResult(data); }); @@ -139,8 +127,10 @@ namespace AsbCloudWebApi.Tests.ServicesTests }); var storageRepositoryMock = new Mock(); - - storageRepositoryMock.Setup(x => x.RootPath).Returns("files"); + storageRepositoryMock.Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((int idWell, int idCategory, int idFile, string dotExtention) => { + return Path.Combine("files", idWell.ToString(), idCategory.ToString(), $"{idFile}{dotExtention}"); + }); fileService = new FileService(repositoryMock.Object, storageRepositoryMock.Object); } @@ -153,9 +143,9 @@ namespace AsbCloudWebApi.Tests.ServicesTests } [Fact] - public async Task GetInfoAsync_returns_FileInfo() + public async Task GetOrDefaultAsync_returns_FileInfo() { - var data = await fileService.GetInfoAsync(1742, CancellationToken.None); + var data = await fileService.GetOrDefaultAsync(1742, CancellationToken.None); Assert.NotNull(data); } @@ -195,20 +185,6 @@ namespace AsbCloudWebApi.Tests.ServicesTests Assert.True(result > 0); } - [Fact] - public async Task GetInfosByWellIdAsync_returns_FileInfo() - { - var data = await fileService.GetInfosByWellIdAsync(90, CancellationToken.None); - Assert.NotNull(data); - } - - [Fact] - public async Task GetInfosByCategoryAsync_returns_FileInfo() - { - var data = await fileService.GetInfosByCategoryAsync(90, 10040, CancellationToken.None); - Assert.NotNull(data); - } - [Fact] public async Task CreateFileMarkAsync() { diff --git a/AsbCloudWebApi/Controllers/FileController.cs b/AsbCloudWebApi/Controllers/FileController.cs index 378b5a50..d4dfad33 100644 --- a/AsbCloudWebApi/Controllers/FileController.cs +++ b/AsbCloudWebApi/Controllers/FileController.cs @@ -1,17 +1,17 @@ using AsbCloudApp.Data; +using AsbCloudApp.Requests; using AsbCloudApp.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using AsbCloudDb.Model; namespace AsbCloudWebApi.Controllers { +#nullable enable /// /// Хранение файлов /// @@ -70,38 +70,27 @@ namespace AsbCloudWebApi.Controllers /// /// Возвращает информацию о файлах для скважины в выбраной категории /// - /// id скважины - /// id категории файла - /// id компаний для фильтрации возвращаемых файлов - /// часть имени файла для поиска - /// дата начала - /// дата окончания - /// для пагинации кол-во записей пропустить - /// для пагинации кол-во записей взять + /// /// Токен отмены задачи /// Список информации о файлах в этой категории [HttpGet] + [Route("/api/files")] [Permission] [ProducesResponseType(typeof(PaginationContainer), (int)System.Net.HttpStatusCode.OK)] public async Task GetFilesInfoAsync( - [FromRoute] int idWell, - int idCategory = default, - string companyName = default, - string fileName = default, - DateTime begin = default, - DateTime end = default, - int skip = 0, - int take = 32, + [FromQuery] FileRequest request, CancellationToken token = default) { int? idCompany = User.GetCompanyId(); - if (idCompany is null || !await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, - idWell, token).ConfigureAwait(false)) + if (request.IdCategory is null || idCompany is null) return Forbid(); - var filesInfo = await fileService.GetInfosAsync(idWell, idCategory, - companyName, fileName, begin, end, skip, take, token).ConfigureAwait(false); + if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, + request.IdWell, token).ConfigureAwait(false)) + return Forbid(); + + var filesInfo = await fileService.GetInfosPaginatedAsync(request, token).ConfigureAwait(false); return Ok(filesInfo); } @@ -110,36 +99,33 @@ namespace AsbCloudWebApi.Controllers /// Возвращает файл с диска на сервере /// /// id скважины - /// id запрашиваемого файла + /// id запрашиваемого файла /// Токен отмены задачи /// Запрашиваемый файл [HttpGet] - [Route("{fileId}")] + [Route("{idFile}")] [Permission] [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] public async Task GetFileAsync([FromRoute] int idWell, - int fileId, CancellationToken token = default) + int idFile, CancellationToken token = default) { int? idCompany = User.GetCompanyId(); if (idCompany is null) return Forbid(); + var fileInfo = await fileService.GetOrDefaultAsync(idFile, token); + + if (fileInfo is null) + return NotFound(idFile); + if (!await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, - idWell, token).ConfigureAwait(false)) + fileInfo.IdWell, token).ConfigureAwait(false)) return Forbid(); - try - { - var fileInfo = await fileService.GetInfoAsync(fileId, token); + var fileStream = fileService.GetFileStream(fileInfo); - var relativePath = fileService.GetUrl(fileInfo); - return PhysicalFile(Path.GetFullPath(relativePath), "application/octet-stream", fileInfo.Name); - } - catch (FileNotFoundException ex) - { - return NotFound(ex.FileName); - } + return File(fileStream, "application/octet-stream", fileInfo.Name); } /// @@ -161,13 +147,16 @@ namespace AsbCloudWebApi.Controllers int? idCompany = User.GetCompanyId(); - if (idCompany is null || !await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, + if (idUser is null || idCompany is null || !await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, idWell, token).ConfigureAwait(false)) return Forbid(); - var file = await fileService.GetInfoAsync((int)idFile, token); + var fileInfo = await fileService.GetOrDefaultAsync(idFile, token); - if (!userService.HasPermission((int)idUser, $"File.edit{file.IdCategory}")) + if (fileInfo is null) + return NotFound(idFile); + + if (!userService.HasPermission((int)idUser, $"File.edit{fileInfo?.IdCategory}")) return Forbid(); var result = await fileService.MarkAsDeletedAsync(idFile, token); @@ -254,4 +243,5 @@ namespace AsbCloudWebApi.Controllers } } } +#nullable disable }