Skip to content

Commit 8317514

Browse files
Test quota
Signed-off-by: tobiasKaminsky <[email protected]>
1 parent d79bc93 commit 8317514

File tree

5 files changed

+207
-0
lines changed

5 files changed

+207
-0
lines changed

.drone.yml

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ services:
175175
- su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test@Test' test@test"
176176
- su www-data -c "OC_PASS=test php /var/www/html/occ user:add --password-from-env --display-name='Test Spaces' 'test test'"
177177
- su www-data -c "php /var/www/html/occ user:setting user2 files quota 1G"
178+
- su www-data -c "php /var/www/html/occ user:setting user3 files quota 1M"
178179
- su www-data -c "php /var/www/html/occ group:add users"
179180
- su www-data -c "php /var/www/html/occ group:adduser users user1"
180181
- 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

+33
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ package com.owncloud.android.lib.resources.files
2323

2424
import android.os.Build
2525
import com.owncloud.android.AbstractIT
26+
import com.owncloud.android.lib.common.OwnCloudBasicCredentials
27+
import com.owncloud.android.lib.common.OwnCloudClientFactory
28+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
2629
import com.owncloud.android.lib.common.utils.Log_OC
2730
import com.owncloud.android.lib.resources.files.model.RemoteFile
2831
import junit.framework.TestCase.assertEquals
32+
import org.junit.Assert.assertFalse
2933
import org.junit.Assert.assertNotNull
3034
import org.junit.Assert.assertTrue
3135
import org.junit.Test
@@ -94,6 +98,34 @@ class UploadFileRemoteOperationIT : AbstractIT() {
9498
)
9599
}
96100

101+
@Throws(Throwable::class)
102+
@Test
103+
fun uploadFileWithQuotaExceeded() {
104+
// user3 has quota of 1Mb
105+
val client3 = OwnCloudClientFactory.createOwnCloudClient(url, context, true)
106+
client3.credentials = OwnCloudBasicCredentials("user3", "user3")
107+
client3.userId = "user3"
108+
109+
// create file
110+
val filePath = createFile("quota", LARGE_FILE)
111+
val remotePath = "/quota.md"
112+
113+
val creationTimestamp = getCreationTimestamp(File(filePath))
114+
val sut = UploadFileRemoteOperation(
115+
filePath,
116+
remotePath,
117+
"text/markdown",
118+
"",
119+
RANDOM_MTIME,
120+
creationTimestamp,
121+
true
122+
)
123+
124+
val uploadResult = sut.execute(client3)
125+
assertFalse(uploadResult.isSuccess)
126+
assertEquals(ResultCode.QUOTA_EXCEEDED, uploadResult.code)
127+
}
128+
97129
private fun getCreationTimestamp(file: File): Long? {
98130
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
99131
return null
@@ -114,5 +146,6 @@ class UploadFileRemoteOperationIT : AbstractIT() {
114146

115147
companion object {
116148
const val TIME_OFFSET = 10
149+
const val LARGE_FILE = 10 * 1024
117150
}
118151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
50+
@Deprecated("Deprecated in Java")
51+
@Suppress("Detekt.ReturnCount")
52+
override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> {
53+
var propfind: PropFindMethod? = null
54+
try {
55+
val file = File(path)
56+
val folder = 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 = PropFindMethod(
65+
client.getFilesDavUri(folder),
66+
propSet,
67+
0
68+
)
69+
val status = client.executeMethod(propfind, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT)
70+
if (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) {
71+
val resp = propfind.responseBodyAsMultiStatus.responses[0]
72+
val string = resp.getProperties(HttpStatus.SC_OK)[QUOTA_PROPERTY].value as String
73+
val quota = string.toLong()
74+
return if (isSuccess(quota)) {
75+
RemoteOperationResult<Boolean>(true, propfind)
76+
} else {
77+
RemoteOperationResult<Boolean>(false, propfind)
78+
}
79+
}
80+
if (status == HttpStatus.SC_NOT_FOUND) {
81+
return RemoteOperationResult(ResultCode.FILE_NOT_FOUND)
82+
}
83+
} catch (e: DavException) {
84+
Log_OC.e(TAG, "Error while retrieving quota")
85+
} catch (e: IOException) {
86+
Log_OC.e(TAG, "Error while retrieving quota")
87+
} catch (e: NumberFormatException) {
88+
Log_OC.e(TAG, "Error while retrieving quota")
89+
} finally {
90+
propfind?.releaseConnection()
91+
}
92+
return RemoteOperationResult(ResultCode.ETAG_CHANGED)
93+
}
94+
95+
private fun isSuccess(quota: Long): Boolean {
96+
return quota >= fileSize ||
97+
quota == UNKNOWN_FREE_SPACE ||
98+
quota == UNCOMPUTED_FREE_SPACE ||
99+
quota == UNLIMITED_FREE_SPACE
100+
}
101+
102+
companion object {
103+
private const val SYNC_READ_TIMEOUT = 40000
104+
private const val SYNC_CONNECTION_TIMEOUT = 5000
105+
private const val UNCOMPUTED_FREE_SPACE = -1L
106+
private const val UNKNOWN_FREE_SPACE = -2L
107+
private const val UNLIMITED_FREE_SPACE = -3L
108+
private val QUOTA_PROPERTY = DavPropertyName.create(WebdavEntry.PROPERTY_QUOTA_AVAILABLE_BYTES)
109+
private val TAG = CheckEnoughQuotaRemoteOperation::class.java.simpleName
110+
}
111+
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ public UploadFileRemoteOperation(String localPath,
152152
@Override
153153
protected RemoteOperationResult<String> run(OwnCloudClient client) {
154154
RemoteOperationResult<String> result;
155+
156+
// check quota
157+
long fileLength = new File(localPath).length();
158+
RemoteOperationResult checkEnoughQuotaResult =
159+
new CheckEnoughQuotaRemoteOperation(remotePath, fileLength)
160+
.run(client);
161+
162+
if (!checkEnoughQuotaResult.isSuccess()) {
163+
return new RemoteOperationResult<>(checkEnoughQuotaResult.getCode());
164+
}
165+
155166
DefaultHttpMethodRetryHandler oldRetryHandler =
156167
(DefaultHttpMethodRetryHandler) client.getParams().getParameter(HttpMethodParams.RETRY_HANDLER);
157168

0 commit comments

Comments
 (0)