Clear existing storage snapshots from storage medium
because that scenario isn't supported at the moment
This commit is contained in:
parent
f373f4bb97
commit
342bd2068a
13 changed files with 82 additions and 16 deletions
|
@ -47,7 +47,7 @@ open class App : Application() {
|
|||
|
||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), 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 { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
|||
import com.stevesoltys.seedvault.transport.requestBackup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.calyxos.backup.storage.api.StorageBackup
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||
|
@ -24,6 +25,7 @@ internal class BackupStorageViewModel(
|
|||
private val app: Application,
|
||||
private val backupManager: IBackupManager,
|
||||
private val backupCoordinator: BackupCoordinator,
|
||||
private val storageBackup: StorageBackup,
|
||||
settingsManager: SettingsManager
|
||||
) : StorageViewModel(app, settingsManager) {
|
||||
|
||||
|
@ -32,6 +34,9 @@ internal class BackupStorageViewModel(
|
|||
override fun onLocationSet(uri: Uri) {
|
||||
val isUsb = saveStorage(uri)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// remove old storage snapshots and clear cache
|
||||
storageBackup.deleteAllSnapshots()
|
||||
storageBackup.clearCache()
|
||||
try {
|
||||
// will also generate a new backup token for the new restore set
|
||||
backupCoordinator.startNewRestoreSet()
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.storage
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
|
@ -37,7 +38,7 @@ class StorageCheckFragment : Fragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
val v: View = inflater.inflate(R.layout.fragment_storage_check, container, false)
|
||||
|
||||
titleView = v.findViewById(R.id.titleView)
|
||||
|
@ -55,6 +56,7 @@ class StorageCheckFragment : Fragment() {
|
|||
|
||||
val errorMsg = requireArguments().getString(ERROR_MSG)
|
||||
if (errorMsg != null) {
|
||||
view.findViewById<View>(R.id.patienceView).visibility = GONE
|
||||
progressBar.visibility = INVISIBLE
|
||||
errorView.text = errorMsg
|
||||
errorView.visibility = VISIBLE
|
||||
|
|
|
@ -83,6 +83,9 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
|||
backView.setOnClickListener { requireActivity().finishAfterTransition() }
|
||||
} else {
|
||||
warningIcon.visibility = VISIBLE
|
||||
if (viewModel.hasStorageSet) {
|
||||
warningText.setText(R.string.storage_fragment_warning_delete)
|
||||
}
|
||||
warningText.visibility = VISIBLE
|
||||
divider.visibility = VISIBLE
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ internal abstract class StorageViewModel(
|
|||
private var storageRoot: StorageRoot? = null
|
||||
|
||||
internal var isSetupWizard: Boolean = false
|
||||
internal val hasStorageSet: Boolean
|
||||
get() = settingsManager.getStorage() != null
|
||||
abstract val isRestoreOperation: Boolean
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -29,6 +29,17 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
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
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:progressBarStyleLarge"
|
||||
|
@ -37,10 +48,10 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/backButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/patienceView"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
<TextView
|
||||
|
@ -56,7 +67,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/patienceView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="@string/storage_check_fragment_backup_error"
|
||||
tools:visibility="visible" />
|
||||
|
|
|
@ -22,24 +22,24 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/storage_fragment_backup_title"
|
||||
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_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
|
||||
android:id="@+id/warningIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:src="@drawable/ic_warning"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/warningText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||
app:layout_constraintTop_toTopOf="@+id/warningText"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
<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_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_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>
|
||||
|
@ -54,6 +55,7 @@
|
|||
<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_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_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>
|
||||
|
|
|
@ -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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -89,7 +89,12 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
|
|||
}
|
||||
|
||||
fun setBackupLocation(uri: Uri?) {
|
||||
if (uri != null) clearDb()
|
||||
if (uri != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
storageBackup.deleteAllSnapshots()
|
||||
storageBackup.clearCache()
|
||||
}
|
||||
}
|
||||
settingsManager.setBackupLocation(uri)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.calyxos.backup.storage.scanner.DocumentScanner
|
|||
import org.calyxos.backup.storage.scanner.FileScanner
|
||||
import org.calyxos.backup.storage.scanner.MediaScanner
|
||||
import org.calyxos.backup.storage.toStoredUri
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
private const val TAG = "StorageBackup"
|
||||
|
@ -33,7 +34,7 @@ private const val TAG = "StorageBackup"
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
public class StorageBackup(
|
||||
private val context: Context,
|
||||
plugin: StoragePlugin,
|
||||
private val plugin: StoragePlugin,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
|
||||
|
@ -98,9 +99,31 @@ public class StorageBackup(
|
|||
list.joinToString(", ", limit = 5)
|
||||
}
|
||||
|
||||
@Deprecated("TODO remove for release")
|
||||
public fun clearCache() {
|
||||
db.clearAllTables()
|
||||
/**
|
||||
* Run this on a new storage location to ensure that there are no old snapshots
|
||||
* (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 =
|
||||
|
|
|
@ -37,6 +37,9 @@ internal interface FilesCache {
|
|||
@Update
|
||||
fun update(file: CachedFile)
|
||||
|
||||
@Query("DELETE FROM CachedFile")
|
||||
fun clear()
|
||||
|
||||
@Query("UPDATE CachedFile SET last_seen = :now WHERE uri IN (:uris)")
|
||||
fun updateLastSeen(uris: Collection<Uri>, now: Long = System.currentTimeMillis())
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.calyxos.backup.storage.plugin
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import org.calyxos.backup.storage.api.StoragePlugin
|
||||
import org.calyxos.backup.storage.backup.BackupSnapshot
|
||||
import org.calyxos.backup.storage.crypto.StreamCrypto
|
||||
|
@ -13,7 +14,11 @@ internal class SnapshotRetriever(
|
|||
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 {
|
||||
return storagePlugin.getBackupSnapshotInputStream(timestamp).use { inputStream ->
|
||||
val version = inputStream.readVersion()
|
||||
|
|
Loading…
Reference in a new issue