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 28, 2025
1 parent a4e2968 commit e07b61d
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 10 deletions.
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,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(
Expand Down Expand Up @@ -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
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
@@ -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

0 comments on commit e07b61d

Please sign in to comment.