From 72b90be667843c9e409c3372a7e7426aa4274ead Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 13 Oct 2026 09:14:18 +0200 Subject: [PATCH 1/3] make compatible with client Signed-off-by: alperozturk96 --- library/build.gradle | 2 +- sample_client/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index 32b7c6177..06a3b7bd3 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -18,7 +18,7 @@ buildscript { junit_version = '4.13.2' } dependencies { - classpath 'com.android.tools.build:gradle:9.4.0' + classpath 'com.android.tools.build:gradle:9.4.1' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.5.11' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" diff --git a/sample_client/build.gradle b/sample_client/build.gradle index a8299c043..1045edda4 100644 --- a/sample_client/build.gradle +++ b/sample_client/build.gradle @@ -11,7 +11,7 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:9.4.0' + classpath 'com.android.tools.build:gradle:9.4.1' classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.5.11' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8" From 36c60a34dd69e1460b6d5410b2309b1d880d89b8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 13 Oct 2026 09:14:33 +0200 Subject: [PATCH 2/3] OC-Total-Length needed for quota check Signed-off-by: alperozturk96 --- .../lib/resources/files/ChunkedFileUploadRemoteOperation.kt | 1 + .../android/lib/resources/files/UploadFileRemoteOperation.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 65ac767a4..f676021e0 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -236,6 +236,7 @@ class ChunkedFileUploadRemoteOperation val put = createPutMethod("$uploadFolderUri/$chunkName") put.addRequestHeader(DESTINATION_HEADER, destinationUri) + put.addRequestHeader(OC_TOTAL_LENGTH_HEADER, file.length().toString()) token?.let { put.addRequestHeader(E2E_TOKEN, it) } val status = client.executeMethod(put) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java index 97de363cb..54d654ec8 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java @@ -40,7 +40,7 @@ */ public class UploadFileRemoteOperation extends RemoteOperation { - private static final String OC_TOTAL_LENGTH_HEADER = "OC-Total-Length"; + protected static final String OC_TOTAL_LENGTH_HEADER = "OC-Total-Length"; private static final String IF_MATCH_HEADER = "If-Match"; protected static final String RESULT_ETAG_HEADER = "etag"; protected static final String OC_X_OC_MTIME_HEADER = "X-OC-Mtime"; From 6a277209ba67b97f36ae8cc815a761d4bf4e242d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 13 Oct 2026 09:24:19 +0200 Subject: [PATCH 3/3] add tests Signed-off-by: alperozturk96 --- .../ChunkedFileUploadRemoteOperationIT.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/webdav/ChunkedFileUploadRemoteOperationIT.kt b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/webdav/ChunkedFileUploadRemoteOperationIT.kt index 31f834e3d..3bd3a5f20 100644 --- a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/webdav/ChunkedFileUploadRemoteOperationIT.kt +++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/webdav/ChunkedFileUploadRemoteOperationIT.kt @@ -2,6 +2,7 @@ * Nextcloud Android Library * * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-License-Identifier: MIT */ @@ -10,14 +11,24 @@ package com.owncloud.android.lib.resources.files.webdav import com.owncloud.android.AbstractIT import com.owncloud.android.lib.common.network.WebdavEntry import com.owncloud.android.lib.common.network.WebdavUtils +import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.resources.files.ChunkedFileUploadRemoteOperation +import com.owncloud.android.lib.resources.files.FileUtils import junit.framework.TestCase import junit.framework.TestCase.assertNotNull +import org.apache.commons.httpclient.HttpStatus +import org.apache.commons.httpclient.methods.PutMethod +import org.apache.commons.httpclient.methods.StringRequestEntity +import org.apache.jackrabbit.webdav.DavConstants +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod import org.apache.jackrabbit.webdav.client.methods.PropFindMethod +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test import java.io.File +import java.net.URLEncoder class ChunkedFileUploadRemoteOperationIT : AbstractIT() { @Test @@ -120,6 +131,66 @@ class ChunkedFileUploadRemoteOperationIT : AbstractIT() { assert(File(filePath).length() == getRemoteSize(remotePath)) } + @Test + fun uploadExceedingQuotaIsRejectedBeforeAnyChunkIsStored() { + val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION) + val uploadFolderUri = "${client.uploadUri}/${client.userId}/${FileUtils.md5Sum(File(filePath))}" + val sut = + ChunkedFileUploadRemoteOperation( + filePath, + "/bigFile.md", + "text/markdown", + "", + RANDOM_MTIME, + System.currentTimeMillis() / MILLI_TO_SECOND, + false, + true + ) + + setQuota(QUOTA_SMALLER_THAN_FILE) + try { + val uploadResult = sut.execute(client) + + assertEquals(ResultCode.QUOTA_EXCEEDED, uploadResult.code) + assertEquals(0, storedChunkCount(uploadFolderUri)) + } finally { + setQuota(QUOTA_UNLIMITED) + DeleteMethod(uploadFolderUri).also { + client.executeMethod(it) + it.releaseConnection() + } + } + } + + private fun setQuota(quota: String) { + val method = PutMethod("${client2.baseUri}/ocs/v2.php/cloud/users/${client.userId}") + method.addRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE) + method.requestEntity = + StringRequestEntity( + "key=quota&value=${URLEncoder.encode(quota, Charsets.UTF_8.name())}", + "application/x-www-form-urlencoded", + Charsets.UTF_8.name() + ) + + try { + assertEquals(HttpStatus.SC_OK, client2.executeMethod(method)) + } finally { + method.releaseConnection() + } + } + + private fun storedChunkCount(uploadFolderUri: String): Int { + val propFind = PropFindMethod(uploadFolderUri, WebdavUtils.getChunksPropSet(), DavConstants.DEPTH_1) + + try { + client.executeMethod(propFind) + assertTrue(propFind.succeeded()) + return propFind.responseBodyAsMultiStatus.responses.size - UPLOAD_FOLDER_ENTRY + } finally { + propFind.releaseConnection() + } + } + private fun genLargeUpload(onWifiConnection: Boolean): ChunkedFileUploadRemoteOperation { // create file val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION) @@ -149,5 +220,8 @@ class ChunkedFileUploadRemoteOperationIT : AbstractIT() { companion object { const val BIG_FILE_ITERATION = 1000000 const val MAX_TRIES = 30 + const val QUOTA_SMALLER_THAN_FILE = "1 MB" + const val QUOTA_UNLIMITED = "none" + const val UPLOAD_FOLDER_ENTRY = 1 } }