From 87f0ecec2f14e20413a9e77172c3c711c355c045 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Wed, 26 Nov 2025 16:10:48 +0100 Subject: [PATCH 1/2] Optimize TUS upload: remove redundant HEAD request --- .../android/workers/TusUploadHelper.kt | 36 ++++++++----------- .../tus/CreateTusUploadRemoteOperation.kt | 26 +++++++++----- .../resources/files/tus/TusIntegrationTest.kt | 10 ++++-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index db7fe730b..2cd64e14a 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -45,6 +45,7 @@ class TusUploadHelper( var tusUrl = transfer.tusUploadUrl val checksumHex = transfer.tusUploadChecksum?.substringAfter("sha256:") + var createdOffset: Long? = null if (tusUrl.isNullOrBlank()) { val fileName = File(remotePath).name @@ -69,24 +70,13 @@ class TusUploadHelper( // Use creation-with-upload like the browser does for OpenCloud compatibility val firstChunkSize = minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize) - val createdLocation = executeRemoteOperation { + val creationResult = executeRemoteOperation { CreateTusUploadRemoteOperation( file = File(localPath), remotePath = remotePath, mimetype = mimeType, metadata = metadata, useCreationWithUpload = true, - firstChunkSize = firstChunkSize, - tusUrl = "", - collectionUrlOverride = collectionUrl, - ).execute(client) - } - - if (createdLocation.isNullOrBlank()) { - throw IllegalStateException("TUS: unable to create upload resource for $remotePath") - } - - tusUrl = createdLocation val metadataString = metadata.entries.joinToString(";") { (key, value) -> "$key=$value" } transferRepository.updateTusState( @@ -103,16 +93,20 @@ class TusUploadHelper( val resolvedTusUrl = tusUrl ?: throw IllegalStateException("TUS: missing upload URL for $remotePath") - var offset = try { - executeRemoteOperation { - GetTusUploadOffsetRemoteOperation(resolvedTusUrl).execute(client) + var offset = if (createdOffset != null) { + createdOffset + } else { + try { + executeRemoteOperation { + GetTusUploadOffsetRemoteOperation(resolvedTusUrl).execute(client) + } + } catch (e: java.io.IOException) { + Timber.w(e, "TUS: failed to fetch current offset") + throw e + } catch (e: Throwable) { + Timber.w(e, "TUS: failed to fetch current offset") + 0L } - } catch (e: java.io.IOException) { - Timber.w(e, "TUS: failed to fetch current offset") - throw e - } catch (e: Throwable) { - Timber.w(e, "TUS: failed to fetch current offset") - 0L }.coerceAtLeast(0L) Timber.d("TUS: resume offset %d / %d", offset, fileSize) progressCallback?.invoke(offset, fileSize) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt index 01251da8c..b482fc5f2 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt @@ -30,7 +30,12 @@ class CreateTusUploadRemoteOperation( private val tusUrl: String?, private val collectionUrlOverride: String? = null, private val base64Encoder: Base64Encoder = DefaultBase64Encoder() -) : RemoteOperation() { +) : RemoteOperation() { + + data class CreationResult( + val uploadUrl: String, + val uploadOffset: Long + ) interface Base64Encoder { fun encode(bytes: ByteArray): String @@ -41,7 +46,7 @@ class CreateTusUploadRemoteOperation( Base64.encodeToString(bytes, Base64.NO_WRAP) } - override fun run(client: OpenCloudClient): RemoteOperationResult = try { + override fun run(client: OpenCloudClient): RemoteOperationResult = try { // Determine TUS endpoint URL based on provided parameters val targetFileUrl = if (!tusUrl.isNullOrBlank()) { tusUrl @@ -135,21 +140,26 @@ class CreateTusUploadRemoteOperation( val base = URL(postMethod.getFinalUrl().toString()) val resolved = resolveLocationToAbsolute(locationHeader, base) + val offsetHeader = postMethod.getResponseHeader(HttpConstants.UPLOAD_OFFSET) + val offset = offsetHeader?.toLongOrNull() ?: 0L + if (resolved != null) { - Timber.d("TUS upload resource created: %s", resolved) - RemoteOperationResult(ResultCode.OK).apply { data = resolved } + Timber.d("TUS upload resource created: %s (offset=%d)", resolved, offset) + RemoteOperationResult(ResultCode.OK).apply { + data = CreationResult(resolved, offset) + } } else { Timber.e("Location header is missing in TUS creation response") - RemoteOperationResult(IllegalStateException("Location header missing")).apply { - data = "" + RemoteOperationResult(IllegalStateException("Location header missing")).apply { + data = null } } } else { Timber.w("TUS creation failed with status: %d", status) - RemoteOperationResult(postMethod).apply { data = "" } + RemoteOperationResult(postMethod).apply { data = null } } } catch (e: Exception) { - val result = RemoteOperationResult(e) + val result = RemoteOperationResult(e) Timber.e(e, "TUS creation operation failed") result } diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt index 94742c4cd..fcfd3b5ce 100644 --- a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt @@ -119,12 +119,16 @@ class TusIntegrationTest { throw RuntimeException(msg, createResult.exception) } assertTrue("Create operation failed", createResult.isSuccess) - val absoluteLocation = createResult.data - assertNotNull(absoluteLocation) + val creationResult = createResult.data + assertNotNull(creationResult) + val absoluteLocation = creationResult!!.uploadUrl + val offset = creationResult.uploadOffset + println("absoluteLocation: $absoluteLocation") println("locationPath: $locationPath") - println("endsWith: ${absoluteLocation!!.endsWith(locationPath)}") + println("endsWith: ${absoluteLocation.endsWith(locationPath)}") assertTrue(absoluteLocation.endsWith(locationPath)) + assertEquals(0L, offset) // Verify POST request headers val postReq = server.takeRequest() From 2d38a39d7d4fb37b6970167da73a398a8172dd80 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Wed, 26 Nov 2025 17:40:05 +0100 Subject: [PATCH 2/2] fix: resolve detekt issues and syntax error in TusUploadHelper --- .../eu/opencloud/android/workers/TusUploadHelper.kt | 13 +++++++++++++ .../lib/resources/files/tus/TusIntegrationTest.kt | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index 2cd64e14a..d5006c635 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -77,6 +77,19 @@ class TusUploadHelper( mimetype = mimeType, metadata = metadata, useCreationWithUpload = true, + useCreationWithUpload = true, + firstChunkSize = firstChunkSize, + tusUrl = "", + collectionUrlOverride = collectionUrl, + ).execute(client) + } + + if (creationResult == null) { + throw java.io.IOException("TUS: unable to create upload resource for $remotePath") + } + + tusUrl = creationResult.uploadUrl + createdOffset = creationResult.uploadOffset val metadataString = metadata.entries.joinToString(";") { (key, value) -> "$key=$value" } transferRepository.updateTusState( diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt index fcfd3b5ce..3ea7f5090 100644 --- a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt @@ -123,7 +123,6 @@ class TusIntegrationTest { assertNotNull(creationResult) val absoluteLocation = creationResult!!.uploadUrl val offset = creationResult.uploadOffset - println("absoluteLocation: $absoluteLocation") println("locationPath: $locationPath") println("endsWith: ${absoluteLocation.endsWith(locationPath)}")