Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ plugins {

allprojects {
group = "com.thelightphone.backup"
version = "0.0.7"
version = "0.0.8"

plugins.withId("com.vanniktech.maven.publish") {
extensions.configure<MavenPublishBaseExtension> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
package com.thelightphone.backup

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.time.Duration
import kotlin.time.Instant

// Outcome of the most recent BackupWorker run, written by BackupWorker itself.
// Outcome of the most recent (or currently running) BackupWorker run, written by BackupWorker itself.
@Serializable
sealed class BackupStatus {
data class Succeeded(val completedAt: Instant) : BackupStatus()
// A backup run is currently underway.
@Serializable
data class InProgress(val startedAt: Instant) : BackupStatus()

// A backup run that has finished, success or failure
sealed interface Terminal {
val finishedAt: Instant
}

@Serializable
data class Succeeded(override val finishedAt: Instant) : BackupStatus(), Terminal

// The linked account's credentials are dead (RemoteBackupError.Unauthorized). Shouldn't automatically retry
data object NeedsReauth : BackupStatus()
@Serializable
data class NeedsReauth(override val finishedAt: Instant) : BackupStatus(), Terminal

// The remote account is out of space (RemoteBackupError.QuotaExceeded). Shouldn't automatically retry
data object QuotaExceeded : BackupStatus()
@Serializable
data class QuotaExceeded(override val finishedAt: Instant) : BackupStatus(), Terminal

// Anything else, likely transient so retries are ok
data class Failed(val message: String) : BackupStatus()
@Serializable
data class Failed(val message: String, override val finishedAt: Instant) : BackupStatus(), Terminal

companion object {
private val json = Json { ignoreUnknownKeys = true }
fun encode(status: BackupStatus): String = json.encodeToString(status)
fun decode(value: String): BackupStatus? = runCatching { json.decodeFromString<BackupStatus>(value) }.getOrNull()
}
}

// Persistent storage for Backup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import androidx.work.WorkManager
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import kotlin.time.Clock
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
Expand All @@ -43,6 +46,7 @@ class BackupWorker(

ensureNotificationChannel()
setForeground(buildForegroundInfo())
preferences.setLastBackupStatus(BackupStatus.InProgress(clock.now()))

val runner = BackupRunner(
remoteBackup,
Expand All @@ -57,14 +61,25 @@ class BackupWorker(
)
},
)
return when (val result = runner.run()) {
is BackupResult.Completed -> {
preferences.setLastBackupStatus(BackupStatus.Succeeded(clock.now()))
Result.success()
return try {
when (val result = runner.run()) {
is BackupResult.Completed -> {
preferences.setLastBackupStatus(BackupStatus.Succeeded(clock.now()))
Result.success()
}

is BackupResult.Partial -> recordFailureAndDecide(result.abortedBy?.cause)
is BackupResult.Failed -> recordFailureAndDecide(result.cause.cause)
}

is BackupResult.Partial -> recordFailureAndDecide(result.abortedBy?.cause)
is BackupResult.Failed -> recordFailureAndDecide(result.cause.cause)
} catch (e: CancellationException) {
// Job's already cancelled, so writing the status needs NonCancellable to run at all.
withContext(NonCancellable) {
preferences.setLastBackupStatus(BackupStatus.Failed(e.message ?: "backup cancelled", clock.now()))
}
throw e
} catch (e: Exception) {
preferences.setLastBackupStatus(BackupStatus.Failed(e.message ?: "backup failed", clock.now()))
Result.retry()
}
}

Expand All @@ -88,17 +103,17 @@ class BackupWorker(

private suspend fun recordFailureAndDecide(cause: Throwable?): Result = when (cause) {
is RemoteBackupError.Unauthorized -> {
preferences.setLastBackupStatus(BackupStatus.NeedsReauth)
preferences.setLastBackupStatus(BackupStatus.NeedsReauth(clock.now()))
Result.failure()
}

is RemoteBackupError.QuotaExceeded -> {
preferences.setLastBackupStatus(BackupStatus.QuotaExceeded)
preferences.setLastBackupStatus(BackupStatus.QuotaExceeded(clock.now()))
Result.failure()
}

else -> {
preferences.setLastBackupStatus(BackupStatus.Failed(cause?.message ?: "backup failed"))
preferences.setLastBackupStatus(BackupStatus.Failed(cause?.message ?: "backup failed", clock.now()))
Result.retry()
}
}
Expand Down
Loading