Skip to content

Commit 8db8819

Browse files
bygu4DedSec256
andauthored
Добавить перенос файлов при создании курса из шаблона (#610)
* add method for adding existing file reference to the file record repository * add file transfer method to ContentService * implement files transfer on course creation inside a transaction * add ContentService client on startup * debug and refactor: use List instead of Dictionary for serialization, move CourseUnitType to Common * add models for handling file transfer * optimize files transfer in FIleRecordRepository: get files info by course id. Refactor * reset course loading flag on error * handle possible references to the same file in FileRecordRepository * change naming, rename TransferFiles models to CourseFilesTransfer * handle possible undefined mapping keys in FileRecordRepository * rename GetFilesTransferDTO to GetCourseFilesTransfer * change GetCourseFilesTransfer signature * docker fix * refactoring * revert * fixes + local mode --------- Co-authored-by: Alexey.Berezhnykh <alexey.berezhnykh@jetbrains.com>
1 parent 65a2376 commit 8db8819

18 files changed

Lines changed: 205 additions & 32 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
3+
namespace HwProj.Models.ContentService.DTO
4+
{
5+
public class CourseFilesTransferDto
6+
{
7+
public long SourceCourseId { get; set; }
8+
public long TargetCourseId { get; set; }
9+
public List<ScopeMappingDto> HomeworksMapping { get; set; } = new List<ScopeMappingDto>();
10+
}
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace HwProj.Models.ContentService.DTO
2+
{
3+
public class ScopeMappingDto
4+
{
5+
public long Source { get; set; }
6+
public long Target { get; set; }
7+
}
8+
}

HwProj.ContentService/HwProj.ContentService.API/Controllers/FilesController.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFiles
3939
{
4040
var userId = Request.GetUserIdFromHeader();
4141
var scope = processFilesDto.FilesScope.ToScope();
42-
42+
4343
if (processFilesDto.DeletingFileIds.Count > 0)
4444
await _messageProducer.PushDeleteFilesMessages(scope, processFilesDto.DeletingFileIds, userId);
4545

@@ -53,7 +53,7 @@ public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFiles
5353
await _localFilesService.SaveFile(newFormFile.OpenReadStream(), localFilePath);
5454
_logger.LogInformation("Файл {FileName} успешно сохранён в локальное хранилище по пути {localFilePath}",
5555
newFormFile.FileName, localFilePath);
56-
56+
5757
var message = new UploadFileMessage(
5858
Scope: processFilesDto.FilesScope.ToScope(),
5959
LocalFilePath: localFilePath,
@@ -64,10 +64,10 @@ public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFiles
6464
);
6565
uploadFilesMessages.Add(message);
6666
}
67-
67+
6868
await _messageProducer.PushUploadFilesMessages(uploadFilesMessages);
6969
}
70-
70+
7171
return Ok();
7272
}
7373

@@ -86,7 +86,7 @@ public async Task<IActionResult> GetDownloadLink([FromQuery] long fileId)
8686
{
8787
var externalKey = await _filesInfoService.GetFileExternalKeyAsync(fileId);
8888
if (externalKey is null) return Ok(Result<string>.Failed("Файл не найден"));
89-
89+
9090
var downloadUrlResult = await _s3FilesService.GetDownloadUrl(externalKey);
9191
return Ok(downloadUrlResult);
9292
}
@@ -98,12 +98,20 @@ public async Task<IActionResult> GetFilesInfo(long courseId)
9898
var filesInfo = await _filesInfoService.GetFilesInfoAsync(courseId);
9999
return Ok(filesInfo);
100100
}
101-
101+
102102
[HttpGet("info/course/{courseId}/uploaded")]
103103
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
104104
public async Task<IActionResult> GetUploadedFilesInfo(long courseId)
105105
{
106106
var filesInfo = await _filesInfoService.GetFilesInfoAsync(courseId, FileStatus.ReadyToUse);
107107
return Ok(filesInfo);
108108
}
109-
}
109+
110+
[HttpPost("transfer")]
111+
[ProducesResponseType((int)HttpStatusCode.OK)]
112+
public async Task<IActionResult> TransferFilesFromCourse(CourseFilesTransferDto filesTransferDto)
113+
{
114+
await _filesInfoService.TransferFilesFromCourse(filesTransferDto);
115+
return Ok();
116+
}
117+
}

HwProj.ContentService/HwProj.ContentService.API/Extensions/ConfigurationExtensions.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace HwProj.ContentService.API.Extensions;
2222

2323
public static class ConfigurationExtensions
2424
{
25-
public static IServiceCollection ConfigureWithAWS(this IServiceCollection services,
25+
public static IServiceCollection ConfigureWithAWS(this IServiceCollection services, IHostEnvironment env,
2626
IConfigurationRoot configuration)
2727
{
2828
// Достаем конфигурацию удаленного хранилища
@@ -43,13 +43,22 @@ public static IServiceCollection ConfigureWithAWS(this IServiceCollection servic
4343
var connectionString = ConnectionString.GetConnectionString(configuration);
4444
services.AddDbContext<ContentContext>(options => options.UseSqlServer(connectionString));
4545
services.AddScoped<IFileRecordRepository, FileRecordRepository>();
46-
47-
services.ConfigureExternalStorageClient(externalStorageSection);
46+
47+
if (env.IsDevelopment())
48+
{
49+
services.AddSingleton<IS3FilesService, LocalTestingS3FilesService>();
50+
}
51+
else
52+
{
53+
services.ConfigureExternalStorageClient(externalStorageSection);
54+
services.AddSingleton<IS3FilesService, S3FilesService>();
55+
}
56+
4857
services.ConfigureChannelInfrastructure<IProcessFileMessage>();
4958

5059
// Регистрируем как синглтоны, чтобы использовать в MessageConsumer
5160
services.AddSingleton<IFileKeyService, FileKeyService>();
52-
services.AddSingleton<IS3FilesService, S3FilesService>();
61+
5362
services.AddSingleton<ILocalFilesService, LocalFilesService>();
5463

5564
services.AddScoped<IFilesInfoService, FilesInfoService>();

HwProj.ContentService/HwProj.ContentService.API/Extensions/MappingExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
using HwProj.ContentService.API.Models;
2+
using HwProj.ContentService.API.Models.Database;
23
using HwProj.ContentService.API.Models.Enums;
34
using HwProj.Models.ContentService.DTO;
45

56
namespace HwProj.ContentService.API.Extensions;
67

78
public static class MappingExtensions
89
{
10+
public static Scope ToScope(this FileToCourseUnit fileToCourseUnit)
11+
=> new Scope(
12+
CourseId: fileToCourseUnit.CourseId,
13+
CourseUnitId: fileToCourseUnit.CourseUnitId,
14+
CourseUnitType: fileToCourseUnit.CourseUnitType
15+
);
16+
917
public static Scope ToScope(this ScopeDTO scopeDTO)
1018
=> new Scope(
1119
CourseId: scopeDTO.CourseId,
@@ -20,4 +28,4 @@ public static ScopeDTO ToScopeDTO(this Scope scope)
2028
CourseUnitType = scope.CourseUnitType.ToString(),
2129
CourseUnitId = scope.CourseUnitId
2230
};
23-
}
31+
}

HwProj.ContentService/HwProj.ContentService.API/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using HwProj.ContentService.API.Services.Interfaces;
33

44
var builder = WebApplication.CreateBuilder(args);
5-
builder.Services.ConfigureWithAWS(builder.Configuration);
5+
builder.Services.ConfigureWithAWS(builder.Environment, builder.Configuration);
66

77
// Увеличиваем размер принимаемых запросов до 200 МБ
88
builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; });
@@ -15,7 +15,7 @@
1515
app.ConfigureWebApp();
1616

1717
// При необходимости создаем пустой бакет в хранилище
18-
await app.CreateBucketIfNotExists();
18+
if (!app.Environment.IsDevelopment()) await app.CreateBucketIfNotExists();
1919

2020
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
2121
lifetime.ApplicationStarted.Register(async () =>

HwProj.ContentService/HwProj.ContentService.API/Repositories/FileRecordRepository.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using HwProj.ContentService.API.Models.Enums;
55
using Microsoft.EntityFrameworkCore;
66
using Microsoft.EntityFrameworkCore.Query;
7+
using HwProj.ContentService.API.Extensions;
78

89
namespace HwProj.ContentService.API.Repositories;
910

@@ -65,7 +66,7 @@ public async Task<List<FileToCourseUnit>> GetByCourseIdAsync(long courseId)
6566
.Where(fc => fc.CourseId == courseId)
6667
.Include(fc => fc.FileRecord)
6768
.ToListAsync();
68-
69+
6970
public async Task<List<FileToCourseUnit>> GetByCourseIdAndStatusAsync(long courseId, FileStatus filesStatus)
7071
=> await _contentContext.FileToCourseUnits
7172
.AsNoTracking()
@@ -140,10 +141,28 @@ await _contentContext.FileRecords
140141
return fileRecord.ReferenceCount;
141142
}
142143

144+
/// Переносит файлы с помощью добавления записей в FileToCourseUnit согласно переданному отображению.
145+
/// Увеличивает число ссылок на файлы на число добавленных записей, соответствующих файлу.
146+
public async Task AddFileUnitsAsync(List<FileToCourseUnit> unitsToAdd)
147+
{
148+
await using var transaction = await _contentContext.Database.BeginTransactionAsync();
149+
150+
await _contentContext.FileToCourseUnits.AddRangeAsync(unitsToAdd);
151+
foreach (var unit in unitsToAdd)
152+
{
153+
await UpdateAsync(
154+
unit.FileRecordId,
155+
setters => setters.SetProperty(x => x.ReferenceCount, x => x.ReferenceCount + 1));
156+
}
157+
158+
await _contentContext.SaveChangesAsync();
159+
await transaction.CommitAsync();
160+
}
161+
143162
public async Task UpdateAsync(long id,
144163
Expression<Func<SetPropertyCalls<FileRecord>, SetPropertyCalls<FileRecord>>> setPropertyCalls)
145164
=> await _contentContext.FileRecords
146165
.AsNoTracking()
147166
.Where(fr => fr.Id == id)
148167
.ExecuteUpdateAsync(setPropertyCalls);
149-
}
168+
}

HwProj.ContentService/HwProj.ContentService.API/Repositories/IFileRecordRepository.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ public Task UpdateAsync(long id,
1919
public Task<List<long>> GetIdsByStatusAsync(FileStatus status);
2020
public Task DeleteWithCourseUnitInfoAsync(long fileRecordId);
2121
public Task DeleteWithCourseUnitInfoAsync(List<long> fileRecordIds);
22+
public Task AddFileUnitsAsync(List<FileToCourseUnit> unitsToAdd);
2223
public Task<int> ReduceReferenceCountAsync(FileRecord fileRecord, Scope scope);
2324
}

HwProj.ContentService/HwProj.ContentService.API/Services/FilesInfoService.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using HwProj.ContentService.API.Extensions;
12
using HwProj.ContentService.API.Models;
3+
using HwProj.ContentService.API.Models.Database;
24
using HwProj.ContentService.API.Models.Enums;
35
using HwProj.ContentService.API.Repositories;
46
using HwProj.ContentService.API.Services.Interfaces;
@@ -62,4 +64,31 @@ public async Task<List<FileInfoDTO>> GetFilesInfoAsync(long courseId, FileStatus
6264
CourseUnitId = fcu.CourseUnitId
6365
}).ToList();
6466
}
65-
}
67+
68+
public async Task TransferFilesFromCourse(CourseFilesTransferDto filesTransfer)
69+
{
70+
var map = filesTransfer.HomeworksMapping.ToDictionary(
71+
x => new Scope(filesTransfer.SourceCourseId, CourseUnitType.Homework, x.Source),
72+
x => new Scope(filesTransfer.TargetCourseId, CourseUnitType.Homework, x.Target)
73+
);
74+
75+
var sourceCourseUnits = await _fileRecordRepository.GetByCourseIdAsync(filesTransfer.SourceCourseId);
76+
var unitsToAdd = sourceCourseUnits
77+
.Select(unit => (unit.FileRecord, Scope: unit.ToScope()))
78+
.Where(pair => map.ContainsKey(pair.Scope))
79+
.Select(pair =>
80+
{
81+
var targetScope = map[pair.Scope];
82+
return new FileToCourseUnit
83+
{
84+
FileRecordId = pair.FileRecord.Id,
85+
CourseId = targetScope.CourseId,
86+
CourseUnitId = targetScope.CourseUnitId,
87+
CourseUnitType = targetScope.CourseUnitType
88+
};
89+
})
90+
.ToList();
91+
92+
await _fileRecordRepository.AddFileUnitsAsync(unitsToAdd);
93+
}
94+
}

HwProj.ContentService/HwProj.ContentService.API/Services/Interfaces/IFilesInfoService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ public interface IFilesInfoService
1010
public Task<string?> GetFileExternalKeyAsync(long fileId);
1111
public Task<List<FileInfoDTO>> GetFilesInfoAsync(long courseId);
1212
public Task<List<FileInfoDTO>> GetFilesInfoAsync(long courseId, FileStatus filesStatus);
13-
}
13+
public Task TransferFilesFromCourse(CourseFilesTransferDto filesTransfer);
14+
}

0 commit comments

Comments
 (0)