diff --git a/.drone.yml b/.drone.yml
index 613fdd3f0c..bef85d1ce6 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -199,6 +199,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"
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 0000000000..4f6062a0da
--- /dev/null
+++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperationIT.kt
@@ -0,0 +1,51 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+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 56e9196928..e0836727e9 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
@@ -101,5 +134,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 0000000000..8f998e9ab4
--- /dev/null
+++ b/library/src/main/java/com/owncloud/android/lib/resources/files/CheckEnoughQuotaRemoteOperation.kt
@@ -0,0 +1,112 @@
+/* Nextcloud Android Library is available under MIT license
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+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 652e22f1f9..d19a3f7470 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);