Skip to content

Commit f57bee4

Browse files
authored
Улучшения механизма работы с файлами (#519)
* implement "unavailable content service" support with displaying warnings to user ; optimize files loading process in the edit homework form * [backend] optimize files microservice: move courseMentor validation from APIGateway to microservice controller * [front] fix files styling * [backend] refactor content microservice: simplify CreateBucket method * [frontend] improve errors reporting functionality * refactor: update warning messages * update run configuration: add ContentService.API component * refactor: move CourseMentorOnlyAttribute filter to APIGateway * update api.ts
1 parent 9ad7a8a commit f57bee4

File tree

20 files changed

+244
-144
lines changed

20 files changed

+244
-144
lines changed

.run/StartAll.run.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<configuration default="false" name="StartAll" type="CompoundRunConfigurationType">
33
<toRun name="HwProj.APIGateway.API" type="LaunchSettings" />
44
<toRun name="HwProj.AuthService.API" type="LaunchSettings" />
5+
<toRun name="HwProj.ContentService.API" type="LaunchSettings" />
56
<toRun name="HwProj.CoursesService.API" type="LaunchSettings" />
67
<toRun name="HwProj.NotificationsService.API" type="LaunchSettings" />
78
<toRun name="HwProj.SolutionsService.API" type="LaunchSettings" />

HwProj.APIGateway/HwProj.APIGateway.API/Controllers/FilesController.cs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
using System.Linq;
21
using System.Net;
32
using System.Threading.Tasks;
3+
using HwProj.APIGateway.API.Filters;
44
using HwProj.AuthService.Client;
55
using HwProj.ContentService.Client;
6-
using HwProj.CoursesService.Client;
76
using HwProj.Models.ContentService.DTO;
87
using HwProj.Models.Roles;
98
using Microsoft.AspNetCore.Authorization;
@@ -17,28 +16,25 @@ namespace HwProj.APIGateway.API.Controllers
1716
public class FilesController : AggregationController
1817
{
1918
private readonly IContentServiceClient _contentServiceClient;
20-
private readonly ICoursesServiceClient _coursesServiceClient;
2119

2220
public FilesController(IAuthServiceClient authServiceClient,
23-
IContentServiceClient contentServiceClient,
24-
ICoursesServiceClient coursesServiceClient) : base(authServiceClient)
21+
IContentServiceClient contentServiceClient) : base(authServiceClient)
2522
{
2623
_contentServiceClient = contentServiceClient;
27-
_coursesServiceClient = coursesServiceClient;
2824
}
2925

3026
[HttpPost("upload")]
3127
[Authorize(Roles = Roles.LecturerRole)]
28+
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
3229
[ProducesResponseType((int)HttpStatusCode.OK)]
3330
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
31+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
3432
public async Task<IActionResult> Upload([FromForm] UploadFileDTO uploadFileDto)
3533
{
36-
var courseLecturersIds = await _coursesServiceClient.GetCourseLecturersIds(uploadFileDto.CourseId);
37-
if (!courseLecturersIds.Contains(UserId))
38-
return BadRequest("Пользователь с такой почтой не является преподавателем курса");
39-
4034
var result = await _contentServiceClient.UploadFileAsync(uploadFileDto);
41-
return result.Succeeded ? Ok() as IActionResult : BadRequest(result.Errors);
35+
return result.Succeeded
36+
? Ok() as IActionResult
37+
: StatusCode((int)HttpStatusCode.ServiceUnavailable, result.Errors);
4238
}
4339

4440
[HttpGet("downloadLink")]
@@ -54,25 +50,23 @@ public async Task<IActionResult> GetDownloadLink([FromQuery] string key)
5450

5551
[HttpGet("filesInfo/{courseId}")]
5652
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
53+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
5754
public async Task<IActionResult> GetFilesInfo(long courseId, [FromQuery] long? homeworkId = null)
5855
{
59-
var filesInfo = await _contentServiceClient.GetFilesInfo(courseId, homeworkId);
60-
return Ok(filesInfo);
56+
var filesInfoResult = await _contentServiceClient.GetFilesInfo(courseId, homeworkId);
57+
return filesInfoResult.Succeeded
58+
? Ok(filesInfoResult.Value) as IActionResult
59+
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesInfoResult.Errors);
6160
}
6261

6362
[HttpDelete]
6463
[Authorize(Roles = Roles.LecturerRole)]
64+
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
6565
[ProducesResponseType((int)HttpStatusCode.OK)]
6666
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
67-
public async Task<IActionResult> DeleteFile([FromQuery] string key)
67+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.NotFound)]
68+
public async Task<IActionResult> DeleteFile([FromQuery] long courseId, [FromQuery] string key)
6869
{
69-
var courseIdResult = await _contentServiceClient.GetCourseIdFromKeyAsync(key);
70-
if (!courseIdResult.Succeeded) return BadRequest(courseIdResult.Errors);
71-
72-
var courseLecturersIds = await _coursesServiceClient.GetCourseLecturersIds(courseIdResult.Value);
73-
if (!courseLecturersIds.Contains(UserId))
74-
return BadRequest("Пользователь с такой почтой не является преподавателем курса");
75-
7670
var deletionResult = await _contentServiceClient.DeleteFileAsync(key);
7771
return deletionResult.Succeeded
7872
? Ok() as IActionResult
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using HwProj.CoursesService.Client;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.AspNetCore.Mvc.Filters;
7+
8+
namespace HwProj.APIGateway.API.Filters
9+
{
10+
public class CourseMentorOnlyAttribute : ActionFilterAttribute
11+
{
12+
private readonly ICoursesServiceClient _coursesServiceClient;
13+
14+
public CourseMentorOnlyAttribute(ICoursesServiceClient coursesServiceClient)
15+
{
16+
_coursesServiceClient = coursesServiceClient;
17+
}
18+
19+
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
20+
{
21+
var userId = context.HttpContext.User.Claims
22+
.FirstOrDefault(claim => claim.Type.ToString() == "_id")?.Value;
23+
if (userId == null)
24+
{
25+
context.Result = new ContentResult
26+
{
27+
StatusCode = StatusCodes.Status403Forbidden,
28+
Content = "В запросе не передан идентификатор пользователя",
29+
ContentType = "application/json"
30+
};
31+
return;
32+
}
33+
34+
string[]? mentorIds = null;
35+
36+
var courseId = GetValueFromRequest(context.HttpContext.Request, "courseId");
37+
if (courseId != null && long.TryParse(courseId, out var id))
38+
{
39+
mentorIds = await _coursesServiceClient.GetCourseLecturersIds(id);
40+
}
41+
42+
if (mentorIds == null || !mentorIds.Contains(userId))
43+
{
44+
context.Result = new ContentResult
45+
{
46+
StatusCode = StatusCodes.Status403Forbidden,
47+
Content = "Недостаточно прав для работы с файлами: Вы не являетесь ментором на курсе",
48+
ContentType = "application/json"
49+
};
50+
return;
51+
}
52+
53+
await next.Invoke();
54+
}
55+
56+
private static string? GetValueFromRequest(HttpRequest request, string key)
57+
{
58+
if (request.Query.TryGetValue(key, out var queryValue))
59+
return queryValue.ToString();
60+
61+
if (request.HasFormContentType && request.Form.TryGetValue(key, out var formValue))
62+
return formValue.ToString();
63+
64+
return null;
65+
}
66+
}
67+
}

HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using HwProj.SolutionsService.Client;
66
using HwProj.Utils.Auth;
77
using HwProj.Utils.Configuration;
8+
using HwProj.APIGateway.API.Filters;
89
using Microsoft.AspNetCore.Builder;
910
using Microsoft.AspNetCore.Hosting;
1011
using Microsoft.AspNetCore.Http.Features;
@@ -56,6 +57,8 @@ public void ConfigureServices(IServiceCollection services)
5657
services.AddSolutionServiceClient();
5758
services.AddNotificationsServiceClient();
5859
services.AddContentServiceClient();
60+
61+
services.AddScoped<CourseMentorOnlyAttribute>();
5962
}
6063

6164
public void Configure(IApplicationBuilder app, IHostingEnvironment env)

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using HwProj.ContentService.API.Services;
2+
using HwProj.Models.ContentService.DTO;
23
using HwProj.Utils.Authorization;
34
using Microsoft.AspNetCore.Mvc;
4-
using UploadFileDTO = HwProj.Models.ContentService.DTO.UploadFileDTO;
55

66
namespace HwProj.ContentService.API.Controllers;
77

@@ -10,7 +10,6 @@ namespace HwProj.ContentService.API.Controllers;
1010
public class FilesController : ControllerBase
1111
{
1212
private readonly IFilesService _filesService;
13-
private readonly IFileKeyService _fileKeyService;
1413

1514
public FilesController(IFilesService filesService)
1615
{

HwProj.ContentService/HwProj.ContentService.API/Extensions/AmazonS3Extensions.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,18 @@ public static class AmazonS3Extensions
88
{
99
public static async Task CreateBucketIfNotExists(this IAmazonS3 amazonS3Client, string bucketName)
1010
{
11-
var doesBucketExist = false;
12-
try
11+
if (!await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, bucketName))
1312
{
14-
doesBucketExist = await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, bucketName);
15-
}
16-
finally
17-
{
18-
if (!doesBucketExist)
13+
try
14+
{
15+
await amazonS3Client.PutBucketAsync(bucketName);
16+
}
17+
catch (AmazonS3Exception exception)
1918
{
20-
var result = await amazonS3Client.PutBucketAsync(bucketName);
21-
if (result.HttpStatusCode != System.Net.HttpStatusCode.OK)
22-
{
23-
throw new ApplicationException($"Не удалось создать бакет {bucketName} для хранения данных");
24-
}
19+
var errorMessage = $"Не удалось создать бакет для хранения данных {bucketName}. " +
20+
$"Код ошибки: {exception.ErrorCode}. " +
21+
$"Сообщение: {exception.Message}";
22+
throw new ApplicationException(errorMessage, exception);
2523
}
2624
}
2725
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static IServiceCollection ConfigureWithAWS(this IServiceCollection servic
3131
services.ConfigureStorageClient(clientConfigurationSection);
3232
services.AddSingleton<IFileKeyService, FileKeyService>();
3333
services.AddScoped<IFilesService, FilesService>();
34+
3435
services.AddHttpClient();
3536

3637
services.ConfigureHwProjContentService();

HwProj.ContentService/HwProj.ContentService.API/HwProj.ContentService.API.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
<ItemGroup>
2020
<ProjectReference Include="..\..\HwProj.Common\HwProj.Utils\HwProj.Utils.csproj" />
2121
<ProjectReference Include="..\..\HwProj.Common\HwProj.Models\HwProj.Models.csproj" />
22+
<ProjectReference Include="..\..\HwProj.CoursesService\HwProj.CoursesService.Client\HwProj.CoursesService.Client.csproj" />
2223
</ItemGroup>
2324
</Project>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using HwProj.ContentService.API.Extensions;
2-
using Microsoft.AspNetCore.Http.Features;
32

43
var builder = WebApplication.CreateBuilder(args);
54
builder.Services.ConfigureWithAWS(builder.Configuration);

HwProj.ContentService/HwProj.ContentService.Client/ContentServiceClient.cs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ public async Task<Result> UploadFileAsync(UploadFileDTO uploadFileDto)
4141
httpRequest.Content = multipartContent;
4242
httpRequest.TryAddUserId(_httpContextAccessor);
4343

44-
var response = await _httpClient.SendAsync(httpRequest);
45-
return await response.DeserializeAsync<Result>();
44+
try
45+
{
46+
var response = await _httpClient.SendAsync(httpRequest);
47+
return await response.DeserializeAsync<Result>();
48+
}
49+
catch (HttpRequestException e)
50+
{
51+
return Result.Failed("Пока не можем сохранить файлы. Попробуйте повторить позже");
52+
}
4653
}
4754

4855
public async Task<Result<string>> GetDownloadLinkAsync(string fileKey)
@@ -56,7 +63,7 @@ public async Task<Result<string>> GetDownloadLinkAsync(string fileKey)
5663
return await response.DeserializeAsync<Result<string>>();
5764
}
5865

59-
public async Task<FileInfoDTO[]> GetFilesInfo(long courseId, long? homeworkId = null)
66+
public async Task<Result<FileInfoDTO[]>> GetFilesInfo(long courseId, long? homeworkId = null)
6067
{
6168
var url = _contentServiceUri + $"api/Files/filesInfo/{courseId}";
6269
if (homeworkId.HasValue)
@@ -65,9 +72,17 @@ public async Task<FileInfoDTO[]> GetFilesInfo(long courseId, long? homeworkId =
6572
}
6673

6774
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
68-
69-
var response = await _httpClient.SendAsync(httpRequest);
70-
return await response.DeserializeAsync<FileInfoDTO[]>();
75+
try
76+
{
77+
var response = await _httpClient.SendAsync(httpRequest);
78+
var filesInfo = await response.DeserializeAsync<FileInfoDTO[]>();
79+
return Result<FileInfoDTO[]>.Success(filesInfo);
80+
}
81+
catch (HttpRequestException e)
82+
{
83+
return Result<FileInfoDTO[]>.Failed("Пока не можем получить информацию о файлах. " +
84+
"\nВсе ваши данные сохранены — попробуйте повторить позже");
85+
}
7186
}
7287

7388
public async Task<Result> DeleteFileAsync(string fileKey)
@@ -81,16 +96,5 @@ public async Task<Result> DeleteFileAsync(string fileKey)
8196
var response = await _httpClient.SendAsync(httpRequest);
8297
return await response.DeserializeAsync<Result>();
8398
}
84-
85-
public async Task<Result<long>> GetCourseIdFromKeyAsync(string fileKey)
86-
{
87-
var encodedFileKey = Uri.EscapeDataString(fileKey);
88-
using var httpRequest = new HttpRequestMessage(
89-
HttpMethod.Get,
90-
_contentServiceUri + $"api/FileKey/courseId?key={encodedFileKey}");
91-
92-
var response = await _httpClient.SendAsync(httpRequest);
93-
return await response.DeserializeAsync<Result<long>>();
94-
}
9599
}
96100
}

0 commit comments

Comments
 (0)