Switch everything to new backends

This commit is contained in:
Torsten Grote 2024-08-27 15:12:25 -03:00
parent 58d58415c5
commit 96a3564610
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
94 changed files with 446 additions and 1569 deletions

View file

@ -56,7 +56,7 @@ class KoinInstrumentationTestApp : App() {
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
backendManager = get(),
fileSelectionManager = get(),
)
)

View file

@ -9,16 +9,17 @@ import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.backend.saf.DocumentsProviderLegacyPlugin
import com.stevesoltys.seedvault.backend.saf.DocumentsStorage
import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -38,11 +39,10 @@ class PluginTest : KoinComponent {
private val mockedSettingsManager: SettingsManager = mockk()
private val storage = DocumentsStorage(
appContext = context,
settingsManager = mockedSettingsManager,
safStorage = settingsManager.getSafStorage() ?: error("No SAF storage"),
safStorage = settingsManager.getSafProperties() ?: error("No SAF storage"),
)
private val storagePlugin = DocumentsProviderStoragePlugin(context, storage.safStorage)
private val backend = SafBackend(context, storage.safStorage)
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin = DocumentsProviderLegacyPlugin(context) {
@ -55,29 +55,30 @@ class PluginTest : KoinComponent {
@Before
fun setup() = runBlocking {
every { mockedSettingsManager.getSafStorage() } returns settingsManager.getSafStorage()
storagePlugin.removeAll()
every {
mockedSettingsManager.getSafProperties()
} returns settingsManager.getSafProperties()
backend.removeAll()
}
@After
fun tearDown() = runBlocking {
storagePlugin.removeAll()
Unit
backend.removeAll()
}
@Test
fun testProviderPackageName() {
assertNotNull(storagePlugin.providerPackageName)
assertNotNull(backend.providerPackageName)
}
@Test
fun testTest() = runBlocking(Dispatchers.IO) {
assertTrue(storagePlugin.test())
assertTrue(backend.test())
}
@Test
fun testGetFreeSpace() = runBlocking(Dispatchers.IO) {
val freeBytes = storagePlugin.getFreeSpace() ?: error("no free space retrieved")
val freeBytes = backend.getFreeSpace() ?: error("no free space retrieved")
assertTrue(freeBytes > 0)
}
@ -91,49 +92,39 @@ class PluginTest : KoinComponent {
@Test
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
assertEquals(0, backend.getAvailableBackups()?.toList()?.size)
// prepare returned tokens requested when initializing device
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
// start new restore set and initialize device afterwards
storagePlugin.startNewRestoreSet(token)
storagePlugin.initializeDevice()
// write metadata (needed for backup to be recognized)
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA)
backend.save(LegacyAppBackupFile.Metadata(token))
.writeAndClose(getRandomByteArray())
// one backup available now
assertEquals(1, storagePlugin.getAvailableBackups()?.toList()?.size)
assertEquals(1, backend.getAvailableBackups()?.toList()?.size)
// initializing again (with another restore set) does add a restore set
storagePlugin.startNewRestoreSet(token + 1)
storagePlugin.initializeDevice()
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
backend.save(LegacyAppBackupFile.Metadata(token + 1))
.writeAndClose(getRandomByteArray())
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
// initializing again (without new restore set) doesn't change number of restore sets
storagePlugin.initializeDevice()
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
backend.save(LegacyAppBackupFile.Metadata(token + 1))
.writeAndClose(getRandomByteArray())
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
}
@Test
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
every { mockedSettingsManager.getToken() } returns token
storagePlugin.startNewRestoreSet(token)
storagePlugin.initializeDevice()
// write metadata
val metadata = getRandomByteArray()
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
// get available backups, expect only one with our token and no error
var availableBackups = storagePlugin.getAvailableBackups()?.toList()
var availableBackups = backend.getAvailableBackups()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
@ -142,9 +133,8 @@ class PluginTest : KoinComponent {
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
// initializing again (without changing storage) keeps restore set with same token
storagePlugin.initializeDevice()
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
availableBackups = storagePlugin.getAvailableBackups()?.toList()
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
availableBackups = backend.getAvailableBackups()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
@ -161,7 +151,8 @@ class PluginTest : KoinComponent {
// write random bytes as APK
val apk1 = getRandomByteArray(1337 * 1024)
storagePlugin.getOutputStream(token, "${packageInfo.packageName}.apk").writeAndClose(apk1)
backend.save(LegacyAppBackupFile.Blob(token, "${packageInfo.packageName}.apk"))
.writeAndClose(apk1)
// assert that read APK bytes match what was written
assertReadEquals(
@ -173,7 +164,7 @@ class PluginTest : KoinComponent {
val suffix2 = getRandomBase64(23)
val apk2 = getRandomByteArray(23 * 1024 * 1024)
storagePlugin.getOutputStream(token, "${packageInfo2.packageName}$suffix2.apk")
backend.save(LegacyAppBackupFile.Blob(token, "${packageInfo2.packageName}$suffix2.apk"))
.writeAndClose(apk2)
// assert that read APK bytes match what was written
@ -193,26 +184,25 @@ class PluginTest : KoinComponent {
// write full backup data
val data = getRandomByteArray(5 * 1024 * 1024)
storagePlugin.getOutputStream(token, name1).writeAndClose(data)
backend.save(LegacyAppBackupFile.Blob(token, name1)).writeAndClose(data)
// restore data matches backed up data
assertReadEquals(data, storagePlugin.getInputStream(token, name1))
assertReadEquals(data, backend.load(LegacyAppBackupFile.Blob(token, name1)))
// write and check data for second package
val data2 = getRandomByteArray(5 * 1024 * 1024)
storagePlugin.getOutputStream(token, name2).writeAndClose(data2)
assertReadEquals(data2, storagePlugin.getInputStream(token, name2))
backend.save(LegacyAppBackupFile.Blob(token, name2)).writeAndClose(data2)
assertReadEquals(data2, backend.load(LegacyAppBackupFile.Blob(token, name2)))
// remove data of first package again and ensure that no more data is found
storagePlugin.removeData(token, name1)
backend.remove(LegacyAppBackupFile.Blob(token, name1))
// ensure that it gets deleted as well
storagePlugin.removeData(token, name2)
backend.remove(LegacyAppBackupFile.Blob(token, name2))
}
private fun initStorage(token: Long) = runBlocking {
every { mockedSettingsManager.getToken() } returns token
storagePlugin.initializeDevice()
}
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.BackendTest
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafConfig
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent
@ -27,15 +27,15 @@ class SafBackendTest : BackendTest(), KoinComponent {
private val settingsManager by inject<SettingsManager>()
override val plugin: Backend
get() {
val safStorage = settingsManager.getSafStorage() ?: error("No SAF storage")
val safConfig = SafConfig(
val safStorage = settingsManager.getSafProperties() ?: error("No SAF storage")
val safProperties = SafProperties(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
return SafBackend(context, safConfig, ".SeedvaultTest")
return SafBackend(context, safProperties, ".SeedvaultTest")
}
@Test

View file

@ -23,7 +23,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
confirmCode()
}
if (settingsManager.getSafStorage() == null) {
if (settingsManager.getSafProperties() == null) {
chooseStorageLocation()
} else {
changeBackupLocation()

View file

@ -9,7 +9,7 @@ import android.content.pm.PackageInfo
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.settings.AppStatus
import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every
@ -30,9 +30,9 @@ class PackageServiceTest : KoinComponent {
private val settingsManager: SettingsManager by inject()
private val storagePluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
private val backend: Backend get() = storagePluginManager.backend
private val backend: Backend get() = backendManager.backend
@Test
fun testNotAllowedPackages() {

View file

@ -24,9 +24,9 @@ import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.plugins.webdav.storagePluginModuleWebDav
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.backend.webdav.storagePluginModuleWebDav
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.restore.restoreUiModule
import com.stevesoltys.seedvault.settings.AppListRetriever
@ -42,6 +42,7 @@ import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
import com.stevesoltys.seedvault.worker.AppBackupWorker
import com.stevesoltys.seedvault.worker.workerModule
import org.calyxos.seedvault.core.backends.BackendFactory
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@ -61,7 +62,8 @@ open class App : Application() {
private val appModule = module {
single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) }
single { StoragePluginManager(this@App, get(), get(), get()) }
single { BackendManager(this@App, get(), get()) }
single { BackendFactory(this@App) }
single { BackupStateManager(this@App) }
single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
@ -72,7 +74,7 @@ open class App : Application() {
app = this@App,
settingsManager = get(),
keyManager = get(),
pluginManager = get(),
backendManager = get(),
metadataManager = get(),
appListRetriever = get(),
storageBackup = get(),
@ -91,7 +93,7 @@ open class App : Application() {
safHandler = get(),
webDavHandler = get(),
settingsManager = get(),
storagePluginManager = get(),
backendManager = get(),
)
}
viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) }
@ -146,7 +148,7 @@ open class App : Application() {
private val settingsManager: SettingsManager by inject()
private val metadataManager: MetadataManager by inject()
private val backupManager: IBackupManager by inject()
private val pluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
private val backupStateManager: BackupStateManager by inject()
/**
@ -170,13 +172,13 @@ open class App : Application() {
protected open fun migrateToOwnScheduling() {
if (!backupStateManager.isFrameworkSchedulingEnabled) { // already on own scheduling
// fix things for removable drive users who had a job scheduled here before
if (pluginManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
if (backendManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
return
}
if (backupManager.currentTransport == TRANSPORT_ID) {
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (backupManager.isBackupEnabled && !pluginManager.isOnRemovableDrive) {
if (backupManager.isBackupEnabled && !backendManager.isOnRemovableDrive) {
AppBackupWorker.schedule(applicationContext, settingsManager, UPDATE)
}
// cancel old D2D worker

View file

@ -3,11 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins
package com.stevesoltys.seedvault.backend
import android.util.Log
import at.bitfire.dav4jvm.exception.HttpException
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
suspend fun Backend.getMetadataOutputStream(token: Long): OutputStream {
@ -35,3 +38,16 @@ suspend fun Backend.getAvailableBackups(): Sequence<EncryptedMetadata>? {
null
}
}
fun Exception.isOutOfSpace(): Boolean {
return when (this) {
is IOException -> message?.contains("No space left on device") == true ||
(cause as? HttpException)?.code == 507
is HttpException -> code == 507
else -> false
}
}
class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)

View file

@ -3,30 +3,28 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins
package com.stevesoltys.seedvault.backend
import android.content.Context
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.saf.SafFactory
import com.stevesoltys.seedvault.plugins.webdav.WebDavFactory
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.StoragePluginType
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.BackendFactory
import org.calyxos.seedvault.core.backends.BackendProperties
import org.calyxos.seedvault.core.backends.saf.SafBackend
class StoragePluginManager(
class BackendManager(
private val context: Context,
private val settingsManager: SettingsManager,
safFactory: SafFactory,
webDavFactory: WebDavFactory,
backendFactory: BackendFactory,
) {
private var mBackend: Backend?
private var mFilesPlugin: org.calyxos.backup.storage.api.StoragePlugin?
private var mStorageProperties: StorageProperties<*>?
private var mBackendProperties: BackendProperties<*>?
val backend: Backend
@Synchronized
@ -34,48 +32,39 @@ class StoragePluginManager(
return mBackend ?: error("App plugin was loaded, but still null")
}
val filesPlugin: org.calyxos.backup.storage.api.StoragePlugin
val backendProperties: BackendProperties<*>?
@Synchronized
get() {
return mFilesPlugin ?: error("Files plugin was loaded, but still null")
return mBackendProperties
}
val storageProperties: StorageProperties<*>?
@Synchronized
get() {
return mStorageProperties
}
val isOnRemovableDrive: Boolean get() = storageProperties?.isUsb == true
val isOnRemovableDrive: Boolean get() = backendProperties?.isUsb == true
init {
when (settingsManager.storagePluginType) {
StoragePluginType.SAF -> {
val safStorage = settingsManager.getSafStorage() ?: error("No SAF storage saved")
mBackend = safFactory.createBackend(safStorage)
mFilesPlugin = safFactory.createFilesStoragePlugin(safStorage)
mStorageProperties = safStorage
val safConfig = settingsManager.getSafProperties() ?: error("No SAF storage saved")
mBackend = backendFactory.createSafBackend(safConfig)
mBackendProperties = safConfig
}
StoragePluginType.WEB_DAV -> {
val webDavProperties =
settingsManager.webDavProperties ?: error("No WebDAV config saved")
mBackend = webDavFactory.createBackend(webDavProperties.config)
mFilesPlugin = webDavFactory.createFilesStoragePlugin(webDavProperties.config)
mStorageProperties = webDavProperties
mBackend = backendFactory.createWebDavBackend(webDavProperties.config)
mBackendProperties = webDavProperties
}
null -> {
mBackend = null
mFilesPlugin = null
mStorageProperties = null
mBackendProperties = null
}
}
}
fun isValidAppPluginSet(): Boolean {
if (mBackend == null || mFilesPlugin == null) return false
if (mBackend == null) return false
if (mBackend is SafBackend) {
val storage = settingsManager.getSafStorage() ?: return false
val storage = settingsManager.getSafProperties() ?: return false
if (storage.isUsb) return true
return permitDiskReads {
storage.getDocumentFile(context).isDirectory
@ -85,20 +74,18 @@ class StoragePluginManager(
}
/**
* Changes the storage plugins and current [StorageProperties].
* Changes the storage plugins and current [BackendProperties].
*
* IMPORTANT: Do no call this while current plugins are being used,
* e.g. while backup/restore operation is still running.
*/
fun <T> changePlugins(
storageProperties: StorageProperties<T>,
backend: Backend,
filesPlugin: org.calyxos.backup.storage.api.StoragePlugin,
storageProperties: BackendProperties<T>,
) {
settingsManager.setStorageBackend(backend)
mStorageProperties = storageProperties
mBackend = backend
mFilesPlugin = filesPlugin
mBackendProperties = storageProperties
}
/**
@ -111,7 +98,7 @@ class StoragePluginManager(
*/
@WorkerThread
fun canDoBackupNow(): Boolean {
val storage = storageProperties ?: return false
val storage = backendProperties ?: return false
return !isOnUnavailableUsb() &&
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
}
@ -126,7 +113,7 @@ class StoragePluginManager(
*/
@WorkerThread
fun isOnUnavailableUsb(): Boolean {
val storage = storageProperties ?: return false
val storage = backendProperties ?: return false
val systemContext = context.getStorageContext { storage.isUsb }
return storage.isUnavailableUsb(systemContext)
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins
package com.stevesoltys.seedvault.backend
import android.content.pm.PackageInfo
import java.io.IOException

View file

@ -3,13 +3,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.content.Context
import android.content.pm.PackageInfo
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream

View file

@ -3,15 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.settings.SettingsManager
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val storagePluginModuleSaf = module {
single { SafFactory(androidContext()) }
single { SafHandler(androidContext(), get(), get(), get()) }
@Suppress("Deprecation")
@ -19,8 +18,9 @@ val storagePluginModuleSaf = module {
DocumentsProviderLegacyPlugin(
context = androidContext(),
storageGetter = {
val safStorage = get<SettingsManager>().getSafStorage() ?: error("No SAF storage")
DocumentsStorage(androidContext(), get(), safStorage)
val safProperties = get<SettingsManager>().getSafProperties()
?: error("No SAF storage")
DocumentsStorage(androidContext(), safProperties)
},
)
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.content.ContentResolver
import android.content.Context
@ -20,34 +20,29 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.settings.SettingsManager
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.saf.getTreeDocumentFile
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
@Deprecated("")
const val DIRECTORY_FULL_BACKUP = "full"
@Deprecated("")
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
const val FILE_BACKUP_METADATA = ".backup.metadata"
const val FILE_NO_MEDIA = ".nomedia"
const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
private val appContext: Context,
private val settingsManager: SettingsManager,
internal val safStorage: SafStorage,
internal val safStorage: SafProperties,
) {
/**

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.content.Context
import android.content.Context.USB_SERVICE
@ -14,34 +14,42 @@ import android.net.Uri
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.isMassStorage
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getAvailableBackups
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.storage.StorageOption
import org.calyxos.seedvault.core.backends.BackendFactory
import org.calyxos.seedvault.core.backends.saf.SafProperties
import java.io.IOException
private const val TAG = "SafHandler"
internal class SafHandler(
private val context: Context,
private val safFactory: SafFactory,
private val backendFactory: BackendFactory,
private val settingsManager: SettingsManager,
private val storagePluginManager: StoragePluginManager,
private val backendManager: BackendManager,
) {
fun onConfigReceived(uri: Uri, safOption: StorageOption.SafOption): SafStorage {
fun onConfigReceived(uri: Uri, safOption: StorageOption.SafOption): SafProperties {
// persist permission to access backup folder across reboots
val takeFlags = FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
val name = if (safOption.isInternal()) {
"${safOption.title} (${context.getString(R.string.settings_backup_location_internal)})"
} else {
safOption.title
}
return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork, safOption.rootId)
return SafProperties(
config = uri,
name = if (safOption.isInternal()) {
val brackets = context.getString(R.string.settings_backup_location_internal)
"${safOption.title} ($brackets)"
} else {
safOption.title
},
isUsb = safOption.isUsb,
requiresNetwork = safOption.requiresNetwork,
rootId = safOption.rootId,
)
}
/**
@ -50,16 +58,16 @@ internal class SafHandler(
*/
@WorkerThread
@Throws(IOException::class)
suspend fun hasAppBackup(safStorage: SafStorage): Boolean {
val appPlugin = safFactory.createBackend(safStorage)
suspend fun hasAppBackup(safProperties: SafProperties): Boolean {
val appPlugin = backendFactory.createSafBackend(safProperties)
val backups = appPlugin.getAvailableBackups()
return backups != null && backups.iterator().hasNext()
}
fun save(safStorage: SafStorage) {
settingsManager.setSafStorage(safStorage)
fun save(safProperties: SafProperties) {
settingsManager.setSafProperties(safProperties)
if (safStorage.isUsb) {
if (safProperties.isUsb) {
Log.d(TAG, "Selected storage is a removable USB device.")
val wasSaved = saveUsbDevice()
// reset stored flash drive, if we did not update it
@ -67,7 +75,7 @@ internal class SafHandler(
} else {
settingsManager.setFlashDrive(null)
}
Log.d(TAG, "New storage location saved: ${safStorage.uri}")
Log.d(TAG, "New storage location saved: ${safProperties.uri}")
}
private fun saveUsbDevice(): Boolean {
@ -84,11 +92,10 @@ internal class SafHandler(
return false
}
fun setPlugin(safStorage: SafStorage) {
storagePluginManager.changePlugins(
storageProperties = safStorage,
backend = safFactory.createBackend(safStorage),
filesPlugin = safFactory.createFilesStoragePlugin(safStorage),
fun setPlugin(safProperties: SafProperties) {
backendManager.changePlugins(
backend = backendFactory.createSafBackend(safProperties),
storageProperties = safProperties,
)
}
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.content.Context
import android.content.Intent
@ -14,7 +14,7 @@ import android.provider.DocumentsContract
import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver.getIcon
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver.getIcon
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_DAVX5
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_NEXTCLOUD
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_ROUND_SYNC

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.Manifest.permission.MANAGE_DOCUMENTS
import android.content.Context

View file

@ -3,20 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
package com.stevesoltys.seedvault.backend.webdav
import android.content.Context
import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getAvailableBackups
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.settings.SettingsManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.BackendFactory
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import java.io.IOException
internal sealed interface WebDavConfigState {
@ -34,9 +36,9 @@ private val TAG = WebDavHandler::class.java.simpleName
internal class WebDavHandler(
private val context: Context,
private val webDavFactory: WebDavFactory,
private val backendFactory: BackendFactory,
private val settingsManager: SettingsManager,
private val storagePluginManager: StoragePluginManager,
private val backendManager: BackendManager,
) {
companion object {
@ -54,7 +56,7 @@ internal class WebDavHandler(
suspend fun onConfigReceived(config: WebDavConfig) {
mConfigState.value = WebDavConfigState.Checking
val backend = webDavFactory.createBackend(config)
val backend = backendFactory.createWebDavBackend(config)
try {
if (backend.test()) {
val properties = createWebDavProperties(context, config)
@ -88,10 +90,9 @@ internal class WebDavHandler(
}
fun setPlugin(properties: WebDavProperties, backend: Backend) {
storagePluginManager.changePlugins(
storageProperties = properties,
backendManager.changePlugins(
backend = backend,
filesPlugin = webDavFactory.createFilesStoragePlugin(properties.config),
storageProperties = properties,
)
}

View file

@ -3,12 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
package com.stevesoltys.seedvault.backend.webdav
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val storagePluginModuleWebDav = module {
single { WebDavFactory(androidContext()) }
single { WebDavHandler(androidContext(), get(), get(), get()) }
}

View file

@ -1,83 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins
import android.app.backup.RestoreSet
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
interface StoragePlugin<T> {
/**
* Returns true if the plugin is working, or false if it isn't.
* @throws Exception any kind of exception to provide more info on the error
*/
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.
*
* This is typically followed by a call to [initializeDevice].
*/
@Throws(IOException::class)
suspend fun startNewRestoreSet(token: Long)
/**
* Initialize the storage for this device, erasing all stored data in the current [RestoreSet].
*/
@Throws(IOException::class)
suspend fun initializeDevice()
/**
* Return a raw byte stream for writing data for the given name.
*/
@Throws(IOException::class)
suspend fun getOutputStream(token: Long, name: String): OutputStream
/**
* Return a raw byte stream with data for the given name.
*/
@Throws(IOException::class)
suspend fun getInputStream(token: Long, name: String): InputStream
/**
* Remove all data associated with the given name.
*/
@Throws(IOException::class)
suspend fun removeData(token: Long, name: String)
/**
* Get the set of all backups currently available for restore.
*
* @return metadata for the set of restore images available,
* or null if an error occurred (the attempt should be rescheduled).
**/
suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>?
/**
* Returns the package name of the app that provides the backend storage
* which is used for the current backup location.
*
* Plugins are advised to cache this as it will be requested frequently.
*
* @return null if no package name could be found
*/
val providerPackageName: String?
}
class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)
internal val tokenRegex = Regex("([0-9]{13})") // good until the year 2286
internal val chunkFolderRegex = Regex("[a-f0-9]{2}")

View file

@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins
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> {
abstract val config: T
abstract val name: String
abstract val isUsb: Boolean
abstract val requiresNetwork: Boolean
@WorkerThread
abstract fun isUnavailableUsb(context: Context): Boolean
/**
* Returns true if this is storage that requires network access,
* but it isn't available right now.
*/
fun isUnavailableNetwork(context: Context, allowMetered: Boolean): Boolean {
return requiresNetwork && !hasUnmeteredInternet(context, allowMetered)
}
private fun hasUnmeteredInternet(context: Context, allowMetered: Boolean): Boolean {
val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false
val isMetered = cm.isActiveNetworkMetered
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
return capabilities.hasCapability(NET_CAPABILITY_INTERNET) && (allowMetered || !isMetered)
}
}
fun Exception.isOutOfSpace(): Boolean {
return when (this) {
is IOException -> message?.contains("No space left on device") == true ||
(cause as? HttpException)?.code == 507
is HttpException -> code == 507
else -> false
}
}

View file

@ -1,113 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import android.net.Uri
import android.util.Log
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafConfig
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
private val TAG = DocumentsProviderStoragePlugin::class.java.simpleName
internal class DocumentsProviderStoragePlugin(
appContext: Context,
safStorage: SafStorage,
root: String = DIRECTORY_ROOT,
) : StoragePlugin<Uri> {
private val safConfig = SafConfig(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
private val delegate: SafBackend = SafBackend(appContext, safConfig, root)
override suspend fun test(): Boolean {
return delegate.test()
}
override suspend fun getFreeSpace(): Long? {
return delegate.getFreeSpace()
}
@Throws(IOException::class)
override suspend fun startNewRestoreSet(token: Long) {
// no-op
}
@Throws(IOException::class)
override suspend fun initializeDevice() {
// no-op
}
@Throws(IOException::class)
override suspend fun getOutputStream(token: Long, name: String): OutputStream {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
return delegate.save(handle)
}
@Throws(IOException::class)
override suspend fun getInputStream(token: Long, name: String): InputStream {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
return delegate.load(handle)
}
@Throws(IOException::class)
override suspend fun removeData(token: Long, name: String) {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
delegate.remove(handle)
}
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
return try {
// get all restore set tokens in root folder that have a metadata file
val tokens = ArrayList<Long>()
delegate.list(null, LegacyAppBackupFile.Metadata::class) { fileInfo ->
val handle = fileInfo.fileHandle as LegacyAppBackupFile.Metadata
tokens.add(handle.token)
}
val tokenIterator = tokens.iterator()
return generateSequence {
if (!tokenIterator.hasNext()) return@generateSequence null // end sequence
val token = tokenIterator.next()
EncryptedMetadata(token) {
getInputStream(token, FILE_BACKUP_METADATA)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting available backups: ", e)
null
}
}
suspend fun removeAll() = delegate.removeAll()
override val providerPackageName: String? get() = delegate.providerPackageName
}

View file

@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import com.stevesoltys.seedvault.storage.SeedvaultSafStoragePlugin
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafBackend
class SafFactory(
private val context: Context,
) {
internal fun createBackend(safStorage: SafStorage): Backend {
return SafBackend(context, safStorage.toSafConfig())
}
internal fun createFilesStoragePlugin(
safStorage: SafStorage,
): org.calyxos.backup.storage.api.StoragePlugin {
return SeedvaultSafStoragePlugin(context, safStorage)
}
}

View file

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
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
import org.calyxos.seedvault.core.backends.saf.SafConfig
data class SafStorage(
override val config: Uri,
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
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, config)
?: throw AssertionError("Should only happen on API < 21.")
/**
* Returns true if this is USB storage that is not available, false otherwise.
*
* Must be run off UI thread (ideally I/O).
*/
@WorkerThread
override fun isUnavailableUsb(context: Context): Boolean {
return isUsb && !getDocumentFile(context).isDirectory
}
fun toSafConfig() = SafConfig(
config = config,
name = name,
isUsb = isUsb,
requiresNetwork = requiresNetwork,
rootId = rootId,
)
}

View file

@ -1,33 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
import android.annotation.SuppressLint
import android.content.Context
import android.provider.Settings
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
class WebDavFactory(
private val context: Context,
) {
fun createBackend(config: WebDavConfig): Backend = WebDavBackend(config)
fun createFilesStoragePlugin(
config: WebDavConfig,
): org.calyxos.backup.storage.api.StoragePlugin {
@SuppressLint("HardwareIds")
val androidId =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
return com.stevesoltys.seedvault.storage.WebDavStoragePlugin(
androidId = androidId,
webDavConfig = config,
)
}
}

View file

@ -1,259 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
import android.util.Log
import at.bitfire.dav4jvm.BasicDigestAuthHandler
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.PropertyRegistry
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation.SELF
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.webdav.DisplayName
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
import at.bitfire.dav4jvm.property.webdav.ResourceType
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import okhttp3.ConnectionSpec
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okio.BufferedSink
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.xmlpull.v1.XmlPullParser
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
const val DEBUG_LOG = true
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
@OptIn(DelicateCoroutinesApi::class)
internal abstract class WebDavStorage(
webDavConfig: WebDavConfig,
root: String = DIRECTORY_ROOT,
) {
companion object {
val TAG: String = WebDavStorage::class.java.simpleName
}
private val authHandler = BasicDigestAuthHandler(
domain = null, // Optional, to only authenticate against hosts with this domain.
username = webDavConfig.username,
password = webDavConfig.password,
)
protected val okHttpClient = OkHttpClient.Builder()
.followRedirects(false)
.authenticator(authHandler)
.addNetworkInterceptor(authHandler)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(240, TimeUnit.SECONDS)
.pingInterval(45, TimeUnit.SECONDS)
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
.retryOnConnectionFailure(true)
.build()
protected val baseUrl = webDavConfig.url
protected val url = "${webDavConfig.url}/$root"
init {
PropertyRegistry.register(GetLastModified.Factory)
}
@Throws(IOException::class)
protected suspend fun getOutputStream(location: HttpUrl): OutputStream {
val davCollection = DavCollection(okHttpClient, location)
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedCloseActionOutputStream(pipedInputStream)
val body = object : RequestBody() {
override fun isOneShot(): Boolean = true
override fun contentType() = "application/octet-stream".toMediaType()
override fun writeTo(sink: BufferedSink) {
pipedInputStream.use { inputStream ->
sink.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
val deferred = GlobalScope.async(Dispatchers.IO) {
davCollection.put(body) { response ->
debugLog { "getOutputStream($location) = $response" }
}
}
pipedOutputStream.doOnClose {
runBlocking { // blocking i/o wait
deferred.await()
}
}
return pipedOutputStream
}
@Throws(IOException::class)
protected fun getInputStream(location: HttpUrl): InputStream {
val davCollection = DavCollection(okHttpClient, location)
val response = davCollection.get(accept = "", headers = null)
debugLog { "getInputStream($location) = $response" }
if (response.code / 100 != 2) throw IOException("HTTP error ${response.code}")
return response.body?.byteStream() ?: throw IOException()
}
/**
* Tries to do [DavCollection.propfind] with a depth of `2` which is not in RFC4918.
* Since `infinity` isn't supported by nginx either,
* we fallback to iterating over all folders found with depth `1`
* and do another PROPFIND on those, passing the given [callback].
*/
protected fun DavCollection.propfindDepthTwo(callback: MultiResponseCallback) {
try {
propfind(
depth = 2, // this isn't defined in RFC4918
reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME),
callback = callback,
)
} catch (e: HttpException) {
if (e.isUnsupportedPropfind()) {
Log.i(TAG, "Got ${e.response}, trying two depth=1 PROPFINDs...")
propfindFakeTwo(callback)
} else {
throw e
}
}
}
private fun DavCollection.propfindFakeTwo(callback: MultiResponseCallback) {
propfind(
depth = 1,
reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME),
) { response, relation ->
debugLog { "propFindFakeTwo() = $response" }
// This callback will be called for everything in the folder
callback.onResponse(response, relation)
if (relation != SELF && response.isFolder()) {
DavCollection(okHttpClient, response.href).propfind(
depth = 1,
reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME),
callback = callback,
)
}
}
}
protected fun HttpException.isUnsupportedPropfind(): Boolean {
// nginx returns 400 for depth=2
if (code == 400) {
return true
}
// lighttpd returns 403 with <DAV:propfind-finite-depth/> error as if we used infinity
if (code == 403 && responseBody?.contains("propfind-finite-depth") == true) {
return true
}
return false
}
protected suspend fun DavCollection.createFolder(xmlBody: String? = null): okhttp3.Response {
return try {
suspendCoroutine { cont ->
mkCol(xmlBody) { response ->
cont.resume(response)
}
}
} catch (e: Exception) {
if (e is IOException) throw e
else throw IOException(e)
}
}
protected inline fun debugLog(block: () -> String) {
if (DEBUG_LOG) Log.d(TAG, block())
}
protected fun Response.isFolder(): Boolean {
return this[ResourceType::class.java]?.types?.contains(ResourceType.COLLECTION) == true
}
private class PipedCloseActionOutputStream(
inputStream: PipedInputStream,
) : PipedOutputStream(inputStream) {
private var onClose: (() -> Unit)? = null
override fun write(b: Int) {
try {
super.write(b)
} catch (e: Exception) {
try {
onClose?.invoke()
} catch (closeException: Exception) {
e.addSuppressed(closeException)
}
throw e
}
}
override fun write(b: ByteArray?, off: Int, len: Int) {
try {
super.write(b, off, len)
} catch (e: Exception) {
try {
onClose?.invoke()
} catch (closeException: Exception) {
e.addSuppressed(closeException)
}
throw e
}
}
@Throws(IOException::class)
override fun close() {
super.close()
try {
onClose?.invoke()
} catch (e: Exception) {
if (e is IOException) throw e
else throw IOException(e)
}
}
fun doOnClose(function: () -> Unit) {
this.onClose = function
}
}
}
/**
* A fake version of [at.bitfire.dav4jvm.property.webdav.GetLastModified] which we register
* so we don't need to depend on `org.apache.commons.lang3` which is used for date parsing.
*/
class GetLastModified : Property {
companion object {
@JvmField
val NAME = Property.Name(NS_WEBDAV, "getlastmodified")
}
object Factory : PropertyFactory {
override fun getName() = NAME
override fun create(parser: XmlPullParser): GetLastModified? = null
}
}

View file

@ -1,99 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
import android.util.Log
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
internal class WebDavStoragePlugin(
webDavConfig: WebDavConfig,
root: String = DIRECTORY_ROOT,
) : WebDavStorage(webDavConfig, root), StoragePlugin<WebDavConfig> {
private val delegate = WebDavBackend(webDavConfig, root)
override suspend fun test(): Boolean {
return delegate.test()
}
override suspend fun getFreeSpace(): Long? {
return delegate.getFreeSpace()
}
@Throws(IOException::class)
override suspend fun startNewRestoreSet(token: Long) {
// no-op
}
@Throws(IOException::class)
override suspend fun initializeDevice() {
// no-op
}
@Throws(IOException::class)
override suspend fun getOutputStream(token: Long, name: String): OutputStream {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
return delegate.save(handle)
}
@Throws(IOException::class)
override suspend fun getInputStream(token: Long, name: String): InputStream {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
return delegate.load(handle)
}
@Throws(IOException::class)
override suspend fun removeData(token: Long, name: String) {
val handle = when (name) {
FILE_BACKUP_METADATA -> LegacyAppBackupFile.Metadata(token)
FILE_BACKUP_ICONS -> LegacyAppBackupFile.IconsFile(token)
else -> LegacyAppBackupFile.Blob(token, name)
}
delegate.remove(handle)
}
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
return try {
// get all restore set tokens in root folder that have a metadata file
val tokens = ArrayList<Long>()
delegate.list(null, LegacyAppBackupFile.Metadata::class) { fileInfo ->
val handle = fileInfo.fileHandle as LegacyAppBackupFile.Metadata
tokens.add(handle.token)
}
val tokenIterator = tokens.iterator()
return generateSequence {
if (!tokenIterator.hasNext()) return@generateSequence null // end sequence
val token = tokenIterator.next()
EncryptedMetadata(token) {
getInputStream(token, FILE_BACKUP_METADATA)
}
}
} catch (e: Throwable) { // NoClassDefFound isn't an [Exception], can get thrown by dav4jvm
Log.e(TAG, "Error getting available backups: ", e)
null
}
}
override val providerPackageName: String? = null // 100% built-in plugin
}

View file

@ -25,7 +25,7 @@ import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
@ -56,7 +56,7 @@ internal class AppDataRestoreManager(
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val restoreCoordinator: RestoreCoordinator,
private val storagePluginManager: StoragePluginManager,
private val backendManager: BackendManager,
) {
private var session: IRestoreSession? = null
@ -101,7 +101,7 @@ internal class AppDataRestoreManager(
return
}
val providerPackageName = storagePluginManager.backend.providerPackageName
val providerPackageName = backendManager.backend.providerPackageName
val observer = RestoreObserver(
restoreCoordinator = restoreCoordinator,
restorableBackup = restorableBackup,

View file

@ -14,7 +14,7 @@ import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
import com.stevesoltys.seedvault.ui.systemData
import com.stevesoltys.seedvault.worker.IconManager
@ -37,7 +37,7 @@ private val TAG = AppSelectionManager::class.simpleName
internal class AppSelectionManager(
private val context: Context,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val iconManager: IconManager,
private val coroutineScope: CoroutineScope,
private val workDispatcher: CoroutineDispatcher = Dispatchers.IO,
@ -88,7 +88,7 @@ internal class AppSelectionManager(
SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false)
// download icons
coroutineScope.launch(workDispatcher) {
val backend = pluginManager.backend
val backend = backendManager.backend
val token = restorableBackup.token
val packagesWithIcons = try {
backend.load(LegacyAppBackupFile.IconsFile(token)).use {

View file

@ -23,7 +23,7 @@ val restoreUiModule = module {
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
backendManager = get(),
fileSelectionManager = get(),
)
}

View file

@ -18,7 +18,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
@ -65,19 +65,19 @@ internal class RestoreViewModel(
private val apkRestore: ApkRestore,
private val iconManager: IconManager,
storageBackup: StorageBackup,
pluginManager: StoragePluginManager,
backendManager: BackendManager,
override val fileSelectionManager: FileSelectionManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager),
RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true
var isSetupWizard = false
private val appSelectionManager =
AppSelectionManager(app, pluginManager, iconManager, viewModelScope)
AppSelectionManager(app, backendManager, iconManager, viewModelScope)
private val appDataRestoreManager = AppDataRestoreManager(
app, backupManager, settingsManager, restoreCoordinator, pluginManager
app, backupManager, settingsManager, restoreCoordinator, backendManager
)
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()

View file

@ -17,9 +17,8 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.RestoreService
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
@ -34,6 +33,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.File
import java.io.IOException
@ -45,7 +45,7 @@ internal class ApkRestore(
private val context: Context,
private val backupManager: IBackupManager,
private val backupStateManager: BackupStateManager,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin,
private val crypto: Crypto,
@ -55,7 +55,7 @@ internal class ApkRestore(
) {
private val pm = context.packageManager
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private val mInstallResult = MutableStateFlow(InstallResult())
val installResult = mInstallResult.asStateFlow()
@ -237,7 +237,7 @@ internal class ApkRestore(
}
/**
* Retrieves APK splits from [StoragePlugin] and caches them locally.
* Retrieves APK splits from [Backend] and caches them locally.
*
* @throws SecurityException if a split has an unexpected SHA-256 hash.
* @return a list of all APKs that need to be installed
@ -275,7 +275,7 @@ internal class ApkRestore(
}
/**
* Retrieves an APK from the [StoragePlugin] and caches it locally
* Retrieves an APK from the [Backend] and caches it locally
* while calculating its SHA-256 hash.
*
* @return a [Pair] of the cached [File] and SHA-256 hash.

View file

@ -17,7 +17,7 @@ import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.settings.preference.M3ListPreference
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -27,7 +27,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
private val viewModel: SettingsViewModel by sharedViewModel()
private val settingsManager: SettingsManager by inject()
private val storagePluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {
@ -39,7 +39,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val storage = storagePluginManager.storageProperties
val storage = backendManager.backendProperties
if (storage?.isUsb == true) {
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
}

View file

@ -25,12 +25,12 @@ import androidx.work.WorkInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.StorageProperties
import com.stevesoltys.seedvault.restore.RestoreActivity
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.toRelativeTime
import org.calyxos.seedvault.core.backends.BackendProperties
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import java.util.concurrent.TimeUnit
@ -40,7 +40,7 @@ private val TAG = SettingsFragment::class.java.name
class SettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel()
private val storagePluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
private val backupStateManager: BackupStateManager by inject()
private val backupManager: IBackupManager by inject()
private val notificationManager: BackupNotificationManager by inject()
@ -57,8 +57,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var menuBackupNow: MenuItem? = null
private var menuRestore: MenuItem? = null
private val storageProperties: StorageProperties<*>?
get() = storagePluginManager.storageProperties
private val backendProperties: BackendProperties<*>?
get() = backendManager.backendProperties
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {
@ -270,7 +270,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
activity?.contentResolver?.let {
autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled
}
val storage = this.storageProperties
val storage = this.backendProperties
if (storage?.isUsb == true) {
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
getString(R.string.settings_auto_restore_summary_usb, storage.name)
@ -282,7 +282,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setBackupLocationSummary() {
// get name of storage location
backupLocation.summary =
storageProperties?.name ?: getString(R.string.settings_backup_location_none)
backendProperties?.name ?: getString(R.string.settings_backup_location_none)
}
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
@ -301,7 +301,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
* says that nothing is scheduled which can happen when backup destination is on flash drive.
*/
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
if (storageProperties?.isUsb == true) {
if (backendProperties?.isUsb == true) {
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
return
}

View file

@ -11,15 +11,15 @@ import android.hardware.usb.UsbDevice
import android.net.Uri
import androidx.annotation.UiThread
import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler.Companion.createWebDavProperties
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler.Companion.createWebDavProperties
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import java.util.concurrent.ConcurrentSkipListSet
internal const val PREF_KEY_TOKEN = "token"
@ -139,17 +139,17 @@ class SettingsManager(private val context: Context) {
.apply()
}
fun setSafStorage(safStorage: SafStorage) {
fun setSafProperties(safProperties: SafProperties) {
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)
.putString(PREF_KEY_STORAGE_URI, safProperties.uri.toString())
.putString(PREF_KEY_STORAGE_ROOT_ID, safProperties.rootId)
.putString(PREF_KEY_STORAGE_NAME, safProperties.name)
.putBoolean(PREF_KEY_STORAGE_IS_USB, safProperties.isUsb)
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safProperties.requiresNetwork)
.apply()
}
fun getSafStorage(): SafStorage? {
fun getSafProperties(): SafProperties? {
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
val uri = Uri.parse(uriStr)
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null)
@ -157,7 +157,7 @@ class SettingsManager(private val context: Context) {
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false)
val rootId = prefs.getString(PREF_KEY_STORAGE_ROOT_ID, null)
return SafStorage(uri, name, isUsb, requiresNetwork, rootId)
return SafProperties(uri, name, isUsb, requiresNetwork, rootId)
}
fun setFlashDrive(usb: FlashDrive?) {

View file

@ -40,8 +40,7 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
@ -59,6 +58,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.seedvault.core.backends.saf.SafProperties
import java.io.IOException
import java.lang.Runtime.getRuntime
import java.util.concurrent.TimeUnit.HOURS
@ -70,14 +70,14 @@ internal class SettingsViewModel(
app: Application,
settingsManager: SettingsManager,
keyManager: KeyManager,
pluginManager: StoragePluginManager,
backendManager: BackendManager,
private val metadataManager: MetadataManager,
private val appListRetriever: AppListRetriever,
private val storageBackup: StorageBackup,
private val backupManager: IBackupManager,
private val backupInitializer: BackupInitializer,
backupStateManager: BackupStateManager,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager) {
private val contentResolver = app.contentResolver
private val connectivityManager: ConnectivityManager? =
@ -158,7 +158,7 @@ internal class SettingsViewModel(
}
override fun onStorageLocationChanged() {
val storage = pluginManager.storageProperties ?: return
val storage = backendManager.backendProperties ?: return
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb})")
if (storage.isUsb) {
@ -177,33 +177,33 @@ internal class SettingsViewModel(
private fun onBackupRunningStateChanged() {
if (isBackupRunning.value) mBackupPossible.postValue(false)
else viewModelScope.launch(Dispatchers.IO) {
val canDo = !isBackupRunning.value && !pluginManager.isOnUnavailableUsb()
val canDo = !isBackupRunning.value && !backendManager.isOnUnavailableUsb()
mBackupPossible.postValue(canDo)
}
}
private fun onStoragePropertiesChanged() {
val storage = pluginManager.storageProperties ?: return
val properties = backendManager.backendProperties ?: return
Log.d(TAG, "onStoragePropertiesChanged")
if (storage is SafStorage) {
if (properties is SafProperties) {
// register storage observer
try {
contentResolver.unregisterContentObserver(storageObserver)
contentResolver.registerContentObserver(storage.uri, false, storageObserver)
contentResolver.registerContentObserver(properties.uri, false, storageObserver)
} catch (e: SecurityException) {
// This can happen if the app providing the storage was uninstalled.
// validLocationIsSet() gets called elsewhere
// and prompts for a new storage location.
Log.e(TAG, "Error registering content observer for ${storage.uri}", e)
Log.e(TAG, "Error registering content observer for ${properties.uri}", e)
}
}
// register network observer if needed
if (networkCallback.registered && !storage.requiresNetwork) {
if (networkCallback.registered && !properties.requiresNetwork) {
connectivityManager?.unregisterNetworkCallback(networkCallback)
networkCallback.registered = false
} else if (!networkCallback.registered && storage.requiresNetwork) {
} else if (!networkCallback.registered && properties.requiresNetwork) {
// TODO we may want to warn the user when they start a backup on a metered connection
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
@ -232,7 +232,7 @@ internal class SettingsViewModel(
i.putExtra(EXTRA_START_APP_BACKUP, isAppBackupEnabled)
startForegroundService(app, i)
} else if (isAppBackupEnabled) {
AppBackupWorker.scheduleNow(app, reschedule = !pluginManager.isOnRemovableDrive)
AppBackupWorker.scheduleNow(app, reschedule = !backendManager.isOnRemovableDrive)
}
}
}
@ -313,14 +313,14 @@ internal class SettingsViewModel(
fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) {
// disable framework scheduling, because another transport may have enabled it
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (!pluginManager.isOnRemovableDrive && backupManager.isBackupEnabled) {
if (!backendManager.isOnRemovableDrive && backupManager.isBackupEnabled) {
AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy)
}
}
fun scheduleFilesBackup() {
if (!pluginManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) {
val requiresNetwork = pluginManager.storageProperties?.requiresNetwork == true
if (!backendManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) {
val requiresNetwork = backendManager.backendProperties?.requiresNetwork == true
BackupJobService.scheduleJob(
context = app,
jobServiceClass = StorageBackupJobService::class.java,

View file

@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.storage
import android.content.Context
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafConfig
internal class SeedvaultSafStoragePlugin(
appContext: Context,
safStorage: SafStorage,
root: String = DIRECTORY_ROOT,
) : SafStoragePlugin(appContext) {
private val safConfig = SafConfig(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
override val delegate: SafBackend = SafBackend(appContext, safConfig, root)
}

View file

@ -6,7 +6,7 @@
package com.stevesoltys.seedvault.storage
import android.content.Intent
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.worker.AppBackupWorker
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -44,7 +44,7 @@ internal class StorageBackupService : BackupService() {
}
override val storageBackup: StorageBackup by inject()
private val storagePluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
// use lazy delegate because context isn't available during construction time
override val backupObserver: BackupObserver by lazy {
@ -63,7 +63,7 @@ internal class StorageBackupService : BackupService() {
override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false
val isUsb = backendManager.backendProperties?.isUsb ?: false
AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb)
}
}

View file

@ -6,10 +6,10 @@
package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import org.calyxos.backup.storage.api.StorageBackup
import org.koin.dsl.module
val storageModule = module {
single { StorageBackup(get(), { get<StoragePluginManager>().backend }, get<KeyManager>()) }
single { StorageBackup(get(), { get<BackendManager>().backend }, get<KeyManager>()) }
}

View file

@ -1,118 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.plugins.webdav.DIRECTORY_ROOT
import org.calyxos.backup.storage.api.StoragePlugin
import org.calyxos.backup.storage.api.StoredSnapshot
import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.backends.TopLevelFolder
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
internal class WebDavStoragePlugin(
/**
* The result of Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
*/
private val androidId: String,
webDavConfig: WebDavConfig,
root: String = DIRECTORY_ROOT,
) : StoragePlugin {
private val topLevelFolder = TopLevelFolder("$androidId.sv")
private val delegate = WebDavBackend(webDavConfig, root)
@Throws(IOException::class)
override suspend fun init() {
// no-op
}
@Throws(IOException::class)
override suspend fun getAvailableChunkIds(): List<String> {
val chunkIds = ArrayList<String>()
delegate.list(topLevelFolder, FileBackupFileType.Blob::class) { fileInfo ->
chunkIds.add(fileInfo.fileHandle.name)
}
return chunkIds
}
@Throws(IOException::class)
override suspend fun getChunkOutputStream(chunkId: String): OutputStream {
val fileHandle = FileBackupFileType.Blob(androidId, chunkId)
return delegate.save(fileHandle)
}
@Throws(IOException::class)
override suspend fun getBackupSnapshotOutputStream(timestamp: Long): OutputStream {
val fileHandle = FileBackupFileType.Snapshot(androidId, timestamp)
return delegate.save(fileHandle)
}
/************************* Restore *******************************/
@Throws(IOException::class)
override suspend fun getBackupSnapshotsForRestore(): List<StoredSnapshot> {
val snapshots = ArrayList<StoredSnapshot>()
delegate.list(null, FileBackupFileType.Snapshot::class) { fileInfo ->
val handle = fileInfo.fileHandle as FileBackupFileType.Snapshot
val folderName = handle.topLevelFolder.name
val timestamp = handle.time
val storedSnapshot = StoredSnapshot(folderName, timestamp)
snapshots.add(storedSnapshot)
}
return snapshots
}
@Throws(IOException::class)
override suspend fun getBackupSnapshotInputStream(storedSnapshot: StoredSnapshot): InputStream {
val androidId = storedSnapshot.androidId
val handle = FileBackupFileType.Snapshot(androidId, storedSnapshot.timestamp)
return delegate.load(handle)
}
@Throws(IOException::class)
override suspend fun getChunkInputStream(
snapshot: StoredSnapshot,
chunkId: String,
): InputStream {
val handle = FileBackupFileType.Blob(snapshot.androidId, chunkId)
return delegate.load(handle)
}
/************************* Pruning *******************************/
@Throws(IOException::class)
override suspend fun getCurrentBackupSnapshots(): List<StoredSnapshot> {
val snapshots = ArrayList<StoredSnapshot>()
delegate.list(topLevelFolder, FileBackupFileType.Snapshot::class) { fileInfo ->
val handle = fileInfo.fileHandle as FileBackupFileType.Snapshot
val folderName = handle.topLevelFolder.name
val timestamp = handle.time
val storedSnapshot = StoredSnapshot(folderName, timestamp)
snapshots.add(storedSnapshot)
}
return snapshots
}
@Throws(IOException::class)
override suspend fun deleteBackupSnapshot(storedSnapshot: StoredSnapshot) {
val androidId = storedSnapshot.androidId
val handle = FileBackupFileType.Snapshot(androidId, storedSnapshot.timestamp)
delegate.remove(handle)
}
@Throws(IOException::class)
override suspend fun deleteChunks(chunkIds: List<String>) {
chunkIds.forEach { chunkId ->
val androidId = topLevelFolder.name.substringBefore(".sv")
val handle = FileBackupFileType.Blob(androidId, chunkId)
delegate.remove(handle)
}
}
}

View file

@ -29,15 +29,12 @@ import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream
import com.stevesoltys.seedvault.plugins.isOutOfSpace
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getMetadataOutputStream
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException
import java.io.OutputStream
import java.util.concurrent.TimeUnit.DAYS
import java.util.concurrent.TimeUnit.HOURS
@ -65,7 +62,7 @@ private class CoordinatorState(
@WorkerThread
internal class BackupCoordinator(
private val context: Context,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val kv: KVBackup,
private val full: FullBackup,
private val clock: Clock,
@ -75,7 +72,7 @@ internal class BackupCoordinator(
private val nm: BackupNotificationManager,
) {
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private val state = CoordinatorState(
calledInitialize = false,
calledClearBackupData = false,
@ -132,7 +129,7 @@ internal class BackupCoordinator(
} catch (e: Exception) {
Log.e(TAG, "Error initializing device", e)
// Show error notification if we needed init or were ready for backups
if (metadataManager.requiresInit || pluginManager.canDoBackupNow()) nm.onBackupError()
if (metadataManager.requiresInit || backendManager.canDoBackupNow()) nm.onBackupError()
TRANSPORT_ERROR
}
@ -370,7 +367,7 @@ internal class BackupCoordinator(
if (result == TRANSPORT_OK) {
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now
if (isNormalBackup || pluginManager.canDoBackupNow()) {
if (isNormalBackup || backendManager.canDoBackupNow()) {
try {
onPackageBackedUp(packageInfo, BackupType.KV, size)
} catch (e: Exception) {
@ -431,7 +428,7 @@ internal class BackupCoordinator(
val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set
val storage = pluginManager.storageProperties ?: return longBackoff
val storage = backendManager.backendProperties ?: return longBackoff
return when {
// back off if storage is removable and not available right now
storage.isUnavailableUsb(context) -> longBackoff
@ -444,12 +441,4 @@ internal class BackupCoordinator(
else -> 0L
}
}
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
token: Long? = null,
): OutputStream {
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
return getOutputStream(t, FILE_BACKUP_METADATA)
}
}

View file

@ -16,13 +16,13 @@ val backupModule = module {
context = androidContext(),
backupManager = get(),
settingsManager = get(),
pluginManager = get(),
backendManager = get(),
)
}
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single {
KVBackup(
pluginManager = get(),
backendManager = get(),
settingsManager = get(),
nm = get(),
inputFactory = get(),
@ -32,7 +32,7 @@ val backupModule = module {
}
single {
FullBackup(
pluginManager = get(),
backendManager = get(),
settingsManager = get(),
nm = get(),
inputFactory = get(),
@ -42,7 +42,7 @@ val backupModule = module {
single {
BackupCoordinator(
context = androidContext(),
pluginManager = get(),
backendManager = get(),
kv = get(),
full = get(),
clock = get(),

View file

@ -16,8 +16,8 @@ import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.isOutOfSpace
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
@ -47,14 +47,14 @@ private val TAG = FullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup(
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager,
private val inputFactory: InputFactory,
private val crypto: Crypto,
) {
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private var state: FullBackupState? = null
fun hasState() = state != null

View file

@ -18,8 +18,8 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
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.backend.BackendManager
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
@ -40,7 +40,7 @@ const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
private val TAG = KVBackup::class.java.simpleName
internal class KVBackup(
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager,
private val inputFactory: InputFactory,
@ -48,7 +48,7 @@ internal class KVBackup(
private val dbManager: KvDbManager,
) {
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private var state: KVBackupState? = null
fun hasState() = state != null
@ -147,7 +147,7 @@ internal class KVBackup(
// K/V backups (typically starting with package manager metadata - @pm@)
// are scheduled with JobInfo.Builder#setOverrideDeadline()
// and thus do not respect backoff.
pluginManager.canDoBackupNow()
backendManager.canDoBackupNow()
} else {
// all other packages always need upload
true

View file

@ -27,7 +27,7 @@ import android.util.Log
import android.util.Log.INFO
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.settings.SettingsManager
import org.calyxos.seedvault.core.backends.Backend
@ -43,12 +43,12 @@ internal class PackageService(
private val context: Context,
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
) {
private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId()
private val backend: Backend get() = pluginManager.backend
private val backend: Backend get() = backendManager.backend
val eligiblePackages: List<String>
@WorkerThread

View file

@ -17,8 +17,8 @@ import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import libcore.io.IoUtils.closeQuietly
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.EOFException
@ -39,7 +39,7 @@ private class FullRestoreState(
private val TAG = FullRestore::class.java.simpleName
internal class FullRestore(
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
@ -47,7 +47,7 @@ internal class FullRestore(
private val crypto: Crypto,
) {
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private var state: FullRestoreState? = null
fun hasState() = state != null

View file

@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.KVDb
import com.stevesoltys.seedvault.transport.backup.KvDbManager
import libcore.io.IoUtils.closeQuietly
@ -45,7 +45,7 @@ private class KVRestoreState(
private val TAG = KVRestore::class.java.simpleName
internal class KVRestore(
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
@ -54,7 +54,7 @@ internal class KVRestore(
private val dbManager: KvDbManager,
) {
private val backend get() = pluginManager.backend
private val backend get() = backendManager.backend
private var state: KVRestoreState? = null
/**

View file

@ -25,8 +25,8 @@ import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getAvailableBackups
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
@ -62,13 +62,13 @@ internal class RestoreCoordinator(
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val kv: KVRestore,
private val full: FullRestore,
private val metadataReader: MetadataReader,
) {
private val backend: Backend get() = pluginManager.backend
private val backend: Backend get() = backendManager.backend
private var state: RestoreCoordinatorState? = null
private var backupMetadata: BackupMetadata? = null
private val failedPackages = ArrayList<String>()
@ -176,7 +176,7 @@ internal class RestoreCoordinator(
// check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
// remind user to plug in storage device
val storageName = pluginManager.storageProperties?.name
val storageName = backendManager.backendProperties?.name
?: context.getString(R.string.settings_backup_location_none)
notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName,
@ -359,7 +359,7 @@ internal class RestoreCoordinator(
fun isFailedPackage(packageName: String) = packageName in failedPackages
private fun isStorageRemovableAndNotAvailable(): Boolean {
val storage = pluginManager.storageProperties ?: return false
val storage = backendManager.backendProperties ?: return false
return storage.isUnavailableUsb(context)
}

View file

@ -8,14 +8,14 @@ package com.stevesoltys.seedvault.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.settings.SettingsManager
abstract class RequireProvisioningViewModel(
protected val app: Application,
protected val settingsManager: SettingsManager,
protected val keyManager: KeyManager,
protected val pluginManager: StoragePluginManager,
protected val backendManager: BackendManager,
) : AndroidViewModel(app) {
abstract val isRestoreOperation: Boolean
@ -24,7 +24,7 @@ abstract class RequireProvisioningViewModel(
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
internal fun validLocationIsSet() = pluginManager.isValidAppPluginSet()
internal fun validLocationIsSet() = backendManager.isValidAppPluginSet()
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()

View file

@ -13,11 +13,10 @@ import android.util.Log
import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.SafHandler
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.saf.SafHandler
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
@ -27,6 +26,7 @@ import kotlinx.coroutines.launch
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import java.io.IOException
import java.util.concurrent.TimeUnit
@ -40,15 +40,15 @@ internal class BackupStorageViewModel(
safHandler: SafHandler,
webDavHandler: WebDavHandler,
settingsManager: SettingsManager,
storagePluginManager: StoragePluginManager,
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
backendManager: BackendManager,
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, backendManager) {
override val isRestoreOperation = false
override fun onSafUriSet(safStorage: SafStorage) {
safHandler.save(safStorage)
safHandler.setPlugin(safStorage)
if (safStorage.isUsb) {
override fun onSafUriSet(safProperties: SafProperties) {
safHandler.save(safProperties)
safHandler.setPlugin(safProperties)
if (safProperties.isUsb) {
// disable storage backup if new storage is on USB
cancelBackupWorkers()
} else {
@ -56,7 +56,7 @@ internal class BackupStorageViewModel(
// also to update the network requirement of the new storage
scheduleBackupWorkers()
}
onStorageLocationSet(safStorage.isUsb)
onStorageLocationSet(safProperties.isUsb)
}
override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) {
@ -100,7 +100,7 @@ internal class BackupStorageViewModel(
}
private fun scheduleBackupWorkers() {
val storage = storagePluginManager.storageProperties ?: error("no storage available")
val storage = backendManager.backendProperties ?: error("no storage available")
// disable framework scheduling, because another transport may have enabled it
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (!storage.isUsb) {

View file

@ -9,16 +9,16 @@ import android.app.Application
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
import com.stevesoltys.seedvault.plugins.saf.SafHandler
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.saf.SafHandler
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
import com.stevesoltys.seedvault.settings.SettingsManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import java.io.IOException
private val TAG = RestoreStorageViewModel::class.java.simpleName
@ -28,25 +28,25 @@ internal class RestoreStorageViewModel(
safHandler: SafHandler,
webDavHandler: WebDavHandler,
settingsManager: SettingsManager,
storagePluginManager: StoragePluginManager,
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
backendManager: BackendManager,
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, backendManager) {
override val isRestoreOperation = true
override fun onSafUriSet(safStorage: SafStorage) {
override fun onSafUriSet(safProperties: SafProperties) {
viewModelScope.launch(Dispatchers.IO) {
val hasBackup = try {
safHandler.hasAppBackup(safStorage)
safHandler.hasAppBackup(safProperties)
} catch (e: IOException) {
Log.e(TAG, "Error reading URI: ${safStorage.uri}", e)
Log.e(TAG, "Error reading URI: ${safProperties.uri}", e)
false
}
if (hasBackup) {
safHandler.save(safStorage)
safHandler.setPlugin(safStorage)
safHandler.save(safProperties)
safHandler.setPlugin(safProperties)
mLocationChecked.postEvent(LocationResult())
} else {
Log.w(TAG, "Location was rejected: ${safStorage.uri}")
Log.w(TAG, "Location was rejected: ${safProperties.uri}")
// notify the UI that the location was invalid
val errorMsg =

View file

@ -18,7 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTre
import androidx.annotation.CallSuper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver
import com.stevesoltys.seedvault.ui.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_SETUP_WIZARD

View file

@ -18,8 +18,8 @@ import android.provider.DocumentsContract.PROVIDER_INTERFACE
import android.provider.DocumentsContract.buildRootsUri
import android.util.Log
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.saf.SafStorageOptions
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver
import com.stevesoltys.seedvault.backend.saf.SafStorageOptions
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
private val TAG = StorageOptionFetcher::class.java.simpleName

View file

@ -13,11 +13,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.SafHandler
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.saf.SafHandler
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
@ -25,6 +24,7 @@ import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
internal abstract class StorageViewModel(
@ -32,7 +32,7 @@ internal abstract class StorageViewModel(
protected val safHandler: SafHandler,
protected val webdavHandler: WebDavHandler,
protected val settingsManager: SettingsManager,
protected val storagePluginManager: StoragePluginManager,
protected val backendManager: BackendManager,
) : AndroidViewModel(app), RemovableStorageListener {
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
@ -49,7 +49,7 @@ internal abstract class StorageViewModel(
internal var isSetupWizard: Boolean = false
internal val hasStorageSet: Boolean
get() = storagePluginManager.storageProperties != null
get() = backendManager.backendProperties != null
abstract val isRestoreOperation: Boolean
internal fun loadStorageRoots() {
@ -88,7 +88,7 @@ internal abstract class StorageViewModel(
onSafUriSet(safStorage)
}
abstract fun onSafUriSet(safStorage: SafStorage)
abstract fun onSafUriSet(safProperties: SafProperties)
abstract fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend)
override fun onCleared() {

View file

@ -22,7 +22,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import com.google.android.material.textfield.TextInputEditText
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.webdav.WebDavConfigState
import com.stevesoltys.seedvault.backend.webdav.WebDavConfigState
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.getSharedViewModel

View file

@ -11,11 +11,9 @@ import android.util.Log
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream
import com.stevesoltys.seedvault.plugins.isOutOfSpace
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.getMetadataOutputStream
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.isStopped
@ -24,7 +22,6 @@ import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.delay
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.IOException
import java.io.OutputStream
internal class ApkBackupManager(
private val context: Context,
@ -33,7 +30,7 @@ internal class ApkBackupManager(
private val packageService: PackageService,
private val iconManager: IconManager,
private val apkBackup: ApkBackup,
private val pluginManager: StoragePluginManager,
private val backendManager: BackendManager,
private val nm: BackupNotificationManager,
) {
@ -58,7 +55,7 @@ internal class ApkBackupManager(
// upload all local changes only at the end,
// so we don't have to re-upload the metadata
val token = settingsManager.getToken() ?: error("no token")
pluginManager.backend.getMetadataOutputStream(token).use { outputStream ->
backendManager.backend.getMetadataOutputStream(token).use { outputStream ->
metadataManager.uploadMetadata(outputStream)
}
}
@ -105,7 +102,7 @@ internal class ApkBackupManager(
try {
val token = settingsManager.getToken() ?: throw IOException("no current token")
val handle = LegacyAppBackupFile.IconsFile(token)
pluginManager.backend.save(handle).use {
backendManager.backend.save(handle).use {
iconManager.uploadIcons(token, it)
}
} catch (e: IOException) {
@ -123,7 +120,7 @@ internal class ApkBackupManager(
return try {
apkBackup.backupApkIfNecessary(packageInfo) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token")
pluginManager.backend.save(LegacyAppBackupFile.Blob(token, name))
backendManager.backend.save(LegacyAppBackupFile.Blob(token, name))
}?.let { packageMetadata ->
metadataManager.onApkBackedUp(packageInfo, packageMetadata)
true
@ -147,11 +144,4 @@ internal class ApkBackupManager(
}
}
}
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
token: Long? = null,
): OutputStream {
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
return getOutputStream(t, FILE_BACKUP_METADATA)
}
}

View file

@ -22,7 +22,7 @@ import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
@ -101,7 +101,7 @@ class AppBackupWorker(
private val backupRequester: BackupRequester by inject()
private val settingsManager: SettingsManager by inject()
private val apkBackupManager: ApkBackupManager by inject()
private val storagePluginManager: StoragePluginManager by inject()
private val backendManager: BackendManager by inject()
private val nm: BackupNotificationManager by inject()
override suspend fun doWork(): Result {
@ -111,7 +111,7 @@ class AppBackupWorker(
} catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e)
}
val freeSpace = storagePluginManager.getFreeSpace()
val freeSpace = backendManager.getFreeSpace()
if (freeSpace != null && freeSpace < MIN_FREE_SPACE) {
nm.onInsufficientSpaceError()
return Result.failure()

View file

@ -39,7 +39,7 @@ val workerModule = module {
packageService = get(),
apkBackup = get(),
iconManager = get(),
pluginManager = get(),
backendManager = get(),
nm = get()
)
}

View file

@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.saf
package com.stevesoltys.seedvault.backend.saf
import android.content.Context
import android.content.pm.PackageManager

View file

@ -1,86 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.TestApp
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.transport.TransportTest
import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.assertThrows
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(
sdk = [34], // TODO: Drop once robolectric supports 35
application = TestApp::class
)
internal class WebDavStoragePluginTest : TransportTest() {
private val plugin = WebDavStoragePlugin(WebDavTestConfig.getConfig())
@Test
fun `test self-test`() = runBlocking {
assertTrue(plugin.test())
val plugin2 = WebDavStoragePlugin(WebDavConfig("https://github.com/", "", ""))
val e = assertThrows<Exception> {
assertFalse(plugin2.test())
}
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()
val metadata = getRandomByteArray()
// need to initialize, to have root .SeedVaultAndroidBackup folder
plugin.initializeDevice()
plugin.startNewRestoreSet(token)
// initially, we don't have any backups
assertEquals(emptySet<EncryptedMetadata>(), plugin.getAvailableBackups()?.toSet())
// write out the metadata file
plugin.getOutputStream(token, FILE_BACKUP_METADATA).use {
it.write(metadata)
}
try {
// now we have one backup matching our token
val backups = plugin.getAvailableBackups()?.toSet() ?: fail()
assertEquals(1, backups.size)
assertEquals(token, backups.first().token)
// read back written data
assertArrayEquals(
metadata,
plugin.getInputStream(token, FILE_BACKUP_METADATA).use { it.readAllBytes() },
)
} finally {
// remove data at the end, so consecutive test runs pass
plugin.removeData(token, FILE_BACKUP_METADATA)
}
}
}

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.junit.Assume.assumeFalse
import org.junit.jupiter.api.Assertions.fail
object WebDavTestConfig {
fun getConfig(): WebDavConfig {
assumeFalse(System.getenv("NEXTCLOUD_URL").isNullOrEmpty())
return WebDavConfig(
url = System.getenv("NEXTCLOUD_URL") ?: fail(),
username = System.getenv("NEXTCLOUD_USER") ?: fail(),
password = System.getenv("NEXTCLOUD_PASS") ?: fail(),
)
}
}

View file

@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_CONTACTS
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SETTINGS
@ -47,7 +47,7 @@ import kotlin.random.Random
)
internal class AppSelectionManagerTest : TransportTest() {
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val iconManager: IconManager = mockk()
private val testDispatcher = UnconfinedTestDispatcher()
private val scope = TestScope(testDispatcher)
@ -63,7 +63,7 @@ internal class AppSelectionManagerTest : TransportTest() {
private val appSelectionManager = AppSelectionManager(
context = context,
pluginManager = storagePluginManager,
backendManager = backendManager,
iconManager = iconManager,
coroutineScope = scope,
workDispatcher = testDispatcher,
@ -222,7 +222,7 @@ internal class AppSelectionManagerTest : TransportTest() {
@Test
fun `test icon loading fails`() = scope.runTest {
val backend: Backend = mockk()
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
coEvery {
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
} throws IOException()
@ -429,7 +429,7 @@ internal class AppSelectionManagerTest : TransportTest() {
private fun expectIconLoading(icons: Set<String> = setOf(packageName1, packageName2)) {
val backend: Backend = mockk()
val inputStream = ByteArrayInputStream(Random.nextBytes(42))
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
coEvery {
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
} returns inputStream

View file

@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
@ -60,7 +60,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
every { packageManager } returns pm
}
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk()
@ -76,7 +76,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
context = strictContext,
backupManager = backupManager,
backupStateManager = backupStateManager,
pluginManager = storagePluginManager,
backendManager = backendManager,
legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto,
splitCompatChecker = splitCompatChecker,
@ -112,7 +112,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
init {
mockkStatic(PackageUtils::class)
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -24,8 +24,8 @@ import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
@ -67,7 +67,7 @@ internal class ApkRestoreTest : TransportTest() {
}
private val backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend: Backend = mockk()
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
@ -78,7 +78,7 @@ internal class ApkRestoreTest : TransportTest() {
context = strictContext,
backupManager = backupManager,
backupStateManager = backupStateManager,
pluginManager = storagePluginManager,
backendManager = backendManager,
legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto,
splitCompatChecker = splitCompatChecker,
@ -109,7 +109,7 @@ internal class ApkRestoreTest : TransportTest() {
// as we don't do strict signature checking, we can use a relaxed mock
packageInfo.signingInfo = mockk(relaxed = true)
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
// related to starting/stopping service
every { strictContext.packageName } returns "org.foo.bar"

View file

@ -1,108 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.plugins.webdav.WebDavTestConfig
import com.stevesoltys.seedvault.transport.backup.BackupTest
import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.api.StoredSnapshot
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
internal class WebDavStoragePluginTest : BackupTest() {
private val androidId = "abcdef0123456789"
private val plugin = WebDavStoragePlugin(androidId, WebDavTestConfig.getConfig())
private val snapshot = StoredSnapshot("$androidId.sv", System.currentTimeMillis())
@Test
fun `test chunks`() = runBlocking {
val chunkId1 = getRandomByteArray(32).toHexString()
val chunkBytes1 = getRandomByteArray()
// init to create root folder
plugin.init()
// first we don't have any chunks
assertEquals(emptyList<String>(), plugin.getAvailableChunkIds())
// we write out chunk1
plugin.getChunkOutputStream(chunkId1).use {
it.write(chunkBytes1)
}
try {
// now we have the ID of chunk1
assertEquals(listOf(chunkId1), plugin.getAvailableChunkIds())
// reading chunk1 matches what we wrote
assertArrayEquals(
chunkBytes1,
plugin.getChunkInputStream(snapshot, chunkId1).readAllBytes(),
)
} finally {
// delete chunk again
plugin.deleteChunks(listOf(chunkId1))
}
}
@Test
fun `test snapshots`() = runBlocking {
val snapshotBytes = getRandomByteArray()
// init to create root folder
plugin.init()
// first we don't have any snapshots
assertEquals(emptyList<StoredSnapshot>(), plugin.getCurrentBackupSnapshots())
assertEquals(emptyList<StoredSnapshot>(), plugin.getBackupSnapshotsForRestore())
// now write one snapshot
plugin.getBackupSnapshotOutputStream(snapshot.timestamp).use {
it.write(snapshotBytes)
}
try {
// now we have that one snapshot
assertEquals(listOf(snapshot), plugin.getCurrentBackupSnapshots())
assertEquals(listOf(snapshot), plugin.getBackupSnapshotsForRestore())
// read back written snapshot
assertArrayEquals(
snapshotBytes,
plugin.getBackupSnapshotInputStream(snapshot).readAllBytes(),
)
// other device writes another snapshot
val androidId2 = "0123456789abcdef"
val otherPlugin = WebDavStoragePlugin(androidId2, WebDavTestConfig.getConfig())
val otherSnapshot = StoredSnapshot("$androidId2.sv", System.currentTimeMillis())
val otherSnapshotBytes = getRandomByteArray()
assertEquals(emptyList<String>(), otherPlugin.getAvailableChunkIds())
otherPlugin.getBackupSnapshotOutputStream(otherSnapshot.timestamp).use {
it.write(otherSnapshotBytes)
}
try {
// now that initial one snapshot is still the only current, but restore has both
assertEquals(listOf(snapshot), plugin.getCurrentBackupSnapshots())
assertEquals(
setOf(snapshot, otherSnapshot),
plugin.getBackupSnapshotsForRestore().toSet(), // set to avoid sorting issues
)
} finally {
plugin.deleteBackupSnapshot(otherSnapshot)
}
} finally {
plugin.deleteBackupSnapshot(snapshot)
}
}
}
private fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

View file

@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory
@ -63,13 +63,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()
private val dbManager = TestKvDbManager()
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backend = mockk<Backend>()
private val kvBackup = KVBackup(
pluginManager = storagePluginManager,
backendManager = backendManager,
settingsManager = settingsManager,
nm = notificationManager,
inputFactory = inputFactory,
@ -77,7 +77,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
dbManager = dbManager,
)
private val fullBackup = FullBackup(
pluginManager = storagePluginManager,
backendManager = backendManager,
settingsManager = settingsManager,
nm = notificationManager,
inputFactory = inputFactory,
@ -87,7 +87,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val packageService: PackageService = mockk()
private val backup = BackupCoordinator(
context,
storagePluginManager,
backendManager,
kvBackup,
fullBackup,
clock,
@ -98,7 +98,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
)
private val kvRestore = KVRestore(
storagePluginManager,
backendManager,
legacyPlugin,
outputFactory,
headerReader,
@ -106,14 +106,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
dbManager
)
private val fullRestore =
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(
context,
crypto,
settingsManager,
metadataManager,
notificationManager,
storagePluginManager,
backendManager,
kvRestore,
fullRestore,
metadataReader
@ -132,7 +132,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
init {
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -20,8 +20,7 @@ import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.ApkBackup
import io.mockk.Runs
@ -33,6 +32,7 @@ import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.io.IOException
@ -41,7 +41,7 @@ import kotlin.random.Random
internal class BackupCoordinatorTest : BackupTest() {
private val pluginManager = mockk<StoragePluginManager>()
private val backendManager = mockk<BackendManager>()
private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>()
@ -50,7 +50,7 @@ internal class BackupCoordinatorTest : BackupTest() {
private val backup = BackupCoordinator(
context = context,
pluginManager = pluginManager,
backendManager = backendManager,
kv = kv,
full = full,
clock = clock,
@ -64,7 +64,7 @@ internal class BackupCoordinatorTest : BackupTest() {
private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk()
private val packageMetadata: PackageMetadata = mockk()
private val safStorage = SafStorage(
private val safProperties = SafProperties(
config = Uri.EMPTY,
name = getRandomString(),
isUsb = false,
@ -73,7 +73,7 @@ internal class BackupCoordinatorTest : BackupTest() {
)
init {
every { pluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test
@ -100,7 +100,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { settingsManager.setNewToken(token) } just Runs
every { metadataManager.onDeviceInitialization(token) } throws IOException()
every { metadataManager.requiresInit } returns maybeTrue
every { pluginManager.canDoBackupNow() } returns !maybeTrue
every { backendManager.canDoBackupNow() } returns !maybeTrue
every { notificationManager.onBackupError() } just Runs
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -120,7 +120,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { settingsManager.setNewToken(token) } just Runs
every { metadataManager.onDeviceInitialization(token) } throws IOException()
every { metadataManager.requiresInit } returns false
every { pluginManager.canDoBackupNow() } returns false
every { backendManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -136,7 +136,7 @@ internal class BackupCoordinatorTest : BackupTest() {
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
every { pluginManager.canDoBackupNow() } returns true
every { backendManager.canDoBackupNow() } returns true
every { metadataManager.requiresInit } returns true
// start new restore set
@ -234,7 +234,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { kv.getCurrentSize() } returns 42L
coEvery { kv.finishBackup() } returns TRANSPORT_OK
every { pluginManager.canDoBackupNow() } returns false
every { backendManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_OK, backup.finishBackup())
}
@ -300,7 +300,7 @@ internal class BackupCoordinatorTest : BackupTest() {
)
} just Runs
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
every { pluginManager.storageProperties } returns safStorage
every { backendManager.backendProperties } returns safProperties
every { settingsManager.useMeteredNetwork } returns false
every { metadataOutputStream.close() } just Runs
@ -350,7 +350,7 @@ internal class BackupCoordinatorTest : BackupTest() {
)
} just Runs
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
every { pluginManager.storageProperties } returns safStorage
every { backendManager.backendProperties } returns safProperties
every { settingsManager.useMeteredNetwork } returns false
every { metadataOutputStream.close() } just Runs

View file

@ -11,7 +11,7 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs
import io.mockk.coEvery
@ -31,11 +31,11 @@ import kotlin.random.Random
internal class FullBackupTest : BackupTest() {
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>()
private val notificationManager = mockk<BackupNotificationManager>()
private val backup = FullBackup(
pluginManager = storagePluginManager,
backendManager = backendManager,
settingsManager = settingsManager,
nm = notificationManager,
inputFactory = inputFactory,
@ -47,7 +47,7 @@ internal class FullBackupTest : BackupTest() {
private val ad = getADForFull(VERSION, packageInfo.packageName)
init {
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -17,7 +17,7 @@ import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.CapturingSlot
import io.mockk.Runs
@ -39,13 +39,13 @@ import kotlin.random.Random
internal class KVBackupTest : BackupTest() {
private val pluginManager = mockk<StoragePluginManager>()
private val backendManager = mockk<BackendManager>()
private val notificationManager = mockk<BackupNotificationManager>()
private val dataInput = mockk<BackupDataInput>()
private val dbManager = mockk<KvDbManager>()
private val backup = KVBackup(
pluginManager = pluginManager,
backendManager = backendManager,
settingsManager = settingsManager,
nm = notificationManager,
inputFactory = inputFactory,
@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() {
private val inputStream = ByteArrayInputStream(dbBytes)
init {
every { pluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test
@ -250,7 +250,7 @@ internal class KVBackupTest : BackupTest() {
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
every { dbManager.getDb(pmPackageInfo.packageName) } returns db
every { pluginManager.canDoBackupNow() } returns false
every { backendManager.canDoBackupNow() } returns false
every { db.put(key, dataValue) } just Runs
getDataInput(listOf(true, false))

View file

@ -16,8 +16,8 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import io.mockk.CapturingSlot
import io.mockk.Runs
import io.mockk.coEvery
@ -39,11 +39,11 @@ import kotlin.random.Random
internal class FullRestoreTest : RestoreTest() {
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>()
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val restore = FullRestore(
pluginManager = storagePluginManager,
backendManager = backendManager,
legacyPlugin = legacyPlugin,
outputFactory = outputFactory,
headerReader = headerReader,
@ -55,7 +55,7 @@ internal class FullRestoreTest : RestoreTest() {
private val ad = getADForFull(VERSION, packageInfo.packageName)
init {
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -15,8 +15,8 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.KVDb
import com.stevesoltys.seedvault.transport.backup.KvDbManager
import io.mockk.Runs
@ -41,14 +41,14 @@ import kotlin.random.Random
internal class KVRestoreTest : RestoreTest() {
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>()
@Suppress("DEPRECATION")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val dbManager = mockk<KvDbManager>()
private val output = mockk<BackupDataOutput>()
private val restore = KVRestore(
pluginManager = storagePluginManager,
backendManager = backendManager,
legacyPlugin = legacyPlugin,
outputFactory = outputFactory,
headerReader = headerReader,
@ -74,7 +74,7 @@ internal class KVRestoreTest : RestoreTest() {
// for InputStream#readBytes()
mockkStatic("kotlin.io.ByteStreamsKt")
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -13,16 +13,15 @@ import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.EncryptedMetadata
import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.getAvailableBackups
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs
@ -34,6 +33,7 @@ import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows
@ -46,7 +46,7 @@ import kotlin.random.Random
internal class RestoreCoordinatorTest : TransportTest() {
private val notificationManager: BackupNotificationManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>()
private val kv = mockk<KVRestore>()
private val full = mockk<FullRestore>()
@ -58,14 +58,14 @@ internal class RestoreCoordinatorTest : TransportTest() {
settingsManager = settingsManager,
metadataManager = metadataManager,
notificationManager = notificationManager,
pluginManager = storagePluginManager,
backendManager = backendManager,
kv = kv,
full = full,
metadataReader = metadataReader,
)
private val inputStream = mockk<InputStream>()
private val safStorage: SafStorage = mockk()
private val safStorage: SafProperties = mockk()
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
private val packageInfoArray = arrayOf(packageInfo)
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
@ -80,8 +80,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
metadata.packageMetadataMap[packageInfo2.packageName] =
PackageMetadata(backupType = BackupType.FULL)
mockkStatic("com.stevesoltys.seedvault.plugins.BackendExtKt")
every { storagePluginManager.backend } returns backend
mockkStatic("com.stevesoltys.seedvault.backend.BackendExtKt")
every { backendManager.backend } returns backend
}
@Test
@ -175,7 +175,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() optimized auto-restore with removed storage shows notification`() =
runBlocking {
every { storagePluginManager.storageProperties } returns safStorage
every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns true
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
every { safStorage.name } returns storageName
@ -199,7 +199,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() optimized auto-restore with available storage shows no notification`() =
runBlocking {
every { storagePluginManager.storageProperties } returns safStorage
every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns false
restore.beforeStartRestore(metadata)
@ -215,7 +215,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() with removed storage shows no notification`() = runBlocking {
every { storagePluginManager.storageProperties } returns safStorage
every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns true
every { metadataManager.getPackageMetadata(packageName) } returns null

View file

@ -18,9 +18,8 @@ import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.toByteArrayFromHex
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.KvDbManager
@ -55,13 +54,13 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val dbManager = mockk<KvDbManager>()
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backend = mockk<Backend>()
private val kvRestore = KVRestore(
pluginManager = storagePluginManager,
backendManager = backendManager,
legacyPlugin = legacyPlugin,
outputFactory = outputFactory,
headerReader = headerReader,
@ -69,14 +68,14 @@ internal class RestoreV0IntegrationTest : TransportTest() {
dbManager = dbManager,
)
private val fullRestore =
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(
context = context,
crypto = crypto,
settingsManager = settingsManager,
metadataManager = metadataManager,
notificationManager = notificationManager,
pluginManager = storagePluginManager,
backendManager = backendManager,
kv = kvRestore,
full = fullRestore,
metadataReader = metadataReader,
@ -124,7 +123,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val key264 = key2.encodeBase64()
init {
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -14,7 +14,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -40,7 +40,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageService: PackageService = mockk()
private val apkBackup: ApkBackup = mockk()
private val iconManager: IconManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk()
private val backendManager: BackendManager = mockk()
private val backend: Backend = mockk()
private val nm: BackupNotificationManager = mockk()
@ -51,7 +51,7 @@ internal class ApkBackupManagerTest : TransportTest() {
packageService = packageService,
apkBackup = apkBackup,
iconManager = iconManager,
pluginManager = storagePluginManager,
backendManager = backendManager,
nm = nm,
)
@ -59,7 +59,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageMetadata: PackageMetadata = mockk()
init {
every { storagePluginManager.backend } returns backend
every { backendManager.backend } returns backend
}
@Test

View file

@ -5,6 +5,7 @@
package org.calyxos.seedvault.core
internal fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
public fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
internal fun String.toByteArrayFromHex() = chunked(2).map { it.toInt(16).toByte() }.toByteArray()
public fun String.toByteArrayFromHex(): ByteArray =
chunked(2).map { it.toInt(16).toByte() }.toByteArray()

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package org.calyxos.seedvault.core.backends
import android.content.Context
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
public class BackendFactory(
private val context: Context,
) {
public fun createSafBackend(config: SafProperties): Backend = SafBackend(context, config)
public fun createWebDavBackend(config: WebDavConfig): Backend = WebDavBackend(config)
}

View file

@ -20,6 +20,12 @@ public sealed class FileHandle {
public data class TopLevelFolder(override val name: String) : FileHandle() {
override val relativePath: String = name
public companion object {
public fun fromAndroidId(androidId: String): TopLevelFolder {
return TopLevelFolder("$androidId.sv")
}
}
}
public sealed class LegacyAppBackupFile : FileHandle() {

View file

@ -39,7 +39,7 @@ internal const val ROOT_ID_DEVICE = "primary"
public class SafBackend(
private val appContext: Context,
private val safConfig: SafConfig,
private val safProperties: SafProperties,
root: String = DIRECTORY_ROOT,
) : Backend {
@ -48,16 +48,16 @@ public class SafBackend(
/**
* Attention: This context might be from a different user. Use with care.
*/
private val context: Context get() = appContext.getBackendContext { safConfig.isUsb }
private val cache = DocumentFileCache(context, safConfig.getDocumentFile(context), root)
private val context: Context get() = appContext.getBackendContext { safProperties.isUsb }
private val cache = DocumentFileCache(context, safProperties.getDocumentFile(context), root)
override suspend fun test(): Boolean {
return cache.getRootFile().isDirectory
}
override suspend fun getFreeSpace(): Long? {
val rootId = safConfig.rootId ?: return null
val authority = safConfig.uri.authority
val rootId = safProperties.rootId ?: return null
val authority = safProperties.uri.authority
// using DocumentsContract#buildRootUri(String, String) with rootId directly doesn't work
val rootUri = DocumentsContract.buildRootsUri(authority)
val projection = arrayOf(COLUMN_AVAILABLE_BYTES)
@ -74,8 +74,8 @@ public class SafBackend(
return if (bytesAvailable == null && authority == AUTHORITY_STORAGE) {
if (rootId == ROOT_ID_DEVICE) {
StatFs(Environment.getDataDirectory().absolutePath).availableBytes
} else if (safConfig.isUsb) {
val documentId = safConfig.uri.lastPathSegment ?: return null
} else if (safProperties.isUsb) {
val documentId = safProperties.uri.lastPathSegment ?: return null
StatFs("/mnt/media_rw/${documentId.trimEnd(':')}").availableBytes
} else null
} else bytesAvailable
@ -185,7 +185,7 @@ public class SafBackend(
}
override val providerPackageName: String? by lazy {
val authority = safConfig.uri.authority ?: return@lazy null
val authority = safProperties.uri.authority ?: return@lazy null
val providerInfo = context.packageManager.resolveContentProvider(authority, 0)
?: return@lazy null
providerInfo.packageName

View file

@ -12,7 +12,7 @@ import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import org.calyxos.seedvault.core.backends.BackendProperties
public data class SafConfig(
public data class SafProperties(
override val config: Uri,
override val name: String,
override val isUsb: Boolean,
@ -24,7 +24,7 @@ public data class SafConfig(
val rootId: String?,
) : BackendProperties<Uri>() {
internal val uri: Uri = config
public val uri: Uri = config
public fun getDocumentFile(context: Context): DocumentFile =
DocumentFile.fromTreeUri(context, config)

View file

@ -3,16 +3,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.plugins.webdav
package org.calyxos.seedvault.core.backends.webdav
import android.content.Context
import com.stevesoltys.seedvault.plugins.StorageProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.calyxos.seedvault.core.backends.BackendProperties
data class WebDavProperties(
public data class WebDavProperties(
override val config: WebDavConfig,
override val name: String,
) : StorageProperties<WebDavConfig>() {
) : BackendProperties<WebDavConfig>() {
override val isUsb: Boolean = false
override val requiresNetwork: Boolean = true
override fun isUnavailableUsb(context: Context): Boolean = false

View file

@ -12,7 +12,7 @@ import org.calyxos.seedvault.core.backends.FileHandle
import org.calyxos.seedvault.core.backends.FileInfo
import org.calyxos.seedvault.core.backends.TopLevelFolder
import org.calyxos.seedvault.core.backends.saf.SafBackend
import org.calyxos.seedvault.core.backends.saf.SafConfig
import org.calyxos.seedvault.core.backends.saf.SafProperties
import java.io.InputStream
import java.io.OutputStream
import kotlin.reflect.KClass
@ -22,15 +22,15 @@ class TestSafBackend(
private val getLocationUri: () -> Uri?,
) : Backend {
private val safConfig
get() = SafConfig(
private val safProperties
get() = SafProperties(
config = getLocationUri() ?: error("no uri"),
name = "foo",
isUsb = false,
requiresNetwork = false,
rootId = "bar",
)
private val delegate: SafBackend get() = SafBackend(appContext, safConfig)
private val delegate: SafBackend get() = SafBackend(appContext, safProperties)
private val nullStream = object : OutputStream() {
override fun write(b: Int) {

View file

@ -1,10 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package org.calyxos.backup.storage
internal fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
internal fun String.toByteArrayFromHex() = chunked(2).map { it.toInt(16).toByte() }.toByteArray()

View file

@ -5,50 +5,14 @@
package org.calyxos.backup.storage
import android.content.ContentResolver
import android.net.Uri
import android.provider.MediaStore
import org.calyxos.backup.storage.api.MediaType
import org.calyxos.backup.storage.api.mediaItems
import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.db.StoredUri
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
internal fun Uri.toStoredUri(): StoredUri = StoredUri(this)
internal fun Uri.getDocumentPath(): String? {
return lastPathSegment?.split(':')?.getOrNull(1)
}
internal fun Uri.getVolume(): String? {
val volume = lastPathSegment?.split(':')?.getOrNull(0)
return if (volume == "primary") MediaStore.VOLUME_EXTERNAL_PRIMARY else volume
}
@Throws(IOException::class)
public fun Uri.openInputStream(contentResolver: ContentResolver): InputStream {
return try {
contentResolver.openInputStream(this)
} catch (e: IllegalArgumentException) {
// This is necessary, because contrary to the documentation, files that have been deleted
// after we retrieved their Uri, will throw an IllegalArgumentException
throw IOException(e)
} ?: throw IOException("Stream for $this returned null")
}
@Throws(IOException::class)
public fun Uri.openOutputStream(contentResolver: ContentResolver): OutputStream {
return try {
contentResolver.openOutputStream(this, "wt")
} catch (e: IllegalArgumentException) {
// This is necessary, because contrary to the documentation, files that have been deleted
// after we retrieved their Uri, will throw an IllegalArgumentException
throw IOException(e)
} ?: throw IOException("Stream for $this returned null")
}
internal fun Uri.getMediaType(): MediaType? {
val str = toString()
for (item in mediaItems) {

View file

@ -11,7 +11,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.calyxos.backup.storage.R
import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.getDocumentPath
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
// hidden in DocumentsContract
public const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY: String =
@ -38,7 +38,7 @@ public sealed class BackupContentType(
public object Custom : BackupContentType(R.drawable.ic_folder) {
public fun getName(uri: Uri): String {
val path = uri.getDocumentPath()!!
return if (path.isBlank()) "/" else path
return path.ifBlank { "/" }
}
}
}

View file

@ -25,7 +25,6 @@ import org.calyxos.backup.storage.backup.BackupSnapshot
import org.calyxos.backup.storage.backup.ChunksCacheRepopulater
import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.backup.storage.getDocumentPath
import org.calyxos.backup.storage.getMediaType
import org.calyxos.backup.storage.prune.Pruner
import org.calyxos.backup.storage.prune.RetentionManager
@ -37,6 +36,7 @@ import org.calyxos.backup.storage.scanner.MediaScanner
import org.calyxos.backup.storage.toStoredUri
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
import org.calyxos.seedvault.core.crypto.KeyManager
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean

View file

@ -6,7 +6,7 @@
package org.calyxos.backup.storage.backup
import org.calyxos.backup.storage.db.CachedChunk
import org.calyxos.backup.storage.toHexString
import org.calyxos.seedvault.core.toHexString
import java.io.IOException
import java.io.InputStream
import javax.crypto.Mac

View file

@ -13,7 +13,7 @@ import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.content.MediaFile
import org.calyxos.backup.storage.db.CachedFile
import org.calyxos.backup.storage.db.FilesCache
import org.calyxos.backup.storage.openInputStream
import org.calyxos.seedvault.core.backends.saf.openInputStream
import java.io.IOException
import java.security.GeneralSecurityException

View file

@ -13,11 +13,10 @@ import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.content.MediaFile
import org.calyxos.backup.storage.db.CachedFile
import org.calyxos.backup.storage.db.FilesCache
import org.calyxos.backup.storage.openInputStream
import org.calyxos.seedvault.core.backends.saf.openInputStream
import java.io.IOException
import java.security.GeneralSecurityException
@Suppress("BlockingMethodInNonBlockingContext")
internal class SmallFileBackup(
private val contentResolver: ContentResolver,
private val filesCache: FilesCache,

View file

@ -7,7 +7,7 @@ package org.calyxos.backup.storage.backup
import org.calyxos.backup.storage.content.ContentFile
import org.calyxos.backup.storage.db.CachedChunk
import org.calyxos.backup.storage.toHexString
import org.calyxos.seedvault.core.toHexString
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream

View file

@ -9,7 +9,7 @@ import com.google.crypto.tink.subtle.AesGcmHkdfStreaming
import org.calyxos.backup.storage.backup.Backup.Companion.VERSION
import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC
import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES
import org.calyxos.backup.storage.toByteArrayFromHex
import org.calyxos.seedvault.core.toByteArrayFromHex
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream

View file

@ -15,8 +15,8 @@ import android.util.Log
import org.calyxos.backup.storage.api.MediaType
import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.openOutputStream
import org.calyxos.backup.storage.scanner.MediaScanner
import org.calyxos.seedvault.core.backends.saf.openOutputStream
import java.io.File
import java.io.IOException
import java.io.OutputStream

View file

@ -14,8 +14,8 @@ import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import org.calyxos.backup.storage.api.BackupFile
import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.getDocumentPath
import org.calyxos.backup.storage.getVolume
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
import org.calyxos.seedvault.core.backends.saf.getVolume
public class DocumentScanner(context: Context) {

View file

@ -18,9 +18,9 @@ import org.calyxos.backup.storage.crypto.StreamCrypto
import org.calyxos.backup.storage.db.ChunksCache
import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog
import org.calyxos.backup.storage.toHexString
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test

View file

@ -22,8 +22,8 @@ import org.calyxos.backup.storage.db.FilesCache
import org.calyxos.backup.storage.getRandomDocFile
import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog
import org.calyxos.backup.storage.toHexString
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.ByteArrayInputStream

View file

@ -13,7 +13,7 @@ import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.getRandomDocFile
import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.toHexString
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue