using Serilog; using Tiku.Application; using Tiku.Application.PlatformBilling; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Infrastructure; using Tiku.Infrastructure.Assets; using Tiku.Infrastructure.Storage; using Tiku.Infrastructure.Observability; using OpenTelemetry.Metrics; namespace Tiku.Worker; internal static class WorkerDependencyInjection { internal static HostApplicationBuilder AddWorkerServices(this HostApplicationBuilder builder) { builder.Services.AddSerilog((services, configuration) => configuration .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext(), preserveStaticLogger: true); var connectionString = builder.Configuration.GetConnectionString("Database") ?? builder.Configuration["DATABASE_URL"] ?? (builder.Environment.IsDevelopment() ? $"Host=localhost;Database=tiku;Username={Environment.UserName}" : throw new InvalidOperationException( "Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL.")); builder.Services.AddApplication(); builder.Services.AddInfrastructure(connectionString); var otlpEndpoint = builder.Configuration["OpenTelemetry:OtlpEndpoint"]; var telemetry = builder.Services.AddOpenTelemetry().WithMetrics(metrics => metrics.AddMeter(WorkerTelemetry.MeterName)); if (Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var endpoint)) { telemetry.WithMetrics(metrics => metrics.AddOtlpExporter(options => options.Endpoint = endpoint)); } builder.Services.Configure(builder.Configuration.GetSection(ObjectStorageOptions.SectionName)); builder.Services.Configure(builder.Configuration.GetSection(AliyunOssOptions.SectionName)); builder.Services.PostConfigure(options => { options.DefaultProvider = builder.Configuration["STORAGE_DEFAULT_PROVIDER"] ?? options.DefaultProvider; options.DefaultBucket = builder.Configuration["STORAGE_DEFAULT_BUCKET"] ?? options.DefaultBucket; options.PublicBaseUrl = builder.Configuration["STORAGE_PUBLIC_BASE_URL"] ?? options.PublicBaseUrl; options.AllowedMimePrefixes = SplitLegacyList( builder.Configuration["STORAGE_ALLOWED_MIME_PREFIXES"], options.AllowedMimePrefixes); options.AllowedMimeTypes = SplitLegacyList( builder.Configuration["STORAGE_ALLOWED_MIME_TYPES"], options.AllowedMimeTypes); options.RequireTenantPrefix = bool.TryParse( builder.Configuration["STORAGE_REQUIRE_TENANT_PREFIX"], out var requireTenantPrefix) ? requireTenantPrefix : options.RequireTenantPrefix; options.MaxUploadBytes = long.TryParse( builder.Configuration["STORAGE_MAX_UPLOAD_BYTES"], out var maxUploadBytes) ? maxUploadBytes : options.MaxUploadBytes; }); builder.Services.PostConfigure(options => { options.Region = builder.Configuration["ALIYUN_OSS_REGION"] ?? options.Region; options.Endpoint = builder.Configuration["ALIYUN_OSS_ENDPOINT"] ?? options.Endpoint; options.AccessKeyId = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ?? options.AccessKeyId; options.AccessKeySecret = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ?? options.AccessKeySecret; options.SecurityToken = builder.Configuration["ALIYUN_OSS_STS_TOKEN"] ?? options.SecurityToken; options.UseInternalEndpoint = bool.TryParse( builder.Configuration["ALIYUN_OSS_INTERNAL"], out var useInternalEndpoint) ? useInternalEndpoint : options.UseInternalEndpoint; }); builder.Services.AddOptions() .Validate( options => !builder.Environment.IsProduction() || options.DefaultProvider == Tiku.Application.Storage.ObjectStorageProviders.AliyunOss, "Production managed storage must use the configured Aliyun OSS provider.") .ValidateOnStart(); builder.Services.AddOptions() .Validate>( (aliyun, storage) => !builder.Environment.IsProduction() || storage.Value.DefaultProvider != Tiku.Application.Storage.ObjectStorageProviders.AliyunOss || aliyun.IsConfigured, "Aliyun OSS credentials and region or endpoint are required when it is the default provider.") .ValidateOnStart(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(ClamAvOptions.SectionName)) .Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.") .Validate>( (clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes, "ClamAV StreamMaxLength must cover the storage max upload size.") .ValidateOnStart(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(WorkerOptions.SectionName)) .Validate(WorkerOptions.BeValid, "Worker settings are invalid.") .ValidateOnStart(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("TenantDomains")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("SaasSubscriptions")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("FeatureUsageReconciliation")); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); return builder; } private static string[] SplitLegacyList(string? value, string[] fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); }