diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 845603b513..4bea2f38de 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -475,7 +475,7 @@ struct DatabaseView: View { await MainActor.run { progressIndicator = true } - if let s = await exportStorageShortfall() { + if let s = await exportStorageShortfall(await exportDataBytes()) { await MainActor.run { alert = .lowStorage(required: s.required, available: s.available) progressIndicator = false diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index d63a6d0d00..62a6fb66b9 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -473,7 +473,13 @@ struct MigrateFromDevice: View { private func exportArchive() { Task { - if let s = await exportStorageShortfall() { + let dataBytes = await exportDataBytes() + if dataBytes > MAX_FILE_SIZE_XFTP_HARD { + await MainActor.run { + alert = databaseTooLargeAlert(dataBytes) + migrationState = .uploadConfirmation + } + } else if let s = await exportStorageShortfall(dataBytes) { await MainActor.run { alert = .lowStorage(required: s.required, available: s.available) } } else { await archiveAndUpload() @@ -489,14 +495,7 @@ struct MigrateFromDevice: View { if archiveBytes > MAX_FILE_SIZE_XFTP_HARD { try? FileManager.default.removeItem(at: archivePath) 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: archiveBytes, countStyle: .binary), - ByteCountFormatter.string(fromByteCount: MAX_FILE_SIZE_XFTP_HARD, countStyle: .binary) - ) - ) + alert = databaseTooLargeAlert(archiveBytes) migrationState = .uploadConfirmation } } else if errs.isEmpty { @@ -815,9 +814,23 @@ struct MigrateFromDevice_Previews: PreviewProvider { } } +private func databaseTooLargeAlert(_ bytes: Int64) -> MigrateFromDeviceViewAlert { + .error( + title: "Database is too large", + error: String.localizedStringWithFormat( + NSLocalizedString("The database (%@) is larger than the maximum size supported for migration (%@).", comment: "migration alert"), + ByteCountFormatter.string(fromByteCount: bytes, countStyle: .binary), + ByteCountFormatter.string(fromByteCount: MAX_FILE_SIZE_XFTP_HARD, countStyle: .binary) + ) + ) +} + +func exportDataBytes() async -> Int64 { + await Task.detached { estimatedExportBytes() }.value +} + // export transiently holds a copy of the data and the archive of it -func exportStorageShortfall() async -> (required: Int64, available: Int64)? { - let dataBytes = await Task.detached { estimatedExportBytes() }.value +func exportStorageShortfall(_ dataBytes: Int64) async -> (required: Int64, available: Int64)? { let required = dataBytes * 2 guard let available = availableImportantBytes(at: getDocumentsDirectory()), available < required else { return nil } return (required, available) @@ -837,12 +850,20 @@ func lowStorageAlert(_ required: Int64, _ available: Int64, onContinue: @escapin } 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 + return filesSize(getAppFilesDirectory()) + filesSize(getWallpaperDirectory()) + fileContentSize(dbPrefix + "_chat.db") + fileContentSize(dbPrefix + "_agent.db") +} + +private func filesSize(_ dir: URL) -> Int64 { + let urls = (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey])) ?? [] + return urls.reduce(0) { sum, url in + let v = try? url.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey]) + return sum + (v?.isRegularFile == true ? Int64(v?.fileSize ?? 0) : 0) + } +} + +private func fileContentSize(_ path: String) -> Int64 { + (try? FileManager.default.attributesOfItem(atPath: path)[.size] as? Int64) ?? 0 } private func availableImportantBytes(at url: URL) -> Int64? { diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 671f88f6d7..43e34fdf76 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -36,7 +36,7 @@ public let MAX_FILE_SIZE_XFTP_LEGEND: Int64 = 5_368_709_120 // 5GB // a badge raises the limit at send for this long after its expiry, shorter than the receiver's grace public let BADGE_SND_GRACE_INTERVAL = TimeInterval(86400) -// Hard limit for standalone XFTP uploads (device migration archive); +// Hard limit for XFTP uploads (e.g. the 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 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 8c2ea21259..af6a3162bf 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 @@ -605,13 +605,9 @@ fun deleteDatabaseBackups() { // export holds a copy of the data in databaseExportDir while it writes the archive to archiveDir, // where a migration upload then also writes an encrypted copy of the archive -suspend fun confirmExportStorage(archiveDir: File = databaseExportDir): Boolean { +suspend fun confirmExportStorage(archiveDir: File = databaseExportDir, dataBytes: Long = exportDataBytes()): Boolean { databaseExportDir.mkdirs() archiveDir.mkdirs() - val dataBytes = directoryFileCountAndSize(appFilesDir.absolutePath).second + - directoryFileCountAndSize(wallpapersDir.absolutePath).second + - File(dataDir, chatDatabaseFileName).length() + - File(dataDir, agentDatabaseFileName).length() val (shortDir, requiredBytes) = listOf(archiveDir to 2L * dataBytes, databaseExportDir to dataBytes) .firstOrNull { (dir, needed) -> dir.usableSpace < needed } ?: return true val availableBytes = shortDir.usableSpace @@ -627,6 +623,12 @@ suspend fun confirmExportStorage(archiveDir: File = databaseExportDir): Boolean return proceed.await() } +fun exportDataBytes(): Long = + directoryFileCountAndSize(appFilesDir.absolutePath).second + + directoryFileCountAndSize(wallpapersDir.absolutePath).second + + File(dataDir, chatDatabaseFileName).length() + + File(dataDir, agentDatabaseFileName).length() + private suspend fun exportArchive( m: ChatModel, progressIndicator: MutableState, 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 188738e833..9d7a7fda6c 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 @@ -133,7 +133,7 @@ const val MAX_FILE_SIZE_XFTP_LEGEND: Long = 5_368_709_120 // 5GB // a badge raises the limit at send for this long after its expiry, shorter than the receiver's grace val BADGE_SND_GRACE_INTERVAL: Duration = 1.days -// Hard limit for standalone XFTP uploads (e.g. device migration archive); +// Hard limit for XFTP uploads (e.g. the 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 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 bfdbb8e64e..86f6cf6b69 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 @@ -479,9 +479,22 @@ private suspend fun MutableState.verifyDatabasePassphrase(db } } +private fun showDatabaseTooLargeAlert(bytes: Long) { + 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(bytes), formatBytes(MAX_FILE_SIZE_XFTP_HARD)) + ) +} + private fun MutableState.exportArchive() { withLongRunningApi { - if (!confirmExportStorage(getMigrationTempFilesDirectory())) { + val dataBytes = exportDataBytes() + if (dataBytes > MAX_FILE_SIZE_XFTP_HARD) { + showDatabaseTooLargeAlert(dataBytes) + state = MigrationFromState.UploadConfirmation + return@withLongRunningApi + } + if (!confirmExportStorage(getMigrationTempFilesDirectory(), dataBytes)) { state = MigrationFromState.UploadConfirmation return@withLongRunningApi } @@ -491,10 +504,7 @@ private fun MutableState.exportArchive() { val archiveBytes = File(archivePath).length() if (archiveBytes > MAX_FILE_SIZE_XFTP_HARD) { File(archivePath).delete() - 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(archiveBytes), formatBytes(MAX_FILE_SIZE_XFTP_HARD)) - ) + showDatabaseTooLargeAlert(archiveBytes) state = MigrationFromState.UploadConfirmation } else if (archiveErrors.isEmpty()) { uploadArchive(archivePath) diff --git a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml index df9092e4e5..9a570690af 100644 --- a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml @@ -2879,7 +2879,7 @@ 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). + The database (%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