namespace ZB.MOM.WW.OtOpcUa.Client.Shared; /// /// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI /// store + persisted settings. Renamed from LmxOpcUaClient to OtOpcUaClient /// 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. /// /// /// Thread-safe: the rename uses 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. /// public static class ClientStoragePaths { /// Canonical client folder name. Post-#208. public const string CanonicalFolderName = "OtOpcUaClient"; /// Pre-#208 folder name. Used only by the migration shim. public const string LegacyFolderName = "LmxOpcUaClient"; private static readonly Lock _migrationLock = new(); private static bool _migrationChecked; /// /// 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. /// public static string GetRoot() { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var canonical = Path.Combine(localAppData, CanonicalFolderName); MigrateLegacyFolderIfNeeded(localAppData, canonical); return canonical; } /// Subfolder for the application's PKI store — used by both CLI + UI. public static string GetPkiPath() => Path.Combine(GetRoot(), "pki"); /// /// 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. /// 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; } } } }