commit 8711c958244602a6447b4c693244d17673439820 Author: ArgonarioD Date: Mon Jul 3 17:57:56 2023 +0800 功能完成,未测试 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.AicsKnowledgeBase_file/.idea/.gitignore b/.idea/.idea.AicsKnowledgeBase_file/.idea/.gitignore new file mode 100644 index 0000000..906cee1 --- /dev/null +++ b/.idea/.idea.AicsKnowledgeBase_file/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.AicsKnowledgeBase_file.iml +/projectSettingsUpdater.xml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.AicsKnowledgeBase_file/.idea/encodings.xml b/.idea/.idea.AicsKnowledgeBase_file/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.AicsKnowledgeBase_file/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.AicsKnowledgeBase_file/.idea/indexLayout.xml b/.idea/.idea.AicsKnowledgeBase_file/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.AicsKnowledgeBase_file/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.AicsKnowledgeBase_file/.idea/vcs.xml b/.idea/.idea.AicsKnowledgeBase_file/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.AicsKnowledgeBase_file/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AicsKnowledgeBase_file.sln b/AicsKnowledgeBase_file.sln new file mode 100644 index 0000000..b6203eb --- /dev/null +++ b/AicsKnowledgeBase_file.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AicsKnowledgeBase_file", "AicsKnowledgeBase_file\AicsKnowledgeBase_file.csproj", "{4C2B6014-9A1C-405C-A144-A1389DA89AEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonResources", "CommonResources\CommonResources.csproj", "{40AA6EA2-7C9A-49D8-B850-C2FC45A72747}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileServerTests", "FileServerTests\FileServerTests.csproj", "{73CE1494-4E4B-4E3A-BB6A-0370F379C4A5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4C2B6014-9A1C-405C-A144-A1389DA89AEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C2B6014-9A1C-405C-A144-A1389DA89AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C2B6014-9A1C-405C-A144-A1389DA89AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C2B6014-9A1C-405C-A144-A1389DA89AEE}.Release|Any CPU.Build.0 = Release|Any CPU + {40AA6EA2-7C9A-49D8-B850-C2FC45A72747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40AA6EA2-7C9A-49D8-B850-C2FC45A72747}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40AA6EA2-7C9A-49D8-B850-C2FC45A72747}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40AA6EA2-7C9A-49D8-B850-C2FC45A72747}.Release|Any CPU.Build.0 = Release|Any CPU + {73CE1494-4E4B-4E3A-BB6A-0370F379C4A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73CE1494-4E4B-4E3A-BB6A-0370F379C4A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73CE1494-4E4B-4E3A-BB6A-0370F379C4A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73CE1494-4E4B-4E3A-BB6A-0370F379C4A5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/AicsKnowledgeBase_file.sln.DotSettings.user b/AicsKnowledgeBase_file.sln.DotSettings.user new file mode 100644 index 0000000..1d61cfb --- /dev/null +++ b/AicsKnowledgeBase_file.sln.DotSettings.user @@ -0,0 +1,9 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="TestMethod1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>NUnit3x::73CE1494-4E4B-4E3A-BB6A-0370F379C4A5::net7.0::FileServerTests.UnitTest1.TestMethod1</TestId> + </TestAncestor> +</SessionState> + + + True \ No newline at end of file diff --git a/AicsKnowledgeBase_file/AicsKnowledgeBase_file.csproj b/AicsKnowledgeBase_file/AicsKnowledgeBase_file.csproj new file mode 100644 index 0000000..7b82206 --- /dev/null +++ b/AicsKnowledgeBase_file/AicsKnowledgeBase_file.csproj @@ -0,0 +1,51 @@ + + + + net7.0 + enable + enable + Linux + + + + + + + + + + + + .dockerignore + + + + + + ResXFileCodeGenerator + ssl.Designer.cs + + + ResXFileCodeGenerator + resources.Designer.cs + + + + + + True + True + ssl.resx + + + True + True + resources.resx + + + + + + + + diff --git a/AicsKnowledgeBase_file/Controllers/FileController.cs b/AicsKnowledgeBase_file/Controllers/FileController.cs new file mode 100644 index 0000000..442961c --- /dev/null +++ b/AicsKnowledgeBase_file/Controllers/FileController.cs @@ -0,0 +1,105 @@ +using System.Net.Mime; +using AicsKnowledgeBase_file.Handlers; +using AicsKnowledgeBase_file.Utilities; +using Microsoft.AspNetCore.Mvc; + +namespace AicsKnowledgeBase_file.Controllers; + +[Route("[controller]/{fileId}")] +public class FileController : ControllerBase { + private readonly KnowledgeFileHandler _knowledgeFileHandler; + + public FileController(KnowledgeFileHandler knowledgeFileHandler) { + _knowledgeFileHandler = knowledgeFileHandler; + } + + [HttpGet("status")] + public async Task GetFileStatus(string fileId) { + try { + var metadata = await _knowledgeFileHandler.GetFileMetadata(fileId); + var existingRanges = await _knowledgeFileHandler.GetAllPartFileRanges(fileId); + + return Ok(new FileStatus(metadata.IsCompleted, metadata.Md5!, metadata.Size!.Value, existingRanges)); + } catch (FileNotFoundException) { + return NotFound(); + } + } + + [HttpGet] + public async Task DownloadFile( + string fileId, + [FromQuery] ulong rangeStart, + [FromQuery] ulong rangeEnd + ) { + try { + var data = await _knowledgeFileHandler.ReadRanged(fileId, rangeStart, rangeEnd); + return File(data.ToArray(), MediaTypeNames.Application.Octet); + } catch (FileNotFoundException) { + return NotFound(); + } + } + + [HttpPost("metadata")] + public async Task PostFileMetadata(string fileId, [FromBody] ClientFileMetadata metadata) { + var currentData = await _knowledgeFileHandler.GetFileMetadata(fileId); + if (currentData.Ticket != metadata.Ticket) { + return this.ProblemFromCode(ErrorCodes.TicketMismatch); + } + + if (currentData.Md5 != null || currentData.Size != null) { + return this.ProblemFromCode(ErrorCodes.FileMetadataConflict); + } + + currentData.Md5 = metadata.Md5; + currentData.Size = metadata.Size; + await _knowledgeFileHandler.SaveFileMetadata(fileId, currentData); + return Ok(); + } + + [HttpPost] + public async Task UploadFilePart(string fileId, [FromForm] UploadFileDto dto) { + var metadata = await _knowledgeFileHandler.GetFileMetadata(fileId); + if (metadata.Ticket != dto.Ticket) { + return this.ProblemFromCode(ErrorCodes.TicketMismatch); + } + + await _knowledgeFileHandler.SaveFileSlice(fileId, dto.File, dto.RangeStart, dto.RangeEnd); + return Ok(); + } + + [HttpPost("complete/{ticket}")] + public async Task FilePostComplete(string fileId, string ticket) { + var metadata = await _knowledgeFileHandler.GetFileMetadata(fileId); + if (metadata.Ticket != ticket) { + return this.ProblemFromCode(ErrorCodes.TicketMismatch); + } + + try { + await _knowledgeFileHandler.MergeFileSlices(fileId); + } catch (InvalidDataException e) { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: e.Message); + } + + return Ok(); + } +} + +public record ClientFileMetadata( + string Ticket, + string Md5, + ulong Size +); + +public record UploadFileDto( + string Ticket, + ulong RangeStart, + ulong RangeEnd, + IFormFile File +); + +internal record FileStatus( + bool IsCompleted, + string Md5, + ulong Size, + List> ExistingRanges +); \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Dockerfile b/AicsKnowledgeBase_file/Dockerfile new file mode 100644 index 0000000..84977fd --- /dev/null +++ b/AicsKnowledgeBase_file/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["AicsKnowledgeBase_file/AicsKnowledgeBase_file.csproj", "AicsKnowledgeBase_file/"] +RUN dotnet restore "AicsKnowledgeBase_file/AicsKnowledgeBase_file.csproj" +COPY . . +WORKDIR "/src/AicsKnowledgeBase_file" +RUN dotnet build "AicsKnowledgeBase_file.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "AicsKnowledgeBase_file.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "AicsKnowledgeBase_file.dll"] diff --git a/AicsKnowledgeBase_file/Handlers/KnowledgeFileHandler.cs b/AicsKnowledgeBase_file/Handlers/KnowledgeFileHandler.cs new file mode 100644 index 0000000..4117ba9 --- /dev/null +++ b/AicsKnowledgeBase_file/Handlers/KnowledgeFileHandler.cs @@ -0,0 +1,85 @@ +using AicsKnowledgeBase_file.Models; +using AicsKnowledgeBase_file.Utilities; + +namespace AicsKnowledgeBase_file.Handlers; + +public class KnowledgeFileHandler { + private const string FilePath = "./files"; + + public async Task SaveFileMetadata(string fileId, FileMetadata metadata) { + await File.WriteAllTextAsync($"{FilePath}/{fileId}.metadata", JsonUtils.Serialize(metadata)); + } + + /// 文件格式与FileMetadata不符 + /// 未找到对应id的文件 + public async Task GetFileMetadata(string fileId) { + var metadata = + JsonUtils.Deserialize(await File.ReadAllTextAsync($"{FilePath}/{fileId}.metadata")) ?? + throw new InvalidDataException(); + return metadata; + } + + public async Task GetAllPartFiles(string fileId) { + return Directory.GetFiles($"{FilePath}/{fileId}.part.*"); + } + + public Tuple ConvertPartFileNameToRange(string fileName) { + var range = fileName.Split('.')[2].Split('-'); + return Tuple.Create(ulong.Parse(range[0]), ulong.Parse(range[1])); + } + + public async Task>> GetAllPartFileRanges(string fileId) { + var result = (await GetAllPartFiles(fileId)).Select(ConvertPartFileNameToRange).ToList(); + result.Sort((a, b) => (int)(a.Item1 - b.Item1)); + return result; + } + + public string GetSliceFileName(string fileId, ulong rangeStart, ulong rangeEnd) { + return $"{FilePath}/{fileId}.part.{rangeStart}-{rangeEnd}"; + } + + public async Task SaveFileSlice(string fileId, IFormFile file, ulong rangeStart, ulong rangeEnd) { + await file.CopyToAsync(File.Create(GetSliceFileName(fileId, rangeStart, rangeEnd))); + } + + /// 缺失分片时抛出,其Message内容是缺失分片的Range + public async Task MergeFileSlices(string fileId) { + var metadata = await GetFileMetadata(fileId); + var slices = await GetAllPartFileRanges(fileId); + if (slices.Count == 0) { + throw new InvalidDataException($"0-{metadata.Size}"); + } + if (slices.First().Item1 != 0) { + throw new InvalidDataException($"0-{slices.First().Item1}"); + } + + if (slices.Last().Item2 != metadata.Size) { + throw new InvalidDataException($"{slices.Last().Item2}-{metadata.Size}"); + } + + for (var i = 1; i < slices.Count; i++) { + if (slices[i - 1].Item2 != slices[i].Item1) { + throw new InvalidDataException($"{slices[i - 1].Item2}-{slices[i].Item1}"); + } + } + + var fileStream = File.Create($"{FilePath}/{fileId}"); + foreach (var slice in slices) { + var sliceStream = File.OpenRead(GetSliceFileName(fileId, slice.Item1, slice.Item2)); + await sliceStream.CopyToAsync(fileStream); + sliceStream.Close(); + await fileStream.FlushAsync(); + File.Delete($"{FilePath}/{fileId}.part.{slice.Item1}-{slice.Item2}"); + } + + fileStream.Close(); + } + + public async Task> ReadRanged(string fileId, ulong rangeStart, ulong rangeEnd) { + var fileStream = File.OpenRead($"{FilePath}/{fileId}"); + var buffer = new Memory(new byte[rangeEnd - rangeStart]); + await fileStream.ReadExactlyAsync(buffer); + fileStream.Close(); + return buffer; + } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Handlers/UpdateFileMessageConsumerHandler.cs b/AicsKnowledgeBase_file/Handlers/UpdateFileMessageConsumerHandler.cs new file mode 100644 index 0000000..6842575 --- /dev/null +++ b/AicsKnowledgeBase_file/Handlers/UpdateFileMessageConsumerHandler.cs @@ -0,0 +1,62 @@ +using AicsKnowledgeBase_file.Models; +using Confluent.Kafka; +using Confluent.Kafka.SyncOverAsync; +using Confluent.SchemaRegistry.Serdes; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace AicsKnowledgeBase_file.Handlers; + +public class UpdateFileMessageConsumerHandler : BackgroundService { + private const string TopicName = "upload_file"; + private readonly KnowledgeFileHandler _knowledgeFileHandler; + private readonly ILogger _logger; + private readonly KafkaOptions _options; + private readonly ConsumerConfig _consumerConfig; + + public UpdateFileMessageConsumerHandler( + IOptions options, + KnowledgeFileHandler knowledgeFileHandler, + ILogger logger + ) { + _logger = logger; + _knowledgeFileHandler = knowledgeFileHandler; + _options = options.Value; + _consumerConfig = new ConsumerConfig { + BootstrapServers = _options.BootstrapServers, + GroupId = _options.ConsumerGroupId, + AutoOffsetReset = AutoOffsetReset.Earliest + }; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) { + return Task.Run(async () => { + using var consumer = new ConsumerBuilder(_consumerConfig) + .SetValueDeserializer(new JsonDeserializer().AsSyncOverAsync()) + .Build(); + consumer.Subscribe(TopicName); + try { + while (true) { + try { + var result = consumer.Consume(stoppingToken); + var msg = result.Message.Value!; + await _knowledgeFileHandler.SaveFileMetadata(msg.Id, new FileMetadata { + IsCompleted = false, + Ticket = msg.Ticket + }); + } catch (ConsumeException e) { + _logger.LogError("Consume Exception: {Exception}", e); + } + } + } catch (OperationCanceledException) { + _logger.LogDebug("Consumer Closing: {Topic}", TopicName); + consumer.Close(); + } + }, stoppingToken); + } +} + +public record FileTicket( + [JsonProperty("ticket")] string Ticket, + [JsonProperty("id")] string Id +); \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Models/FileMetadata.cs b/AicsKnowledgeBase_file/Models/FileMetadata.cs new file mode 100644 index 0000000..2242906 --- /dev/null +++ b/AicsKnowledgeBase_file/Models/FileMetadata.cs @@ -0,0 +1,8 @@ +namespace AicsKnowledgeBase_file.Models; + +public class FileMetadata { + public bool IsCompleted { get; set; } = false; + public string Ticket { get; set; } = null!; + public string? Md5 { get; set; } + public ulong? Size { get; set; } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Models/KafkaOptions.cs b/AicsKnowledgeBase_file/Models/KafkaOptions.cs new file mode 100644 index 0000000..b9eb17a --- /dev/null +++ b/AicsKnowledgeBase_file/Models/KafkaOptions.cs @@ -0,0 +1,7 @@ +namespace AicsKnowledgeBase_file.Models; + +public class KafkaOptions { + public const string SectionName = "Kafka"; + public string BootstrapServers { get; set; } = null!; + public string ConsumerGroupId { get; set; } = null!; +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Program.cs b/AicsKnowledgeBase_file/Program.cs new file mode 100644 index 0000000..430eb5b --- /dev/null +++ b/AicsKnowledgeBase_file/Program.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using AicsKnowledgeBase_file.Handlers; +using AicsKnowledgeBase_file.Models; +using CommonResources; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.ConfigureKestrel((context, options) => { + options.ListenAnyIP(8090, listenOptions => { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; + // listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + listenOptions.UseHttps(Resources.GetServerCertificate()); + }); +}); + +// Add services to the container. +builder.Services.Configure(builder.Configuration.GetSection(KafkaOptions.SectionName)); +builder.Services.Configure(options => { options.LowercaseUrls = true; }); +builder.Services.AddControllers().AddJsonOptions(options => { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; +}); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); +} +app.MapGet("/hello", () => "hello"); + +app.UseHttpsRedirection(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Properties/launchSettings.json b/AicsKnowledgeBase_file/Properties/launchSettings.json new file mode 100644 index 0000000..0e18c85 --- /dev/null +++ b/AicsKnowledgeBase_file/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:26772", + "sslPort": 44313 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5289", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://127.0.0.1:8090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AicsKnowledgeBase_file/Utilities/CollectionUtils.cs b/AicsKnowledgeBase_file/Utilities/CollectionUtils.cs new file mode 100644 index 0000000..6ddb0b4 --- /dev/null +++ b/AicsKnowledgeBase_file/Utilities/CollectionUtils.cs @@ -0,0 +1,13 @@ +namespace AicsKnowledgeBase_file.Utilities; + +public static class CollectionUtils { + public static TValue GetOrPut(this Dictionary dict, TKey key, Func func) where TKey : notnull { + if (dict.TryGetValue(key, out var value)) { + return value; + } + + value = func(); + dict[key] = value; + return value; + } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Utilities/HashUtils.cs b/AicsKnowledgeBase_file/Utilities/HashUtils.cs new file mode 100644 index 0000000..44fa3c3 --- /dev/null +++ b/AicsKnowledgeBase_file/Utilities/HashUtils.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography; + +namespace AicsKnowledgeBase_file.Utilities; + +public static class HashUtils { + public static async Task GetMd5Hash(Stream stream) { + using var md5 = MD5.Create(); + var hash = await md5.ComputeHashAsync(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Utilities/JsonUtils.cs b/AicsKnowledgeBase_file/Utilities/JsonUtils.cs new file mode 100644 index 0000000..dc13441 --- /dev/null +++ b/AicsKnowledgeBase_file/Utilities/JsonUtils.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace AicsKnowledgeBase_file.Utilities; + +public static class JsonUtils { + private static readonly JsonSerializerSettings DefaultSettings = new() { + NullValueHandling = NullValueHandling.Include, + MissingMemberHandling = MissingMemberHandling.Error, + ReferenceLoopHandling = ReferenceLoopHandling.Error, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public static string Serialize(T obj) { + return JsonConvert.SerializeObject(obj, DefaultSettings); + } + + public static T? Deserialize(string json) { + return JsonConvert.DeserializeObject(json, DefaultSettings); + } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/Utilities/ProblemUtils.cs b/AicsKnowledgeBase_file/Utilities/ProblemUtils.cs new file mode 100644 index 0000000..c9f302d --- /dev/null +++ b/AicsKnowledgeBase_file/Utilities/ProblemUtils.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; + +namespace AicsKnowledgeBase_file.Utilities; + +public static class ProblemUtils { + + private static readonly Dictionary ProblemCache = new(); + + public static ObjectResult ProblemFromCode(this ControllerBase controller, ErrorCodes code) { + var errorAttribute = ProblemCache.GetOrPut(code, () => { + var field = code.GetType().GetField(code.ToString()); + return field!.GetCustomAttribute()!; + }); + return controller.Problem(title: errorAttribute.Title, detail: errorAttribute.Detail, statusCode: errorAttribute.StatusCode); + } +} + +public enum ErrorCodes { + [ErrorCode(StatusCodes.Status403Forbidden, "Ticket不匹配", "请检查Ticket是否正确")] + TicketMismatch = 0, + [ErrorCode(StatusCodes.Status409Conflict, "文件元数据已被上传", "如果需要重新上传,请先删除文件")] + FileMetadataConflict = 1, +} + +internal class ErrorCodeAttribute : Attribute { + public int StatusCode { get; } + public string Title { get; } + public string? Detail { get; } + + public ErrorCodeAttribute(int statusCode, string title, string? detail = null) { + StatusCode = statusCode; + Title = title; + Detail = detail; + } +} \ No newline at end of file diff --git a/AicsKnowledgeBase_file/appsettings.Development.json b/AicsKnowledgeBase_file/appsettings.Development.json new file mode 100644 index 0000000..d3beb18 --- /dev/null +++ b/AicsKnowledgeBase_file/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + } + }, + "Kafka": { + "BootstrapServers": "localhost:9094", + "ConsumerGroupId": "file-server" + } +} diff --git a/AicsKnowledgeBase_file/appsettings.json b/AicsKnowledgeBase_file/appsettings.json new file mode 100644 index 0000000..6a845cf --- /dev/null +++ b/AicsKnowledgeBase_file/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/CommonResources/CommonResources.csproj b/CommonResources/CommonResources.csproj new file mode 100644 index 0000000..5779e03 --- /dev/null +++ b/CommonResources/CommonResources.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/CommonResources/Resources.cs b/CommonResources/Resources.cs new file mode 100644 index 0000000..5689063 --- /dev/null +++ b/CommonResources/Resources.cs @@ -0,0 +1,9 @@ +using System.Security.Cryptography.X509Certificates; + +namespace CommonResources; + +public static class Resources { + public static X509Certificate2 GetServerCertificate() { + return new X509Certificate2("../CommonResources/ssl/ssl-rsa.pfx", "auto"); + } +} \ No newline at end of file diff --git a/CommonResources/ssl/private-rsa.key b/CommonResources/ssl/private-rsa.key new file mode 100644 index 0000000..e9080c0 --- /dev/null +++ b/CommonResources/ssl/private-rsa.key @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,2D03D2D4C1E67F6A + +eBzvrgxnWHMOFiwN5GdOvDaMtiP0lhbt4gkYbGiUtoyJaOfiovIiDpDRcQayRuAj +n3O+3r8HZknJIBleFfJr7adf69gHtAqlq69TpoTu3mjZ83WRQesOw4twA8GtYbQu +e/8m53oeH7Wqy7Xs95mnN+XZb/NI+KCtytWIu/f0FgXNBOhqYdsYLahUdWdNokpK +DGQTTX+2EFLjg5s7CsffpKSZA/hXlG5bhAUA2NLaiJD9OW0tzJnV/YaDBe7+chJ9 +GlqvCItQ/hunC2/ZEWOG7/pcTKjfV85znQPDfq6AeS/nhtbR5UozHU9vVCxuUjJQ +Qg/bc1ZjwyisyHoC9kuih7PeLiS2gXo99tafWHL80XRgnol6aofC4FqGeUvtuu/0 +Qnkbgc4M5PUu0gGtp51rAVQg6Fmd1v1HRyD3gyI5JsDeCV8pl71iK4kN3F6nTcjP +AXeqBzosSZnOA2vhIZvRABdtN/7QGlGWtxvFBNaeT2CZB3ErxlX2+3cEqbz24wT0 +gsDZ7JWBcTItCfG7Ab8408DuNTdFUp+yldlOdfLF9V3CEWvimZdu0NONa/EGy8UK +ZmoFqEelrpQJ1d+vlDuwPwSdxHB0uGruS9DOE9NvrC6bMQG5hmhC42JkxrJa6vvN +anPzMqnZhz9QIpiouAsBFXH5ooNkJz3mF3pHhtTbb0Febh1WrSMUjrejdmVKFiMq +7OrAVU2h55rLPqD+SWc//qpYDMPpKDMd3V+cWvFvY5HwjuNWT/NWz802oKaAuhjH +WteNW2qQpZshgTYKTcVlKGzhjxQuin4hwe33/PVvJ02XuoUL/lPBYg== +-----END RSA PRIVATE KEY----- diff --git a/CommonResources/ssl/public-rsa.cer b/CommonResources/ssl/public-rsa.cer new file mode 100644 index 0000000..86b7a15 --- /dev/null +++ b/CommonResources/ssl/public-rsa.cer @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6jCCAlOgAwIBAgIUGqSNDfhqZ7KRM+iuknn5qJlB0wQwDQYJKoZIhvcNAQEL +BQAwgYYxCzAJBgNVBAYTAkNOMQ4wDAYDVQQIDAVBbmh1aTEOMAwGA1UEBwwFSGVm +ZWkxDTALBgNVBAoMBGF1dG8xDTALBgNVBAsMBGF1dG8xEjAQBgNVBAMMCTEyNy4w +LjAuMTElMCMGCSqGSIb3DQEJARYWYXJnb25hcmlvZEBvdXRsb29rLmNvbTAeFw0y +MzA2MTgxOTI3MTFaFw0zMzA2MTUxOTI3MTFaMIGGMQswCQYDVQQGEwJDTjEOMAwG +A1UECAwFQW5odWkxDjAMBgNVBAcMBUhlZmVpMQ0wCwYDVQQKDARhdXRvMQ0wCwYD +VQQLDARhdXRvMRIwEAYDVQQDDAkxMjcuMC4wLjExJTAjBgkqhkiG9w0BCQEWFmFy +Z29uYXJpb2RAb3V0bG9vay5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB +ANBQFC6HuicohMKG9aIeOTPp5lN3yy9q9jsns2cBWLbQic0Y61puisVNOF0vPRCk +2tHL1vdXut9l2RvZC6ujgJ830xkA/da2SxwHVvPBFIHV/kVE5N3096jJ2kiKb/c3 +j+pnwzL6WZoKTZiaFpJAWWK6lT6va3oQyUwB2Eid6vzvAgMBAAGjUzBRMB0GA1Ud +DgQWBBQ4LBr5R/YECBQVRSNi8HdHNa1XnDAfBgNVHSMEGDAWgBQ4LBr5R/YECBQV +RSNi8HdHNa1XnDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAMs7 +rc5qFLaJ3htIGBokWQcneUpwblJKl/An8D/lyNE3BU0Lc+5/bBIW4m66J6PKOf1A +Sedv9mUdnQMOb19k+BuztBAK2oTFdVpAKgf7iRdpxDSkgXlanY2kadcWFOO9N0Af +4onPF3o1Tbp5f8wLwYKGmlFy5H4ks/9KIT/YuhRb +-----END CERTIFICATE----- diff --git a/CommonResources/ssl/ssl-rsa.pfx b/CommonResources/ssl/ssl-rsa.pfx new file mode 100644 index 0000000..6aebe3c Binary files /dev/null and b/CommonResources/ssl/ssl-rsa.pfx differ diff --git a/FileServerTests/FileServerTests.csproj b/FileServerTests/FileServerTests.csproj new file mode 100644 index 0000000..a848fca --- /dev/null +++ b/FileServerTests/FileServerTests.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + diff --git a/FileServerTests/UnitTest1.cs b/FileServerTests/UnitTest1.cs new file mode 100644 index 0000000..9a0b901 --- /dev/null +++ b/FileServerTests/UnitTest1.cs @@ -0,0 +1,30 @@ +using System.Net; +using System.Text; + +namespace FileServerTests; + +public class UnitTest1 { + [Test] + public async Task TestMethod1() { + using var client = new HttpClient + { + DefaultRequestVersion = HttpVersion.Version30, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + Console.WriteLine("--- 127.0.0.1:8090 ---"); + + var resp = await client.GetAsync("https://127.0.0.1:8090/hello"); + var headers = new StringBuilder(); + foreach (var (headerName, headerValues) in resp.Headers) { + headers.AppendLine($"{headerName}: {string.Join(", ", headerValues)}"); + } + var body = await resp.Content.ReadAsStringAsync(); + + Console.WriteLine( + $"status: {resp.StatusCode}, version: {resp.Version}, \n" + + $"headers: \n{headers}\n" + + $"body: \n{body}" + ); + } +} \ No newline at end of file diff --git a/FileServerTests/Usings.cs b/FileServerTests/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/FileServerTests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file