From 635bed794e998c64cb9a0455399d083c50ac0ff6 Mon Sep 17 00:00:00 2001 From: Jamie Rees Date: Fri, 3 Jan 2025 15:19:01 +0000 Subject: [PATCH 1/5] 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 27ba142e96..7b08163ceb 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 30951a63b7..1ce993d894 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 0000000000..2f19331843 --- /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 0000000000..97b9da0bfe --- /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 0000000000..da2c9bba56 --- /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 0000000000..5508001080 --- /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 0000000000..ef1f50be80 --- /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 0000000000..3530bf9133 --- /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 8a55099633..caceb9b0ec 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -236,6 +236,8 @@ public static void RegisterServices(this IServiceCollection services) 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 0000000000..4952cd9470 --- /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 0000000000..ae04c1428b --- /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 0000000000..41043a24be --- /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 0f65112658..03cf9768d6 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 5d693f8345..d6cfa5fd73 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 @@

Welcome to Ombi!

+ + Database + + + +
@@ -82,5 +89,22 @@

All setup!

+ } @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 8f15f503ad..b8974e52fe 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 e8a9055308..a2a38e4614 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 501995ce61..917f46ad37 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 bb3bed5b6a..07f3d82cc9 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.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 @@ public async Task OmbiConfig([FromBody] OmbiConfigModel config) 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 @@ public async Task OmbiConfig([FromBody] OmbiConfigModel config) 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 c56e2f52de..b0f04d730b 100644 --- a/src/Ombi/Extensions/DatabaseExtensions.cs +++ b/src/Ombi/Extensions/DatabaseExtensions.cs @@ -8,6 +8,8 @@ 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 @@ public static void ConfigureDatabases(this IServiceCollection services, IHealthC 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 @@ public static void ConfigureDatabases(this IServiceCollection services, IHealthC 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 @@ public static void ConfigureDatabases(this IServiceCollection services, IHealthC 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 @@ public static void ConfigureSqlite(DbContextOptionsBuilder options, PerDatabaseC 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 0000000000..923b23b773 --- /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 4e7b55b8b8..0de46e8c54 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -54,10 +54,6 @@ - - - - From cbb22b9bc136b212f5d7c9193dcc235f5757e760 Mon Sep 17 00:00:00 2001 From: Jamie Rees Date: Fri, 3 Jan 2025 15:45:08 +0000 Subject: [PATCH 2/5] Added postgres --- src/.idea/.idea.Ombi/.idea/workspace.xml | 153 +++++++++++------- .../Services/DatabaseConfigurationService.cs | 5 - .../wizard/database/database.component.html | 52 +++++- .../app/wizard/database/database.component.ts | 59 +++++-- 4 files changed, 196 insertions(+), 73 deletions(-) diff --git a/src/.idea/.idea.Ombi/.idea/workspace.xml b/src/.idea/.idea.Ombi/.idea/workspace.xml index 1ce993d894..de55774d1e 100644 --- a/src/.idea/.idea.Ombi/.idea/workspace.xml +++ b/src/.idea/.idea.Ombi/.idea/workspace.xml @@ -1,25 +1,23 @@ + + - - - - - - - - - - + + + - + + @@ -237,27 +235,63 @@ + + + + + + - - - - - - - - + + + + + + + + - - - + - + + + + + + + + - + - - - - - - - - - - - - - - + + - - + @@ -505,7 +524,7 @@ file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs 48 - + @@ -518,7 +537,7 @@ file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs 59 - + @@ -531,7 +550,7 @@ file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs 49 - + @@ -544,7 +563,7 @@ file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs 30 - + @@ -554,6 +573,32 @@ + + file://$PROJECT_DIR$/Ombi/Controllers/V2/WizardController.cs + 112 + + + + + + + + + file://$PROJECT_DIR$/Ombi/Controllers/V2/WizardController.cs + 121 + + + + + + + diff --git a/src/Ombi.Core/Services/DatabaseConfigurationService.cs b/src/Ombi.Core/Services/DatabaseConfigurationService.cs index ef1f50be80..750499b196 100644 --- a/src/Ombi.Core/Services/DatabaseConfigurationService.cs +++ b/src/Ombi.Core/Services/DatabaseConfigurationService.cs @@ -2,16 +2,11 @@ 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; diff --git a/src/Ombi/ClientApp/src/app/wizard/database/database.component.html b/src/Ombi/ClientApp/src/app/wizard/database/database.component.html index 4952cd9470..40aa353b6b 100644 --- a/src/Ombi/ClientApp/src/app/wizard/database/database.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.html @@ -14,7 +14,7 @@

For more information on using alternate databases, see the documentation.

- +

Just press next to continue with SQLite @@ -27,16 +27,19 @@

+ This field is required
- + + This field is required
- + + This field is required
@@ -51,7 +54,47 @@

{{connectionString | async}}

-
+ +
+
+ + + +

+ Please enter your Postgres connection details below +

+
+ + + This field is required + +
+
+ + + This field is required + +
+
+ + + This field is required + +
+
+ + + +
+
+ + + +
+

{{connectionString | async}}

+
+ +
@@ -59,4 +102,3 @@

- diff --git a/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts b/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts index ae04c1428b..8c037f07fa 100644 --- a/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts @@ -3,6 +3,7 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { BehaviorSubject } from "rxjs"; import { WizardService } from "../services/wizard.service"; import { NotificationService } from "app/services"; +import { MatTabChangeEvent } from "@angular/material/tabs"; @Component({ templateUrl: "./database.component.html", @@ -19,7 +20,7 @@ export class DatabaseComponent implements OnInit { public ngOnInit(): void { this.form = this.fb.group({ - type: ["MySQL"], + type: [""], host: ["", [Validators.required]], port: [3306, [Validators.required]], name: ["ombi", [Validators.required]], @@ -28,24 +29,64 @@ export class DatabaseComponent implements OnInit { }); this.form.valueChanges.subscribe(x => { + console.log(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}`; + connection += `;User=${x.user}`; if (x.password) { - connection = `Server=${x.host};Port=${x.port};Database=${x.name};User=${x.user};Password=*******`; + connection += `;Password=*******`; } } + + if (x.type !== "MySQL") { + connection = connection.replace("Server", "Host").replace("User", "Username"); + } + this.connectionString.next(connection); }); } + public tabChange(event: MatTabChangeEvent) { + if (event.index === 0) { + this.form.reset(); + } + if (event.index === 1) { + this.form.reset({ + type: "MySQL", + host: "", + name: "ombi", + port: 3306, + }); + this.form.controls.type.setValue("MySQL"); + + } + if (event.index === 2) { + this.form.reset({ + type:"Postgres", + host: "", + name: "ombi", + port: 5432, + }); + + } + this.form.markAllAsTouched(); + } + 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); - }) + this.service.addDatabaseConfig(this.form.value).subscribe({ + next: () => { + this.notification.success(`Database configuration updated! Please now restart Ombi!`); + this.configuredDatabase.emit(); + }, + error: error => { + if (error.error.message) { + this.notification.error(error.error.message); + } else { + this.notification.error("Something went wrong, please check the logs"); + } + }, + }); } } From c0422cb7fa77d9bf0a7384c2a1fc0ce7f99ff460 Mon Sep 17 00:00:00 2001 From: Jamie Rees Date: Fri, 3 Jan 2025 15:46:59 +0000 Subject: [PATCH 3/5] Fix code scanning alert no. 238: Log entries created from user input Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/Ombi/Controllers/V2/WizardController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ombi/Controllers/V2/WizardController.cs b/src/Ombi/Controllers/V2/WizardController.cs index 07f3d82cc9..2b3ab4f625 100644 --- a/src/Ombi/Controllers/V2/WizardController.cs +++ b/src/Ombi/Controllers/V2/WizardController.cs @@ -90,7 +90,8 @@ public async Task DatabaseConfig([FromBody] WizardDatabaseConfigu return BadRequest(); } - _logger.LogInformation("Setting up database type: {0}", config.Type); + var sanitizedType = config.Type.Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""); + _logger.LogInformation("Setting up database type: {0}", sanitizedType); var connectionString = string.Empty; if (config.Type == IDatabaseConfigurationService.MySqlDatabase) From 4cb131d600fae11fa3a59d720c0e3cabd2b0ffb8 Mon Sep 17 00:00:00 2001 From: Jamie Rees Date: Fri, 3 Jan 2025 15:58:25 +0000 Subject: [PATCH 4/5] fixed tests --- src/.idea/.idea.Ombi/.idea/workspace.xml | 33 ++----------------- .../app/wizard/welcome/welcome.component.html | 2 +- src/Ombi/Controllers/V2/WizardController.cs | 2 +- tests/cypress/features/01-wizard/wizard.ts | 4 ++- .../page-objects/wizard/wizard.page.ts | 8 +++++ 5 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/.idea/.idea.Ombi/.idea/workspace.xml b/src/.idea/.idea.Ombi/.idea/workspace.xml index de55774d1e..5f23978638 100644 --- a/src/.idea/.idea.Ombi/.idea/workspace.xml +++ b/src/.idea/.idea.Ombi/.idea/workspace.xml @@ -6,9 +6,8 @@ - - - + +