Skip to content

Commit 8ca6d88

Browse files
semrosinDedSec256
andauthored
Поддержать механизм загрузки файлов в качестве решения к задаче со стороны студентов (#636)
* add: file uploader element to add solution page * add: files count validation * add: file type validation * fix: files count validation * fix: default lastSolution id * feat: files processing in taskSolutionPage * feat: files processing in studentSolutionPage * feat: make course files state universal * feat: add props in task solutions component ^ Conflicts: ^ hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx * feat: get files info for solutions in converter * feat: files preview in solution component * feat: processing files after adding solution * fix: add solution imports * feat: make edit files intarface exporting * fix: start processing after adding solution * feat: add solution privacy attribute * feat: add privacy attribute for processing * feat: add attributes to startup * feat: add lecturer or student role * feat: change processing validation (back) * feat: change get statuses validation (back) * feat: change download link validation (back) * feat: add scope dto with file id * feat: change download link api call (front) * fix: files preview without comment * feat: file type validation (back) * fix: front file type validation * feat: add files access for groups * refactor: make studentIds HashSet * feat: process files for groupmates * fix: dispose stream in back type validation * feat: separate access files functionality * fix: show solution files uploading status for students only * refactor: delete unused function in files accessor * fix: intervalRef usage in files accessor * fix: subscribe updating course files on course id * feat: update solutions components for files accessor * fix: padding after solution files * fix: return alien code * refactor: deleteunused variables, await with async calls * feat: [back] add class for privacy validation * feat [back]: method to get file scope * fix: delete attribute validation * fix: delete unused role * feat [back]: method to get files scope in info service * refactor [back]: return dto from files controller download link * refactor [back]: return dto from content client download link * feat [back]: add privacy filter to start up * feat [back]: file link dto * fix: download link request * feat [back]: privacy validation * refactor: delete unused validation attributes * refactor [front]: rename files upload waiter * feat [front]: variability for max files count * refactor [front]: course files access to upload waiter * fix [front]: rename usage of upload waiter * fix [front]: rename usages of download link getter * refactor [front]: unify unit files info getter * feat [front]: delete files saved status text * refactor [front]: delete unused files info array * refactor [front]: separate files handle logic * refactor [back]: type validation by foreign library * refactor [back]: scope usage in privacy filter * fix [back]: return with privacy error * feat [back]: add max files count filter * fix [front]: max files count showing * fix [back]: add files count limit to start up * feat [back]: showing max files count on limit exceeding * refactor: separate methods in privacy filter * refactor: create courseUnitType constans * fix: privacy filter * wip * wip * fix * fix * wip * wip --------- Co-authored-by: Alexey.Berezhnykh <alexey.berezhnykh@jetbrains.com>
1 parent 7fc1bf6 commit 8ca6d88

34 files changed

Lines changed: 840 additions & 549 deletions

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

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using HwProj.AuthService.Client;
55
using HwProj.ContentService.Client;
66
using HwProj.Models.ContentService.DTO;
7-
using HwProj.Models.Roles;
7+
using HwProj.Models.CourseUnitType;
88
using Microsoft.AspNetCore.Authorization;
99
using Microsoft.AspNetCore.Mvc;
1010

@@ -13,72 +13,79 @@ namespace HwProj.APIGateway.API.Controllers;
1313
[Route("api/[controller]")]
1414
[Authorize]
1515
[ApiController]
16-
public class FilesController : AggregationController
16+
public class FilesController(
17+
IAuthServiceClient authServiceClient,
18+
IContentServiceClient contentServiceClient,
19+
FilesPrivacyFilter privacyFilter,
20+
FilesCountLimiter filesCountLimiter)
21+
: AggregationController(authServiceClient)
1722
{
18-
private readonly IContentServiceClient _contentServiceClient;
19-
20-
public FilesController(IAuthServiceClient authServiceClient,
21-
IContentServiceClient contentServiceClient) : base(authServiceClient)
22-
{
23-
_contentServiceClient = contentServiceClient;
24-
}
25-
2623
[HttpPost("process")]
27-
[Authorize(Roles = Roles.LecturerRole)]
28-
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
2924
[ProducesResponseType((int)HttpStatusCode.OK)]
30-
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
25+
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
26+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
3127
public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFilesDto)
3228
{
33-
var result = await _contentServiceClient.ProcessFilesAsync(processFilesDto);
29+
var checkRights = await privacyFilter.CheckUploadRights(UserId, processFilesDto.FilesScope);
30+
if (!checkRights) return Forbid("Недостаточно прав для загрузки файлов");
31+
32+
var checkCountLimit = await filesCountLimiter.CheckCountLimit(processFilesDto);
33+
if (!checkCountLimit)
34+
return Forbid("Слишком много файлов в решении." +
35+
$"Максимальное количество файлов - ${FilesCountLimiter.MaxSolutionFiles}");
36+
37+
var result = await contentServiceClient.ProcessFilesAsync(processFilesDto);
3438
return result.Succeeded
3539
? Ok()
36-
: StatusCode((int)HttpStatusCode.ServiceUnavailable, result.Errors);
40+
: BadRequest(result.Errors);
3741
}
3842

3943
[HttpPost("statuses")]
40-
[Authorize(Roles = Roles.LecturerRole)]
41-
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
44+
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
4245
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
43-
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
46+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
4447
public async Task<IActionResult> GetStatuses(ScopeDTO filesScope)
4548
{
46-
var filesStatusesResult = await _contentServiceClient.GetFilesStatuses(filesScope);
47-
return filesStatusesResult.Succeeded
48-
? Ok(filesStatusesResult.Value) as IActionResult
49-
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesStatusesResult.Errors);
49+
var checkRights = await privacyFilter.CheckUploadRights(UserId, filesScope);
50+
if (!checkRights) return Forbid("Недостаточно прав для получения информации о файлах");
51+
52+
var result = await contentServiceClient.GetFilesStatuses(filesScope);
53+
return result.Succeeded
54+
? Ok(result.Value)
55+
: BadRequest(result.Errors);
5056
}
5157

5258
[HttpGet("downloadLink")]
59+
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
5360
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
5461
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.NotFound)]
5562
public async Task<IActionResult> GetDownloadLink([FromQuery] long fileId)
5663
{
57-
var result = await _contentServiceClient.GetDownloadLinkAsync(fileId);
58-
return result.Succeeded
59-
? Ok(result.Value)
60-
: NotFound(result.Errors);
61-
}
64+
var linkDto = await contentServiceClient.GetDownloadLinkAsync(fileId);
65+
if (linkDto.Succeeded) return BadRequest(linkDto.Errors);
6266

63-
[HttpGet("info/course/{courseId}")]
64-
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
65-
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
66-
public async Task<IActionResult> GetFilesInfo(long courseId)
67-
{
68-
var filesInfoResult = await _contentServiceClient.GetFilesInfo(courseId);
69-
return filesInfoResult.Succeeded
70-
? Ok(filesInfoResult.Value) as IActionResult
71-
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesInfoResult.Errors);
67+
var result = linkDto.Value;
68+
var userId = UserId;
69+
70+
foreach (var scope in result.FileScopes)
71+
{
72+
if (await privacyFilter.CheckDownloadRights(userId, scope))
73+
return Ok(result.DownloadUrl);
74+
}
75+
76+
return Forbid("Недостаточно прав для получения ссылки на файл");
7277
}
7378

74-
[HttpGet("info/course/{courseId}/uploaded")]
79+
[HttpGet("info/course/{courseId}")]
7580
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
76-
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
77-
public async Task<IActionResult> GetUploadedFilesInfo(long courseId)
81+
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
82+
public async Task<IActionResult> GetFilesInfo(long courseId,
83+
[FromQuery] bool uploadedOnly = true,
84+
[FromQuery] string courseUnitType = CourseUnitType.Homework)
7885
{
79-
var filesInfoResult = await _contentServiceClient.GetUploadedFilesInfo(courseId);
86+
var filesInfoResult = await contentServiceClient.GetFilesInfo(courseId, uploadedOnly, courseUnitType);
8087
return filesInfoResult.Succeeded
81-
? Ok(filesInfoResult.Value) as IActionResult
82-
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesInfoResult.Errors);
88+
? Ok(filesInfoResult.Value)
89+
: BadRequest(filesInfoResult.Errors);
8390
}
8491
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using HwProj.ContentService.Client;
4+
using HwProj.Models.ContentService.DTO;
5+
using HwProj.Models.CourseUnitType;
6+
7+
namespace HwProj.APIGateway.API.Filters;
8+
9+
public class FilesCountLimiter(IContentServiceClient contentServiceClient)
10+
{
11+
public const long MaxSolutionFiles = 5;
12+
13+
public async Task<bool> CheckCountLimit(ProcessFilesDTO processFilesDto)
14+
{
15+
if (processFilesDto.FilesScope.CourseUnitType == CourseUnitType.Homework) return true;
16+
17+
var existingStatuses = await contentServiceClient.GetFilesStatuses(processFilesDto.FilesScope);
18+
if (!existingStatuses.Succeeded) return false;
19+
20+
var existingIds = existingStatuses.Value.Select(f => f.Id).ToList();
21+
if (processFilesDto.DeletingFileIds.Any(id => !existingIds.Contains(id)))
22+
return false;
23+
24+
return existingIds.Count + processFilesDto.NewFiles.Count - processFilesDto.DeletingFileIds.Count <=
25+
MaxSolutionFiles;
26+
}
27+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using HwProj.CoursesService.Client;
5+
using HwProj.Models.ContentService.DTO;
6+
using HwProj.Models.CourseUnitType;
7+
using HwProj.SolutionsService.Client;
8+
9+
namespace HwProj.APIGateway.API.Filters;
10+
11+
public class FilesPrivacyFilter(
12+
ICoursesServiceClient coursesServiceClient,
13+
ISolutionsServiceClient solutionsServiceClient)
14+
{
15+
private async Task<HashSet<string>> GetSolutionStudentIds(long solutionId)
16+
{
17+
var studentIds = new HashSet<string>();
18+
var solution = await solutionsServiceClient.GetSolutionById(solutionId);
19+
studentIds.Add(solution.StudentId);
20+
21+
if (solution.GroupId is { } groupId)
22+
{
23+
var groups = await coursesServiceClient.GetGroupsById(groupId);
24+
if (groups is [var group]) studentIds.UnionWith(group.StudentsIds.ToHashSet());
25+
}
26+
27+
return studentIds;
28+
}
29+
30+
public async Task<bool> CheckDownloadRights(string? userId, ScopeDTO fileScope)
31+
{
32+
if (userId == null) return false;
33+
34+
switch (fileScope.CourseUnitType)
35+
{
36+
case CourseUnitType.Homework:
37+
return true;
38+
case CourseUnitType.Solution:
39+
{
40+
var studentIds = await GetSolutionStudentIds(fileScope.CourseUnitId);
41+
if (studentIds.Contains(userId)) return true;
42+
43+
var mentorIds = await coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);
44+
return mentorIds.Contains(userId);
45+
}
46+
default:
47+
return false;
48+
}
49+
}
50+
51+
public async Task<bool> CheckUploadRights(string? userId, ScopeDTO fileScope)
52+
{
53+
if (userId == null) return false;
54+
55+
switch (fileScope.CourseUnitType)
56+
{
57+
case CourseUnitType.Homework:
58+
{
59+
var mentorIds = await coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);
60+
return mentorIds.Contains(userId);
61+
}
62+
case CourseUnitType.Solution:
63+
{
64+
var studentIds = await GetSolutionStudentIds(fileScope.CourseUnitId);
65+
return studentIds.Contains(userId);
66+
}
67+
default:
68+
return false;
69+
}
70+
}
71+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public void ConfigureServices(IServiceCollection services)
8080
services.AddContentServiceClient();
8181

8282
services.AddScoped<CourseMentorOnlyAttribute>();
83+
services.AddScoped<FilesPrivacyFilter>();
84+
services.AddScoped<FilesCountLimiter>();
8385
}
8486

8587
public void Configure(IApplicationBuilder app, IHostEnvironment env)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.Linq;
5+
using Microsoft.AspNetCore.Http;
6+
using FileTypeChecker.Abstracts;
7+
using FileTypeChecker.Types;
8+
9+
namespace HwProj.Models.ContentService.Attributes
10+
{
11+
[AttributeUsage(AttributeTargets.Property)]
12+
public class CorrectFileTypeAttribute : FileValidationAttribute
13+
{
14+
private static readonly HashSet<FileType> ForbiddenFileTypes = new HashSet<FileType>
15+
{
16+
new MachO(), new Executable(), new ExecutableAndLinkableFormat()
17+
};
18+
19+
protected override ValidationResult Validate(IFormFile file)
20+
{
21+
try
22+
{
23+
using var fileContent = file.OpenReadStream();
24+
//FileTypeValidator.RegisterCustomTypes(typeof(MachO).Assembly);
25+
if ( //!FileTypeValidator.IsTypeRecognizable(fileContent) ||
26+
ForbiddenFileTypes.Any(type => type.DoesMatchWith(fileContent)))
27+
{
28+
return new ValidationResult(
29+
$"Файл `{file.FileName}` имеет недопустимый тип ${file.ContentType}");
30+
}
31+
}
32+
catch
33+
{
34+
return new ValidationResult(
35+
$"Невозможно прочитать файл `{file.FileName}`");
36+
}
37+
38+
return ValidationResult.Success;
39+
}
40+
41+
private class MachO : FileType
42+
{
43+
private const string TypeName = "MacOS executable";
44+
private const string TypeExtension = "macho";
45+
46+
private static readonly byte[][] MagicBytes =
47+
{
48+
new byte[] { 0xfe, 0xed, 0xfa, 0xce }, // Mach-O BE 32-bit
49+
new byte[] { 0xfe, 0xed, 0xfa, 0xcf }, // Mach-O BE 64-bit
50+
new byte[] { 0xce, 0xfa, 0xed, 0xfe }, // Mach-O LE 32-bit
51+
new byte[] { 0xcf, 0xfa, 0xed, 0xfe }, // Mach-O LE 64-bit
52+
};
53+
54+
public MachO() : base(TypeName, TypeExtension, MagicBytes)
55+
{
56+
}
57+
}
58+
}
59+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace HwProj.Models.ContentService.Attributes
7+
{
8+
public abstract class FileValidationAttribute : ValidationAttribute
9+
{
10+
protected abstract ValidationResult Validate(IFormFile file);
11+
12+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) =>
13+
value switch
14+
{
15+
IFormFile singleFile => Validate(singleFile),
16+
IEnumerable<IFormFile> files => files
17+
.Select(Validate)
18+
.FirstOrDefault(x => x != ValidationResult.Success) ?? ValidationResult.Success,
19+
_ => null
20+
};
21+
}
22+
}
Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,24 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.ComponentModel.DataAnnotations;
43
using Microsoft.AspNetCore.Http;
54

65
namespace HwProj.Models.ContentService.Attributes
76
{
87
[AttributeUsage(AttributeTargets.Property)]
9-
public class MaxFileSizeAttribute : ValidationAttribute
8+
public class MaxFileSizeAttribute : FileValidationAttribute
109
{
1110
private readonly long _maxFileSizeInBytes;
1211

1312
public MaxFileSizeAttribute(long maxFileSizeInBytes)
14-
=>_maxFileSizeInBytes = maxFileSizeInBytes;
13+
=> _maxFileSizeInBytes = maxFileSizeInBytes;
1514

16-
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
15+
protected override ValidationResult Validate(IFormFile file)
1716
{
18-
var files = value switch
19-
{
20-
IFormFile singleFile => new[] { singleFile },
21-
IEnumerable<IFormFile> filesCollection => filesCollection,
22-
_ => null
23-
};
17+
if (file.Length > _maxFileSizeInBytes)
18+
return new ValidationResult(
19+
$"Файл `{file.FileName}` превышает лимит в {_maxFileSizeInBytes / 1024 / 1024} MB");
2420

25-
if (files == null) return ValidationResult.Success;
26-
27-
foreach (var file in files)
28-
if (file.Length > _maxFileSizeInBytes)
29-
return new ValidationResult(
30-
$"Файл `{file.FileName}` превышает лимит в {_maxFileSizeInBytes / 1024 / 1024} MB");
31-
3221
return ValidationResult.Success;
3322
}
3423
}
35-
}
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace HwProj.Models.CourseUnitType
2+
{
3+
public static class CourseUnitType
4+
{
5+
public const string Homework = "Homework";
6+
public const string Solution = "Solution";
7+
public const string Task = "Task";
8+
};
9+
};
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+
6+
public class FileLinkDTO
7+
{
8+
public string DownloadUrl { get; set; }
9+
public List<ScopeDTO> FileScopes { get; set; }
10+
}
11+
}

0 commit comments

Comments
 (0)