diff --git a/.vscode/settings.json b/.vscode/settings.json index 8bc27f5..d774237 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "azureFunctions.deploySubpath": "bin/Release/net6.0/publish", + "azureFunctions.deploySubpath": "bin/Release/net8.0/publish", "azureFunctions.projectLanguage": "C#", "azureFunctions.projectRuntime": "~4", "debug.internalConsoleOptions": "neverOpen", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index be13430..5199259 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -59,7 +59,7 @@ "type": "func", "dependsOn": "build (functions)", "options": { - "cwd": "${workspaceFolder}/bin/Debug/net6.0" + "cwd": "${workspaceFolder}/bin/Debug/net8.0" }, "command": "host start", "isBackground": true, diff --git a/Dockerfile b/Dockerfile index d0c533c..3553103 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ARG DatabaseService="ApacheCouchDB" ### Build Python FROM debian:11 as build-python -RUN apt-get update && apt-get install -y --no-install-recommends python3=3.9.2-3 python3-pip && \ +RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip && \ apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -28,7 +28,7 @@ RUN --mount=type=cache,id=pip-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/r find "/root/.local" -type d -name '__pycache__' -print0 | xargs -0 rm -rf || true ; ### Build .NET -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-dotnet +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-dotnet WORKDIR /src @@ -43,9 +43,9 @@ RUN --mount=source=.,target=.,rw \ dotnet publish "LivestreamRecorderBackend.csproj" -c $BUILD_CONFIGURATION -o /app/publish ### Final image -FROM mcr.microsoft.com/azure-functions/dotnet:4 +FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 -RUN apt-get update && apt-get install -y --no-install-recommends python3=3.9.2-3 dumb-init=1.2.5-1 && \ +RUN apt-get update && apt-get install -y --no-install-recommends python3 dumb-init && \ apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -61,7 +61,7 @@ ENV ASPNETCORE_URLS=http://+:8080 ENV AzureWebJobsScriptRoot=/home/site/wwwroot ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true ENV CORS_SUPPORT_CREDENTIALS=true -ENV FUNCTIONS_WORKER_RUNTIME=dotnet +ENV FUNCTIONS_WORKER_RUNTIME=dotnet-isolated ENV CORS_ALLOWED_ORIGINS=["https://localhost:4200"] ENV FrontEndUri=https://localhost:4200 ENV AzureWebJobsStorage= diff --git a/Functions/Authentication.cs b/Functions/Authentication.cs index 9fb8a58..29d9314 100644 --- a/Functions/Authentication.cs +++ b/Functions/Authentication.cs @@ -3,9 +3,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Microsoft.OpenApi.Models; using Serilog; using System; @@ -13,6 +10,8 @@ using System.Net; using System.Threading.Tasks; using System.Web; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; namespace LivestreamRecorderBackend.Functions; @@ -28,7 +27,7 @@ public Authentication(GithubService githubService) _githubService = githubService; } - [FunctionName(nameof(GithubSignin))] + [Function(nameof(GithubSignin))] [OpenApiOperation(operationId: nameof(GithubSignin), tags: new[] { "Authentication" })] [OpenApiParameter(name: "code", In = ParameterLocation.Query, Required = true, Type = typeof(string))] [OpenApiParameter(name: "state", In = ParameterLocation.Query, Required = true, Type = typeof(string))] diff --git a/Functions/Channel.cs b/Functions/Channel.cs index c403344..d6e6177 100644 --- a/Functions/Channel.cs +++ b/Functions/Channel.cs @@ -4,10 +4,6 @@ using LivestreamRecorderBackend.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Microsoft.OpenApi.Models; using Serilog; using System; @@ -18,6 +14,11 @@ using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace LivestreamRecorderBackend.Functions; @@ -37,14 +38,14 @@ public Channel( _userService = userService; } - [FunctionName(nameof(AddChannelAsync))] + [Function(nameof(AddChannelAsync))] [OpenApiOperation(operationId: nameof(AddChannelAsync), tags: new[] { nameof(Channel) })] [OpenApiRequestBody("application/json", typeof(AddChannelRequest), Required = true)] [OpenApiResponseWithBody(HttpStatusCode.OK, "application/json", typeof(string), Description = "Response")] public async Task AddChannelAsync( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "Channel")] HttpRequest req, - [DurableClient] IDurableClient starter) + [DurableClient] DurableTaskClient starter) { try { @@ -122,8 +123,8 @@ public async Task AddChannelAsync( _logger.Information("Finish adding channel {channelName}:{channelId}", channelName, channelId); - var instanceId = await starter.StartNewAsync( - orchestratorFunctionName: nameof(UpdateChannel_Durable), + var instanceId = await starter.ScheduleNewOrchestrationInstanceAsync( + orchestratorName: nameof(UpdateChannel_Durable), input: new UpdateChannelRequest() { id = channelId, @@ -137,7 +138,8 @@ public async Task AddChannelAsync( _logger.Information("Started orchestration with ID {instanceId}.", instanceId); // Wait for the instance to start executing - return await starter.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, TimeSpan.FromSeconds(15)); + await starter.WaitForInstanceStartAsync(instanceId); + return new OkResult(); } catch (Exception e) { @@ -151,7 +153,7 @@ public async Task AddChannelAsync( } } - [FunctionName(nameof(UpdateChannel_Http))] + [Function(nameof(UpdateChannel_Http))] [OpenApiOperation(operationId: nameof(UpdateChannel_Http), tags: new[] { nameof(Channel) })] [OpenApiParameter(name: "channelId", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "ChannelId")] [OpenApiResponseWithBody(HttpStatusCode.OK, "application/json", typeof(string), Description = "Response")] @@ -159,7 +161,7 @@ public async Task AddChannelAsync( public async Task UpdateChannel_Http( [HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "Channel")] HttpRequest req, - [DurableClient] IDurableClient starter) + [DurableClient] DurableTaskClient starter) { try { @@ -176,8 +178,8 @@ public async Task UpdateChannel_Http( var data = JsonSerializer.Deserialize(requestBody) ?? throw new InvalidOperationException("Invalid request body!!"); - await starter.StartNewAsync( - orchestratorFunctionName: nameof(UpdateChannel_Durable), + await starter.ScheduleNewOrchestrationInstanceAsync( + orchestratorName: nameof(UpdateChannel_Durable), input: data); return new OkResult(); @@ -189,11 +191,12 @@ await starter.StartNewAsync( } } - [FunctionName(nameof(UpdateChannel_Durable))] + [Function(nameof(UpdateChannel_Durable))] public bool UpdateChannel_Durable( - [OrchestrationTrigger] IDurableOrchestrationContext context) + [OrchestrationTrigger] TaskOrchestrationContext context) { var data = context.GetInput(); + if (null == data) throw new InvalidOperationException("Invalid request body!!"); _ = Task.Run(async () => { _logger.Information("Start updating channel {channelId}", data.id); @@ -219,7 +222,7 @@ public bool UpdateChannel_Durable( return true; } - [FunctionName(nameof(EnableChannelAsync))] + [Function(nameof(EnableChannelAsync))] [OpenApiOperation(operationId: nameof(EnableChannelAsync), tags: new[] { nameof(Channel) })] [OpenApiRequestBody("application/json", typeof(EnableChannelRequest), Required = true)] [OpenApiResponseWithoutBody(HttpStatusCode.OK, Description = "Response")] @@ -258,7 +261,7 @@ public async Task EnableChannelAsync( } } - [FunctionName(nameof(HideChannelAsync))] + [Function(nameof(HideChannelAsync))] [OpenApiOperation(operationId: nameof(HideChannelAsync), tags: new[] { nameof(Channel) })] [OpenApiRequestBody("application/json", typeof(HideChannelRequest), Required = true)] [OpenApiResponseWithoutBody(HttpStatusCode.OK, Description = "Response")] diff --git a/Functions/User.cs b/Functions/User.cs index 6861cce..51c28a8 100644 --- a/Functions/User.cs +++ b/Functions/User.cs @@ -4,9 +4,6 @@ using LivestreamRecorderBackend.Services.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Omu.ValueInjecter; using Serilog; using System; @@ -16,6 +13,8 @@ using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; namespace LivestreamRecorderBackend.Functions; @@ -35,7 +34,7 @@ public User( _authenticationService = authenticationService; } - [FunctionName(nameof(GetUserAsync))] + [Function(nameof(GetUserAsync))] [OpenApiOperation(operationId: nameof(GetUserAsync), tags: new[] { nameof(User) })] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(GetUserResponse), Description = "User")] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "User not found.")] @@ -61,7 +60,7 @@ public async Task GetUserAsync( } - [FunctionName(nameof(CreateOrUpdateUser))] + [Function(nameof(CreateOrUpdateUser))] [OpenApiOperation(operationId: nameof(CreateOrUpdateUser), tags: new[] { nameof(User) })] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "The OK response")] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "Issuer not supported")] @@ -97,7 +96,7 @@ public async Task CreateOrUpdateUser( } } - [FunctionName(nameof(UpdateUserAsync))] + [Function(nameof(UpdateUserAsync))] [OpenApiOperation(operationId: nameof(UpdateUserAsync), tags: new[] { nameof(User) })] [OpenApiRequestBody("application/json", typeof(UpdateUserRequest), Required = true)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(GetUserResponse), Description = "User")] diff --git a/Functions/Utility.cs b/Functions/Utility.cs index ccd6f22..37c66cf 100644 --- a/Functions/Utility.cs +++ b/Functions/Utility.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Serilog; using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; namespace LivestreamRecorderBackend.Functions; @@ -18,17 +17,19 @@ public Utility( _logger = logger; } - [FunctionName(nameof(Wake))] + [Function(nameof(Wake))] [OpenApiOperation(operationId: nameof(Wake), tags: new[] { nameof(Utility) })] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "Waked.")] - public IActionResult Wake([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Utility/Wake")] HttpRequest req) + public IActionResult Wake( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Utility/Wake")] + HttpRequest req) { Wake(); return new OkResult(); } #if RELEASE && Windows - [FunctionName(nameof(WakeByTimer))] + [Function(nameof(WakeByTimer))] public void WakeByTimer([TimerTrigger("0 * * * * *")] TimerInfo timerInfo) => Wake(); #endif diff --git a/Functions/Video.cs b/Functions/Video.cs index 7f3b924..5b766cf 100644 --- a/Functions/Video.cs +++ b/Functions/Video.cs @@ -3,9 +3,6 @@ using LivestreamRecorderBackend.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Microsoft.OpenApi.Models; using Serilog; using System; @@ -16,6 +13,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using System.Linq; namespace LivestreamRecorderBackend.Functions; @@ -37,7 +37,7 @@ public Video( _frontEndUri = Environment.GetEnvironmentVariable("FrontEndUri") ?? "http://localhost:4200"; } - [FunctionName(nameof(AddVideoAsync))] + [Function(nameof(AddVideoAsync))] [OpenApiOperation(operationId: nameof(AddVideoAsync), tags: new[] { nameof(Video) })] [OpenApiRequestBody("application/json", typeof(AddVideoRequest), Required = true)] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "Ok")] @@ -83,7 +83,7 @@ public async Task AddVideoAsync( } } - [FunctionName(nameof(UpdateVideoAsync))] + [Function(nameof(UpdateVideoAsync))] [OpenApiOperation(operationId: nameof(UpdateVideoAsync), tags: new[] { nameof(Video) })] [OpenApiRequestBody("application/json", typeof(UpdateVideoRequest), Required = true)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Video), Description = "Video")] @@ -145,7 +145,7 @@ public async Task UpdateVideoAsync( } } - [FunctionName(nameof(RemoveVideoAsync))] + [Function(nameof(RemoveVideoAsync))] [OpenApiOperation(operationId: nameof(RemoveVideoAsync), tags: new[] { nameof(Video) })] [OpenApiParameter(name: "videoId", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "VideoId")] [OpenApiParameter(name: "channelId", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "ChannelId")] @@ -161,7 +161,7 @@ public async Task RemoveVideoAsync( if (null == user) return new UnauthorizedResult(); if (!user.IsAdmin) return new StatusCodeResult(403); - IDictionary queryDictionary = req.GetQueryParameterDictionary(); + IDictionary queryDictionary = req.Query.ToDictionary(p => p.Key, p => p.Value.Last()); queryDictionary.TryGetValue("videoId", out var videoId); queryDictionary.TryGetValue("channelId", out var channelId); @@ -196,7 +196,7 @@ public async Task RemoveVideoAsync( } } - [FunctionName(nameof(GetToken))] + [Function(nameof(GetToken))] [OpenApiOperation(operationId: nameof(GetToken), tags: new[] { nameof(Video) })] [OpenApiParameter(name: "videoId", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "VideoId")] [OpenApiParameter(name: "channelId", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "ChannelId")] @@ -215,7 +215,7 @@ public async Task GetToken( && !user.IsAdmin) return new StatusCodeResult(403); - IDictionary queryDictionary = req.GetQueryParameterDictionary(); + IDictionary queryDictionary = req.Query.ToDictionary(p => p.Key, p => p.Value.Last()); queryDictionary.TryGetValue("videoId", out var videoId); queryDictionary.TryGetValue("channelId", out var channelId); diff --git a/LivestreamRecorder.DB b/LivestreamRecorder.DB index b761244..b6b572b 160000 --- a/LivestreamRecorder.DB +++ b/LivestreamRecorder.DB @@ -1 +1 @@ -Subproject commit b761244f6a0d41f5134944df6abd8c560d493a5b +Subproject commit b6b572b0295288d61bb63f46060f02763d6afc85 diff --git a/LivestreamRecorderBackend.csproj b/LivestreamRecorderBackend.csproj index babcfd3..a1ad7e0 100644 --- a/LivestreamRecorderBackend.csproj +++ b/LivestreamRecorderBackend.csproj @@ -1,8 +1,9 @@  - net6.0 + net8.0 v4 enable + Exe true @@ -37,29 +38,33 @@ + + - - - - - - + + + + - - + + + + + + - + + - - + + - @@ -84,4 +89,7 @@ Never - \ No newline at end of file + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..234bf9b --- /dev/null +++ b/Program.cs @@ -0,0 +1,192 @@ +#if COUCHDB +using CouchDB.Driver.DependencyInjection; +using CouchDB.Driver.Options; +using LivestreamRecorder.DB.CouchDB; +using Serilog; +#endif +#if COSMOSDB +using LivestreamRecorder.DB.CosmosDB; +using Microsoft.EntityFrameworkCore; +#endif +using LivestreamRecorder.DB.Interfaces; +using LivestreamRecorder.DB.Models; +using LivestreamRecorderBackend.Interfaces; +using LivestreamRecorderBackend.Services; +using LivestreamRecorderBackend.Services.Authentication; +using LivestreamRecorderBackend.Services.StorageService; +using Microsoft.Extensions.DependencyInjection; +using Minio; +using System; +using System.Configuration; +using System.Net.Http.Headers; +using Azure.Identity; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Hosting; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; + +var builder = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices((context, services) => + { + services.AddHttpClient("client", + config => + { + config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(".NET", "8.0")); + config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Recorder.moe", "1.0")); + config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("(+https://recorder.moe)")); + }); + + services.AddSingleton(_ => + { + var options = new OpenApiConfigurationOptions() + { + Servers = DefaultOpenApiConfigurationOptions.GetHostNames(), + OpenApiVersion = OpenApiVersionType.V3, + IncludeRequestingHostName = true, + ForceHttps = false, + ForceHttp = false, + }; + + return options; + }); + + var logger = LivestreamRecorderBackend.Helper.Log.MakeLogger(); + services.AddSingleton(logger); + services.AddMemoryCache(option => option.SizeLimit = 1024); + + #region CosmosDB + +#if COSMOSDB + services.AddDbContext((options) => + { + options + //.EnableSensitiveDataLogging() + .UseCosmos(connectionString: Environment.GetEnvironmentVariable("CosmosDB_Public_ConnectionString")!, + databaseName: "Public", + cosmosOptionsAction: option => option.GatewayModeMaxConnectionLimit(380)); + }, + ServiceLifetime.Singleton, + ServiceLifetime.Singleton); + + services.AddDbContext((options) => + { + options + //.EnableSensitiveDataLogging() + .UseCosmos(connectionString: Environment.GetEnvironmentVariable("CosmosDB_Private_ConnectionString")!, + databaseName: "Private", + cosmosOptionsAction: option => option.GatewayModeMaxConnectionLimit(380)); + }, + ServiceLifetime.Singleton, + ServiceLifetime.Singleton); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton((s) => new VideoRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); + services.AddSingleton((s) => new ChannelRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); + services.AddSingleton((s) => new UserRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Private)))); +#endif + + #endregion + + #region CouchDB + +#if COUCHDB + services.AddCouchContext((options) => + { + options + .UseEndpoint(Environment.GetEnvironmentVariable("CouchDB_Endpoint")!) + .UseCookieAuthentication(username: Environment.GetEnvironmentVariable("CouchDB_Username")!, + password: Environment.GetEnvironmentVariable("CouchDB_Password")!) +#if !RELEASE + .ConfigureFlurlClient(setting + => setting.BeforeCall = call + => Log.Debug("Sending request to couch: {request} {body}", call, call.RequestBody)) +#endif + .SetPropertyCase(PropertyCaseType.None); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton((s) => new VideoRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); + services.AddSingleton( + (s) => new ChannelRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); + + services.AddSingleton((s) => new UserRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Private)))); +#endif + + #endregion + + #region Storage + + if (Environment.GetEnvironmentVariable("StorageService") == "AzureBlobStorage") + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ConnectionString")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ContainerNamePrivate")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ContainerNamePublic"))) + { + logger.Fatal( + "Invalid ENV for BlobStorage. Please set Blob_ConnectionString, Blob_ContainerNamePrivate, Blob_ContainerNamePublic"); + + throw new ConfigurationErrorsException( + "Invalid ENV for BlobStorage. Please set Blob_ConnectionString, Blob_ContainerNamePrivate, Blob_ContainerNamePublic"); + } + + services.AddAzureClients(clientsBuilder => + { + clientsBuilder.UseCredential(new DefaultAzureCredential()) + .AddBlobServiceClient(Environment.GetEnvironmentVariable("Blob_ConnectionString")); + }); + + services.AddSingleton(); + } + else if (Environment.GetEnvironmentVariable("StorageService") == "S3") + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_Endpoint")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_AccessKey")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_SecretKey")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_Secure")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_BucketNamePrivate")) + || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_BucketNamePublic"))) + { + logger.Fatal( + "Invalid ENV for S3. Please set S3_Endpoint, S3_AccessKey, S3_SecretKey, S3_Secure, S3_BucketNamePrivate, S3_BucketNamePublic"); + + throw new ConfigurationErrorsException( + "Invalid ENV for S3. Please set S3_Endpoint, S3_AccessKey, S3_SecretKey, S3_Secure, S3_BucketNamePrivate, S3_BucketNamePublic"); + } + + var minio = new MinioClient() + .WithEndpoint(Environment.GetEnvironmentVariable("S3_Endpoint")) + .WithCredentials(Environment.GetEnvironmentVariable("S3_AccessKey"), + Environment.GetEnvironmentVariable("S3_SecretKey")) + .WithSSL(bool.Parse(Environment.GetEnvironmentVariable("S3_Secure") ?? "false")) + .Build(); + + services.AddSingleton(minio); + services.AddSingleton(); + } + else + { + logger.Fatal("Invalid ENV StorageService. Should be AzureBlobStorage or S3."); + throw new ConfigurationErrorsException("Invalid ENV StorageService. Should be AzureBlobStorage or S3."); + } + + #endregion + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + +var host = builder.Build(); + +host.Run(); diff --git a/Services/StorageService/S3Service.cs b/Services/StorageService/S3Service.cs index 8447571..798db56 100644 --- a/Services/StorageService/S3Service.cs +++ b/Services/StorageService/S3Service.cs @@ -4,6 +4,7 @@ using LivestreamRecorder.DB.Models; using LivestreamRecorderBackend.Interfaces; using Minio; +using Minio.DataModel.Args; using Minio.Exceptions; using Serilog; diff --git a/Startup.cs b/Startup.cs deleted file mode 100644 index 043b99f..0000000 --- a/Startup.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Azure.Identity; -#if COUCHDB -using CouchDB.Driver.DependencyInjection; -using CouchDB.Driver.Options; -using LivestreamRecorder.DB.CouchDB; -using Serilog; -#endif -#if COSMOSDB -using LivestreamRecorder.DB.CosmosDB; -using Microsoft.EntityFrameworkCore; -#endif -using LivestreamRecorder.DB.Interfaces; -using LivestreamRecorder.DB.Models; -using LivestreamRecorderBackend.Interfaces; -using LivestreamRecorderBackend.Services; -using LivestreamRecorderBackend.Services.Authentication; -using LivestreamRecorderBackend.Services.StorageService; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.DependencyInjection; -using Minio; -using System; -using System.Configuration; -using System.Net.Http.Headers; - -[assembly: FunctionsStartup(typeof(LivestreamRecorderBackend.Startup))] - -namespace LivestreamRecorderBackend; - -public class Startup : FunctionsStartup -{ - public override void Configure(IFunctionsHostBuilder builder) - { - builder.Services.AddHttpClient("client", - config => - { - config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(".NET", "6.0")); - config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Recorder.moe", "1.0")); - config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("(+https://recorder.moe)")); - }); - - var logger = Helper.Log.MakeLogger(); - builder.Services.AddSingleton(logger); - builder.Services.AddMemoryCache(option => option.SizeLimit = 1024); - - #region CosmosDB - -#if COSMOSDB - builder.Services.AddDbContext((options) => - { - options - //.EnableSensitiveDataLogging() - .UseCosmos(connectionString: Environment.GetEnvironmentVariable("CosmosDB_Public_ConnectionString")!, - databaseName: "Public", - cosmosOptionsAction: option => option.GatewayModeMaxConnectionLimit(380)); - }, - ServiceLifetime.Singleton, - ServiceLifetime.Singleton); - - builder.Services.AddDbContext((options) => - { - options - //.EnableSensitiveDataLogging() - .UseCosmos(connectionString: Environment.GetEnvironmentVariable("CosmosDB_Private_ConnectionString")!, - databaseName: "Private", - cosmosOptionsAction: option => option.GatewayModeMaxConnectionLimit(380)); - }, - ServiceLifetime.Singleton, - ServiceLifetime.Singleton); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton((s) => new VideoRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); - builder.Services.AddSingleton((s) => new ChannelRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); - builder.Services.AddSingleton((s) => new UserRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Private)))); -#endif - - #endregion - - #region CouchDB - -#if COUCHDB - builder.Services.AddCouchContext((options) => - { - options - .UseEndpoint(Environment.GetEnvironmentVariable("CouchDB_Endpoint")!) - .UseCookieAuthentication(username: Environment.GetEnvironmentVariable("CouchDB_Username")!, password: Environment.GetEnvironmentVariable("CouchDB_Password")!) -#if !RELEASE - .ConfigureFlurlClient(setting - => setting.BeforeCall = call - => Log.Debug("Sending request to couch: {request} {body}", call, call.RequestBody)) -#endif - .SetPropertyCase(PropertyCaseType.None); - }); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton((s) => new VideoRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); - builder.Services.AddSingleton((s) => new ChannelRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Public)))); - builder.Services.AddSingleton((s) => new UserRepository((IUnitOfWork)s.GetRequiredService(typeof(UnitOfWork_Private)))); -#endif - - #endregion - - #region Storage - - if (Environment.GetEnvironmentVariable("StorageService") == "AzureBlobStorage") - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ConnectionString")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ContainerNamePrivate")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("Blob_ContainerNamePublic"))) - { - logger.Fatal("Invalid ENV for BlobStorage. Please set Blob_ConnectionString, Blob_ContainerNamePrivate, Blob_ContainerNamePublic"); - throw new ConfigurationErrorsException( - "Invalid ENV for BlobStorage. Please set Blob_ConnectionString, Blob_ContainerNamePrivate, Blob_ContainerNamePublic"); - } - - builder.Services.AddAzureClients(clientsBuilder => - { - clientsBuilder.UseCredential(new DefaultAzureCredential()) - .AddBlobServiceClient(Environment.GetEnvironmentVariable("Blob_ConnectionString")); - }); - - builder.Services.AddSingleton(); - } - else if (Environment.GetEnvironmentVariable("StorageService") == "S3") - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_Endpoint")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_AccessKey")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_SecretKey")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_Secure")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_BucketNamePrivate")) - || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("S3_BucketNamePublic"))) - { - logger.Fatal( - "Invalid ENV for S3. Please set S3_Endpoint, S3_AccessKey, S3_SecretKey, S3_Secure, S3_BucketNamePrivate, S3_BucketNamePublic"); - - throw new ConfigurationErrorsException( - "Invalid ENV for S3. Please set S3_Endpoint, S3_AccessKey, S3_SecretKey, S3_Secure, S3_BucketNamePrivate, S3_BucketNamePublic"); - } - - var minio = new MinioClient() - .WithEndpoint(Environment.GetEnvironmentVariable("S3_Endpoint")) - .WithCredentials(Environment.GetEnvironmentVariable("S3_AccessKey"), Environment.GetEnvironmentVariable("S3_SecretKey")) - .WithSSL(bool.Parse(Environment.GetEnvironmentVariable("S3_Secure") ?? "false")) - .Build(); - - builder.Services.AddSingleton(minio); - builder.Services.AddSingleton(); - } - else - { - logger.Fatal("Invalid ENV StorageService. Should be AzureBlobStorage or S3."); - throw new ConfigurationErrorsException("Invalid ENV StorageService. Should be AzureBlobStorage or S3."); - } - - #endregion - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 767eb85..2960e84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - 80:8080 environment: - CORS_SUPPORT_CREDENTIALS=true - - FUNCTIONS_WORKER_RUNTIME=dotnet + - FUNCTIONS_WORKER_RUNTIME=dotnet-isolated - CORS_ALLOWED_ORIGINS=["https://localhost:4200"] - FrontEndUri=https://localhost:4200 - StorageService=S3