Skip to content

Commit

Permalink
Ensure root folder exists when using storage
Browse files Browse the repository at this point in the history
We use the same root folder for app and files backup. App backup usually creates the root folder, but if only storage backup is used, it will be missing and needs to be created.
  • Loading branch information
grote committed Apr 4, 2024
1 parent 298c1d2 commit d899891
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
@OptIn(DelicateCoroutinesApi::class)
internal abstract class WebDavStorage(
webDavConfig: WebDavConfig,
root: String = DIRECTORY_ROOT,
) {

companion object {
Expand All @@ -61,7 +62,7 @@ internal abstract class WebDavStorage(
.retryOnConnectionFailure(true)
.build()

protected val url = "${webDavConfig.url}/$DIRECTORY_ROOT"
protected val url = "${webDavConfig.url}/$root"

@Throws(IOException::class)
protected suspend fun getOutputStream(location: HttpUrl): OutputStream {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import kotlin.coroutines.suspendCoroutine
internal class WebDavStoragePlugin(
context: Context,
webDavConfig: WebDavConfig,
) : WebDavStorage(webDavConfig), StoragePlugin {
root: String = DIRECTORY_ROOT,
) : WebDavStorage(webDavConfig, root), StoragePlugin {

@Throws(IOException::class)
override suspend fun startNewRestoreSet(token: Long) {
Expand All @@ -39,6 +40,20 @@ internal class WebDavStoragePlugin(
override suspend fun initializeDevice() {
// TODO does it make sense to delete anything
// when [startNewRestoreSet] is always called first? Maybe unify both calls?
val location = url.toHttpUrl()
val davCollection = DavCollection(okHttpClient, location)

try {
davCollection.head { response ->
debugLog { "Root exists: $response" }
}
} catch (e: NotFoundException) {
val response = davCollection.createFolder()
debugLog { "initializeDevice() = $response" }
} catch (e: Exception) {
if (e is IOException) throw e
else throw IOException(e)
}
}

@Throws(IOException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.ResourceType
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.plugins.chunkFolderRegex
import com.stevesoltys.seedvault.plugins.webdav.DIRECTORY_ROOT
import com.stevesoltys.seedvault.plugins.webdav.WebDavConfig
import com.stevesoltys.seedvault.plugins.webdav.WebDavStorage
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand All @@ -30,7 +31,8 @@ internal class WebDavStoragePlugin(
*/
androidId: String,
webDavConfig: WebDavConfig,
) : WebDavStorage(webDavConfig), StoragePlugin {
root: String = DIRECTORY_ROOT,
) : WebDavStorage(webDavConfig, root), StoragePlugin {

/**
* The folder name is our user ID plus .sv extension (for SeedVault).
Expand All @@ -39,6 +41,24 @@ internal class WebDavStoragePlugin(
*/
private val folder: String = "$androidId.sv"

@Throws(IOException::class)
override suspend fun init() {
val location = url.toHttpUrl()
val davCollection = DavCollection(okHttpClient, location)

try {
davCollection.head { response ->
debugLog { "Root exists: $response" }
}
} catch (e: NotFoundException) {
val response = davCollection.createFolder()
debugLog { "init() = $response" }
} catch (e: Exception) {
if (e is IOException) throw e
else throw IOException(e)
}
}

@Throws(IOException::class)
override suspend fun getAvailableChunkIds(): List<String> {
val location = "$url/$folder".toHttpUrl()
Expand Down Expand Up @@ -211,18 +231,13 @@ internal class WebDavStoragePlugin(
val match = snapshotRegex.matchEntire(response.hrefName())
if (match != null) {
val timestamp = match.groupValues[1].toLong()
val folderName =
response.href.pathSegments[response.href.pathSegments.size - 2]
val storedSnapshot = StoredSnapshot(folderName, timestamp)
val storedSnapshot = StoredSnapshot(folder, timestamp)
snapshots.add(storedSnapshot)
}
}
}
}
Log.i(TAG, "getCurrentBackupSnapshots took $duration")
} catch (e: NotFoundException) {
debugLog { "Folder not found: $location" }
davCollection.createFolder()
} catch (e: Exception) {
if (e is IOException) throw e
else throw IOException("Error populating chunk folders: ", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ internal class RecoveryCodeViewModel(
// TODO this code is almost identical to BackupStorageViewModel#onLocationSet(), unify?
GlobalScope.launch(Dispatchers.IO) {
// remove old storage snapshots and clear cache
storageBackup.deleteAllSnapshots()
storageBackup.clearCache()
storageBackup.init()
try {
// initialize the new location
if (backupManager.isBackupEnabled) backupInitializer.initialize({ }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ internal class BackupStorageViewModel(
// remove old storage snapshots and clear cache
// TODO is this needed? It also does create all 255 chunk folders which takes time
// pass a flag to getCurrentBackupSnapshots() to not create missing folders?
storageBackup.deleteAllSnapshots()
storageBackup.clearCache()
storageBackup.init()
try {
// initialize the new location (if backups are enabled)
if (backupManager.isBackupEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ internal class WebDavStoragePluginTest : TransportTest() {
val token = System.currentTimeMillis()
val metadata = getRandomByteArray()

// need to initialize, to have root .SeedVaultAndroidBackup folder
plugin.initializeDevice()
plugin.startNewRestoreSet(token)

// initially, we don't have any backups
assertEquals(emptySet<EncryptedMetadata>(), plugin.getAvailableBackups()?.toSet())

// and no data
assertFalse(plugin.hasData(token, FILE_BACKUP_METADATA))

// start a new restore set, initialize it and write out the metadata file
plugin.startNewRestoreSet(token)
plugin.initializeDevice()
// write out the metadata file
plugin.getOutputStream(token, FILE_BACKUP_METADATA).use {
it.write(metadata)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ internal class WebDavStoragePluginTest : BackupTest() {
val chunkId1 = getRandomByteArray(32).toHexString()
val chunkBytes1 = getRandomByteArray()

// init to create root folder
plugin.init()

// first we don't have any chunks
assertEquals(emptyList<String>(), plugin.getAvailableChunkIds())

Expand Down Expand Up @@ -55,6 +58,9 @@ internal class WebDavStoragePluginTest : BackupTest() {
fun `test snapshots`() = runBlocking {
val snapshotBytes = getRandomByteArray()

// init to create root folder
plugin.init()

// first we don't have any snapshots
assertEquals(emptyList<StoredSnapshot>(), plugin.getCurrentBackupSnapshots())
assertEquals(emptyList<StoredSnapshot>(), plugin.getBackupSnapshotsForRestore())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ internal abstract class TransportTest {
mockkStatic(Log::class)
val logTagSlot = slot<String>()
val logMsgSlot = slot<String>()
val logExSlot = slot<Throwable>()
every { Log.v(any(), any()) } returns 0
every { Log.d(capture(logTagSlot), capture(logMsgSlot)) } answers {
println("${logTagSlot.captured} - ${logMsgSlot.captured}")
Expand All @@ -83,7 +84,11 @@ internal abstract class TransportTest {
every { Log.w(any(), ofType(String::class)) } returns 0
every { Log.w(any(), ofType(String::class), any()) } returns 0
every { Log.e(any(), any()) } returns 0
every { Log.e(any(), any(), any()) } returns 0
every { Log.e(capture(logTagSlot), capture(logMsgSlot), capture(logExSlot)) } answers {
println("${logTagSlot.captured} - ${logMsgSlot.captured} ${logExSlot.captured}")
logExSlot.captured.printStackTrace()
0
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ import org.calyxos.backup.storage.ui.restore.SnapshotViewModel

private val logEmptyState = """
Press the button below to simulate a backup. Your files won't be changed and not uploaded anywhere. This is just to test code for a future real backup.
Please come back to this app from time to time and run a backup again to see if it correctly identifies files that were added/changed.
Note that after updating this app, it might need to re-backup all files again.
Thanks for testing!
""".trimIndent()
private const val TAG = "MainViewModel"
Expand Down Expand Up @@ -98,8 +98,7 @@ class MainViewModel(application: Application) : BackupContentViewModel(applicati
fun setBackupLocation(uri: Uri?) {
if (uri != null) {
viewModelScope.launch(Dispatchers.IO) {
storageBackup.deleteAllSnapshots()
storageBackup.clearCache()
storageBackup.init()
}
}
settingsManager.setBackupLocation(uri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ public class StorageBackup(
list.joinToString(", ", limit = 5)
}

/**
* Ensures the storage is set-up to receive backups and deletes all snapshots
* (see [deleteAllSnapshots]) as well as clears local cache (see [clearCache]).
*/
public suspend fun init() {
plugin.init()
deleteAllSnapshots()
clearCache()
}

/**
* Run this on a new storage location to ensure that there are no old snapshots
* (potentially encrypted with an old key) laying around.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import javax.crypto.SecretKey

public interface StoragePlugin {

/**
* Prepares the storage location for storing backups.
* Call this before using the [StoragePlugin] for the first time.
*/
@Throws(IOException::class)
public suspend fun init()

/**
* Called before starting a backup run to ensure that all cached chunks are still available.
* Plugins should use this opportunity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public abstract class SafStoragePlugin(
return "$timestamp.SeedSnap"
}

override suspend fun init() {
// no-op as we are getting [root] created from super class
}

@Throws(IOException::class)
override suspend fun getAvailableChunkIds(): List<String> {
val folder = folder ?: return emptyList()
Expand Down

0 comments on commit d899891

Please sign in to comment.