91 lines
4.0 KiB
C#
91 lines
4.0 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
|
|
|
/// <summary>
|
|
/// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI
|
|
/// store + persisted settings. Renamed from <c>LmxOpcUaClient</c> to <c>OtOpcUaClient</c>
|
|
/// in task #208; a one-shot migration shim moves a pre-rename folder in place on first
|
|
/// resolution so existing developer boxes keep their trusted server certs + saved
|
|
/// connection settings on upgrade.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Thread-safe: the rename uses <see cref="Directory.Move"/> which is atomic on NTFS
|
|
/// within the same volume. The lock guarantees the migration runs at most once per
|
|
/// process even under concurrent first-touch from CLI + UI.
|
|
/// </remarks>
|
|
public static class ClientStoragePaths
|
|
{
|
|
/// <summary>Canonical client folder name. Post-#208.</summary>
|
|
public const string CanonicalFolderName = "OtOpcUaClient";
|
|
|
|
/// <summary>Pre-#208 folder name. Used only by the migration shim.</summary>
|
|
public const string LegacyFolderName = "LmxOpcUaClient";
|
|
|
|
private static readonly Lock _migrationLock = new();
|
|
private static bool _migrationChecked;
|
|
|
|
/// <summary>
|
|
/// Absolute path to the client's top-level folder under LocalApplicationData. Runs the
|
|
/// one-shot legacy-folder migration before returning so callers that depend on this
|
|
/// path (PKI store, settings file) find their existing state at the canonical name.
|
|
/// </summary>
|
|
public static string GetRoot()
|
|
{
|
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
|
MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
|
return canonical;
|
|
}
|
|
|
|
/// <summary>Subfolder for the application's PKI store — used by both CLI + UI.</summary>
|
|
public static string GetPkiPath() => Path.Combine(GetRoot(), "pki");
|
|
|
|
/// <summary>
|
|
/// Expose the migration probe for tests + for callers that want to check whether the
|
|
/// legacy folder still exists without forcing the rename. Returns true when a legacy
|
|
/// folder existed + was moved to canonical, false when no migration was needed or
|
|
/// canonical was already present.
|
|
/// </summary>
|
|
public static bool TryRunLegacyMigration()
|
|
{
|
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
|
return MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
|
}
|
|
|
|
private static bool MigrateLegacyFolderIfNeeded(string localAppData, string canonical)
|
|
{
|
|
// Fast-path out of the lock when the migration has already been attempted this process
|
|
// — saves the IO on every subsequent call, + the migration is idempotent within the
|
|
// same process anyway.
|
|
if (_migrationChecked) return false;
|
|
|
|
lock (_migrationLock)
|
|
{
|
|
if (_migrationChecked) return false;
|
|
_migrationChecked = true;
|
|
|
|
var legacy = Path.Combine(localAppData, LegacyFolderName);
|
|
|
|
// Only migrate when the legacy folder is present + canonical isn't. Either of the
|
|
// other three combinations (neither / only-canonical / both) means migration
|
|
// should NOT run: no-op fresh install, already-migrated, or manual state the
|
|
// developer has set up — don't clobber.
|
|
if (!Directory.Exists(legacy)) return false;
|
|
if (Directory.Exists(canonical)) return false;
|
|
|
|
try
|
|
{
|
|
Directory.Move(legacy, canonical);
|
|
return true;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Concurrent another-process-moved-it or volume-boundary or permissions — leave
|
|
// the legacy folder alone; callers that need it can either re-run migration
|
|
// manually or point CertificateStorePath explicitly.
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|