Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 47685e66 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

[WebDAV] Rewrite COPY/MOVE (including rename) to Ktor (#1940)

* [WebDAV] Refactor RenameDocumentOperation to Ktor

- Update imports to use Ktor-based classes
- Refactor `RenameDocumentOperation` to use Ktor HTTP client
- Add support for both HttpException types in `throwForDocumentProvider`

* Rewrite CopyDocumentOperation.kt to Ktor

* Refactor URLBuilder usage

- Update URLBuilder usage in RenameDocumentOperation.kt
- Update URLBuilder usage in CopyDocumentOperation.kt
- Update URLBuilder usage in MoveDocumentOperation.kt

* - Pass `ioDispatcher` to `runBlocking` in WebDAV operations
- Refactor timeout configuration in HttpClientBuilder for reusability

* Add logging to DocumentProviderUtils

- Introduce a logger instance
- Log URI when notifying folder changes
parent 2c7b36ec
Loading
Loading
Loading
Loading
+18 −6
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ class HttpClientBuilder @Inject constructor(
) {

    companion object {

        init {
            // make sure Conscrypt is available when the HttpClientBuilder class is loaded the first time
            ConscryptIntegration().initialize()
@@ -84,12 +85,18 @@ class HttpClientBuilder @Inject constructor(
         * The shared client is available for the lifetime of the application and must not be shut down or
         * closed (which is not necessary, according to its documentation).
         */
        val sharedOkHttpClient = OkHttpClient.Builder()
        val sharedOkHttpClient = OkHttpClient.Builder().apply {
            configureTimeouts(this)
        }.build()

        private fun configureTimeouts(okBuilder: OkHttpClient.Builder) {
            okBuilder
                .connectTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(120, TimeUnit.SECONDS)
                .pingInterval(45, TimeUnit.SECONDS)     // avoid cancellation because of missing traffic; only works for HTTP/2
            .build()
        }

    }

    /**
@@ -407,6 +414,11 @@ class HttpClientBuilder @Inject constructor(

                config {
                    // OkHttpClient.Builder configuration here

                    // we don't use the sharedOkHttpClient, so we have to apply timeouts again
                    configureTimeouts(this)

                    // build most config on okhttp level
                    configureOkHttp(this)
                }
            }
+28 −17
Original line number Diff line number Diff line
@@ -13,15 +13,19 @@ import android.provider.DocumentsContract.buildChildDocumentsUri
import android.provider.DocumentsContract.buildRootsUri
import android.webkit.MimeTypeMap
import androidx.core.app.TaskStackBuilder
import at.bitfire.dav4jvm.okhttp.exception.HttpException
import at.bitfire.dav4jvm.ktor.exception.HttpException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import java.io.FileNotFoundException
import java.util.logging.Logger

object DocumentProviderUtils {

    const val MAX_DISPLAYNAME_TO_MEMBERNAME_ATTEMPTS = 5

    private val logger
        get() = Logger.getLogger(javaClass.name)

    internal fun displayNameToMemberName(displayName: String, appendNumber: Int = 0): String {
        val safeName = displayName.filterNot { it.isISOControl() }

@@ -37,24 +41,23 @@ object DocumentProviderUtils {
    }

    internal fun notifyFolderChanged(context: Context, parentDocumentId: Long?) {
        if (parentDocumentId != null)
            context.contentResolver.notifyChange(
                buildChildDocumentsUri(
        if (parentDocumentId != null) {
            val uri = buildChildDocumentsUri(
                context.getString(R.string.webdav_authority),
                parentDocumentId.toString()
                ),
                null
            )
            logger.fine("Notifying observers of $uri")
            context.contentResolver.notifyChange(uri, null)
        }
    }

    internal fun notifyFolderChanged(context: Context, parentDocumentId: String) {
        context.contentResolver.notifyChange(
            buildChildDocumentsUri(
        val uri = buildChildDocumentsUri(
            context.getString(R.string.webdav_authority),
            parentDocumentId
            ),
            null
        )
        logger.fine("Notifying observers of $uri")
        context.contentResolver.notifyChange(uri, null)
    }

    internal fun notifyMountsChanged(context: Context) {
@@ -66,12 +69,20 @@ object DocumentProviderUtils {
}

internal fun HttpException.throwForDocumentProvider(context: Context, ignorePreconditionFailed: Boolean = false) {
    throwForDocumentProvider(context, statusCode, this, ignorePreconditionFailed)
}

internal fun at.bitfire.dav4jvm.okhttp.exception.HttpException.throwForDocumentProvider(context: Context, ignorePreconditionFailed: Boolean = false) {
    throwForDocumentProvider(context, statusCode, this, ignorePreconditionFailed)
}

private fun throwForDocumentProvider(context: Context, statusCode: Int, ex: Exception, ignorePreconditionFailed: Boolean) {
    when (statusCode) {
        401 -> {
            if (Build.VERSION.SDK_INT >= 26) {
                val intent = Intent(context, WebdavMountsActivity::class.java)
                throw AuthenticationRequiredException(
                    this,
                    ex,
                    TaskStackBuilder.create(context)
                        .addNextIntentWithParentStack(intent)
                        .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
@@ -86,5 +97,5 @@ internal fun HttpException.throwForDocumentProvider(context: Context, ignorePrec
    }

    // re-throw
    throw this
    throw ex
}
 No newline at end of file
+18 −16
Original line number Diff line number Diff line
@@ -5,8 +5,8 @@
package at.bitfire.davdroid.webdav.operation

import android.content.Context
import at.bitfire.dav4jvm.okhttp.DavResource
import at.bitfire.dav4jvm.okhttp.exception.HttpException
import at.bitfire.dav4jvm.ktor.DavResource
import at.bitfire.dav4jvm.ktor.exception.HttpException
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.di.IoDispatcher
@@ -14,9 +14,10 @@ import at.bitfire.davdroid.webdav.DavHttpClientBuilder
import at.bitfire.davdroid.webdav.DocumentProviderUtils
import at.bitfire.davdroid.webdav.throwForDocumentProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.http.URLBuilder
import io.ktor.http.appendPathSegments
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import java.io.FileNotFoundException
import java.util.logging.Logger
import javax.inject.Inject
@@ -31,7 +32,7 @@ class CopyDocumentOperation @Inject constructor(

    private val documentDao = db.webDavDocumentDao()

    operator fun invoke(sourceDocumentId: String, targetParentDocumentId: String): String = runBlocking {
    operator fun invoke(sourceDocumentId: String, targetParentDocumentId: String): String = runBlocking(ioDispatcher) {
        logger.fine("WebDAV copyDocument $sourceDocumentId $targetParentDocumentId")
        val srcDoc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException()
        val dstFolder = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException()
@@ -40,21 +41,22 @@ class CopyDocumentOperation @Inject constructor(
        if (srcDoc.mountId != dstFolder.mountId)
            throw UnsupportedOperationException("Can't COPY between WebDAV servers")

        val client = httpClientBuilder.build(srcDoc.mountId)
        val dav = DavResource(client, srcDoc.toHttpUrl(db))
        val dstUrl = dstFolder.toHttpUrl(db).newBuilder()
            .addPathSegment(name)
        httpClientBuilder
            .buildKtor(srcDoc.mountId)
            .use { httpClient ->
                val dav = DavResource(httpClient, srcDoc.toKtorUrl(db))
                val dstUrl = URLBuilder(dstFolder.toKtorUrl(db))
                    .appendPathSegments(name)
                    .build()

                try {
            runInterruptible(ioDispatcher) {
                    dav.copy(dstUrl, false) {
                        // successfully copied
                    }
            }
                } catch (e: HttpException) {
                    e.throwForDocumentProvider(context)
                }
            }

        val dstDocId = documentDao.insertOrReplace(
            WebDavDocument(
+23 −21
Original line number Diff line number Diff line
@@ -5,17 +5,18 @@
package at.bitfire.davdroid.webdav.operation

import android.content.Context
import at.bitfire.dav4jvm.okhttp.DavResource
import at.bitfire.dav4jvm.okhttp.exception.HttpException
import at.bitfire.dav4jvm.ktor.DavResource
import at.bitfire.dav4jvm.ktor.exception.HttpException
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.di.IoDispatcher
import at.bitfire.davdroid.webdav.DavHttpClientBuilder
import at.bitfire.davdroid.webdav.DocumentProviderUtils
import at.bitfire.davdroid.webdav.throwForDocumentProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.http.URLBuilder
import io.ktor.http.appendPathSegments
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import java.io.FileNotFoundException
import java.util.logging.Logger
import javax.inject.Inject
@@ -30,7 +31,7 @@ class MoveDocumentOperation @Inject constructor(

    private val documentDao = db.webDavDocumentDao()

    operator fun invoke(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String): String = runBlocking {
    operator fun invoke(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String): String = runBlocking(ioDispatcher) {
        logger.fine("WebDAV moveDocument $sourceDocumentId $sourceParentDocumentId $targetParentDocumentId")
        val doc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException()
        val dstParent = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException()
@@ -38,18 +39,18 @@ class MoveDocumentOperation @Inject constructor(
        if (doc.mountId != dstParent.mountId)
            throw UnsupportedOperationException("Can't MOVE between WebDAV servers")

        val newLocation = dstParent.toHttpUrl(db).newBuilder()
            .addPathSegment(doc.name)
        httpClientBuilder
            .buildKtor(doc.mountId)
            .use { httpClient ->
                val newLocation = URLBuilder(dstParent.toKtorUrl(db))
                    .appendPathSegments(doc.name)
                    .build()

        val client = httpClientBuilder.build(doc.mountId)
        val dav = DavResource(client, doc.toHttpUrl(db))
                val dav = DavResource(httpClient, doc.toKtorUrl(db))
                try {
            runInterruptible(ioDispatcher) {
                    dav.move(newLocation, false) {
                        // successfully moved
                    }
            }

                    documentDao.update(doc.copy(parentId = dstParent.id))

@@ -58,6 +59,7 @@ class MoveDocumentOperation @Inject constructor(
                } catch (e: HttpException) {
                    e.throwForDocumentProvider(context)
                }
            }

        doc.id.toString()
    }
+27 −25
Original line number Diff line number Diff line
@@ -5,8 +5,8 @@
package at.bitfire.davdroid.webdav.operation

import android.content.Context
import at.bitfire.dav4jvm.okhttp.DavResource
import at.bitfire.dav4jvm.okhttp.exception.HttpException
import at.bitfire.dav4jvm.ktor.DavResource
import at.bitfire.dav4jvm.ktor.exception.HttpException
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.di.IoDispatcher
import at.bitfire.davdroid.webdav.DavHttpClientBuilder
@@ -14,9 +14,9 @@ import at.bitfire.davdroid.webdav.DocumentProviderUtils
import at.bitfire.davdroid.webdav.DocumentProviderUtils.displayNameToMemberName
import at.bitfire.davdroid.webdav.throwForDocumentProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.http.URLBuilder
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import java.io.FileNotFoundException
import java.util.logging.Logger
import javax.inject.Inject
@@ -31,25 +31,26 @@ class RenameDocumentOperation @Inject constructor(

    private val documentDao = db.webDavDocumentDao()

    operator fun invoke(documentId: String, displayName: String): String? = runBlocking {
    operator fun invoke(documentId: String, displayName: String): String? = runBlocking(ioDispatcher) {
        logger.fine("WebDAV renameDocument $documentId $displayName")
        val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()

        val client = httpClientBuilder.build(doc.mountId)
        httpClientBuilder
            .buildKtor(doc.mountId)
            .use { httpClient ->
                for (attempt in 0..DocumentProviderUtils.MAX_DISPLAYNAME_TO_MEMBERNAME_ATTEMPTS) {
                    val newName = displayNameToMemberName(displayName, attempt)
            val oldUrl = doc.toHttpUrl(db)
            val newLocation = oldUrl.newBuilder()
                .removePathSegment(oldUrl.pathSegments.lastIndex)
                .addPathSegment(newName)
                .build()
                    val oldUrl = doc.toKtorUrl(db)
                    val newLocation = URLBuilder(oldUrl)
                        .apply {
                        // Remove the last path segment (current file name) and add the new name
                        pathSegments = pathSegments.dropLast(1) + newName
                    }.build()
                    try {
                val dav = DavResource(client, oldUrl)
                runInterruptible(ioDispatcher) {
                        val dav = DavResource(httpClient, oldUrl)
                        dav.move(newLocation, false) {
                            // successfully renamed
                        }
                }
                        documentDao.update(doc.copy(name = newName))

                        DocumentProviderUtils.notifyFolderChanged(context, doc.parentId)
@@ -59,6 +60,7 @@ class RenameDocumentOperation @Inject constructor(
                        e.throwForDocumentProvider(context, true)
                    }
                }
            }

        null
    }