Files
Umbraco-CMS/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs
Mole 8ffede0441 V10: merge release branch 20220620 (#12590)
* Add Umbraco specific global usings

* Enable implicit usings

* v10: Wait for updated ConnectionStrings during install (#12536)

* Do not change/reload configuration

* Wait for updated connection string options

* recase assigndomain (#12448)

* Add depth property to ICoreScope (#12540)

* Remove ambient scope stack from httpcontext.items. (#12539)

This change makes it easier to use service calls in parallel whilst
a httpcontext is available.

* v10: Prefer SQLite primitive types to flexible types (#12541)

* Prefer SQLite primitive types to flexible types.

* SQLite - column mappings use TEXT for decimals

Thanks @mattbrailsford for sense check.

* Fix issue where languages files are not found in subdir of package dir (#12543)

* Make FindContent return type nullable (#12545)

* Updated nuget dependencies (07-06-2022) (#12525)

* Updated nuget dependencies

* Move Nerdbank.GitVersioning update to Directory.Build.props

* Updated more dependencies

* Improve FlagOutOfDateModels property behaviour.

(cherry picked from commit 54077725c373495fce0d3fbc5cdb6469aad3b676)

* Fix logic error WRT models builder flag out of date models. (#12548)

(cherry picked from commit 6b0149803a879d1c6902a5f61d1f2e9dc8545aac)

* Fixed issue with expected null value. (#12550)

Fixes https://github.com/umbraco/Umbraco-CMS/issues/12526

* Updated Examine to 3.0.0

* Fixes relation issue, when moving a root item to recycle bin, the "Relate Parent Media Folder On Delete"/"Relate Parent Document On Delete" cannot get the parent node type, because it is a fake root.

* Fix possible null error

* Bump version to 10.0.0 final

* Fix attempting to write lock files to LocalTempPath before it exists (#12563)

* Re fix usage statements

Co-authored-by: Ronald Barendse <ronald@barend.se>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Paul Johnson <pmj@umbraco.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2022-06-20 09:20:47 +02:00

411 lines
16 KiB
C#

using System.Data.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Install
{
/// <summary>
/// Supports building and configuring the database.
/// </summary>
public class DatabaseBuilder
{
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IScopeAccessor _scopeAccessor;
private readonly IRuntimeState _runtimeState;
private readonly IKeyValueService _keyValueService;
private readonly ILogger<DatabaseBuilder> _logger;
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
private readonly IConfigManipulator _configManipulator;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IMigrationPlanExecutor _migrationPlanExecutor;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata;
private DatabaseSchemaResult? _databaseSchemaValidationResult;
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseBuilder"/> class.
/// </summary>
public DatabaseBuilder(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
IUmbracoDatabaseFactory databaseFactory,
IRuntimeState runtimeState,
ILoggerFactory loggerFactory,
IKeyValueService keyValueService,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IConfigManipulator configManipulator,
IOptionsMonitor<GlobalSettings> globalSettings,
IOptionsMonitor<ConnectionStrings> connectionStrings,
IMigrationPlanExecutor migrationPlanExecutor,
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata)
{
_scopeProvider = scopeProvider;
_scopeAccessor = scopeAccessor;
_databaseFactory = databaseFactory;
_runtimeState = runtimeState;
_logger = loggerFactory.CreateLogger<DatabaseBuilder>();
_keyValueService = keyValueService;
_dbProviderFactoryCreator = dbProviderFactoryCreator;
_configManipulator = configManipulator;
_globalSettings = globalSettings;
_connectionStrings = connectionStrings;
_migrationPlanExecutor = migrationPlanExecutor;
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
_databaseProviderMetadata = databaseProviderMetadata;
}
#region Status
/// <summary>
/// Gets a value indicating whether the database is configured. It does not necessarily
/// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date.
/// </summary>
public bool IsDatabaseConfigured => _databaseFactory.Configured;
/// <summary>
/// Gets a value indicating whether it is possible to connect to the configured database.
/// It does not necessarily mean that Umbraco is installed, nor up-to-date.
/// </summary>
public bool CanConnectToDatabase => _databaseFactory.CanConnect;
/// <summary>
/// Verifies whether a it is possible to connect to a database.
/// </summary>
public bool CanConnect(string? connectionString, string providerName)
{
DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(providerName);
return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory);
}
public bool HasSomeNonDefaultUser()
{
using (var scope = _scopeProvider.CreateCoreScope())
{
// look for the super user with default password
var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql()
.SelectCount()
.From<UserDto>()
.Where<UserDto>(x => x.Id == Constants.Security.SuperUserId && x.Password == "default");
var result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar<int>(sql);
var has = result != 1;
if (has == false)
{
// found only 1 user == the default user with default password
// however this always exists on uCloud, also need to check if there are other users too
result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoUser");
has = result != 1;
}
scope.Complete();
return has;
}
}
internal bool IsUmbracoInstalled()
{
using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
{
return _scopeAccessor.AmbientScope?.Database.IsUmbracoInstalled() ?? false;
}
}
#endregion
#region Configure Connection String
public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun)
{
IDatabaseProviderMetadata? providerMeta;
// if the database model is null then we will attempt quick install.
if (databaseSettings == null)
{
providerMeta = _databaseProviderMetadata.GetAvailable(true).FirstOrDefault();
databaseSettings = new DatabaseModel
{
DatabaseName = providerMeta?.DefaultDatabaseName!
};
}
else
{
providerMeta = _databaseProviderMetadata.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
}
if (providerMeta == null)
{
throw new InstallException("Unable to determine database provider configuration.");
}
var connectionString = providerMeta.GenerateConnectionString(databaseSettings);
var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName;
if (string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(providerName) ||
(providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName)))
{
return false;
}
if (!isTrialRun)
{
// File configuration providers use a delay before reloading and triggering changes, so wait
using var isChanged = new ManualResetEvent(false);
using IDisposable? onChange = _connectionStrings.OnChange((options, name) =>
{
// Only watch default named option (CurrentValue)
if (name != Options.DefaultName)
{
return;
}
// Signal change
isChanged.Set();
});
// Update configuration and wait for change
_configManipulator.SaveConnectionString(connectionString, providerName);
if (!isChanged.WaitOne(10_000))
{
throw new InstallException("Didn't retrieve updated connection string within 10 seconds, try manual configuration instead.");
}
Configure(_globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase);
}
return true;
}
private void Configure(bool installMissingDatabase)
{
_databaseFactory.Configure(_connectionStrings.CurrentValue);
if (installMissingDatabase)
{
CreateDatabase();
}
}
#endregion
#region Database Schema
public void CreateDatabase() => _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!);
/// <summary>
/// Validates the database schema.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// </remarks>
public DatabaseSchemaResult? ValidateSchema()
{
using (var scope = _scopeProvider.CreateCoreScope())
{
var result = ValidateSchema(scope);
scope.Complete();
return result;
}
}
private DatabaseSchemaResult? ValidateSchema(ICoreScope scope)
{
if (_databaseFactory.Initialized == false)
return new DatabaseSchemaResult();
if (_databaseSchemaValidationResult != null)
return _databaseSchemaValidationResult;
_databaseSchemaValidationResult = _scopeAccessor.AmbientScope?.Database.ValidateSchema();
scope.Complete();
return _databaseSchemaValidationResult;
}
/// <summary>
/// Creates the database schema and inserts initial data.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// </remarks>
public Result? CreateSchemaAndData()
{
using (var scope = _scopeProvider.CreateCoreScope())
{
var result = CreateSchemaAndData(scope);
scope.Complete();
return result;
}
}
private Result? CreateSchemaAndData(ICoreScope scope)
{
try
{
var readyForInstall = CheckReadyForInstall();
if (readyForInstall.Success == false)
{
return readyForInstall.Result;
}
_logger.LogInformation("Database configuration status: Started");
var database = _scopeAccessor.AmbientScope?.Database;
var message = string.Empty;
var schemaResult = ValidateSchema();
var hasInstalledVersion = schemaResult?.DetermineHasInstalledVersion() ?? false;
//If the determined version is "empty" its a new install - otherwise upgrade the existing
if (!hasInstalledVersion)
{
if (_runtimeState.Level == RuntimeLevel.Run)
throw new Exception("Umbraco is already configured!");
var creator = _databaseSchemaCreatorFactory.Create(database);
creator.InitializeDatabaseSchema();
message = message + "<p>Installation completed!</p>";
//now that everything is done, we need to determine the version of SQL server that is executing
_logger.LogInformation("Database configuration status: {DbConfigStatus}", message);
return new Result { Message = message, Success = true, Percentage = "100" };
}
//we need to do an upgrade so return a new status message and it will need to be done during the next step
_logger.LogInformation("Database requires upgrade");
message = "<p>Upgrading database, this may take some time...</p>";
return new Result
{
RequiresUpgrade = true,
Message = message,
Success = true,
Percentage = "30"
};
}
catch (Exception ex)
{
return HandleInstallException(ex);
}
}
/// <summary>
/// Upgrades the database schema and data by running migrations.
/// </summary>
/// <remarks>
/// <para>This assumes that the database exists and the connection string is
/// configured and it is possible to connect to the database.</para>
/// <para>Runs whichever migrations need to run.</para>
/// </remarks>
public Result? UpgradeSchemaAndData(UmbracoPlan plan)
{
try
{
var readyForInstall = CheckReadyForInstall();
if (readyForInstall.Success == false)
{
return readyForInstall.Result;
}
_logger.LogInformation("Database upgrade started");
// upgrade
var upgrader = new Upgrader(plan);
upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService);
var message = "<p>Upgrade completed!</p>";
//now that everything is done, we need to determine the version of SQL server that is executing
_logger.LogInformation("Database configuration status: {DbConfigStatus}", message);
return new Result { Message = message, Success = true, Percentage = "100" };
}
catch (Exception ex)
{
return HandleInstallException(ex);
}
}
private Attempt<Result?> CheckReadyForInstall()
{
if (_databaseFactory.CanConnect == false)
{
return Attempt.Fail(new Result
{
Message = "Database configuration is invalid. Please check that the entered database exists and"
+ " that the provided username and password has write access to the database.",
Success = false,
Percentage = "10"
});
}
return Attempt<Result?>.Succeed();
}
private Result HandleInstallException(Exception ex)
{
_logger.LogError(ex, "Database configuration failed");
if (_databaseSchemaValidationResult != null)
{
_logger.LogInformation("The database schema validation produced the following summary: {DbSchemaSummary}", _databaseSchemaValidationResult.GetSummary());
}
return new Result
{
Message =
"The database configuration failed with the following message: " + ex.Message +
$"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')",
Success = false,
Percentage = "90"
};
}
/// <summary>
/// Represents the result of a database creation or upgrade.
/// </summary>
public class Result
{
/// <summary>
/// Gets or sets a value indicating whether an upgrade is required.
/// </summary>
public bool RequiresUpgrade { get; set; }
/// <summary>
/// Gets or sets the message returned by the operation.
/// </summary>
public string? Message { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the operation succeeded.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets an install progress pseudo-percentage.
/// </summary>
public string? Percentage { get; set; }
}
#endregion
}
}