Skip to content

Commit c473e89

Browse files
tobiasKaminskyAndyScherzinger
authored andcommitted
Test quota
Signed-off-by: tobiasKaminsky <[email protected]>
1 parent e66304e commit c473e89

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed

.drone.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ services:
194194
- su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test@Test' test@test"
195195
- su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test Spaces' 'test test'"
196196
- su www-data -c "php /var/www/html/occ user:setting user2 files quota 1G"
197+
- su www-data -c "php /var/www/html/occ user:setting user3 files quota 1M"
197198
- su www-data -c "php /var/www/html/occ group:add users"
198199
- su www-data -c "php /var/www/html/occ group:adduser users user1"
199200
- su www-data -c "php /var/www/html/occ group:adduser users user2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Tobias Kaminsky
5+
* Copyright (C) 2023 Tobias Kaminsky
6+
* Copyright (C) 2023 Nextcloud GmbH
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
package com.owncloud.android.lib.resources.files
24+
25+
import com.owncloud.android.AbstractIT
26+
import com.owncloud.android.lib.common.OwnCloudBasicCredentials
27+
import com.owncloud.android.lib.common.OwnCloudClientFactory
28+
import org.junit.Assert.assertFalse
29+
import org.junit.Assert.assertTrue
30+
import org.junit.Test
31+
32+
class CheckEnoughQuotaRemoteOperationIT : AbstractIT() {
33+
@Test
34+
fun enoughQuota() {
35+
val sut = CheckEnoughQuotaRemoteOperation("/", LARGE_FILE).execute(client)
36+
assertTrue(sut.isSuccess)
37+
}
38+
39+
@Test
40+
fun noQuota() {
41+
// user3 has only 1M quota
42+
val client3 = OwnCloudClientFactory.createOwnCloudClient(url, context, true)
43+
client3.credentials = OwnCloudBasicCredentials("user3", "user3")
44+
val sut = CheckEnoughQuotaRemoteOperation("/", LARGE_FILE).execute(client3)
45+
assertFalse(sut.isSuccess)
46+
}
47+
48+
companion object {
49+
const val LARGE_FILE = 5 * 1024 * 1024L
50+
}
51+
}

library/src/androidTest/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperationIT.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ package com.owncloud.android.lib.resources.files
99

1010
import android.os.Build
1111
import com.owncloud.android.AbstractIT
12+
import com.owncloud.android.lib.common.OwnCloudBasicCredentials
13+
import com.owncloud.android.lib.common.OwnCloudClientFactory
14+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
1215
import com.owncloud.android.lib.common.utils.Log_OC
1316
import com.owncloud.android.lib.resources.files.model.RemoteFile
1417
import junit.framework.TestCase.assertEquals
18+
import org.junit.Assert.assertFalse
1519
import org.junit.Assert.assertNotNull
1620
import org.junit.Assert.assertTrue
1721
import org.junit.Test
@@ -80,6 +84,35 @@ class UploadFileRemoteOperationIT : AbstractIT() {
8084
)
8185
}
8286

87+
@Throws(Throwable::class)
88+
@Test
89+
fun uploadFileWithQuotaExceeded() {
90+
// user3 has quota of 1Mb
91+
val client3 = OwnCloudClientFactory.createOwnCloudClient(url, context, true)
92+
client3.credentials = OwnCloudBasicCredentials("user3", "user3")
93+
client3.userId = "user3"
94+
95+
// create file
96+
val filePath = createFile("quota", LARGE_FILE)
97+
val remotePath = "/quota.md"
98+
99+
val creationTimestamp = getCreationTimestamp(File(filePath))
100+
val sut =
101+
UploadFileRemoteOperation(
102+
filePath,
103+
remotePath,
104+
"text/markdown",
105+
"",
106+
RANDOM_MTIME,
107+
creationTimestamp,
108+
true
109+
)
110+
111+
val uploadResult = sut.execute(client3)
112+
assertFalse(uploadResult.isSuccess)
113+
assertEquals(ResultCode.QUOTA_EXCEEDED, uploadResult.code)
114+
}
115+
83116
private fun getCreationTimestamp(file: File): Long? {
84117
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
85118
return null
@@ -100,5 +133,6 @@ class UploadFileRemoteOperationIT : AbstractIT() {
100133

101134
companion object {
102135
const val TIME_OFFSET = 10
136+
const val LARGE_FILE = 10 * 1024
103137
}
104138
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* Nextcloud Android Library is available under MIT license
2+
*
3+
* @author Tobias Kaminsky
4+
* Copyright (C) 2023 Tobias Kaminsky
5+
* Copyright (C) 2023 Nextcloud GmbH
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21+
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22+
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*
26+
*/
27+
package com.owncloud.android.lib.resources.files
28+
29+
import com.owncloud.android.lib.common.OwnCloudClient
30+
import com.owncloud.android.lib.common.network.WebdavEntry
31+
import com.owncloud.android.lib.common.operations.RemoteOperation
32+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
33+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
34+
import com.owncloud.android.lib.common.utils.Log_OC
35+
import org.apache.commons.httpclient.HttpStatus
36+
import org.apache.jackrabbit.webdav.DavException
37+
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod
38+
import org.apache.jackrabbit.webdav.property.DavPropertyName
39+
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet
40+
import java.io.File
41+
import java.io.IOException
42+
43+
/**
44+
* Check if remaining quota is big enough
45+
* @param fileSize filesize in bytes
46+
*/
47+
class CheckEnoughQuotaRemoteOperation(val path: String, private val fileSize: Long) :
48+
RemoteOperation<Boolean>() {
49+
@Deprecated("Deprecated in Java")
50+
@Suppress("Detekt.ReturnCount")
51+
override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> {
52+
var propfind: PropFindMethod? = null
53+
try {
54+
val file = File(path)
55+
val folder =
56+
if (file.path.endsWith(FileUtils.PATH_SEPARATOR)) {
57+
file.path
58+
} else {
59+
file.parent ?: throw IllegalStateException("Parent path not found")
60+
}
61+
62+
val propSet = DavPropertyNameSet()
63+
propSet.add(QUOTA_PROPERTY)
64+
propfind =
65+
PropFindMethod(
66+
client.getFilesDavUri(folder),
67+
propSet,
68+
0
69+
)
70+
val status = client.executeMethod(propfind, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT)
71+
if (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) {
72+
val resp = propfind.responseBodyAsMultiStatus.responses[0]
73+
val string = resp.getProperties(HttpStatus.SC_OK)[QUOTA_PROPERTY].value as String
74+
val quota = string.toLong()
75+
return if (isSuccess(quota)) {
76+
RemoteOperationResult<Boolean>(true, propfind)
77+
} else {
78+
RemoteOperationResult<Boolean>(false, propfind)
79+
}
80+
}
81+
if (status == HttpStatus.SC_NOT_FOUND) {
82+
return RemoteOperationResult(ResultCode.FILE_NOT_FOUND)
83+
}
84+
} catch (e: DavException) {
85+
Log_OC.e(TAG, "Error while retrieving quota")
86+
} catch (e: IOException) {
87+
Log_OC.e(TAG, "Error while retrieving quota")
88+
} catch (e: NumberFormatException) {
89+
Log_OC.e(TAG, "Error while retrieving quota")
90+
} finally {
91+
propfind?.releaseConnection()
92+
}
93+
return RemoteOperationResult(ResultCode.ETAG_CHANGED)
94+
}
95+
96+
private fun isSuccess(quota: Long): Boolean {
97+
return quota >= fileSize ||
98+
quota == UNKNOWN_FREE_SPACE ||
99+
quota == UNCOMPUTED_FREE_SPACE ||
100+
quota == UNLIMITED_FREE_SPACE
101+
}
102+
103+
companion object {
104+
private const val SYNC_READ_TIMEOUT = 40000
105+
private const val SYNC_CONNECTION_TIMEOUT = 5000
106+
private const val UNCOMPUTED_FREE_SPACE = -1L
107+
private const val UNKNOWN_FREE_SPACE = -2L
108+
private const val UNLIMITED_FREE_SPACE = -3L
109+
private val QUOTA_PROPERTY = DavPropertyName.create(WebdavEntry.PROPERTY_QUOTA_AVAILABLE_BYTES)
110+
private val TAG = CheckEnoughQuotaRemoteOperation::class.java.simpleName
111+
}
112+
}

library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ public UploadFileRemoteOperation(String localPath,
137137
@Override
138138
protected RemoteOperationResult<String> run(OwnCloudClient client) {
139139
RemoteOperationResult<String> result;
140+
141+
// check quota
142+
long fileLength = new File(localPath).length();
143+
RemoteOperationResult checkEnoughQuotaResult =
144+
new CheckEnoughQuotaRemoteOperation(remotePath, fileLength)
145+
.run(client);
146+
147+
if (!checkEnoughQuotaResult.isSuccess()) {
148+
return new RemoteOperationResult<>(checkEnoughQuotaResult.getCode());
149+
}
150+
140151
DefaultHttpMethodRetryHandler oldRetryHandler =
141152
(DefaultHttpMethodRetryHandler) client.getParams().getParameter(HttpMethodParams.RETRY_HANDLER);
142153

0 commit comments

Comments
 (0)