From e07b61d7f0788b3a2b58dd7be2f8dbf35b072e6d Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Tue, 28 Jan 2025 14:23:28 +0100 Subject: [PATCH] chore(backup): improve backup export error handling and result encapsulation Introduced `BackupExportResult` and `ExportResult` to encapsulate and represent export operation outcomes, including success and specific failure types (`IOError`, `ZipError`). Refactored relevant methods to use these types, added coroutine support annotations, and implemented error handling for zipping and I/O operations. Added unit tests to ensure correct error handling behavior. --- .../com/wire/backup/dump/ExportResult.kt | 28 ++++++++++ .../com/wire/backup/dump/MPBackupExporter.kt | 16 +++++- .../wire/backup/dump/MPBackupExporterTest.kt | 52 +++++++++++++++++++ .../wire/backup/dump/BackupExportResult.kt | 36 +++++++++++++ .../com/wire/backup/dump/MPBackupExporter.kt | 11 ++-- .../wire/backup/dump/BackupExportResult.kt | 40 ++++++++++++++ .../com/wire/backup/dump/MPBackupExporter.kt | 14 +++-- .../wire/backup/ingest/MPBackupImporter.kt | 2 + 8 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/dump/ExportResult.kt create mode 100644 backup/src/commonTest/kotlin/com/wire/backup/dump/MPBackupExporterTest.kt create mode 100644 backup/src/jsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt create mode 100644 backup/src/nonJsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt diff --git a/backup/src/commonMain/kotlin/com/wire/backup/dump/ExportResult.kt b/backup/src/commonMain/kotlin/com/wire/backup/dump/ExportResult.kt new file mode 100644 index 0000000000..5f66d0a807 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/dump/ExportResult.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.backup.dump + +internal sealed interface ExportResult { + + data object Success : ExportResult + + sealed class Failure(val message: String) : ExportResult { + class IOError(message: String) : Failure(message) + class ZipError(message: String) : Failure(message) + } +} diff --git a/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt index ccc77b07e4..bd9a25e217 100644 --- a/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt +++ b/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -131,9 +131,18 @@ public abstract class CommonMPBackupExporter( return buffer.write(this.encodeToByteArray()) } - internal suspend fun finalize(password: String?, output: Sink) { + @Suppress("TooGenericExceptionCaught") + internal suspend fun finalize(password: String?, output: Sink): ExportResult { flushAll() - val zippedData = zipEntries(storage.listEntries()).await() + val zippedData = try { + zipEntries(storage.listEntries()).await() + } catch (t: Throwable) { + return ExportResult.Failure.ZipError(t.message ?: "Unknown ZIP error.") + } + return writeBackupArtifact(output, password, zippedData) + } + + private suspend fun writeBackupArtifact(output: Sink, password: String?, zippedData: Source): ExportResult = try { val salt = XChaChaPoly1305AuthenticationData.newSalt() val header = BackupHeader( @@ -165,6 +174,9 @@ public abstract class CommonMPBackupExporter( } bufferedOutput } + ExportResult.Success + } catch (t: Throwable) { + ExportResult.Failure.IOError(t.message ?: "Unknown IO error.") } internal abstract val storage: EntryStorage diff --git a/backup/src/commonTest/kotlin/com/wire/backup/dump/MPBackupExporterTest.kt b/backup/src/commonTest/kotlin/com/wire/backup/dump/MPBackupExporterTest.kt new file mode 100644 index 0000000000..3c3528c939 --- /dev/null +++ b/backup/src/commonTest/kotlin/com/wire/backup/dump/MPBackupExporterTest.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.backup.dump + +import com.wire.backup.data.BackupQualifiedId +import com.wire.backup.filesystem.BackupEntry +import com.wire.backup.filesystem.EntryStorage +import com.wire.backup.filesystem.InMemoryEntryStorage +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.test.runTest +import okio.Buffer +import okio.Source +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class MPBackupExporterTest { + + @Test + fun givenZippingError_whenFinalizing_thenZipErrorShouldBeReturned() = runTest { + val thrownException = IllegalStateException("Zipping failed!") + val subject = object : CommonMPBackupExporter( + BackupQualifiedId("user", "domain") + ) { + override val storage: EntryStorage = InMemoryEntryStorage() + + override fun zipEntries(data: List): Deferred { + throw thrownException + } + } + + val result = subject.finalize(null, Buffer()) + assertIs(result) + assertEquals(thrownException.message, result.message) + } + +} diff --git a/backup/src/jsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt b/backup/src/jsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt new file mode 100644 index 0000000000..018c3d18c0 --- /dev/null +++ b/backup/src/jsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.backup.dump + +@JsExport +public sealed class BackupExportResult { + public class Success(public val bytes: ByteArray) : BackupExportResult() + public sealed class Failure(public val message: String) : BackupExportResult() { + /** + * Represents an I/O error that occurs during an export process. + * + * It's unlikely for this to ever be thrown on JavaScript/Browser + */ + public class IOError(message: String) : Failure(message) + + /** + * An error happened during the zipping process. + */ + public class ZipError(message: String) : Failure(message) + } +} diff --git a/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt index 33887b047b..2138976508 100644 --- a/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt +++ b/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -58,11 +58,12 @@ public actual class MPBackupExporter( return result.asDeferred() } - public fun finalize(password: String?): Promise { - return GlobalScope.promise { - val output = Buffer() - finalize(password, output) - output.readByteArray() + public fun finalize(password: String?): Promise = GlobalScope.promise { + val output = Buffer() + when (val result = finalize(password, output)) { + is ExportResult.Failure.IOError -> BackupExportResult.Failure.IOError(result.message) + is ExportResult.Failure.ZipError -> BackupExportResult.Failure.ZipError(result.message) + ExportResult.Success -> BackupExportResult.Success(output.readByteArray()) } } } diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt new file mode 100644 index 0000000000..edfd597a32 --- /dev/null +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/BackupExportResult.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.backup.dump + +public sealed interface BackupExportResult { + /** + * Represents a successful result of a backup export operation. + * + * @property pathToOutputFile The path to the resulting output file of the export. + */ + public class Success(public val pathToOutputFile: String) : BackupExportResult + public sealed interface Failure : BackupExportResult { + public val message: String + + /** + * Represents an I/O error that occurs during an export process. + */ + public class IOError(override val message: String) : Failure + + /** + * An error happened during the zipping process. + */ + public class ZipError(override val message: String) : Failure + } +} diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt index b1d099fe77..ced4dec1ed 100644 --- a/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -17,6 +17,7 @@ */ package com.wire.backup.dump +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import com.wire.backup.data.BackupQualifiedId import com.wire.backup.filesystem.BackupEntry import com.wire.backup.filesystem.EntryStorage @@ -54,13 +55,20 @@ public actual class MPBackupExporter( ) } - public suspend fun finalize(password: String?): String { + @NativeCoroutines + @Suppress("TooGenericExceptionCaught") + public suspend fun finalize(password: String?): BackupExportResult = try { val fileName = "export.wbu" val path = outputDirectory.toPath() / fileName fileSystem.delete(path) fileSystem.createDirectories(path.parent!!) val fileHandle = fileSystem.openReadWrite(path) - finalize(password, fileHandle.sink()) - return path.toString() + when (val result = finalize(password, fileHandle.sink())) { + is ExportResult.Failure.IOError -> BackupExportResult.Failure.IOError(result.message) + is ExportResult.Failure.ZipError -> BackupExportResult.Failure.ZipError(result.message) + ExportResult.Success -> BackupExportResult.Success(path.toString()) + } + } catch (io: Throwable) { + BackupExportResult.Failure.IOError(io.message ?: "Unknown IO error.") } } diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt index ec01e9c5be..76b5a44bf8 100644 --- a/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt @@ -17,6 +17,7 @@ */ package com.wire.backup.ingest +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import com.wire.backup.filesystem.EntryStorage import com.wire.backup.filesystem.FileBasedEntryStorage import okio.FileSystem @@ -45,6 +46,7 @@ public actual class MPBackupImporter( * @param multiplatformBackupFilePath the path to the decrypted, unzipped backup data file */ @ObjCName("importFile") + @NativeCoroutines public suspend fun importFromFile( multiplatformBackupFilePath: String, passphrase: String?,