Remove legacy backup plugin code
This commit is contained in:
parent
1885021c1c
commit
183e34afd2
13 changed files with 46 additions and 450 deletions
|
@ -4,20 +4,14 @@ import androidx.test.core.content.pm.PackageInfoBuilder
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderBackupPlugin
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderBackupPlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderFullBackup
|
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderFullRestorePlugin
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderFullRestorePlugin
|
||||||
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.FILE_BACKUP_METADATA
|
||||||
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.deleteContents
|
import com.stevesoltys.seedvault.plugins.saf.deleteContents
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
|
||||||
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
|
||||||
|
@ -35,7 +29,6 @@ import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
@ -46,14 +39,7 @@ class PluginTest : KoinComponent {
|
||||||
private val mockedSettingsManager: SettingsManager = mockk()
|
private val mockedSettingsManager: SettingsManager = mockk()
|
||||||
private val storage = DocumentsStorage(context, mockedSettingsManager)
|
private val storage = DocumentsStorage(context, mockedSettingsManager)
|
||||||
|
|
||||||
private val kvBackupPlugin: KVBackupPlugin = DocumentsProviderKVBackup(context, storage)
|
private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(context, storage)
|
||||||
private val fullBackupPlugin: FullBackupPlugin = DocumentsProviderFullBackup(context, storage)
|
|
||||||
private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(
|
|
||||||
context,
|
|
||||||
storage,
|
|
||||||
kvBackupPlugin,
|
|
||||||
fullBackupPlugin
|
|
||||||
)
|
|
||||||
|
|
||||||
private val kvRestorePlugin: KVRestorePlugin =
|
private val kvRestorePlugin: KVRestorePlugin =
|
||||||
DocumentsProviderKVRestorePlugin(context, storage)
|
DocumentsProviderKVRestorePlugin(context, storage)
|
||||||
|
@ -127,9 +113,8 @@ class PluginTest : KoinComponent {
|
||||||
.writeAndClose(getRandomByteArray())
|
.writeAndClose(getRandomByteArray())
|
||||||
assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
|
||||||
|
|
||||||
// ensure that the new backup dirs exist
|
// ensure that the new backup dir exist
|
||||||
assertTrue(storage.currentKvBackupDir!!.exists())
|
assertTrue(storage.currentSetDir!!.exists())
|
||||||
assertTrue(storage.currentFullBackupDir!!.exists())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -165,7 +150,8 @@ class PluginTest : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testApkWriteRead() = runBlocking {
|
@Suppress("Deprecation")
|
||||||
|
fun v0testApkWriteRead() = runBlocking {
|
||||||
// initialize storage with given token
|
// initialize storage with given token
|
||||||
initStorage(token)
|
initStorage(token)
|
||||||
|
|
||||||
|
@ -191,157 +177,44 @@ class PluginTest : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testKvBackupRestore() = runBlocking {
|
fun testBackupRestore() = runBlocking {
|
||||||
// define shortcuts
|
|
||||||
val kvBackup = backupPlugin.kvBackupPlugin
|
|
||||||
val kvRestore = restorePlugin.kvRestorePlugin
|
|
||||||
|
|
||||||
// initialize storage with given token
|
// initialize storage with given token
|
||||||
initStorage(token)
|
initStorage(token)
|
||||||
|
|
||||||
// no data available for given package
|
val name1 = getRandomBase64()
|
||||||
assertFalse(kvBackup.hasDataForPackage(packageInfo))
|
val name2 = getRandomBase64()
|
||||||
assertFalse(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
|
|
||||||
// define key/value pair records
|
|
||||||
val record1 = Pair(getRandomBase64(23), getRandomByteArray(1337))
|
|
||||||
val record2 = Pair(getRandomBase64(42), getRandomByteArray(42 * 1024))
|
|
||||||
val record3 = Pair(getRandomBase64(128), getRandomByteArray(5 * 1024 * 1024))
|
|
||||||
|
|
||||||
// write first record
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, record1.first).writeAndClose(record1.second)
|
|
||||||
|
|
||||||
// data is now available for current token and given package, but not for different token
|
|
||||||
assertTrue(kvBackup.hasDataForPackage(packageInfo))
|
|
||||||
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
assertFalse(kvRestore.hasDataForPackage(token + 1, packageInfo))
|
|
||||||
|
|
||||||
// record for package is found and returned properly
|
|
||||||
var records = kvRestore.listRecords(token, packageInfo)
|
|
||||||
assertEquals(1, records.size)
|
|
||||||
assertEquals(record1.first, records[0])
|
|
||||||
assertReadEquals(
|
|
||||||
record1.second,
|
|
||||||
kvRestore.getInputStreamForRecord(token, packageInfo, record1.first)
|
|
||||||
)
|
|
||||||
|
|
||||||
// write second and third record
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, record2.first).writeAndClose(record2.second)
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, record3.first).writeAndClose(record3.second)
|
|
||||||
|
|
||||||
// all records for package are found and returned properly
|
|
||||||
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
records = kvRestore.listRecords(token, packageInfo)
|
|
||||||
assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted())
|
|
||||||
assertReadEquals(
|
|
||||||
record1.second,
|
|
||||||
kvRestore.getInputStreamForRecord(token, packageInfo, record1.first)
|
|
||||||
)
|
|
||||||
assertReadEquals(
|
|
||||||
record2.second,
|
|
||||||
kvRestore.getInputStreamForRecord(token, packageInfo, record2.first)
|
|
||||||
)
|
|
||||||
assertReadEquals(
|
|
||||||
record3.second,
|
|
||||||
kvRestore.getInputStreamForRecord(token, packageInfo, record3.first)
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete record3 and ensure that the other two are still found
|
|
||||||
kvBackup.deleteRecord(packageInfo, record3.first)
|
|
||||||
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
records = kvRestore.listRecords(token, packageInfo)
|
|
||||||
assertEquals(listOf(record1.first, record2.first).sorted(), records.sorted())
|
|
||||||
|
|
||||||
// remove all data of package and ensure that it is gone
|
|
||||||
kvBackup.removeDataOfPackage(packageInfo)
|
|
||||||
assertFalse(kvBackup.hasDataForPackage(packageInfo))
|
|
||||||
assertFalse(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMaxKvKeyLength() = runBlocking {
|
|
||||||
// define shortcuts
|
|
||||||
val kvBackup = backupPlugin.kvBackupPlugin
|
|
||||||
val kvRestore = restorePlugin.kvRestorePlugin
|
|
||||||
|
|
||||||
// initialize storage with given token
|
|
||||||
initStorage(token)
|
|
||||||
assertFalse(kvBackup.hasDataForPackage(packageInfo))
|
|
||||||
|
|
||||||
// FIXME get Nextcloud to have the same limit
|
|
||||||
// Since Nextcloud is using WebDAV and that seems to have undefined lower file name limits
|
|
||||||
// we might have to lower our maximum to accommodate for that.
|
|
||||||
val max = if (isNextcloud()) MAX_KEY_LENGTH_NEXTCLOUD else MAX_KEY_LENGTH
|
|
||||||
val maxOver = if (isNextcloud()) max + 10 else max + 1
|
|
||||||
|
|
||||||
// define record with maximum key length and one above the maximum
|
|
||||||
val recordMax = Pair(getRandomBase64(max), getRandomByteArray(1024))
|
|
||||||
val recordOver = Pair(getRandomBase64(maxOver), getRandomByteArray(1024))
|
|
||||||
|
|
||||||
// write max record
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first)
|
|
||||||
.writeAndClose(recordMax.second)
|
|
||||||
|
|
||||||
// max record is found correctly
|
|
||||||
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
|
|
||||||
val records = kvRestore.listRecords(token, packageInfo)
|
|
||||||
assertEquals(listOf(recordMax.first), records)
|
|
||||||
|
|
||||||
// write exceeding key length record
|
|
||||||
if (isNextcloud()) {
|
|
||||||
// Nextcloud simply refuses to write long filenames
|
|
||||||
coAssertThrows(IOException::class.java) {
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first)
|
|
||||||
.writeAndClose(recordOver.second)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
coAssertThrows(IllegalStateException::class.java) {
|
|
||||||
kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first)
|
|
||||||
.writeAndClose(recordOver.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testFullBackupRestore() = runBlocking {
|
|
||||||
// define shortcuts
|
|
||||||
val fullBackup = backupPlugin.fullBackupPlugin
|
|
||||||
val fullRestore = restorePlugin.fullRestorePlugin
|
|
||||||
|
|
||||||
// initialize storage with given token
|
|
||||||
initStorage(token)
|
|
||||||
|
|
||||||
// no data available initially
|
// no data available initially
|
||||||
assertFalse(fullRestore.hasDataForPackage(token, packageInfo))
|
assertFalse(backupPlugin.hasData(token, name1))
|
||||||
assertFalse(fullRestore.hasDataForPackage(token, packageInfo2))
|
assertFalse(backupPlugin.hasData(token, name2))
|
||||||
|
|
||||||
// write full backup data
|
// write full backup data
|
||||||
val data = getRandomByteArray(5 * 1024 * 1024)
|
val data = getRandomByteArray(5 * 1024 * 1024)
|
||||||
fullBackup.getOutputStream(packageInfo).writeAndClose(data)
|
backupPlugin.getOutputStream(token, name1).writeAndClose(data)
|
||||||
|
|
||||||
// data is available now, but only this token
|
// data is available now, but only this token
|
||||||
assertTrue(fullRestore.hasDataForPackage(token, packageInfo))
|
assertTrue(backupPlugin.hasData(token, name1))
|
||||||
assertFalse(fullRestore.hasDataForPackage(token + 1, packageInfo))
|
assertFalse(backupPlugin.hasData(token + 1, name1))
|
||||||
|
|
||||||
// restore data matches backed up data
|
// restore data matches backed up data
|
||||||
assertReadEquals(data, fullRestore.getInputStreamForPackage(token, packageInfo))
|
assertReadEquals(data, backupPlugin.getInputStream(token, name1))
|
||||||
|
|
||||||
// write and check data for second package
|
// write and check data for second package
|
||||||
val data2 = getRandomByteArray(5 * 1024 * 1024)
|
val data2 = getRandomByteArray(5 * 1024 * 1024)
|
||||||
fullBackup.getOutputStream(packageInfo2).writeAndClose(data2)
|
backupPlugin.getOutputStream(token, name2).writeAndClose(data2)
|
||||||
assertTrue(fullRestore.hasDataForPackage(token, packageInfo2))
|
assertTrue(backupPlugin.hasData(token, name2))
|
||||||
assertReadEquals(data2, fullRestore.getInputStreamForPackage(token, packageInfo2))
|
assertReadEquals(data2, backupPlugin.getInputStream(token, name2))
|
||||||
|
|
||||||
// remove data of first package again and ensure that no more data is found
|
// remove data of first package again and ensure that no more data is found
|
||||||
fullBackup.removeDataOfPackage(packageInfo)
|
backupPlugin.removeData(token, name1)
|
||||||
assertFalse(fullRestore.hasDataForPackage(token, packageInfo))
|
assertFalse(backupPlugin.hasData(token, name1))
|
||||||
|
|
||||||
// second package is still there
|
// second package is still there
|
||||||
assertTrue(fullRestore.hasDataForPackage(token, packageInfo2))
|
assertTrue(backupPlugin.hasData(token, name2))
|
||||||
|
|
||||||
// ensure that it gets deleted as well
|
// ensure that it gets deleted as well
|
||||||
fullBackup.removeDataOfPackage(packageInfo2)
|
backupPlugin.removeData(token, name2)
|
||||||
assertFalse(fullRestore.hasDataForPackage(token, packageInfo2))
|
assertFalse(backupPlugin.hasData(token, name2))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initStorage(token: Long) = runBlocking {
|
private fun initStorage(token: Long) = runBlocking {
|
||||||
|
@ -349,8 +222,4 @@ class PluginTest : KoinComponent {
|
||||||
backupPlugin.initializeDevice()
|
backupPlugin.initializeDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNextcloud(): Boolean {
|
|
||||||
return backupPlugin.providerPackageName?.startsWith("com.nextcloud") ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.seedvault.transport.backup.EncryptedMetadata
|
import com.stevesoltys.seedvault.transport.backup.EncryptedMetadata
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -19,9 +17,7 @@ private val TAG = DocumentsProviderBackupPlugin::class.java.simpleName
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class DocumentsProviderBackupPlugin(
|
internal class DocumentsProviderBackupPlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage,
|
private val storage: DocumentsStorage
|
||||||
override val kvBackupPlugin: KVBackupPlugin,
|
|
||||||
override val fullBackupPlugin: FullBackupPlugin
|
|
||||||
) : BackupPlugin {
|
) : BackupPlugin {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
|
@ -45,8 +41,7 @@ internal class DocumentsProviderBackupPlugin(
|
||||||
storage.reset(null)
|
storage.reset(null)
|
||||||
|
|
||||||
// create backup folders
|
// create backup folders
|
||||||
storage.currentKvBackupDir ?: throw IOException()
|
storage.currentSetDir ?: throw IOException()
|
||||||
storage.currentFullBackupDir ?: throw IOException()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.util.Log
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.DEFAULT_QUOTA_FULL_BACKUP
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
private val TAG = DocumentsProviderFullBackup::class.java.simpleName
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class DocumentsProviderFullBackup(
|
|
||||||
private val context: Context,
|
|
||||||
private val storage: DocumentsStorage
|
|
||||||
) : FullBackupPlugin {
|
|
||||||
|
|
||||||
override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream {
|
|
||||||
val file = storage.currentFullBackupDir?.createOrGetFile(context, targetPackage.packageName)
|
|
||||||
?: throw IOException()
|
|
||||||
return storage.getOutputStream(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) {
|
|
||||||
val packageName = packageInfo.packageName
|
|
||||||
Log.i(TAG, "Deleting $packageName...")
|
|
||||||
val file = storage.currentFullBackupDir?.findFileBlocking(context, packageName)
|
|
||||||
?: return
|
|
||||||
if (!file.delete()) throw IOException("Failed to delete $packageName")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -6,7 +6,8 @@ import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation")
|
||||||
|
@Deprecated("Use only for v0 restore")
|
||||||
internal class DocumentsProviderFullRestorePlugin(
|
internal class DocumentsProviderFullRestorePlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val documentsStorage: DocumentsStorage
|
private val documentsStorage: DocumentsStorage
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.DEFAULT_QUOTA_KEY_VALUE_BACKUP
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
const val MAX_KEY_LENGTH = 255
|
|
||||||
const val MAX_KEY_LENGTH_NEXTCLOUD = 225
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class DocumentsProviderKVBackup(
|
|
||||||
private val context: Context,
|
|
||||||
private val storage: DocumentsStorage
|
|
||||||
) : KVBackupPlugin {
|
|
||||||
|
|
||||||
private var packageFile: DocumentFile? = null
|
|
||||||
private var packageChildren: List<DocumentFile>? = null
|
|
||||||
|
|
||||||
override fun getQuota(): Long = DEFAULT_QUOTA_KEY_VALUE_BACKUP
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
|
|
||||||
// get the folder for the package (or create it) and all files in it
|
|
||||||
val dir =
|
|
||||||
storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName)
|
|
||||||
val children = dir.listFilesBlocking(context)
|
|
||||||
// cache package file for subsequent operations
|
|
||||||
packageFile = dir
|
|
||||||
// also cache children as doing this for every record is super slow
|
|
||||||
packageChildren = children
|
|
||||||
return children.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun getOutputStreamForRecord(
|
|
||||||
packageInfo: PackageInfo,
|
|
||||||
key: String
|
|
||||||
): OutputStream {
|
|
||||||
// check maximum key lengths
|
|
||||||
check(key.length <= MAX_KEY_LENGTH) {
|
|
||||||
"Key $key for ${packageInfo.packageName} is too long: ${key.length} chars."
|
|
||||||
}
|
|
||||||
if (key.length > MAX_KEY_LENGTH_NEXTCLOUD) {
|
|
||||||
Log.e(
|
|
||||||
DocumentsProviderKVBackup::class.java.simpleName,
|
|
||||||
"Key $key for ${packageInfo.packageName} is too long: ${key.length} chars."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// get dir and children from cache
|
|
||||||
val packageFile = this.packageFile
|
|
||||||
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
|
|
||||||
packageFile.assertRightFile(packageInfo)
|
|
||||||
val children = packageChildren
|
|
||||||
?: throw AssertionError("No cached children for ${packageInfo.packageName}")
|
|
||||||
|
|
||||||
// get file for key from cache,
|
|
||||||
val keyFile = children.find { it.name == key } // try cache first
|
|
||||||
?: packageFile.createFile(MIME_TYPE, key) // assume it doesn't exist, create it
|
|
||||||
?: packageFile.createOrGetFile(context, key) // cache was stale, so try to find it
|
|
||||||
check(keyFile.name == key) { "Key file named ${keyFile.name}, but should be $key" }
|
|
||||||
return storage.getOutputStream(keyFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun deleteRecord(packageInfo: PackageInfo, key: String) {
|
|
||||||
val packageFile = this.packageFile
|
|
||||||
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
|
|
||||||
packageFile.assertRightFile(packageInfo)
|
|
||||||
|
|
||||||
val children = packageChildren
|
|
||||||
?: throw AssertionError("No cached children for ${packageInfo.packageName}")
|
|
||||||
|
|
||||||
// try to find file for given key and delete it if found
|
|
||||||
val keyFile = children.find { it.name == key } // try to find in cache
|
|
||||||
?: packageFile.findFileBlocking(context, key) // fall-back to provider
|
|
||||||
?: return // not found, nothing left to do
|
|
||||||
keyFile.delete()
|
|
||||||
|
|
||||||
// we don't update the children cache as deleted records
|
|
||||||
// are not expected to get re-added in the same backup pass
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) {
|
|
||||||
val packageFile = this.packageFile
|
|
||||||
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
|
|
||||||
packageFile.assertRightFile(packageInfo)
|
|
||||||
// We are not using the cached children here in case they are stale.
|
|
||||||
// This operation isn't frequent, so we don't need to heavily optimize it.
|
|
||||||
packageFile.deleteContents(context)
|
|
||||||
// clear children cache
|
|
||||||
packageChildren = ArrayList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun packageFinished(packageInfo: PackageInfo) {
|
|
||||||
packageFile = null
|
|
||||||
packageChildren = null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -7,7 +7,8 @@ import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation")
|
||||||
|
@Deprecated("Use only for v0 restore")
|
||||||
internal class DocumentsProviderKVRestorePlugin(
|
internal class DocumentsProviderKVRestorePlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage
|
private val storage: DocumentsStorage
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
|
||||||
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
|
||||||
|
@ -12,9 +10,7 @@ import org.koin.dsl.module
|
||||||
val documentsProviderModule = module {
|
val documentsProviderModule = module {
|
||||||
single { DocumentsStorage(androidContext(), get()) }
|
single { DocumentsStorage(androidContext(), get()) }
|
||||||
|
|
||||||
single<KVBackupPlugin> { DocumentsProviderKVBackup(androidContext(), get()) }
|
single<BackupPlugin> { DocumentsProviderBackupPlugin(androidContext(), get()) }
|
||||||
single<FullBackupPlugin> { DocumentsProviderFullBackup(androidContext(), get()) }
|
|
||||||
single<BackupPlugin> { DocumentsProviderBackupPlugin(androidContext(), get(), get(), get()) }
|
|
||||||
|
|
||||||
single<KVRestorePlugin> { DocumentsProviderKVRestorePlugin(androidContext(), get()) }
|
single<KVRestorePlugin> { DocumentsProviderKVRestorePlugin(androidContext(), get()) }
|
||||||
single<FullRestorePlugin> { DocumentsProviderFullRestorePlugin(androidContext(), get()) }
|
single<FullRestorePlugin> { DocumentsProviderFullRestorePlugin(androidContext(), get()) }
|
||||||
|
|
|
@ -27,7 +27,9 @@ import java.io.OutputStream
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
|
const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
|
||||||
|
@Deprecated("")
|
||||||
const val DIRECTORY_FULL_BACKUP = "full"
|
const val DIRECTORY_FULL_BACKUP = "full"
|
||||||
|
@Deprecated("")
|
||||||
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
||||||
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
||||||
const val FILE_NO_MEDIA = ".nomedia"
|
const val FILE_NO_MEDIA = ".nomedia"
|
||||||
|
@ -73,7 +75,7 @@ internal class DocumentsStorage(
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentSetDir: DocumentFile? = null
|
var currentSetDir: DocumentFile? = null
|
||||||
get() = runBlocking {
|
get() = runBlocking {
|
||||||
if (field == null) {
|
if (field == null) {
|
||||||
if (currentToken == 0L) return@runBlocking null
|
if (currentToken == 0L) return@runBlocking null
|
||||||
|
@ -86,32 +88,7 @@ internal class DocumentsStorage(
|
||||||
}
|
}
|
||||||
field
|
field
|
||||||
}
|
}
|
||||||
|
private set
|
||||||
var currentFullBackupDir: DocumentFile? = null
|
|
||||||
get() = runBlocking {
|
|
||||||
if (field == null) {
|
|
||||||
field = try {
|
|
||||||
currentSetDir?.createOrGetDirectory(context, DIRECTORY_FULL_BACKUP)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, "Error creating full backup dir.", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentKvBackupDir: DocumentFile? = null
|
|
||||||
get() = runBlocking {
|
|
||||||
if (field == null) {
|
|
||||||
field = try {
|
|
||||||
currentSetDir?.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, "Error creating K/V backup dir.", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets this storage abstraction, forcing it to re-fetch cached values on next access.
|
* Resets this storage abstraction, forcing it to re-fetch cached values on next access.
|
||||||
|
@ -121,8 +98,6 @@ internal class DocumentsStorage(
|
||||||
currentToken = newToken
|
currentToken = newToken
|
||||||
rootBackupDir = null
|
rootBackupDir = null
|
||||||
currentSetDir = null
|
currentSetDir = null
|
||||||
currentKvBackupDir = null
|
|
||||||
currentFullBackupDir = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAuthority(): String? = storage?.uri?.authority
|
fun getAuthority(): String? = storage?.uri?.authority
|
||||||
|
@ -134,23 +109,16 @@ internal class DocumentsStorage(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun getKVBackupDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
|
@Suppress("Deprecation")
|
||||||
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
@Deprecated("Use only for v0 restore")
|
||||||
|
suspend fun getKVBackupDir(token: Long): DocumentFile? {
|
||||||
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
|
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun getOrCreateKVBackupDir(
|
@Suppress("Deprecation")
|
||||||
token: Long = currentToken ?: error("no token")
|
@Deprecated("Use only for v0 restore")
|
||||||
): DocumentFile {
|
suspend fun getFullBackupDir(token: Long): DocumentFile? {
|
||||||
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
|
||||||
val setDir = getSetDir(token) ?: throw IOException()
|
|
||||||
return setDir.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun getFullBackupDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
|
|
||||||
if (token == currentToken) return currentFullBackupDir ?: throw IOException()
|
|
||||||
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
|
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,6 @@ import java.io.OutputStream
|
||||||
|
|
||||||
interface BackupPlugin {
|
interface BackupPlugin {
|
||||||
|
|
||||||
@Deprecated("Use methods in this interface instead")
|
|
||||||
val kvBackupPlugin: KVBackupPlugin
|
|
||||||
|
|
||||||
@Deprecated("Use methods in this interface instead")
|
|
||||||
val fullBackupPlugin: FullBackupPlugin
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new [RestoreSet] with the given token.
|
* Start a new [RestoreSet] with the given token.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
@Deprecated("Use BackupPlugin instead")
|
|
||||||
interface FullBackupPlugin {
|
|
||||||
|
|
||||||
fun getQuota(): Long
|
|
||||||
|
|
||||||
// TODO consider using a salted hash for the package name to not leak it to the storage server
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all data associated with the given package.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun removeDataOfPackage(packageInfo: PackageInfo)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
@Deprecated("Use BackupPlugin instead")
|
|
||||||
interface KVBackupPlugin {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get quota for key/value backups.
|
|
||||||
*/
|
|
||||||
fun getQuota(): Long
|
|
||||||
|
|
||||||
// TODO consider using a salted hash for the package name (and key) to not leak it to the storage server
|
|
||||||
/**
|
|
||||||
* Return true if there are records stored for the given package.
|
|
||||||
* This is always called first per [PackageInfo], before subsequent methods.
|
|
||||||
*
|
|
||||||
* Independent of the return value, the storage should now be prepared to store K/V pairs.
|
|
||||||
* E.g. file-based plugins should a create a directory for the package, if none exists.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an [OutputStream] for the given package and key
|
|
||||||
* which will receive the record's encrypted value.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun getOutputStreamForRecord(packageInfo: PackageInfo, key: String): OutputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the record for the given package identified by the given key.
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun deleteRecord(packageInfo: PackageInfo, key: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all data associated with the given package,
|
|
||||||
* but be prepared to receive new records afterwards with [getOutputStreamForRecord].
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun removeDataOfPackage(packageInfo: PackageInfo)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The package finished backup.
|
|
||||||
* This can be an opportunity to clear existing caches or to do other clean-up work.
|
|
||||||
*/
|
|
||||||
fun packageFinished(packageInfo: PackageInfo)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -32,7 +31,7 @@ fun getRandomString(size: Int = Random.nextInt(1, 255)): String {
|
||||||
private val base64CharPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') // + '+' + '_' + '='
|
private val base64CharPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') // + '+' + '_' + '='
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
fun getRandomBase64(size: Int = Random.nextInt(1, MAX_KEY_LENGTH_NEXTCLOUD)): String {
|
fun getRandomBase64(size: Int = Random.nextInt(1, 64)): String {
|
||||||
return (1..size)
|
return (1..size)
|
||||||
.map { Random.nextInt(0, base64CharPool.size) }
|
.map { Random.nextInt(0, base64CharPool.size) }
|
||||||
.map(base64CharPool::get)
|
.map(base64CharPool::get)
|
||||||
|
|
|
@ -2,8 +2,6 @@ package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -17,19 +15,11 @@ import org.junit.jupiter.api.Test
|
||||||
internal class BackupPluginTest : BackupTest() {
|
internal class BackupPluginTest : BackupTest() {
|
||||||
|
|
||||||
private val storage = mockk<DocumentsStorage>()
|
private val storage = mockk<DocumentsStorage>()
|
||||||
private val kvBackupPlugin: KVBackupPlugin = mockk<DocumentsProviderKVBackup>()
|
|
||||||
private val fullBackupPlugin: FullBackupPlugin = mockk<DocumentsProviderFullBackup>()
|
|
||||||
|
|
||||||
private val plugin = DocumentsProviderBackupPlugin(
|
private val plugin = DocumentsProviderBackupPlugin(context, storage)
|
||||||
context,
|
|
||||||
storage,
|
|
||||||
kvBackupPlugin,
|
|
||||||
fullBackupPlugin
|
|
||||||
)
|
|
||||||
|
|
||||||
private val setDir: DocumentFile = mockk()
|
private val setDir: DocumentFile = mockk()
|
||||||
private val kvDir: DocumentFile = mockk()
|
private val backupFile: DocumentFile = mockk()
|
||||||
private val fullDir: DocumentFile = mockk()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// to mock extension functions on DocumentFile
|
// to mock extension functions on DocumentFile
|
||||||
|
@ -51,13 +41,12 @@ internal class BackupPluginTest : BackupTest() {
|
||||||
every { settingsManager.getToken() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
coEvery { storage.getSetDir(token) } returns setDir
|
coEvery { storage.getSetDir(token) } returns setDir
|
||||||
// delete contents of current set dir
|
// delete contents of current set dir
|
||||||
coEvery { setDir.listFilesBlocking(context) } returns listOf(kvDir)
|
coEvery { setDir.listFilesBlocking(context) } returns listOf(backupFile)
|
||||||
every { kvDir.delete() } returns true
|
every { backupFile.delete() } returns true
|
||||||
// reset storage
|
// reset storage
|
||||||
every { storage.reset(null) } just Runs
|
every { storage.reset(null) } just Runs
|
||||||
// create kv and full dir
|
// create new set dir
|
||||||
every { storage getProperty "currentKvBackupDir" } returns kvDir
|
every { storage getProperty "currentSetDir" } returns setDir
|
||||||
every { storage getProperty "currentFullBackupDir" } returns fullDir
|
|
||||||
|
|
||||||
plugin.initializeDevice()
|
plugin.initializeDevice()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue