Implement a storage plugin method to get free space
This commit is contained in:
parent
81cbb6e4dc
commit
1d8c438723
9 changed files with 90 additions and 2 deletions
|
@ -70,6 +70,17 @@ class PluginTest : KoinComponent {
|
||||||
assertNotNull(storagePlugin.providerPackageName)
|
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.
|
* This test initializes the storage three times while creating two new restore sets.
|
||||||
*
|
*
|
||||||
|
|
|
@ -13,6 +13,13 @@ interface StoragePlugin<T> {
|
||||||
*/
|
*/
|
||||||
suspend fun test(): Boolean
|
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.
|
* Start a new [RestoreSet] with the given token.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,13 +3,21 @@ package com.stevesoltys.seedvault.plugins.saf
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
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 android.util.Log
|
||||||
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.chunkFolderRegex
|
import com.stevesoltys.seedvault.plugins.chunkFolderRegex
|
||||||
import com.stevesoltys.seedvault.plugins.tokenRegex
|
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 org.calyxos.backup.storage.plugin.PluginConstants.SNAPSHOT_EXT
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -35,6 +43,32 @@ internal class DocumentsProviderStoragePlugin(
|
||||||
return dir != null && dir.exists()
|
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)
|
@Throws(IOException::class)
|
||||||
override suspend fun startNewRestoreSet(token: Long) {
|
override suspend fun startNewRestoreSet(token: Long) {
|
||||||
// reset current storage
|
// reset current storage
|
||||||
|
|
|
@ -40,7 +40,7 @@ internal class SafHandler(
|
||||||
} else {
|
} else {
|
||||||
safOption.title
|
safOption.title
|
||||||
}
|
}
|
||||||
return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork)
|
return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork, safOption.rootId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.plugins.StorageProperties
|
import com.stevesoltys.seedvault.plugins.StorageProperties
|
||||||
|
@ -16,6 +17,11 @@ data class SafStorage(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val isUsb: Boolean,
|
override val isUsb: Boolean,
|
||||||
override val requiresNetwork: 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>() {
|
) : StorageProperties<Uri>() {
|
||||||
|
|
||||||
val uri: Uri = config
|
val uri: Uri = config
|
||||||
|
|
|
@ -6,6 +6,7 @@ import at.bitfire.dav4jvm.DavCollection
|
||||||
import at.bitfire.dav4jvm.Response.HrefRelation.SELF
|
import at.bitfire.dav4jvm.Response.HrefRelation.SELF
|
||||||
import at.bitfire.dav4jvm.exception.NotFoundException
|
import at.bitfire.dav4jvm.exception.NotFoundException
|
||||||
import at.bitfire.dav4jvm.property.DisplayName
|
import at.bitfire.dav4jvm.property.DisplayName
|
||||||
|
import at.bitfire.dav4jvm.property.QuotaAvailableBytes
|
||||||
import at.bitfire.dav4jvm.property.ResourceType
|
import at.bitfire.dav4jvm.property.ResourceType
|
||||||
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
@ -43,6 +44,25 @@ internal class WebDavStoragePlugin(
|
||||||
return webDavSupported
|
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)
|
@Throws(IOException::class)
|
||||||
override suspend fun startNewRestoreSet(token: Long) {
|
override suspend fun startNewRestoreSet(token: Long) {
|
||||||
val location = "$url/$token".toHttpUrl()
|
val location = "$url/$token".toHttpUrl()
|
||||||
|
|
|
@ -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_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_NAME = "storageName"
|
||||||
private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
|
private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
|
||||||
private const val PREF_KEY_STORAGE_REQUIRES_NETWORK = "storageRequiresNetwork"
|
private const val PREF_KEY_STORAGE_REQUIRES_NETWORK = "storageRequiresNetwork"
|
||||||
|
@ -136,6 +137,7 @@ class SettingsManager(private val context: Context) {
|
||||||
fun setSafStorage(safStorage: SafStorage) {
|
fun setSafStorage(safStorage: SafStorage) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString())
|
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString())
|
||||||
|
.putString(PREF_KEY_STORAGE_ROOT_ID, safStorage.rootId)
|
||||||
.putString(PREF_KEY_STORAGE_NAME, safStorage.name)
|
.putString(PREF_KEY_STORAGE_NAME, safStorage.name)
|
||||||
.putBoolean(PREF_KEY_STORAGE_IS_USB, safStorage.isUsb)
|
.putBoolean(PREF_KEY_STORAGE_IS_USB, safStorage.isUsb)
|
||||||
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safStorage.requiresNetwork)
|
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safStorage.requiresNetwork)
|
||||||
|
@ -149,7 +151,8 @@ class SettingsManager(private val context: Context) {
|
||||||
?: throw IllegalStateException("no storage name")
|
?: throw IllegalStateException("no storage name")
|
||||||
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
|
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
|
||||||
val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, 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?) {
|
fun setFlashDrive(usb: FlashDrive?) {
|
||||||
|
|
|
@ -41,6 +41,12 @@ internal class WebDavStoragePluginTest : TransportTest() {
|
||||||
println(e)
|
println(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test getting free space`() = runBlocking {
|
||||||
|
val freeBytes = plugin.getFreeSpace() ?: fail()
|
||||||
|
assertTrue(freeBytes > 0)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test restore sets and reading+writing`() = runBlocking {
|
fun `test restore sets and reading+writing`() = runBlocking {
|
||||||
val token = System.currentTimeMillis()
|
val token = System.currentTimeMillis()
|
||||||
|
|
|
@ -64,6 +64,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
name = getRandomString(),
|
name = getRandomString(),
|
||||||
isUsb = false,
|
isUsb = false,
|
||||||
requiresNetwork = false,
|
requiresNetwork = false,
|
||||||
|
rootId = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
Loading…
Reference in a new issue