Clear existing storage snapshots from storage medium

because that scenario isn't supported at the moment
This commit is contained in:
Torsten Grote 2021-04-20 11:29:16 -03:00 committed by Chirayu Desai
parent f373f4bb97
commit 342bd2068a
13 changed files with 82 additions and 16 deletions

View file

@ -47,7 +47,7 @@ open class App : Application() {
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { FileSelectionViewModel(this@App, get()) } viewModel { FileSelectionViewModel(this@App, get()) }

View file

@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.transport.requestBackup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.calyxos.backup.storage.api.StorageBackup
import java.io.IOException import java.io.IOException
private val TAG = BackupStorageViewModel::class.java.simpleName private val TAG = BackupStorageViewModel::class.java.simpleName
@ -24,6 +25,7 @@ internal class BackupStorageViewModel(
private val app: Application, private val app: Application,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator, private val backupCoordinator: BackupCoordinator,
private val storageBackup: StorageBackup,
settingsManager: SettingsManager settingsManager: SettingsManager
) : StorageViewModel(app, settingsManager) { ) : StorageViewModel(app, settingsManager) {
@ -32,6 +34,9 @@ internal class BackupStorageViewModel(
override fun onLocationSet(uri: Uri) { override fun onLocationSet(uri: Uri) {
val isUsb = saveStorage(uri) val isUsb = saveStorage(uri)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
// remove old storage snapshots and clear cache
storageBackup.deleteAllSnapshots()
storageBackup.clearCache()
try { try {
// will also generate a new backup token for the new restore set // will also generate a new backup token for the new restore set
backupCoordinator.startNewRestoreSet() backupCoordinator.startNewRestoreSet()

View file

@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.storage
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
@ -37,7 +38,7 @@ class StorageCheckFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
val v: View = inflater.inflate(R.layout.fragment_storage_check, container, false) val v: View = inflater.inflate(R.layout.fragment_storage_check, container, false)
titleView = v.findViewById(R.id.titleView) titleView = v.findViewById(R.id.titleView)
@ -55,6 +56,7 @@ class StorageCheckFragment : Fragment() {
val errorMsg = requireArguments().getString(ERROR_MSG) val errorMsg = requireArguments().getString(ERROR_MSG)
if (errorMsg != null) { if (errorMsg != null) {
view.findViewById<View>(R.id.patienceView).visibility = GONE
progressBar.visibility = INVISIBLE progressBar.visibility = INVISIBLE
errorView.text = errorMsg errorView.text = errorMsg
errorView.visibility = VISIBLE errorView.visibility = VISIBLE

View file

@ -83,6 +83,9 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
backView.setOnClickListener { requireActivity().finishAfterTransition() } backView.setOnClickListener { requireActivity().finishAfterTransition() }
} else { } else {
warningIcon.visibility = VISIBLE warningIcon.visibility = VISIBLE
if (viewModel.hasStorageSet) {
warningText.setText(R.string.storage_fragment_warning_delete)
}
warningText.visibility = VISIBLE warningText.visibility = VISIBLE
divider.visibility = VISIBLE divider.visibility = VISIBLE
} }

View file

@ -41,6 +41,8 @@ internal abstract class StorageViewModel(
private var storageRoot: StorageRoot? = null private var storageRoot: StorageRoot? = null
internal var isSetupWizard: Boolean = false internal var isSetupWizard: Boolean = false
internal val hasStorageSet: Boolean
get() = settingsManager.getStorage() != null
abstract val isRestoreOperation: Boolean abstract val isRestoreOperation: Boolean
companion object { companion object {

View file

@ -29,6 +29,17 @@
app:layout_constraintTop_toBottomOf="@+id/imageView" app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="@string/storage_check_fragment_backup_title" /> tools:text="@string/storage_check_fragment_backup_title" />
<TextView
android:id="@+id/patienceView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center_horizontal"
android:text="@string/storage_check_fragment_patience"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:progressBarStyleLarge" style="?android:progressBarStyleLarge"
@ -37,10 +48,10 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/backButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" app:layout_constraintTop_toBottomOf="@+id/patienceView"
app:layout_constraintVertical_bias="0.0" /> app:layout_constraintVertical_bias="0.0" />
<TextView <TextView
@ -56,7 +67,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" app:layout_constraintTop_toBottomOf="@+id/patienceView"
app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_bias="0.0"
tools:text="@string/storage_check_fragment_backup_error" tools:text="@string/storage_check_fragment_backup_error"
tools:visibility="visible" /> tools:visibility="visible" />

View file

@ -22,24 +22,24 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:gravity="center"
android:text="@string/storage_fragment_backup_title" android:text="@string/storage_fragment_backup_title"
android:textSize="24sp" android:textSize="24sp"
android:gravity="center"
tools:text="Choose where to store backup (is a short title, but it can be longer)"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" /> app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="Choose where to store backup (is a short title, but it can be longer)" />
<ImageView <ImageView
android:id="@+id/warningIcon" android:id="@+id/warningIcon"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:src="@drawable/ic_warning" android:src="@drawable/ic_warning"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/warningText"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" app:layout_constraintTop_toTopOf="@+id/warningText"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:visibility="visible" /> tools:visibility="visible" />

View file

@ -46,6 +46,7 @@
<string name="storage_fragment_backup_title">Choose where to store backups</string> <string name="storage_fragment_backup_title">Choose where to store backups</string>
<string name="storage_fragment_restore_title">Where to find your backups?</string> <string name="storage_fragment_restore_title">Where to find your backups?</string>
<string name="storage_fragment_warning">People with access to your storage location can learn which apps you use, but do not get access to the apps\' data.</string> <string name="storage_fragment_warning">People with access to your storage location can learn which apps you use, but do not get access to the apps\' data.</string>
<string name="storage_fragment_warning_delete">Existing backups in this location will be deleted.</string>
<string name="storage_fake_drive_title">USB flash drive</string> <string name="storage_fake_drive_title">USB flash drive</string>
<string name="storage_fake_drive_summary">Needs to be plugged in</string> <string name="storage_fake_drive_summary">Needs to be plugged in</string>
<string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string> <string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string>
@ -54,6 +55,7 @@
<string name="storage_fake_nextcloud_summary_installed">Tap to set up account</string> <string name="storage_fake_nextcloud_summary_installed">Tap to set up account</string>
<string name="storage_fake_nextcloud_summary_unavailable">Account not available. Set one up (or disable passcode).</string> <string name="storage_fake_nextcloud_summary_unavailable">Account not available. Set one up (or disable passcode).</string>
<string name="storage_check_fragment_backup_title">Initializing backup location…</string> <string name="storage_check_fragment_backup_title">Initializing backup location…</string>
<string name="storage_check_fragment_patience">This may take some time…</string>
<string name="storage_check_fragment_restore_title">Looking for backups…</string> <string name="storage_check_fragment_restore_title">Looking for backups…</string>
<string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string> <string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string>
<string name="storage_check_fragment_permission_error">Unable to get the permission to write to the backup location.</string> <string name="storage_check_fragment_permission_error">Unable to get the permission to write to the backup location.</string>

View file

@ -8,3 +8,8 @@ Please see the [design document](doc/design.md) for more information.
There is also a [demo app](demo) that illustrates the working of the library There is also a [demo app](demo) that illustrates the working of the library
and does not need to be a system app with elevated permissions. and does not need to be a system app with elevated permissions.
It can be built and installed as a regular app requesting permissions at runtime. It can be built and installed as a regular app requesting permissions at runtime.
## Limitations
The design document mentions several limitations of this initial implementation.
One of them is that you cannot backup more than one device to the same storage location.

View file

@ -89,7 +89,12 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
} }
fun setBackupLocation(uri: Uri?) { fun setBackupLocation(uri: Uri?) {
if (uri != null) clearDb() if (uri != null) {
viewModelScope.launch(Dispatchers.IO) {
storageBackup.deleteAllSnapshots()
storageBackup.clearCache()
}
}
settingsManager.setBackupLocation(uri) settingsManager.setBackupLocation(uri)
} }

View file

@ -26,6 +26,7 @@ import org.calyxos.backup.storage.scanner.DocumentScanner
import org.calyxos.backup.storage.scanner.FileScanner import org.calyxos.backup.storage.scanner.FileScanner
import org.calyxos.backup.storage.scanner.MediaScanner import org.calyxos.backup.storage.scanner.MediaScanner
import org.calyxos.backup.storage.toStoredUri import org.calyxos.backup.storage.toStoredUri
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
private const val TAG = "StorageBackup" private const val TAG = "StorageBackup"
@ -33,7 +34,7 @@ private const val TAG = "StorageBackup"
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
public class StorageBackup( public class StorageBackup(
private val context: Context, private val context: Context,
plugin: StoragePlugin, private val plugin: StoragePlugin,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO, private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) { ) {
@ -98,9 +99,31 @@ public class StorageBackup(
list.joinToString(", ", limit = 5) list.joinToString(", ", limit = 5)
} }
@Deprecated("TODO remove for release") /**
public fun clearCache() { * Run this on a new storage location to ensure that there are no old snapshots
db.clearAllTables() * (potentially encrypted with an old key) laying around.
* Using a storage location with existing data is not supported.
*/
public suspend fun deleteAllSnapshots(): Unit = withContext(dispatcher) {
try {
plugin.getAvailableBackupSnapshots().forEach {
try {
plugin.deleteBackupSnapshot(it)
} catch (e: IOException) {
Log.e(TAG, "Error deleting snapshot $it", e)
}
}
} catch (e: IOException) {
Log.e(TAG, "Error deleting all snapshots", e)
}
}
/**
* It is advised to clear existing cache when selecting a new storage location.
*/
public suspend fun clearCache(): Unit = withContext(dispatcher) {
db.getChunksCache().clear()
db.getFilesCache().clear()
} }
public suspend fun runBackup(backupObserver: BackupObserver?): Boolean = public suspend fun runBackup(backupObserver: BackupObserver?): Boolean =

View file

@ -37,6 +37,9 @@ internal interface FilesCache {
@Update @Update
fun update(file: CachedFile) fun update(file: CachedFile)
@Query("DELETE FROM CachedFile")
fun clear()
@Query("UPDATE CachedFile SET last_seen = :now WHERE uri IN (:uris)") @Query("UPDATE CachedFile SET last_seen = :now WHERE uri IN (:uris)")
fun updateLastSeen(uris: Collection<Uri>, now: Long = System.currentTimeMillis()) fun updateLastSeen(uris: Collection<Uri>, now: Long = System.currentTimeMillis())

View file

@ -1,5 +1,6 @@
package org.calyxos.backup.storage.plugin package org.calyxos.backup.storage.plugin
import com.google.protobuf.InvalidProtocolBufferException
import org.calyxos.backup.storage.api.StoragePlugin import org.calyxos.backup.storage.api.StoragePlugin
import org.calyxos.backup.storage.backup.BackupSnapshot import org.calyxos.backup.storage.backup.BackupSnapshot
import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.backup.storage.crypto.StreamCrypto
@ -13,7 +14,11 @@ internal class SnapshotRetriever(
private val streamCrypto: StreamCrypto = StreamCrypto, private val streamCrypto: StreamCrypto = StreamCrypto,
) { ) {
@Throws(IOException::class, GeneralSecurityException::class) @Throws(
IOException::class,
GeneralSecurityException::class,
InvalidProtocolBufferException::class,
)
suspend fun getSnapshot(streamKey: ByteArray, timestamp: Long): BackupSnapshot { suspend fun getSnapshot(streamKey: ByteArray, timestamp: Long): BackupSnapshot {
return storagePlugin.getBackupSnapshotInputStream(timestamp).use { inputStream -> return storagePlugin.getBackupSnapshotInputStream(timestamp).use { inputStream ->
val version = inputStream.readVersion() val version = inputStream.readVersion()