From 89a34516963ce5dd1244e0bdfbca61ac0c96bbf7 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:32:24 +0000 Subject: [PATCH] android, desktop, ios: warn on low storage before exporting or migrating the database, reject oversized migration --- .../Shared/Views/Database/DatabaseView.swift | 1 + .../Views/Migration/MigrateFromDevice.swift | 60 ++++++++++++++++++- apps/ios/SimpleXChat/FileUtils.swift | 4 ++ .../common/views/database/DatabaseView.kt | 24 ++++++++ .../simplex/common/views/helpers/Utils.kt | 4 ++ .../views/migration/MigrateFromDevice.kt | 19 +++++- .../commonMain/resources/MR/base/strings.xml | 4 ++ 7 files changed, 113 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 278893a669..b6834a2229 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -457,6 +457,7 @@ struct DatabaseView: View { } private func exportArchive() async -> Bool { + if !(await confirmExportStorage()) { return false } await MainActor.run { progressIndicator = true } diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 2ff376701c..ca5a5dcbd7 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -262,7 +262,7 @@ struct MigrateFromDevice: View { progressView() } .onAppear { - exportArchive() + exportArchiveCheckingStorage() } } @@ -463,6 +463,13 @@ struct MigrateFromDevice: View { } } + private func exportArchiveCheckingStorage() { + Task { + if await confirmExportStorage() { await MainActor.run { exportArchive() } } + else { await MainActor.run { migrationState = .uploadConfirmation } } + } + } + private func exportArchive() { Task { do { @@ -488,6 +495,20 @@ struct MigrateFromDevice: View { private func uploadArchive(path archivePath: URL) async { if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), let totalBytes = attrs[.size] as? Int64 { + if totalBytes > MAX_FILE_SIZE_XFTP_HARD { + await MainActor.run { + alert = .error( + title: "Database is too large", + error: String.localizedStringWithFormat( + NSLocalizedString("The exported archive (%@) is larger than the maximum size supported for migration (%@).", comment: "migration alert"), + ByteCountFormatter.string(fromByteCount: totalBytes, countStyle: .binary), + ByteCountFormatter.string(fromByteCount: MAX_FILE_SIZE_XFTP_HARD, countStyle: .binary) + ) + ) + migrationState = .uploadConfirmation + } + return + } await MainActor.run { migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) } @@ -782,3 +803,40 @@ struct MigrateFromDevice_Previews: PreviewProvider { MigrateFromDevice(showProgressOnSettings: Binding.constant(false)) } } + +// Advisory check before exporting the database — export transiently uses ~2x the data on disk. +func confirmExportStorage() async -> Bool { + let dataBytes = await Task.detached { estimatedExportBytes() }.value + let required = dataBytes * 2 + guard let available = availableImportantBytes(at: getDocumentsDirectory()), available < required else { return true } + return await withCheckedContinuation { cont in + DispatchQueue.main.async { + showAlert( + NSLocalizedString("You may not have enough storage", comment: "alert title"), + message: String.localizedStringWithFormat( + NSLocalizedString("Exporting the database may need about %@ of free space, but only %@ is available. Continue anyway?", comment: "alert message"), + ByteCountFormatter.string(fromByteCount: required, countStyle: .binary), + ByteCountFormatter.string(fromByteCount: available, countStyle: .binary) + ), + actions: { + [ UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { _ in cont.resume(returning: true) }, + UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in cont.resume(returning: false) } ] + } + ) + } + } +} + +private func estimatedExportBytes() -> Int64 { + let files = Int64(directoryFileCountAndSize(getAppFilesDirectory())?.1 ?? 0) + let wallpapers = Int64(directoryFileCountAndSize(getWallpaperDirectory())?.1 ?? 0) + let dbPrefix = getAppDatabasePath().path + let chatDb = Int64(fileSize(URL(fileURLWithPath: dbPrefix + "_chat.db")) ?? 0) + let agentDb = Int64(fileSize(URL(fileURLWithPath: dbPrefix + "_agent.db")) ?? 0) + return files + wallpapers + chatDb + agentDb +} + +private func availableImportantBytes(at url: URL) -> Int64? { + (try? url.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]))? + .volumeAvailableCapacityForImportantUsage +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 3d0dd663c1..603b171459 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -29,6 +29,10 @@ public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB // Spec: spec/services/files.md#MAX_FILE_SIZE_XFTP public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB +// Hard limit for standalone XFTP uploads (device migration archive); +// mirrors maxFileSizeHard (gb 5) in simplexmq Simplex/FileTransfer/Description.hs +public let MAX_FILE_SIZE_XFTP_HARD: Int64 = 5_368_709_120 // 5GB + public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max public let MAX_FILE_SIZE_SMP: Int64 = 8000000 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 80f97d1caf..e38c1e6d12 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -594,12 +594,36 @@ fun deleteChatDatabaseFilesAndState() { ntfManager.cancelAllNotifications() } +// Advisory check before exporting the database — export transiently uses ~2x the data on disk. +suspend fun confirmExportStorage(): Boolean { + databaseExportDir.mkdirs() + val requiredBytes = 2L * ( + directoryFileCountAndSize(appFilesDir.absolutePath).second + + directoryFileCountAndSize(wallpapersDir.absolutePath).second + + File(dataDir, chatDatabaseFileName).length() + + File(dataDir, agentDatabaseFileName).length() + ) + val availableBytes = databaseExportDir.usableSpace + if (availableBytes >= requiredBytes) return true + val proceed = CompletableDeferred() + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.not_enough_storage_title), + text = String.format(generalGetString(MR.strings.not_enough_storage_desc), formatBytes(requiredBytes), formatBytes(availableBytes)), + confirmText = generalGetString(MR.strings.chat_database_exported_continue), + onConfirm = { proceed.complete(true) }, + onDismiss = { proceed.complete(false) }, + onDismissRequest = { proceed.complete(false) }, + ) + return proceed.await() +} + private suspend fun exportArchive( m: ChatModel, progressIndicator: MutableState, chatArchiveFile: MutableState, saveArchiveLauncher: FileChooserLauncher ): Boolean { + if (!confirmExportStorage()) return false progressIndicator.value = true try { val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveFile) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 23c622bc34..705c25c659 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -126,6 +126,10 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000 const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB +// Hard limit for standalone XFTP uploads (e.g. device migration archive); +// mirrors maxFileSizeHard (gb 5) in simplexmq Simplex/FileTransfer/Description.hs +const val MAX_FILE_SIZE_XFTP_HARD: Long = 5_368_709_120 // 5GB + const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE expect fun getAppFileUri(fileName: String): URI diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 39c4cb0b7f..4ca9cbb8bb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -274,7 +274,7 @@ private fun MutableState.ArchivingView() { ProgressView() } LaunchedEffect(Unit) { - exportArchive() + exportArchiveCheckingStorage() } } @@ -480,6 +480,13 @@ private suspend fun MutableState.verifyDatabasePassphrase(db } } +private fun MutableState.exportArchiveCheckingStorage() { + withBGApi { + if (confirmExportStorage()) exportArchive() + else state = MigrationFromState.UploadConfirmation + } +} + private fun MutableState.exportArchive() { withLongRunningApi { try { @@ -506,7 +513,15 @@ private fun MutableState.exportArchive() { private fun MutableState.uploadArchive(archivePath: String) { val totalBytes = File(archivePath).length() if (totalBytes > 0L) { - state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + if (totalBytes > MAX_FILE_SIZE_XFTP_HARD) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_from_device_database_too_large_title), + String.format(generalGetString(MR.strings.migrate_from_device_database_too_large_desc), formatBytes(totalBytes), formatBytes(MAX_FILE_SIZE_XFTP_HARD)) + ) + state = MigrationFromState.UploadConfirmation + } else { + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + } } else { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) state = MigrationFromState.UploadConfirmation diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 5bca9402a8..2ed7de7e31 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2831,6 +2831,10 @@ Migrate to another device Error saving settings Exported file doesn\'t exist + Database is too large + The exported archive (%1$s) is larger than the maximum size supported for migration (%2$s). + You may not have enough storage + Exporting the database may need about %1$s of free space, but only %2$s is available. Continue anyway? Error exporting chat database Preparing upload Error uploading the archive