Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test fails on master that proves: #17074

Image


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 testUploadFileThenDeleteYearFolderOnServerOnlyThenUploadAgainShouldRecreateMonthFolderAndReturnOk() {
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 testUploadFileThenDeleteRootOnServerOnlyThenUploadAgainShouldRecreateAllFolderLevelsAndReturnOk() {
uploadAndAssertSuccess("first.txt")

assertTrue(
"root folder 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 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 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,
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UploadResult.FOLDER_ERROR this must be excluded from isNonRetryable

UploadResult.CANNOT_CREATE_FILE,
UploadResult.SYNC_CONFLICT,
UploadResult.CONFLICT_ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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) {
Expand Down
Loading