Skip to content

Commit

Permalink
Merge pull request #1173 from nextcloud/s3-multipart-upload
Browse files Browse the repository at this point in the history
S3 multipart upload
  • Loading branch information
tobiasKaminsky authored Aug 28, 2023
2 parents 4a7ad62 + ee3d96d commit 593f49c
Show file tree
Hide file tree
Showing 8 changed files with 467 additions and 440 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,58 +22,83 @@
package com.owncloud.android.lib.resources.files.webdav

import com.owncloud.android.AbstractIT
import com.owncloud.android.lib.common.network.WebdavEntry
import com.owncloud.android.lib.common.network.WebdavUtils
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.resources.files.ChunkedFileUploadRemoteOperation
import junit.framework.TestCase
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod
import org.junit.Test
import java.io.File

class ChunkedFileUploadRemoteOperationIT : AbstractIT() {
@Test
fun upload() {
// create file
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
val remotePath = "/bigFile.md"

val sut = ChunkedFileUploadRemoteOperation(
filePath,
remotePath,
"text/markdown",
"",
RANDOM_MTIME,
System.currentTimeMillis() / MILLI_TO_SECOND,
true,
true
)
fun uploadWifi() {
val sut = genLargeUpload(true)
val uploadResult = sut.execute(client)
assert(uploadResult.isSuccess)
}

@Test
fun uploadMobile() {
val sut = genLargeUpload(false)
val uploadResult = sut.execute(client)
assertTrue(uploadResult.isSuccess)
assert(uploadResult.isSuccess)
}

@Test
fun cancel() {
// create file
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
val remotePath = "/cancelFile.md"
val sut = genLargeUpload(false)

var uploadResult: RemoteOperationResult<String>? = null
Thread {
uploadResult = sut.execute(client)
}.start()

shortSleep()
sut.cancel(ResultCode.CANCELLED)

for (i in 1..MAX_TRIES) {
shortSleep()

if (uploadResult != null) {
break
}
}

assertNotNull(uploadResult)
TestCase.assertFalse(uploadResult?.isSuccess == true)
TestCase.assertSame(ResultCode.CANCELLED, uploadResult?.code)
}

val sut = ChunkedFileUploadRemoteOperation(
@Test
fun resume() {
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION * 2)
val timestamp = System.currentTimeMillis() / MILLI_TO_SECOND
val remotePath = "/bigFile.md"

// set up first upload
var sut = ChunkedFileUploadRemoteOperation(
filePath,
remotePath,
"text/markdown",
"",
RANDOM_MTIME,
System.currentTimeMillis() / MILLI_TO_SECOND,
timestamp,
false,
true
)

// start first upload
var uploadResult: RemoteOperationResult<String>? = null
Thread {
uploadResult = sut.execute(client)
}.start()

// delay and cancel upload
shortSleep()
shortSleep()
sut.cancel(ResultCode.CANCELLED)

Expand All @@ -85,13 +110,56 @@ class ChunkedFileUploadRemoteOperationIT : AbstractIT() {
}
}

assertNotNull(uploadResult)
TestCase.assertFalse(uploadResult?.isSuccess == true)
TestCase.assertSame(ResultCode.CANCELLED, uploadResult?.code)
// start second upload of same file
sut = ChunkedFileUploadRemoteOperation(
filePath,
remotePath,
"text/markdown",
"",
RANDOM_MTIME,
timestamp,
false,
true
)

// reset result; start second upload
uploadResult = null
uploadResult = sut.execute(client)

// second upload should succeed
assert(uploadResult?.isSuccess == true)

assert(File(filePath).length() == getRemoteSize(remotePath))
}

private fun genLargeUpload(onWifiConnection: Boolean): ChunkedFileUploadRemoteOperation {
// create file
val filePath = createFile("chunkedFile.txt", BIG_FILE_ITERATION)
val remotePath = "/bigFile.md"

return ChunkedFileUploadRemoteOperation(
filePath,
remotePath,
"text/markdown",
"",
RANDOM_MTIME,
System.currentTimeMillis() / MILLI_TO_SECOND,
onWifiConnection,
true
)
}

private fun getRemoteSize(remotePath: String): Long {
val davPath = client.filesDavUri.toString() + "/" + WebdavUtils.encodePath(remotePath)
val propFindMethod = PropFindMethod(davPath, WebdavUtils.getFilePropSet(), 0)
client.executeMethod(propFindMethod)
assert(propFindMethod.succeeded())

return WebdavEntry(propFindMethod.responseBodyAsMultiStatus.responses[0], remotePath).contentLength
}

companion object {
val BIG_FILE_ITERATION = 500000
val MAX_TRIES = 30
const val BIG_FILE_ITERATION = 1000000
const val MAX_TRIES = 30
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ public static DavPropertyNameSet getChunksPropSet() {
DavPropertyNameSet propSet = new DavPropertyNameSet();
propSet.add(DavPropertyName.GETCONTENTTYPE);
propSet.add(DavPropertyName.RESOURCETYPE);
propSet.add(DavPropertyName.GETCONTENTLENGTH);

return propSet;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* Nextcloud Android Library is available under MIT license
*
* @author ZetaTom
* Copyright (C) 2023 ZetaTom
* 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

data class Chunk(val id: Int, val start: Long, val length: Long)
Loading

0 comments on commit 593f49c

Please sign in to comment.