feat(wizard): Added the ability to start with a different database

This commit is contained in:
Jamie Rees 2025-01-03 15:19:01 +00:00
commit 635bed794e
21 changed files with 485 additions and 111 deletions

View file

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

View file

@ -376,7 +376,7 @@
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" enabled="true" />
<option name="Build" />
</method>
</configuration>
<configuration name="Ombi: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
@ -391,7 +391,7 @@
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" enabled="true" />
<option name="Build" />
</method>
</configuration>
<configuration name="Ombi.Schedule.Tests" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
@ -406,7 +406,7 @@
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" enabled="true" />
<option name="Build" />
</method>
</configuration>
<configuration name="Ombi.Schedule.Tests: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
@ -421,7 +421,7 @@
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" enabled="true" />
<option name="Build" />
</method>
</configuration>
<configuration name="Ombi.Updater" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
@ -436,7 +436,7 @@
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" enabled="true" />
<option name="Build" />
</method>
</configuration>
</component>

View file

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

View file

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

View file

@ -0,0 +1,7 @@
namespace Ombi.Core.Helpers;
public interface IFileSystem
{
bool FileExists(string path);
// Add other file system operations as needed
}

View file

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

View file

@ -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<DatabaseConfigurationService> logger,
IFileSystem fileSystem)
{
_logger = logger;
_fileSystem = fileSystem;
}
public async Task<bool> 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;
}
}

View file

@ -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<bool> ConfigureDatabase(string databaseType, string connectionString, CancellationToken token);
}

View file

@ -236,6 +236,8 @@ namespace Ombi.DependencyInjection
services.AddScoped<IFeatureService, FeatureService>();
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
services.AddTransient<IPlexService, PlexService>();
services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IDatabaseConfigurationService, DatabaseConfigurationService>();
}
public static void RegisterJobs(this IServiceCollection services)

View file

@ -0,0 +1,62 @@
<div class="mediaserver-container">
<div class="left-container mediaserver">
<i class="fa fa-database text-logo"></i>
</div>
<div class="right-container mediaserver">
<div class="right-container-content mediaserver">
<h1>Choose a Database</h1>
<h4>
SQLite is the default option and the easiest to set up, as it requires no additional configuration.
<br>However, it has significant limitations, including potential performance issues and database locking.
<br>While many users start with SQLite and later migrate to MySQL or MariaDB, we <b>recommend</b> beginning with MySQL or MariaDB from the start for a more robust and scalable experience.
<br/>
<br/>
For more information on using alternate databases, <a target="_blank" href="https://docs.ombi.app/info/alternate-databases/">see the documentation.</a>
</h4>
<form [formGroup]="form">
<mat-tab-group>
<mat-tab label="SQLite">
<p class="space-or">
Just press next to continue with SQLite
</p>
</mat-tab>
<mat-tab label="MySQL/MariaDB">
<p class="space-or">
Please enter your MySQL/MariaDB connection details below
</p>
<div>
<mat-form-field>
<input matInput type="text" formControlName="host" id="host" placeholder="Host">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput type="text" formControlName="port" id="port" placeholder="Port">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput type="text" formControlName="name" id="database" placeholder="Database Name">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput type="text" formControlName="user" id="user" placeholder="User">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput type="password" formControlName="password" id="password" placeholder="Password">
</mat-form-field>
</div>
<p>{{connectionString | async}}</p>
<div style="text-align: center; margin-top: 20px">
<button (click)="save()" id="databaseSave" mat-raised-button color="accent" type="button" class="viewon-btn database">Save</button><div id="spinner"></div>
</div>
</mat-tab>
</mat-tab-group>
</form>
</div>
</div>
</div>

View file

@ -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<void>();
public form: FormGroup;
public connectionString = new BehaviorSubject<string>("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);
})
}
}

View file

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

View file

@ -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<ICustomizationSettings> {
return this.http.post<ICustomizationSettings>(`${this.url}config`, config, {headers: this.headers});
}
public addDatabaseConfig(config: DatabaseSettings): Observable<DatabaseConfigurationResult> {
return this.http.post<DatabaseConfigurationResult>(`${this.url}database`, config, {headers: this.headers});
}
}

View file

@ -1,6 +1,7 @@
<div class="wizard-background">
<div class="container wizard-inner">
<mat-stepper linear #stepper>
@if (!needsRestart) {
<mat-stepper linear #stepper>
<mat-step >
<form >
<ng-template matStepLabel>Welcome</ng-template>
@ -29,6 +30,12 @@
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>Database</ng-template>
<wizard-database-selector (configuredDatabase)="databaseConfigured()"></wizard-database-selector>
<button mat-button matStepperPrevious class="mat-raised-button mat-error left">Back</button>
<button mat-button matStepperNext class="mat-raised-button mat-accent right" data-test="nextMediaServer">Next</button>
</mat-step>
<mat-step [optional]="true">
<form >
@ -82,5 +89,22 @@
</div>
</mat-step>
</mat-stepper>
} @else {
<mat-stepper linear>
<mat-step >
<ng-template matStepLabel>Restart</ng-template>
<div class="welcome-container">
<div class="left-container mediaserver">
<i class="fa fa-database text-logo"></i>
</div>
<div class="right-container">
<div class="right-container-content">
<h1>Please Restart Ombi for the database changes to take effect!</h1>
</div>
</div>
</div>
</mat-step>
</mat-stepper>
}
</div>
</div>

View file

@ -151,6 +151,12 @@ p.space-or{
color: #A45FC4;
}
.viewon-btn.database {
border: 1px solid #A45FC4;
color: #A45FC4;
}
.text-logo{
font-size:12em;
}

View file

@ -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) => {

View file

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

View file

@ -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> _ombiSettings;
private readonly IDatabaseConfigurationService _databaseConfigurationService;
private readonly ILogger _logger;
private ISettingsService<CustomizationSettings> _customizationSettings { get; }
public WizardController(ISettingsService<CustomizationSettings> customizationSettings)
public WizardController(
ISettingsService<CustomizationSettings> customizationSettings,
ISettingsService<OmbiSettings> ombiSettings,
IDatabaseConfigurationService databaseConfigurationService,
ILogger<WizardController> logger)
{
_ombiSettings = ombiSettings;
_databaseConfigurationService = databaseConfigurationService;
_logger = logger;
_customizationSettings = customizationSettings;
}
[HttpPost("config")]
[ApiExplorerSettings(IgnoreApi =true)]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> 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<IActionResult> 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);
}
}

View file

@ -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<OmbiContext, OmbiMySqlContext>(x => ConfigureMySql(x, configuration.OmbiDatabase));
services.AddDbContext<OmbiContext, OmbiMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.OmbiDatabase));
AddMySqlHealthCheck(hcBuilder, "Ombi Database", configuration.OmbiDatabase);
break;
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
services.AddDbContext<OmbiContext, OmbiPostgresContext>(x => ConfigurePostgres(x, configuration.OmbiDatabase));
services.AddDbContext<OmbiContext, OmbiPostgresContext>(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<ExternalContext, ExternalMySqlContext>(x => ConfigureMySql(x, configuration.ExternalDatabase));
services.AddDbContext<ExternalContext, ExternalMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.ExternalDatabase));
AddMySqlHealthCheck(hcBuilder, "External Database", configuration.ExternalDatabase);
break;
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
services.AddDbContext<ExternalContext, ExternalPostgresContext>(x => ConfigurePostgres(x, configuration.ExternalDatabase));
services.AddDbContext<ExternalContext, ExternalPostgresContext>(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<SettingsContext, SettingsMySqlContext>(x => ConfigureMySql(x, configuration.SettingsDatabase));
services.AddDbContext<SettingsContext, SettingsMySqlContext>(x => DatabaseConfigurationSetup.ConfigureMySql(x, configuration.SettingsDatabase));
AddMySqlHealthCheck(hcBuilder, "Settings Database", configuration.SettingsDatabase);
break;
case var type when type.Equals(PostgresDatabase, StringComparison.InvariantCultureIgnoreCase):
services.AddDbContext<SettingsContext, SettingsPostgresContext>(x => ConfigurePostgres(x, configuration.SettingsDatabase));
services.AddDbContext<SettingsContext, SettingsPostgresContext>(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<ISqlGenerationHelper, NpgsqlCaseInsensitiveSqlGenerationHelper>();
}
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>(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());
}
}
}

View file

@ -0,0 +1,3 @@
namespace Ombi.Models.V2;
public record WizardDatabaseConfiguration(string Type, string Host, int Port, string Name, string User, string Password);

View file

@ -54,10 +54,6 @@
<Content Remove="database.json" />
</ItemGroup>
<ItemGroup>
<None Include="database.json" />
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>