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);