Switch everything to new backends
This commit is contained in:
parent
58d58415c5
commit
96a3564610
94 changed files with 446 additions and 1569 deletions
|
@ -56,7 +56,7 @@ class KoinInstrumentationTestApp : App() {
|
|||
apkRestore = get(),
|
||||
iconManager = get(),
|
||||
storageBackup = get(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
fileSelectionManager = get(),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -9,16 +9,17 @@ import androidx.test.core.content.pm.PackageInfoBuilder
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
|
||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.saf.DocumentsProviderLegacyPlugin
|
||||
import com.stevesoltys.seedvault.backend.saf.DocumentsStorage
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import org.calyxos.seedvault.core.backends.saf.SafBackend
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
|
@ -38,11 +39,10 @@ class PluginTest : KoinComponent {
|
|||
private val mockedSettingsManager: SettingsManager = mockk()
|
||||
private val storage = DocumentsStorage(
|
||||
appContext = context,
|
||||
settingsManager = mockedSettingsManager,
|
||||
safStorage = settingsManager.getSafStorage() ?: error("No SAF storage"),
|
||||
safStorage = settingsManager.getSafProperties() ?: error("No SAF storage"),
|
||||
)
|
||||
|
||||
private val storagePlugin = DocumentsProviderStoragePlugin(context, storage.safStorage)
|
||||
private val backend = SafBackend(context, storage.safStorage)
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val legacyStoragePlugin: LegacyStoragePlugin = DocumentsProviderLegacyPlugin(context) {
|
||||
|
@ -55,29 +55,30 @@ class PluginTest : KoinComponent {
|
|||
|
||||
@Before
|
||||
fun setup() = runBlocking {
|
||||
every { mockedSettingsManager.getSafStorage() } returns settingsManager.getSafStorage()
|
||||
storagePlugin.removeAll()
|
||||
every {
|
||||
mockedSettingsManager.getSafProperties()
|
||||
} returns settingsManager.getSafProperties()
|
||||
backend.removeAll()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() = runBlocking {
|
||||
storagePlugin.removeAll()
|
||||
Unit
|
||||
backend.removeAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProviderPackageName() {
|
||||
assertNotNull(storagePlugin.providerPackageName)
|
||||
assertNotNull(backend.providerPackageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTest() = runBlocking(Dispatchers.IO) {
|
||||
assertTrue(storagePlugin.test())
|
||||
assertTrue(backend.test())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFreeSpace() = runBlocking(Dispatchers.IO) {
|
||||
val freeBytes = storagePlugin.getFreeSpace() ?: error("no free space retrieved")
|
||||
val freeBytes = backend.getFreeSpace() ?: error("no free space retrieved")
|
||||
assertTrue(freeBytes > 0)
|
||||
}
|
||||
|
||||
|
@ -91,49 +92,39 @@ class PluginTest : KoinComponent {
|
|||
@Test
|
||||
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
||||
// no backups available initially
|
||||
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(0, backend.getAvailableBackups()?.toList()?.size)
|
||||
|
||||
// prepare returned tokens requested when initializing device
|
||||
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
||||
|
||||
// start new restore set and initialize device afterwards
|
||||
storagePlugin.startNewRestoreSet(token)
|
||||
storagePlugin.initializeDevice()
|
||||
|
||||
// write metadata (needed for backup to be recognized)
|
||||
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA)
|
||||
backend.save(LegacyAppBackupFile.Metadata(token))
|
||||
.writeAndClose(getRandomByteArray())
|
||||
|
||||
// one backup available now
|
||||
assertEquals(1, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(1, backend.getAvailableBackups()?.toList()?.size)
|
||||
|
||||
// initializing again (with another restore set) does add a restore set
|
||||
storagePlugin.startNewRestoreSet(token + 1)
|
||||
storagePlugin.initializeDevice()
|
||||
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
|
||||
backend.save(LegacyAppBackupFile.Metadata(token + 1))
|
||||
.writeAndClose(getRandomByteArray())
|
||||
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
|
||||
|
||||
// initializing again (without new restore set) doesn't change number of restore sets
|
||||
storagePlugin.initializeDevice()
|
||||
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
|
||||
backend.save(LegacyAppBackupFile.Metadata(token + 1))
|
||||
.writeAndClose(getRandomByteArray())
|
||||
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
|
||||
every { mockedSettingsManager.getToken() } returns token
|
||||
|
||||
storagePlugin.startNewRestoreSet(token)
|
||||
storagePlugin.initializeDevice()
|
||||
|
||||
// write metadata
|
||||
val metadata = getRandomByteArray()
|
||||
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
|
||||
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
|
||||
|
||||
// get available backups, expect only one with our token and no error
|
||||
var availableBackups = storagePlugin.getAvailableBackups()?.toList()
|
||||
var availableBackups = backend.getAvailableBackups()?.toList()
|
||||
check(availableBackups != null)
|
||||
assertEquals(1, availableBackups.size)
|
||||
assertEquals(token, availableBackups[0].token)
|
||||
|
@ -142,9 +133,8 @@ class PluginTest : KoinComponent {
|
|||
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
|
||||
|
||||
// initializing again (without changing storage) keeps restore set with same token
|
||||
storagePlugin.initializeDevice()
|
||||
storagePlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
|
||||
availableBackups = storagePlugin.getAvailableBackups()?.toList()
|
||||
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
|
||||
availableBackups = backend.getAvailableBackups()?.toList()
|
||||
check(availableBackups != null)
|
||||
assertEquals(1, availableBackups.size)
|
||||
assertEquals(token, availableBackups[0].token)
|
||||
|
@ -161,7 +151,8 @@ class PluginTest : KoinComponent {
|
|||
|
||||
// write random bytes as APK
|
||||
val apk1 = getRandomByteArray(1337 * 1024)
|
||||
storagePlugin.getOutputStream(token, "${packageInfo.packageName}.apk").writeAndClose(apk1)
|
||||
backend.save(LegacyAppBackupFile.Blob(token, "${packageInfo.packageName}.apk"))
|
||||
.writeAndClose(apk1)
|
||||
|
||||
// assert that read APK bytes match what was written
|
||||
assertReadEquals(
|
||||
|
@ -173,7 +164,7 @@ class PluginTest : KoinComponent {
|
|||
val suffix2 = getRandomBase64(23)
|
||||
val apk2 = getRandomByteArray(23 * 1024 * 1024)
|
||||
|
||||
storagePlugin.getOutputStream(token, "${packageInfo2.packageName}$suffix2.apk")
|
||||
backend.save(LegacyAppBackupFile.Blob(token, "${packageInfo2.packageName}$suffix2.apk"))
|
||||
.writeAndClose(apk2)
|
||||
|
||||
// assert that read APK bytes match what was written
|
||||
|
@ -193,26 +184,25 @@ class PluginTest : KoinComponent {
|
|||
|
||||
// write full backup data
|
||||
val data = getRandomByteArray(5 * 1024 * 1024)
|
||||
storagePlugin.getOutputStream(token, name1).writeAndClose(data)
|
||||
backend.save(LegacyAppBackupFile.Blob(token, name1)).writeAndClose(data)
|
||||
|
||||
// restore data matches backed up data
|
||||
assertReadEquals(data, storagePlugin.getInputStream(token, name1))
|
||||
assertReadEquals(data, backend.load(LegacyAppBackupFile.Blob(token, name1)))
|
||||
|
||||
// write and check data for second package
|
||||
val data2 = getRandomByteArray(5 * 1024 * 1024)
|
||||
storagePlugin.getOutputStream(token, name2).writeAndClose(data2)
|
||||
assertReadEquals(data2, storagePlugin.getInputStream(token, name2))
|
||||
backend.save(LegacyAppBackupFile.Blob(token, name2)).writeAndClose(data2)
|
||||
assertReadEquals(data2, backend.load(LegacyAppBackupFile.Blob(token, name2)))
|
||||
|
||||
// remove data of first package again and ensure that no more data is found
|
||||
storagePlugin.removeData(token, name1)
|
||||
backend.remove(LegacyAppBackupFile.Blob(token, name1))
|
||||
|
||||
// ensure that it gets deleted as well
|
||||
storagePlugin.removeData(token, name2)
|
||||
backend.remove(LegacyAppBackupFile.Blob(token, name2))
|
||||
}
|
||||
|
||||
private fun initStorage(token: Long) = runBlocking {
|
||||
every { mockedSettingsManager.getToken() } returns token
|
||||
storagePlugin.initializeDevice()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
|
@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.BackendTest
|
||||
import org.calyxos.seedvault.core.backends.saf.SafBackend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafConfig
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
@ -27,15 +27,15 @@ class SafBackendTest : BackendTest(), KoinComponent {
|
|||
private val settingsManager by inject<SettingsManager>()
|
||||
override val plugin: Backend
|
||||
get() {
|
||||
val safStorage = settingsManager.getSafStorage() ?: error("No SAF storage")
|
||||
val safConfig = SafConfig(
|
||||
val safStorage = settingsManager.getSafProperties() ?: error("No SAF storage")
|
||||
val safProperties = SafProperties(
|
||||
config = safStorage.config,
|
||||
name = safStorage.name,
|
||||
isUsb = safStorage.isUsb,
|
||||
requiresNetwork = safStorage.requiresNetwork,
|
||||
rootId = safStorage.rootId,
|
||||
)
|
||||
return SafBackend(context, safConfig, ".SeedvaultTest")
|
||||
return SafBackend(context, safProperties, ".SeedvaultTest")
|
||||
}
|
||||
|
||||
@Test
|
|
@ -23,7 +23,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
|
|||
confirmCode()
|
||||
}
|
||||
|
||||
if (settingsManager.getSafStorage() == null) {
|
||||
if (settingsManager.getSafProperties() == null) {
|
||||
chooseStorageLocation()
|
||||
} else {
|
||||
changeBackupLocation()
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.content.pm.PackageInfo
|
|||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.settings.AppStatus
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import io.mockk.every
|
||||
|
@ -30,9 +30,9 @@ class PackageServiceTest : KoinComponent {
|
|||
|
||||
private val settingsManager: SettingsManager by inject()
|
||||
|
||||
private val storagePluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
|
||||
private val backend: Backend get() = storagePluginManager.backend
|
||||
private val backend: Backend get() = backendManager.backend
|
||||
|
||||
@Test
|
||||
fun testNotAllowedPackages() {
|
||||
|
|
|
@ -24,9 +24,9 @@ import com.stevesoltys.seedvault.crypto.cryptoModule
|
|||
import com.stevesoltys.seedvault.header.headerModule
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
||||
import com.stevesoltys.seedvault.plugins.webdav.storagePluginModuleWebDav
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
|
||||
import com.stevesoltys.seedvault.backend.webdav.storagePluginModuleWebDav
|
||||
import com.stevesoltys.seedvault.restore.install.installModule
|
||||
import com.stevesoltys.seedvault.restore.restoreUiModule
|
||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||
|
@ -42,6 +42,7 @@ import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
|||
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||
import com.stevesoltys.seedvault.worker.workerModule
|
||||
import org.calyxos.seedvault.core.backends.BackendFactory
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
|
@ -61,7 +62,8 @@ open class App : Application() {
|
|||
private val appModule = module {
|
||||
single { SettingsManager(this@App) }
|
||||
single { BackupNotificationManager(this@App) }
|
||||
single { StoragePluginManager(this@App, get(), get(), get()) }
|
||||
single { BackendManager(this@App, get(), get()) }
|
||||
single { BackendFactory(this@App) }
|
||||
single { BackupStateManager(this@App) }
|
||||
single { Clock() }
|
||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||
|
@ -72,7 +74,7 @@ open class App : Application() {
|
|||
app = this@App,
|
||||
settingsManager = get(),
|
||||
keyManager = get(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
metadataManager = get(),
|
||||
appListRetriever = get(),
|
||||
storageBackup = get(),
|
||||
|
@ -91,7 +93,7 @@ open class App : Application() {
|
|||
safHandler = get(),
|
||||
webDavHandler = get(),
|
||||
settingsManager = get(),
|
||||
storagePluginManager = get(),
|
||||
backendManager = get(),
|
||||
)
|
||||
}
|
||||
viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) }
|
||||
|
@ -146,7 +148,7 @@ open class App : Application() {
|
|||
private val settingsManager: SettingsManager by inject()
|
||||
private val metadataManager: MetadataManager by inject()
|
||||
private val backupManager: IBackupManager by inject()
|
||||
private val pluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
private val backupStateManager: BackupStateManager by inject()
|
||||
|
||||
/**
|
||||
|
@ -170,13 +172,13 @@ open class App : Application() {
|
|||
protected open fun migrateToOwnScheduling() {
|
||||
if (!backupStateManager.isFrameworkSchedulingEnabled) { // already on own scheduling
|
||||
// fix things for removable drive users who had a job scheduled here before
|
||||
if (pluginManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
|
||||
if (backendManager.isOnRemovableDrive) AppBackupWorker.unschedule(applicationContext)
|
||||
return
|
||||
}
|
||||
|
||||
if (backupManager.currentTransport == TRANSPORT_ID) {
|
||||
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
|
||||
if (backupManager.isBackupEnabled && !pluginManager.isOnRemovableDrive) {
|
||||
if (backupManager.isBackupEnabled && !backendManager.isOnRemovableDrive) {
|
||||
AppBackupWorker.schedule(applicationContext, settingsManager, UPDATE)
|
||||
}
|
||||
// cancel old D2D worker
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins
|
||||
package com.stevesoltys.seedvault.backend
|
||||
|
||||
import android.util.Log
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
suspend fun Backend.getMetadataOutputStream(token: Long): OutputStream {
|
||||
|
@ -35,3 +38,16 @@ suspend fun Backend.getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Exception.isOutOfSpace(): Boolean {
|
||||
return when (this) {
|
||||
is IOException -> message?.contains("No space left on device") == true ||
|
||||
(cause as? HttpException)?.code == 507
|
||||
|
||||
is HttpException -> code == 507
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)
|
|
@ -3,30 +3,28 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins
|
||||
package com.stevesoltys.seedvault.backend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.getStorageContext
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafFactory
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavFactory
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.settings.StoragePluginType
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.BackendFactory
|
||||
import org.calyxos.seedvault.core.backends.BackendProperties
|
||||
import org.calyxos.seedvault.core.backends.saf.SafBackend
|
||||
|
||||
class StoragePluginManager(
|
||||
class BackendManager(
|
||||
private val context: Context,
|
||||
private val settingsManager: SettingsManager,
|
||||
safFactory: SafFactory,
|
||||
webDavFactory: WebDavFactory,
|
||||
backendFactory: BackendFactory,
|
||||
) {
|
||||
|
||||
private var mBackend: Backend?
|
||||
private var mFilesPlugin: org.calyxos.backup.storage.api.StoragePlugin?
|
||||
private var mStorageProperties: StorageProperties<*>?
|
||||
private var mBackendProperties: BackendProperties<*>?
|
||||
|
||||
val backend: Backend
|
||||
@Synchronized
|
||||
|
@ -34,48 +32,39 @@ class StoragePluginManager(
|
|||
return mBackend ?: error("App plugin was loaded, but still null")
|
||||
}
|
||||
|
||||
val filesPlugin: org.calyxos.backup.storage.api.StoragePlugin
|
||||
val backendProperties: BackendProperties<*>?
|
||||
@Synchronized
|
||||
get() {
|
||||
return mFilesPlugin ?: error("Files plugin was loaded, but still null")
|
||||
return mBackendProperties
|
||||
}
|
||||
|
||||
val storageProperties: StorageProperties<*>?
|
||||
@Synchronized
|
||||
get() {
|
||||
return mStorageProperties
|
||||
}
|
||||
val isOnRemovableDrive: Boolean get() = storageProperties?.isUsb == true
|
||||
val isOnRemovableDrive: Boolean get() = backendProperties?.isUsb == true
|
||||
|
||||
init {
|
||||
when (settingsManager.storagePluginType) {
|
||||
StoragePluginType.SAF -> {
|
||||
val safStorage = settingsManager.getSafStorage() ?: error("No SAF storage saved")
|
||||
mBackend = safFactory.createBackend(safStorage)
|
||||
mFilesPlugin = safFactory.createFilesStoragePlugin(safStorage)
|
||||
mStorageProperties = safStorage
|
||||
val safConfig = settingsManager.getSafProperties() ?: error("No SAF storage saved")
|
||||
mBackend = backendFactory.createSafBackend(safConfig)
|
||||
mBackendProperties = safConfig
|
||||
}
|
||||
|
||||
StoragePluginType.WEB_DAV -> {
|
||||
val webDavProperties =
|
||||
settingsManager.webDavProperties ?: error("No WebDAV config saved")
|
||||
mBackend = webDavFactory.createBackend(webDavProperties.config)
|
||||
mFilesPlugin = webDavFactory.createFilesStoragePlugin(webDavProperties.config)
|
||||
mStorageProperties = webDavProperties
|
||||
mBackend = backendFactory.createWebDavBackend(webDavProperties.config)
|
||||
mBackendProperties = webDavProperties
|
||||
}
|
||||
|
||||
null -> {
|
||||
mBackend = null
|
||||
mFilesPlugin = null
|
||||
mStorageProperties = null
|
||||
mBackendProperties = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isValidAppPluginSet(): Boolean {
|
||||
if (mBackend == null || mFilesPlugin == null) return false
|
||||
if (mBackend == null) return false
|
||||
if (mBackend is SafBackend) {
|
||||
val storage = settingsManager.getSafStorage() ?: return false
|
||||
val storage = settingsManager.getSafProperties() ?: return false
|
||||
if (storage.isUsb) return true
|
||||
return permitDiskReads {
|
||||
storage.getDocumentFile(context).isDirectory
|
||||
|
@ -85,20 +74,18 @@ class StoragePluginManager(
|
|||
}
|
||||
|
||||
/**
|
||||
* Changes the storage plugins and current [StorageProperties].
|
||||
* Changes the storage plugins and current [BackendProperties].
|
||||
*
|
||||
* IMPORTANT: Do no call this while current plugins are being used,
|
||||
* e.g. while backup/restore operation is still running.
|
||||
*/
|
||||
fun <T> changePlugins(
|
||||
storageProperties: StorageProperties<T>,
|
||||
backend: Backend,
|
||||
filesPlugin: org.calyxos.backup.storage.api.StoragePlugin,
|
||||
storageProperties: BackendProperties<T>,
|
||||
) {
|
||||
settingsManager.setStorageBackend(backend)
|
||||
mStorageProperties = storageProperties
|
||||
mBackend = backend
|
||||
mFilesPlugin = filesPlugin
|
||||
mBackendProperties = storageProperties
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +98,7 @@ class StoragePluginManager(
|
|||
*/
|
||||
@WorkerThread
|
||||
fun canDoBackupNow(): Boolean {
|
||||
val storage = storageProperties ?: return false
|
||||
val storage = backendProperties ?: return false
|
||||
return !isOnUnavailableUsb() &&
|
||||
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
|
||||
}
|
||||
|
@ -126,7 +113,7 @@ class StoragePluginManager(
|
|||
*/
|
||||
@WorkerThread
|
||||
fun isOnUnavailableUsb(): Boolean {
|
||||
val storage = storageProperties ?: return false
|
||||
val storage = backendProperties ?: return false
|
||||
val systemContext = context.getStorageContext { storage.isUsb }
|
||||
return storage.isUnavailableUsb(systemContext)
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins
|
||||
package com.stevesoltys.seedvault.backend
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import java.io.IOException
|
|
@ -3,13 +3,13 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
|
@ -3,15 +3,14 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val storagePluginModuleSaf = module {
|
||||
single { SafFactory(androidContext()) }
|
||||
single { SafHandler(androidContext(), get(), get(), get()) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
|
@ -19,8 +18,9 @@ val storagePluginModuleSaf = module {
|
|||
DocumentsProviderLegacyPlugin(
|
||||
context = androidContext(),
|
||||
storageGetter = {
|
||||
val safStorage = get<SettingsManager>().getSafStorage() ?: error("No SAF storage")
|
||||
DocumentsStorage(androidContext(), get(), safStorage)
|
||||
val safProperties = get<SettingsManager>().getSafProperties()
|
||||
?: error("No SAF storage")
|
||||
DocumentsStorage(androidContext(), safProperties)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
|
@ -20,34 +20,29 @@ import android.util.Log
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.getStorageContext
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.calyxos.seedvault.core.backends.saf.getTreeDocumentFile
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
|
||||
|
||||
@Deprecated("")
|
||||
const val DIRECTORY_FULL_BACKUP = "full"
|
||||
|
||||
@Deprecated("")
|
||||
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
||||
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
||||
const val FILE_NO_MEDIA = ".nomedia"
|
||||
const val MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private val TAG = DocumentsStorage::class.java.simpleName
|
||||
|
||||
internal class DocumentsStorage(
|
||||
private val appContext: Context,
|
||||
private val settingsManager: SettingsManager,
|
||||
internal val safStorage: SafStorage,
|
||||
internal val safStorage: SafProperties,
|
||||
) {
|
||||
|
||||
/**
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.USB_SERVICE
|
||||
|
@ -14,34 +14,42 @@ import android.net.Uri
|
|||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.isMassStorage
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption
|
||||
import org.calyxos.seedvault.core.backends.BackendFactory
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import java.io.IOException
|
||||
|
||||
private const val TAG = "SafHandler"
|
||||
|
||||
internal class SafHandler(
|
||||
private val context: Context,
|
||||
private val safFactory: SafFactory,
|
||||
private val backendFactory: BackendFactory,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val storagePluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
) {
|
||||
|
||||
fun onConfigReceived(uri: Uri, safOption: StorageOption.SafOption): SafStorage {
|
||||
fun onConfigReceived(uri: Uri, safOption: StorageOption.SafOption): SafProperties {
|
||||
// persist permission to access backup folder across reboots
|
||||
val takeFlags = FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
val name = if (safOption.isInternal()) {
|
||||
"${safOption.title} (${context.getString(R.string.settings_backup_location_internal)})"
|
||||
} else {
|
||||
safOption.title
|
||||
}
|
||||
return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork, safOption.rootId)
|
||||
return SafProperties(
|
||||
config = uri,
|
||||
name = if (safOption.isInternal()) {
|
||||
val brackets = context.getString(R.string.settings_backup_location_internal)
|
||||
"${safOption.title} ($brackets)"
|
||||
} else {
|
||||
safOption.title
|
||||
},
|
||||
isUsb = safOption.isUsb,
|
||||
requiresNetwork = safOption.requiresNetwork,
|
||||
rootId = safOption.rootId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,16 +58,16 @@ internal class SafHandler(
|
|||
*/
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
suspend fun hasAppBackup(safStorage: SafStorage): Boolean {
|
||||
val appPlugin = safFactory.createBackend(safStorage)
|
||||
suspend fun hasAppBackup(safProperties: SafProperties): Boolean {
|
||||
val appPlugin = backendFactory.createSafBackend(safProperties)
|
||||
val backups = appPlugin.getAvailableBackups()
|
||||
return backups != null && backups.iterator().hasNext()
|
||||
}
|
||||
|
||||
fun save(safStorage: SafStorage) {
|
||||
settingsManager.setSafStorage(safStorage)
|
||||
fun save(safProperties: SafProperties) {
|
||||
settingsManager.setSafProperties(safProperties)
|
||||
|
||||
if (safStorage.isUsb) {
|
||||
if (safProperties.isUsb) {
|
||||
Log.d(TAG, "Selected storage is a removable USB device.")
|
||||
val wasSaved = saveUsbDevice()
|
||||
// reset stored flash drive, if we did not update it
|
||||
|
@ -67,7 +75,7 @@ internal class SafHandler(
|
|||
} else {
|
||||
settingsManager.setFlashDrive(null)
|
||||
}
|
||||
Log.d(TAG, "New storage location saved: ${safStorage.uri}")
|
||||
Log.d(TAG, "New storage location saved: ${safProperties.uri}")
|
||||
}
|
||||
|
||||
private fun saveUsbDevice(): Boolean {
|
||||
|
@ -84,11 +92,10 @@ internal class SafHandler(
|
|||
return false
|
||||
}
|
||||
|
||||
fun setPlugin(safStorage: SafStorage) {
|
||||
storagePluginManager.changePlugins(
|
||||
storageProperties = safStorage,
|
||||
backend = safFactory.createBackend(safStorage),
|
||||
filesPlugin = safFactory.createFilesStoragePlugin(safStorage),
|
||||
fun setPlugin(safProperties: SafProperties) {
|
||||
backendManager.changePlugins(
|
||||
backend = backendFactory.createSafBackend(safProperties),
|
||||
storageProperties = safProperties,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -14,7 +14,7 @@ import android.provider.DocumentsContract
|
|||
import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver.getIcon
|
||||
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver.getIcon
|
||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_DAVX5
|
||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_NEXTCLOUD
|
||||
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_ROUND_SYNC
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.Manifest.permission.MANAGE_DOCUMENTS
|
||||
import android.content.Context
|
|
@ -3,20 +3,22 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.webdav
|
||||
package com.stevesoltys.seedvault.backend.webdav
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.BackendFactory
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import java.io.IOException
|
||||
|
||||
internal sealed interface WebDavConfigState {
|
||||
|
@ -34,9 +36,9 @@ private val TAG = WebDavHandler::class.java.simpleName
|
|||
|
||||
internal class WebDavHandler(
|
||||
private val context: Context,
|
||||
private val webDavFactory: WebDavFactory,
|
||||
private val backendFactory: BackendFactory,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val storagePluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@ -54,7 +56,7 @@ internal class WebDavHandler(
|
|||
|
||||
suspend fun onConfigReceived(config: WebDavConfig) {
|
||||
mConfigState.value = WebDavConfigState.Checking
|
||||
val backend = webDavFactory.createBackend(config)
|
||||
val backend = backendFactory.createWebDavBackend(config)
|
||||
try {
|
||||
if (backend.test()) {
|
||||
val properties = createWebDavProperties(context, config)
|
||||
|
@ -88,10 +90,9 @@ internal class WebDavHandler(
|
|||
}
|
||||
|
||||
fun setPlugin(properties: WebDavProperties, backend: Backend) {
|
||||
storagePluginManager.changePlugins(
|
||||
storageProperties = properties,
|
||||
backendManager.changePlugins(
|
||||
backend = backend,
|
||||
filesPlugin = webDavFactory.createFilesStoragePlugin(properties.config),
|
||||
storageProperties = properties,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,12 +3,11 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.webdav
|
||||
package com.stevesoltys.seedvault.backend.webdav
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val storagePluginModuleWebDav = module {
|
||||
single { WebDavFactory(androidContext()) }
|
||||
single { WebDavHandler(androidContext(), get(), get(), get()) }
|
||||
}
|
|
@ -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}")
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -25,7 +25,7 @@ import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
|
|||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.restore.install.isInstalled
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||
|
@ -56,7 +56,7 @@ internal class AppDataRestoreManager(
|
|||
private val backupManager: IBackupManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val restoreCoordinator: RestoreCoordinator,
|
||||
private val storagePluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
) {
|
||||
|
||||
private var session: IRestoreSession? = null
|
||||
|
@ -101,7 +101,7 @@ internal class AppDataRestoreManager(
|
|||
return
|
||||
}
|
||||
|
||||
val providerPackageName = storagePluginManager.backend.providerPackageName
|
||||
val providerPackageName = backendManager.backend.providerPackageName
|
||||
val observer = RestoreObserver(
|
||||
restoreCoordinator = restoreCoordinator,
|
||||
restorableBackup = restorableBackup,
|
||||
|
|
|
@ -14,7 +14,7 @@ import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
|
|||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
|
||||
import com.stevesoltys.seedvault.ui.systemData
|
||||
import com.stevesoltys.seedvault.worker.IconManager
|
||||
|
@ -37,7 +37,7 @@ private val TAG = AppSelectionManager::class.simpleName
|
|||
|
||||
internal class AppSelectionManager(
|
||||
private val context: Context,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val iconManager: IconManager,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val workDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
|
@ -88,7 +88,7 @@ internal class AppSelectionManager(
|
|||
SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false)
|
||||
// download icons
|
||||
coroutineScope.launch(workDispatcher) {
|
||||
val backend = pluginManager.backend
|
||||
val backend = backendManager.backend
|
||||
val token = restorableBackup.token
|
||||
val packagesWithIcons = try {
|
||||
backend.load(LegacyAppBackupFile.IconsFile(token)).use {
|
||||
|
|
|
@ -23,7 +23,7 @@ val restoreUiModule = module {
|
|||
apkRestore = get(),
|
||||
iconManager = get(),
|
||||
storageBackup = get(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
fileSelectionManager = get(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import androidx.lifecycle.asLiveData
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||
|
@ -65,19 +65,19 @@ internal class RestoreViewModel(
|
|||
private val apkRestore: ApkRestore,
|
||||
private val iconManager: IconManager,
|
||||
storageBackup: StorageBackup,
|
||||
pluginManager: StoragePluginManager,
|
||||
backendManager: BackendManager,
|
||||
override val fileSelectionManager: FileSelectionManager,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager),
|
||||
RestorableBackupClickListener, SnapshotViewModel {
|
||||
|
||||
override val isRestoreOperation = true
|
||||
var isSetupWizard = false
|
||||
|
||||
private val appSelectionManager =
|
||||
AppSelectionManager(app, pluginManager, iconManager, viewModelScope)
|
||||
AppSelectionManager(app, backendManager, iconManager, viewModelScope)
|
||||
private val appDataRestoreManager = AppDataRestoreManager(
|
||||
app, backupManager, settingsManager, restoreCoordinator, pluginManager
|
||||
app, backupManager, settingsManager, restoreCoordinator, backendManager
|
||||
)
|
||||
|
||||
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
|
||||
|
|
|
@ -17,9 +17,8 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
|||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.RestoreService
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||
|
@ -34,6 +33,7 @@ import kotlinx.coroutines.TimeoutCancellationException
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
@ -45,7 +45,7 @@ internal class ApkRestore(
|
|||
private val context: Context,
|
||||
private val backupManager: IBackupManager,
|
||||
private val backupStateManager: BackupStateManager,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
@Suppress("Deprecation")
|
||||
private val legacyStoragePlugin: LegacyStoragePlugin,
|
||||
private val crypto: Crypto,
|
||||
|
@ -55,7 +55,7 @@ internal class ApkRestore(
|
|||
) {
|
||||
|
||||
private val pm = context.packageManager
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
|
||||
private val mInstallResult = MutableStateFlow(InstallResult())
|
||||
val installResult = mInstallResult.asStateFlow()
|
||||
|
@ -237,7 +237,7 @@ internal class ApkRestore(
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves APK splits from [StoragePlugin] and caches them locally.
|
||||
* Retrieves APK splits from [Backend] and caches them locally.
|
||||
*
|
||||
* @throws SecurityException if a split has an unexpected SHA-256 hash.
|
||||
* @return a list of all APKs that need to be installed
|
||||
|
@ -275,7 +275,7 @@ internal class ApkRestore(
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves an APK from the [StoragePlugin] and caches it locally
|
||||
* Retrieves an APK from the [Backend] and caches it locally
|
||||
* while calculating its SHA-256 hash.
|
||||
*
|
||||
* @return a [Pair] of the cached [File] and SHA-256 hash.
|
||||
|
|
|
@ -17,7 +17,7 @@ import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
|||
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.settings.preference.M3ListPreference
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
@ -27,7 +27,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
|
|||
|
||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||
private val settingsManager: SettingsManager by inject()
|
||||
private val storagePluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
permitDiskReads {
|
||||
|
@ -39,7 +39,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val storage = storagePluginManager.storageProperties
|
||||
val storage = backendManager.backendProperties
|
||||
if (storage?.isUsb == true) {
|
||||
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
|
||||
}
|
||||
|
|
|
@ -25,12 +25,12 @@ import androidx.work.WorkInfo
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.stevesoltys.seedvault.BackupStateManager
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.StorageProperties
|
||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||
import org.calyxos.seedvault.core.backends.BackendProperties
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -40,7 +40,7 @@ private val TAG = SettingsFragment::class.java.name
|
|||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||
private val storagePluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
private val backupStateManager: BackupStateManager by inject()
|
||||
private val backupManager: IBackupManager by inject()
|
||||
private val notificationManager: BackupNotificationManager by inject()
|
||||
|
@ -57,8 +57,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private var menuBackupNow: MenuItem? = null
|
||||
private var menuRestore: MenuItem? = null
|
||||
|
||||
private val storageProperties: StorageProperties<*>?
|
||||
get() = storagePluginManager.storageProperties
|
||||
private val backendProperties: BackendProperties<*>?
|
||||
get() = backendManager.backendProperties
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
permitDiskReads {
|
||||
|
@ -270,7 +270,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
activity?.contentResolver?.let {
|
||||
autoRestore.isChecked = backupStateManager.isAutoRestoreEnabled
|
||||
}
|
||||
val storage = this.storageProperties
|
||||
val storage = this.backendProperties
|
||||
if (storage?.isUsb == true) {
|
||||
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
||||
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
||||
|
@ -282,7 +282,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private fun setBackupLocationSummary() {
|
||||
// get name of storage location
|
||||
backupLocation.summary =
|
||||
storageProperties?.name ?: getString(R.string.settings_backup_location_none)
|
||||
backendProperties?.name ?: getString(R.string.settings_backup_location_none)
|
||||
}
|
||||
|
||||
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
|
||||
|
@ -301,7 +301,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
* says that nothing is scheduled which can happen when backup destination is on flash drive.
|
||||
*/
|
||||
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
|
||||
if (storageProperties?.isUsb == true) {
|
||||
if (backendProperties?.isUsb == true) {
|
||||
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -11,15 +11,15 @@ import android.hardware.usb.UsbDevice
|
|||
import android.net.Uri
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler.Companion.createWebDavProperties
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler.Companion.createWebDavProperties
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafBackend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavBackend
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import java.util.concurrent.ConcurrentSkipListSet
|
||||
|
||||
internal const val PREF_KEY_TOKEN = "token"
|
||||
|
@ -139,17 +139,17 @@ class SettingsManager(private val context: Context) {
|
|||
.apply()
|
||||
}
|
||||
|
||||
fun setSafStorage(safStorage: SafStorage) {
|
||||
fun setSafProperties(safProperties: SafProperties) {
|
||||
prefs.edit()
|
||||
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString())
|
||||
.putString(PREF_KEY_STORAGE_ROOT_ID, safStorage.rootId)
|
||||
.putString(PREF_KEY_STORAGE_NAME, safStorage.name)
|
||||
.putBoolean(PREF_KEY_STORAGE_IS_USB, safStorage.isUsb)
|
||||
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safStorage.requiresNetwork)
|
||||
.putString(PREF_KEY_STORAGE_URI, safProperties.uri.toString())
|
||||
.putString(PREF_KEY_STORAGE_ROOT_ID, safProperties.rootId)
|
||||
.putString(PREF_KEY_STORAGE_NAME, safProperties.name)
|
||||
.putBoolean(PREF_KEY_STORAGE_IS_USB, safProperties.isUsb)
|
||||
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, safProperties.requiresNetwork)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getSafStorage(): SafStorage? {
|
||||
fun getSafProperties(): SafProperties? {
|
||||
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
|
||||
val uri = Uri.parse(uriStr)
|
||||
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null)
|
||||
|
@ -157,7 +157,7 @@ class SettingsManager(private val context: Context) {
|
|||
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
|
||||
val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false)
|
||||
val rootId = prefs.getString(PREF_KEY_STORAGE_ROOT_ID, null)
|
||||
return SafStorage(uri, name, isUsb, requiresNetwork, rootId)
|
||||
return SafProperties(uri, name, isUsb, requiresNetwork, rootId)
|
||||
}
|
||||
|
||||
fun setFlashDrive(usb: FlashDrive?) {
|
||||
|
|
|
@ -40,8 +40,7 @@ import com.stevesoltys.seedvault.R
|
|||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||
import com.stevesoltys.seedvault.storage.StorageBackupService
|
||||
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||
|
@ -59,6 +58,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import org.calyxos.backup.storage.backup.BackupJobService
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import java.io.IOException
|
||||
import java.lang.Runtime.getRuntime
|
||||
import java.util.concurrent.TimeUnit.HOURS
|
||||
|
@ -70,14 +70,14 @@ internal class SettingsViewModel(
|
|||
app: Application,
|
||||
settingsManager: SettingsManager,
|
||||
keyManager: KeyManager,
|
||||
pluginManager: StoragePluginManager,
|
||||
backendManager: BackendManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val appListRetriever: AppListRetriever,
|
||||
private val storageBackup: StorageBackup,
|
||||
private val backupManager: IBackupManager,
|
||||
private val backupInitializer: BackupInitializer,
|
||||
backupStateManager: BackupStateManager,
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager, backendManager) {
|
||||
|
||||
private val contentResolver = app.contentResolver
|
||||
private val connectivityManager: ConnectivityManager? =
|
||||
|
@ -158,7 +158,7 @@ internal class SettingsViewModel(
|
|||
}
|
||||
|
||||
override fun onStorageLocationChanged() {
|
||||
val storage = pluginManager.storageProperties ?: return
|
||||
val storage = backendManager.backendProperties ?: return
|
||||
|
||||
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb})")
|
||||
if (storage.isUsb) {
|
||||
|
@ -177,33 +177,33 @@ internal class SettingsViewModel(
|
|||
private fun onBackupRunningStateChanged() {
|
||||
if (isBackupRunning.value) mBackupPossible.postValue(false)
|
||||
else viewModelScope.launch(Dispatchers.IO) {
|
||||
val canDo = !isBackupRunning.value && !pluginManager.isOnUnavailableUsb()
|
||||
val canDo = !isBackupRunning.value && !backendManager.isOnUnavailableUsb()
|
||||
mBackupPossible.postValue(canDo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStoragePropertiesChanged() {
|
||||
val storage = pluginManager.storageProperties ?: return
|
||||
val properties = backendManager.backendProperties ?: return
|
||||
|
||||
Log.d(TAG, "onStoragePropertiesChanged")
|
||||
if (storage is SafStorage) {
|
||||
if (properties is SafProperties) {
|
||||
// register storage observer
|
||||
try {
|
||||
contentResolver.unregisterContentObserver(storageObserver)
|
||||
contentResolver.registerContentObserver(storage.uri, false, storageObserver)
|
||||
contentResolver.registerContentObserver(properties.uri, false, storageObserver)
|
||||
} catch (e: SecurityException) {
|
||||
// This can happen if the app providing the storage was uninstalled.
|
||||
// validLocationIsSet() gets called elsewhere
|
||||
// and prompts for a new storage location.
|
||||
Log.e(TAG, "Error registering content observer for ${storage.uri}", e)
|
||||
Log.e(TAG, "Error registering content observer for ${properties.uri}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// register network observer if needed
|
||||
if (networkCallback.registered && !storage.requiresNetwork) {
|
||||
if (networkCallback.registered && !properties.requiresNetwork) {
|
||||
connectivityManager?.unregisterNetworkCallback(networkCallback)
|
||||
networkCallback.registered = false
|
||||
} else if (!networkCallback.registered && storage.requiresNetwork) {
|
||||
} else if (!networkCallback.registered && properties.requiresNetwork) {
|
||||
// TODO we may want to warn the user when they start a backup on a metered connection
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
|
@ -232,7 +232,7 @@ internal class SettingsViewModel(
|
|||
i.putExtra(EXTRA_START_APP_BACKUP, isAppBackupEnabled)
|
||||
startForegroundService(app, i)
|
||||
} else if (isAppBackupEnabled) {
|
||||
AppBackupWorker.scheduleNow(app, reschedule = !pluginManager.isOnRemovableDrive)
|
||||
AppBackupWorker.scheduleNow(app, reschedule = !backendManager.isOnRemovableDrive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,14 +313,14 @@ internal class SettingsViewModel(
|
|||
fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) {
|
||||
// disable framework scheduling, because another transport may have enabled it
|
||||
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
|
||||
if (!pluginManager.isOnRemovableDrive && backupManager.isBackupEnabled) {
|
||||
if (!backendManager.isOnRemovableDrive && backupManager.isBackupEnabled) {
|
||||
AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleFilesBackup() {
|
||||
if (!pluginManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) {
|
||||
val requiresNetwork = pluginManager.storageProperties?.requiresNetwork == true
|
||||
if (!backendManager.isOnRemovableDrive && settingsManager.isStorageBackupEnabled()) {
|
||||
val requiresNetwork = backendManager.backendProperties?.requiresNetwork == true
|
||||
BackupJobService.scheduleJob(
|
||||
context = app,
|
||||
jobServiceClass = StorageBackupJobService::class.java,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
package com.stevesoltys.seedvault.storage
|
||||
|
||||
import android.content.Intent
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
@ -44,7 +44,7 @@ internal class StorageBackupService : BackupService() {
|
|||
}
|
||||
|
||||
override val storageBackup: StorageBackup by inject()
|
||||
private val storagePluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
|
||||
// use lazy delegate because context isn't available during construction time
|
||||
override val backupObserver: BackupObserver by lazy {
|
||||
|
@ -63,7 +63,7 @@ internal class StorageBackupService : BackupService() {
|
|||
|
||||
override fun onBackupFinished(intent: Intent, success: Boolean) {
|
||||
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
|
||||
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false
|
||||
val isUsb = backendManager.backendProperties?.isUsb ?: false
|
||||
AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
package com.stevesoltys.seedvault.storage
|
||||
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import org.koin.dsl.module
|
||||
|
||||
val storageModule = module {
|
||||
single { StorageBackup(get(), { get<StoragePluginManager>().backend }, get<KeyManager>()) }
|
||||
single { StorageBackup(get(), { get<BackendManager>().backend }, get<KeyManager>()) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,15 +29,12 @@ import com.stevesoltys.seedvault.metadata.PackageState
|
|||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream
|
||||
import com.stevesoltys.seedvault.plugins.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getMetadataOutputStream
|
||||
import com.stevesoltys.seedvault.backend.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
import java.util.concurrent.TimeUnit.HOURS
|
||||
|
||||
|
@ -65,7 +62,7 @@ private class CoordinatorState(
|
|||
@WorkerThread
|
||||
internal class BackupCoordinator(
|
||||
private val context: Context,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val kv: KVBackup,
|
||||
private val full: FullBackup,
|
||||
private val clock: Clock,
|
||||
|
@ -75,7 +72,7 @@ internal class BackupCoordinator(
|
|||
private val nm: BackupNotificationManager,
|
||||
) {
|
||||
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
private val state = CoordinatorState(
|
||||
calledInitialize = false,
|
||||
calledClearBackupData = false,
|
||||
|
@ -132,7 +129,7 @@ internal class BackupCoordinator(
|
|||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing device", e)
|
||||
// Show error notification if we needed init or were ready for backups
|
||||
if (metadataManager.requiresInit || pluginManager.canDoBackupNow()) nm.onBackupError()
|
||||
if (metadataManager.requiresInit || backendManager.canDoBackupNow()) nm.onBackupError()
|
||||
TRANSPORT_ERROR
|
||||
}
|
||||
|
||||
|
@ -370,7 +367,7 @@ internal class BackupCoordinator(
|
|||
if (result == TRANSPORT_OK) {
|
||||
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
|
||||
// call onPackageBackedUp for @pm@ only if we can do backups right now
|
||||
if (isNormalBackup || pluginManager.canDoBackupNow()) {
|
||||
if (isNormalBackup || backendManager.canDoBackupNow()) {
|
||||
try {
|
||||
onPackageBackedUp(packageInfo, BackupType.KV, size)
|
||||
} catch (e: Exception) {
|
||||
|
@ -431,7 +428,7 @@ internal class BackupCoordinator(
|
|||
val longBackoff = DAYS.toMillis(30)
|
||||
|
||||
// back off if there's no storage set
|
||||
val storage = pluginManager.storageProperties ?: return longBackoff
|
||||
val storage = backendManager.backendProperties ?: return longBackoff
|
||||
return when {
|
||||
// back off if storage is removable and not available right now
|
||||
storage.isUnavailableUsb(context) -> longBackoff
|
||||
|
@ -444,12 +441,4 @@ internal class BackupCoordinator(
|
|||
else -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
|
||||
token: Long? = null,
|
||||
): OutputStream {
|
||||
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
||||
return getOutputStream(t, FILE_BACKUP_METADATA)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@ val backupModule = module {
|
|||
context = androidContext(),
|
||||
backupManager = get(),
|
||||
settingsManager = get(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
)
|
||||
}
|
||||
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
|
||||
single {
|
||||
KVBackup(
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
settingsManager = get(),
|
||||
nm = get(),
|
||||
inputFactory = get(),
|
||||
|
@ -32,7 +32,7 @@ val backupModule = module {
|
|||
}
|
||||
single {
|
||||
FullBackup(
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
settingsManager = get(),
|
||||
nm = get(),
|
||||
inputFactory = get(),
|
||||
|
@ -42,7 +42,7 @@ val backupModule = module {
|
|||
single {
|
||||
BackupCoordinator(
|
||||
context = androidContext(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
kv = get(),
|
||||
full = get(),
|
||||
clock = get(),
|
||||
|
|
|
@ -16,8 +16,8 @@ import android.util.Log
|
|||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.getADForFull
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
|
@ -47,14 +47,14 @@ private val TAG = FullBackup::class.java.simpleName
|
|||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
internal class FullBackup(
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val nm: BackupNotificationManager,
|
||||
private val inputFactory: InputFactory,
|
||||
private val crypto: Crypto,
|
||||
) {
|
||||
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
private var state: FullBackupState? = null
|
||||
|
||||
fun hasState() = state != null
|
||||
|
|
|
@ -18,8 +18,8 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
|||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.getADForKV
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
|
@ -40,7 +40,7 @@ const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
|
|||
private val TAG = KVBackup::class.java.simpleName
|
||||
|
||||
internal class KVBackup(
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val nm: BackupNotificationManager,
|
||||
private val inputFactory: InputFactory,
|
||||
|
@ -48,7 +48,7 @@ internal class KVBackup(
|
|||
private val dbManager: KvDbManager,
|
||||
) {
|
||||
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
private var state: KVBackupState? = null
|
||||
|
||||
fun hasState() = state != null
|
||||
|
@ -147,7 +147,7 @@ internal class KVBackup(
|
|||
// K/V backups (typically starting with package manager metadata - @pm@)
|
||||
// are scheduled with JobInfo.Builder#setOverrideDeadline()
|
||||
// and thus do not respect backoff.
|
||||
pluginManager.canDoBackupNow()
|
||||
backendManager.canDoBackupNow()
|
||||
} else {
|
||||
// all other packages always need upload
|
||||
true
|
||||
|
|
|
@ -27,7 +27,7 @@ import android.util.Log
|
|||
import android.util.Log.INFO
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
|
||||
|
@ -43,12 +43,12 @@ internal class PackageService(
|
|||
private val context: Context,
|
||||
private val backupManager: IBackupManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
) {
|
||||
|
||||
private val packageManager: PackageManager = context.packageManager
|
||||
private val myUserId = UserHandle.myUserId()
|
||||
private val backend: Backend get() = pluginManager.backend
|
||||
private val backend: Backend get() = backendManager.backend
|
||||
|
||||
val eligiblePackages: List<String>
|
||||
@WorkerThread
|
||||
|
|
|
@ -17,8 +17,8 @@ import com.stevesoltys.seedvault.header.HeaderReader
|
|||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.getADForFull
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.EOFException
|
||||
|
@ -39,7 +39,7 @@ private class FullRestoreState(
|
|||
private val TAG = FullRestore::class.java.simpleName
|
||||
|
||||
internal class FullRestore(
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
@Suppress("Deprecation")
|
||||
private val legacyPlugin: LegacyStoragePlugin,
|
||||
private val outputFactory: OutputFactory,
|
||||
|
@ -47,7 +47,7 @@ internal class FullRestore(
|
|||
private val crypto: Crypto,
|
||||
) {
|
||||
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
private var state: FullRestoreState? = null
|
||||
|
||||
fun hasState() = state != null
|
||||
|
|
|
@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.header.HeaderReader
|
|||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.getADForKV
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.transport.backup.KVDb
|
||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
|
@ -45,7 +45,7 @@ private class KVRestoreState(
|
|||
private val TAG = KVRestore::class.java.simpleName
|
||||
|
||||
internal class KVRestore(
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
@Suppress("Deprecation")
|
||||
private val legacyPlugin: LegacyStoragePlugin,
|
||||
private val outputFactory: OutputFactory,
|
||||
|
@ -54,7 +54,7 @@ internal class KVRestore(
|
|||
private val dbManager: KvDbManager,
|
||||
) {
|
||||
|
||||
private val backend get() = pluginManager.backend
|
||||
private val backend get() = backendManager.backend
|
||||
private var state: KVRestoreState? = null
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,8 +25,8 @@ import com.stevesoltys.seedvault.metadata.BackupType
|
|||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
||||
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
||||
|
@ -62,13 +62,13 @@ internal class RestoreCoordinator(
|
|||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val notificationManager: BackupNotificationManager,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val kv: KVRestore,
|
||||
private val full: FullRestore,
|
||||
private val metadataReader: MetadataReader,
|
||||
) {
|
||||
|
||||
private val backend: Backend get() = pluginManager.backend
|
||||
private val backend: Backend get() = backendManager.backend
|
||||
private var state: RestoreCoordinatorState? = null
|
||||
private var backupMetadata: BackupMetadata? = null
|
||||
private val failedPackages = ArrayList<String>()
|
||||
|
@ -176,7 +176,7 @@ internal class RestoreCoordinator(
|
|||
// check if we even have a backup of that app
|
||||
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
|
||||
// remind user to plug in storage device
|
||||
val storageName = pluginManager.storageProperties?.name
|
||||
val storageName = backendManager.backendProperties?.name
|
||||
?: context.getString(R.string.settings_backup_location_none)
|
||||
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||
pmPackageName,
|
||||
|
@ -359,7 +359,7 @@ internal class RestoreCoordinator(
|
|||
fun isFailedPackage(packageName: String) = packageName in failedPackages
|
||||
|
||||
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
||||
val storage = pluginManager.storageProperties ?: return false
|
||||
val storage = backendManager.backendProperties ?: return false
|
||||
return storage.isUnavailableUsb(context)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,14 +8,14 @@ package com.stevesoltys.seedvault.ui
|
|||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
||||
abstract class RequireProvisioningViewModel(
|
||||
protected val app: Application,
|
||||
protected val settingsManager: SettingsManager,
|
||||
protected val keyManager: KeyManager,
|
||||
protected val pluginManager: StoragePluginManager,
|
||||
protected val backendManager: BackendManager,
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
abstract val isRestoreOperation: Boolean
|
||||
|
@ -24,7 +24,7 @@ abstract class RequireProvisioningViewModel(
|
|||
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
||||
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
||||
|
||||
internal fun validLocationIsSet() = pluginManager.isValidAppPluginSet()
|
||||
internal fun validLocationIsSet() = backendManager.isValidAppPluginSet()
|
||||
|
||||
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
|
||||
|
||||
|
|
|
@ -13,11 +13,10 @@ import android.util.Log
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
||||
|
@ -27,6 +26,7 @@ import kotlinx.coroutines.launch
|
|||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import org.calyxos.backup.storage.backup.BackupJobService
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -40,15 +40,15 @@ internal class BackupStorageViewModel(
|
|||
safHandler: SafHandler,
|
||||
webDavHandler: WebDavHandler,
|
||||
settingsManager: SettingsManager,
|
||||
storagePluginManager: StoragePluginManager,
|
||||
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
|
||||
backendManager: BackendManager,
|
||||
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, backendManager) {
|
||||
|
||||
override val isRestoreOperation = false
|
||||
|
||||
override fun onSafUriSet(safStorage: SafStorage) {
|
||||
safHandler.save(safStorage)
|
||||
safHandler.setPlugin(safStorage)
|
||||
if (safStorage.isUsb) {
|
||||
override fun onSafUriSet(safProperties: SafProperties) {
|
||||
safHandler.save(safProperties)
|
||||
safHandler.setPlugin(safProperties)
|
||||
if (safProperties.isUsb) {
|
||||
// disable storage backup if new storage is on USB
|
||||
cancelBackupWorkers()
|
||||
} else {
|
||||
|
@ -56,7 +56,7 @@ internal class BackupStorageViewModel(
|
|||
// also to update the network requirement of the new storage
|
||||
scheduleBackupWorkers()
|
||||
}
|
||||
onStorageLocationSet(safStorage.isUsb)
|
||||
onStorageLocationSet(safProperties.isUsb)
|
||||
}
|
||||
|
||||
override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) {
|
||||
|
@ -100,7 +100,7 @@ internal class BackupStorageViewModel(
|
|||
}
|
||||
|
||||
private fun scheduleBackupWorkers() {
|
||||
val storage = storagePluginManager.storageProperties ?: error("no storage available")
|
||||
val storage = backendManager.backendProperties ?: error("no storage available")
|
||||
// disable framework scheduling, because another transport may have enabled it
|
||||
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
|
||||
if (!storage.isUsb) {
|
||||
|
|
|
@ -9,16 +9,16 @@ import android.app.Application
|
|||
import android.util.Log
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = RestoreStorageViewModel::class.java.simpleName
|
||||
|
@ -28,25 +28,25 @@ internal class RestoreStorageViewModel(
|
|||
safHandler: SafHandler,
|
||||
webDavHandler: WebDavHandler,
|
||||
settingsManager: SettingsManager,
|
||||
storagePluginManager: StoragePluginManager,
|
||||
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
|
||||
backendManager: BackendManager,
|
||||
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, backendManager) {
|
||||
|
||||
override val isRestoreOperation = true
|
||||
|
||||
override fun onSafUriSet(safStorage: SafStorage) {
|
||||
override fun onSafUriSet(safProperties: SafProperties) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val hasBackup = try {
|
||||
safHandler.hasAppBackup(safStorage)
|
||||
safHandler.hasAppBackup(safProperties)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error reading URI: ${safStorage.uri}", e)
|
||||
Log.e(TAG, "Error reading URI: ${safProperties.uri}", e)
|
||||
false
|
||||
}
|
||||
if (hasBackup) {
|
||||
safHandler.save(safStorage)
|
||||
safHandler.setPlugin(safStorage)
|
||||
safHandler.save(safProperties)
|
||||
safHandler.setPlugin(safProperties)
|
||||
mLocationChecked.postEvent(LocationResult())
|
||||
} else {
|
||||
Log.w(TAG, "Location was rejected: ${safStorage.uri}")
|
||||
Log.w(TAG, "Location was rejected: ${safProperties.uri}")
|
||||
|
||||
// notify the UI that the location was invalid
|
||||
val errorMsg =
|
||||
|
|
|
@ -18,7 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTre
|
|||
import androidx.annotation.CallSuper
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver
|
||||
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver
|
||||
import com.stevesoltys.seedvault.ui.BackupActivity
|
||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_SETUP_WIZARD
|
||||
|
|
|
@ -18,8 +18,8 @@ import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
|||
import android.provider.DocumentsContract.buildRootsUri
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorageOptions
|
||||
import com.stevesoltys.seedvault.plugins.saf.StorageRootResolver
|
||||
import com.stevesoltys.seedvault.backend.saf.SafStorageOptions
|
||||
import com.stevesoltys.seedvault.backend.saf.StorageRootResolver
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
|
||||
private val TAG = StorageOptionFetcher::class.java.simpleName
|
||||
|
|
|
@ -13,11 +13,10 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.saf.SafHandler
|
||||
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||
|
@ -25,6 +24,7 @@ import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
|
||||
|
||||
internal abstract class StorageViewModel(
|
||||
|
@ -32,7 +32,7 @@ internal abstract class StorageViewModel(
|
|||
protected val safHandler: SafHandler,
|
||||
protected val webdavHandler: WebDavHandler,
|
||||
protected val settingsManager: SettingsManager,
|
||||
protected val storagePluginManager: StoragePluginManager,
|
||||
protected val backendManager: BackendManager,
|
||||
) : AndroidViewModel(app), RemovableStorageListener {
|
||||
|
||||
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
|
||||
|
@ -49,7 +49,7 @@ internal abstract class StorageViewModel(
|
|||
|
||||
internal var isSetupWizard: Boolean = false
|
||||
internal val hasStorageSet: Boolean
|
||||
get() = storagePluginManager.storageProperties != null
|
||||
get() = backendManager.backendProperties != null
|
||||
abstract val isRestoreOperation: Boolean
|
||||
|
||||
internal fun loadStorageRoots() {
|
||||
|
@ -88,7 +88,7 @@ internal abstract class StorageViewModel(
|
|||
onSafUriSet(safStorage)
|
||||
}
|
||||
|
||||
abstract fun onSafUriSet(safStorage: SafStorage)
|
||||
abstract fun onSafUriSet(safProperties: SafProperties)
|
||||
abstract fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend)
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -22,7 +22,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavConfigState
|
||||
import com.stevesoltys.seedvault.backend.webdav.WebDavConfigState
|
||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.viewmodel.ext.android.getSharedViewModel
|
||||
|
|
|
@ -11,11 +11,9 @@ import android.util.Log
|
|||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getMetadataOutputStream
|
||||
import com.stevesoltys.seedvault.plugins.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getMetadataOutputStream
|
||||
import com.stevesoltys.seedvault.backend.isOutOfSpace
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.transport.backup.isStopped
|
||||
|
@ -24,7 +22,6 @@ import com.stevesoltys.seedvault.ui.notification.getAppName
|
|||
import kotlinx.coroutines.delay
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class ApkBackupManager(
|
||||
private val context: Context,
|
||||
|
@ -33,7 +30,7 @@ internal class ApkBackupManager(
|
|||
private val packageService: PackageService,
|
||||
private val iconManager: IconManager,
|
||||
private val apkBackup: ApkBackup,
|
||||
private val pluginManager: StoragePluginManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val nm: BackupNotificationManager,
|
||||
) {
|
||||
|
||||
|
@ -58,7 +55,7 @@ internal class ApkBackupManager(
|
|||
// upload all local changes only at the end,
|
||||
// so we don't have to re-upload the metadata
|
||||
val token = settingsManager.getToken() ?: error("no token")
|
||||
pluginManager.backend.getMetadataOutputStream(token).use { outputStream ->
|
||||
backendManager.backend.getMetadataOutputStream(token).use { outputStream ->
|
||||
metadataManager.uploadMetadata(outputStream)
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +102,7 @@ internal class ApkBackupManager(
|
|||
try {
|
||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
||||
val handle = LegacyAppBackupFile.IconsFile(token)
|
||||
pluginManager.backend.save(handle).use {
|
||||
backendManager.backend.save(handle).use {
|
||||
iconManager.uploadIcons(token, it)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
|
@ -123,7 +120,7 @@ internal class ApkBackupManager(
|
|||
return try {
|
||||
apkBackup.backupApkIfNecessary(packageInfo) { name ->
|
||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
||||
pluginManager.backend.save(LegacyAppBackupFile.Blob(token, name))
|
||||
backendManager.backend.save(LegacyAppBackupFile.Blob(token, name))
|
||||
}?.let { packageMetadata ->
|
||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata)
|
||||
true
|
||||
|
@ -147,11 +144,4 @@ internal class ApkBackupManager(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
|
||||
token: Long? = null,
|
||||
): OutputStream {
|
||||
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
||||
return getOutputStream(t, FILE_BACKUP_METADATA)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
|
|||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
|
||||
|
@ -101,7 +101,7 @@ class AppBackupWorker(
|
|||
private val backupRequester: BackupRequester by inject()
|
||||
private val settingsManager: SettingsManager by inject()
|
||||
private val apkBackupManager: ApkBackupManager by inject()
|
||||
private val storagePluginManager: StoragePluginManager by inject()
|
||||
private val backendManager: BackendManager by inject()
|
||||
private val nm: BackupNotificationManager by inject()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
@ -111,7 +111,7 @@ class AppBackupWorker(
|
|||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while running setForeground: ", e)
|
||||
}
|
||||
val freeSpace = storagePluginManager.getFreeSpace()
|
||||
val freeSpace = backendManager.getFreeSpace()
|
||||
if (freeSpace != null && freeSpace < MIN_FREE_SPACE) {
|
||||
nm.onInsufficientSpaceError()
|
||||
return Result.failure()
|
||||
|
|
|
@ -39,7 +39,7 @@ val workerModule = module {
|
|||
packageService = get(),
|
||||
apkBackup = get(),
|
||||
iconManager = get(),
|
||||
pluginManager = get(),
|
||||
backendManager = get(),
|
||||
nm = get()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.crypto.KeyManager
|
|||
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
||||
import com.stevesoltys.seedvault.header.headerModule
|
||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
||||
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
|
||||
import com.stevesoltys.seedvault.restore.install.installModule
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.saf
|
||||
package com.stevesoltys.seedvault.backend.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.getRandomString
|
|||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_CONTACTS
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SETTINGS
|
||||
|
@ -47,7 +47,7 @@ import kotlin.random.Random
|
|||
)
|
||||
internal class AppSelectionManagerTest : TransportTest() {
|
||||
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val iconManager: IconManager = mockk()
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val scope = TestScope(testDispatcher)
|
||||
|
@ -63,7 +63,7 @@ internal class AppSelectionManagerTest : TransportTest() {
|
|||
|
||||
private val appSelectionManager = AppSelectionManager(
|
||||
context = context,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
iconManager = iconManager,
|
||||
coroutineScope = scope,
|
||||
workDispatcher = testDispatcher,
|
||||
|
@ -222,7 +222,7 @@ internal class AppSelectionManagerTest : TransportTest() {
|
|||
@Test
|
||||
fun `test icon loading fails`() = scope.runTest {
|
||||
val backend: Backend = mockk()
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
coEvery {
|
||||
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
|
||||
} throws IOException()
|
||||
|
@ -429,7 +429,7 @@ internal class AppSelectionManagerTest : TransportTest() {
|
|||
private fun expectIconLoading(icons: Set<String> = setOf(packageName1, packageName2)) {
|
||||
val backend: Backend = mockk()
|
||||
val inputStream = ByteArrayInputStream(Random.nextBytes(42))
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
coEvery {
|
||||
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
|
||||
} returns inputStream
|
||||
|
|
|
@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.getRandomString
|
|||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
||||
|
@ -60,7 +60,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
every { packageManager } returns pm
|
||||
}
|
||||
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backupManager: IBackupManager = mockk()
|
||||
private val backupStateManager: BackupStateManager = mockk()
|
||||
|
||||
|
@ -76,7 +76,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
context = strictContext,
|
||||
backupManager = backupManager,
|
||||
backupStateManager = backupStateManager,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
legacyStoragePlugin = legacyStoragePlugin,
|
||||
crypto = crypto,
|
||||
splitCompatChecker = splitCompatChecker,
|
||||
|
@ -112,7 +112,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
|||
|
||||
init {
|
||||
mockkStatic(PackageUtils::class)
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -24,8 +24,8 @@ import com.stevesoltys.seedvault.getRandomString
|
|||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||
|
@ -67,7 +67,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
}
|
||||
private val backupManager: IBackupManager = mockk()
|
||||
private val backupStateManager: BackupStateManager = mockk()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend: Backend = mockk()
|
||||
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||
|
@ -78,7 +78,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
context = strictContext,
|
||||
backupManager = backupManager,
|
||||
backupStateManager = backupStateManager,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
legacyStoragePlugin = legacyStoragePlugin,
|
||||
crypto = crypto,
|
||||
splitCompatChecker = splitCompatChecker,
|
||||
|
@ -109,7 +109,7 @@ internal class ApkRestoreTest : TransportTest() {
|
|||
// as we don't do strict signature checking, we can use a relaxed mock
|
||||
packageInfo.signingInfo = mockk(relaxed = true)
|
||||
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
|
||||
// related to starting/stopping service
|
||||
every { strictContext.packageName } returns "org.foo.bar"
|
||||
|
|
|
@ -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) }
|
|
@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
|||
import com.stevesoltys.seedvault.metadata.BackupType
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
||||
|
@ -63,13 +63,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val dbManager = TestKvDbManager()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
private val backend = mockk<Backend>()
|
||||
private val kvBackup = KVBackup(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
settingsManager = settingsManager,
|
||||
nm = notificationManager,
|
||||
inputFactory = inputFactory,
|
||||
|
@ -77,7 +77,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
dbManager = dbManager,
|
||||
)
|
||||
private val fullBackup = FullBackup(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
settingsManager = settingsManager,
|
||||
nm = notificationManager,
|
||||
inputFactory = inputFactory,
|
||||
|
@ -87,7 +87,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val packageService: PackageService = mockk()
|
||||
private val backup = BackupCoordinator(
|
||||
context,
|
||||
storagePluginManager,
|
||||
backendManager,
|
||||
kvBackup,
|
||||
fullBackup,
|
||||
clock,
|
||||
|
@ -98,7 +98,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
)
|
||||
|
||||
private val kvRestore = KVRestore(
|
||||
storagePluginManager,
|
||||
backendManager,
|
||||
legacyPlugin,
|
||||
outputFactory,
|
||||
headerReader,
|
||||
|
@ -106,14 +106,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
dbManager
|
||||
)
|
||||
private val fullRestore =
|
||||
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||
FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val restore = RestoreCoordinator(
|
||||
context,
|
||||
crypto,
|
||||
settingsManager,
|
||||
metadataManager,
|
||||
notificationManager,
|
||||
storagePluginManager,
|
||||
backendManager,
|
||||
kvRestore,
|
||||
fullRestore,
|
||||
metadataReader
|
||||
|
@ -132,7 +132,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
|
||||
|
||||
init {
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -20,8 +20,7 @@ import com.stevesoltys.seedvault.metadata.BackupType
|
|||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.worker.ApkBackup
|
||||
import io.mockk.Runs
|
||||
|
@ -33,6 +32,7 @@ import io.mockk.verify
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
|
@ -41,7 +41,7 @@ import kotlin.random.Random
|
|||
|
||||
internal class BackupCoordinatorTest : BackupTest() {
|
||||
|
||||
private val pluginManager = mockk<StoragePluginManager>()
|
||||
private val backendManager = mockk<BackendManager>()
|
||||
private val kv = mockk<KVBackup>()
|
||||
private val full = mockk<FullBackup>()
|
||||
private val apkBackup = mockk<ApkBackup>()
|
||||
|
@ -50,7 +50,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
|
||||
private val backup = BackupCoordinator(
|
||||
context = context,
|
||||
pluginManager = pluginManager,
|
||||
backendManager = backendManager,
|
||||
kv = kv,
|
||||
full = full,
|
||||
clock = clock,
|
||||
|
@ -64,7 +64,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
private val metadataOutputStream = mockk<OutputStream>()
|
||||
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
||||
private val packageMetadata: PackageMetadata = mockk()
|
||||
private val safStorage = SafStorage(
|
||||
private val safProperties = SafProperties(
|
||||
config = Uri.EMPTY,
|
||||
name = getRandomString(),
|
||||
isUsb = false,
|
||||
|
@ -73,7 +73,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
)
|
||||
|
||||
init {
|
||||
every { pluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -100,7 +100,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
every { settingsManager.setNewToken(token) } just Runs
|
||||
every { metadataManager.onDeviceInitialization(token) } throws IOException()
|
||||
every { metadataManager.requiresInit } returns maybeTrue
|
||||
every { pluginManager.canDoBackupNow() } returns !maybeTrue
|
||||
every { backendManager.canDoBackupNow() } returns !maybeTrue
|
||||
every { notificationManager.onBackupError() } just Runs
|
||||
|
||||
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||
|
@ -120,7 +120,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
every { settingsManager.setNewToken(token) } just Runs
|
||||
every { metadataManager.onDeviceInitialization(token) } throws IOException()
|
||||
every { metadataManager.requiresInit } returns false
|
||||
every { pluginManager.canDoBackupNow() } returns false
|
||||
every { backendManager.canDoBackupNow() } returns false
|
||||
|
||||
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||
|
||||
|
@ -136,7 +136,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
|
||||
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
||||
|
||||
every { pluginManager.canDoBackupNow() } returns true
|
||||
every { backendManager.canDoBackupNow() } returns true
|
||||
every { metadataManager.requiresInit } returns true
|
||||
|
||||
// start new restore set
|
||||
|
@ -234,7 +234,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
every { kv.getCurrentSize() } returns 42L
|
||||
|
||||
coEvery { kv.finishBackup() } returns TRANSPORT_OK
|
||||
every { pluginManager.canDoBackupNow() } returns false
|
||||
every { backendManager.canDoBackupNow() } returns false
|
||||
|
||||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
)
|
||||
} just Runs
|
||||
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
||||
every { pluginManager.storageProperties } returns safStorage
|
||||
every { backendManager.backendProperties } returns safProperties
|
||||
every { settingsManager.useMeteredNetwork } returns false
|
||||
every { metadataOutputStream.close() } just Runs
|
||||
|
||||
|
@ -350,7 +350,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
)
|
||||
} just Runs
|
||||
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
||||
every { pluginManager.storageProperties } returns safStorage
|
||||
every { backendManager.backendProperties } returns safProperties
|
||||
every { settingsManager.useMeteredNetwork } returns false
|
||||
every { metadataOutputStream.close() } just Runs
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
|||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.getADForFull
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
|
@ -31,11 +31,11 @@ import kotlin.random.Random
|
|||
|
||||
internal class FullBackupTest : BackupTest() {
|
||||
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend = mockk<Backend>()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val backup = FullBackup(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
settingsManager = settingsManager,
|
||||
nm = notificationManager,
|
||||
inputFactory = inputFactory,
|
||||
|
@ -47,7 +47,7 @@ internal class FullBackupTest : BackupTest() {
|
|||
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
||||
|
||||
init {
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -17,7 +17,7 @@ import com.stevesoltys.seedvault.getRandomString
|
|||
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.getADForKV
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.Runs
|
||||
|
@ -39,13 +39,13 @@ import kotlin.random.Random
|
|||
|
||||
internal class KVBackupTest : BackupTest() {
|
||||
|
||||
private val pluginManager = mockk<StoragePluginManager>()
|
||||
private val backendManager = mockk<BackendManager>()
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val dataInput = mockk<BackupDataInput>()
|
||||
private val dbManager = mockk<KvDbManager>()
|
||||
|
||||
private val backup = KVBackup(
|
||||
pluginManager = pluginManager,
|
||||
backendManager = backendManager,
|
||||
settingsManager = settingsManager,
|
||||
nm = notificationManager,
|
||||
inputFactory = inputFactory,
|
||||
|
@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() {
|
|||
private val inputStream = ByteArrayInputStream(dbBytes)
|
||||
|
||||
init {
|
||||
every { pluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -250,7 +250,7 @@ internal class KVBackupTest : BackupTest() {
|
|||
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
|
||||
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
|
||||
every { dbManager.getDb(pmPackageInfo.packageName) } returns db
|
||||
every { pluginManager.canDoBackupNow() } returns false
|
||||
every { backendManager.canDoBackupNow() } returns false
|
||||
every { db.put(key, dataValue) } just Runs
|
||||
getDataInput(listOf(true, false))
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
|||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.VersionHeader
|
||||
import com.stevesoltys.seedvault.header.getADForFull
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
|
@ -39,11 +39,11 @@ import kotlin.random.Random
|
|||
|
||||
internal class FullRestoreTest : RestoreTest() {
|
||||
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend = mockk<Backend>()
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
private val restore = FullRestore(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
legacyPlugin = legacyPlugin,
|
||||
outputFactory = outputFactory,
|
||||
headerReader = headerReader,
|
||||
|
@ -55,7 +55,7 @@ internal class FullRestoreTest : RestoreTest() {
|
|||
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
||||
|
||||
init {
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -15,8 +15,8 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
|||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.VersionHeader
|
||||
import com.stevesoltys.seedvault.header.getADForKV
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.transport.backup.KVDb
|
||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||
import io.mockk.Runs
|
||||
|
@ -41,14 +41,14 @@ import kotlin.random.Random
|
|||
|
||||
internal class KVRestoreTest : RestoreTest() {
|
||||
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend = mockk<Backend>()
|
||||
@Suppress("DEPRECATION")
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
private val dbManager = mockk<KvDbManager>()
|
||||
private val output = mockk<BackupDataOutput>()
|
||||
private val restore = KVRestore(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
legacyPlugin = legacyPlugin,
|
||||
outputFactory = outputFactory,
|
||||
headerReader = headerReader,
|
||||
|
@ -74,7 +74,7 @@ internal class KVRestoreTest : RestoreTest() {
|
|||
// for InputStream#readBytes()
|
||||
mockkStatic("kotlin.io.ByteStreamsKt")
|
||||
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -13,16 +13,15 @@ import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
|
|||
import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.EncryptedMetadata
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.coAssertThrows
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.metadata.BackupType
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.plugins.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import io.mockk.Runs
|
||||
|
@ -34,6 +33,7 @@ import io.mockk.mockkStatic
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
|
@ -46,7 +46,7 @@ import kotlin.random.Random
|
|||
internal class RestoreCoordinatorTest : TransportTest() {
|
||||
|
||||
private val notificationManager: BackupNotificationManager = mockk()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend = mockk<Backend>()
|
||||
private val kv = mockk<KVRestore>()
|
||||
private val full = mockk<FullRestore>()
|
||||
|
@ -58,14 +58,14 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
settingsManager = settingsManager,
|
||||
metadataManager = metadataManager,
|
||||
notificationManager = notificationManager,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
kv = kv,
|
||||
full = full,
|
||||
metadataReader = metadataReader,
|
||||
)
|
||||
|
||||
private val inputStream = mockk<InputStream>()
|
||||
private val safStorage: SafStorage = mockk()
|
||||
private val safStorage: SafProperties = mockk()
|
||||
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
||||
private val packageInfoArray = arrayOf(packageInfo)
|
||||
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
||||
|
@ -80,8 +80,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
metadata.packageMetadataMap[packageInfo2.packageName] =
|
||||
PackageMetadata(backupType = BackupType.FULL)
|
||||
|
||||
mockkStatic("com.stevesoltys.seedvault.plugins.BackendExtKt")
|
||||
every { storagePluginManager.backend } returns backend
|
||||
mockkStatic("com.stevesoltys.seedvault.backend.BackendExtKt")
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -175,7 +175,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
fun `startRestore() optimized auto-restore with removed storage shows notification`() =
|
||||
runBlocking {
|
||||
every { storagePluginManager.storageProperties } returns safStorage
|
||||
every { backendManager.backendProperties } returns safStorage
|
||||
every { safStorage.isUnavailableUsb(context) } returns true
|
||||
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
||||
every { safStorage.name } returns storageName
|
||||
|
@ -199,7 +199,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
fun `startRestore() optimized auto-restore with available storage shows no notification`() =
|
||||
runBlocking {
|
||||
every { storagePluginManager.storageProperties } returns safStorage
|
||||
every { backendManager.backendProperties } returns safStorage
|
||||
every { safStorage.isUnavailableUsb(context) } returns false
|
||||
|
||||
restore.beforeStartRestore(metadata)
|
||||
|
@ -215,7 +215,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `startRestore() with removed storage shows no notification`() = runBlocking {
|
||||
every { storagePluginManager.storageProperties } returns safStorage
|
||||
every { backendManager.backendProperties } returns safStorage
|
||||
every { safStorage.isUnavailableUsb(context) } returns true
|
||||
every { metadataManager.getPackageMetadata(packageName) } returns null
|
||||
|
||||
|
|
|
@ -18,9 +18,8 @@ import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
|||
import com.stevesoltys.seedvault.encodeBase64
|
||||
import com.stevesoltys.seedvault.header.HeaderReaderImpl
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.toByteArrayFromHex
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||
|
@ -55,13 +54,13 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
|||
private val dbManager = mockk<KvDbManager>()
|
||||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
private val backend = mockk<Backend>()
|
||||
private val kvRestore = KVRestore(
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
legacyPlugin = legacyPlugin,
|
||||
outputFactory = outputFactory,
|
||||
headerReader = headerReader,
|
||||
|
@ -69,14 +68,14 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
|||
dbManager = dbManager,
|
||||
)
|
||||
private val fullRestore =
|
||||
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||
FullRestore(backendManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val restore = RestoreCoordinator(
|
||||
context = context,
|
||||
crypto = crypto,
|
||||
settingsManager = settingsManager,
|
||||
metadataManager = metadataManager,
|
||||
notificationManager = notificationManager,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
kv = kvRestore,
|
||||
full = fullRestore,
|
||||
metadataReader = metadataReader,
|
||||
|
@ -124,7 +123,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
|||
private val key264 = key2.encodeBase64()
|
||||
|
||||
init {
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -14,7 +14,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
|||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
|
@ -40,7 +40,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
private val packageService: PackageService = mockk()
|
||||
private val apkBackup: ApkBackup = mockk()
|
||||
private val iconManager: IconManager = mockk()
|
||||
private val storagePluginManager: StoragePluginManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend: Backend = mockk()
|
||||
private val nm: BackupNotificationManager = mockk()
|
||||
|
||||
|
@ -51,7 +51,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
packageService = packageService,
|
||||
apkBackup = apkBackup,
|
||||
iconManager = iconManager,
|
||||
pluginManager = storagePluginManager,
|
||||
backendManager = backendManager,
|
||||
nm = nm,
|
||||
)
|
||||
|
||||
|
@ -59,7 +59,7 @@ internal class ApkBackupManagerTest : TransportTest() {
|
|||
private val packageMetadata: PackageMetadata = mockk()
|
||||
|
||||
init {
|
||||
every { storagePluginManager.backend } returns backend
|
||||
every { backendManager.backend } returns backend
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.calyxos.seedvault.core
|
||||
|
||||
internal fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
|
||||
public fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
|
||||
|
||||
internal fun String.toByteArrayFromHex() = chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
public fun String.toByteArrayFromHex(): ByteArray =
|
||||
chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -20,6 +20,12 @@ public sealed class FileHandle {
|
|||
|
||||
public data class TopLevelFolder(override val name: String) : FileHandle() {
|
||||
override val relativePath: String = name
|
||||
|
||||
public companion object {
|
||||
public fun fromAndroidId(androidId: String): TopLevelFolder {
|
||||
return TopLevelFolder("$androidId.sv")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LegacyAppBackupFile : FileHandle() {
|
||||
|
|
|
@ -39,7 +39,7 @@ internal const val ROOT_ID_DEVICE = "primary"
|
|||
|
||||
public class SafBackend(
|
||||
private val appContext: Context,
|
||||
private val safConfig: SafConfig,
|
||||
private val safProperties: SafProperties,
|
||||
root: String = DIRECTORY_ROOT,
|
||||
) : Backend {
|
||||
|
||||
|
@ -48,16 +48,16 @@ public class SafBackend(
|
|||
/**
|
||||
* Attention: This context might be from a different user. Use with care.
|
||||
*/
|
||||
private val context: Context get() = appContext.getBackendContext { safConfig.isUsb }
|
||||
private val cache = DocumentFileCache(context, safConfig.getDocumentFile(context), root)
|
||||
private val context: Context get() = appContext.getBackendContext { safProperties.isUsb }
|
||||
private val cache = DocumentFileCache(context, safProperties.getDocumentFile(context), root)
|
||||
|
||||
override suspend fun test(): Boolean {
|
||||
return cache.getRootFile().isDirectory
|
||||
}
|
||||
|
||||
override suspend fun getFreeSpace(): Long? {
|
||||
val rootId = safConfig.rootId ?: return null
|
||||
val authority = safConfig.uri.authority
|
||||
val rootId = safProperties.rootId ?: return null
|
||||
val authority = safProperties.uri.authority
|
||||
// using DocumentsContract#buildRootUri(String, String) with rootId directly doesn't work
|
||||
val rootUri = DocumentsContract.buildRootsUri(authority)
|
||||
val projection = arrayOf(COLUMN_AVAILABLE_BYTES)
|
||||
|
@ -74,8 +74,8 @@ public class SafBackend(
|
|||
return if (bytesAvailable == null && authority == AUTHORITY_STORAGE) {
|
||||
if (rootId == ROOT_ID_DEVICE) {
|
||||
StatFs(Environment.getDataDirectory().absolutePath).availableBytes
|
||||
} else if (safConfig.isUsb) {
|
||||
val documentId = safConfig.uri.lastPathSegment ?: return null
|
||||
} else if (safProperties.isUsb) {
|
||||
val documentId = safProperties.uri.lastPathSegment ?: return null
|
||||
StatFs("/mnt/media_rw/${documentId.trimEnd(':')}").availableBytes
|
||||
} else null
|
||||
} else bytesAvailable
|
||||
|
@ -185,7 +185,7 @@ public class SafBackend(
|
|||
}
|
||||
|
||||
override val providerPackageName: String? by lazy {
|
||||
val authority = safConfig.uri.authority ?: return@lazy null
|
||||
val authority = safProperties.uri.authority ?: return@lazy null
|
||||
val providerInfo = context.packageManager.resolveContentProvider(authority, 0)
|
||||
?: return@lazy null
|
||||
providerInfo.packageName
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.annotation.WorkerThread
|
|||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.calyxos.seedvault.core.backends.BackendProperties
|
||||
|
||||
public data class SafConfig(
|
||||
public data class SafProperties(
|
||||
override val config: Uri,
|
||||
override val name: String,
|
||||
override val isUsb: Boolean,
|
||||
|
@ -24,7 +24,7 @@ public data class SafConfig(
|
|||
val rootId: String?,
|
||||
) : BackendProperties<Uri>() {
|
||||
|
||||
internal val uri: Uri = config
|
||||
public val uri: Uri = config
|
||||
|
||||
public fun getDocumentFile(context: Context): DocumentFile =
|
||||
DocumentFile.fromTreeUri(context, config)
|
|
@ -3,16 +3,15 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.plugins.webdav
|
||||
package org.calyxos.seedvault.core.backends.webdav
|
||||
|
||||
import android.content.Context
|
||||
import com.stevesoltys.seedvault.plugins.StorageProperties
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavConfig
|
||||
import org.calyxos.seedvault.core.backends.BackendProperties
|
||||
|
||||
data class WebDavProperties(
|
||||
public data class WebDavProperties(
|
||||
override val config: WebDavConfig,
|
||||
override val name: String,
|
||||
) : StorageProperties<WebDavConfig>() {
|
||||
) : BackendProperties<WebDavConfig>() {
|
||||
override val isUsb: Boolean = false
|
||||
override val requiresNetwork: Boolean = true
|
||||
override fun isUnavailableUsb(context: Context): Boolean = false
|
|
@ -12,7 +12,7 @@ import org.calyxos.seedvault.core.backends.FileHandle
|
|||
import org.calyxos.seedvault.core.backends.FileInfo
|
||||
import org.calyxos.seedvault.core.backends.TopLevelFolder
|
||||
import org.calyxos.seedvault.core.backends.saf.SafBackend
|
||||
import org.calyxos.seedvault.core.backends.saf.SafConfig
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.reflect.KClass
|
||||
|
@ -22,15 +22,15 @@ class TestSafBackend(
|
|||
private val getLocationUri: () -> Uri?,
|
||||
) : Backend {
|
||||
|
||||
private val safConfig
|
||||
get() = SafConfig(
|
||||
private val safProperties
|
||||
get() = SafProperties(
|
||||
config = getLocationUri() ?: error("no uri"),
|
||||
name = "foo",
|
||||
isUsb = false,
|
||||
requiresNetwork = false,
|
||||
rootId = "bar",
|
||||
)
|
||||
private val delegate: SafBackend get() = SafBackend(appContext, safConfig)
|
||||
private val delegate: SafBackend get() = SafBackend(appContext, safProperties)
|
||||
|
||||
private val nullStream = object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
|
|
|
@ -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()
|
|
@ -5,50 +5,14 @@
|
|||
|
||||
package org.calyxos.backup.storage
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import org.calyxos.backup.storage.api.MediaType
|
||||
import org.calyxos.backup.storage.api.mediaItems
|
||||
import org.calyxos.backup.storage.backup.BackupMediaFile
|
||||
import org.calyxos.backup.storage.db.StoredUri
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal fun Uri.toStoredUri(): StoredUri = StoredUri(this)
|
||||
|
||||
internal fun Uri.getDocumentPath(): String? {
|
||||
return lastPathSegment?.split(':')?.getOrNull(1)
|
||||
}
|
||||
|
||||
internal fun Uri.getVolume(): String? {
|
||||
val volume = lastPathSegment?.split(':')?.getOrNull(0)
|
||||
return if (volume == "primary") MediaStore.VOLUME_EXTERNAL_PRIMARY else volume
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
public fun Uri.openInputStream(contentResolver: ContentResolver): InputStream {
|
||||
return try {
|
||||
contentResolver.openInputStream(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// This is necessary, because contrary to the documentation, files that have been deleted
|
||||
// after we retrieved their Uri, will throw an IllegalArgumentException
|
||||
throw IOException(e)
|
||||
} ?: throw IOException("Stream for $this returned null")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
public fun Uri.openOutputStream(contentResolver: ContentResolver): OutputStream {
|
||||
return try {
|
||||
contentResolver.openOutputStream(this, "wt")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// This is necessary, because contrary to the documentation, files that have been deleted
|
||||
// after we retrieved their Uri, will throw an IllegalArgumentException
|
||||
throw IOException(e)
|
||||
} ?: throw IOException("Stream for $this returned null")
|
||||
}
|
||||
|
||||
internal fun Uri.getMediaType(): MediaType? {
|
||||
val str = toString()
|
||||
for (item in mediaItems) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import org.calyxos.backup.storage.R
|
||||
import org.calyxos.backup.storage.backup.BackupMediaFile
|
||||
import org.calyxos.backup.storage.getDocumentPath
|
||||
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
|
||||
|
||||
// hidden in DocumentsContract
|
||||
public const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY: String =
|
||||
|
@ -38,7 +38,7 @@ public sealed class BackupContentType(
|
|||
public object Custom : BackupContentType(R.drawable.ic_folder) {
|
||||
public fun getName(uri: Uri): String {
|
||||
val path = uri.getDocumentPath()!!
|
||||
return if (path.isBlank()) "/" else path
|
||||
return path.ifBlank { "/" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.calyxos.backup.storage.backup.BackupSnapshot
|
|||
import org.calyxos.backup.storage.backup.ChunksCacheRepopulater
|
||||
import org.calyxos.backup.storage.db.Db
|
||||
import org.calyxos.backup.storage.getCurrentBackupSnapshots
|
||||
import org.calyxos.backup.storage.getDocumentPath
|
||||
import org.calyxos.backup.storage.getMediaType
|
||||
import org.calyxos.backup.storage.prune.Pruner
|
||||
import org.calyxos.backup.storage.prune.RetentionManager
|
||||
|
@ -37,6 +36,7 @@ import org.calyxos.backup.storage.scanner.MediaScanner
|
|||
import org.calyxos.backup.storage.toStoredUri
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
||||
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
|
||||
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
package org.calyxos.backup.storage.backup
|
||||
|
||||
import org.calyxos.backup.storage.db.CachedChunk
|
||||
import org.calyxos.backup.storage.toHexString
|
||||
import org.calyxos.seedvault.core.toHexString
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.crypto.Mac
|
||||
|
|
|
@ -13,7 +13,7 @@ import org.calyxos.backup.storage.content.DocFile
|
|||
import org.calyxos.backup.storage.content.MediaFile
|
||||
import org.calyxos.backup.storage.db.CachedFile
|
||||
import org.calyxos.backup.storage.db.FilesCache
|
||||
import org.calyxos.backup.storage.openInputStream
|
||||
import org.calyxos.seedvault.core.backends.saf.openInputStream
|
||||
import java.io.IOException
|
||||
import java.security.GeneralSecurityException
|
||||
|
||||
|
|
|
@ -13,11 +13,10 @@ import org.calyxos.backup.storage.content.DocFile
|
|||
import org.calyxos.backup.storage.content.MediaFile
|
||||
import org.calyxos.backup.storage.db.CachedFile
|
||||
import org.calyxos.backup.storage.db.FilesCache
|
||||
import org.calyxos.backup.storage.openInputStream
|
||||
import org.calyxos.seedvault.core.backends.saf.openInputStream
|
||||
import java.io.IOException
|
||||
import java.security.GeneralSecurityException
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
internal class SmallFileBackup(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val filesCache: FilesCache,
|
||||
|
|
|
@ -7,7 +7,7 @@ package org.calyxos.backup.storage.backup
|
|||
|
||||
import org.calyxos.backup.storage.content.ContentFile
|
||||
import org.calyxos.backup.storage.db.CachedChunk
|
||||
import org.calyxos.backup.storage.toHexString
|
||||
import org.calyxos.seedvault.core.toHexString
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.google.crypto.tink.subtle.AesGcmHkdfStreaming
|
|||
import org.calyxos.backup.storage.backup.Backup.Companion.VERSION
|
||||
import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC
|
||||
import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES
|
||||
import org.calyxos.backup.storage.toByteArrayFromHex
|
||||
import org.calyxos.seedvault.core.toByteArrayFromHex
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
|
|
@ -15,8 +15,8 @@ import android.util.Log
|
|||
import org.calyxos.backup.storage.api.MediaType
|
||||
import org.calyxos.backup.storage.api.RestoreObserver
|
||||
import org.calyxos.backup.storage.backup.BackupMediaFile
|
||||
import org.calyxos.backup.storage.openOutputStream
|
||||
import org.calyxos.backup.storage.scanner.MediaScanner
|
||||
import org.calyxos.seedvault.core.backends.saf.openOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
|
|
@ -14,8 +14,8 @@ import androidx.core.database.getLongOrNull
|
|||
import androidx.core.database.getStringOrNull
|
||||
import org.calyxos.backup.storage.api.BackupFile
|
||||
import org.calyxos.backup.storage.content.DocFile
|
||||
import org.calyxos.backup.storage.getDocumentPath
|
||||
import org.calyxos.backup.storage.getVolume
|
||||
import org.calyxos.seedvault.core.backends.saf.getDocumentPath
|
||||
import org.calyxos.seedvault.core.backends.saf.getVolume
|
||||
|
||||
public class DocumentScanner(context: Context) {
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ import org.calyxos.backup.storage.crypto.StreamCrypto
|
|||
import org.calyxos.backup.storage.db.ChunksCache
|
||||
import org.calyxos.backup.storage.getRandomString
|
||||
import org.calyxos.backup.storage.mockLog
|
||||
import org.calyxos.backup.storage.toHexString
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob
|
||||
import org.calyxos.seedvault.core.toHexString
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
|
|
@ -22,8 +22,8 @@ import org.calyxos.backup.storage.db.FilesCache
|
|||
import org.calyxos.backup.storage.getRandomDocFile
|
||||
import org.calyxos.backup.storage.getRandomString
|
||||
import org.calyxos.backup.storage.mockLog
|
||||
import org.calyxos.backup.storage.toHexString
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.toHexString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
|
|
|
@ -13,7 +13,7 @@ import io.mockk.mockk
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.calyxos.backup.storage.getRandomDocFile
|
||||
import org.calyxos.backup.storage.getRandomString
|
||||
import org.calyxos.backup.storage.toHexString
|
||||
import org.calyxos.seedvault.core.toHexString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
Loading…
Reference in a new issue