功能完成,未测试
commit
8711c95824
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=be86906f_002D506c_002D4340_002Dba3e_002D1608ce09cd77/@EntryIndexedValue"><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></s:String>
|
||||
|
||||
|
||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
@ -0,0 +1,51 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Confluent.Kafka" Version="2.1.1" />
|
||||
<PackageReference Include="Confluent.SchemaRegistry.Serdes.Json" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="ssl.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ssl.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="ssl.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ssl.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CommonResources\CommonResources.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<Tuple<ulong, ulong>> ExistingRanges
|
||||
);
|
|
@ -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"]
|
|
@ -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));
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException">文件格式与FileMetadata不符</exception>
|
||||
/// <exception cref="FileNotFoundException">未找到对应id的文件</exception>
|
||||
public async Task<FileMetadata> GetFileMetadata(string fileId) {
|
||||
var metadata =
|
||||
JsonUtils.Deserialize<FileMetadata>(await File.ReadAllTextAsync($"{FilePath}/{fileId}.metadata")) ??
|
||||
throw new InvalidDataException();
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public async Task<string[]> GetAllPartFiles(string fileId) {
|
||||
return Directory.GetFiles($"{FilePath}/{fileId}.part.*");
|
||||
}
|
||||
|
||||
public Tuple<ulong, ulong> ConvertPartFileNameToRange(string fileName) {
|
||||
var range = fileName.Split('.')[2].Split('-');
|
||||
return Tuple.Create(ulong.Parse(range[0]), ulong.Parse(range[1]));
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<ulong, ulong>>> 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)));
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException">缺失分片时抛出,其Message内容是缺失分片的Range</exception>
|
||||
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<Memory<byte>> ReadRanged(string fileId, ulong rangeStart, ulong rangeEnd) {
|
||||
var fileStream = File.OpenRead($"{FilePath}/{fileId}");
|
||||
var buffer = new Memory<byte>(new byte[rangeEnd - rangeStart]);
|
||||
await fileStream.ReadExactlyAsync(buffer);
|
||||
fileStream.Close();
|
||||
return buffer;
|
||||
}
|
||||
}
|
|
@ -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<UpdateFileMessageConsumerHandler> _logger;
|
||||
private readonly KafkaOptions _options;
|
||||
private readonly ConsumerConfig _consumerConfig;
|
||||
|
||||
public UpdateFileMessageConsumerHandler(
|
||||
IOptions<KafkaOptions> options,
|
||||
KnowledgeFileHandler knowledgeFileHandler,
|
||||
ILogger<UpdateFileMessageConsumerHandler> 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<string, FileTicket>(_consumerConfig)
|
||||
.SetValueDeserializer(new JsonDeserializer<FileTicket>().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
|
||||
);
|
|
@ -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; }
|
||||
}
|
|
@ -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!;
|
||||
}
|
|
@ -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<KafkaOptions>(builder.Configuration.GetSection(KafkaOptions.SectionName));
|
||||
builder.Services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });
|
||||
builder.Services.AddControllers().AddJsonOptions(options => {
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
});
|
||||
builder.Services.AddSingleton<KnowledgeFileHandler>();
|
||||
builder.Services.AddHostedService<UpdateFileMessageConsumerHandler>();
|
||||
// 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();
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace AicsKnowledgeBase_file.Utilities;
|
||||
|
||||
public static class CollectionUtils {
|
||||
public static TValue GetOrPut<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, Func<TValue> func) where TKey : notnull {
|
||||
if (dict.TryGetValue(key, out var value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
value = func();
|
||||
dict[key] = value;
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace AicsKnowledgeBase_file.Utilities;
|
||||
|
||||
public static class HashUtils {
|
||||
public static async Task<string> GetMd5Hash(Stream stream) {
|
||||
using var md5 = MD5.Create();
|
||||
var hash = await md5.ComputeHashAsync(stream);
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
|
@ -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>(T obj) {
|
||||
return JsonConvert.SerializeObject(obj, DefaultSettings);
|
||||
}
|
||||
|
||||
public static T? Deserialize<T>(string json) {
|
||||
return JsonConvert.DeserializeObject<T>(json, DefaultSettings);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AicsKnowledgeBase_file.Utilities;
|
||||
|
||||
public static class ProblemUtils {
|
||||
|
||||
private static readonly Dictionary<ErrorCodes, ErrorCodeAttribute> 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<ErrorCodeAttribute>()!;
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
},
|
||||
"Kafka": {
|
||||
"BootstrapServers": "localhost:9094",
|
||||
"ConsumerGroupId": "file-server"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Resources\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="Resources\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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-----
|
|
@ -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-----
|
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
|
||||
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
global using NUnit.Framework;
|
Loading…
Reference in New Issue