From bb259d7fb688baae1658fc5d501bb4bd0543aecb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 08:59:49 +0200 Subject: [PATCH 01/12] Rename .java to .kt Signed-off-by: alperozturk96 --- ...adRemoteOperation.java => ChunkedFileUploadRemoteOperation.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename library/src/main/java/com/owncloud/android/lib/resources/files/{ChunkedFileUploadRemoteOperation.java => ChunkedFileUploadRemoteOperation.kt} (100%) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt similarity index 100% rename from library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java rename to library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt From 4732bdacc20eb5c020934e219ec5f52ca295b726 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 08:59:50 +0200 Subject: [PATCH 02/12] convert to kotlin Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 538 ++++++++---------- .../ChunkedFileUploadRemoteOperationTest.kt | 20 +- 2 files changed, 263 insertions(+), 295 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 818c4e0e4a..f62ddff81a 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -4,337 +4,305 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-License-Identifier: MIT */ -package com.owncloud.android.lib.resources.files; - -import android.text.TextUtils; - -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.network.ChunkFromFileChannelRequestEntity; -import com.owncloud.android.lib.common.network.ProgressiveDataTransfer; -import com.owncloud.android.lib.common.network.WebdavEntry; -import com.owncloud.android.lib.common.network.WebdavUtils; -import com.owncloud.android.lib.common.operations.OperationCancelledException; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; - -import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; -import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.params.HttpMethodParams; -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.MultiStatus; -import org.apache.jackrabbit.webdav.MultiStatusResponse; -import org.apache.jackrabbit.webdav.client.methods.MkColMethod; -import org.apache.jackrabbit.webdav.client.methods.MoveMethod; -import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.util.Locale; -import java.util.Objects; - -import androidx.annotation.VisibleForTesting; - - -public class ChunkedFileUploadRemoteOperation extends UploadFileRemoteOperation { - - public static final long CHUNK_SIZE_MOBILE = 10240000; - public static final long CHUNK_SIZE_WIFI = 40960000; - public static final String DESTINATION_HEADER = "Destination"; - public static final int CHUNK_NAME_LENGTH = 6; - private static final String TAG = ChunkedFileUploadRemoteOperation.class.getSimpleName(); - public final int ASSEMBLE_TIME_MIN = 30 * 1000; // 30s - public final int ASSEMBLE_TIME_MAX = 30 * 60 * 1000; // 30min - public final int ASSEMBLE_TIME_PER_GB = 3 * 60 * 1000; // 3 min - private final boolean onWifiConnection; - private String uploadFolderUri; - private String destinationUri; - - public ChunkedFileUploadRemoteOperation(String storagePath, - String remotePath, - String mimeType, - String requiredEtag, - long lastModificationTimestamp, - boolean onWifiConnection) { - this(storagePath, remotePath, mimeType, requiredEtag, lastModificationTimestamp, onWifiConnection, null); - } - - public ChunkedFileUploadRemoteOperation(String storagePath, - String remotePath, - String mimeType, - String requiredEtag, - long lastModificationTimestamp, - Long creationTimestamp, - boolean onWifiConnection, - boolean disableRetries) { - this(storagePath, - remotePath, - mimeType, - requiredEtag, - lastModificationTimestamp, - onWifiConnection, - null, - creationTimestamp, - disableRetries); - } - - public ChunkedFileUploadRemoteOperation(String storagePath, - String remotePath, - String mimeType, - String requiredEtag, - long lastModificationTimestamp, - boolean onWifiConnection, - String token) { - this(storagePath, - remotePath, - mimeType, - requiredEtag, - lastModificationTimestamp, - onWifiConnection, - token, - null, - true); - } - - public ChunkedFileUploadRemoteOperation(String storagePath, - String remotePath, - String mimeType, - String requiredEtag, - long lastModificationTimestamp, - boolean onWifiConnection, - String token, - Long creationTimestamp, - boolean disableRetries) { - super(storagePath, - remotePath, - mimeType, - requiredEtag, - lastModificationTimestamp, - creationTimestamp, - token, - disableRetries); - this.onWifiConnection = onWifiConnection; - } - - protected static Chunk calcNextChunk(long fileSize, int chunkId, long startByte, long chunkSize) { - if (chunkId < 0 || String.valueOf(chunkId).length() > CHUNK_NAME_LENGTH) { - throw new IllegalArgumentException( - "chunkId must not exceed length specified in CHUNK_NAME_LENGTH (" + CHUNK_NAME_LENGTH + ")"); - } - - long length = startByte + chunkSize > fileSize ? fileSize - startByte : chunkSize; - return new Chunk(chunkId, startByte, length); - } - - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result; - DefaultHttpMethodRetryHandler oldRetryHandler = (DefaultHttpMethodRetryHandler) client.getParams() - .getParameter(HttpMethodParams.RETRY_HANDLER); - File file = new File(localPath); - MoveMethod moveMethod = null; - try { +package com.owncloud.android.lib.resources.files + +import androidx.annotation.VisibleForTesting +import androidx.core.text.isDigitsOnly +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.network.ChunkFromFileChannelRequestEntity +import com.owncloud.android.lib.common.network.ProgressiveDataTransfer +import com.owncloud.android.lib.common.network.WebdavEntry +import com.owncloud.android.lib.common.network.WebdavUtils +import com.owncloud.android.lib.common.operations.OperationCancelledException +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler +import org.apache.commons.httpclient.methods.PutMethod +import org.apache.commons.httpclient.params.HttpMethodParams +import org.apache.jackrabbit.webdav.DavConstants +import org.apache.jackrabbit.webdav.MultiStatus +import org.apache.jackrabbit.webdav.client.methods.MkColMethod +import org.apache.jackrabbit.webdav.client.methods.MoveMethod +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile +import java.nio.channels.FileChannel +import java.util.Locale +import kotlin.math.max +import kotlin.math.min + +@Suppress("LongParameterList") +class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( + storagePath: String?, + remotePath: String?, + mimeType: String?, + requiredEtag: String?, + lastModificationTimestamp: Long, + private val onWifiConnection: Boolean, + token: String? = null, + creationTimestamp: Long? = null, + disableRetries: Boolean = true +) : UploadFileRemoteOperation( + storagePath, + remotePath, + mimeType, + requiredEtag, + lastModificationTimestamp, + creationTimestamp, + token, + disableRetries +) { + @Suppress("VariableNaming", "MagicNumber") + @JvmField + val ASSEMBLE_TIME_MIN: Int = 30 * 1000 // 30s + + @Suppress("VariableNaming", "MagicNumber") + @JvmField + val ASSEMBLE_TIME_MAX: Int = 30 * 60 * 1000 // 30min + + @Suppress("VariableNaming", "MagicNumber") + @JvmField + val ASSEMBLE_TIME_PER_GB: Int = 3 * 60 * 1000 // 3 min + + private lateinit var uploadFolderUri: String + private lateinit var destinationUri: String + private var moveMethod: MoveMethod? = null + + constructor( + storagePath: String?, + remotePath: String?, + mimeType: String?, + requiredEtag: String?, + lastModificationTimestamp: Long, + creationTimestamp: Long?, + onWifiConnection: Boolean, + disableRetries: Boolean + ) : this( + storagePath, + remotePath, + mimeType, + requiredEtag, + lastModificationTimestamp, + onWifiConnection, + null, + creationTimestamp, + disableRetries + ) + + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + val oldRetryHandler = client.params + .getParameter(HttpMethodParams.RETRY_HANDLER) as? DefaultHttpMethodRetryHandler + + return try { if (disableRetries) { // prevent that uploads are retried automatically by network library - client.getParams() - .setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(0, false)); + client.params.setParameter(HttpMethodParams.RETRY_HANDLER, DefaultHttpMethodRetryHandler(0, false)) } - // chunk length - long chunkSize; - if (onWifiConnection) { - chunkSize = CHUNK_SIZE_WIFI; - } else { - chunkSize = CHUNK_SIZE_MOBILE; + uploadAndAssemble(client) + } catch (e: Exception) { + cancelledOrFailed(e) + } finally { + if (disableRetries) { + // reset previous retry handler + client.params.setParameter(HttpMethodParams.RETRY_HANDLER, oldRetryHandler) } + } + } - uploadFolderUri = client.getUploadUri() + "/" + client.getUserId() + "/" + FileUtils.md5Sum(file); + private fun uploadAndAssemble(client: OwnCloudClient): RemoteOperationResult { + val file = File(localPath) + val userId = client.userId - destinationUri = client.getDavUri() + "/files/" + client.getUserId() + WebdavUtils.encodePath(remotePath); + uploadFolderUri = "${client.uploadUri}/$userId/${FileUtils.md5Sum(file)}" + destinationUri = "${client.davUri}/files/$userId${WebdavUtils.encodePath(remotePath)}" - // create folder - MkColMethod createFolder = new MkColMethod(uploadFolderUri); + createUploadFolder(client) - createFolder.addRequestHeader(DESTINATION_HEADER, destinationUri); + val listChunks = PropFindMethod(uploadFolderUri, WebdavUtils.getChunksPropSet(), DavConstants.DEPTH_1) + client.executeMethod(listChunks) - client.executeMethod(createFolder, 30000, 5000); + if (!listChunks.succeeded()) { + return RemoteOperationResult(false, listChunks) + } - // list chunks - PropFindMethod listChunks = new PropFindMethod(uploadFolderUri, - WebdavUtils.getChunksPropSet(), - DavConstants.DEPTH_1); + val uploaded = uploadedChunks(client, listChunks.responseBodyAsMultiStatus) - client.executeMethod(listChunks); + return uploadRemainingChunks(client, file, uploaded) ?: assemble(client, file) + } - if (!listChunks.succeeded()) { - return new RemoteOperationResult(listChunks.succeeded(), listChunks); - } + private fun createUploadFolder(client: OwnCloudClient) { + val createFolder = MkColMethod(uploadFolderUri) + createFolder.addRequestHeader(DESTINATION_HEADER, destinationUri) + client.executeMethod(createFolder, CREATE_FOLDER_READ_TIMEOUT, CREATE_FOLDER_CONNECTION_TIMEOUT) + } - MultiStatus dataInServer = listChunks.getResponseBodyAsMultiStatus(); - - // determine chunks already on server - // chunks are assumed to be uploaded linearly, starting at 0B - long nextByte = 0; - int lastId = 0; - for (MultiStatusResponse response : dataInServer.getResponses()) { - WebdavEntry we = new WebdavEntry(response, Objects.requireNonNull(client.getUploadUri().getPath())); - String name = we.getName(); - - // filter out any objects not matching expected chunk name - if (!we.isDirectory() && name != null && (name.length() <= CHUNK_NAME_LENGTH) && - TextUtils.isDigitsOnly(name)) { - // is part of upload - int id = Integer.parseInt(name); - if (id > lastId) { - lastId = id; - } - nextByte += we.getContentLength(); - } - } + private fun uploadedChunks(client: OwnCloudClient, dataInServer: MultiStatus): UploadedChunks { + val uploadPath = requireNotNull(client.uploadUri.path) + var nextByte = 0L + var lastId = 0 - // iteratively upload remaining chunks - while (nextByte + 1 < file.length()) { - // determine size of next chunk - Chunk chunk = calcNextChunk(file.length(), ++lastId, nextByte, chunkSize); + for (response in dataInServer.responses) { + val entry = WebdavEntry(response, uploadPath) + val chunkId = entry.chunkId() ?: continue - RemoteOperationResult chunkResult = uploadChunk(client, chunk); - if (!chunkResult.isSuccess()) { - return chunkResult; - } + lastId = max(lastId, chunkId) + nextByte += entry.contentLength + } - if (cancellationRequested.get()) { - return new RemoteOperationResult(new OperationCancelledException()); - } + return UploadedChunks(nextByte, lastId) + } - nextByte += chunk.getLength(); + /** Id of the chunk this entry holds, or `null` for any object not matching the expected chunk name. */ + private fun WebdavEntry.chunkId(): Int? = name + ?.takeIf { !isDirectory && it.length <= CHUNK_NAME_LENGTH && it.isDigitsOnly() } + ?.toIntOrNull() + + private fun uploadRemainingChunks( + client: OwnCloudClient, + file: File, + uploaded: UploadedChunks + ): RemoteOperationResult? { + val chunkSize = if (onWifiConnection) CHUNK_SIZE_WIFI else CHUNK_SIZE_MOBILE + var nextByte = uploaded.nextByte + var lastId = uploaded.lastId + var failure: RemoteOperationResult? = null + + while (failure == null && nextByte + 1 < file.length()) { + // determine size of next chunk + val chunk = calcNextChunk(file.length(), ++lastId, nextByte, chunkSize) + val chunkResult = uploadChunk(client, chunk) + + failure = when { + !chunkResult.isSuccess -> chunkResult + cancellationRequested.get() -> RemoteOperationResult(OperationCancelledException()) + else -> null } - // assemble - String originUri = uploadFolderUri + "/.file"; + nextByte += chunk.length + } - moveMethod = new MoveMethod(originUri, destinationUri, true); - moveMethod.addRequestHeader(OC_X_OC_MTIME_HEADER, String.valueOf(lastModificationTimestamp)); + return failure + } - if (creationTimestamp != null && creationTimestamp > 0) { - moveMethod.addRequestHeader(OC_X_OC_CTIME_HEADER, String.valueOf(creationTimestamp)); - } + private fun assemble(client: OwnCloudClient, file: File): RemoteOperationResult { + val move = MoveMethod(uploadFolderUri + ASSEMBLED_FILE_SUFFIX, destinationUri, true) + moveMethod = move - if (token != null) { - moveMethod.addRequestHeader(E2E_TOKEN, token); - } + move.addRequestHeader(OC_X_OC_MTIME_HEADER, lastModificationTimestamp.toString()) + creationTimestamp?.takeIf { it > 0 }?.let { move.addRequestHeader(OC_X_OC_CTIME_HEADER, it.toString()) } + token?.let { move.addRequestHeader(E2E_TOKEN, it) } - final int DO_NOT_CHANGE_DEFAULT = -1; - int moveResult = client.executeMethod(moveMethod, calculateAssembleTimeout(file), DO_NOT_CHANGE_DEFAULT); - - result = new RemoteOperationResult(isSuccess(moveResult), moveMethod); - } catch (Exception e) { - if (putMethod != null && putMethod.isAborted()) { - if (cancellationRequested.get() && cancellationReason != null) { - result = new RemoteOperationResult(cancellationReason); - } else { - result = new RemoteOperationResult(new OperationCancelledException()); - } - } else if (moveMethod != null && moveMethod.isAborted()) { - if (cancellationRequested.get() && cancellationReason != null) { - result = new RemoteOperationResult(cancellationReason); - } else { - result = new RemoteOperationResult(new OperationCancelledException()); - } - } else { - result = new RemoteOperationResult(e); - } - } finally { - if (disableRetries) { - // reset previous retry handler - client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, oldRetryHandler); - } - } - return result; - } + val status = client.executeMethod(move, calculateAssembleTimeout(file), DO_NOT_CHANGE_DEFAULT) - private RemoteOperationResult uploadChunk(OwnCloudClient client, Chunk chunk) throws IOException { - int status; - RemoteOperationResult result; + return RemoteOperationResult(isSuccess(status), move) + } - FileChannel channel = null; - RandomAccessFile raf = null; + @Throws(IOException::class) + private fun uploadChunk(client: OwnCloudClient, chunk: Chunk): RemoteOperationResult { + val file = File(localPath) + var raf: RandomAccessFile? = null + var channel: FileChannel? = null - File file = new File(localPath); + return try { + raf = RandomAccessFile(file, "r") + channel = raf.channel + entity = ChunkFromFileChannelRequestEntity(channel, mimeType, chunk.start, chunk.length, file) - try { - raf = new RandomAccessFile(file, "r"); - channel = raf.getChannel(); - entity = new ChunkFromFileChannelRequestEntity(channel, - mimeType, - chunk.getStart(), - chunk.getLength(), - file); - - synchronized (dataTransferListeners) { - ((ProgressiveDataTransfer) entity).addDataTransferProgressListeners(dataTransferListeners); + synchronized(dataTransferListeners) { + (entity as ProgressiveDataTransfer).addDataTransferProgressListeners(dataTransferListeners) } // pad chunk name to 6 digits - String chunkUri = - uploadFolderUri + "/" + String.format(Locale.ROOT, "%0" + CHUNK_NAME_LENGTH + "d", chunk.getId()); - - if (putMethod != null) { - putMethod.releaseConnection(); // let the connection available for other methods - } - - putMethod = createPutMethod(chunkUri); + val chunkName = String.format(Locale.ROOT, "%0${CHUNK_NAME_LENGTH}d", chunk.id) - putMethod.addRequestHeader(DESTINATION_HEADER, destinationUri); + putMethod?.releaseConnection() // let the connection available for other methods - if (token != null) { - putMethod.addRequestHeader(E2E_TOKEN, token); - } + val put = createPutMethod("$uploadFolderUri/$chunkName") + put.addRequestHeader(DESTINATION_HEADER, destinationUri) + token?.let { put.addRequestHeader(E2E_TOKEN, it) } - status = client.executeMethod(putMethod); + val status = client.executeMethod(put) + val result = RemoteOperationResult(isSuccess(status), put) - result = new RemoteOperationResult(isSuccess(status), putMethod); + client.exhaustResponse(put.responseBodyAsStream) + Log_OC.d( + TAG, + "Upload of $localPath to $remotePath, chunk id: ${chunk.id} from ${chunk.start} " + + "size: ${chunk.length}, HTTP result status $status" + ) - client.exhaustResponse(putMethod.getResponseBodyAsStream()); - Log_OC.d(TAG, - "Upload of " + localPath + " to " + remotePath + ", chunk id: " + chunk.getId() + " from " + - chunk.getStart() + " size: " + chunk.getLength() + ", HTTP result status " + status); + result } finally { - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - Log_OC.e(TAG, "Error closing file channel!", e); - } - } - if (raf != null) { - try { - raf.close(); - } catch (IOException e) { - Log_OC.e(TAG, "Error closing file access!", e); - } - } - if (putMethod != null) { - putMethod.releaseConnection(); // let the connection available for other methods - } + channel.closeQuietly("Error closing file channel!") + raf.closeQuietly("Error closing file access!") + putMethod?.releaseConnection() } - return result; } - private PutMethod createPutMethod(String uriPrefix) { - putMethod = new PutMethod(uriPrefix); - putMethod.setRequestEntity(entity); + private fun createPutMethod(uri: String): PutMethod { + val put = PutMethod(uri) + putMethod = put + put.requestEntity = entity + if (cancellationRequested.get()) { - putMethod.abort(); // next method will throw an exception + put.abort() // next method will throw an exception } - return putMethod; + return put + } + + private fun cancelledOrFailed(e: Exception): RemoteOperationResult = when { + putMethod?.isAborted == true || moveMethod?.isAborted == true -> + cancellationReason + ?.takeIf { cancellationRequested.get() } + ?.let { RemoteOperationResult(it) } + ?: RemoteOperationResult(OperationCancelledException()) + + else -> RemoteOperationResult(e) + } + + private fun Closeable?.closeQuietly(errorMessage: String) { + try { + this?.close() + } catch (e: IOException) { + Log_OC.e(TAG, errorMessage, e) + } } @VisibleForTesting - public int calculateAssembleTimeout(File file) { - final double fileSizeInGb = file.length() / 1e9; + fun calculateAssembleTimeout(file: File): Int { + val fileSizeInGb = file.length() / BYTES_PER_GB + + return max(ASSEMBLE_TIME_MIN, min((ASSEMBLE_TIME_PER_GB * fileSizeInGb).toInt(), ASSEMBLE_TIME_MAX)) + } - return Math.max(ASSEMBLE_TIME_MIN, Math.min((int) (ASSEMBLE_TIME_PER_GB * fileSizeInGb), ASSEMBLE_TIME_MAX)); + private data class UploadedChunks(val nextByte: Long, val lastId: Int) + + companion object { + const val CHUNK_SIZE_MOBILE: Long = 10240000 + const val CHUNK_SIZE_WIFI: Long = 40960000 + const val DESTINATION_HEADER: String = "Destination" + const val CHUNK_NAME_LENGTH: Int = 6 + + private const val ASSEMBLED_FILE_SUFFIX = "/.file" + private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 + private const val CREATE_FOLDER_CONNECTION_TIMEOUT = 5_000 + private const val DO_NOT_CHANGE_DEFAULT = -1 + private const val BYTES_PER_GB = 1e9 + private val TAG = ChunkedFileUploadRemoteOperation::class.java.simpleName + + internal fun calcNextChunk(fileSize: Long, chunkId: Int, startByte: Long, chunkSize: Long): Chunk { + require(chunkId >= 0 && chunkId.toString().length <= CHUNK_NAME_LENGTH) { + "chunkId must not exceed length specified in CHUNK_NAME_LENGTH ($CHUNK_NAME_LENGTH)" + } + + val length = if (startByte + chunkSize > fileSize) fileSize - startByte else chunkSize + + return Chunk(chunkId, startByte, length) + } } } diff --git a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt index 7c2dc66ef0..550bc69872 100644 --- a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt +++ b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt @@ -19,7 +19,7 @@ import kotlin.math.ceil class ChunkedFileUploadRemoteOperationTest { @Mock - var file: File? = null + lateinit var file: File @Test fun testAssembleTimeout() { @@ -35,39 +35,39 @@ class ChunkedFileUploadRemoteOperationTest { ) // 0b - Mockito.`when`(file!!.length()).thenReturn(0L) + Mockito.`when`(file.length()).thenReturn(0L) assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) // 100b - Mockito.`when`(file!!.length()).thenReturn(100L) + Mockito.`when`(file.length()).thenReturn(100L) assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) // 1Mb - Mockito.`when`(file!!.length()).thenReturn(1 * MB) + Mockito.`when`(file.length()).thenReturn(1 * MB) assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) // 100Mb - Mockito.`when`(file!!.length()).thenReturn(100 * MB) + Mockito.`when`(file.length()).thenReturn(100 * MB) assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) // 1Gb - Mockito.`when`(file!!.length()).thenReturn(1 * GB) + Mockito.`when`(file.length()).thenReturn(1 * GB) assertEquals(sut.ASSEMBLE_TIME_PER_GB, sut.calculateAssembleTimeout(file)) // 2Gb - Mockito.`when`(file!!.length()).thenReturn(2 * GB) + Mockito.`when`(file.length()).thenReturn(2 * GB) assertEquals((2 * sut.ASSEMBLE_TIME_PER_GB), sut.calculateAssembleTimeout(file)) // 5Gb - Mockito.`when`(file!!.length()).thenReturn(5 * GB) + Mockito.`when`(file.length()).thenReturn(5 * GB) assertEquals((5 * sut.ASSEMBLE_TIME_PER_GB), sut.calculateAssembleTimeout(file)) // 50Gb - Mockito.`when`(file!!.length()).thenReturn(50 * GB) + Mockito.`when`(file.length()).thenReturn(50 * GB) assertEquals(sut.ASSEMBLE_TIME_MAX, sut.calculateAssembleTimeout(file)) // 500Gb - Mockito.`when`(file!!.length()).thenReturn(500 * GB) + Mockito.`when`(file.length()).thenReturn(500 * GB) assertEquals(sut.ASSEMBLE_TIME_MAX, sut.calculateAssembleTimeout(file)) } From d4ab33fb60d478c0948f42ef9cf192ef7f4c81a8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 09:00:55 +0200 Subject: [PATCH 03/12] simplify Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 12 +++++------- .../ChunkedFileUploadRemoteOperationTest.kt | 18 +++++++++--------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index f62ddff81a..52e7219a2c 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-License-Identifier: MIT */ @@ -54,17 +55,14 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( token, disableRetries ) { - @Suppress("VariableNaming", "MagicNumber") @JvmField - val ASSEMBLE_TIME_MIN: Int = 30 * 1000 // 30s + val assembleTimeMin: Int = 30 * 1000 // 30s - @Suppress("VariableNaming", "MagicNumber") @JvmField - val ASSEMBLE_TIME_MAX: Int = 30 * 60 * 1000 // 30min + val assembleTimeMax: Int = 30 * 60 * 1000 // 30min - @Suppress("VariableNaming", "MagicNumber") @JvmField - val ASSEMBLE_TIME_PER_GB: Int = 3 * 60 * 1000 // 3 min + val assembleTimePerGB: Int = 3 * 60 * 1000 // 3 min private lateinit var uploadFolderUri: String private lateinit var destinationUri: String @@ -277,7 +275,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( fun calculateAssembleTimeout(file: File): Int { val fileSizeInGb = file.length() / BYTES_PER_GB - return max(ASSEMBLE_TIME_MIN, min((ASSEMBLE_TIME_PER_GB * fileSizeInGb).toInt(), ASSEMBLE_TIME_MAX)) + return max(assembleTimeMin, min((assembleTimePerGB * fileSizeInGb).toInt(), assembleTimeMax)) } private data class UploadedChunks(val nextByte: Long, val lastId: Int) diff --git a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt index 550bc69872..6fb4780827 100644 --- a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt +++ b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt @@ -36,39 +36,39 @@ class ChunkedFileUploadRemoteOperationTest { // 0b Mockito.`when`(file.length()).thenReturn(0L) - assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMin, sut.calculateAssembleTimeout(file)) // 100b Mockito.`when`(file.length()).thenReturn(100L) - assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMin, sut.calculateAssembleTimeout(file)) // 1Mb Mockito.`when`(file.length()).thenReturn(1 * MB) - assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMin, sut.calculateAssembleTimeout(file)) // 100Mb Mockito.`when`(file.length()).thenReturn(100 * MB) - assertEquals(sut.ASSEMBLE_TIME_MIN, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMin, sut.calculateAssembleTimeout(file)) // 1Gb Mockito.`when`(file.length()).thenReturn(1 * GB) - assertEquals(sut.ASSEMBLE_TIME_PER_GB, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimePerGB, sut.calculateAssembleTimeout(file)) // 2Gb Mockito.`when`(file.length()).thenReturn(2 * GB) - assertEquals((2 * sut.ASSEMBLE_TIME_PER_GB), sut.calculateAssembleTimeout(file)) + assertEquals((2 * sut.assembleTimePerGB), sut.calculateAssembleTimeout(file)) // 5Gb Mockito.`when`(file.length()).thenReturn(5 * GB) - assertEquals((5 * sut.ASSEMBLE_TIME_PER_GB), sut.calculateAssembleTimeout(file)) + assertEquals((5 * sut.assembleTimePerGB), sut.calculateAssembleTimeout(file)) // 50Gb Mockito.`when`(file.length()).thenReturn(50 * GB) - assertEquals(sut.ASSEMBLE_TIME_MAX, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMax, sut.calculateAssembleTimeout(file)) // 500Gb Mockito.`when`(file.length()).thenReturn(500 * GB) - assertEquals(sut.ASSEMBLE_TIME_MAX, sut.calculateAssembleTimeout(file)) + assertEquals(sut.assembleTimeMax, sut.calculateAssembleTimeout(file)) } @Test From 5f07ec2595e5b2d658249c37976e267fa9ae2f7e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 09:19:13 +0200 Subject: [PATCH 04/12] add chunked max upload size Signed-off-by: alperozturk96 --- .../status/GetCapabilitiesRemoteOperation.java | 12 ++++++++++++ .../android/lib/resources/status/OCCapability.kt | 2 ++ 2 files changed, 14 insertions(+) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java index d1a96fce37..b6ecb690c6 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/status/GetCapabilitiesRemoteOperation.java @@ -94,6 +94,8 @@ public class GetCapabilitiesRemoteOperation extends RemoteOperation Date: Thu, 20 Aug 2026 10:10:19 +0200 Subject: [PATCH 05/12] inject serverMaxChunkSize Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 52e7219a2c..c3ec011dba 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -44,7 +44,8 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( private val onWifiConnection: Boolean, token: String? = null, creationTimestamp: Long? = null, - disableRetries: Boolean = true + disableRetries: Boolean = true, + private val serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN ) : UploadFileRemoteOperation( storagePath, remotePath, @@ -68,6 +69,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( private lateinit var destinationUri: String private var moveMethod: MoveMethod? = null + @JvmOverloads constructor( storagePath: String?, remotePath: String?, @@ -76,7 +78,8 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( lastModificationTimestamp: Long, creationTimestamp: Long?, onWifiConnection: Boolean, - disableRetries: Boolean + disableRetries: Boolean, + serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN ) : this( storagePath, remotePath, @@ -86,7 +89,8 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( onWifiConnection, null, creationTimestamp, - disableRetries + disableRetries, + serverMaxChunkSize ) @Suppress("TooGenericExceptionCaught") @@ -164,7 +168,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( file: File, uploaded: UploadedChunks ): RemoteOperationResult? { - val chunkSize = if (onWifiConnection) CHUNK_SIZE_WIFI else CHUNK_SIZE_MOBILE + val chunkSize = chunkSize(onWifiConnection, serverMaxChunkSize) var nextByte = uploaded.nextByte var lastId = uploaded.lastId var failure: RemoteOperationResult? = null @@ -283,9 +287,23 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( companion object { const val CHUNK_SIZE_MOBILE: Long = 10240000 const val CHUNK_SIZE_WIFI: Long = 40960000 + const val SERVER_MAX_CHUNK_SIZE_UNKNOWN: Long = -1 const val DESTINATION_HEADER: String = "Destination" const val CHUNK_NAME_LENGTH: Int = 6 + @JvmStatic + fun chunkSize(onWifiConnection: Boolean, serverMaxChunkSize: Long): Long { + if (serverMaxChunkSize <= 0) { + return if (onWifiConnection) CHUNK_SIZE_WIFI else CHUNK_SIZE_MOBILE + } + + return serverMaxChunkSize + } + + @JvmStatic + fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = + chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) + private const val ASSEMBLED_FILE_SUFFIX = "/.file" private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 private const val CREATE_FOLDER_CONNECTION_TIMEOUT = 5_000 From 4f7463acab8984a3d1a6932fb2ec3473d6458ddc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 10:30:14 +0200 Subject: [PATCH 06/12] better naming Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 6 +++--- .../ChunkedFileUploadRemoteOperationTest.kt | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index c3ec011dba..95dd3eb3bb 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -285,8 +285,8 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( private data class UploadedChunks(val nextByte: Long, val lastId: Int) companion object { - const val CHUNK_SIZE_MOBILE: Long = 10240000 - const val CHUNK_SIZE_WIFI: Long = 40960000 + const val MIN_CHUNK_SIZE: Long = 10240000 + const val DEFAULT_CHUNK_SIZE: Long = 40960000 const val SERVER_MAX_CHUNK_SIZE_UNKNOWN: Long = -1 const val DESTINATION_HEADER: String = "Destination" const val CHUNK_NAME_LENGTH: Int = 6 @@ -294,7 +294,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( @JvmStatic fun chunkSize(onWifiConnection: Boolean, serverMaxChunkSize: Long): Long { if (serverMaxChunkSize <= 0) { - return if (onWifiConnection) CHUNK_SIZE_WIFI else CHUNK_SIZE_MOBILE + return if (onWifiConnection) DEFAULT_CHUNK_SIZE else MIN_CHUNK_SIZE } return serverMaxChunkSize diff --git a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt index 6fb4780827..4ce08c368b 100644 --- a/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt +++ b/library/src/test/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperationTest.kt @@ -92,8 +92,8 @@ class ChunkedFileUploadRemoteOperationTest { @Test fun testChunking() { listOf(1 * MB, 10 * MB, 100 * MB, 1 * GB).forEach { length -> - checkChunks(length, ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE, 0) - checkChunks(length, ChunkedFileUploadRemoteOperation.CHUNK_SIZE_WIFI, 0) + checkChunks(length, ChunkedFileUploadRemoteOperation.MIN_CHUNK_SIZE, 0) + checkChunks(length, ChunkedFileUploadRemoteOperation.DEFAULT_CHUNK_SIZE, 0) } } @@ -102,8 +102,8 @@ class ChunkedFileUploadRemoteOperationTest { // test chunking with offset (chunks already on server) // -2: last byte missing (because the file starts at 0B, file.length() at 1; 1B offset) listOf(1, 1 * MB, 10 * MB, 100 * MB, 256 * MB, 1 * GB - 2).forEach { offset -> - checkChunks(1 * GB, ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE, offset) - checkChunks(1 * GB, ChunkedFileUploadRemoteOperation.CHUNK_SIZE_WIFI, offset) + checkChunks(1 * GB, ChunkedFileUploadRemoteOperation.MIN_CHUNK_SIZE, offset) + checkChunks(1 * GB, ChunkedFileUploadRemoteOperation.DEFAULT_CHUNK_SIZE, offset) } } @@ -121,7 +121,7 @@ class ChunkedFileUploadRemoteOperationTest { length, ++id, nextByte, - ChunkedFileUploadRemoteOperation.CHUNK_SIZE_WIFI + ChunkedFileUploadRemoteOperation.DEFAULT_CHUNK_SIZE ) chunks.add(chunk) @@ -135,7 +135,7 @@ class ChunkedFileUploadRemoteOperationTest { length, ++id, nextByte, - ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE + ChunkedFileUploadRemoteOperation.MIN_CHUNK_SIZE ) chunks.add(chunk) @@ -144,11 +144,11 @@ class ChunkedFileUploadRemoteOperationTest { // calculate expected number of chunks var expectedChunkCount = - ceil((length / 2) / ChunkedFileUploadRemoteOperation.CHUNK_SIZE_WIFI.toFloat()) + ceil((length / 2) / ChunkedFileUploadRemoteOperation.DEFAULT_CHUNK_SIZE.toFloat()) expectedChunkCount += ceil( - (length - expectedChunkCount * ChunkedFileUploadRemoteOperation.CHUNK_SIZE_WIFI) / - ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE.toFloat() + (length - expectedChunkCount * ChunkedFileUploadRemoteOperation.DEFAULT_CHUNK_SIZE) / + ChunkedFileUploadRemoteOperation.MIN_CHUNK_SIZE.toFloat() ) assertEquals(expectedChunkCount.toInt(), chunks.size) From a04c6d3ed1c1d62d7be7170000c256d958b716ef Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 10:38:53 +0200 Subject: [PATCH 07/12] implement chunk timeout Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 95dd3eb3bb..4a24e26a4d 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -139,7 +139,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( private fun createUploadFolder(client: OwnCloudClient) { val createFolder = MkColMethod(uploadFolderUri) createFolder.addRequestHeader(DESTINATION_HEADER, destinationUri) - client.executeMethod(createFolder, CREATE_FOLDER_READ_TIMEOUT, CREATE_FOLDER_CONNECTION_TIMEOUT) + client.executeMethod(createFolder, CREATE_FOLDER_READ_TIMEOUT, DO_NOT_CHANGE_DEFAULT) } private fun uploadedChunks(client: OwnCloudClient, dataInServer: MultiStatus): UploadedChunks { @@ -227,7 +227,7 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( put.addRequestHeader(DESTINATION_HEADER, destinationUri) token?.let { put.addRequestHeader(E2E_TOKEN, it) } - val status = client.executeMethod(put) + val status = client.executeMethod(put, calculateChunkTimeout(chunk.length), DO_NOT_CHANGE_DEFAULT) val result = RemoteOperationResult(isSuccess(status), put) client.exhaustResponse(put.responseBodyAsStream) @@ -282,6 +282,13 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( return max(assembleTimeMin, min((assembleTimePerGB * fileSizeInGb).toInt(), assembleTimeMax)) } + @VisibleForTesting + fun calculateChunkTimeout(chunkLength: Long): Int { + val chunkSizeInMib = chunkLength / BYTES_PER_MIB + + return max(CHUNK_TIMEOUT_MIN, min((CHUNK_TIMEOUT_PER_MIB * chunkSizeInMib).toInt(), CHUNK_TIMEOUT_MAX)) + } + private data class UploadedChunks(val nextByte: Long, val lastId: Int) companion object { @@ -306,8 +313,11 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( private const val ASSEMBLED_FILE_SUFFIX = "/.file" private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 - private const val CREATE_FOLDER_CONNECTION_TIMEOUT = 5_000 private const val DO_NOT_CHANGE_DEFAULT = -1 + private const val CHUNK_TIMEOUT_MIN = 60_000 + private const val CHUNK_TIMEOUT_MAX = 10 * 60 * 1000 + private const val CHUNK_TIMEOUT_PER_MIB = 1_000 + private const val BYTES_PER_MIB = 1024.0 * 1024.0 private const val BYTES_PER_GB = 1e9 private val TAG = ChunkedFileUploadRemoteOperation::class.java.simpleName From f13bc84822dda6d075212a630995cf7750ab32f5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 10:49:05 +0200 Subject: [PATCH 08/12] fix detekt Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 529 ++++++++++-------- 1 file changed, 281 insertions(+), 248 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 4a24e26a4d..25cad207f8 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -35,40 +35,7 @@ import kotlin.math.max import kotlin.math.min @Suppress("LongParameterList") -class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( - storagePath: String?, - remotePath: String?, - mimeType: String?, - requiredEtag: String?, - lastModificationTimestamp: Long, - private val onWifiConnection: Boolean, - token: String? = null, - creationTimestamp: Long? = null, - disableRetries: Boolean = true, - private val serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN -) : UploadFileRemoteOperation( - storagePath, - remotePath, - mimeType, - requiredEtag, - lastModificationTimestamp, - creationTimestamp, - token, - disableRetries -) { - @JvmField - val assembleTimeMin: Int = 30 * 1000 // 30s - - @JvmField - val assembleTimeMax: Int = 30 * 60 * 1000 // 30min - - @JvmField - val assembleTimePerGB: Int = 3 * 60 * 1000 // 3 min - - private lateinit var uploadFolderUri: String - private lateinit var destinationUri: String - private var moveMethod: MoveMethod? = null - +class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( storagePath: String?, @@ -76,259 +43,325 @@ class ChunkedFileUploadRemoteOperation @JvmOverloads constructor( mimeType: String?, requiredEtag: String?, lastModificationTimestamp: Long, - creationTimestamp: Long?, - onWifiConnection: Boolean, - disableRetries: Boolean, - serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN - ) : this( - storagePath, - remotePath, - mimeType, - requiredEtag, - lastModificationTimestamp, - onWifiConnection, - null, - creationTimestamp, - disableRetries, - serverMaxChunkSize - ) - - @Suppress("TooGenericExceptionCaught") - override fun run(client: OwnCloudClient): RemoteOperationResult { - val oldRetryHandler = client.params - .getParameter(HttpMethodParams.RETRY_HANDLER) as? DefaultHttpMethodRetryHandler - - return try { - if (disableRetries) { - // prevent that uploads are retried automatically by network library - client.params.setParameter(HttpMethodParams.RETRY_HANDLER, DefaultHttpMethodRetryHandler(0, false)) - } - - uploadAndAssemble(client) - } catch (e: Exception) { - cancelledOrFailed(e) - } finally { - if (disableRetries) { - // reset previous retry handler - client.params.setParameter(HttpMethodParams.RETRY_HANDLER, oldRetryHandler) + private val onWifiConnection: Boolean, + token: String? = null, + creationTimestamp: Long? = null, + disableRetries: Boolean = true, + private val serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN + ) : UploadFileRemoteOperation( + storagePath, + remotePath, + mimeType, + requiredEtag, + lastModificationTimestamp, + creationTimestamp, + token, + disableRetries + ) { + // Assemble timeouts, in milliseconds. The literals are the definition itself, hence the MagicNumber opt-out. + @Suppress("MagicNumber") + @JvmField + val assembleTimeMin: Int = 30 * 1000 // 30s + + @Suppress("MagicNumber") + @JvmField + val assembleTimeMax: Int = 30 * 60 * 1000 // 30min + + @Suppress("MagicNumber") + @JvmField + val assembleTimePerGB: Int = 3 * 60 * 1000 // 3 min + + private lateinit var uploadFolderUri: String + private lateinit var destinationUri: String + private var moveMethod: MoveMethod? = null + + @JvmOverloads + constructor( + storagePath: String?, + remotePath: String?, + mimeType: String?, + requiredEtag: String?, + lastModificationTimestamp: Long, + creationTimestamp: Long?, + onWifiConnection: Boolean, + disableRetries: Boolean, + serverMaxChunkSize: Long = SERVER_MAX_CHUNK_SIZE_UNKNOWN + ) : this( + storagePath, + remotePath, + mimeType, + requiredEtag, + lastModificationTimestamp, + onWifiConnection, + null, + creationTimestamp, + disableRetries, + serverMaxChunkSize + ) + + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + val oldRetryHandler = + client.params + .getParameter(HttpMethodParams.RETRY_HANDLER) as? DefaultHttpMethodRetryHandler + + return try { + if (disableRetries) { + // prevent that uploads are retried automatically by network library + client.params.setParameter(HttpMethodParams.RETRY_HANDLER, DefaultHttpMethodRetryHandler(0, false)) + } + + uploadAndAssemble(client) + } catch (e: Exception) { + cancelledOrFailed(e) + } finally { + if (disableRetries) { + // reset previous retry handler + client.params.setParameter(HttpMethodParams.RETRY_HANDLER, oldRetryHandler) + } } } - } - - private fun uploadAndAssemble(client: OwnCloudClient): RemoteOperationResult { - val file = File(localPath) - val userId = client.userId - uploadFolderUri = "${client.uploadUri}/$userId/${FileUtils.md5Sum(file)}" - destinationUri = "${client.davUri}/files/$userId${WebdavUtils.encodePath(remotePath)}" + private fun uploadAndAssemble(client: OwnCloudClient): RemoteOperationResult { + val file = File(localPath) + val userId = client.userId - createUploadFolder(client) + uploadFolderUri = "${client.uploadUri}/$userId/${FileUtils.md5Sum(file)}" + destinationUri = "${client.davUri}/files/$userId${WebdavUtils.encodePath(remotePath)}" - val listChunks = PropFindMethod(uploadFolderUri, WebdavUtils.getChunksPropSet(), DavConstants.DEPTH_1) - client.executeMethod(listChunks) + createUploadFolder(client) - if (!listChunks.succeeded()) { - return RemoteOperationResult(false, listChunks) - } - - val uploaded = uploadedChunks(client, listChunks.responseBodyAsMultiStatus) - - return uploadRemainingChunks(client, file, uploaded) ?: assemble(client, file) - } - - private fun createUploadFolder(client: OwnCloudClient) { - val createFolder = MkColMethod(uploadFolderUri) - createFolder.addRequestHeader(DESTINATION_HEADER, destinationUri) - client.executeMethod(createFolder, CREATE_FOLDER_READ_TIMEOUT, DO_NOT_CHANGE_DEFAULT) - } + val listChunks = PropFindMethod(uploadFolderUri, WebdavUtils.getChunksPropSet(), DavConstants.DEPTH_1) + client.executeMethod(listChunks) - private fun uploadedChunks(client: OwnCloudClient, dataInServer: MultiStatus): UploadedChunks { - val uploadPath = requireNotNull(client.uploadUri.path) - var nextByte = 0L - var lastId = 0 + if (!listChunks.succeeded()) { + return RemoteOperationResult(false, listChunks) + } - for (response in dataInServer.responses) { - val entry = WebdavEntry(response, uploadPath) - val chunkId = entry.chunkId() ?: continue + val uploaded = uploadedChunks(client, listChunks.responseBodyAsMultiStatus) - lastId = max(lastId, chunkId) - nextByte += entry.contentLength + return uploadRemainingChunks(client, file, uploaded) ?: assemble(client, file) } - return UploadedChunks(nextByte, lastId) - } - - /** Id of the chunk this entry holds, or `null` for any object not matching the expected chunk name. */ - private fun WebdavEntry.chunkId(): Int? = name - ?.takeIf { !isDirectory && it.length <= CHUNK_NAME_LENGTH && it.isDigitsOnly() } - ?.toIntOrNull() - - private fun uploadRemainingChunks( - client: OwnCloudClient, - file: File, - uploaded: UploadedChunks - ): RemoteOperationResult? { - val chunkSize = chunkSize(onWifiConnection, serverMaxChunkSize) - var nextByte = uploaded.nextByte - var lastId = uploaded.lastId - var failure: RemoteOperationResult? = null - - while (failure == null && nextByte + 1 < file.length()) { - // determine size of next chunk - val chunk = calcNextChunk(file.length(), ++lastId, nextByte, chunkSize) - val chunkResult = uploadChunk(client, chunk) - - failure = when { - !chunkResult.isSuccess -> chunkResult - cancellationRequested.get() -> RemoteOperationResult(OperationCancelledException()) - else -> null - } - - nextByte += chunk.length + private fun createUploadFolder(client: OwnCloudClient) { + val createFolder = MkColMethod(uploadFolderUri) + createFolder.addRequestHeader(DESTINATION_HEADER, destinationUri) + client.executeMethod(createFolder, CREATE_FOLDER_READ_TIMEOUT, DO_NOT_CHANGE_DEFAULT) } - return failure - } + private fun uploadedChunks( + client: OwnCloudClient, + dataInServer: MultiStatus + ): UploadedChunks { + val uploadPath = requireNotNull(client.uploadUri.path) + var nextByte = 0L + var lastId = 0 - private fun assemble(client: OwnCloudClient, file: File): RemoteOperationResult { - val move = MoveMethod(uploadFolderUri + ASSEMBLED_FILE_SUFFIX, destinationUri, true) - moveMethod = move + for (response in dataInServer.responses) { + val entry = WebdavEntry(response, uploadPath) + val chunkId = entry.chunkId() ?: continue - move.addRequestHeader(OC_X_OC_MTIME_HEADER, lastModificationTimestamp.toString()) - creationTimestamp?.takeIf { it > 0 }?.let { move.addRequestHeader(OC_X_OC_CTIME_HEADER, it.toString()) } - token?.let { move.addRequestHeader(E2E_TOKEN, it) } - - val status = client.executeMethod(move, calculateAssembleTimeout(file), DO_NOT_CHANGE_DEFAULT) - - return RemoteOperationResult(isSuccess(status), move) - } - - @Throws(IOException::class) - private fun uploadChunk(client: OwnCloudClient, chunk: Chunk): RemoteOperationResult { - val file = File(localPath) - var raf: RandomAccessFile? = null - var channel: FileChannel? = null + lastId = max(lastId, chunkId) + nextByte += entry.contentLength + } - return try { - raf = RandomAccessFile(file, "r") - channel = raf.channel - entity = ChunkFromFileChannelRequestEntity(channel, mimeType, chunk.start, chunk.length, file) + return UploadedChunks(nextByte, lastId) + } - synchronized(dataTransferListeners) { - (entity as ProgressiveDataTransfer).addDataTransferProgressListeners(dataTransferListeners) + /** Id of the chunk this entry holds, or `null` for any object not matching the expected chunk name. */ + private fun WebdavEntry.chunkId(): Int? = + name + ?.takeIf { !isDirectory && it.length <= CHUNK_NAME_LENGTH && it.isDigitsOnly() } + ?.toIntOrNull() + + private fun uploadRemainingChunks( + client: OwnCloudClient, + file: File, + uploaded: UploadedChunks + ): RemoteOperationResult? { + val chunkSize = chunkSize(onWifiConnection, serverMaxChunkSize) + var nextByte = uploaded.nextByte + var lastId = uploaded.lastId + var failure: RemoteOperationResult? = null + + while (failure == null && nextByte + 1 < file.length()) { + // determine size of next chunk + val chunk = calcNextChunk(file.length(), ++lastId, nextByte, chunkSize) + val chunkResult = uploadChunk(client, chunk) + + failure = + when { + !chunkResult.isSuccess -> chunkResult + cancellationRequested.get() -> RemoteOperationResult(OperationCancelledException()) + else -> null + } + + nextByte += chunk.length } - // pad chunk name to 6 digits - val chunkName = String.format(Locale.ROOT, "%0${CHUNK_NAME_LENGTH}d", chunk.id) - - putMethod?.releaseConnection() // let the connection available for other methods + return failure + } - val put = createPutMethod("$uploadFolderUri/$chunkName") - put.addRequestHeader(DESTINATION_HEADER, destinationUri) - token?.let { put.addRequestHeader(E2E_TOKEN, it) } + private fun assemble( + client: OwnCloudClient, + file: File + ): RemoteOperationResult { + val move = MoveMethod(uploadFolderUri + ASSEMBLED_FILE_SUFFIX, destinationUri, true) + moveMethod = move - val status = client.executeMethod(put, calculateChunkTimeout(chunk.length), DO_NOT_CHANGE_DEFAULT) - val result = RemoteOperationResult(isSuccess(status), put) + move.addRequestHeader(OC_X_OC_MTIME_HEADER, lastModificationTimestamp.toString()) + creationTimestamp?.takeIf { it > 0 }?.let { move.addRequestHeader(OC_X_OC_CTIME_HEADER, it.toString()) } + token?.let { move.addRequestHeader(E2E_TOKEN, it) } - client.exhaustResponse(put.responseBodyAsStream) - Log_OC.d( - TAG, - "Upload of $localPath to $remotePath, chunk id: ${chunk.id} from ${chunk.start} " + - "size: ${chunk.length}, HTTP result status $status" - ) + val status = client.executeMethod(move, calculateAssembleTimeout(file), DO_NOT_CHANGE_DEFAULT) - result - } finally { - channel.closeQuietly("Error closing file channel!") - raf.closeQuietly("Error closing file access!") - putMethod?.releaseConnection() + return RemoteOperationResult(isSuccess(status), move) } - } - - private fun createPutMethod(uri: String): PutMethod { - val put = PutMethod(uri) - putMethod = put - put.requestEntity = entity - if (cancellationRequested.get()) { - put.abort() // next method will throw an exception + @Throws(IOException::class) + private fun uploadChunk( + client: OwnCloudClient, + chunk: Chunk + ): RemoteOperationResult { + val file = File(localPath) + var raf: RandomAccessFile? = null + var channel: FileChannel? = null + + return try { + raf = RandomAccessFile(file, "r") + channel = raf.channel + entity = ChunkFromFileChannelRequestEntity(channel, mimeType, chunk.start, chunk.length, file) + + synchronized(dataTransferListeners) { + (entity as ProgressiveDataTransfer).addDataTransferProgressListeners(dataTransferListeners) + } + + // pad chunk name to 6 digits + val chunkName = String.format(Locale.ROOT, "%0${CHUNK_NAME_LENGTH}d", chunk.id) + + putMethod?.releaseConnection() // let the connection available for other methods + + val put = createPutMethod("$uploadFolderUri/$chunkName") + put.addRequestHeader(DESTINATION_HEADER, destinationUri) + token?.let { put.addRequestHeader(E2E_TOKEN, it) } + + val status = client.executeMethod(put, calculateChunkTimeout(chunk.length), DO_NOT_CHANGE_DEFAULT) + val result = RemoteOperationResult(isSuccess(status), put) + + client.exhaustResponse(put.responseBodyAsStream) + Log_OC.d( + TAG, + "Upload of $localPath to $remotePath, chunk id: ${chunk.id} from ${chunk.start} " + + "size: ${chunk.length}, HTTP result status $status" + ) + + result + } finally { + channel.closeQuietly("Error closing file channel!") + raf.closeQuietly("Error closing file access!") + putMethod?.releaseConnection() + } } - return put - } - - private fun cancelledOrFailed(e: Exception): RemoteOperationResult = when { - putMethod?.isAborted == true || moveMethod?.isAborted == true -> - cancellationReason - ?.takeIf { cancellationRequested.get() } - ?.let { RemoteOperationResult(it) } - ?: RemoteOperationResult(OperationCancelledException()) + private fun createPutMethod(uri: String): PutMethod { + val put = PutMethod(uri) + putMethod = put + put.requestEntity = entity - else -> RemoteOperationResult(e) - } + if (cancellationRequested.get()) { + put.abort() // next method will throw an exception + } - private fun Closeable?.closeQuietly(errorMessage: String) { - try { - this?.close() - } catch (e: IOException) { - Log_OC.e(TAG, errorMessage, e) + return put } - } - @VisibleForTesting - fun calculateAssembleTimeout(file: File): Int { - val fileSizeInGb = file.length() / BYTES_PER_GB - - return max(assembleTimeMin, min((assembleTimePerGB * fileSizeInGb).toInt(), assembleTimeMax)) - } - - @VisibleForTesting - fun calculateChunkTimeout(chunkLength: Long): Int { - val chunkSizeInMib = chunkLength / BYTES_PER_MIB + private fun cancelledOrFailed(e: Exception): RemoteOperationResult = + when { + putMethod?.isAborted == true || moveMethod?.isAborted == true -> { + cancellationReason + ?.takeIf { cancellationRequested.get() } + ?.let { RemoteOperationResult(it) } + ?: RemoteOperationResult(OperationCancelledException()) + } + + else -> { + RemoteOperationResult(e) + } + } - return max(CHUNK_TIMEOUT_MIN, min((CHUNK_TIMEOUT_PER_MIB * chunkSizeInMib).toInt(), CHUNK_TIMEOUT_MAX)) - } + private fun Closeable?.closeQuietly(errorMessage: String) { + try { + this?.close() + } catch (e: IOException) { + Log_OC.e(TAG, errorMessage, e) + } + } - private data class UploadedChunks(val nextByte: Long, val lastId: Int) + @VisibleForTesting + fun calculateAssembleTimeout(file: File): Int { + val fileSizeInGb = file.length() / BYTES_PER_GB - companion object { - const val MIN_CHUNK_SIZE: Long = 10240000 - const val DEFAULT_CHUNK_SIZE: Long = 40960000 - const val SERVER_MAX_CHUNK_SIZE_UNKNOWN: Long = -1 - const val DESTINATION_HEADER: String = "Destination" - const val CHUNK_NAME_LENGTH: Int = 6 + return max(assembleTimeMin, min((assembleTimePerGB * fileSizeInGb).toInt(), assembleTimeMax)) + } - @JvmStatic - fun chunkSize(onWifiConnection: Boolean, serverMaxChunkSize: Long): Long { - if (serverMaxChunkSize <= 0) { - return if (onWifiConnection) DEFAULT_CHUNK_SIZE else MIN_CHUNK_SIZE - } + @VisibleForTesting + fun calculateChunkTimeout(chunkLength: Long): Int { + val chunkSizeInMib = chunkLength / BYTES_PER_MIB - return serverMaxChunkSize + return max(CHUNK_TIMEOUT_MIN, min((CHUNK_TIMEOUT_PER_MIB * chunkSizeInMib).toInt(), CHUNK_TIMEOUT_MAX)) } - @JvmStatic - fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = - chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) - - private const val ASSEMBLED_FILE_SUFFIX = "/.file" - private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 - private const val DO_NOT_CHANGE_DEFAULT = -1 - private const val CHUNK_TIMEOUT_MIN = 60_000 - private const val CHUNK_TIMEOUT_MAX = 10 * 60 * 1000 - private const val CHUNK_TIMEOUT_PER_MIB = 1_000 - private const val BYTES_PER_MIB = 1024.0 * 1024.0 - private const val BYTES_PER_GB = 1e9 - private val TAG = ChunkedFileUploadRemoteOperation::class.java.simpleName - - internal fun calcNextChunk(fileSize: Long, chunkId: Int, startByte: Long, chunkSize: Long): Chunk { - require(chunkId >= 0 && chunkId.toString().length <= CHUNK_NAME_LENGTH) { - "chunkId must not exceed length specified in CHUNK_NAME_LENGTH ($CHUNK_NAME_LENGTH)" + private data class UploadedChunks( + val nextByte: Long, + val lastId: Int + ) + + companion object { + const val MIN_CHUNK_SIZE: Long = 10240000 + const val DEFAULT_CHUNK_SIZE: Long = 40960000 + const val SERVER_MAX_CHUNK_SIZE_UNKNOWN: Long = -1 + const val DESTINATION_HEADER: String = "Destination" + const val CHUNK_NAME_LENGTH: Int = 6 + + @JvmStatic + fun chunkSize( + onWifiConnection: Boolean, + serverMaxChunkSize: Long + ): Long { + if (serverMaxChunkSize <= 0) { + return if (onWifiConnection) DEFAULT_CHUNK_SIZE else MIN_CHUNK_SIZE + } + + return serverMaxChunkSize } - val length = if (startByte + chunkSize > fileSize) fileSize - startByte else chunkSize - - return Chunk(chunkId, startByte, length) + @JvmStatic + fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = + chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) + + private const val ASSEMBLED_FILE_SUFFIX = "/.file" + private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 + private const val DO_NOT_CHANGE_DEFAULT = -1 + private const val CHUNK_TIMEOUT_MIN = 60_000 + private const val CHUNK_TIMEOUT_MAX = 10 * 60 * 1000 + private const val CHUNK_TIMEOUT_PER_MIB = 1_000 + private const val BYTES_PER_MIB = 1024.0 * 1024.0 + private const val BYTES_PER_GB = 1e9 + private val TAG = ChunkedFileUploadRemoteOperation::class.java.simpleName + + internal fun calcNextChunk( + fileSize: Long, + chunkId: Int, + startByte: Long, + chunkSize: Long + ): Chunk { + require(chunkId >= 0 && chunkId.toString().length <= CHUNK_NAME_LENGTH) { + "chunkId must not exceed length specified in CHUNK_NAME_LENGTH ($CHUNK_NAME_LENGTH)" + } + + val length = if (startByte + chunkSize > fileSize) fileSize - startByte else chunkSize + + return Chunk(chunkId, startByte, length) + } } } -} From 2c5acd7d4b6b725cb52b65e6bb4e623916edd365 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 10:55:47 +0200 Subject: [PATCH 09/12] improve uploadRemainingChunks Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 25cad207f8..7bf3133165 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -178,30 +178,32 @@ class ChunkedFileUploadRemoteOperation client: OwnCloudClient, file: File, uploaded: UploadedChunks - ): RemoteOperationResult? { - val chunkSize = chunkSize(onWifiConnection, serverMaxChunkSize) - var nextByte = uploaded.nextByte - var lastId = uploaded.lastId - var failure: RemoteOperationResult? = null - - while (failure == null && nextByte + 1 < file.length()) { - // determine size of next chunk - val chunk = calcNextChunk(file.length(), ++lastId, nextByte, chunkSize) - val chunkResult = uploadChunk(client, chunk) - - failure = - when { - !chunkResult.isSuccess -> chunkResult - cancellationRequested.get() -> RemoteOperationResult(OperationCancelledException()) - else -> null - } - - nextByte += chunk.length + ): RemoteOperationResult? = + remainingChunks(file.length(), uploaded).firstNotNullOfOrNull { chunk -> + uploadChunk(client, chunk).failureOrNull() } - return failure + private fun remainingChunks( + fileLength: Long, + uploaded: UploadedChunks + ): Sequence { + val chunkSize = chunkSize(onWifiConnection, serverMaxChunkSize) + val firstId = uploaded.lastId + 1 + + // everything below uploaded.nextByte is already on the server, and a single trailing byte is not chunked + return (uploaded.nextByte until fileLength - 1 step chunkSize) + .asSequence() + .mapIndexed { index, startByte -> calcNextChunk(fileLength, firstId + index, startByte, chunkSize) } } + /** The result to report, or `null` when the chunk went through and the upload may continue. */ + private fun RemoteOperationResult.failureOrNull(): RemoteOperationResult? = + when { + !isSuccess -> this + cancellationRequested.get() -> RemoteOperationResult(OperationCancelledException()) + else -> null + } + private fun assemble( client: OwnCloudClient, file: File From 232d9b92db8097899a3548858d86a7350fdb4933 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 13:33:34 +0200 Subject: [PATCH 10/12] revert chunk upload timeout Signed-off-by: alperozturk96 --- .../files/ChunkedFileUploadRemoteOperation.kt | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 7bf3133165..34744a7447 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -247,7 +247,7 @@ class ChunkedFileUploadRemoteOperation put.addRequestHeader(DESTINATION_HEADER, destinationUri) token?.let { put.addRequestHeader(E2E_TOKEN, it) } - val status = client.executeMethod(put, calculateChunkTimeout(chunk.length), DO_NOT_CHANGE_DEFAULT) + val status = client.executeMethod(put) val result = RemoteOperationResult(isSuccess(status), put) client.exhaustResponse(put.responseBodyAsStream) @@ -306,13 +306,6 @@ class ChunkedFileUploadRemoteOperation return max(assembleTimeMin, min((assembleTimePerGB * fileSizeInGb).toInt(), assembleTimeMax)) } - @VisibleForTesting - fun calculateChunkTimeout(chunkLength: Long): Int { - val chunkSizeInMib = chunkLength / BYTES_PER_MIB - - return max(CHUNK_TIMEOUT_MIN, min((CHUNK_TIMEOUT_PER_MIB * chunkSizeInMib).toInt(), CHUNK_TIMEOUT_MAX)) - } - private data class UploadedChunks( val nextByte: Long, val lastId: Int @@ -337,17 +330,9 @@ class ChunkedFileUploadRemoteOperation return serverMaxChunkSize } - @JvmStatic - fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = - chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) - private const val ASSEMBLED_FILE_SUFFIX = "/.file" private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 private const val DO_NOT_CHANGE_DEFAULT = -1 - private const val CHUNK_TIMEOUT_MIN = 60_000 - private const val CHUNK_TIMEOUT_MAX = 10 * 60 * 1000 - private const val CHUNK_TIMEOUT_PER_MIB = 1_000 - private const val BYTES_PER_MIB = 1024.0 * 1024.0 private const val BYTES_PER_GB = 1e9 private val TAG = ChunkedFileUploadRemoteOperation::class.java.simpleName From edd4a3d4a563280dc63c4dd431e9e91b7d6d33f6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 13:44:04 +0200 Subject: [PATCH 11/12] chunkedUploadThreshold Signed-off-by: alperozturk96 --- .../lib/resources/files/ChunkedFileUploadRemoteOperation.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 34744a7447..4210a778c4 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -330,6 +330,10 @@ class ChunkedFileUploadRemoteOperation return serverMaxChunkSize } + @JvmStatic + fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = + chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) + private const val ASSEMBLED_FILE_SUFFIX = "/.file" private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 private const val DO_NOT_CHANGE_DEFAULT = -1 From c33cad8ee15e2fb8a6cbb1282912b82ff4eef64d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 13:45:17 +0200 Subject: [PATCH 12/12] chunkedUploadThreshold Signed-off-by: alperozturk96 --- .../lib/resources/files/ChunkedFileUploadRemoteOperation.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt index 4210a778c4..34744a7447 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.kt @@ -330,10 +330,6 @@ class ChunkedFileUploadRemoteOperation return serverMaxChunkSize } - @JvmStatic - fun chunkedUploadThreshold(serverMaxChunkSize: Long): Long = - chunkSize(onWifiConnection = false, serverMaxChunkSize = serverMaxChunkSize) - private const val ASSEMBLED_FILE_SUFFIX = "/.file" private const val CREATE_FOLDER_READ_TIMEOUT = 30_000 private const val DO_NOT_CHANGE_DEFAULT = -1