From dbbc6a96db37807bd957cecc457db6251e2513f6 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 31 Oct 2025 15:54:18 +0100 Subject: [PATCH] Improve persist db to encrypted storage Kotlin flow (#1325) --- .../app/vaultstore/VaultDatabase.kt | 69 ++++++++++++------- .../storageprovider/AndroidStorageProvider.kt | 10 +++ .../storageprovider/StorageProvider.kt | 6 ++ .../storageprovider/TestStorageProvider.kt | 6 ++ 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultDatabase.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultDatabase.kt index 81f647cbd..eff6f1c6c 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultDatabase.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultDatabase.kt @@ -158,49 +158,68 @@ class VaultDatabase( * This method can be called independently to persist the database without committing a transaction. */ fun persistDatabaseToEncryptedStorage() { - val tempDbFile = File.createTempFile("temp_db", ".sqlite") - tempDbFile.deleteOnExit() + val db = dbConnection ?: error(IllegalStateException("Database not initialized")) + + // Slight delay tolerance for busy databases + try { db.execSQL("PRAGMA busy_timeout=5000") } catch (_: Exception) {} + + val tempDbFile = File(storageProvider.getRandomTempFilePath()) + + // Ensure the temp file does not exist yet + if (tempDbFile.exists()) { + tempDbFile.delete() + } try { - dbConnection?.execSQL("ATTACH DATABASE '${tempDbFile.path}' AS target") + // Properly quote the path for SQL + val quotedPath = tempDbFile.absolutePath.replace("'", "''") + val vacuumIntoSql = "VACUUM INTO '$quotedPath'" - dbConnection?.beginTransaction() + // Retry up to 5 times if we hit transient locking errors + for (attempt in 1..5) { + try { + // VACUUM cannot run inside a transaction + if (db.inTransaction()) { + Log.w(TAG, "Database was in a transaction; ending before VACUUM") + db.endTransaction() + } - try { - val cursor = dbConnection?.rawQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'android_%'", - null, - ) + db.execSQL(vacuumIntoSql) + break // Success, exit the loop + } catch (e: Exception) { + val msg = e.message?.lowercase().orEmpty() + val transient = msg.contains("locked") || msg.contains("busy") || msg.contains("statements in progress") - cursor?.use { - while (it.moveToNext()) { - val tableName = it.getString(0) - dbConnection?.execSQL( - "CREATE TABLE target.$tableName AS SELECT * FROM main.$tableName", - ) + Log.w(TAG, "VACUUM INTO attempt $attempt/5 failed: ${e.message}") + + if (transient && attempt < 5) { + Thread.sleep((150L * attempt)) + } else { + Log.e(TAG, "VACUUM INTO failed after retries", e) + throw e } } - - dbConnection?.setTransactionSuccessful() - } finally { - dbConnection?.endTransaction() } - dbConnection?.execSQL("DETACH DATABASE target") + // Validate output file exists and has content + if (!tempDbFile.exists() || tempDbFile.length() == 0L) { + Log.e(TAG, "VACUUM INTO produced no file or empty file at ${tempDbFile.absolutePath}") + error(IllegalStateException("VACUUM INTO produced no output")) + } val rawData = tempDbFile.readBytes() - - val base64String = Base64.encodeToString(rawData, Base64.NO_WRAP) + val base64String = android.util.Base64.encodeToString(rawData, android.util.Base64.NO_WRAP) val encryptedBase64Data = crypto.encryptData(base64String) - storeEncryptedDatabase(encryptedBase64Data) } catch (e: Exception) { Log.e(TAG, "Error exporting and encrypting database", e) throw e } finally { - if (tempDbFile.exists()) { - tempDbFile.setWritable(true, true) + // Always clean up the temp file + try { tempDbFile.delete() + } catch (e: Exception) { + Log.e(TAG, "Error deleting temp file", e) } } } diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/AndroidStorageProvider.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/AndroidStorageProvider.kt index c1c45c5ac..9bcda970c 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/AndroidStorageProvider.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/AndroidStorageProvider.kt @@ -14,6 +14,16 @@ class AndroidStorageProvider(private val context: Context) : StorageProvider { return File(context.filesDir, "encrypted_database.db") } + /** + * Get a random temporary file path. + * @return The random temporary file path as a string + */ + override fun getRandomTempFilePath(): String { + val tempFile = File(context.cacheDir, "temp_db_${System.nanoTime()}_${java.util.UUID.randomUUID()}.sqlite") + tempFile.deleteOnExit() + return tempFile.absolutePath + } + override fun setEncryptedDatabaseFile(encryptedData: String) { val file = File(context.filesDir, "encrypted_database.db") file.writeText(encryptedData) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/StorageProvider.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/StorageProvider.kt index 05a41ae89..b32eef88f 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/StorageProvider.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/StorageProvider.kt @@ -13,6 +13,12 @@ interface StorageProvider { */ fun getEncryptedDatabaseFile(): File + /** + * Get a random temporary file path. + * @return The random temporary file path as a string + */ + fun getRandomTempFilePath(): String + /** * Set the encrypted database file. * @param encryptedData The encrypted database data as a base64 encoded string diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/TestStorageProvider.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/TestStorageProvider.kt index 0a72362f0..eca7f0518 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/TestStorageProvider.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/storageprovider/TestStorageProvider.kt @@ -22,6 +22,12 @@ class TestStorageProvider : StorageProvider { tempFile.writeText(encryptedData) } + override fun getRandomTempFilePath(): String { + val tempFile = File.createTempFile("temp_db", ".sqlite") + tempFile.deleteOnExit() + return tempFile.absolutePath + } + override fun setMetadata(metadata: String) { tempMetadata = metadata }