diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStorage.kt index 605eca72..314a7d25 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStorage.kt @@ -8,7 +8,11 @@ package com.stevesoltys.seedvault.plugins.webdav import android.util.Log import at.bitfire.dav4jvm.BasicDigestAuthHandler import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.MultiResponseCallback import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.Response.HrefRelation.SELF +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.DisplayName import at.bitfire.dav4jvm.property.ResourceType import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -124,6 +128,47 @@ internal abstract class WebDavStorage( return pipedInputStream } + /** + * Tries to do [DavCollection.propfind] with a depth of `2` which is not in RFC4918. + * Since `infinity` isn't supported by nginx either, + * we fallback to iterating over all folders found with depth `1` + * and do another PROPFIND on those, passing the given [callback]. + */ + protected fun DavCollection.propfindDepthTwo(callback: MultiResponseCallback) { + try { + propfind( + depth = 2, // this isn't defined in RFC4918 + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + callback = callback, + ) + } catch (e: HttpException) { + if (e.code == 400) { + Log.i(TAG, "Got ${e.response}, trying two depth=1 PROPFINDs...") + propfindFakeTwo(callback) + } else { + throw e + } + } + } + + private fun DavCollection.propfindFakeTwo(callback: MultiResponseCallback) { + propfind( + depth = 1, + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + ) { response, relation -> + debugLog { "propFindFakeTwo() = $response" } + // This callback will be called for everything in the folder + callback.onResponse(response, relation) + if (relation != SELF && response.isFolder()) { + DavCollection(okHttpClient, response.href).propfind( + depth = 1, + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + callback = callback, + ) + } + } + } + protected suspend fun DavCollection.createFolder(xmlBody: String? = null): okhttp3.Response { return try { suspendCoroutine { cont -> diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt index b940e393..e2de1e63 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt @@ -9,6 +9,7 @@ import android.content.Context import android.util.Log import at.bitfire.dav4jvm.DavCollection import at.bitfire.dav4jvm.Response.HrefRelation.SELF +import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.exception.NotFoundException import at.bitfire.dav4jvm.property.DisplayName import at.bitfire.dav4jvm.property.QuotaAvailableBytes @@ -174,20 +175,25 @@ internal class WebDavStoragePlugin( // get all restore set tokens in root folder val tokens = ArrayList() - davCollection.propfind( - depth = 2, - reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), - ) { response, relation -> - debugLog { "getAvailableBackups() = $response" } - // This callback will be called for every file in the folder - if (relation != SELF && !response.isFolder() && response.href.pathSize >= 2 && - response.hrefName() == FILE_BACKUP_METADATA - ) { - val tokenName = response.href.pathSegments[response.href.pathSegments.size - 2] - getTokenOrNull(tokenName)?.let { token -> - tokens.add(token) + try { + davCollection.propfind( + depth = 2, + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + ) { response, relation -> + debugLog { "getAvailableBackups() = $response" } + // This callback will be called for every file in the folder + if (relation != SELF && !response.isFolder() && response.href.pathSize >= 2 && + response.hrefName() == FILE_BACKUP_METADATA + ) { + val tokenName = response.href.pathSegments[response.href.pathSegments.size - 2] + getTokenOrNull(tokenName)?.let { token -> + tokens.add(token) + } } } + } catch (e: HttpException) { + if (e.code == 400) getBackupTokenWithDepthOne(davCollection, tokens) + else throw e } val tokenIterator = tokens.iterator() return generateSequence { @@ -199,6 +205,34 @@ internal class WebDavStoragePlugin( } } + private fun getBackupTokenWithDepthOne(davCollection: DavCollection, tokens: ArrayList) { + davCollection.propfind( + depth = 1, + reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), + ) { response, relation -> + debugLog { "getBackupTokenWithDepthOne() = $response" } + + // we are only interested in sub-folders, skip rest + if (relation == SELF || !response.isFolder()) return@propfind + + val token = getTokenOrNull(response.hrefName()) ?: return@propfind + val tokenUrl = response.href.newBuilder() + .addPathSegment(FILE_BACKUP_METADATA) + .build() + // check if .backup.metadata file exists using HEAD request, + // because some servers (e.g. nginx don't list hidden files with PROPFIND) + try { + DavCollection(okHttpClient, tokenUrl).head { + debugLog { "getBackupTokenWithDepthOne() = $response" } + tokens.add(token) + } + } catch (e: Exception) { + // just log exception and continue, we want to find all files that are there + Log.e(TAG, "Error retrieving $tokenUrl: ", e) + } + } + } + private fun getTokenOrNull(name: String): Long? { val looksLikeToken = name.isNotEmpty() && tokenRegex.matches(name) if (looksLikeToken) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/WebDavStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/WebDavStoragePlugin.kt index e3019868..f8592af6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/WebDavStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/WebDavStoragePlugin.kt @@ -77,10 +77,7 @@ internal class WebDavStoragePlugin( val chunkIds = ArrayList() try { val duration = measureDuration { - davCollection.propfind( - depth = 2, - reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), - ) { response, relation -> + davCollection.propfindDepthTwo { response, relation -> debugLog { "getAvailableChunkIds() = $response" } // This callback will be called for every file in the folder if (relation != SELF && response.isFolder()) { @@ -162,13 +159,10 @@ internal class WebDavStoragePlugin( val snapshots = ArrayList() try { - davCollection.propfind( - depth = 2, - reqProp = arrayOf(DisplayName.NAME, ResourceType.NAME), - ) { response, relation -> + davCollection.propfindDepthTwo { response, relation -> debugLog { "getBackupSnapshotsForRestore() = $response" } // This callback will be called for every file in the folder - if (relation != SELF && !response.isFolder()) { + if (relation != SELF && !response.isFolder() && response.href.pathSize >= 2) { val name = response.hrefName() val match = snapshotRegex.matchEntire(name) if (match != null) { diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt index c549ba6f..dbbc7cc4 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePluginTest.kt @@ -71,6 +71,10 @@ internal class WebDavStoragePluginTest : TransportTest() { plugin.getOutputStream(token, FILE_BACKUP_METADATA).use { it.write(metadata) } + + // now we have data + assertTrue(plugin.hasData(token, FILE_BACKUP_METADATA)) + try { // now we have one backup matching our token val backups = plugin.getAvailableBackups()?.toSet() ?: fail()