From 7189d609c9588207e2d61a9104db1af207969fe7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 24 Apr 2024 16:37:31 -0300 Subject: [PATCH] Implement a storage plugin method to get free space --- .../com/stevesoltys/seedvault/PluginTest.kt | 11 ++++++ .../seedvault/plugins/StoragePlugin.kt | 7 ++++ .../saf/DocumentsProviderStoragePlugin.kt | 34 +++++++++++++++++++ .../seedvault/plugins/saf/SafHandler.kt | 2 +- .../seedvault/plugins/saf/SafStorage.kt | 6 ++++ .../plugins/webdav/WebDavStoragePlugin.kt | 20 +++++++++++ .../seedvault/settings/SettingsManager.kt | 5 ++- .../plugins/webdav/WebDavStoragePluginTest.kt | 6 ++++ 8 files changed, 89 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt index 39e4be9ed..67fc0426d 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -70,6 +70,17 @@ class PluginTest : KoinComponent { assertNotNull(storagePlugin.providerPackageName) } + @Test + fun testTest() = runBlocking(Dispatchers.IO) { + assertTrue(storagePlugin.test()) + } + + @Test + fun testGetFreeSpace() = runBlocking(Dispatchers.IO) { + val freeBytes = storagePlugin.getFreeSpace() ?: error("no free space retrieved") + assertTrue(freeBytes > 0) + } + /** * This test initializes the storage three times while creating two new restore sets. * diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt index f5109addd..f8a844984 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt @@ -13,6 +13,13 @@ interface StoragePlugin { */ suspend fun test(): Boolean + /** + * Retrieves the available storage space in bytes. + * @return the number of bytes available or null if the number is unknown. + * Returning a negative number or zero to indicate unknown is discouraged. + */ + suspend fun getFreeSpace(): Long? + /** * Start a new [RestoreSet] with the given token. * diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt index e1d88f543..b2614e9c0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt @@ -3,13 +3,21 @@ package com.stevesoltys.seedvault.plugins.saf import android.content.Context import android.content.pm.PackageManager import android.net.Uri +import android.os.Environment +import android.os.StatFs +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES +import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID import android.util.Log +import androidx.core.database.getIntOrNull import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.getStorageContext import com.stevesoltys.seedvault.plugins.EncryptedMetadata import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.plugins.chunkFolderRegex import com.stevesoltys.seedvault.plugins.tokenRegex +import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE +import com.stevesoltys.seedvault.ui.storage.ROOT_ID_DEVICE import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -34,6 +42,32 @@ internal class DocumentsProviderStoragePlugin( return dir != null && dir.exists() } + override suspend fun getFreeSpace(): Long? { + val rootId = storage.safStorage.rootId ?: return null + val authority = storage.safStorage.uri.authority + // using DocumentsContract#buildRootUri(String, String) with rootId directly doesn't work + val rootUri = DocumentsContract.buildRootsUri(authority) + val projection = arrayOf(COLUMN_AVAILABLE_BYTES) + // query directly for our rootId + val bytesAvailable = context.contentResolver.query( + rootUri, projection, "$COLUMN_ROOT_ID=?", arrayOf(rootId), null + )?.use { c -> + if (!c.moveToNext()) return@use null // no results + val bytes = c.getIntOrNull(c.getColumnIndex(COLUMN_AVAILABLE_BYTES)) + if (bytes != null && bytes >= 0) return@use bytes.toLong() + else return@use null + } + // if we didn't get anything from SAF, try some known hacks + return if (bytesAvailable == null && authority == AUTHORITY_STORAGE) { + if (rootId == ROOT_ID_DEVICE) { + StatFs(Environment.getDataDirectory().absolutePath).availableBytes + } else if (storage.safStorage.isUsb) { + val documentId = storage.safStorage.uri.lastPathSegment ?: return null + StatFs("/mnt/media_rw/${documentId.trimEnd(':')}").availableBytes + } else null + } else bytesAvailable + } + @Throws(IOException::class) override suspend fun startNewRestoreSet(token: Long) { // reset current storage diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt index 3b8dc78c9..ec4554f73 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt @@ -40,7 +40,7 @@ internal class SafHandler( } else { safOption.title } - return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork) + return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork, safOption.rootId) } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt index 35d58fba4..dae9410b5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt @@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.plugins.saf import android.content.Context import android.net.Uri +import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID import androidx.annotation.WorkerThread import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.plugins.StorageProperties @@ -16,6 +17,11 @@ data class SafStorage( override val name: String, override val isUsb: Boolean, override val requiresNetwork: Boolean, + /** + * The [COLUMN_ROOT_ID] for the [uri]. + * This is only nullable for historic reasons, because we didn't always store it. + */ + val rootId: String?, ) : StorageProperties() { val uri: Uri = config diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt index 0d9986cd8..fee4db6f6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt @@ -6,6 +6,7 @@ import at.bitfire.dav4jvm.DavCollection import at.bitfire.dav4jvm.Response.HrefRelation.SELF import at.bitfire.dav4jvm.exception.NotFoundException import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.QuotaAvailableBytes import at.bitfire.dav4jvm.property.ResourceType import com.stevesoltys.seedvault.plugins.EncryptedMetadata import com.stevesoltys.seedvault.plugins.StoragePlugin @@ -42,6 +43,25 @@ internal class WebDavStoragePlugin( return webDavSupported } + override suspend fun getFreeSpace(): Long? { + val location = url.toHttpUrl() + val davCollection = DavCollection(okHttpClient, location) + + val availableBytes = suspendCoroutine { cont -> + davCollection.propfind(depth = 0, QuotaAvailableBytes.NAME) { response, _ -> + debugLog { "getFreeSpace() = $response" } + val quota = response.properties.getOrNull(0) as? QuotaAvailableBytes + val availableBytes = quota?.quotaAvailableBytes ?: -1 + if (availableBytes > 0) { + cont.resume(availableBytes) + } else { + cont.resume(null) + } + } + } + return availableBytes + } + @Throws(IOException::class) override suspend fun startNewRestoreSet(token: Long) { val location = "$url/$token".toHttpUrl() diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index d6e977c0a..fcadac37b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -32,6 +32,7 @@ internal enum class StoragePluginEnum { // don't rename, will break existing ins } private const val PREF_KEY_STORAGE_URI = "storageUri" +private const val PREF_KEY_STORAGE_ROOT_ID = "storageRootId" private const val PREF_KEY_STORAGE_NAME = "storageName" private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb" private const val PREF_KEY_STORAGE_REQUIRES_NETWORK = "storageRequiresNetwork" @@ -136,6 +137,7 @@ class SettingsManager(private val context: Context) { fun setSafStorage(safStorage: SafStorage) { prefs.edit() .putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString()) + .putString(PREF_KEY_STORAGE_ROOT_ID, safStorage.rootId) .putString(PREF_KEY_STORAGE_NAME, safStorage.name) .putBoolean(PREF_KEY_STORAGE_IS_USB, safStorage.isUsb) .putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safStorage.requiresNetwork) @@ -149,7 +151,8 @@ class SettingsManager(private val context: Context) { ?: throw IllegalStateException("no storage name") val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false) val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false) - return SafStorage(uri, name, isUsb, requiresNetwork) + val rootId = prefs.getString(PREF_KEY_STORAGE_ROOT_ID, null) + return SafStorage(uri, name, isUsb, requiresNetwork, rootId) } fun setFlashDrive(usb: FlashDrive?) { diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt index d36910156..769dc5ba7 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt @@ -41,6 +41,12 @@ internal class WebDavStoragePluginTest : TransportTest() { println(e) } + @Test + fun `test getting free space`() = runBlocking { + val freeBytes = plugin.getFreeSpace() ?: fail() + assertTrue(freeBytes > 0) + } + @Test fun `test restore sets and reading+writing`() = runBlocking { val token = System.currentTimeMillis()