Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify Storage Backends #734

Merged
merged 12 commits into from
Sep 19, 2024
4 changes: 2 additions & 2 deletions Android.bp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ android_app {
"com.google.android.material_material",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
// storage backup lib
// our own gradle module libs
"seedvault-lib-core",
"seedvault-lib-storage",
// koin
"seedvault-lib-koin-core-jvm", // did not manage to add this as transitive dependency
Expand All @@ -36,7 +37,6 @@ android_app {
// WebDAV
"seedvault-lib-dav4jvm",
"seedvault-lib-okhttp",
"seedvault-lib-okio",
],
manifest: "app/src/main/AndroidManifest.xml",

Expand Down
18 changes: 4 additions & 14 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,7 @@ android {
}

dependencies {

val aospLibs = fileTree("$projectDir/libs") {
// For more information about this module:
// https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-11.0.0_r3/Android.bp#507
// framework_intermediates/classes-header.jar works for gradle build as well,
// but not unit tests, so we use the actual classes (without updatable modules).
//
// out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/classes.jar
include("android.jar")
// out/target/common/obj/JAVA_LIBRARIES/core-libart.com.android.art_intermediates/classes.jar
include("libcore.jar")
}

val aospLibs: FileTree by rootProject.extra
compileOnly(aospLibs)

/**
Expand Down Expand Up @@ -149,6 +137,7 @@ dependencies {
/**
* Storage Dependencies
*/
implementation(project(":core"))
implementation(project(":storage:lib"))

/**
Expand Down Expand Up @@ -188,6 +177,7 @@ dependencies {
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:${libs.versions.junit5.get()}")

androidTestImplementation(aospLibs)
androidTestImplementation(kotlin("test"))
androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
Expand All @@ -197,7 +187,7 @@ dependencies {

gradle.projectsEvaluated {
tasks.withType(JavaCompile::class) {
options.compilerArgs.add("-Xbootclasspath/p:app/libs/android.jar:app/libs/libcore.jar")
options.compilerArgs.add("-Xbootclasspath/p:libs/aosp/android.jar:libs/aosp/libcore.jar")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class KoinInstrumentationTestApp : App() {
apkRestore = get(),
iconManager = get(),
storageBackup = get(),
pluginManager = get(),
backendManager = get(),
fileSelectionManager = get(),
)
)
Expand Down
104 changes: 36 additions & 68 deletions app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,23 @@

package com.stevesoltys.seedvault

import android.net.Uri
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.StoragePlugin
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.plugins.saf.deleteContents
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.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
Expand All @@ -42,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: StoragePlugin<Uri> = DocumentsProviderStoragePlugin(context, storage)
private val backend = SafBackend(context, storage.safStorage)

@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin = DocumentsProviderLegacyPlugin(context) {
Expand All @@ -59,30 +55,30 @@ class PluginTest : KoinComponent {

@Before
fun setup() = runBlocking {
every { mockedSettingsManager.getSafStorage() } returns settingsManager.getSafStorage()
storage.rootBackupDir?.deleteContents(context)
?: error("Select a storage location in the app first!")
every {
mockedSettingsManager.getSafProperties()
} returns settingsManager.getSafProperties()
backend.removeAll()
}

@After
fun tearDown() = runBlocking {
storage.rootBackupDir?.deleteContents(context)
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)
}

Expand All @@ -96,52 +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)

// ensure that the new backup dir exist
assertTrue(storage.currentSetDir!!.exists())
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)
Expand All @@ -150,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)
Expand All @@ -169,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(
Expand All @@ -181,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
Expand All @@ -199,42 +182,27 @@ class PluginTest : KoinComponent {
val name1 = getRandomBase64()
val name2 = getRandomBase64()

// no data available initially
assertFalse(storagePlugin.hasData(token, name1))
assertFalse(storagePlugin.hasData(token, name2))

// write full backup data
val data = getRandomByteArray(5 * 1024 * 1024)
storagePlugin.getOutputStream(token, name1).writeAndClose(data)

// data is available now, but only this token
assertTrue(storagePlugin.hasData(token, name1))
assertFalse(storagePlugin.hasData(token + 1, name1))
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)
assertTrue(storagePlugin.hasData(token, name2))
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)
assertFalse(storagePlugin.hasData(token, name1))

// second package is still there
assertTrue(storagePlugin.hasData(token, name2))
backend.remove(LegacyAppBackupFile.Blob(token, name1))

// ensure that it gets deleted as well
storagePlugin.removeData(token, name2)
assertFalse(storagePlugin.hasData(token, name2))
backend.remove(LegacyAppBackupFile.Blob(token, name2))
}

private fun initStorage(token: Long) = runBlocking {
every { mockedSettingsManager.getToken() } returns token
storagePlugin.initializeDevice()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/

package com.stevesoltys.seedvault.backend.saf

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.settings.SettingsManager
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.SafProperties
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

@RunWith(AndroidJUnit4::class)
@MediumTest
class SafBackendTest : BackendTest(), KoinComponent {

private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager by inject<SettingsManager>()
private val safStorage = settingsManager.getSafProperties() ?: error("No SAF storage")
private val safProperties = SafProperties(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
override val plugin: Backend = SafBackend(context, safProperties, ".SeedvaultTest")

@Test
fun `test write list read rename delete`(): Unit = runBlocking {
testWriteListReadRenameDelete()
}

@Test
fun `test remove create write file`(): Unit = runBlocking {
testRemoveCreateWriteFile()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
confirmCode()
}

if (settingsManager.getSafStorage() == null) {
if (settingsManager.getSafProperties() == null) {
chooseStorageLocation()
} else {
changeBackupLocation()
Expand Down
Loading
Loading