Files
MBN_STOCK_WEBVIEW/src/MBN_STOCK_WEBVIEW.Infrastructure/DatabaseRuntime.cs

207 lines
7.3 KiB
C#

using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.Infrastructure;
public sealed class DatabaseRuntime
{
private DatabaseRuntime(
DatabaseOptions options,
IDatabaseConnectionFactory connectionFactory,
IDataQueryExecutor executor,
IDatabaseHealthService healthService)
{
Options = options;
ConnectionFactory = connectionFactory;
Executor = executor;
HealthService = healthService;
}
public DatabaseOptions Options { get; }
public IDatabaseConnectionFactory ConnectionFactory { get; }
public IDataQueryExecutor Executor { get; }
public IDatabaseHealthService HealthService { get; }
public static DatabaseRuntime CreateDefault(string? configurationPath = null)
{
var options = new DatabaseOptionsLoader().Load(configurationPath);
return Create(options);
}
public static DatabaseRuntime CreateLegacyExecutableDefault(
string baseDirectory,
string? fallbackConfigurationPath = null,
string? legacyOverridePath = null,
bool preferLegacyOverride = false)
{
ArgumentException.ThrowIfNullOrWhiteSpace(baseDirectory);
if (preferLegacyOverride &&
!string.IsNullOrWhiteSpace(legacyOverridePath) &&
File.Exists(legacyOverridePath))
{
return Create(new LegacyIniDatabaseOptionsLoader().Load(legacyOverridePath));
}
var legacyIniPath = Path.Combine(
Path.GetFullPath(baseDirectory),
"Res",
"MmoneyCoder.ini");
if (File.Exists(legacyIniPath))
{
return Create(new LegacyIniDatabaseOptionsLoader().Load(legacyIniPath));
}
var localOverride = legacyOverridePath ?? ResolveLegacyOverridePath();
if (!string.IsNullOrWhiteSpace(localOverride) && File.Exists(localOverride))
{
return Create(new LegacyIniDatabaseOptionsLoader().Load(localOverride));
}
return CreateDefault(fallbackConfigurationPath);
}
/// <summary>
/// Creates the Development Live database runtime from the one protected
/// current-user LocalAppData legacy INI, or from the protected LocalAppData
/// JSON when that INI is absent. Unlike the ordinary compatibility path, this
/// never probes executable or operator-selected Res folders and ignores all
/// database environment overrides.
/// </summary>
public static DatabaseRuntime CreateDevelopmentLiveLocalOverride()
{
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
return CreateDevelopmentLiveLocalOverride(localApplicationData);
}
internal static DatabaseRuntime CreateDevelopmentLiveLocalOverride(
string localApplicationData)
{
try
{
ArgumentException.ThrowIfNullOrWhiteSpace(localApplicationData);
var localRoot = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(localApplicationData));
var resourceDirectory = Path.Combine(
localRoot,
"MBN_STOCK_WEBVIEW",
"Res");
var legacyIniPath = Path.Combine(resourceDirectory, "MmoneyCoder.ini");
if (TryEnsureOrdinaryFile(legacyIniPath))
{
EnsureNoReparsePointDirectoryChain(resourceDirectory, localRoot);
return Create(
new LegacyIniDatabaseOptionsLoader(_ => null).Load(legacyIniPath));
}
var configurationDirectory = Path.Combine(
localRoot,
"MBN_STOCK_WEBVIEW",
"Config");
EnsureNoReparsePointDirectoryChain(configurationDirectory, localRoot);
var jsonPath = Path.Combine(
configurationDirectory,
"database.local.json");
EnsureOrdinaryFile(jsonPath);
var options = new DatabaseOptionsLoader(_ => null).Load(jsonPath);
if (!options.IsConfigured(MMoneyCoderSharp.Data.DataSourceKind.Oracle) ||
!options.IsConfigured(MMoneyCoderSharp.Data.DataSourceKind.MariaDb))
{
throw new DatabaseConfigurationException(
"Development Live database configuration is unavailable.");
}
return Create(options);
}
catch (DatabaseConfigurationException)
{
throw;
}
catch (Exception exception) when (
exception is ArgumentException or IOException or UnauthorizedAccessException or
InvalidDataException or NotSupportedException or
System.Security.SecurityException)
{
throw new DatabaseConfigurationException(
"Development Live database configuration is unavailable.");
}
}
private static string? ResolveLegacyOverridePath()
{
var localAppData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
return string.IsNullOrWhiteSpace(localAppData)
? null
: Path.Combine(
localAppData,
"MBN_STOCK_WEBVIEW",
"Res",
"MmoneyCoder.ini");
}
private static void EnsureNoReparsePointDirectoryChain(
string directory,
string trustedRoot)
{
var current = Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory));
var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(trustedRoot));
while (!string.Equals(current, root, StringComparison.OrdinalIgnoreCase))
{
if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
{
throw new InvalidDataException();
}
var parent = Path.GetDirectoryName(current);
if (string.IsNullOrWhiteSpace(parent) ||
string.Equals(parent, current, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException();
}
current = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parent));
}
}
private static void EnsureOrdinaryFile(string path)
{
var attributes = File.GetAttributes(Path.GetFullPath(path));
if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
{
throw new InvalidDataException();
}
}
private static bool TryEnsureOrdinaryFile(string path)
{
try
{
EnsureOrdinaryFile(path);
return true;
}
catch (FileNotFoundException)
{
return false;
}
catch (DirectoryNotFoundException)
{
return false;
}
}
public static DatabaseRuntime Create(DatabaseOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.Normalize();
options.ValidateRuntimeSettings();
var connectionFactory = new ProviderDatabaseConnectionFactory(options);
var executor = new ResilientDataQueryExecutor(connectionFactory, options.Resilience);
var healthService = new DatabaseHealthService(executor, options);
return new DatabaseRuntime(options, connectionFactory, executor, healthService);
}
}