@@ -71,7 +71,7 @@ public DatabaseUnlockService(
7171 }
7272
7373 /// <summary>
74- /// Archive encrypted database and create fresh database when password forgotten
74+ /// Archive encrypted database and create fresh database when password forgotten.
7575 /// </summary>
7676 /// <param name="databasePath">Path to encrypted database</param>
7777 /// <returns>(Success, ArchivedPath, ErrorMessage)</returns>
@@ -92,12 +92,54 @@ public DatabaseUnlockService(
9292 var timestamp = DateTime . Now . ToString ( "yyyyMMdd-HHmmss" ) ;
9393 var archivedPath = Path . Combine ( backupsDir , $ "{ dbFileNameWithoutExt } .{ timestamp } .encrypted.db") ;
9494
95- // Move encrypted database to backups
96- File . Move ( databasePath , archivedPath ) ;
95+ // On Windows the OS enforces mandatory file locks. Even after SqliteConnection is
96+ // disposed, the connection pool keeps the Win32 file handle open until explicitly
97+ // cleared. Clear all pools and give the GC a chance to release any lingering
98+ // handles before we attempt the file move.
99+ SqliteConnection . ClearAllPools ( ) ;
100+ if ( OperatingSystem . IsWindows ( ) )
101+ {
102+ GC . Collect ( ) ;
103+ GC . WaitForPendingFinalizers ( ) ;
104+ GC . Collect ( ) ;
105+ }
106+
107+ // Move the main database file. Retry once on Windows in case a finalizer
108+ // hadn't yet released its handle on the first attempt.
109+ try
110+ {
111+ File . Move ( databasePath , archivedPath ) ;
112+ }
113+ catch ( IOException ) when ( OperatingSystem . IsWindows ( ) )
114+ {
115+ _logger . LogWarning ( "File move failed on first attempt (Windows lock), retrying after 500 ms" ) ;
116+ await Task . Delay ( 500 ) ;
117+ File . Move ( databasePath , archivedPath ) ;
118+ }
119+
120+ // Remove WAL companion files if present. These are created when WAL mode is active
121+ // and must be cleaned up so the fresh database starts without a stale journal.
122+ // On Windows these files may also be locked; delete rather than move since the
123+ // archived backup doesn't need them.
124+ foreach ( var sidecar in new [ ] { databasePath + "-wal" , databasePath + "-shm" } )
125+ {
126+ if ( ! File . Exists ( sidecar ) ) continue ;
127+ try
128+ {
129+ File . Delete ( sidecar ) ;
130+ _logger . LogInformation ( "Removed WAL companion file: {Sidecar}" , sidecar ) ;
131+ }
132+ catch ( Exception ex )
133+ {
134+ // Non-fatal: a stale WAL without its main database is harmless.
135+ _logger . LogWarning ( "Could not remove WAL companion file {Sidecar}: {Message}" , sidecar , ex . Message ) ;
136+ }
137+ }
138+
97139 _logger . LogInformation ( "Encrypted database archived to: {ArchivedPath}" , archivedPath ) ;
98140
99- // New unencrypted database will be created automatically on app restart
100- // The app will detect no database exists and go through first-time setup
141+ // New database will be created automatically on app restart — the app detects
142+ // no database exists and runs through first-time setup / migrations.
101143 return ( true , archivedPath , null ) ;
102144 }
103145 catch ( Exception ex )
0 commit comments