Implement a storage plugin method to get free space

This commit is contained in:
Torsten Grote 2024-04-24 16:37:31 -03:00 committed by Chirayu Desai
parent 81cbb6e4dc
commit 1d8c438723
9 changed files with 90 additions and 2 deletions

View file

@ -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.
*

View file

@ -13,6 +13,13 @@ interface StoragePlugin<T> {
*/
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.
*

View file

@ -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 org.calyxos.backup.storage.plugin.PluginConstants.SNAPSHOT_EXT
import java.io.FileNotFoundException
import java.io.IOException
@ -35,6 +43,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

View file

@ -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)
}
/**

View file

@ -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<Uri>() {
val uri: Uri = config

View file

@ -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
@ -43,6 +44,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()

View file

@ -32,6 +32,7 @@ internal enum class StoragePluginType { // 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?) {

View file

@ -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()

View file

@ -64,6 +64,7 @@ internal class BackupCoordinatorTest : BackupTest() {
name = getRandomString(),
isUsb = false,
requiresNetwork = false,
rootId = null,
)
init {