From 827efb738f504ae49dc674af0bee7816d413a879 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 15:23:15 +0200 Subject: [PATCH 1/6] fix alignment Signed-off-by: alperozturk96 --- .../client/jobs/autoUpload/AutoUploadWorker.kt | 3 ++- .../client/jobs/upload/FileUploadHelper.kt | 15 ++++++++++++++- .../utils/extensions/UploadResultExtensions.kt | 1 - .../android/operations/UploadFileOperation.java | 10 ++++------ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index ff897db233d6..f74e2fd7a98b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -424,10 +424,11 @@ class AutoUploadWorker( isWhileChargingOnly = needsCharging localAction = uploadAction + isCreateRemoteFolder = true + // Only set these for new uploads if (uploadEntity == null) { createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE - isCreateRemoteFolder = true } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 714a7d0eb382..de3579038ccd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -134,7 +134,16 @@ class FileUploadHelper { appScope.launch { try { - val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) + /** + * Rows are marked `UPLOAD_IN_PROGRESS` before upload starts. + * Only `updateDatabaseUploadResult()` writes a terminal status. + * If the process dies first, the row remains `UPLOAD_IN_PROGRESS` + * and is treated as a failed upload eligible for retry. + * + * e.g. AutoUploadWorker killed before completed + */ + val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) + + getUploadsByStatus(null, UploadStatus.UPLOAD_IN_PROGRESS, capability) retryUploads( uploadsStorageManager, @@ -186,6 +195,10 @@ class FileUploadHelper { val client = accountManager.createOwncloudClient() for (upload in uploads) { + if (FileUploadWorker.isUploading(upload.remotePath, upload.accountName)) { + continue + } + if (upload.isLastResultConflictError()) { client?.let { conflictHandlingResult = diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt index 4ca297751522..674256cd034b 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt @@ -14,7 +14,6 @@ import com.owncloud.android.db.UploadResult fun UploadResult.isNonRetryable(): Boolean = when (this) { UploadResult.FILE_NOT_FOUND, UploadResult.FILE_ERROR, - UploadResult.FOLDER_ERROR, UploadResult.CANNOT_CREATE_FILE, UploadResult.SYNC_CONFLICT, UploadResult.CONFLICT_ERROR, diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index dd7f88d33588..5b1cef57f58f 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -467,10 +467,9 @@ protected RemoteOperationResult run(OwnCloudClient client) { Log_OC.d(TAG, "parent lookup for path: " + remoteParentPath + " → " + (parent == null ? "not found in DB" : "found, id=" + parent.getFileId())); - // in case of a fresh upload with subfolder, where parent does not exist yet - if (parent == null && (mFolderUnlockToken == null || mFolderUnlockToken.isEmpty())) { - Log_OC.d(TAG, "parent not in DB and no unlock token, attempting to grant folder existence: " - + remoteParentPath); + final boolean isResumingEncryptedUpload = (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()); + if (!isResumingEncryptedUpload && (parent == null || mRemoteFolderToBeCreated)) { + Log_OC.d(TAG, "verifying remote parent folder exists: " + remoteParentPath); final var result = grantFolderExistence(remoteParentPath, client); if (!result.isSuccess()) { @@ -485,8 +484,7 @@ protected RemoteOperationResult run(OwnCloudClient client) { return new RemoteOperationResult<>(ResultCode.UNKNOWN_ERROR); } - Log_OC.d(TAG, "parent created and retrieved successfully: " + remoteParentPath + ", id=" + - parent.getFileId()); + Log_OC.d(TAG, "remote parent folder confirmed: " + remoteParentPath + ", id=" + parent.getFileId()); } if (parent == null) { From a6e6c15f9a73372f99a8df3c25bfc72bb463c4a5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 16:02:05 +0200 Subject: [PATCH 2/6] add tests Signed-off-by: alperozturk96 --- .../android/GrantFolderExistenceTests.kt | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt new file mode 100644 index 000000000000..836fc3c8cff1 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -0,0 +1,148 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android + +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.client.device.BatteryStatus +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation +import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation +import com.owncloud.android.operations.UploadFileOperation +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.IOException + +class GrantFolderExistenceTests : AbstractOnServerIT() { + + private val root = "/autoupload/" + private val yearFolder = root + "2026/" + private val monthFolder = yearFolder + "07/" + + private val uploadsStorageManager = UploadsStorageManager( + UserAccountManagerImpl.fromContext(targetContext), + targetContext.contentResolver + ) + + private val powerManagementServiceMock = object : PowerManagementService { + override val isPowerSavingEnabled = false + override val isIgnoringOptimization = true + override val battery = BatteryStatus(false, 0) + } + + @Before + @Throws(IOException::class) + fun before() { + createDummyFiles() + } + + @Test + fun testUploadFileThenDeleteFolder() { + uploadAndAssertSuccess("first.txt") + + assertTrue("month folder should exist on server", existsOnServer(monthFolder)) + assertNotNull("month folder should be cached locally", storageManager.getFileByDecryptedRemotePath(monthFolder)) + + removeYearFolderOnServerOnly() + + assertFalse("month folder should be deleted", existsOnServer(monthFolder)) + assertNotNull( + "app still has a cached entry for the removed folder", + storageManager.getFileByDecryptedRemotePath(monthFolder) + ) + + // attempting upload to the same folder + val result = upload("nonEmpty.txt", monthFolder + "nonEmpty.txt") + + assertEquals( + "upload must not fail with a conflict, the missing folder has to be recreated", + RemoteOperationResult.ResultCode.OK, + result.code + ) + assertTrue("month folder should be recreated on server", existsOnServer(monthFolder)) + assertTrue("uploaded file should exist on server", existsOnServer(monthFolder + "nonEmpty.txt")) + } + + @Test + fun testRemoveRootThenUploadFile() { + uploadAndAssertSuccess("first.txt") + + assertTrue( + "root should be removed", + RemoveFileRemoteOperation(root).execute(client).isSuccess + ) + assertFalse(existsOnServer(root)) + + val result = upload("nonEmpty.txt", monthFolder + "nonEmpty.txt") + + assertEquals( + "every missing folder level has to be recreated", + RemoteOperationResult.ResultCode.OK, + result.code + ) + assertTrue(existsOnServer(yearFolder)) + assertTrue(existsOnServer(monthFolder)) + assertTrue(existsOnServer(monthFolder + "nonEmpty.txt")) + } + + private fun uploadAndAssertSuccess(filename: String) { + val result = upload(filename, monthFolder + filename) + assertTrue(result.logMessage, result.isSuccess) + assertTrue("uploaded file should exist on server", existsOnServer(monthFolder + filename)) + } + + private fun removeYearFolderOnServerOnly() { + assertTrue( + "year folder should be removed", + RemoveFileRemoteOperation(yearFolder).execute(client).isSuccess + ) + } + + private fun existsOnServer(remotePath: String): Boolean = + ExistenceCheckRemoteOperation(remotePath, false).execute(client).isSuccess + + private fun upload(localFileName: String, remotePath: String): RemoteOperationResult<*> { + val localFile = createFile(localFileName, FILE_LINE_COUNT) + assertTrue("local test file must exist before uploading", localFile.exists()) + + val ocUpload = OCUpload( + localFile.absolutePath, + remotePath, + account.name + ).apply { + isCreateRemoteFolder = true + createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE + } + + return UploadFileOperation( + uploadsStorageManager, + connectivityServiceMock, + powerManagementServiceMock, + user, + null, + ocUpload, + NameCollisionPolicy.ASK_USER, + FileUploadWorker.LOCAL_BEHAVIOUR_COPY, + targetContext, + false, + false, + storageManager + ).execute(client) + } + + companion object { + private const val FILE_LINE_COUNT = 100 + } +} From c278a2453a475fa24f42a20f90c9fe6381c63f9a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 16:08:00 +0200 Subject: [PATCH 3/6] add tests Signed-off-by: alperozturk96 --- .../java/com/owncloud/android/GrantFolderExistenceTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt index 836fc3c8cff1..20b0c10c11fb 100644 --- a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -49,7 +49,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { } @Test - fun testUploadFileThenDeleteFolder() { + fun testUploadFileThenDeleteYearFolderOnServerOnlyThenUploadAgainShouldRecreateMonthFolderAndReturnOk() { uploadAndAssertSuccess("first.txt") assertTrue("month folder should exist on server", existsOnServer(monthFolder)) @@ -76,7 +76,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { } @Test - fun testRemoveRootThenUploadFile() { + fun testUploadFileThenDeleteRootOnServerOnlyThenUploadAgainShouldRecreateAllFolderLevelsAndReturnOk() { uploadAndAssertSuccess("first.txt") assertTrue( From d0096f37189473eb24b928a381bf206787292e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?= <67455295+alperozturk96@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:18:13 +0200 Subject: [PATCH 4/6] Apply suggestion from @alperozturk96 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com> --- .../java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index de3579038ccd..64ace5403da4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -136,7 +136,7 @@ class FileUploadHelper { try { /** * Rows are marked `UPLOAD_IN_PROGRESS` before upload starts. - * Only `updateDatabaseUploadResult()` writes a terminal status. + * Only `updateDatabaseUploadResult()` writes status. * If the process dies first, the row remains `UPLOAD_IN_PROGRESS` * and is treated as a failed upload eligible for retry. * From 5429ad5d7f8303bdbc147ae084c41198532a2f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?= <67455295+alperozturk96@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:19:23 +0200 Subject: [PATCH 5/6] Apply suggestion from @alperozturk96 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com> --- .../java/com/owncloud/android/GrantFolderExistenceTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt index 20b0c10c11fb..8c381e7e3ecd 100644 --- a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -115,7 +115,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { private fun upload(localFileName: String, remotePath: String): RemoteOperationResult<*> { val localFile = createFile(localFileName, FILE_LINE_COUNT) - assertTrue("local test file must exist before uploading", localFile.exists()) + assertTrue("local file must exist before uploading", localFile.exists()) val ocUpload = OCUpload( localFile.absolutePath, From be011054b21a82fcbae7f8e12b6d644baa1cdc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?= <67455295+alperozturk96@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:19:54 +0200 Subject: [PATCH 6/6] Apply suggestion from @alperozturk96 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com> --- .../java/com/owncloud/android/GrantFolderExistenceTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt index 8c381e7e3ecd..a5c296340fd4 100644 --- a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -80,7 +80,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { uploadAndAssertSuccess("first.txt") assertTrue( - "root should be removed", + "root folder should be removed", RemoveFileRemoteOperation(root).execute(client).isSuccess ) assertFalse(existsOnServer(root))