From c19787a7fa6ddaf63894327b370845a6a7448143 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 3 Sep 2024 13:51:13 -0300 Subject: [PATCH] Prepare backends for new app backup repository --- .../seedvault/backend/saf/SafBackendTest.kt | 2 +- .../seedvault/core/backends/BackendTest.kt | 91 ++++++++++++++----- .../seedvault/core/backends/Constants.kt | 18 ++-- .../seedvault/core/backends/FileHandle.kt | 26 +++++- .../core/backends/saf/DocumentFileCache.kt | 31 ++++++- .../seedvault/core/backends/saf/SafBackend.kt | 63 ++++++++++--- .../core/backends/webdav/WebDavBackend.kt | 58 +++++++++--- .../core/backends/webdav/WebDavBackendTest.kt | 2 +- 8 files changed, 228 insertions(+), 63 deletions(-) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/backend/saf/SafBackendTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/backend/saf/SafBackendTest.kt index 173246bf..6e27738f 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/backend/saf/SafBackendTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/backend/saf/SafBackendTest.kt @@ -33,7 +33,7 @@ class SafBackendTest : BackendTest(), KoinComponent { requiresNetwork = safStorage.requiresNetwork, rootId = safStorage.rootId, ) - override val plugin: Backend = SafBackend(context, safProperties, ".SeedvaultTest") + override val backend: Backend = SafBackend(context, safProperties, ".SeedvaultTest") @Test fun `test write list read rename delete`(): Unit = runBlocking { diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt index 1fd40ee9..9046160c 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt @@ -16,26 +16,26 @@ import kotlin.test.assertNotNull @VisibleForTesting public abstract class BackendTest { - public abstract val plugin: Backend + public abstract val backend: Backend protected suspend fun testWriteListReadRenameDelete() { - plugin.removeAll() + backend.removeAll() val androidId = "0123456789abcdef" val now = System.currentTimeMillis() val bytes1 = Random.nextBytes(1337) val bytes2 = Random.nextBytes(1337 * 8) - plugin.save(LegacyAppBackupFile.Metadata(now)).use { + backend.save(LegacyAppBackupFile.Metadata(now)).use { it.write(bytes1) } - plugin.save(FileBackupFileType.Snapshot(androidId, now)).use { + backend.save(FileBackupFileType.Snapshot(androidId, now)).use { it.write(bytes2) } var metadata: LegacyAppBackupFile.Metadata? = null - var snapshot: FileBackupFileType.Snapshot? = null - plugin.list( + var fileSnapshot: FileBackupFileType.Snapshot? = null + backend.list( null, FileBackupFileType.Snapshot::class, FileBackupFileType.Blob::class, @@ -45,22 +45,22 @@ public abstract class BackendTest { if (handle is LegacyAppBackupFile.Metadata && handle.token == now) { metadata = handle } else if (handle is FileBackupFileType.Snapshot && handle.time == now) { - snapshot = handle + fileSnapshot = handle } } assertNotNull(metadata) - assertNotNull(snapshot) + assertNotNull(fileSnapshot) - assertContentEquals(bytes1, plugin.load(metadata as FileHandle).readAllBytes()) - assertContentEquals(bytes2, plugin.load(snapshot as FileHandle).readAllBytes()) + assertContentEquals(bytes1, backend.load(metadata as FileHandle).readAllBytes()) + assertContentEquals(bytes2, backend.load(fileSnapshot as FileHandle).readAllBytes()) val blobName = Random.nextBytes(32).toHexString() var blob: FileBackupFileType.Blob? = null val bytes3 = Random.nextBytes(1337 * 16) - plugin.save(FileBackupFileType.Blob(androidId, blobName)).use { + backend.save(FileBackupFileType.Blob(androidId, blobName)).use { it.write(bytes3) } - plugin.list( + backend.list( null, FileBackupFileType.Snapshot::class, FileBackupFileType.Blob::class, @@ -72,32 +72,75 @@ public abstract class BackendTest { } } assertNotNull(blob) - assertContentEquals(bytes3, plugin.load(blob as FileHandle).readAllBytes()) + assertContentEquals(bytes3, backend.load(blob as FileHandle).readAllBytes()) // try listing with top-level folder, should find two files of FileBackupFileType in there var numFiles = 0 - plugin.list( - snapshot!!.topLevelFolder, + backend.list( + fileSnapshot!!.topLevelFolder, FileBackupFileType.Snapshot::class, FileBackupFileType.Blob::class, LegacyAppBackupFile.Metadata::class, ) { numFiles++ } assertEquals(2, numFiles) - plugin.remove(snapshot as FileHandle) + val repoId = Random.nextBytes(32).toHexString() + val snapshotId = Random.nextBytes(32).toHexString() + val blobId = Random.nextBytes(32).toHexString() + + val bytes4 = Random.nextBytes(1337) + val bytes5 = Random.nextBytes(1337 * 8) + backend.save(AppBackupFileType.Snapshot(repoId, snapshotId)).use { + it.write(bytes4) + } + + var appSnapshot: AppBackupFileType.Snapshot? = null + backend.list( + null, + AppBackupFileType.Snapshot::class, + ) { fileInfo -> + val handle = fileInfo.fileHandle + if (handle is AppBackupFileType.Snapshot) { + appSnapshot = handle + } + } + assertNotNull(appSnapshot) + assertContentEquals(bytes4, backend.load(appSnapshot as FileHandle).readAllBytes()) + + backend.save(AppBackupFileType.Blob(repoId, blobId)).use { + it.write(bytes5) + } + + var blobHandle: AppBackupFileType.Blob? = null + backend.list( + TopLevelFolder(repoId), + AppBackupFileType.Blob::class, + LegacyAppBackupFile.Metadata::class, + ) { fileInfo -> + val handle = fileInfo.fileHandle + if (handle is AppBackupFileType.Blob) { + blobHandle = handle + } + } + assertNotNull(blobHandle) + assertContentEquals(bytes5, backend.load(blobHandle as FileHandle).readAllBytes()) + + backend.remove(fileSnapshot as FileHandle) + backend.remove(appSnapshot as FileHandle) + backend.remove(blobHandle as FileHandle) // rename snapshots val snapshotNewFolder = TopLevelFolder("a123456789abcdef.sv") - plugin.rename(snapshot!!.topLevelFolder, snapshotNewFolder) + backend.rename(fileSnapshot!!.topLevelFolder, snapshotNewFolder) // rename to existing folder should fail val e = assertFailsWith { - plugin.rename(snapshotNewFolder, metadata!!.topLevelFolder) + backend.rename(snapshotNewFolder, metadata!!.topLevelFolder) } println(e) - plugin.remove(metadata!!.topLevelFolder) - plugin.remove(snapshotNewFolder) + backend.remove(metadata!!.topLevelFolder) + backend.remove(snapshotNewFolder) } protected suspend fun testRemoveCreateWriteFile() { @@ -105,14 +148,14 @@ public abstract class BackendTest { val blob = LegacyAppBackupFile.Blob(now, Random.nextBytes(32).toHexString()) val bytes = Random.nextBytes(2342) - plugin.remove(blob) + backend.remove(blob) try { - plugin.save(blob).use { + backend.save(blob).use { it.write(bytes) } - assertContentEquals(bytes, plugin.load(blob as FileHandle).readAllBytes()) + assertContentEquals(bytes, backend.load(blob as FileHandle).readAllBytes()) } finally { - plugin.remove(blob) + backend.remove(blob) } } diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/Constants.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/Constants.kt index d006b0da..717640ba 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/Constants.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/Constants.kt @@ -10,12 +10,18 @@ public object Constants { public const val DIRECTORY_ROOT: String = ".SeedVaultAndroidBackup" internal const val FILE_BACKUP_METADATA = ".backup.metadata" internal const val FILE_BACKUP_ICONS = ".backup.icons" - public val tokenRegex: Regex = Regex("([0-9]{13})") // good until the year 2286 - public const val SNAPSHOT_EXT: String = ".SeedSnap" - public val folderRegex: Regex = Regex("^[a-f0-9]{16}\\.sv$") - public val chunkFolderRegex: Regex = Regex("[a-f0-9]{2}") - public val chunkRegex: Regex = Regex("[a-f0-9]{64}") - public val snapshotRegex: Regex = Regex("([0-9]{13})\\.SeedSnap") // good until the year 2286 + public val tokenRegex: Regex = Regex("^([0-9]{13})$") // good until the year 2286 + public const val APP_SNAPSHOT_EXT: String = ".snapshot" + public const val FILE_SNAPSHOT_EXT: String = ".SeedSnap" + public val repoIdRegex: Regex = Regex("^[a-f0-9]{64}$") + public val fileFolderRegex: Regex = Regex("^[a-f0-9]{16}\\.sv$") + public val chunkFolderRegex: Regex = Regex("^[a-f0-9]{2}$") + public val blobFolderRegex: Regex = chunkFolderRegex + public val chunkRegex: Regex = repoIdRegex + public val blobRegex: Regex = repoIdRegex + // good until year 2286 + public val appSnapshotRegex: Regex = Regex("(^[a-f0-9]{64})\\.snapshot$") + public val fileSnapshotRegex: Regex = Regex("(^[0-9]{13})\\.SeedSnap$") // good until year 2286 public const val MIME_TYPE: String = "application/octet-stream" public const val CHUNK_FOLDER_COUNT: Int = 256 diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/FileHandle.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/FileHandle.kt index 6c2a1240..ce63aa75 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/FileHandle.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/FileHandle.kt @@ -5,9 +5,10 @@ package org.calyxos.seedvault.core.backends +import org.calyxos.seedvault.core.backends.Constants.APP_SNAPSHOT_EXT import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_ICONS import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA -import org.calyxos.seedvault.core.backends.Constants.SNAPSHOT_EXT +import org.calyxos.seedvault.core.backends.Constants.FILE_SNAPSHOT_EXT public sealed class FileHandle { public abstract val name: String @@ -68,11 +69,32 @@ public sealed class FileBackupFileType : FileHandle() { override val androidId: String, val time: Long, ) : FileBackupFileType() { - override val name: String = "$time$SNAPSHOT_EXT" + override val name: String = "$time$FILE_SNAPSHOT_EXT" override val relativePath: String get() = "$androidId.sv/$name" } } +public sealed class AppBackupFileType : FileHandle() { + public abstract val repoId: String + + public val topLevelFolder: TopLevelFolder get() = TopLevelFolder(repoId) + + public data class Blob( + override val repoId: String, + override val name: String, + ) : AppBackupFileType() { + override val relativePath: String get() = "$repoId/${name.substring(0, 2)}/$name" + } + + public data class Snapshot( + override val repoId: String, + val hash: String, + ) : AppBackupFileType() { + override val name: String = "$hash$APP_SNAPSHOT_EXT" + override val relativePath: String get() = "$repoId/$name" + } +} + public data class FileInfo( val fileHandle: FileHandle, val size: Long, diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/saf/DocumentFileCache.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/saf/DocumentFileCache.kt index 9adf0da8..eb845995 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/saf/DocumentFileCache.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/saf/DocumentFileCache.kt @@ -7,6 +7,7 @@ package org.calyxos.seedvault.core.backends.saf import android.content.Context import androidx.documentfile.provider.DocumentFile +import org.calyxos.seedvault.core.backends.AppBackupFileType import org.calyxos.seedvault.core.backends.FileBackupFileType import org.calyxos.seedvault.core.backends.FileHandle import org.calyxos.seedvault.core.backends.LegacyAppBackupFile @@ -32,9 +33,16 @@ internal class DocumentFileCache( getRootFile().getOrCreateDirectory(context, fh.name) } - is LegacyAppBackupFile -> cache.getOrPut("$root/${fh.relativePath}") { - getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name) - } + is AppBackupFileType.Blob -> { + val subFolderName = fh.name.substring(0, 2) + cache.getOrPut("$root/${fh.topLevelFolder.name}/$subFolderName") { + getOrCreateFile(fh.topLevelFolder).getOrCreateDirectory(context, subFolderName) + }.getOrCreateFile(context, fh.name) + } + + is AppBackupFileType.Snapshot -> { + getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name) + } is FileBackupFileType.Blob -> { val subFolderName = fh.name.substring(0, 2) @@ -46,6 +54,10 @@ internal class DocumentFileCache( is FileBackupFileType.Snapshot -> { getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name) } + + is LegacyAppBackupFile -> cache.getOrPut("$root/${fh.relativePath}") { + getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name) + } } internal suspend fun getFile(fh: FileHandle): DocumentFile? = when (fh) { @@ -53,7 +65,14 @@ internal class DocumentFileCache( getRootFile().findFileBlocking(context, fh.name) } - is LegacyAppBackupFile -> cache.getOrElse("$root/${fh.relativePath}") { + is AppBackupFileType.Blob -> { + val subFolderName = fh.name.substring(0, 2) + cache.getOrElse("$root/${fh.topLevelFolder.name}/$subFolderName") { + getFile(fh.topLevelFolder)?.findFileBlocking(context, subFolderName) + }?.findFileBlocking(context, fh.name) + } + + is AppBackupFileType.Snapshot -> { getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name) } @@ -67,6 +86,10 @@ internal class DocumentFileCache( is FileBackupFileType.Snapshot -> { getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name) } + + is LegacyAppBackupFile -> cache.getOrElse("$root/${fh.relativePath}") { + getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name) + } } internal fun removeFromCache(fh: FileHandle) { diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/saf/SafBackend.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/saf/SafBackend.kt index 028c521e..d23121f4 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/saf/SafBackend.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/saf/SafBackend.kt @@ -16,13 +16,18 @@ import androidx.core.database.getIntOrNull import androidx.documentfile.provider.DocumentFile import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import org.calyxos.seedvault.core.backends.AppBackupFileType import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA +import org.calyxos.seedvault.core.backends.Constants.appSnapshotRegex +import org.calyxos.seedvault.core.backends.Constants.blobFolderRegex +import org.calyxos.seedvault.core.backends.Constants.blobRegex import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex import org.calyxos.seedvault.core.backends.Constants.chunkRegex -import org.calyxos.seedvault.core.backends.Constants.folderRegex -import org.calyxos.seedvault.core.backends.Constants.snapshotRegex +import org.calyxos.seedvault.core.backends.Constants.fileFolderRegex +import org.calyxos.seedvault.core.backends.Constants.fileSnapshotRegex +import org.calyxos.seedvault.core.backends.Constants.repoIdRegex import org.calyxos.seedvault.core.backends.Constants.tokenRegex import org.calyxos.seedvault.core.backends.FileBackupFileType import org.calyxos.seedvault.core.backends.FileHandle @@ -103,7 +108,7 @@ public class SafBackend( if (LegacyAppBackupFile.IconsFile::class in fileTypes) throw UnsupportedOperationException() if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException() - log.debugLog { "list($topLevelFolder, $fileTypes)" } + log.debugLog { "list($topLevelFolder, ${fileTypes.map { it.simpleName }})" } val folder = if (topLevelFolder == null) { cache.getRootFile() @@ -111,23 +116,45 @@ public class SafBackend( cache.getOrCreateFile(topLevelFolder) } // limit depth based on wanted types and if top-level folder is given - var depth = if (FileBackupFileType.Blob::class in fileTypes) 3 else 2 + var depth = if (FileBackupFileType.Blob::class in fileTypes || + AppBackupFileType.Blob::class in fileTypes + ) 3 else 2 if (topLevelFolder != null) depth -= 1 folder.listFilesRecursive(depth) { file -> if (!file.isFile) return@listFilesRecursive val parentName = file.parentFile?.name ?: return@listFilesRecursive val name = file.name ?: return@listFilesRecursive - if (LegacyAppBackupFile.Metadata::class in fileTypes && name == FILE_BACKUP_METADATA && - parentName.matches(tokenRegex) + if (AppBackupFileType.Snapshot::class in fileTypes || + AppBackupFileType::class in fileTypes ) { - val metadata = LegacyAppBackupFile.Metadata(parentName.toLong()) - callback(FileInfo(metadata, file.length())) + val match = appSnapshotRegex.matchEntire(name) + if (match != null && repoIdRegex.matches(parentName)) { + val snapshot = AppBackupFileType.Snapshot( + repoId = parentName, + hash = match.groupValues[1], + ) + callback(FileInfo(snapshot, file.length())) + } + } + if ((AppBackupFileType.Blob::class in fileTypes || + AppBackupFileType::class in fileTypes) + ) { + val repoId = file.parentFile?.parentFile?.name ?: "" + if (repoIdRegex.matches(repoId) && blobFolderRegex.matches(parentName)) { + if (blobRegex.matches(name)) { + val blob = AppBackupFileType.Blob( + repoId = repoId, + name = name, + ) + callback(FileInfo(blob, file.length())) + } + } } if (FileBackupFileType.Snapshot::class in fileTypes || FileBackupFileType::class in fileTypes ) { - val match = snapshotRegex.matchEntire(name) + val match = fileSnapshotRegex.matchEntire(name) if (match != null) { val snapshot = FileBackupFileType.Snapshot( androidId = parentName.substringBefore('.'), @@ -140,7 +167,7 @@ public class SafBackend( FileBackupFileType::class in fileTypes) ) { val androidIdSv = file.parentFile?.parentFile?.name ?: "" - if (folderRegex.matches(androidIdSv) && chunkFolderRegex.matches(parentName)) { + if (fileFolderRegex.matches(androidIdSv) && chunkFolderRegex.matches(parentName)) { if (chunkRegex.matches(name)) { val blob = FileBackupFileType.Blob( androidId = androidIdSv.substringBefore('.'), @@ -150,6 +177,12 @@ public class SafBackend( } } } + if (LegacyAppBackupFile.Metadata::class in fileTypes && name == FILE_BACKUP_METADATA && + parentName.matches(tokenRegex) + ) { + val metadata = LegacyAppBackupFile.Metadata(parentName.toLong()) + callback(FileInfo(metadata, file.length())) + } } } @@ -173,16 +206,18 @@ public class SafBackend( } override suspend fun rename(from: TopLevelFolder, to: TopLevelFolder) { - log.debugLog { "rename($from, ${to.name})" } + val toName = to.name // querying name is expensive + log.debugLog { "rename($from, $toName)" } val fromFile = cache.getOrCreateFile(from) // don't use fromFile.renameTo(to.name) as that creates "${to.name} (1)" - val newUri = renameDocument(context.contentResolver, fromFile.uri, to.name) + val newUri = renameDocument(context.contentResolver, fromFile.uri, toName) ?: throw IOException("could not rename ${from.relativePath}") + cache.removeFromCache(from) // after renaming cached file isn't valid anymore val toFile = DocumentFile.fromTreeUri(context, newUri) ?: throw IOException("renamed URI invalid: $newUri") - if (toFile.name != to.name) { + if (toFile.name != toName) { toFile.delete() - throw IOException("renamed to ${toFile.name}, but expected ${to.name}") + throw IOException("renamed to ${toFile.name}, but expected $toName") } } diff --git a/core/src/main/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackend.kt b/core/src/main/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackend.kt index ee24b895..c7de6d04 100644 --- a/core/src/main/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackend.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackend.kt @@ -26,13 +26,17 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.RequestBody import okio.BufferedSink +import org.calyxos.seedvault.core.backends.AppBackupFileType import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA +import org.calyxos.seedvault.core.backends.Constants.appSnapshotRegex +import org.calyxos.seedvault.core.backends.Constants.blobFolderRegex import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex import org.calyxos.seedvault.core.backends.Constants.chunkRegex -import org.calyxos.seedvault.core.backends.Constants.folderRegex -import org.calyxos.seedvault.core.backends.Constants.snapshotRegex +import org.calyxos.seedvault.core.backends.Constants.fileFolderRegex +import org.calyxos.seedvault.core.backends.Constants.fileSnapshotRegex +import org.calyxos.seedvault.core.backends.Constants.repoIdRegex import org.calyxos.seedvault.core.backends.Constants.tokenRegex import org.calyxos.seedvault.core.backends.FileBackupFileType import org.calyxos.seedvault.core.backends.FileHandle @@ -180,7 +184,9 @@ public class WebDavBackend( if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException() // limit depth based on wanted types and if top-level folder is given - var depth = if (FileBackupFileType.Blob::class in fileTypes) 3 else 2 + var depth = if (FileBackupFileType.Blob::class in fileTypes || + AppBackupFileType.Blob::class in fileTypes + ) 3 else 2 if (topLevelFolder != null) depth -= 1 val location = if (topLevelFolder == null) { @@ -204,19 +210,40 @@ public class WebDavBackend( val name = response.hrefName() val parentName = response.href.pathSegments[response.href.pathSegments.size - 2] - if (LegacyAppBackupFile.Metadata::class in fileTypes) { - if (name == FILE_BACKUP_METADATA && parentName.matches(tokenRegex)) { - val metadata = LegacyAppBackupFile.Metadata(parentName.toLong()) + if (AppBackupFileType.Snapshot::class in fileTypes || + AppBackupFileType::class in fileTypes + ) { + val match = appSnapshotRegex.matchEntire(name) + if (match != null && repoIdRegex.matches(parentName)) { val size = response.properties.contentLength() - callback(FileInfo(metadata, size)) - // we can find .backup.metadata files, so no need for nginx workaround - tokenFolders.clear() + val snapshot = AppBackupFileType.Snapshot( + repoId = parentName, + hash = match.groupValues[1], + ) + callback(FileInfo(snapshot, size)) + } + } + if ((AppBackupFileType.Blob::class in fileTypes || + AppBackupFileType::class in fileTypes) && response.href.pathSize >= 3 + ) { + val repoId = response.href.pathSegments[response.href.pathSegments.size - 3] + if (repoIdRegex.matches(repoId) && + blobFolderRegex.matches(parentName) + ) { + if (chunkRegex.matches(name)) { + val blob = AppBackupFileType.Blob( + repoId = repoId, + name = name, + ) + val size = response.properties.contentLength() + callback(FileInfo(blob, size)) + } } } if (FileBackupFileType.Snapshot::class in fileTypes || FileBackupFileType::class in fileTypes ) { - val match = snapshotRegex.matchEntire(name) + val match = fileSnapshotRegex.matchEntire(name) if (match != null) { val size = response.properties.contentLength() val snapshot = FileBackupFileType.Snapshot( @@ -231,7 +258,7 @@ public class WebDavBackend( ) { val androidIdSv = response.href.pathSegments[response.href.pathSegments.size - 3] - if (folderRegex.matches(androidIdSv) && + if (fileFolderRegex.matches(androidIdSv) && chunkFolderRegex.matches(parentName) ) { if (chunkRegex.matches(name)) { @@ -244,6 +271,15 @@ public class WebDavBackend( } } } + if (LegacyAppBackupFile.Metadata::class in fileTypes) { + if (name == FILE_BACKUP_METADATA && parentName.matches(tokenRegex)) { + val metadata = LegacyAppBackupFile.Metadata(parentName.toLong()) + val size = response.properties.contentLength() + callback(FileInfo(metadata, size)) + // we can find .backup.metadata files, so no need for nginx workaround + tokenFolders.clear() + } + } } } } catch (e: NotFoundException) { diff --git a/core/src/test/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackendTest.kt b/core/src/test/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackendTest.kt index 8b366c82..e4fb4ca7 100644 --- a/core/src/test/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackendTest.kt +++ b/core/src/test/java/org/calyxos/seedvault/core/backends/webdav/WebDavBackendTest.kt @@ -11,7 +11,7 @@ import org.calyxos.seedvault.core.backends.BackendTest import kotlin.test.Test public class WebDavBackendTest : BackendTest() { - override val plugin: Backend = WebDavBackend(WebDavTestConfig.getConfig(), ".SeedvaultTest") + override val backend: Backend = WebDavBackend(WebDavTestConfig.getConfig(), ".SeedvaultTest") @Test public fun `test write, list, read, rename, delete`(): Unit = runBlocking {