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..d5006c635 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,26 @@ 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, + useCreationWithUpload = true, firstChunkSize = firstChunkSize, tusUrl = "", collectionUrlOverride = collectionUrl, ).execute(client) } - if (createdLocation.isNullOrBlank()) { - throw IllegalStateException("TUS: unable to create upload resource for $remotePath") + if (creationResult == null) { + throw java.io.IOException("TUS: unable to create upload resource for $remotePath") } - tusUrl = createdLocation + tusUrl = creationResult.uploadUrl + createdOffset = creationResult.uploadOffset val metadataString = metadata.entries.joinToString(";") { (key, value) -> "$key=$value" } transferRepository.updateTusState( @@ -103,16 +106,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..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 @@ -119,12 +119,15 @@ 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()