Skip to content

Commit 593f49c

Browse files
Merge pull request #1173 from nextcloud/s3-multipart-upload
S3 multipart upload
2 parents 4a7ad62 + ee3d96d commit 593f49c

File tree

8 files changed

+467
-440
lines changed

8 files changed

+467
-440
lines changed

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

+95-27
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,83 @@
2222
package com.owncloud.android.lib.resources.files.webdav
2323

2424
import com.owncloud.android.AbstractIT
25+
import com.owncloud.android.lib.common.network.WebdavEntry
26+
import com.owncloud.android.lib.common.network.WebdavUtils
2527
import com.owncloud.android.lib.common.operations.RemoteOperationResult
2628
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
2729
import com.owncloud.android.lib.resources.files.ChunkedFileUploadRemoteOperation
2830
import junit.framework.TestCase
2931
import junit.framework.TestCase.assertNotNull
30-
import junit.framework.TestCase.assertTrue
32+
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod
3133
import org.junit.Test
34+
import java.io.File
3235

3336
class ChunkedFileUploadRemoteOperationIT : AbstractIT() {
3437
@Test
35-
fun upload() {
36-
// create file
37-
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
38-
val remotePath = "/bigFile.md"
39-
40-
val sut = ChunkedFileUploadRemoteOperation(
41-
filePath,
42-
remotePath,
43-
"text/markdown",
44-
"",
45-
RANDOM_MTIME,
46-
System.currentTimeMillis() / MILLI_TO_SECOND,
47-
true,
48-
true
49-
)
38+
fun uploadWifi() {
39+
val sut = genLargeUpload(true)
40+
val uploadResult = sut.execute(client)
41+
assert(uploadResult.isSuccess)
42+
}
5043

44+
@Test
45+
fun uploadMobile() {
46+
val sut = genLargeUpload(false)
5147
val uploadResult = sut.execute(client)
52-
assertTrue(uploadResult.isSuccess)
48+
assert(uploadResult.isSuccess)
5349
}
5450

5551
@Test
5652
fun cancel() {
57-
// create file
58-
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
59-
val remotePath = "/cancelFile.md"
53+
val sut = genLargeUpload(false)
54+
55+
var uploadResult: RemoteOperationResult<String>? = null
56+
Thread {
57+
uploadResult = sut.execute(client)
58+
}.start()
59+
60+
shortSleep()
61+
sut.cancel(ResultCode.CANCELLED)
62+
63+
for (i in 1..MAX_TRIES) {
64+
shortSleep()
65+
66+
if (uploadResult != null) {
67+
break
68+
}
69+
}
70+
71+
assertNotNull(uploadResult)
72+
TestCase.assertFalse(uploadResult?.isSuccess == true)
73+
TestCase.assertSame(ResultCode.CANCELLED, uploadResult?.code)
74+
}
6075

61-
val sut = ChunkedFileUploadRemoteOperation(
76+
@Test
77+
fun resume() {
78+
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION * 2)
79+
val timestamp = System.currentTimeMillis() / MILLI_TO_SECOND
80+
val remotePath = "/bigFile.md"
81+
82+
// set up first upload
83+
var sut = ChunkedFileUploadRemoteOperation(
6284
filePath,
6385
remotePath,
6486
"text/markdown",
6587
"",
6688
RANDOM_MTIME,
67-
System.currentTimeMillis() / MILLI_TO_SECOND,
89+
timestamp,
6890
false,
6991
true
7092
)
7193

94+
// start first upload
7295
var uploadResult: RemoteOperationResult<String>? = null
7396
Thread {
7497
uploadResult = sut.execute(client)
7598
}.start()
7699

100+
// delay and cancel upload
101+
shortSleep()
77102
shortSleep()
78103
sut.cancel(ResultCode.CANCELLED)
79104

@@ -85,13 +110,56 @@ class ChunkedFileUploadRemoteOperationIT : AbstractIT() {
85110
}
86111
}
87112

88-
assertNotNull(uploadResult)
89-
TestCase.assertFalse(uploadResult?.isSuccess == true)
90-
TestCase.assertSame(ResultCode.CANCELLED, uploadResult?.code)
113+
// start second upload of same file
114+
sut = ChunkedFileUploadRemoteOperation(
115+
filePath,
116+
remotePath,
117+
"text/markdown",
118+
"",
119+
RANDOM_MTIME,
120+
timestamp,
121+
false,
122+
true
123+
)
124+
125+
// reset result; start second upload
126+
uploadResult = null
127+
uploadResult = sut.execute(client)
128+
129+
// second upload should succeed
130+
assert(uploadResult?.isSuccess == true)
131+
132+
assert(File(filePath).length() == getRemoteSize(remotePath))
133+
}
134+
135+
private fun genLargeUpload(onWifiConnection: Boolean): ChunkedFileUploadRemoteOperation {
136+
// create file
137+
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
138+
val remotePath = "/bigFile.md"
139+
140+
return ChunkedFileUploadRemoteOperation(
141+
filePath,
142+
remotePath,
143+
"text/markdown",
144+
"",
145+
RANDOM_MTIME,
146+
System.currentTimeMillis() / MILLI_TO_SECOND,
147+
onWifiConnection,
148+
true
149+
)
150+
}
151+
152+
private fun getRemoteSize(remotePath: String): Long {
153+
val davPath = client.filesDavUri.toString() + "/" + WebdavUtils.encodePath(remotePath)
154+
val propFindMethod = PropFindMethod(davPath, WebdavUtils.getFilePropSet(), 0)
155+
client.executeMethod(propFindMethod)
156+
assert(propFindMethod.succeeded())
157+
158+
return WebdavEntry(propFindMethod.responseBodyAsMultiStatus.responses[0], remotePath).contentLength
91159
}
92160

93161
companion object {
94-
val BIG_FILE_ITERATION = 500000
95-
val MAX_TRIES = 30
162+
const val BIG_FILE_ITERATION = 1000000
163+
const val MAX_TRIES = 30
96164
}
97165
}

library/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java

+1
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ public static DavPropertyNameSet getChunksPropSet() {
211211
DavPropertyNameSet propSet = new DavPropertyNameSet();
212212
propSet.add(DavPropertyName.GETCONTENTTYPE);
213213
propSet.add(DavPropertyName.RESOURCETYPE);
214+
propSet.add(DavPropertyName.GETCONTENTLENGTH);
214215

215216
return propSet;
216217
}

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

-58
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* Nextcloud Android Library is available under MIT license
2+
*
3+
* @author ZetaTom
4+
* Copyright (C) 2023 ZetaTom
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+
28+
package com.owncloud.android.lib.resources.files
29+
30+
data class Chunk(val id: Int, val start: Long, val length: Long)

0 commit comments

Comments
 (0)