diff --git a/README.md b/README.md index 8091207a6..67c199960 100644 --- a/README.md +++ b/README.md @@ -659,10 +659,10 @@ Here are some of the features Ombi has: - - sussycatgirl + + Drewster727
- Lea + Drew
@@ -759,13 +759,6 @@ Here are some of the features Ombi has: - - - AlexandrePicavet -
- Alexandre Picavet -
- XanderStrike @@ -787,6 +780,13 @@ Here are some of the features Ombi has: Abe Kline + + + sussycatgirl +
+ Lea +
+ kmlucy @@ -901,13 +901,6 @@ Here are some of the features Ombi has:
Eli
- - - - Drewster727 -
- Drew -
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..5f2397863 100644 --- a/src/.idea/.idea.Ombi/.idea/workspace.xml +++ b/src/.idea/.idea.Ombi/.idea/workspace.xml @@ -1,25 +1,22 @@ + + - - - - - - - - - - + + - + + @@ -237,27 +234,63 @@ + + + + + + - - - - - - - - + + + + + + + + - - - + - + + + + + + + + - + - - - - - - - - - - - - - - + + @@ -391,7 +411,7 @@ @@ -406,7 +426,7 @@ @@ -421,7 +441,7 @@ @@ -436,7 +456,7 @@ @@ -492,9 +512,7 @@ - - + @@ -505,7 +523,7 @@ file://$PROJECT_DIR$/Ombi/Controllers/V1/TokenController.cs 48 - + @@ -518,7 +536,7 @@ file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs 59 - + @@ -531,7 +549,7 @@ file://$PROJECT_DIR$/Ombi.Core/Engine/V2/MultiSearchEngine.cs 49 - + @@ -544,7 +562,7 @@ file://$PROJECT_DIR$/Ombi.Api.MusicBrainz/MusicBrainzApi.cs 30 - + 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..750499b19 --- /dev/null +++ b/src/Ombi.Core/Services/DatabaseConfigurationService.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ombi.Core.Helpers; +using Ombi.Core.Models; +using Ombi.Helpers; + +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..40aa353b6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.html @@ -0,0 +1,104 @@ +
+
+ +
+
+
+

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 +

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

{{connectionString | async}}

+
+ +
+
+
+ + +

+ Please enter your Postgres connection details below +

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

{{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..8c037f07f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/wizard/database/database.component.ts @@ -0,0 +1,92 @@ +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"; +import { MatTabChangeEvent } from "@angular/material/tabs"; + +@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: [""], + host: ["", [Validators.required]], + port: [3306, [Validators.required]], + name: ["ombi", [Validators.required]], + user: [""], + password: [""], + }); + + this.form.valueChanges.subscribe(x => { + console.log(x); + let connection = `Server=${x.host};Port=${x.port};Database=${x.name}`; + + if (x.user) { + connection += `;User=${x.user}`; + if (x.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({ + 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"); + } + }, + }); + } + +} 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..f2958ccc4 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..2b3ab4f62 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,67 @@ 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(); + } + + 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) + { + _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 @@ - - - - diff --git a/tests/cypress/features/01-wizard/wizard.ts b/tests/cypress/features/01-wizard/wizard.ts index 7ce819d1a..53d3bfe7c 100644 --- a/tests/cypress/features/01-wizard/wizard.ts +++ b/tests/cypress/features/01-wizard/wizard.ts @@ -11,6 +11,7 @@ When("I visit Ombi", () => { When("I click through all of the pages", () => { Page.welcomeTab.next.click(); + Page.databaseTab.next.click(); Page.mediaServerTab.next.click(); Page.localUserTab.next.click(); Page.ombiConfigTab.next.click(); @@ -22,6 +23,7 @@ When("I click through all of the pages", () => { When("I click through to the user page", () => { Page.welcomeTab.next.click(); + Page.databaseTab.next.click(); Page.mediaServerTab.next.click(); }); @@ -48,6 +50,6 @@ Then("I should get a notification {string}", (string) => { Then("I should be on the User tab", () => { Page.matStepsHeader.then((_) => { - cy.get('#cdk-step-label-0-2').should('have.attr', 'aria-selected', 'true'); + cy.get('#cdk-step-label-0-3').should('have.attr', 'aria-selected', 'true'); }); }); \ No newline at end of file diff --git a/tests/cypress/fixtures/api/v1/tv-search-extra-info.json b/tests/cypress/fixtures/api/v1/tv-search-extra-info.json index 3421514a2..78f644acb 100644 --- a/tests/cypress/fixtures/api/v1/tv-search-extra-info.json +++ b/tests/cypress/fixtures/api/v1/tv-search-extra-info.json @@ -25,7 +25,7 @@ { "episodeNumber": 1, "title": "Our Cup Runneth Over", - "airDate": "2015-01-13T00:00:00", + "airDate": "2015-01-13T00:00:00Z", "url": "https://www.tvmaze.com/episodes/153107/schitts-creek-1x01-our-cup-runneth-over", "available": false, "approved": false, diff --git a/tests/cypress/integration/page-objects/wizard/wizard.page.ts b/tests/cypress/integration/page-objects/wizard/wizard.page.ts index 0b0f7d2c2..2a3bee1ec 100644 --- a/tests/cypress/integration/page-objects/wizard/wizard.page.ts +++ b/tests/cypress/integration/page-objects/wizard/wizard.page.ts @@ -20,6 +20,12 @@ class WelcomeTab { } } +class DatabaseTab { + get next(): Cypress.Chainable { + return cy.getByData('nextDatabase'); + } +} + class MediaServerTab { get next(): Cypress.Chainable { return cy.getByData('nextMediaServer'); @@ -35,6 +41,7 @@ class OmbiConfigTab { class WizardPage extends BasePage { + databaseTab: DatabaseTab; localUserTab: LocalUserTab; welcomeTab: WelcomeTab; mediaServerTab: MediaServerTab; @@ -54,6 +61,7 @@ class WizardPage extends BasePage { this.welcomeTab = new WelcomeTab(); this.mediaServerTab = new MediaServerTab(); this.ombiConfigTab = new OmbiConfigTab(); + this.databaseTab = new DatabaseTab(); } visit(options: Cypress.VisitOptions): Cypress.Chainable;