From 635bed794e998c64cb9a0455399d083c50ac0ff6 Mon Sep 17 00:00:00 2001 From: Jamie Rees Date: Fri, 3 Jan 2025 15:19:01 +0000 Subject: [PATCH] feat(wizard): :sparkles: Added the ability to start with a different database --- src/.idea/.idea.Ombi/.idea/indexLayout.xml | 2 +- src/.idea/.idea.Ombi/.idea/workspace.xml | 10 +- .../Helpers/DatabaseConfigurationSetup.cs | 67 +++++++++++ src/Ombi.Core/Helpers/FileSystem.cs | 10 ++ src/Ombi.Core/Helpers/IFileSystem.cs | 7 ++ src/Ombi.Core/Models/DatabaseConfiguration.cs | 40 +++++++ .../Services/DatabaseConfigurationService.cs | 74 +++++++++++++ .../Services/IDatabaseConfigurationService.cs | 11 ++ src/Ombi.DependencyInjection/IocExtensions.cs | 2 + .../wizard/database/database.component.html | 62 +++++++++++ .../app/wizard/database/database.component.ts | 51 +++++++++ .../src/app/wizard/models/DatabaseSettings.ts | 13 +++ .../src/app/wizard/services/wizard.service.ts | 5 + .../app/wizard/welcome/welcome.component.html | 26 ++++- .../app/wizard/welcome/welcome.component.scss | 6 + .../app/wizard/welcome/welcome.component.ts | 7 +- .../ClientApp/src/app/wizard/wizard.module.ts | 2 + src/Ombi/Controllers/V2/WizardController.cs | 90 ++++++++++++++- src/Ombi/Extensions/DatabaseExtensions.cs | 104 ++---------------- .../Models/V2/WizardDatabaseConfiguration.cs | 3 + src/Ombi/Ombi.csproj | 4 - 21 files changed, 485 insertions(+), 111 deletions(-) create mode 100644 src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs create mode 100644 src/Ombi.Core/Helpers/FileSystem.cs create mode 100644 src/Ombi.Core/Helpers/IFileSystem.cs create mode 100644 src/Ombi.Core/Models/DatabaseConfiguration.cs create mode 100644 src/Ombi.Core/Services/DatabaseConfigurationService.cs create mode 100644 src/Ombi.Core/Services/IDatabaseConfigurationService.cs create mode 100644 src/Ombi/ClientApp/src/app/wizard/database/database.component.html create mode 100644 src/Ombi/ClientApp/src/app/wizard/database/database.component.ts create mode 100644 src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts create mode 100644 src/Ombi/Models/V2/WizardDatabaseConfiguration.cs diff --git a/src/.idea/.idea.Ombi/.idea/indexLayout.xml b/src/.idea/.idea.Ombi/.idea/indexLayout.xml index 27ba142e9..7b08163ce 100644 --- a/src/.idea/.idea.Ombi/.idea/indexLayout.xml +++ b/src/.idea/.idea.Ombi/.idea/indexLayout.xml @@ -1,6 +1,6 @@ - + diff --git a/src/.idea/.idea.Ombi/.idea/workspace.xml b/src/.idea/.idea.Ombi/.idea/workspace.xml index 30951a63b..1ce993d89 100644 --- a/src/.idea/.idea.Ombi/.idea/workspace.xml +++ b/src/.idea/.idea.Ombi/.idea/workspace.xml @@ -376,7 +376,7 @@ diff --git a/src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs b/src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs new file mode 100644 index 000000000..2f1933184 --- /dev/null +++ b/src/Ombi.Core/Helpers/DatabaseConfigurationSetup.cs @@ -0,0 +1,67 @@ +using System; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Ombi.Core.Models; +using Polly; +using Pomelo.EntityFrameworkCore.MySql.Storage.Internal; + +namespace Ombi.Core.Helpers; + +public static class DatabaseConfigurationSetup +{ + public static void ConfigurePostgres(DbContextOptionsBuilder options, PerDatabaseConfiguration config) + { + options.UseNpgsql(config.ConnectionString, b => + { + b.EnableRetryOnFailure(); + }).ReplaceService(); + } + + public static void ConfigureMySql(DbContextOptionsBuilder options, PerDatabaseConfiguration config) + { + if (string.IsNullOrEmpty(config.ConnectionString)) + { + throw new ArgumentNullException("ConnectionString for the MySql/Mariadb database is empty"); + } + + options.UseMySql(config.ConnectionString, GetServerVersion(config.ConnectionString), b => + { + //b.CharSetBehavior(Pomelo.EntityFrameworkCore.MySql.Infrastructure.CharSetBehavior.NeverAppend); // ##ISSUE, link to migrations? + b.EnableRetryOnFailure(); + }); + } + + private static ServerVersion GetServerVersion(string connectionString) + { + // Workaround Windows bug, that can lead to the following exception: + // + // MySqlConnector.MySqlException (0x80004005): SSL Authentication Error + // ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception. + // ---> System.ComponentModel.Win32Exception (0x8009030F): The message or signature supplied for verification has been altered + // + // See https://github.com/dotnet/runtime/issues/17005#issuecomment-305848835 + // + // Also workaround for the fact, that ServerVersion.AutoDetect() does not use any retrying strategy. + ServerVersion serverVersion = null; +#pragma warning disable EF1001 + var retryPolicy = Policy.Handle(exception => MySqlTransientExceptionDetector.ShouldRetryOn(exception)) +#pragma warning restore EF1001 + .WaitAndRetry(3, (count, context) => TimeSpan.FromMilliseconds(count * 250)); + + serverVersion = retryPolicy.Execute(() => serverVersion = ServerVersion.AutoDetect(connectionString)); + + return serverVersion; + } + public class NpgsqlCaseInsensitiveSqlGenerationHelper : NpgsqlSqlGenerationHelper + { + const string EFMigrationsHisory = "__EFMigrationsHistory"; + public NpgsqlCaseInsensitiveSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies) + : base(dependencies) { } + public override string DelimitIdentifier(string identifier) => + base.DelimitIdentifier(identifier == EFMigrationsHisory ? identifier : identifier.ToLower()); + public override void DelimitIdentifier(StringBuilder builder, string identifier) + => base.DelimitIdentifier(builder, identifier == EFMigrationsHisory ? identifier : identifier.ToLower()); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Helpers/FileSystem.cs b/src/Ombi.Core/Helpers/FileSystem.cs new file mode 100644 index 000000000..97b9da0bf --- /dev/null +++ b/src/Ombi.Core/Helpers/FileSystem.cs @@ -0,0 +1,10 @@ +namespace Ombi.Core.Helpers; + +public class FileSystem : IFileSystem +{ + public bool FileExists(string path) + { + return System.IO.File.Exists(path); + } + // Implement other file system operations as needed +} \ No newline at end of file diff --git a/src/Ombi.Core/Helpers/IFileSystem.cs b/src/Ombi.Core/Helpers/IFileSystem.cs new file mode 100644 index 000000000..da2c9bba5 --- /dev/null +++ b/src/Ombi.Core/Helpers/IFileSystem.cs @@ -0,0 +1,7 @@ +namespace Ombi.Core.Helpers; + +public interface IFileSystem +{ + bool FileExists(string path); + // Add other file system operations as needed +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/DatabaseConfiguration.cs b/src/Ombi.Core/Models/DatabaseConfiguration.cs new file mode 100644 index 000000000..550800108 --- /dev/null +++ b/src/Ombi.Core/Models/DatabaseConfiguration.cs @@ -0,0 +1,40 @@ +using System.IO; + +namespace Ombi.Core.Models; + +public class DatabaseConfiguration +{ + public const string SqliteDatabase = "Sqlite"; + + public DatabaseConfiguration() + { + + } + + public DatabaseConfiguration(string defaultSqlitePath) + { + OmbiDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "Ombi.db")}"); + SettingsDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiSettings.db")}"); + ExternalDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiExternal.db")}"); + } + public PerDatabaseConfiguration OmbiDatabase { get; set; } + public PerDatabaseConfiguration SettingsDatabase { get; set; } + public PerDatabaseConfiguration ExternalDatabase { get; set; } +} + +public class PerDatabaseConfiguration +{ + public PerDatabaseConfiguration(string type, string connectionString) + { + Type = type; + ConnectionString = connectionString; + } + + // Used in Deserialization + public PerDatabaseConfiguration() + { + + } + public string Type { get; set; } + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/src/Ombi.Core/Services/DatabaseConfigurationService.cs b/src/Ombi.Core/Services/DatabaseConfigurationService.cs new file mode 100644 index 000000000..ef1f50be8 --- /dev/null +++ b/src/Ombi.Core/Services/DatabaseConfigurationService.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ombi.Core.Helpers; +using Ombi.Core.Models; +using Ombi.Helpers; +using Ombi.Store.Context; +using Ombi.Store.Context.MySql; +using Ombi.Store.Context.Postgres; + +namespace Ombi.Core.Services; + +public class DatabaseConfigurationService : IDatabaseConfigurationService +{ + + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + + public DatabaseConfigurationService( + ILogger logger, + IFileSystem fileSystem) + { + _logger = logger; + _fileSystem = fileSystem; + } + + public async Task ConfigureDatabase(string databaseType, string connectionString, CancellationToken token) + { + var i = StartupSingleton.Instance; + if (string.IsNullOrEmpty(i.StoragePath)) + { + i.StoragePath = string.Empty; + } + + var databaseFileLocation = Path.Combine(i.StoragePath, "database.json"); + if (_fileSystem.FileExists(databaseFileLocation)) + { + var error = $"The database file at '{databaseFileLocation}' already exists"; + _logger.LogError(error); + return false; + } + + var configuration = new DatabaseConfiguration + { + ExternalDatabase = new PerDatabaseConfiguration(databaseType, connectionString), + OmbiDatabase = new PerDatabaseConfiguration(databaseType, connectionString), + SettingsDatabase = new PerDatabaseConfiguration(databaseType, connectionString) + }; + + var json = JsonConvert.SerializeObject(configuration, Formatting.Indented); + + _logger.LogInformation("Writing database configuration to file"); + + try + { + await File.WriteAllTextAsync(databaseFileLocation, json, token); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to write database configuration to file"); + return false; + } + + _logger.LogInformation("Database configuration written to file"); + + + return true; + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Services/IDatabaseConfigurationService.cs b/src/Ombi.Core/Services/IDatabaseConfigurationService.cs new file mode 100644 index 000000000..3530bf913 --- /dev/null +++ b/src/Ombi.Core/Services/IDatabaseConfigurationService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Ombi.Core.Services; + +public interface IDatabaseConfigurationService +{ + const string MySqlDatabase = "MySQL"; + const string PostgresDatabase = "Postgres"; + Task ConfigureDatabase(string databaseType, string connectionString, CancellationToken token); +} \ No newline at end of file diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 8a5509963..caceb9b0e 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -236,6 +236,8 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); } public static void RegisterJobs(this IServiceCollection services) diff --git a/src/Ombi/ClientApp/src/app/wizard/database/database.component.html b/src/Ombi/ClientApp/src/app/wizard/database/database.component.html new file mode 100644 index 000000000..4952cd947 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.html @@ -0,0 +1,62 @@ +
+
+ +
+
+
+

Choose a Database

+

+ SQLite is the default option and the easiest to set up, as it requires no additional configuration. +
However, it has significant limitations, including potential performance issues and database locking. +
While many users start with SQLite and later migrate to MySQL or MariaDB, we recommend beginning with MySQL or MariaDB from the start for a more robust and scalable experience. +
+
+ For more information on using alternate databases, see the documentation. +

+
+ + +

+ Just press next to continue with SQLite +

+
+ +

+ Please enter your MySQL/MariaDB connection details below +

+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+

{{connectionString | async}}

+
+
+
+
+
+
+
+
+
+ diff --git a/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts b/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts new file mode 100644 index 000000000..ae04c1428 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, OnInit, Output } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { BehaviorSubject } from "rxjs"; +import { WizardService } from "../services/wizard.service"; +import { NotificationService } from "app/services"; + +@Component({ + templateUrl: "./database.component.html", + styleUrls: ["../welcome/welcome.component.scss"], + selector: "wizard-database-selector", +}) +export class DatabaseComponent implements OnInit { + public constructor(private fb: FormBuilder, private service: WizardService, private notification: NotificationService) { } + @Output() public configuredDatabase = new EventEmitter(); + + public form: FormGroup; + + public connectionString = new BehaviorSubject("Server=;Port=3306;Database=ombi"); + + public ngOnInit(): void { + this.form = this.fb.group({ + type: ["MySQL"], + host: ["", [Validators.required]], + port: [3306, [Validators.required]], + name: ["ombi", [Validators.required]], + user: [""], + password: [""], + }); + + this.form.valueChanges.subscribe(x => { + let connection = `Server=${x.host};Port=${x.port};Database=${x.name}`; + if (x.user) { + connection = `Server=${x.host};Port=${x.port};Database=${x.name};User=${x.user}`; + if (x.password) { + connection = `Server=${x.host};Port=${x.port};Database=${x.name};User=${x.user};Password=*******`; + } + } + this.connectionString.next(connection); + }); + } + + public save() { + this.service.addDatabaseConfig(this.form.value).subscribe(x => { + this.notification.success(`Database configuration updated! Please now restart ombi!`); + this.configuredDatabase.emit(); + }, error => { + this.notification.error(error.error.message); + }) + } + +} diff --git a/src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts b/src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts new file mode 100644 index 000000000..41043a24b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/models/DatabaseSettings.ts @@ -0,0 +1,13 @@ +export interface DatabaseSettings { + type: string; + host: string; + port: number; + name: string; + user: string; + password: string; +} + +export interface DatabaseConfigurationResult { + success: boolean; + message: string; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts b/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts index 0f6511265..03cf9768d 100644 --- a/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts +++ b/src/Ombi/ClientApp/src/app/wizard/services/wizard.service.ts @@ -5,6 +5,7 @@ import { Observable } from "rxjs"; import { ICustomizationSettings } from "../../interfaces"; import { ServiceHelpers } from "../../services"; import { IOmbiConfigModel } from "../models/OmbiConfigModel"; +import { DatabaseConfigurationResult, DatabaseSettings } from "../models/DatabaseSettings"; @Injectable() @@ -16,4 +17,8 @@ export class WizardService extends ServiceHelpers { public addOmbiConfig(config: IOmbiConfigModel): Observable { return this.http.post(`${this.url}config`, config, {headers: this.headers}); } + + public addDatabaseConfig(config: DatabaseSettings): Observable { + return this.http.post(`${this.url}database`, config, {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html index 5d693f834..d6cfa5fd7 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html @@ -1,6 +1,7 @@ 
- + @if (!needsRestart) { +
Welcome @@ -29,6 +30,12 @@
+ + Database + + + +
@@ -82,5 +89,22 @@
+ } @else { + + + Restart +
+
+ +
+
+
+

Please Restart Ombi for the database changes to take effect!

+
+
+
+
+
+ } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss index 8f15f503a..b8974e52f 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.scss @@ -151,6 +151,12 @@ p.space-or{ color: #A45FC4; } + +.viewon-btn.database { + border: 1px solid #A45FC4; + color: #A45FC4; +} + .text-logo{ font-size:12em; } diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts index e8a905530..a2a38e461 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.ts @@ -17,6 +17,7 @@ export class WelcomeComponent implements OnInit { @ViewChild('stepper', {static: false}) public stepper: MatStepper; public localUser: ICreateWizardUser; + public needsRestart: boolean = false; public config: IOmbiConfigModel; constructor(private router: Router, private identityService: IdentityService, @@ -48,7 +49,7 @@ export class WelcomeComponent implements OnInit { this.settingsService.verifyUrl(this.config.applicationUrl).subscribe(x => { if (!x) { this.notificationService.error(`The URL "${this.config.applicationUrl}" is not valid. Please format it correctly e.g. http://www.google.com/`); - this.stepper.selectedIndex = 3; + this.stepper.selectedIndex = 4; return; } this.saveConfig(); @@ -58,6 +59,10 @@ export class WelcomeComponent implements OnInit { } } + public databaseConfigured() { + this.needsRestart = true; + } + private saveConfig() { this.WizardService.addOmbiConfig(this.config).subscribe({ next: (config) => { diff --git a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts index 501995ce6..917f46ad3 100644 --- a/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts +++ b/src/Ombi/ClientApp/src/app/wizard/wizard.module.ts @@ -12,6 +12,7 @@ import { MediaServerComponent } from "./mediaserver/mediaserver.component"; import { PlexComponent } from "./plex/plex.component"; import { WelcomeComponent } from "./welcome/welcome.component"; import { OmbiConfigComponent } from "./ombiconfig/ombiconfig.component"; +import { DatabaseComponent } from "./database/database.component"; import { EmbyService } from "../services"; import { JellyfinService } from "../services"; @@ -48,6 +49,7 @@ const routes: Routes = [ EmbyComponent, JellyfinComponent, OmbiConfigComponent, + DatabaseComponent, ], exports: [ RouterModule, diff --git a/src/Ombi/Controllers/V2/WizardController.cs b/src/Ombi/Controllers/V2/WizardController.cs index bb3bed5b6..07f3d82cc 100644 --- a/src/Ombi/Controllers/V2/WizardController.cs +++ b/src/Ombi/Controllers/V2/WizardController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Authorization; +using System; +using System.Threading; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Ombi.Attributes; using Ombi.Core.Settings; @@ -6,6 +8,10 @@ using Ombi.Helpers; using Ombi.Models.V2; using Ombi.Settings.Settings.Models; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using MySqlConnector; +using Npgsql; +using Ombi.Core.Services; namespace Ombi.Controllers.V2 { @@ -13,15 +19,25 @@ namespace Ombi.Controllers.V2 [AllowAnonymous] public class WizardController : V2Controller { + private readonly ISettingsService _ombiSettings; + private readonly IDatabaseConfigurationService _databaseConfigurationService; + private readonly ILogger _logger; private ISettingsService _customizationSettings { get; } - public WizardController(ISettingsService customizationSettings) + public WizardController( + ISettingsService customizationSettings, + ISettingsService ombiSettings, + IDatabaseConfigurationService databaseConfigurationService, + ILogger logger) { + _ombiSettings = ombiSettings; + _databaseConfigurationService = databaseConfigurationService; + _logger = logger; _customizationSettings = customizationSettings; } [HttpPost("config")] - [ApiExplorerSettings(IgnoreApi =true)] + [ApiExplorerSettings(IgnoreApi = true)] public async Task OmbiConfig([FromBody] OmbiConfigModel config) { if (config == null) @@ -29,6 +45,13 @@ namespace Ombi.Controllers.V2 return BadRequest(); } + var ombiSettings = await _ombiSettings.GetSettingsAsync(); + if (ombiSettings.Wizard) + { + _logger.LogError("Wizard has already been completed"); + return BadRequest(); + } + var settings = await _customizationSettings.GetSettingsAsync(); if (config.ApplicationName.HasValue()) @@ -50,5 +73,66 @@ namespace Ombi.Controllers.V2 return new OkObjectResult(settings); } + + [HttpPost("database")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task DatabaseConfig([FromBody] WizardDatabaseConfiguration config, CancellationToken token) + { + if (config == null) + { + return BadRequest(); + } + + var ombiSettings = await _ombiSettings.GetSettingsAsync(); + if (ombiSettings.Wizard) + { + _logger.LogError("Wizard has already been completed"); + return BadRequest(); + } + + _logger.LogInformation("Setting up database type: {0}", config.Type); + + var connectionString = string.Empty; + if (config.Type == IDatabaseConfigurationService.MySqlDatabase) + { + _logger.LogInformation("Building MySQL connectionstring"); + var builder = new MySqlConnectionStringBuilder + { + Database = config.Name, + Port = Convert.ToUInt32(config.Port), + Server = config.Host, + UserID = config.User, + Password = config.Password + }; + + connectionString = builder.ToString(); + } + + if (config.Type == IDatabaseConfigurationService.PostgresDatabase) + { + _logger.LogInformation("Building Postgres connectionstring"); + var builder = new NpgsqlConnectionStringBuilder + { + Host = config.Host, + Port = config.Port, + Database = config.Name, + Username = config.User, + Password = config.Password + }; + connectionString = builder.ToString(); + } + + var result = await _databaseConfigurationService.ConfigureDatabase(config.Type, connectionString, token); + + if (!result) + { + return BadRequest(new DatabaseConfigurationResult(false, "Could not configure the database, please check the logs")); + } + + return Ok(new DatabaseConfigurationResult(true, "Database configured successfully")); + } + + public record DatabaseConfigurationResult(bool Success, string Message); + } } diff --git a/src/Ombi/Extensions/DatabaseExtensions.cs b/src/Ombi/Extensions/DatabaseExtensions.cs index c56e2f52d..b0f04d730 100644 --- a/src/Ombi/Extensions/DatabaseExtensions.cs +++ b/src/Ombi/Extensions/DatabaseExtensions.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using MySqlConnector; using Newtonsoft.Json; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Ombi.Core.Helpers; +using Ombi.Core.Models; using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Context.MySql; @@ -38,11 +40,11 @@ namespace Ombi.Extensions AddSqliteHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase); break; case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigureMySql(x, configuration.OmbiDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.OmbiDatabase)); AddMySqlHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase); break; case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigurePostgres(x, configuration.OmbiDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.OmbiDatabase)); AddPostgresHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase); break; } @@ -54,11 +56,11 @@ namespace Ombi.Extensions AddSqliteHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase); break; case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigureMySql(x, configuration.ExternalDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.ExternalDatabase)); AddMySqlHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase); break; case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigurePostgres(x, configuration.ExternalDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.ExternalDatabase)); AddPostgresHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase); break; } @@ -70,11 +72,11 @@ namespace Ombi.Extensions AddSqliteHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase); break; case var type when type.Equals(MySqlDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigureMySql(x, configuration.SettingsDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.SettingsDatabase)); AddMySqlHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase); break; case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase): - services.AddDbContext(x => ConfigurePostgres(x, configuration.SettingsDatabase)); + services.AddDbContext(x => DatabaseConfigurationSetup.ConfigurePostgres(x, configuration.SettingsDatabase)); AddPostgresHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase); break; } @@ -150,95 +152,5 @@ namespace Ombi.Extensions SQLitePCL.raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD); options.UseSqlite(config.ConnectionString); } - - public static void ConfigureMySql(DbContextOptionsBuilder options, PerDatabaseConfiguration config) - { - if (string.IsNullOrEmpty(config.ConnectionString)) - { - throw new ArgumentNullException("ConnectionString for the MySql/Mariadb database is empty"); - } - - options.UseMySql(config.ConnectionString, GetServerVersion(config.ConnectionString), b => - { - //b.CharSetBehavior(Pomelo.EntityFrameworkCore.MySql.Infrastructure.CharSetBehavior.NeverAppend); // ##ISSUE, link to migrations? - b.EnableRetryOnFailure(); - }); - } - - public static void ConfigurePostgres(DbContextOptionsBuilder options, PerDatabaseConfiguration config) - { - options.UseNpgsql(config.ConnectionString, b => - { - b.EnableRetryOnFailure(); - }).ReplaceService(); - } - - private static ServerVersion GetServerVersion(string connectionString) - { - // Workaround Windows bug, that can lead to the following exception: - // - // MySqlConnector.MySqlException (0x80004005): SSL Authentication Error - // ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception. - // ---> System.ComponentModel.Win32Exception (0x8009030F): The message or signature supplied for verification has been altered - // - // See https://github.com/dotnet/runtime/issues/17005#issuecomment-305848835 - // - // Also workaround for the fact, that ServerVersion.AutoDetect() does not use any retrying strategy. - ServerVersion serverVersion = null; -#pragma warning disable EF1001 - var retryPolicy = Policy.Handle(exception => MySqlTransientExceptionDetector.ShouldRetryOn(exception)) -#pragma warning restore EF1001 - .WaitAndRetry(3, (count, context) => TimeSpan.FromMilliseconds(count * 250)); - - serverVersion = retryPolicy.Execute(() => serverVersion = ServerVersion.AutoDetect(connectionString)); - - return serverVersion; - } - - public class DatabaseConfiguration - { - public DatabaseConfiguration() - { - - } - - public DatabaseConfiguration(string defaultSqlitePath) - { - OmbiDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "Ombi.db")}"); - SettingsDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiSettings.db")}"); - ExternalDatabase = new PerDatabaseConfiguration(SqliteDatabase, $"Data Source={Path.Combine(defaultSqlitePath, "OmbiExternal.db")}"); - } - public PerDatabaseConfiguration OmbiDatabase { get; set; } - public PerDatabaseConfiguration SettingsDatabase { get; set; } - public PerDatabaseConfiguration ExternalDatabase { get; set; } - } - - public class PerDatabaseConfiguration - { - public PerDatabaseConfiguration(string type, string connectionString) - { - Type = type; - ConnectionString = connectionString; - } - - // Used in Deserialization - public PerDatabaseConfiguration() - { - - } - public string Type { get; set; } - public string ConnectionString { get; set; } - } - - public class NpgsqlCaseInsensitiveSqlGenerationHelper : NpgsqlSqlGenerationHelper - { - const string EFMigrationsHisory = "__EFMigrationsHistory"; - public NpgsqlCaseInsensitiveSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies) - : base(dependencies) { } - public override string DelimitIdentifier(string identifier) => - base.DelimitIdentifier(identifier == EFMigrationsHisory ? identifier : identifier.ToLower()); - public override void DelimitIdentifier(StringBuilder builder, string identifier) - => base.DelimitIdentifier(builder, identifier == EFMigrationsHisory ? identifier : identifier.ToLower()); - } } } diff --git a/src/Ombi/Models/V2/WizardDatabaseConfiguration.cs b/src/Ombi/Models/V2/WizardDatabaseConfiguration.cs new file mode 100644 index 000000000..923b23b77 --- /dev/null +++ b/src/Ombi/Models/V2/WizardDatabaseConfiguration.cs @@ -0,0 +1,3 @@ +namespace Ombi.Models.V2; + +public record WizardDatabaseConfiguration(string Type, string Host, int Port, string Name, string User, string Password); \ No newline at end of file diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 4e7b55b8b..0de46e8c5 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -54,10 +54,6 @@ - - - -