From ba2891826af0ee35d667c08cbd6807a3e497e785 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 9 May 2024 14:28:03 -0300
Subject: [PATCH] Catch out 507 HTTP error when using WebDAV

Nextcloud has a bug that lets us write chunked transfers over quota:
https://github.com/nextcloud/server/issues/7993

However, when we upload small files, we can get the proper 507 response and thus detect out of space situations and warn the user about them.
---
 .../seedvault/KoinInstrumentationTestApp.kt           |  2 +-
 .../seedvault/plugins/StorageProperties.kt            | 10 +++++++++-
 .../seedvault/transport/backup/BackupCoordinator.kt   |  1 +
 .../seedvault/transport/backup/BackupModule.kt        |  1 +
 .../seedvault/transport/backup/KVBackup.kt            |  4 ++++
 .../seedvault/transport/CoordinatorIntegrationTest.kt | 10 ++++++++--
 .../seedvault/transport/backup/KVBackupTest.kt        | 11 ++++++++++-
 7 files changed, 34 insertions(+), 5 deletions(-)

diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
index d5f98989..d5b78ecc 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
@@ -32,7 +32,7 @@ class KoinInstrumentationTestApp : App() {
 
             single { spyk(BackupNotificationManager(context)) }
             single { spyk(FullBackup(get(), get(), get(), get(), get())) }
-            single { spyk(KVBackup(get(), get(), get(), get(), get())) }
+            single { spyk(KVBackup(get(), get(), get(), get(), get(), get())) }
             single { spyk(InputFactory()) }
 
             single { spyk(FullRestore(get(), get(), get(), get(), get())) }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StorageProperties.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StorageProperties.kt
index 8187d312..5fa3cfd4 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StorageProperties.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StorageProperties.kt
@@ -9,6 +9,7 @@ import android.content.Context
 import android.net.ConnectivityManager
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import androidx.annotation.WorkerThread
+import at.bitfire.dav4jvm.exception.HttpException
 import java.io.IOException
 
 abstract class StorageProperties<T> {
@@ -37,5 +38,12 @@ abstract class StorageProperties<T> {
 }
 
 fun Exception.isOutOfSpace(): Boolean {
-    return this is IOException && message?.contains("No space left on device") == true
+    return when (this) {
+        is IOException -> message?.contains("No space left on device") == true ||
+            (cause as? HttpException)?.code == 507
+
+        is HttpException -> code == 507
+
+        else -> false
+    }
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
index bacd39a6..82022934 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
@@ -382,6 +382,7 @@ internal class BackupCoordinator(
                 onPackageBackedUp(packageInfo, BackupType.FULL, size)
             } catch (e: Exception) {
                 Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
+                if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
                 result = TRANSPORT_PACKAGE_REJECTED
             }
             result
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
index f600c8b0..e120c689 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
@@ -19,6 +19,7 @@ val backupModule = module {
         KVBackup(
             pluginManager = get(),
             settingsManager = get(),
+            nm = get(),
             inputFactory = get(),
             crypto = get(),
             dbManager = get(),
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
index c867144f..a4732388 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt
@@ -14,7 +14,9 @@ import com.stevesoltys.seedvault.crypto.Crypto
 import com.stevesoltys.seedvault.header.VERSION
 import com.stevesoltys.seedvault.header.getADForKV
 import com.stevesoltys.seedvault.plugins.StoragePluginManager
+import com.stevesoltys.seedvault.plugins.isOutOfSpace
 import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import java.io.IOException
 import java.util.zip.GZIPOutputStream
 
@@ -34,6 +36,7 @@ private val TAG = KVBackup::class.java.simpleName
 internal class KVBackup(
     private val pluginManager: StoragePluginManager,
     private val settingsManager: SettingsManager,
+    private val nm: BackupNotificationManager,
     private val inputFactory: InputFactory,
     private val crypto: Crypto,
     private val dbManager: KvDbManager,
@@ -214,6 +217,7 @@ internal class KVBackup(
             TRANSPORT_OK
         } catch (e: IOException) {
             Log.e(TAG, "Error uploading DB", e)
+            if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
             TRANSPORT_ERROR
         } finally {
             this.state = null
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
index 347641f1..4e0ffd43 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -63,8 +63,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     @Suppress("Deprecation")
     private val legacyPlugin = mockk<LegacyStoragePlugin>()
     private val backupPlugin = mockk<StoragePlugin<*>>()
-    private val kvBackup =
-        KVBackup(storagePluginManager, settingsManager, inputFactory, cryptoImpl, dbManager)
+    private val kvBackup = KVBackup(
+        pluginManager = storagePluginManager,
+        settingsManager = settingsManager,
+        nm = notificationManager,
+        inputFactory = inputFactory,
+        crypto = cryptoImpl,
+        dbManager = dbManager,
+    )
     private val fullBackup = FullBackup(
         pluginManager = storagePluginManager,
         settingsManager = settingsManager,
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
index f9bbe264..a60c9a11 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt
@@ -14,6 +14,7 @@ import com.stevesoltys.seedvault.header.VERSION
 import com.stevesoltys.seedvault.header.getADForKV
 import com.stevesoltys.seedvault.plugins.StoragePlugin
 import com.stevesoltys.seedvault.plugins.StoragePluginManager
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.CapturingSlot
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -34,10 +35,18 @@ import kotlin.random.Random
 internal class KVBackupTest : BackupTest() {
 
     private val pluginManager = mockk<StoragePluginManager>()
+    private val notificationManager = mockk<BackupNotificationManager>()
     private val dataInput = mockk<BackupDataInput>()
     private val dbManager = mockk<KvDbManager>()
 
-    private val backup = KVBackup(pluginManager, settingsManager, inputFactory, crypto, dbManager)
+    private val backup = KVBackup(
+        pluginManager = pluginManager,
+        settingsManager = settingsManager,
+        nm = notificationManager,
+        inputFactory = inputFactory,
+        crypto = crypto,
+        dbManager = dbManager
+    )
 
     private val db = mockk<KVDb>()
     private val plugin = mockk<StoragePlugin<*>>()