Move code to get available backups from RestorePlugin to BackupPlugin

This commit is contained in:
Torsten Grote 2021-09-16 14:04:02 +02:00 committed by Chirayu Desai
parent 5d1e3debd1
commit db4103e752
10 changed files with 41 additions and 111 deletions

View file

@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVBackup
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVRestorePlugin import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVRestorePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH
import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD
import com.stevesoltys.seedvault.plugins.saf.deleteContents import com.stevesoltys.seedvault.plugins.saf.deleteContents
@ -94,9 +95,9 @@ class PluginTest : KoinComponent {
@Test @Test
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) { fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially // no backups available initially
assertEquals(0, restorePlugin.getAvailableBackups()?.toList()?.size) assertEquals(0, backupPlugin.getAvailableBackups()?.toList()?.size)
val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage") val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage")
assertFalse(restorePlugin.hasBackup(uri)) assertFalse(backupPlugin.hasBackup(uri))
// prepare returned tokens requested when initializing device // prepare returned tokens requested when initializing device
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1) every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
@ -106,23 +107,26 @@ class PluginTest : KoinComponent {
backupPlugin.initializeDevice() backupPlugin.initializeDevice()
// write metadata (needed for backup to be recognized) // write metadata (needed for backup to be recognized)
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
.writeAndClose(getRandomByteArray())
// one backup available now // one backup available now
assertEquals(1, restorePlugin.getAvailableBackups()?.toList()?.size) assertEquals(1, backupPlugin.getAvailableBackups()?.toList()?.size)
assertTrue(restorePlugin.hasBackup(uri)) assertTrue(backupPlugin.hasBackup(uri))
// initializing again (with another restore set) does add a restore set // initializing again (with another restore set) does add a restore set
backupPlugin.startNewRestoreSet(token + 1) backupPlugin.startNewRestoreSet(token + 1)
backupPlugin.initializeDevice() backupPlugin.initializeDevice()
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size) .writeAndClose(getRandomByteArray())
assertTrue(restorePlugin.hasBackup(uri)) assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
assertTrue(backupPlugin.hasBackup(uri))
// initializing again (without new restore set) doesn't change number of restore sets // initializing again (without new restore set) doesn't change number of restore sets
backupPlugin.initializeDevice() backupPlugin.initializeDevice()
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size) .writeAndClose(getRandomByteArray())
assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
// ensure that the new backup dirs exist // ensure that the new backup dirs exist
assertTrue(storage.currentKvBackupDir!!.exists()) assertTrue(storage.currentKvBackupDir!!.exists())
@ -138,29 +142,27 @@ class PluginTest : KoinComponent {
// write metadata // write metadata
val metadata = getRandomByteArray() val metadata = getRandomByteArray()
backupPlugin.getMetadataOutputStream().writeAndClose(metadata) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
// get available backups, expect only one with our token and no error // get available backups, expect only one with our token and no error
var availableBackups = restorePlugin.getAvailableBackups()?.toList() var availableBackups = backupPlugin.getAvailableBackups()?.toList()
check(availableBackups != null) check(availableBackups != null)
assertEquals(1, availableBackups.size) assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token) assertEquals(token, availableBackups[0].token)
assertFalse(availableBackups[0].error)
// read metadata matches what was written earlier // read metadata matches what was written earlier
assertReadEquals(metadata, availableBackups[0].inputStream) assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
// initializing again (without changing storage) keeps restore set with same token // initializing again (without changing storage) keeps restore set with same token
backupPlugin.initializeDevice() backupPlugin.initializeDevice()
backupPlugin.getMetadataOutputStream().writeAndClose(metadata) backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
availableBackups = restorePlugin.getAvailableBackups()?.toList() availableBackups = backupPlugin.getAvailableBackups()?.toList()
check(availableBackups != null) check(availableBackups != null)
assertEquals(1, availableBackups.size) assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token) assertEquals(token, availableBackups[0].token)
assertFalse(availableBackups[0].error)
// metadata hasn't changed // metadata hasn't changed
assertReadEquals(metadata, availableBackups[0].inputStream) assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
} }
@Test @Test

View file

@ -6,7 +6,6 @@ import com.stevesoltys.seedvault.crypto.TYPE_METADATA
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
typealias PackageMetadataMap = HashMap<String, PackageMetadata> typealias PackageMetadataMap = HashMap<String, PackageMetadata>
@ -100,20 +99,6 @@ internal const val JSON_PACKAGE_SIGNATURES = "signatures"
internal class DecryptionFailedException(cause: Throwable) : Exception(cause) internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
class EncryptedBackupMetadata private constructor(
val token: Long,
val inputStream: InputStream?,
val error: Boolean
) {
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
/**
* Indicates that there was an error retrieving the encrypted backup metadata.
*/
constructor(token: Long) : this(token, null, true)
}
internal fun getAD(version: Byte, token: Long) = ByteBuffer.allocate(2 + 8) internal fun getAD(version: Byte, token: Long) = ByteBuffer.allocate(2 + 8)
.put(version) .put(version)
.put(TYPE_METADATA) .put(TYPE_METADATA)

View file

@ -1,11 +1,7 @@
package com.stevesoltys.seedvault.plugins.saf package com.stevesoltys.seedvault.plugins.saf
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin import com.stevesoltys.seedvault.transport.restore.RestorePlugin
@ -13,8 +9,6 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
@WorkerThread @WorkerThread
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O @Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
internal class DocumentsProviderRestorePlugin( internal class DocumentsProviderRestorePlugin(
@ -24,31 +18,6 @@ internal class DocumentsProviderRestorePlugin(
override val fullRestorePlugin: FullRestorePlugin override val fullRestorePlugin: FullRestorePlugin
) : RestorePlugin { ) : RestorePlugin {
@Throws(IOException::class)
override suspend fun hasBackup(uri: Uri): Boolean {
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
val rootDir = parent.findFileBlocking(context, DIRECTORY_ROOT) ?: return false
val backupSets = getBackups(context, rootDir)
return backupSets.isNotEmpty()
}
override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
val rootDir = storage.rootBackupDir ?: return null
val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator()
return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence
val backupSet = iterator.next()
try {
val stream = storage.getInputStream(backupSet.metadataFile)
EncryptedBackupMetadata(backupSet.token, stream)
} catch (e: IOException) {
Log.e(TAG, "Error getting InputStream for backup metadata.", e)
EncryptedBackupMetadata(backupSet.token)
}
}
}
@Throws(IOException::class) @Throws(IOException::class)
override suspend fun getApkInputStream( override suspend fun getApkInputStream(
token: Long, token: Long,

View file

@ -58,7 +58,7 @@ interface BackupPlugin {
* Returns an [OutputStream] for writing backup metadata. * Returns an [OutputStream] for writing backup metadata.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
@Deprecated("use getOutputStream() instead") @Deprecated("use getOutputStream(token, FILE_BACKUP_METADATA) instead")
suspend fun getMetadataOutputStream(token: Long): OutputStream suspend fun getMetadataOutputStream(token: Long): OutputStream
/** /**

View file

@ -19,8 +19,8 @@ import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
private data class RestoreCoordinatorState( private data class RestoreCoordinatorState(
@ -43,7 +43,7 @@ internal class RestoreCoordinator(
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager, private val notificationManager: BackupNotificationManager,
private val plugin: RestorePlugin, private val plugin: BackupPlugin,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
private val metadataReader: MetadataReader private val metadataReader: MetadataReader
@ -57,15 +57,10 @@ internal class RestoreCoordinator(
val availableBackups = plugin.getAvailableBackups() ?: return null val availableBackups = plugin.getAvailableBackups() ?: return null
val metadataMap = HashMap<Long, BackupMetadata>() val metadataMap = HashMap<Long, BackupMetadata>()
for (encryptedMetadata in availableBackups) { for (encryptedMetadata in availableBackups) {
if (encryptedMetadata.error) continue
check(encryptedMetadata.inputStream != null) {
"No error when getting encrypted metadata, but stream is still missing."
}
try { try {
val metadata = metadataReader.readMetadata( val metadata = encryptedMetadata.inputStreamRetriever().use { inputStream ->
encryptedMetadata.inputStream, metadataReader.readMetadata(inputStream, encryptedMetadata.token)
encryptedMetadata.token }
)
metadataMap[encryptedMetadata.token] = metadata metadataMap[encryptedMetadata.token] = metadata
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e) Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
@ -79,8 +74,6 @@ internal class RestoreCoordinator(
} catch (e: UnsupportedVersionException) { } catch (e: UnsupportedVersionException) {
Log.w(TAG, "Backup with unsupported version read", e) Log.w(TAG, "Backup with unsupported version read", e)
continue continue
} finally {
closeQuietly(encryptedMetadata.inputStream)
} }
} }
Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}") Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}")

View file

@ -1,8 +1,5 @@
package com.stevesoltys.seedvault.transport.restore package com.stevesoltys.seedvault.transport.restore
import android.net.Uri
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -12,24 +9,6 @@ interface RestorePlugin {
val fullRestorePlugin: FullRestorePlugin val fullRestorePlugin: FullRestorePlugin
/**
* 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<EncryptedBackupMetadata>?
/**
* Searches if there's really a backup available in the given location.
* Returns true if at least one was found and false otherwise.
*
* FIXME: Passing a Uri is maybe too plugin-specific?
*/
@WorkerThread
@Throws(IOException::class)
suspend fun hasBackup(uri: Uri): Boolean
/** /**
* Returns an [InputStream] for the given token, for reading an APK that is to be restored. * Returns an [InputStream] for the given token, for reading an APK that is to be restored.
*/ */

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.restore.RestorePlugin import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
@ -16,7 +16,7 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName
internal class RestoreStorageViewModel( internal class RestoreStorageViewModel(
private val app: Application, private val app: Application,
private val restorePlugin: RestorePlugin, private val backupPlugin: BackupPlugin,
settingsManager: SettingsManager settingsManager: SettingsManager
) : StorageViewModel(app, settingsManager) { ) : StorageViewModel(app, settingsManager) {
@ -25,7 +25,7 @@ internal class RestoreStorageViewModel(
override fun onLocationSet(uri: Uri) { override fun onLocationSet(uri: Uri) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val hasBackup = try { val hasBackup = try {
restorePlugin.hasBackup(uri) backupPlugin.hasBackup(uri)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error reading URI: $uri", e) Log.e(TAG, "Error reading URI: $uri", e)
false false

View file

@ -104,7 +104,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
settingsManager, settingsManager,
metadataManager, metadataManager,
notificationManager, notificationManager,
restorePlugin, backupPlugin,
kvRestore, kvRestore,
fullRestore, fullRestore,
metadataReader metadataReader

View file

@ -11,11 +11,12 @@ import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.transport.backup.EncryptedMetadata
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
@ -37,7 +38,7 @@ import kotlin.random.Random
internal class RestoreCoordinatorTest : TransportTest() { internal class RestoreCoordinatorTest : TransportTest() {
private val notificationManager: BackupNotificationManager = mockk() private val notificationManager: BackupNotificationManager = mockk()
private val plugin = mockk<RestorePlugin>() private val plugin = mockk<BackupPlugin>()
private val kv = mockk<KVRestore>() private val kv = mockk<KVRestore>()
private val full = mockk<FullRestore>() private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>() private val metadataReader = mockk<MetadataReader>()
@ -67,11 +68,11 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking { fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
val encryptedMetadata = EncryptedBackupMetadata(token, inputStream) val encryptedMetadata = EncryptedMetadata(token) { inputStream }
coEvery { plugin.getAvailableBackups() } returns sequenceOf( coEvery { plugin.getAvailableBackups() } returns sequenceOf(
encryptedMetadata, encryptedMetadata,
EncryptedBackupMetadata(token + 1, inputStream) EncryptedMetadata(token + 1) { inputStream }
) )
every { metadataReader.readMetadata(inputStream, token) } returns metadata every { metadataReader.readMetadata(inputStream, token) } returns metadata
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@ -99,8 +100,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `startRestore() fetches metadata if missing`() = runBlocking { fun `startRestore() fetches metadata if missing`() = runBlocking {
coEvery { plugin.getAvailableBackups() } returns sequenceOf( coEvery { plugin.getAvailableBackups() } returns sequenceOf(
EncryptedBackupMetadata(token, inputStream), EncryptedMetadata(token) { inputStream },
EncryptedBackupMetadata(token + 1, inputStream) EncryptedMetadata(token + 1) { inputStream }
) )
every { metadataReader.readMetadata(inputStream, token) } returns metadata every { metadataReader.readMetadata(inputStream, token) } returns metadata
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@ -112,7 +113,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `startRestore() errors if metadata is not matching token`() = runBlocking { fun `startRestore() errors if metadata is not matching token`() = runBlocking {
coEvery { plugin.getAvailableBackups() } returns sequenceOf( coEvery { plugin.getAvailableBackups() } returns sequenceOf(
EncryptedBackupMetadata(token + 42, inputStream) EncryptedMetadata(token + 42) { inputStream }
) )
every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata
every { inputStream.close() } just Runs every { inputStream.close() } just Runs

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.toByteArrayFromHex import com.stevesoltys.seedvault.toByteArrayFromHex
import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
@ -46,7 +47,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val metadataReader = MetadataReaderImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val restorePlugin = mockk<RestorePlugin>() private val backupPlugin = mockk<BackupPlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>() private val kvRestorePlugin = mockk<KVRestorePlugin>()
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>() private val fullRestorePlugin = mockk<FullRestorePlugin>()
@ -57,7 +58,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
settingsManager, settingsManager,
metadataManager, metadataManager,
notificationManager, notificationManager,
restorePlugin, backupPlugin,
kvRestore, kvRestore,
fullRestore, fullRestore,
metadataReader metadataReader