diff --git a/.drone.yml b/.drone.yml index b795a0328..6d5b0fa20 100644 --- a/.drone.yml +++ b/.drone.yml @@ -194,6 +194,7 @@ services: - su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test@Test' test@test" - su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test Spaces' 'test test'" - su www-data -c "php /var/www/html/occ user:setting user2 files quota 1G" + - su www-data -c "php /var/www/html/occ user:setting user3 files quota 1M" - su www-data -c "php /var/www/html/occ group:add users" - su www-data -c "php /var/www/html/occ group:adduser users user1" - su www-data -c "php /var/www/html/occ group:adduser users user2" @@ -231,6 +232,6 @@ trigger: - pull_request --- kind: signature -hmac: 1fb704f7c721412a3d25d79472652236c11bebb82c8748e132b413b8906e7a84 +hmac: 95696572016fc48915fb9d1f479217d822db3afc6ac06dcb4071a304361b483f ... diff --git a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperationIT.kt b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperationIT.kt new file mode 100644 index 000000000..aa7a3d642 --- /dev/null +++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperationIT.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2023 Tobias Kaminsky + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.files + +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.common.OwnCloudBasicCredentials +import com.owncloud.android.lib.common.OwnCloudClientFactory +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CheckEnoughQuotaRemoteOperationIT : AbstractIT() { + @Test + fun enoughQuota() { + val sut = CheckEnoughQuotaRemoteOperation("/", LARGE_FILE).execute(client) + assertTrue(sut.isSuccess) + } + + @Test + fun noQuota() { + // user3 has only 1M quota + val client3 = OwnCloudClientFactory.createOwnCloudClient(url, context, true) + client3.credentials = OwnCloudBasicCredentials("user3", "user3") + val sut = CheckEnoughQuotaRemoteOperation("/", LARGE_FILE).execute(client3) + assertFalse(sut.isSuccess) + } + + companion object { + const val LARGE_FILE = 5 * 1024 * 1024L + } +} diff --git a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperationIT.kt b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperationIT.kt index 8376af46c..3846a221a 100644 --- a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperationIT.kt +++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperationIT.kt @@ -9,9 +9,13 @@ package com.owncloud.android.lib.resources.files import android.os.Build import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.common.OwnCloudBasicCredentials +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.RemoteFile import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test @@ -80,6 +84,35 @@ class UploadFileRemoteOperationIT : AbstractIT() { ) } + @Throws(Throwable::class) + @Test + fun uploadFileWithQuotaExceeded() { + // user3 has quota of 1Mb + val client3 = OwnCloudClientFactory.createOwnCloudClient(url, context, true) + client3.credentials = OwnCloudBasicCredentials("user3", "user3") + client3.userId = "user3" + + // create file + val filePath = createFile("quota", LARGE_FILE) + val remotePath = "/quota.md" + + val creationTimestamp = getCreationTimestamp(File(filePath)) + val sut = + UploadFileRemoteOperation( + filePath, + remotePath, + "text/markdown", + "", + RANDOM_MTIME, + creationTimestamp, + true + ) + + val uploadResult = sut.execute(client3) + assertFalse(uploadResult.isSuccess) + assertEquals(ResultCode.QUOTA_EXCEEDED, uploadResult.code) + } + private fun getCreationTimestamp(file: File): Long? { return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return null @@ -100,5 +133,6 @@ class UploadFileRemoteOperationIT : AbstractIT() { companion object { const val TIME_OFFSET = 10 + const val LARGE_FILE = 10 * 1024 } } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperation.kt new file mode 100644 index 000000000..31b04a15b --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperation.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2023 Tobias Kaminsky + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.files + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.network.WebdavEntry +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.common.utils.Log_OC +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.DavException +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod +import org.apache.jackrabbit.webdav.property.DavPropertyName +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet +import java.io.File +import java.io.IOException + +/** + * Check if remaining quota is big enough + * @param fileSize filesize in bytes + */ +class CheckEnoughQuotaRemoteOperation(val path: String, private val fileSize: Long) : + RemoteOperation() { + @Deprecated("Deprecated in Java") + @Suppress("Detekt.ReturnCount") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var propfind: PropFindMethod? = null + try { + val file = File(path) + val folder = + if (file.path.endsWith(FileUtils.PATH_SEPARATOR)) { + file.path + } else { + file.parent ?: throw IllegalStateException("Parent path not found") + } + + val propSet = DavPropertyNameSet() + propSet.add(QUOTA_PROPERTY) + propfind = + PropFindMethod( + client.getFilesDavUri(folder), + propSet, + 0 + ) + val status = client.executeMethod(propfind, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT) + if (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) { + val resp = propfind.responseBodyAsMultiStatus.responses[0] + val string = resp.getProperties(HttpStatus.SC_OK)[QUOTA_PROPERTY].value as String + val quota = string.toLong() + return if (isSuccess(quota)) { + RemoteOperationResult(true, propfind) + } else { + RemoteOperationResult(false, propfind) + } + } + if (status == HttpStatus.SC_NOT_FOUND) { + return RemoteOperationResult(ResultCode.FILE_NOT_FOUND) + } + } catch (e: DavException) { + Log_OC.e(TAG, "Error while retrieving quota") + } catch (e: IOException) { + Log_OC.e(TAG, "Error while retrieving quota") + } catch (e: NumberFormatException) { + Log_OC.e(TAG, "Error while retrieving quota") + } finally { + propfind?.releaseConnection() + } + return RemoteOperationResult(ResultCode.ETAG_CHANGED) + } + + private fun isSuccess(quota: Long): Boolean { + return quota >= fileSize || + quota == UNKNOWN_FREE_SPACE || + quota == UNCOMPUTED_FREE_SPACE || + quota == UNLIMITED_FREE_SPACE + } + + companion object { + private const val SYNC_READ_TIMEOUT = 40000 + private const val SYNC_CONNECTION_TIMEOUT = 5000 + private const val UNCOMPUTED_FREE_SPACE = -1L + private const val UNKNOWN_FREE_SPACE = -2L + private const val UNLIMITED_FREE_SPACE = -3L + private val QUOTA_PROPERTY = DavPropertyName.create(WebdavEntry.PROPERTY_QUOTA_AVAILABLE_BYTES) + private val TAG = CheckEnoughQuotaRemoteOperation::class.java.simpleName + } +} 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 652e22f1f..d19a3f747 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 @@ -137,6 +137,17 @@ public UploadFileRemoteOperation(String localPath, @Override protected RemoteOperationResult run(OwnCloudClient client) { RemoteOperationResult result; + + // check quota + long fileLength = new File(localPath).length(); + RemoteOperationResult checkEnoughQuotaResult = + new CheckEnoughQuotaRemoteOperation(remotePath, fileLength) + .run(client); + + if (!checkEnoughQuotaResult.isSuccess()) { + return new RemoteOperationResult<>(checkEnoughQuotaResult.getCode()); + } + DefaultHttpMethodRetryHandler oldRetryHandler = (DefaultHttpMethodRetryHandler) client.getParams().getParameter(HttpMethodParams.RETRY_HANDLER);