207 lines
7.3 KiB
C#
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);
|
|
}
|
|
}
|