android, desktop, ios: warn on low storage before exporting or migrating the database, reject oversized migration

This commit is contained in:
Narasimha-sc
2026-06-08 11:32:24 +00:00
parent b9d1f0c0a3
commit 89a3451696
7 changed files with 113 additions and 3 deletions
@@ -457,6 +457,7 @@ struct DatabaseView: View {
}
private func exportArchive() async -> Bool {
if !(await confirmExportStorage()) { return false }
await MainActor.run {
progressIndicator = true
}
@@ -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
}
+4
View File
@@ -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
@@ -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<Boolean>()
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<Boolean>,
chatArchiveFile: MutableState<String?>,
saveArchiveLauncher: FileChooserLauncher
): Boolean {
if (!confirmExportStorage()) return false
progressIndicator.value = true
try {
val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveFile)
@@ -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
@@ -274,7 +274,7 @@ private fun MutableState<MigrationFromState>.ArchivingView() {
ProgressView()
}
LaunchedEffect(Unit) {
exportArchive()
exportArchiveCheckingStorage()
}
}
@@ -480,6 +480,13 @@ private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(db
}
}
private fun MutableState<MigrationFromState>.exportArchiveCheckingStorage() {
withBGApi {
if (confirmExportStorage()) exportArchive()
else state = MigrationFromState.UploadConfirmation
}
}
private fun MutableState<MigrationFromState>.exportArchive() {
withLongRunningApi {
try {
@@ -506,7 +513,15 @@ private fun MutableState<MigrationFromState>.exportArchive() {
private fun MutableState<MigrationFromState>.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
@@ -2831,6 +2831,10 @@
<string name="migrate_from_device_to_another_device">Migrate to another device</string>
<string name="migrate_from_device_error_saving_settings">Error saving settings</string>
<string name="migrate_from_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
<string name="migrate_from_device_database_too_large_title">Database is too large</string>
<string name="migrate_from_device_database_too_large_desc">The exported archive (%1$s) is larger than the maximum size supported for migration (%2$s).</string>
<string name="not_enough_storage_title">You may not have enough storage</string>
<string name="not_enough_storage_desc">Exporting the database may need about %1$s of free space, but only %2$s is available. Continue anyway?</string>
<string name="migrate_from_device_error_exporting_archive">Error exporting chat database</string>
<string name="migrate_from_device_database_init">Preparing upload</string>
<string name="migrate_from_device_error_uploading_archive">Error uploading the archive</string>