Skip to content

Commit

Permalink
Implement a storage plugin method to get free space
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Apr 25, 2024
1 parent af16799 commit 7189d60
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 2 deletions.
11 changes: 11 additions & 0 deletions app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 7189d60

Please sign in to comment.