功能完成,未测试

master
ArgonarioD 2023-07-03 17:57:56 +08:00
commit 8711c95824
31 changed files with 736 additions and 0 deletions

25
.dockerignore Normal file
View File

@ -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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -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

View File

@ -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">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="TestMethod1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;NUnit3x::73CE1494-4E4B-4E3A-BB6A-0370F379C4A5::net7.0::FileServerTests.UnitTest1.TestMethod1&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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>

View File

@ -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
);

View File

@ -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"]

View File

@ -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;
}
}

View File

@ -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
);

View File

@ -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; }
}

View File

@ -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!;
}

View File

@ -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();

View File

@ -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"
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"Kafka": {
"BootstrapServers": "localhost:9094",
"ConsumerGroupId": "file-server"
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -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>

View File

@ -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");
}
}

View File

@ -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-----

View File

@ -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.

View File

@ -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>

View File

@ -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}"
);
}
}

View File

@ -0,0 +1 @@
global using NUnit.Framework;