mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-03 22:01:41 +00:00
android, desktop, ios: clear migration errors for oversized database and low storage
This commit is contained in:
@@ -37,6 +37,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError])
|
||||
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
case notEnoughStorage(required: Int64, available: Int64)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
@@ -52,6 +53,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
case let .archiveExportedWithErrors(path, _): return "archiveExportedWithErrors \(path)"
|
||||
|
||||
case let .error(title, _): return "error \(title)"
|
||||
case .notEnoughStorage: return "notEnoughStorage"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +187,16 @@ struct MigrateFromDevice: View {
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
case let .notEnoughStorage(required, available):
|
||||
return Alert(
|
||||
title: Text("You may not have enough storage"),
|
||||
message: Text(String.localizedStringWithFormat(
|
||||
NSLocalizedString("Exporting the database may need about %@ of free space, but only %@ is available. Continue anyway?", comment: "migration alert"),
|
||||
ByteCountFormatter.string(fromByteCount: required, countStyle: .binary),
|
||||
ByteCountFormatter.string(fromByteCount: available, countStyle: .binary))),
|
||||
primaryButton: .default(Text("Continue")) { exportArchive() },
|
||||
secondaryButton: .cancel { migrationState = .uploadConfirmation }
|
||||
)
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(backDisabled)
|
||||
@@ -262,7 +274,7 @@ struct MigrateFromDevice: View {
|
||||
progressView()
|
||||
}
|
||||
.onAppear {
|
||||
exportArchive()
|
||||
exportArchiveCheckingStorage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +475,19 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchiveCheckingStorage() {
|
||||
Task {
|
||||
let dataBytes = await Task.detached { estimatedExportBytes() }.value
|
||||
// migration transiently writes an uncompressed temp copy and a padded encrypted upload copy, so require ~2x
|
||||
let required = dataBytes * 2
|
||||
if let available = availableImportantBytes(at: getDocumentsDirectory()), available < required {
|
||||
await MainActor.run { alert = .notEnoughStorage(required: required, available: available) }
|
||||
} else {
|
||||
await MainActor.run { exportArchive() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() {
|
||||
Task {
|
||||
do {
|
||||
@@ -488,6 +513,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 +821,17 @@ struct MigrateFromDevice_Previews: PreviewProvider {
|
||||
MigrateFromDevice(showProgressOnSettings: Binding.constant(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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
+37
-2
@@ -274,7 +274,7 @@ private fun MutableState<MigrationFromState>.ArchivingView() {
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
exportArchive()
|
||||
exportArchiveCheckingStorage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,6 +480,33 @@ private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(db
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationFromState>.exportArchiveCheckingStorage() {
|
||||
withBGApi {
|
||||
databaseExportDir.mkdirs()
|
||||
// migration transiently writes an uncompressed temp copy and a padded encrypted upload copy,
|
||||
// so require ~2x the data on the volume
|
||||
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) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.migrate_from_device_not_enough_space_title),
|
||||
text = String.format(generalGetString(MR.strings.migrate_from_device_not_enough_space_desc), formatBytes(requiredBytes), formatBytes(availableBytes)),
|
||||
confirmText = generalGetString(MR.strings.chat_database_exported_continue),
|
||||
onConfirm = { exportArchive() },
|
||||
onDismiss = { state = MigrationFromState.UploadConfirmation },
|
||||
onDismissRequest = { state = MigrationFromState.UploadConfirmation },
|
||||
)
|
||||
} else {
|
||||
exportArchive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationFromState>.exportArchive() {
|
||||
withLongRunningApi {
|
||||
try {
|
||||
@@ -506,7 +533,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="migrate_from_device_not_enough_space_title">You may not have enough storage</string>
|
||||
<string name="migrate_from_device_not_enough_space_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>
|
||||
|
||||
Reference in New Issue
Block a user