Skip to content

Commit

Permalink
chore(backup): improve backup export error handling and result encaps…
Browse files Browse the repository at this point in the history
…ulation

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.
  • Loading branch information
vitorhugods committed Jan 29, 2025
1 parent a4e2968 commit effdec9
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 12 deletions.
2 changes: 2 additions & 0 deletions backup/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ plugins {
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
id(libs.plugins.kalium.library.get().pluginId)
alias(libs.plugins.kotlinNativeCoroutines)
}

kaliumLibrary {
Expand Down Expand Up @@ -59,6 +60,7 @@ kotlin {
all {
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.experimental.ExperimentalObjCRefinement")
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
languageSettings.optIn("kotlin.js.ExperimentalJsExport")
}
val commonMain by getting {
Expand Down
28 changes: 28 additions & 0 deletions backup/src/commonMain/kotlin/com/wire/backup/dump/ExportResult.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,19 @@ 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)
}

@Suppress("TooGenericExceptionCaught")
private suspend fun writeBackupArtifact(output: Sink, password: String?, zippedData: Source): ExportResult = try {
val salt = XChaChaPoly1305AuthenticationData.newSalt()

val header = BackupHeader(
Expand Down Expand Up @@ -165,6 +175,9 @@ public abstract class CommonMPBackupExporter(
}
bufferedOutput
}
ExportResult.Success
} catch (t: Throwable) {
ExportResult.Failure.IOError(t.message ?: "Unknown IO error.")
}

internal abstract val storage: EntryStorage
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BackupEntry>): Deferred<Source> {
throw thrownException
}
}

val result = subject.finalize(null, Buffer())
assertIs<ExportResult.Failure.ZipError>(result)
assertEquals(thrownException.message, result.message)
}

}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ public actual class MPBackupExporter(
return result.asDeferred()
}

public fun finalize(password: String?): Promise<ByteArray> {
return GlobalScope.promise {
val output = Buffer()
finalize(password, output)
output.readByteArray()
public fun finalize(password: String?): Promise<BackupExportResult> = 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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.wire.backup

import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.dump.BackupExportResult
import com.wire.backup.dump.CommonMPBackupExporter
import com.wire.backup.dump.MPBackupExporter
import com.wire.backup.ingest.BackupImportResult
Expand All @@ -34,7 +35,7 @@ actual fun endToEndTestSubjectProvider() = object : CommonBackupEndToEndTestSubj
val exporter = MPBackupExporter(selfUserId)
exporter.export()
val artifactPath = exporter.finalize(passphrase)
val artifactData = artifactPath.await()
val artifactData = (artifactPath.await() as BackupExportResult.Success).bytes
val importer = MPBackupImporter()
return importer.importFromFileData(artifactData, passphrase).await()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.wire.backup

import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.dump.BackupExportResult
import com.wire.backup.dump.CommonMPBackupExporter
import com.wire.backup.dump.MPBackupExporter
import com.wire.backup.ingest.BackupImportResult
Expand Down Expand Up @@ -51,7 +52,8 @@ actual fun endToEndTestSubjectProvider() = object : CommonBackupEndToEndTestSubj
zipper
)
exporter.export()
val artifactPath = exporter.finalize(passphrase)
val artifactPath = (exporter.finalize(passphrase) as BackupExportResult.Success).pathToOutputFile

val importer = MPBackupImporter(exportDirectory.toString(), zipper)
return importer.importFromFile(artifactPath, passphrase)
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ cryptobox4j = "1.4.0"
cryptobox-android = "1.1.5"
android-security = "1.1.0-alpha06"
ktor = "2.3.10"
kotlinNativeCoroutines = "1.0.0-ALPHA-26"
okio = "3.9.0"
ok-http = "4.12.0"
mockative = "2.2.0"
Expand Down Expand Up @@ -79,6 +80,7 @@ moduleGraph = { id = "dev.iurysouza.modulegraph", version.ref = "moduleGraph" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinNativeCoroutines = { id = "com.rickclephas.kmp.nativecoroutines", version.ref = "kotlinNativeCoroutines"}
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
carthage = { id = "com.wire.carthage-gradle-plugin", version.ref = "carthage" }
Expand Down

0 comments on commit effdec9

Please sign in to comment.