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 { 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()) }
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue