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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest 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.AppStatus
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import io.mockk.every import io.mockk.every
@ -30,9 +30,9 @@ class PackageServiceTest : KoinComponent {
private val settingsManager: SettingsManager by inject() 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 @Test
fun testNotAllowedPackages() { fun testNotAllowedPackages() {

View file

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

View file

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

View file

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

View file

@ -3,13 +3,13 @@
* SPDX-License-Identifier: Apache-2.0 * 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
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * 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
import android.content.Intent 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_DISPLAY_NAME
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
import com.stevesoltys.seedvault.R 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_DAVX5
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_NEXTCLOUD import com.stevesoltys.seedvault.ui.storage.AUTHORITY_NEXTCLOUD
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_ROUND_SYNC import com.stevesoltys.seedvault.ui.storage.AUTHORITY_ROUND_SYNC

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * 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.Manifest.permission.MANAGE_DOCUMENTS
import android.content.Context import android.content.Context

View file

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

View file

@ -3,12 +3,11 @@
* SPDX-License-Identifier: Apache-2.0 * 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.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val storagePluginModuleWebDav = module { val storagePluginModuleWebDav = module {
single { WebDavFactory(androidContext()) }
single { WebDavHandler(androidContext(), get(), get(), get()) } 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.R
import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.metadata.PackageState 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.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.TRANSPORT_ID
@ -56,7 +56,7 @@ internal class AppDataRestoreManager(
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val restoreCoordinator: RestoreCoordinator, private val restoreCoordinator: RestoreCoordinator,
private val storagePluginManager: StoragePluginManager, private val backendManager: BackendManager,
) { ) {
private var session: IRestoreSession? = null private var session: IRestoreSession? = null
@ -101,7 +101,7 @@ internal class AppDataRestoreManager(
return return
} }
val providerPackageName = storagePluginManager.backend.providerPackageName val providerPackageName = backendManager.backend.providerPackageName
val observer = RestoreObserver( val observer = RestoreObserver(
restoreCoordinator = restoreCoordinator, restoreCoordinator = restoreCoordinator,
restorableBackup = restorableBackup, 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.R
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap 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.PACKAGE_NAME_SYSTEM
import com.stevesoltys.seedvault.ui.systemData import com.stevesoltys.seedvault.ui.systemData
import com.stevesoltys.seedvault.worker.IconManager import com.stevesoltys.seedvault.worker.IconManager
@ -37,7 +37,7 @@ private val TAG = AppSelectionManager::class.simpleName
internal class AppSelectionManager( internal class AppSelectionManager(
private val context: Context, private val context: Context,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val iconManager: IconManager, private val iconManager: IconManager,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val workDispatcher: CoroutineDispatcher = Dispatchers.IO, private val workDispatcher: CoroutineDispatcher = Dispatchers.IO,
@ -88,7 +88,7 @@ internal class AppSelectionManager(
SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false) SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false)
// download icons // download icons
coroutineScope.launch(workDispatcher) { coroutineScope.launch(workDispatcher) {
val backend = pluginManager.backend val backend = backendManager.backend
val token = restorableBackup.token val token = restorableBackup.token
val packagesWithIcons = try { val packagesWithIcons = try {
backend.load(LegacyAppBackupFile.IconsFile(token)).use { backend.load(LegacyAppBackupFile.IconsFile(token)).use {

View file

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

View file

@ -18,7 +18,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager 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_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
@ -65,19 +65,19 @@ internal class RestoreViewModel(
private val apkRestore: ApkRestore, private val apkRestore: ApkRestore,
private val iconManager: IconManager, private val iconManager: IconManager,
storageBackup: StorageBackup, storageBackup: StorageBackup,
pluginManager: StoragePluginManager, backendManager: BackendManager,
override val fileSelectionManager: FileSelectionManager, override val fileSelectionManager: FileSelectionManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager), ) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager),
RestorableBackupClickListener, SnapshotViewModel { RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true override val isRestoreOperation = true
var isSetupWizard = false var isSetupWizard = false
private val appSelectionManager = private val appSelectionManager =
AppSelectionManager(app, pluginManager, iconManager, viewModelScope) AppSelectionManager(app, backendManager, iconManager, viewModelScope)
private val appDataRestoreManager = AppDataRestoreManager( private val appDataRestoreManager = AppDataRestoreManager(
app, backupManager, settingsManager, restoreCoordinator, pluginManager app, backupManager, settingsManager, restoreCoordinator, backendManager
) )
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>() 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.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.RestorableBackup import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.RestoreService import com.stevesoltys.seedvault.restore.RestoreService
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -45,7 +45,7 @@ internal class ApkRestore(
private val context: Context, private val context: Context,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupStateManager: BackupStateManager, private val backupStateManager: BackupStateManager,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin, private val legacyStoragePlugin: LegacyStoragePlugin,
private val crypto: Crypto, private val crypto: Crypto,
@ -55,7 +55,7 @@ internal class ApkRestore(
) { ) {
private val pm = context.packageManager private val pm = context.packageManager
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private val mInstallResult = MutableStateFlow(InstallResult()) private val mInstallResult = MutableStateFlow(InstallResult())
val installResult = mInstallResult.asStateFlow() 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. * @throws SecurityException if a split has an unexpected SHA-256 hash.
* @return a list of all APKs that need to be installed * @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. * while calculating its SHA-256 hash.
* *
* @return a [Pair] of the cached [File] and 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 androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads 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 com.stevesoltys.seedvault.settings.preference.M3ListPreference
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -27,7 +27,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
private val viewModel: SettingsViewModel by sharedViewModel() private val viewModel: SettingsViewModel by sharedViewModel()
private val settingsManager: SettingsManager by inject() 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads { permitDiskReads {
@ -39,7 +39,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val storage = storagePluginManager.storageProperties val storage = backendManager.backendProperties
if (storage?.isUsb == true) { if (storage?.isUsb == true) {
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false 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.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.BackupStateManager import com.stevesoltys.seedvault.BackupStateManager
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.permitDiskReads 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.restore.RestoreActivity
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.toRelativeTime import com.stevesoltys.seedvault.ui.toRelativeTime
import org.calyxos.seedvault.core.backends.BackendProperties
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -40,7 +40,7 @@ private val TAG = SettingsFragment::class.java.name
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel() 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 backupStateManager: BackupStateManager by inject()
private val backupManager: IBackupManager by inject() private val backupManager: IBackupManager by inject()
private val notificationManager: BackupNotificationManager by inject() private val notificationManager: BackupNotificationManager by inject()
@ -57,8 +57,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var menuBackupNow: MenuItem? = null private var menuBackupNow: MenuItem? = null
private var menuRestore: MenuItem? = null private var menuRestore: MenuItem? = null
private val storageProperties: StorageProperties<*>? private val backendProperties: BackendProperties<*>?
get() = storagePluginManager.storageProperties get() = backendManager.backendProperties
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads { permitDiskReads {
@ -270,7 +270,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
activity?.contentResolver?.let { activity?.contentResolver?.let {
autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled
} }
val storage = this.storageProperties val storage = this.backendProperties
if (storage?.isUsb == true) { if (storage?.isUsb == true) {
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" + autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
getString(R.string.settings_auto_restore_summary_usb, storage.name) getString(R.string.settings_auto_restore_summary_usb, storage.name)
@ -282,7 +282,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setBackupLocationSummary() { private fun setBackupLocationSummary() {
// get name of storage location // get name of storage location
backupLocation.summary = 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?) { 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. * says that nothing is scheduled which can happen when backup destination is on flash drive.
*/ */
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) { private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
if (storageProperties?.isUsb == true) { if (backendProperties?.isUsb == true) {
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb) backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
return return
} }

View file

@ -11,15 +11,15 @@ import android.hardware.usb.UsbDevice
import android.net.Uri import android.net.Uri
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler.Companion.createWebDavProperties
import com.stevesoltys.seedvault.permitDiskReads 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 com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafBackend 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.WebDavBackend
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.ConcurrentSkipListSet
internal const val PREF_KEY_TOKEN = "token" internal const val PREF_KEY_TOKEN = "token"
@ -139,17 +139,17 @@ class SettingsManager(private val context: Context) {
.apply() .apply()
} }
fun setSafStorage(safStorage: SafStorage) { fun setSafProperties(safProperties: SafProperties) {
prefs.edit() prefs.edit()
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString()) .putString(PREF_KEY_STORAGE_URI, safProperties.uri.toString())
.putString(PREF_KEY_STORAGE_ROOT_ID, safStorage.rootId) .putString(PREF_KEY_STORAGE_ROOT_ID, safProperties.rootId)
.putString(PREF_KEY_STORAGE_NAME, safStorage.name) .putString(PREF_KEY_STORAGE_NAME, safProperties.name)
.putBoolean(PREF_KEY_STORAGE_IS_USB, safStorage.isUsb) .putBoolean(PREF_KEY_STORAGE_IS_USB, safProperties.isUsb)
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safStorage.requiresNetwork) .putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safProperties.requiresNetwork)
.apply() .apply()
} }
fun getSafStorage(): SafStorage? { fun getSafProperties(): SafProperties? {
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
val uri = Uri.parse(uriStr) val uri = Uri.parse(uriStr)
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) 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 isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false) val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false)
val rootId = prefs.getString(PREF_KEY_STORAGE_ROOT_ID, null) 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?) { 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.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.storage.StorageBackupService import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
@ -59,6 +58,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService import org.calyxos.backup.storage.backup.BackupJobService
import org.calyxos.seedvault.core.backends.saf.SafProperties
import java.io.IOException import java.io.IOException
import java.lang.Runtime.getRuntime import java.lang.Runtime.getRuntime
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
@ -70,14 +70,14 @@ internal class SettingsViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsManager: SettingsManager,
keyManager: KeyManager, keyManager: KeyManager,
pluginManager: StoragePluginManager, backendManager: BackendManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val appListRetriever: AppListRetriever, private val appListRetriever: AppListRetriever,
private val storageBackup: StorageBackup, private val storageBackup: StorageBackup,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupInitializer: BackupInitializer, private val backupInitializer: BackupInitializer,
backupStateManager: BackupStateManager, backupStateManager: BackupStateManager,
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) { ) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager) {
private val contentResolver = app.contentResolver private val contentResolver = app.contentResolver
private val connectivityManager: ConnectivityManager? = private val connectivityManager: ConnectivityManager? =
@ -158,7 +158,7 @@ internal class SettingsViewModel(
} }
override fun onStorageLocationChanged() { override fun onStorageLocationChanged() {
val storage = pluginManager.storageProperties ?: return val storage = backendManager.backendProperties ?: return
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb})") Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb})")
if (storage.isUsb) { if (storage.isUsb) {
@ -177,33 +177,33 @@ internal class SettingsViewModel(
private fun onBackupRunningStateChanged() { private fun onBackupRunningStateChanged() {
if (isBackupRunning.value) mBackupPossible.postValue(false) if (isBackupRunning.value) mBackupPossible.postValue(false)
else viewModelScope.launch(Dispatchers.IO) { else viewModelScope.launch(Dispatchers.IO) {
val canDo = !isBackupRunning.value && !pluginManager.isOnUnavailableUsb() val canDo = !isBackupRunning.value && !backendManager.isOnUnavailableUsb()
mBackupPossible.postValue(canDo) mBackupPossible.postValue(canDo)
} }
} }
private fun onStoragePropertiesChanged() { private fun onStoragePropertiesChanged() {
val storage = pluginManager.storageProperties ?: return val properties = backendManager.backendProperties ?: return
Log.d(TAG, "onStoragePropertiesChanged") Log.d(TAG, "onStoragePropertiesChanged")
if (storage is SafStorage) { if (properties is SafProperties) {
// register storage observer // register storage observer
try { try {
contentResolver.unregisterContentObserver(storageObserver) contentResolver.unregisterContentObserver(storageObserver)
contentResolver.registerContentObserver(storage.uri, false, storageObserver) contentResolver.registerContentObserver(properties.uri, false, storageObserver)
} catch (e: SecurityException) { } catch (e: SecurityException) {
// This can happen if the app providing the storage was uninstalled. // This can happen if the app providing the storage was uninstalled.
// validLocationIsSet() gets called elsewhere // validLocationIsSet() gets called elsewhere
// and prompts for a new storage location. // 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 // register network observer if needed
if (networkCallback.registered && !storage.requiresNetwork) { if (networkCallback.registered && !properties.requiresNetwork) {
connectivityManager?.unregisterNetworkCallback(networkCallback) connectivityManager?.unregisterNetworkCallback(networkCallback)
networkCallback.registered = false 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 // TODO we may want to warn the user when they start a backup on a metered connection
val request = NetworkRequest.Builder() val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
@ -232,7 +232,7 @@ internal class SettingsViewModel(
i.putExtra(EXTRA_START_APP_BACKUP, isAppBackupEnabled) i.putExtra(EXTRA_START_APP_BACKUP, isAppBackupEnabled)
startForegroundService(app, i) startForegroundService(app, i)
} else if (isAppBackupEnabled) { } 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) { fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) {
// disable framework scheduling, because another transport may have enabled it // disable framework scheduling, because another transport may have enabled it
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false) backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (!pluginManager.isOnRemovableDrive && backupManager.isBackupEnabled) { if (!backendManager.isOnRemovableDrive && backupManager.isBackupEnabled) {
AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy) AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy)
} }
} }
fun scheduleFilesBackup() { fun scheduleFilesBackup() {
if (!pluginManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) { if (!backendManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) {
val requiresNetwork = pluginManager.storageProperties?.requiresNetwork == true val requiresNetwork = backendManager.backendProperties?.requiresNetwork == true
BackupJobService.scheduleJob( BackupJobService.scheduleJob(
context = app, context = app,
jobServiceClass = StorageBackupJobService::class.java, 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 package com.stevesoltys.seedvault.storage
import android.content.Intent import android.content.Intent
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.worker.AppBackupWorker import com.stevesoltys.seedvault.worker.AppBackupWorker
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -44,7 +44,7 @@ internal class StorageBackupService : BackupService() {
} }
override val storageBackup: StorageBackup by inject() 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 // use lazy delegate because context isn't available during construction time
override val backupObserver: BackupObserver by lazy { override val backupObserver: BackupObserver by lazy {
@ -63,7 +63,7 @@ internal class StorageBackupService : BackupService() {
override fun onBackupFinished(intent: Intent, success: Boolean) { override fun onBackupFinished(intent: Intent, success: Boolean) {
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) { 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) AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb)
} }
} }

View file

@ -6,10 +6,10 @@
package com.stevesoltys.seedvault.storage package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.crypto.KeyManager 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.calyxos.backup.storage.api.StorageBackup
import org.koin.dsl.module import org.koin.dsl.module
val storageModule = 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.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.getMetadataOutputStream
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.plugins.isOutOfSpace
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException import java.io.IOException
import java.io.OutputStream
import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.DAYS
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
@ -65,7 +62,7 @@ private class CoordinatorState(
@WorkerThread @WorkerThread
internal class BackupCoordinator( internal class BackupCoordinator(
private val context: Context, private val context: Context,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
private val clock: Clock, private val clock: Clock,
@ -75,7 +72,7 @@ internal class BackupCoordinator(
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
) { ) {
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private val state = CoordinatorState( private val state = CoordinatorState(
calledInitialize = false, calledInitialize = false,
calledClearBackupData = false, calledClearBackupData = false,
@ -132,7 +129,7 @@ internal class BackupCoordinator(
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error initializing device", e) Log.e(TAG, "Error initializing device", e)
// Show error notification if we needed init or were ready for backups // 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 TRANSPORT_ERROR
} }
@ -370,7 +367,7 @@ internal class BackupCoordinator(
if (result == TRANSPORT_OK) { if (result == TRANSPORT_OK) {
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now // call onPackageBackedUp for @pm@ only if we can do backups right now
if (isNormalBackup || pluginManager.canDoBackupNow()) { if (isNormalBackup || backendManager.canDoBackupNow()) {
try { try {
onPackageBackedUp(packageInfo, BackupType.KV, size) onPackageBackedUp(packageInfo, BackupType.KV, size)
} catch (e: Exception) { } catch (e: Exception) {
@ -431,7 +428,7 @@ internal class BackupCoordinator(
val longBackoff = DAYS.toMillis(30) val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set // back off if there's no storage set
val storage = pluginManager.storageProperties ?: return longBackoff val storage = backendManager.backendProperties ?: return longBackoff
return when { return when {
// back off if storage is removable and not available right now // back off if storage is removable and not available right now
storage.isUnavailableUsb(context) -> longBackoff storage.isUnavailableUsb(context) -> longBackoff
@ -444,12 +441,4 @@ internal class BackupCoordinator(
else -> 0L 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(), context = androidContext(),
backupManager = get(), backupManager = get(),
settingsManager = get(), settingsManager = get(),
pluginManager = get(), backendManager = get(),
) )
} }
single<KvDbManager> { KvDbManagerImpl(androidContext()) } single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single { single {
KVBackup( KVBackup(
pluginManager = get(), backendManager = get(),
settingsManager = get(), settingsManager = get(),
nm = get(), nm = get(),
inputFactory = get(), inputFactory = get(),
@ -32,7 +32,7 @@ val backupModule = module {
} }
single { single {
FullBackup( FullBackup(
pluginManager = get(), backendManager = get(),
settingsManager = get(), settingsManager = get(),
nm = get(), nm = get(),
inputFactory = get(), inputFactory = get(),
@ -42,7 +42,7 @@ val backupModule = module {
single { single {
BackupCoordinator( BackupCoordinator(
context = androidContext(), context = androidContext(),
pluginManager = get(), backendManager = get(),
kv = get(), kv = get(),
full = get(), full = get(),
clock = get(), clock = get(),

View file

@ -16,8 +16,8 @@ import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.isOutOfSpace import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
@ -47,14 +47,14 @@ private val TAG = FullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup( internal class FullBackup(
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
private val crypto: Crypto, private val crypto: Crypto,
) { ) {
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private var state: FullBackupState? = null private var state: FullBackupState? = null
fun hasState() = state != 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.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.isOutOfSpace import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile 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 private val TAG = KVBackup::class.java.simpleName
internal class KVBackup( internal class KVBackup(
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
@ -48,7 +48,7 @@ internal class KVBackup(
private val dbManager: KvDbManager, private val dbManager: KvDbManager,
) { ) {
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private var state: KVBackupState? = null private var state: KVBackupState? = null
fun hasState() = state != null fun hasState() = state != null
@ -147,7 +147,7 @@ internal class KVBackup(
// K/V backups (typically starting with package manager metadata - @pm@) // K/V backups (typically starting with package manager metadata - @pm@)
// are scheduled with JobInfo.Builder#setOverrideDeadline() // are scheduled with JobInfo.Builder#setOverrideDeadline()
// and thus do not respect backoff. // and thus do not respect backoff.
pluginManager.canDoBackupNow() backendManager.canDoBackupNow()
} else { } else {
// all other packages always need upload // all other packages always need upload
true true

View file

@ -27,7 +27,7 @@ import android.util.Log
import android.util.Log.INFO import android.util.Log.INFO
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER 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 com.stevesoltys.seedvault.settings.SettingsManager
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
@ -43,12 +43,12 @@ internal class PackageService(
private val context: Context, private val context: Context,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
) { ) {
private val packageManager: PackageManager = context.packageManager private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId() private val myUserId = UserHandle.myUserId()
private val backend: Backend get() = pluginManager.backend private val backend: Backend get() = backendManager.backend
val eligiblePackages: List<String> val eligiblePackages: List<String>
@WorkerThread @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.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.getADForFull import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.EOFException import java.io.EOFException
@ -39,7 +39,7 @@ private class FullRestoreState(
private val TAG = FullRestore::class.java.simpleName private val TAG = FullRestore::class.java.simpleName
internal class FullRestore( internal class FullRestore(
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin, private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory, private val outputFactory: OutputFactory,
@ -47,7 +47,7 @@ internal class FullRestore(
private val crypto: Crypto, private val crypto: Crypto,
) { ) {
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private var state: FullRestoreState? = null private var state: FullRestoreState? = null
fun hasState() = state != 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.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.KVDb import com.stevesoltys.seedvault.transport.backup.KVDb
import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.transport.backup.KvDbManager
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
@ -45,7 +45,7 @@ private class KVRestoreState(
private val TAG = KVRestore::class.java.simpleName private val TAG = KVRestore::class.java.simpleName
internal class KVRestore( internal class KVRestore(
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin, private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory, private val outputFactory: OutputFactory,
@ -54,7 +54,7 @@ internal class KVRestore(
private val dbManager: KvDbManager, private val dbManager: KvDbManager,
) { ) {
private val backend get() = pluginManager.backend private val backend get() = backendManager.backend
private var state: KVRestoreState? = null 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.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.getAvailableBackups import com.stevesoltys.seedvault.backend.getAvailableBackups
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
@ -62,13 +62,13 @@ internal class RestoreCoordinator(
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager, private val notificationManager: BackupNotificationManager,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
private val metadataReader: MetadataReader, 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 state: RestoreCoordinatorState? = null
private var backupMetadata: BackupMetadata? = null private var backupMetadata: BackupMetadata? = null
private val failedPackages = ArrayList<String>() private val failedPackages = ArrayList<String>()
@ -176,7 +176,7 @@ internal class RestoreCoordinator(
// check if we even have a backup of that app // check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) { if (metadataManager.getPackageMetadata(pmPackageName) != null) {
// remind user to plug in storage device // 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) ?: context.getString(R.string.settings_backup_location_none)
notificationManager.onRemovableStorageNotAvailableForRestore( notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName, pmPackageName,
@ -359,7 +359,7 @@ internal class RestoreCoordinator(
fun isFailedPackage(packageName: String) = packageName in failedPackages fun isFailedPackage(packageName: String) = packageName in failedPackages
private fun isStorageRemovableAndNotAvailable(): Boolean { private fun isStorageRemovableAndNotAvailable(): Boolean {
val storage = pluginManager.storageProperties ?: return false val storage = backendManager.backendProperties ?: return false
return storage.isUnavailableUsb(context) return storage.isUnavailableUsb(context)
} }

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTre
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.stevesoltys.seedvault.R 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.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_SETUP_WIZARD 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.provider.DocumentsContract.buildRootsUri
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.saf.SafStorageOptions import com.stevesoltys.seedvault.backend.saf.SafStorageOptions
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver import com.stevesoltys.seedvault.backend.saf.StorageRootResolver
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
private val TAG = StorageOptionFetcher::class.java.simpleName private val TAG = StorageOptionFetcher::class.java.simpleName

View file

@ -13,11 +13,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.saf.SafHandler import com.stevesoltys.seedvault.backend.saf.SafHandler
import com.stevesoltys.seedvault.plugins.saf.SafStorage import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.saf.SafProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
internal abstract class StorageViewModel( internal abstract class StorageViewModel(
@ -32,7 +32,7 @@ internal abstract class StorageViewModel(
protected val safHandler: SafHandler, protected val safHandler: SafHandler,
protected val webdavHandler: WebDavHandler, protected val webdavHandler: WebDavHandler,
protected val settingsManager: SettingsManager, protected val settingsManager: SettingsManager,
protected val storagePluginManager: StoragePluginManager, protected val backendManager: BackendManager,
) : AndroidViewModel(app), RemovableStorageListener { ) : AndroidViewModel(app), RemovableStorageListener {
private val mStorageOptions = MutableLiveData<List<StorageOption>>() private val mStorageOptions = MutableLiveData<List<StorageOption>>()
@ -49,7 +49,7 @@ internal abstract class StorageViewModel(
internal var isSetupWizard: Boolean = false internal var isSetupWizard: Boolean = false
internal val hasStorageSet: Boolean internal val hasStorageSet: Boolean
get() = storagePluginManager.storageProperties != null get() = backendManager.backendProperties != null
abstract val isRestoreOperation: Boolean abstract val isRestoreOperation: Boolean
internal fun loadStorageRoots() { internal fun loadStorageRoots() {
@ -88,7 +88,7 @@ internal abstract class StorageViewModel(
onSafUriSet(safStorage) onSafUriSet(safStorage)
} }
abstract fun onSafUriSet(safStorage: SafStorage) abstract fun onSafUriSet(safProperties: SafProperties)
abstract fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) abstract fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend)
override fun onCleared() { 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.snackbar.Snackbar.LENGTH_LONG
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.stevesoltys.seedvault.R 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 com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.getSharedViewModel 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.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.getMetadataOutputStream
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.plugins.isOutOfSpace
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.isStopped import com.stevesoltys.seedvault.transport.backup.isStopped
@ -24,7 +22,6 @@ import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import java.io.IOException import java.io.IOException
import java.io.OutputStream
internal class ApkBackupManager( internal class ApkBackupManager(
private val context: Context, private val context: Context,
@ -33,7 +30,7 @@ internal class ApkBackupManager(
private val packageService: PackageService, private val packageService: PackageService,
private val iconManager: IconManager, private val iconManager: IconManager,
private val apkBackup: ApkBackup, private val apkBackup: ApkBackup,
private val pluginManager: StoragePluginManager, private val backendManager: BackendManager,
private val nm: BackupNotificationManager, private val nm: BackupNotificationManager,
) { ) {
@ -58,7 +55,7 @@ internal class ApkBackupManager(
// upload all local changes only at the end, // upload all local changes only at the end,
// so we don't have to re-upload the metadata // so we don't have to re-upload the metadata
val token = settingsManager.getToken() ?: error("no token") val token = settingsManager.getToken() ?: error("no token")
pluginManager.backend.getMetadataOutputStream(token).use { outputStream -> backendManager.backend.getMetadataOutputStream(token).use { outputStream ->
metadataManager.uploadMetadata(outputStream) metadataManager.uploadMetadata(outputStream)
} }
} }
@ -105,7 +102,7 @@ internal class ApkBackupManager(
try { try {
val token = settingsManager.getToken() ?: throw IOException("no current token") val token = settingsManager.getToken() ?: throw IOException("no current token")
val handle = LegacyAppBackupFile.IconsFile(token) val handle = LegacyAppBackupFile.IconsFile(token)
pluginManager.backend.save(handle).use { backendManager.backend.save(handle).use {
iconManager.uploadIcons(token, it) iconManager.uploadIcons(token, it)
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -123,7 +120,7 @@ internal class ApkBackupManager(
return try { return try {
apkBackup.backupApkIfNecessary(packageInfo) { name -> apkBackup.backupApkIfNecessary(packageInfo) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token") 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 -> }?.let { packageMetadata ->
metadataManager.onApkBackedUp(packageInfo, packageMetadata) metadataManager.onApkBackedUp(packageInfo, packageMetadata)
true 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.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters 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.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
@ -101,7 +101,7 @@ class AppBackupWorker(
private val backupRequester: BackupRequester by inject() private val backupRequester: BackupRequester by inject()
private val settingsManager: SettingsManager by inject() private val settingsManager: SettingsManager by inject()
private val apkBackupManager: ApkBackupManager 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() private val nm: BackupNotificationManager by inject()
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
@ -111,7 +111,7 @@ class AppBackupWorker(
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e) Log.e(TAG, "Error while running setForeground: ", e)
} }
val freeSpace = storagePluginManager.getFreeSpace() val freeSpace = backendManager.getFreeSpace()
if (freeSpace != null && freeSpace < MIN_FREE_SPACE) { if (freeSpace != null && freeSpace < MIN_FREE_SPACE) {
nm.onInsufficientSpaceError() nm.onInsufficientSpaceError()
return Result.failure() return Result.failure()

View file

@ -39,7 +39,7 @@ val workerModule = module {
packageService = get(), packageService = get(),
apkBackup = get(), apkBackup = get(),
iconManager = get(), iconManager = get(),
pluginManager = get(), backendManager = get(),
nm = 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.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.metadataModule 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.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService

View file

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

View file

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

View file

@ -24,8 +24,8 @@ import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.ApkSplit import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.restore.RestorableBackup import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP 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 backupManager: IBackupManager = mockk()
private val backupStateManager: BackupStateManager = mockk() private val backupStateManager: BackupStateManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend: Backend = mockk() private val backend: Backend = mockk()
private val legacyStoragePlugin: LegacyStoragePlugin = mockk() private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk() private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
@ -78,7 +78,7 @@ internal class ApkRestoreTest : TransportTest() {
context = strictContext, context = strictContext,
backupManager = backupManager, backupManager = backupManager,
backupStateManager = backupStateManager, backupStateManager = backupStateManager,
pluginManager = storagePluginManager, backendManager = backendManager,
legacyStoragePlugin = legacyStoragePlugin, legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto, crypto = crypto,
splitCompatChecker = splitCompatChecker, splitCompatChecker = splitCompatChecker,
@ -109,7 +109,7 @@ internal class ApkRestoreTest : TransportTest() {
// as we don't do strict signature checking, we can use a relaxed mock // as we don't do strict signature checking, we can use a relaxed mock
packageInfo.signingInfo = mockk(relaxed = true) packageInfo.signingInfo = mockk(relaxed = true)
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
// related to starting/stopping service // related to starting/stopping service
every { strictContext.packageName } returns "org.foo.bar" 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.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory import com.stevesoltys.seedvault.transport.backup.InputFactory
@ -63,13 +63,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val metadataReader = MetadataReaderImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val dbManager = TestKvDbManager() private val dbManager = TestKvDbManager()
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>() private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
private val kvBackup = KVBackup( private val kvBackup = KVBackup(
pluginManager = storagePluginManager, backendManager = backendManager,
settingsManager = settingsManager, settingsManager = settingsManager,
nm = notificationManager, nm = notificationManager,
inputFactory = inputFactory, inputFactory = inputFactory,
@ -77,7 +77,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
dbManager = dbManager, dbManager = dbManager,
) )
private val fullBackup = FullBackup( private val fullBackup = FullBackup(
pluginManager = storagePluginManager, backendManager = backendManager,
settingsManager = settingsManager, settingsManager = settingsManager,
nm = notificationManager, nm = notificationManager,
inputFactory = inputFactory, inputFactory = inputFactory,
@ -87,7 +87,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val packageService: PackageService = mockk() private val packageService: PackageService = mockk()
private val backup = BackupCoordinator( private val backup = BackupCoordinator(
context, context,
storagePluginManager, backendManager,
kvBackup, kvBackup,
fullBackup, fullBackup,
clock, clock,
@ -98,7 +98,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
) )
private val kvRestore = KVRestore( private val kvRestore = KVRestore(
storagePluginManager, backendManager,
legacyPlugin, legacyPlugin,
outputFactory, outputFactory,
headerReader, headerReader,
@ -106,14 +106,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
dbManager dbManager
) )
private val fullRestore = private val fullRestore =
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl) FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator( private val restore = RestoreCoordinator(
context, context,
crypto, crypto,
settingsManager, settingsManager,
metadataManager, metadataManager,
notificationManager, notificationManager,
storagePluginManager, backendManager,
kvRestore, kvRestore,
fullRestore, fullRestore,
metadataReader metadataReader
@ -132,7 +132,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName) private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
init { init {
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test

View file

@ -20,8 +20,7 @@ import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.ApkBackup import com.stevesoltys.seedvault.worker.ApkBackup
import io.mockk.Runs import io.mockk.Runs
@ -33,6 +32,7 @@ import io.mockk.verify
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile 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.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.IOException import java.io.IOException
@ -41,7 +41,7 @@ import kotlin.random.Random
internal class BackupCoordinatorTest : BackupTest() { internal class BackupCoordinatorTest : BackupTest() {
private val pluginManager = mockk<StoragePluginManager>() private val backendManager = mockk<BackendManager>()
private val kv = mockk<KVBackup>() private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>() private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>() private val apkBackup = mockk<ApkBackup>()
@ -50,7 +50,7 @@ internal class BackupCoordinatorTest : BackupTest() {
private val backup = BackupCoordinator( private val backup = BackupCoordinator(
context = context, context = context,
pluginManager = pluginManager, backendManager = backendManager,
kv = kv, kv = kv,
full = full, full = full,
clock = clock, clock = clock,
@ -64,7 +64,7 @@ internal class BackupCoordinatorTest : BackupTest() {
private val metadataOutputStream = mockk<OutputStream>() private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk() private val fileDescriptor: ParcelFileDescriptor = mockk()
private val packageMetadata: PackageMetadata = mockk() private val packageMetadata: PackageMetadata = mockk()
private val safStorage = SafStorage( private val safProperties = SafProperties(
config = Uri.EMPTY, config = Uri.EMPTY,
name = getRandomString(), name = getRandomString(),
isUsb = false, isUsb = false,
@ -73,7 +73,7 @@ internal class BackupCoordinatorTest : BackupTest() {
) )
init { init {
every { pluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test
@ -100,7 +100,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { settingsManager.setNewToken(token) } just Runs every { settingsManager.setNewToken(token) } just Runs
every { metadataManager.onDeviceInitialization(token) } throws IOException() every { metadataManager.onDeviceInitialization(token) } throws IOException()
every { metadataManager.requiresInit } returns maybeTrue every { metadataManager.requiresInit } returns maybeTrue
every { pluginManager.canDoBackupNow() } returns !maybeTrue every { backendManager.canDoBackupNow() } returns !maybeTrue
every { notificationManager.onBackupError() } just Runs every { notificationManager.onBackupError() } just Runs
assertEquals(TRANSPORT_ERROR, backup.initializeDevice()) assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -120,7 +120,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { settingsManager.setNewToken(token) } just Runs every { settingsManager.setNewToken(token) } just Runs
every { metadataManager.onDeviceInitialization(token) } throws IOException() every { metadataManager.onDeviceInitialization(token) } throws IOException()
every { metadataManager.requiresInit } returns false every { metadataManager.requiresInit } returns false
every { pluginManager.canDoBackupNow() } returns false every { backendManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_ERROR, backup.initializeDevice()) assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -136,7 +136,7 @@ internal class BackupCoordinatorTest : BackupTest() {
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking { fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
every { pluginManager.canDoBackupNow() } returns true every { backendManager.canDoBackupNow() } returns true
every { metadataManager.requiresInit } returns true every { metadataManager.requiresInit } returns true
// start new restore set // start new restore set
@ -234,7 +234,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { kv.getCurrentSize() } returns 42L every { kv.getCurrentSize() } returns 42L
coEvery { kv.finishBackup() } returns TRANSPORT_OK coEvery { kv.finishBackup() } returns TRANSPORT_OK
every { pluginManager.canDoBackupNow() } returns false every { backendManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_OK, backup.finishBackup()) assertEquals(TRANSPORT_OK, backup.finishBackup())
} }
@ -300,7 +300,7 @@ internal class BackupCoordinatorTest : BackupTest() {
) )
} just Runs } just Runs
coEvery { full.cancelFullBackup(token, metadata.salt, false) } 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 { settingsManager.useMeteredNetwork } returns false
every { metadataOutputStream.close() } just Runs every { metadataOutputStream.close() } just Runs
@ -350,7 +350,7 @@ internal class BackupCoordinatorTest : BackupTest() {
) )
} just Runs } just Runs
coEvery { full.cancelFullBackup(token, metadata.salt, false) } 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 { settingsManager.useMeteredNetwork } returns false
every { metadataOutputStream.close() } just Runs 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 android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull 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 com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
@ -31,11 +31,11 @@ import kotlin.random.Random
internal class FullBackupTest : BackupTest() { internal class FullBackupTest : BackupTest() {
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val backup = FullBackup( private val backup = FullBackup(
pluginManager = storagePluginManager, backendManager = backendManager,
settingsManager = settingsManager, settingsManager = settingsManager,
nm = notificationManager, nm = notificationManager,
inputFactory = inputFactory, inputFactory = inputFactory,
@ -47,7 +47,7 @@ internal class FullBackupTest : BackupTest() {
private val ad = getADForFull(VERSION, packageInfo.packageName) private val ad = getADForFull(VERSION, packageInfo.packageName)
init { init {
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @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.MAX_KEY_LENGTH_SIZE
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV 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 com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.CapturingSlot import io.mockk.CapturingSlot
import io.mockk.Runs import io.mockk.Runs
@ -39,13 +39,13 @@ import kotlin.random.Random
internal class KVBackupTest : BackupTest() { internal class KVBackupTest : BackupTest() {
private val pluginManager = mockk<StoragePluginManager>() private val backendManager = mockk<BackendManager>()
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val dataInput = mockk<BackupDataInput>() private val dataInput = mockk<BackupDataInput>()
private val dbManager = mockk<KvDbManager>() private val dbManager = mockk<KvDbManager>()
private val backup = KVBackup( private val backup = KVBackup(
pluginManager = pluginManager, backendManager = backendManager,
settingsManager = settingsManager, settingsManager = settingsManager,
nm = notificationManager, nm = notificationManager,
inputFactory = inputFactory, inputFactory = inputFactory,
@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() {
private val inputStream = ByteArrayInputStream(dbBytes) private val inputStream = ByteArrayInputStream(dbBytes)
init { init {
every { pluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test
@ -250,7 +250,7 @@ internal class KVBackupTest : BackupTest() {
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
every { dbManager.getDb(pmPackageInfo.packageName) } returns db every { dbManager.getDb(pmPackageInfo.packageName) } returns db
every { pluginManager.canDoBackupNow() } returns false every { backendManager.canDoBackupNow() } returns false
every { db.put(key, dataValue) } just Runs every { db.put(key, dataValue) } just Runs
getDataInput(listOf(true, false)) 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.VERSION
import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForFull import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import io.mockk.CapturingSlot import io.mockk.CapturingSlot
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
@ -39,11 +39,11 @@ import kotlin.random.Random
internal class FullRestoreTest : RestoreTest() { internal class FullRestoreTest : RestoreTest() {
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
private val legacyPlugin = mockk<LegacyStoragePlugin>() private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val restore = FullRestore( private val restore = FullRestore(
pluginManager = storagePluginManager, backendManager = backendManager,
legacyPlugin = legacyPlugin, legacyPlugin = legacyPlugin,
outputFactory = outputFactory, outputFactory = outputFactory,
headerReader = headerReader, headerReader = headerReader,
@ -55,7 +55,7 @@ internal class FullRestoreTest : RestoreTest() {
private val ad = getADForFull(VERSION, packageInfo.packageName) private val ad = getADForFull(VERSION, packageInfo.packageName)
init { init {
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test

View file

@ -15,8 +15,8 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForKV import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.transport.backup.KVDb import com.stevesoltys.seedvault.transport.backup.KVDb
import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.transport.backup.KvDbManager
import io.mockk.Runs import io.mockk.Runs
@ -41,14 +41,14 @@ import kotlin.random.Random
internal class KVRestoreTest : RestoreTest() { internal class KVRestoreTest : RestoreTest() {
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private val legacyPlugin = mockk<LegacyStoragePlugin>() private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val dbManager = mockk<KvDbManager>() private val dbManager = mockk<KvDbManager>()
private val output = mockk<BackupDataOutput>() private val output = mockk<BackupDataOutput>()
private val restore = KVRestore( private val restore = KVRestore(
pluginManager = storagePluginManager, backendManager = backendManager,
legacyPlugin = legacyPlugin, legacyPlugin = legacyPlugin,
outputFactory = outputFactory, outputFactory = outputFactory,
headerReader = headerReader, headerReader = headerReader,
@ -74,7 +74,7 @@ internal class KVRestoreTest : RestoreTest() {
// for InputStream#readBytes() // for InputStream#readBytes()
mockkStatic("kotlin.io.ByteStreamsKt") mockkStatic("kotlin.io.ByteStreamsKt")
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @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.app.backup.RestoreDescription.TYPE_KEY_VALUE
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor 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.coAssertThrows
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.metadata.PackageMetadata 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.transport.TransportTest
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs import io.mockk.Runs
@ -34,6 +33,7 @@ import io.mockk.mockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.seedvault.core.backends.Backend 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.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
@ -46,7 +46,7 @@ import kotlin.random.Random
internal class RestoreCoordinatorTest : TransportTest() { internal class RestoreCoordinatorTest : TransportTest() {
private val notificationManager: BackupNotificationManager = mockk() private val notificationManager: BackupNotificationManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
private val kv = mockk<KVRestore>() private val kv = mockk<KVRestore>()
private val full = mockk<FullRestore>() private val full = mockk<FullRestore>()
@ -58,14 +58,14 @@ internal class RestoreCoordinatorTest : TransportTest() {
settingsManager = settingsManager, settingsManager = settingsManager,
metadataManager = metadataManager, metadataManager = metadataManager,
notificationManager = notificationManager, notificationManager = notificationManager,
pluginManager = storagePluginManager, backendManager = backendManager,
kv = kv, kv = kv,
full = full, full = full,
metadataReader = metadataReader, metadataReader = metadataReader,
) )
private val inputStream = mockk<InputStream>() 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 packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
private val packageInfoArray = arrayOf(packageInfo) private val packageInfoArray = arrayOf(packageInfo)
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2) private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
@ -80,8 +80,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
metadata.packageMetadataMap[packageInfo2.packageName] = metadata.packageMetadataMap[packageInfo2.packageName] =
PackageMetadata(backupType = BackupType.FULL) PackageMetadata(backupType = BackupType.FULL)
mockkStatic("com.stevesoltys.seedvault.plugins.BackendExtKt") mockkStatic("com.stevesoltys.seedvault.backend.BackendExtKt")
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test
@ -175,7 +175,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `startRestore() optimized auto-restore with removed storage shows notification`() = fun `startRestore() optimized auto-restore with removed storage shows notification`() =
runBlocking { runBlocking {
every { storagePluginManager.storageProperties } returns safStorage every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns true every { safStorage.isUnavailableUsb(context) } returns true
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L) every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
every { safStorage.name } returns storageName every { safStorage.name } returns storageName
@ -199,7 +199,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `startRestore() optimized auto-restore with available storage shows no notification`() = fun `startRestore() optimized auto-restore with available storage shows no notification`() =
runBlocking { runBlocking {
every { storagePluginManager.storageProperties } returns safStorage every { backendManager.backendProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns false every { safStorage.isUnavailableUsb(context) } returns false
restore.beforeStartRestore(metadata) restore.beforeStartRestore(metadata)
@ -215,7 +215,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `startRestore() with removed storage shows no notification`() = runBlocking { 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 { safStorage.isUnavailableUsb(context) } returns true
every { metadataManager.getPackageMetadata(packageName) } returns null 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.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.toByteArrayFromHex import com.stevesoltys.seedvault.toByteArrayFromHex
import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.transport.backup.KvDbManager
@ -55,13 +54,13 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val dbManager = mockk<KvDbManager>() private val dbManager = mockk<KvDbManager>()
private val metadataReader = MetadataReaderImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
@Suppress("Deprecation") @Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>() private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backend = mockk<Backend>() private val backend = mockk<Backend>()
private val kvRestore = KVRestore( private val kvRestore = KVRestore(
pluginManager = storagePluginManager, backendManager = backendManager,
legacyPlugin = legacyPlugin, legacyPlugin = legacyPlugin,
outputFactory = outputFactory, outputFactory = outputFactory,
headerReader = headerReader, headerReader = headerReader,
@ -69,14 +68,14 @@ internal class RestoreV0IntegrationTest : TransportTest() {
dbManager = dbManager, dbManager = dbManager,
) )
private val fullRestore = private val fullRestore =
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl) FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator( private val restore = RestoreCoordinator(
context = context, context = context,
crypto = crypto, crypto = crypto,
settingsManager = settingsManager, settingsManager = settingsManager,
metadataManager = metadataManager, metadataManager = metadataManager,
notificationManager = notificationManager, notificationManager = notificationManager,
pluginManager = storagePluginManager, backendManager = backendManager,
kv = kvRestore, kv = kvRestore,
full = fullRestore, full = fullRestore,
metadataReader = metadataReader, metadataReader = metadataReader,
@ -124,7 +123,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val key264 = key2.encodeBase64() private val key264 = key2.encodeBase64()
init { init {
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @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.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED 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.TransportTest
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -40,7 +40,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageService: PackageService = mockk() private val packageService: PackageService = mockk()
private val apkBackup: ApkBackup = mockk() private val apkBackup: ApkBackup = mockk()
private val iconManager: IconManager = mockk() private val iconManager: IconManager = mockk()
private val storagePluginManager: StoragePluginManager = mockk() private val backendManager: BackendManager = mockk()
private val backend: Backend = mockk() private val backend: Backend = mockk()
private val nm: BackupNotificationManager = mockk() private val nm: BackupNotificationManager = mockk()
@ -51,7 +51,7 @@ internal class ApkBackupManagerTest : TransportTest() {
packageService = packageService, packageService = packageService,
apkBackup = apkBackup, apkBackup = apkBackup,
iconManager = iconManager, iconManager = iconManager,
pluginManager = storagePluginManager, backendManager = backendManager,
nm = nm, nm = nm,
) )
@ -59,7 +59,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageMetadata: PackageMetadata = mockk() private val packageMetadata: PackageMetadata = mockk()
init { init {
every { storagePluginManager.backend } returns backend every { backendManager.backend } returns backend
} }
@Test @Test

View file

@ -5,6 +5,7 @@
package org.calyxos.seedvault.core 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() { public data class TopLevelFolder(override val name: String) : FileHandle() {
override val relativePath: String = name override val relativePath: String = name
public companion object {
public fun fromAndroidId(androidId: String): TopLevelFolder {
return TopLevelFolder("$androidId.sv")
}
}
} }
public sealed class LegacyAppBackupFile : FileHandle() { public sealed class LegacyAppBackupFile : FileHandle() {

View file

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

View file

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

View file

@ -3,16 +3,15 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
package com.stevesoltys.seedvault.plugins.webdav package org.calyxos.seedvault.core.backends.webdav
import android.content.Context import android.content.Context
import com.stevesoltys.seedvault.plugins.StorageProperties import org.calyxos.seedvault.core.backends.BackendProperties
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
data class WebDavProperties( public data class WebDavProperties(
override val config: WebDavConfig, override val config: WebDavConfig,
override val name: String, override val name: String,
) : StorageProperties<WebDavConfig>() { ) : BackendProperties<WebDavConfig>() {
override val isUsb: Boolean = false override val isUsb: Boolean = false
override val requiresNetwork: Boolean = true override val requiresNetwork: Boolean = true
override fun isUnavailableUsb(context: Context): Boolean = false 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.FileInfo
import org.calyxos.seedvault.core.backends.TopLevelFolder import org.calyxos.seedvault.core.backends.TopLevelFolder
import org.calyxos.seedvault.core.backends.saf.SafBackend 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.InputStream
import java.io.OutputStream import java.io.OutputStream
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -22,15 +22,15 @@ class TestSafBackend(
private val getLocationUri: () -> Uri?, private val getLocationUri: () -> Uri?,
) : Backend { ) : Backend {
private val safConfig private val safProperties
get() = SafConfig( get() = SafProperties(
config = getLocationUri() ?: error("no uri"), config = getLocationUri() ?: error("no uri"),
name = "foo", name = "foo",
isUsb = false, isUsb = false,
requiresNetwork = false, requiresNetwork = false,
rootId = "bar", rootId = "bar",
) )
private val delegate: SafBackend get() = SafBackend(appContext, safConfig) private val delegate: SafBackend get() = SafBackend(appContext, safProperties)
private val nullStream = object : OutputStream() { private val nullStream = object : OutputStream() {
override fun write(b: Int) { 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 package org.calyxos.backup.storage
import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.provider.MediaStore
import org.calyxos.backup.storage.api.MediaType import org.calyxos.backup.storage.api.MediaType
import org.calyxos.backup.storage.api.mediaItems import org.calyxos.backup.storage.api.mediaItems
import org.calyxos.backup.storage.backup.BackupMediaFile import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.db.StoredUri 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.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? { internal fun Uri.getMediaType(): MediaType? {
val str = toString() val str = toString()
for (item in mediaItems) { for (item in mediaItems) {

View file

@ -11,7 +11,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.calyxos.backup.storage.R import org.calyxos.backup.storage.R
import org.calyxos.backup.storage.backup.BackupMediaFile import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.getDocumentPath import org.calyxos.seedvault.core.backends.saf.getDocumentPath
// hidden in DocumentsContract // hidden in DocumentsContract
public const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY: String = 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 object Custom : BackupContentType(R.drawable.ic_folder) {
public fun getName(uri: Uri): String { public fun getName(uri: Uri): String {
val path = uri.getDocumentPath()!! 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.backup.ChunksCacheRepopulater
import org.calyxos.backup.storage.db.Db import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.getCurrentBackupSnapshots import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.backup.storage.getDocumentPath
import org.calyxos.backup.storage.getMediaType import org.calyxos.backup.storage.getMediaType
import org.calyxos.backup.storage.prune.Pruner import org.calyxos.backup.storage.prune.Pruner
import org.calyxos.backup.storage.prune.RetentionManager 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.backup.storage.toStoredUri
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
import org.calyxos.seedvault.core.crypto.KeyManager import org.calyxos.seedvault.core.crypto.KeyManager
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean

View file

@ -6,7 +6,7 @@
package org.calyxos.backup.storage.backup package org.calyxos.backup.storage.backup
import org.calyxos.backup.storage.db.CachedChunk 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.IOException
import java.io.InputStream import java.io.InputStream
import javax.crypto.Mac 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.content.MediaFile
import org.calyxos.backup.storage.db.CachedFile import org.calyxos.backup.storage.db.CachedFile
import org.calyxos.backup.storage.db.FilesCache 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.io.IOException
import java.security.GeneralSecurityException 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.content.MediaFile
import org.calyxos.backup.storage.db.CachedFile import org.calyxos.backup.storage.db.CachedFile
import org.calyxos.backup.storage.db.FilesCache 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.io.IOException
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
@Suppress("BlockingMethodInNonBlockingContext")
internal class SmallFileBackup( internal class SmallFileBackup(
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val filesCache: FilesCache, 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.content.ContentFile
import org.calyxos.backup.storage.db.CachedChunk 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.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream 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.backup.Backup.Companion.VERSION
import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC
import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES 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.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream 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.MediaType
import org.calyxos.backup.storage.api.RestoreObserver import org.calyxos.backup.storage.api.RestoreObserver
import org.calyxos.backup.storage.backup.BackupMediaFile import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.openOutputStream
import org.calyxos.backup.storage.scanner.MediaScanner import org.calyxos.backup.storage.scanner.MediaScanner
import org.calyxos.seedvault.core.backends.saf.openOutputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream

View file

@ -14,8 +14,8 @@ import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import org.calyxos.backup.storage.api.BackupFile import org.calyxos.backup.storage.api.BackupFile
import org.calyxos.backup.storage.content.DocFile import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.getDocumentPath import org.calyxos.seedvault.core.backends.saf.getDocumentPath
import org.calyxos.backup.storage.getVolume import org.calyxos.seedvault.core.backends.saf.getVolume
public class DocumentScanner(context: Context) { 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.db.ChunksCache
import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog 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.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test 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.getRandomDocFile
import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog 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.Backend
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream

View file

@ -13,7 +13,7 @@ import io.mockk.mockk
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.getRandomDocFile import org.calyxos.backup.storage.getRandomDocFile
import org.calyxos.backup.storage.getRandomString 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.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue