< Summary

Information
Class: LOCKnet.Data.StorageRewriteArtifacts
Assembly: LOCKnet.Data
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/StorageRewriteArtifacts.cs
Line coverage
73%
Covered lines: 131
Uncovered lines: 47
Coverable lines: 178
Total lines: 250
Line coverage: 73.5%
Branch coverage
72%
Covered branches: 61
Total branches: 84
Branch coverage: 72.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryResolveDatabasePath(...)100%88100%
GetTempPath(...)100%11100%
GetBackupPath(...)100%11100%
HasPendingArtifacts(...)50%44100%
Recover(...)100%3434100%
ClearPendingState(...)100%11100%
IsUsableSqliteDatabase(...)62.5%8884.61%
ReplacePrimaryDatabase(...)30%331038.7%
RestoreFile(...)100%11100%
PromoteFile(...)100%11100%
TryDeleteFile(...)66.66%10652.38%
MoveFileWithRetry(...)35.71%531441.66%
get_ShouldClearPendingState()100%11100%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/StorageRewriteArtifacts.cs

#LineLine coverage
 1using Microsoft.Data.Sqlite;
 2using System.Runtime.InteropServices;
 3
 4namespace LOCKnet.Data;
 5
 6internal static class StorageRewriteArtifacts
 7{
 8  internal const string RewriteTempSuffix = ".rewrite.tmp";
 9  internal const string RewriteBackupSuffix = ".rewrite.bak";
 10
 11  internal static string? TryResolveDatabasePath(string connectionString)
 20612  {
 20613    if (string.IsNullOrWhiteSpace(connectionString))
 114      return null;
 15
 20516    var builder = new SqliteConnectionStringBuilder(connectionString);
 20517    if (builder.Mode == SqliteOpenMode.Memory)
 14618      return null;
 19
 5920    if (string.IsNullOrWhiteSpace(builder.DataSource) || builder.DataSource == ":memory:")
 221      return null;
 22
 5723    return Path.GetFullPath(builder.DataSource);
 20624  }
 25
 12326  internal static string GetTempPath(string databasePath) => databasePath + RewriteTempSuffix;
 27
 12228  internal static string GetBackupPath(string databasePath) => databasePath + RewriteBackupSuffix;
 29
 30  internal static bool HasPendingArtifacts(string? databasePath)
 231    => databasePath is not null &&
 232      (File.Exists(GetTempPath(databasePath)) || File.Exists(GetBackupPath(databasePath)));
 33
 34  internal static StartupRecoveryOutcome Recover(string? databasePath)
 16335  {
 16336    if (string.IsNullOrWhiteSpace(databasePath))
 6637      return default;
 38
 9739    var primaryPath = Path.GetFullPath(databasePath);
 9740    var tempPath = GetTempPath(primaryPath);
 9741    var backupPath = GetBackupPath(primaryPath);
 9742    var shouldClearPendingState = false;
 43
 9744    var mainExists = File.Exists(primaryPath);
 9745    var backupExists = File.Exists(backupPath);
 9746    var tempExists = File.Exists(tempPath);
 9747    var mainValid = mainExists && IsUsableSqliteDatabase(primaryPath);
 9748    var backupValid = backupExists && IsUsableSqliteDatabase(backupPath);
 9749    var tempValid = tempExists && IsUsableSqliteDatabase(tempPath);
 50
 9751    if ((!mainExists || !mainValid) && backupValid)
 152    {
 153      RestoreFile(backupPath, primaryPath);
 154      mainExists = true;
 155      mainValid = true;
 156      backupExists = false;
 157      backupValid = false;
 158    }
 9659    else if ((!mainExists || !mainValid) && !backupExists && tempValid)
 160    {
 161      PromoteFile(tempPath, primaryPath);
 162      mainExists = true;
 163      mainValid = true;
 164      tempExists = false;
 165      tempValid = false;
 166    }
 67
 9768    if (!mainExists && (backupExists || tempExists))
 169      throw new InvalidOperationException("Vault-Rewrite-Artefakte gefunden, aber keine verwendbare Hauptdatenbank konnt
 70
 9671    if (mainExists && !mainValid && (backupExists || tempExists))
 172      throw new InvalidOperationException("Vault-Datei ist nach einer unterbrochenen Rewrite-Bereinigung ungueltig. Back
 73
 9574    if (mainValid && backupExists)
 175    {
 176      if (TryDeleteFile(backupPath))
 177        shouldClearPendingState = true;
 178    }
 79
 9580    if (mainValid && tempExists)
 281      TryDeleteFile(tempPath);
 82
 9583    return new StartupRecoveryOutcome(shouldClearPendingState);
 16184  }
 85
 86  internal static void ClearPendingState(SqliteConnection connection)
 287  {
 288    ArgumentNullException.ThrowIfNull(connection);
 89
 290    using var command = connection.CreateCommand();
 291    command.CommandText = @"
 292                UPDATE MasterKey
 293                SET RequiresStorageCompaction = 0,
 294                    LastStorageCompactionAttemptUtc = NULL,
 295                    LastStorageCompactionFailureKind = 0,
 296                    LastStorageCompactionError = NULL,
 297                    UpdatedAt = CURRENT_TIMESTAMP
 298                WHERE Id = 1;";
 299    command.ExecuteNonQuery();
 4100  }
 101
 102  internal static bool IsUsableSqliteDatabase(string databasePath)
 38103  {
 38104    if (!File.Exists(databasePath))
 1105      return false;
 106
 107    try
 37108    {
 37109      var builder = new SqliteConnectionStringBuilder
 37110      {
 37111        DataSource = databasePath,
 37112        Mode = SqliteOpenMode.ReadOnly,
 37113      };
 114
 37115      using var connection = new SqliteConnection(builder.ToString());
 37116      connection.Open();
 117
 37118      using var quickCheck = connection.CreateCommand();
 37119      quickCheck.CommandText = "PRAGMA quick_check(1);";
 37120      var quickCheckResult = Convert.ToString(quickCheck.ExecuteScalar()) ?? string.Empty;
 28121      if (!string.Equals(quickCheckResult, "ok", StringComparison.OrdinalIgnoreCase))
 0122        return false;
 123
 28124      using var tableCheck = connection.CreateCommand();
 28125      tableCheck.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='MasterKey';";
 28126      return Convert.ToInt64(tableCheck.ExecuteScalar() ?? 0L) > 0;
 127    }
 9128    catch (SqliteException)
 9129    {
 9130      return false;
 131    }
 0132    catch (IOException)
 0133    {
 0134      return false;
 135    }
 38136  }
 137
 138  internal static void ReplacePrimaryDatabase(string tempPath, string primaryPath, string backupPath)
 8139  {
 16140    for (var attempt = 0; attempt < 120; attempt++)
 8141    {
 142      try
 8143      {
 8144        SqliteConnection.ClearAllPools();
 8145        Thread.Sleep(25);
 146
 8147        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && File.Exists(primaryPath))
 0148        {
 0149          File.Replace(tempPath, primaryPath, backupPath, ignoreMetadataErrors: true);
 0150        }
 151        else
 8152        {
 8153          File.Move(tempPath, primaryPath, overwrite: true);
 8154        }
 155
 8156        return;
 157      }
 0158      catch (IOException) when (attempt < 119)
 0159      {
 0160        Thread.Sleep(25);
 0161      }
 0162      catch (UnauthorizedAccessException) when (attempt < 119)
 0163      {
 0164        Thread.Sleep(25);
 0165      }
 0166    }
 167
 0168    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && File.Exists(primaryPath))
 0169    {
 0170      SqliteConnection.ClearAllPools();
 0171      File.Replace(tempPath, primaryPath, backupPath, ignoreMetadataErrors: true);
 0172      return;
 173    }
 174
 0175    SqliteConnection.ClearAllPools();
 0176    File.Move(tempPath, primaryPath, overwrite: true);
 8177  }
 178
 179  private static void RestoreFile(string sourcePath, string destinationPath)
 1180  {
 1181    MoveFileWithRetry(sourcePath, destinationPath, deleteDestinationIfPresent: true);
 1182  }
 183
 184  private static void PromoteFile(string sourcePath, string destinationPath)
 1185  {
 1186    MoveFileWithRetry(sourcePath, destinationPath, deleteDestinationIfPresent: true);
 1187  }
 188
 189  internal static bool TryDeleteFile(string path)
 46190  {
 92191    for (var attempt = 0; attempt < 120; attempt++)
 46192    {
 193      try
 46194      {
 46195        SqliteConnection.ClearAllPools();
 46196        Thread.Sleep(25);
 197
 46198        if (File.Exists(path))
 24199          File.Delete(path);
 200
 46201        if (!File.Exists(path))
 46202          return true;
 0203      }
 0204      catch (IOException)
 0205      {
 0206      }
 0207      catch (UnauthorizedAccessException)
 0208      {
 0209      }
 210
 0211      Thread.Sleep(25);
 0212    }
 213
 0214    return !File.Exists(path);
 46215  }
 216
 217  private static void MoveFileWithRetry(string sourcePath, string destinationPath, bool deleteDestinationIfPresent)
 2218  {
 4219    for (var attempt = 0; attempt < 120; attempt++)
 2220    {
 221      try
 2222      {
 2223        SqliteConnection.ClearAllPools();
 2224        Thread.Sleep(25);
 225
 2226        if (deleteDestinationIfPresent && File.Exists(destinationPath) && !TryDeleteFile(destinationPath))
 0227          throw new IOException($"Zieldatei konnte nicht entfernt werden: {destinationPath}");
 228
 2229        File.Move(sourcePath, destinationPath);
 2230        return;
 231      }
 0232      catch (IOException) when (attempt < 119)
 0233      {
 0234        Thread.Sleep(25);
 0235      }
 0236      catch (UnauthorizedAccessException) when (attempt < 119)
 0237      {
 0238        Thread.Sleep(25);
 0239      }
 0240    }
 241
 0242    if (deleteDestinationIfPresent && File.Exists(destinationPath) && !TryDeleteFile(destinationPath))
 0243      throw new IOException($"Zieldatei konnte nicht entfernt werden: {destinationPath}");
 244
 0245    SqliteConnection.ClearAllPools();
 0246    File.Move(sourcePath, destinationPath);
 2247  }
 248
 160249  internal readonly record struct StartupRecoveryOutcome(bool ShouldClearPendingState);
 250}