From 60df73e35a529cc88bc5c8904a04f2195d755b43 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 11:53:34 +0200 Subject: [PATCH 01/11] Rename .java to .kt Signed-off-by: alperozturk --- .../com/owncloud/android/utils/{FileUtil.java => FileUtil.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/utils/{FileUtil.java => FileUtil.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/utils/FileUtil.java b/app/src/main/java/com/owncloud/android/utils/FileUtil.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/utils/FileUtil.java rename to app/src/main/java/com/owncloud/android/utils/FileUtil.kt From af6e5db40d18fd0f371d6adaa56d42114b4c9dcd Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 11:53:35 +0200 Subject: [PATCH 02/11] check isSameRemoteFileAlreadyPresent due to file extension Signed-off-by: alperozturk --- .../owncloud/android/utils/FileUtilTest.kt | 55 +++++++++-- .../client/jobs/BackgroundJobFactory.kt | 6 +- .../OfflineOperationsWorker.kt | 31 +------ .../jobs/operation/FileOperationHelper.kt | 53 +++++++++++ .../client/jobs/upload/FileUploadWorker.kt | 31 ++++--- .../com/owncloud/android/utils/FileUtil.kt | 92 +++++++++++-------- 6 files changed, 184 insertions(+), 84 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt index 6c27e85983bd..3a4e4b965701 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt @@ -8,24 +8,25 @@ package com.owncloud.android.utils import com.owncloud.android.AbstractIT import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Test import java.io.File class FileUtilTest : AbstractIT() { @Test fun assertNullInput() { - Assert.assertEquals("", FileUtil.getFilenameFromPathString(null)) + assertEquals("", FileUtil.getFilenameFromPathString(null)) } @Test fun assertEmptyInput() { - Assert.assertEquals("", FileUtil.getFilenameFromPathString("")) + assertEquals("", FileUtil.getFilenameFromPathString("")) } @Test fun assertFileInput() { val file = getDummyFile("empty.txt") - Assert.assertEquals("empty.txt", FileUtil.getFilenameFromPathString(file.absolutePath)) + assertEquals("empty.txt", FileUtil.getFilenameFromPathString(file.absolutePath)) } @Test @@ -34,13 +35,13 @@ class FileUtilTest : AbstractIT() { if (!tempPath.exists()) { Assert.assertTrue(tempPath.mkdirs()) } - Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath)) + assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath)) } @Test fun assertDotFileInput() { val file = getDummyFile(".dotfile.ext") - Assert.assertEquals(".dotfile.ext", FileUtil.getFilenameFromPathString(file.absolutePath)) + assertEquals(".dotfile.ext", FileUtil.getFilenameFromPathString(file.absolutePath)) } @Test @@ -50,12 +51,52 @@ class FileUtilTest : AbstractIT() { Assert.assertTrue(tempPath.mkdirs()) } - Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath)) + assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath)) } @Test fun assertNoFileExtensionInput() { val file = getDummyFile("file") - Assert.assertEquals("file", FileUtil.getFilenameFromPathString(file.absolutePath)) + assertEquals("file", FileUtil.getFilenameFromPathString(file.absolutePath)) + } + + @Test + fun testGetRemotePathVariantsWithUppercaseExtension() { + val path = "/TesTFolder/abc.JPG" + val expected = Pair("/TesTFolder/abc.jpg", "/TesTFolder/abc.JPG") + val actual = FileUtil.getRemotePathVariants(path) + assertEquals(expected, actual) + } + + @Test + fun testGetRemotePathVariantsWithLowercaseExtension() { + val path = "/TesTFolder/abc.png" + val expected = Pair("/TesTFolder/abc.png", "/TesTFolder/abc.PNG") + val actual = FileUtil.getRemotePathVariants(path) + assertEquals(expected, actual) + } + + @Test + fun testGetRemotePathVariantsMixedCaseExtension() { + val path = "/TesTFolder/abc.JpEg" + val expected = Pair("/TesTFolder/abc.jpeg", "/TesTFolder/abc.JPEG") + val actual = FileUtil.getRemotePathVariants(path) + assertEquals(expected, actual) + } + + @Test + fun testGetRemotePathVariantsNoExtension() { + val path = "/TesTFolder/abc" + val expected = Pair(path, path) + val actual = FileUtil.getRemotePathVariants(path) + assertEquals(expected, actual) + } + + @Test + fun testGetRemotePathVariantsDotAtEnd() { + val path = "/TesTFolder/abc." + val expected = Pair(path, path) + val actual = FileUtil.getRemotePathVariants(path) + assertEquals(expected, actual) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index e58d8b8ab1d1..abd469ef3822 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -24,6 +24,7 @@ import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService @@ -61,7 +62,8 @@ class BackgroundJobFactory @Inject constructor( private val viewThemeUtils: Provider, private val localBroadcastManager: Provider, private val generatePdfUseCase: GeneratePDFUseCase, - private val syncedFolderProvider: SyncedFolderProvider + private val syncedFolderProvider: SyncedFolderProvider, + private val fileOperationHelper: FileOperationHelper ) : WorkerFactory() { @SuppressLint("NewApi") @@ -108,6 +110,7 @@ class BackgroundJobFactory @Inject constructor( accountManager.user, context, connectivityService, + fileOperationHelper, viewThemeUtils.get(), params ) @@ -230,6 +233,7 @@ class BackgroundJobFactory @Inject constructor( localBroadcastManager.get(), backgroundJobManager.get(), preferences, + fileOperationHelper, context, params ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt index 45f8d79fd31e..a614f848d491 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt @@ -13,6 +13,7 @@ import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.OfflineOperationEntity import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.network.ClientFactoryImpl import com.nextcloud.client.network.ConnectivityService import com.nextcloud.model.OfflineOperationType @@ -24,14 +25,10 @@ import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation -import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation -import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.RenameFileOperation -import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable @@ -42,10 +39,12 @@ import kotlin.coroutines.suspendCoroutine private typealias OfflineOperationResult = Pair?, RemoteOperation<*>?>? +@Suppress("LongParameterList") class OfflineOperationsWorker( private val user: User, private val context: Context, private val connectivityService: ConnectivityService, + private val fileOperationHelper: FileOperationHelper, viewThemeUtils: ViewThemeUtils, params: WorkerParameters ) : CoroutineWorker(context, params) { @@ -126,10 +125,10 @@ class OfflineOperationsWorker( return@withContext null } - val remoteFile = getRemoteFile(path) + val remoteFile = fileOperationHelper.getRemoteFile(path, client) val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path) - if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) { + if (ocFile != null && fileOperationHelper.isFileChanged(remoteFile, ocFile)) { Log_OC.w(TAG, "Offline operation skipped, file already exists: $operation") if (operation.isRenameOrRemove()) { @@ -259,24 +258,4 @@ class OfflineOperationsWorker( notificationManager.showNewNotification(operationResult, operation) } } - - @Suppress("DEPRECATION") - private fun getRemoteFile(remotePath: String): RemoteFile? { - val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath) - val isFolder = MimeTypeUtil.isFolder(mimeType) - val client = ClientFactoryImpl(context).create(user) - val result = if (isFolder) { - ReadFolderRemoteOperation(remotePath).execute(client) - } else { - ReadFileRemoteOperation(remotePath).execute(client) - } - - return if (result.isSuccess) { - result.data[0] as? RemoteFile - } else { - null - } - } - - private fun isFileChanged(remoteFile: RemoteFile, ocFile: OCFile): Boolean = remoteFile.etag != ocFile.etagOnServer } diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 6cbcb7766893..60439d800233 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -14,7 +14,12 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation +import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.utils.FileUtil +import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -28,6 +33,54 @@ class FileOperationHelper( private val TAG = FileOperationHelper::class.java.simpleName } + /** + * Checks if a file with the same remote path (case-insensitive) and unchanged content + * already exists in local storage by considering both lowercase and uppercase variants + * of the file extension. + * + * ### Example: + * ``` + * On the server, 0001.WEBP exists and the user tries to upload the same file + * with the lowercased version 0001.webp — in that case, this will return true. + * ``` + */ + fun isSameRemoteFileAlreadyPresent(remotePath: String, client: OwnCloudClient): Boolean { + val (lc, uc) = FileUtil.getRemotePathVariants(remotePath) + + val localOCFile = fileDataStorageManager.getFileByDecryptedRemotePath(lc) + ?: fileDataStorageManager.getFileByDecryptedRemotePath(uc) + + if (localOCFile != null && localOCFile.remotePath.equals(remotePath, ignoreCase = true)) { + val remoteFile = getRemoteFile(remotePath, client) + if (!isFileChanged(remoteFile, localOCFile)) { + Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") + return true + } + } + + return false + } + + @Suppress("DEPRECATION") + fun getRemoteFile(remotePath: String, client: OwnCloudClient): RemoteFile? { + val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath) + val isFolder = MimeTypeUtil.isFolder(mimeType) + val result = if (isFolder) { + ReadFolderRemoteOperation(remotePath).execute(client) + } else { + ReadFileRemoteOperation(remotePath).execute(client) + } + + return if (result.isSuccess) { + result.data[0] as? RemoteFile + } else { + null + } + } + + fun isFileChanged(remoteFile: RemoteFile?, ocFile: OCFile?): Boolean = + (remoteFile != null && ocFile != null && remoteFile.etag != ocFile.etagOnServer) + @Suppress("TooGenericExceptionCaught", "Deprecation") suspend fun removeFile( file: OCFile, diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index f7cfb0b0ba25..2c78638e58b1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -17,6 +17,7 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.BackgroundJobManagerImpl +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState @@ -26,7 +27,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload -import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.network.OnDatatransferProgressListener import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -48,6 +49,7 @@ class FileUploadWorker( val localBroadcastManager: LocalBroadcastManager, private val backgroundJobManager: BackgroundJobManager, val preferences: AppPreferences, + private val fileOperationHelper: FileOperationHelper, val context: Context, params: WorkerParameters ) : Worker(context, params), @@ -130,6 +132,12 @@ class FileUploadWorker( val uploadIds = inputData.getLongArray(UPLOAD_IDS) ?: return Result.success() val uploads = uploadIds.map { id -> uploadsStorageManager.getUploadById(id) }.filterNotNull() val totalUploadSize = uploadIds.size + val user = userAccountManager.getUser(accountName) + if (!user.isPresent) { + return Result.failure() + } + val client = + OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.get().toOwnCloudAccount(), context) for ((index, upload) in uploads.withIndex()) { if (preferences.isGlobalUploadPaused) { @@ -145,12 +153,6 @@ class FileUploadWorker( return Result.failure() } - val user = userAccountManager.getUser(accountName) - if (!user.isPresent) { - uploadsStorageManager.removeUpload(upload.uploadId) - continue - } - if (isStopped) { continue } @@ -158,6 +160,11 @@ class FileUploadWorker( setWorkerState(user.get()) val operation = createUploadFileOperation(upload, user.get()) + if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload.remotePath, client)) { + uploadsStorageManager.removeUpload(upload.uploadId) + continue + } + currentUploadFileOperation = operation notificationManager.prepareForStart( @@ -168,7 +175,7 @@ class FileUploadWorker( totalUploadSize = totalUploadSize ) - val result = upload(operation, user.get()) + val result = upload(client,operation, user.get()) currentUploadFileOperation = null fileUploaderDelegate.sendBroadcastUploadFinished( @@ -216,13 +223,15 @@ class FileUploadWorker( } @Suppress("TooGenericExceptionCaught", "DEPRECATION") - private fun upload(uploadFileOperation: UploadFileOperation, user: User): RemoteOperationResult { + private fun upload( + uploadClient: OwnCloudClient, + uploadFileOperation: UploadFileOperation, + user: User + ): RemoteOperationResult { lateinit var result: RemoteOperationResult try { val storageManager = uploadFileOperation.storageManager - val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) - val uploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context) result = uploadFileOperation.execute(uploadClient) val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user) diff --git a/app/src/main/java/com/owncloud/android/utils/FileUtil.kt b/app/src/main/java/com/owncloud/android/utils/FileUtil.kt index d41411db69e3..04aa83b99bcd 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileUtil.kt @@ -1,61 +1,75 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Andy Scherzinger * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.utils; +package com.owncloud.android.utils -import android.text.TextUtils; - -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.concurrent.TimeUnit; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class FileUtil { - - private FileUtil() { - // utility class -> private constructor - } +import com.nextcloud.utils.extensions.StringConstants +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.attribute.BasicFileAttributes +import java.util.concurrent.TimeUnit +object FileUtil { /** * returns the file name of a given path. * * @param filePath (absolute) file path - * @return the filename including its file extension, empty String for invalid input values + * @return the filename including its file extension, `empty String` for invalid input values */ - public static @NonNull - String getFilenameFromPathString(@Nullable String filePath) { - if (!TextUtils.isEmpty(filePath)) { - File file = new File(filePath); - if (file.isFile()) { - return file.getName(); - } else { - return ""; - } + fun getFilenameFromPathString(filePath: String?): String = if (!filePath.isNullOrBlank()) { + val file = File(filePath) + if (file.isFile()) { + file.getName() } else { - return ""; + "" } + } else { + "" } - public static @Nullable - Long getCreationTimestamp(File file) { + @JvmStatic + fun getCreationTimestamp(file: File): Long? { try { - return Files.readAttributes(file.toPath(), BasicFileAttributes.class) + return Files.readAttributes(file.toPath(), BasicFileAttributes::class.java) .creationTime() - .to(TimeUnit.SECONDS); - } catch (IOException e) { - Log_OC.e(UploadFileRemoteOperation.class.getSimpleName(), - "Failed to read creation timestamp for file: " + file.getName()); - return null; + .to(TimeUnit.SECONDS) + } catch (e: IOException) { + Log_OC.e( + UploadFileRemoteOperation::class.java.getSimpleName(), + "Failed to read creation timestamp for file: " + file.getName() + ) + return null } } + + /** + * Returns remote path variants (lowercase and uppercase extension) for the given path. + * + * Example: + * ``` + * If you pass "/TesTFolder/abc.JPG", it will return: + * "/TesTFolder/abc.jpg" and "/TesTFolder/abc.JPG" + * ``` + */ + fun getRemotePathVariants(path: String): Pair { + val lastDot = path.lastIndexOf(StringConstants.DOT) + if (lastDot == -1 || lastDot == path.length - 1) { + return Pair(path, path) + } + + val base = path.substring(0, lastDot) + val ext = path.substring(lastDot + 1) + + val lower = "$base.${ext.lowercase()}" + val upper = "$base.${ext.uppercase()}" + + return Pair(lower, upper) + } } From 4d3a8db22a43ae35097bd31de38cb9e9e381e981 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:06:01 +0200 Subject: [PATCH 03/11] fix kt spotless Signed-off-by: alperozturk --- .../java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 2c78638e58b1..8b9638763a4c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -175,7 +175,7 @@ class FileUploadWorker( totalUploadSize = totalUploadSize ) - val result = upload(client,operation, user.get()) + val result = upload(client, operation, user.get()) currentUploadFileOperation = null fileUploaderDelegate.sendBroadcastUploadFinished( From 23cf035e953be6d671543906855d90dbc50ee2c5 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:12:28 +0200 Subject: [PATCH 04/11] use correct storage manager to prevent null oc file Signed-off-by: alperozturk --- .../java/com/nextcloud/client/di/AppModule.java | 2 +- .../client/jobs/operation/FileOperationHelper.kt | 16 ++++++++++------ .../client/jobs/upload/FileUploadWorker.kt | 3 ++- .../ui/activity/ConflictsResolveActivity.kt | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index 32a0150aa0f3..f0ab192287cd 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.java @@ -253,7 +253,7 @@ PassCodeManager passCodeManager(AppPreferences preferences, Clock clock) { @Provides FileOperationHelper fileOperationHelper(CurrentAccountProvider currentAccountProvider, Context context) { - return new FileOperationHelper(currentAccountProvider.getUser(), context, fileDataStorageManager(currentAccountProvider, context)); + return new FileOperationHelper(currentAccountProvider.getUser(), context); } @Provides diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 60439d800233..8d34dd3ddada 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -26,8 +26,7 @@ import kotlinx.coroutines.withContext class FileOperationHelper( private val user: User, - private val context: Context, - private val fileDataStorageManager: FileDataStorageManager + private val context: Context ) { companion object { private val TAG = FileOperationHelper::class.java.simpleName @@ -44,11 +43,15 @@ class FileOperationHelper( * with the lowercased version 0001.webp — in that case, this will return true. * ``` */ - fun isSameRemoteFileAlreadyPresent(remotePath: String, client: OwnCloudClient): Boolean { + fun isSameRemoteFileAlreadyPresent( + remotePath: String, + storageManager: FileDataStorageManager, + client: OwnCloudClient + ): Boolean { val (lc, uc) = FileUtil.getRemotePathVariants(remotePath) - val localOCFile = fileDataStorageManager.getFileByDecryptedRemotePath(lc) - ?: fileDataStorageManager.getFileByDecryptedRemotePath(uc) + val localOCFile = storageManager.getFileByDecryptedRemotePath(lc) + ?: storageManager.getFileByDecryptedRemotePath(uc) if (localOCFile != null && localOCFile.remotePath.equals(remotePath, ignoreCase = true)) { val remoteFile = getRemoteFile(remotePath, client) @@ -84,6 +87,7 @@ class FileOperationHelper( @Suppress("TooGenericExceptionCaught", "Deprecation") suspend fun removeFile( file: OCFile, + storageManager: FileDataStorageManager, onlyLocalCopy: Boolean, inBackground: Boolean, client: OwnCloudClient @@ -97,7 +101,7 @@ class FileOperationHelper( user, inBackground, context, - fileDataStorageManager + storageManager ) } val operationResult = operation.await() diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 8b9638763a4c..16a7f766b522 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -160,7 +160,8 @@ class FileUploadWorker( setWorkerState(user.get()) val operation = createUploadFileOperation(upload, user.get()) - if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload.remotePath, client)) { + val storageManager = FileDataStorageManager(user.get(), context.contentResolver) + if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload.remotePath, storageManager, client)) { uploadsStorageManager.removeUpload(upload.uploadId) continue } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index 05b49d622c0b..1ce11a71eed5 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -177,7 +177,8 @@ class ConflictsResolveActivity : lifecycleScope.launch(Dispatchers.IO) { val client = clientRepository.getOwncloudClient() ?: return@launch val isSuccess = fileOperationHelper.removeFile( - serverFile, + file = serverFile, + storageManager = fileDataStorageManager, onlyLocalCopy = false, inBackground = false, client = client From 5c2aa00bd22159372f47486c32e1bd7b740177c0 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:12:43 +0200 Subject: [PATCH 05/11] fix kt spotless Signed-off-by: alperozturk --- .../nextcloud/client/jobs/operation/FileOperationHelper.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 8d34dd3ddada..2188e48903b3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -24,10 +24,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext -class FileOperationHelper( - private val user: User, - private val context: Context -) { +class FileOperationHelper(private val user: User, private val context: Context) { companion object { private val TAG = FileOperationHelper::class.java.simpleName } From f9f26ab80c1d597f4d44ac96c9cd1ef7477c3196 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:14:11 +0200 Subject: [PATCH 06/11] get user correctly Signed-off-by: alperozturk --- .../client/jobs/upload/FileUploadWorker.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 16a7f766b522..cec5bce10736 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -132,12 +132,13 @@ class FileUploadWorker( val uploadIds = inputData.getLongArray(UPLOAD_IDS) ?: return Result.success() val uploads = uploadIds.map { id -> uploadsStorageManager.getUploadById(id) }.filterNotNull() val totalUploadSize = uploadIds.size - val user = userAccountManager.getUser(accountName) - if (!user.isPresent) { + val optionalUser = userAccountManager.getUser(accountName) + if (!optionalUser.isPresent) { return Result.failure() } + val user = optionalUser.get() val client = - OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.get().toOwnCloudAccount(), context) + OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context) for ((index, upload) in uploads.withIndex()) { if (preferences.isGlobalUploadPaused) { @@ -157,10 +158,9 @@ class FileUploadWorker( continue } - setWorkerState(user.get()) - - val operation = createUploadFileOperation(upload, user.get()) - val storageManager = FileDataStorageManager(user.get(), context.contentResolver) + setWorkerState(user) + val operation = createUploadFileOperation(upload, user) + val storageManager = FileDataStorageManager(user, context.contentResolver) if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload.remotePath, storageManager, client)) { uploadsStorageManager.removeUpload(upload.uploadId) continue @@ -176,7 +176,7 @@ class FileUploadWorker( totalUploadSize = totalUploadSize ) - val result = upload(client, operation, user.get()) + val result = upload(client, operation, user) currentUploadFileOperation = null fileUploaderDelegate.sendBroadcastUploadFinished( From d22acba021dbd1c34cad3b267e2beb2ad1610ef2 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:37:03 +0200 Subject: [PATCH 07/11] isSameFileOnRemote Signed-off-by: alperozturk --- .../jobs/operation/FileOperationHelper.kt | 21 +++++++++++++------ .../client/jobs/upload/FileUploadWorker.kt | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 2188e48903b3..01834dc24507 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -12,6 +12,7 @@ import com.nextcloud.client.account.User import com.nextcloud.utils.extensions.getErrorMessage import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation @@ -23,6 +24,7 @@ import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import java.io.File class FileOperationHelper(private val user: User, private val context: Context) { companion object { @@ -41,18 +43,16 @@ class FileOperationHelper(private val user: User, private val context: Context) * ``` */ fun isSameRemoteFileAlreadyPresent( - remotePath: String, + upload: OCUpload, storageManager: FileDataStorageManager, - client: OwnCloudClient ): Boolean { - val (lc, uc) = FileUtil.getRemotePathVariants(remotePath) + val (lc, uc) = FileUtil.getRemotePathVariants(upload.remotePath) val localOCFile = storageManager.getFileByDecryptedRemotePath(lc) ?: storageManager.getFileByDecryptedRemotePath(uc) - if (localOCFile != null && localOCFile.remotePath.equals(remotePath, ignoreCase = true)) { - val remoteFile = getRemoteFile(remotePath, client) - if (!isFileChanged(remoteFile, localOCFile)) { + if (localOCFile != null && localOCFile.remotePath.equals(upload.remotePath, ignoreCase = true)) { + if (isSameFileOnRemote(localOCFile, upload)) { Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") return true } @@ -61,6 +61,15 @@ class FileOperationHelper(private val user: User, private val context: Context) return false } + fun isSameFileOnRemote(ocFile: OCFile, upload: OCUpload): Boolean { + val localFile = File(upload.localPath) + if (!localFile.exists()) { + return false + } + val localSize: Long = localFile.length() + return ocFile.fileLength == localSize + } + @Suppress("DEPRECATION") fun getRemoteFile(remotePath: String, client: OwnCloudClient): RemoteFile? { val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index cec5bce10736..151fa65accb7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -161,7 +161,7 @@ class FileUploadWorker( setWorkerState(user) val operation = createUploadFileOperation(upload, user) val storageManager = FileDataStorageManager(user, context.contentResolver) - if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload.remotePath, storageManager, client)) { + if (fileOperationHelper.isSameRemoteFileAlreadyPresent(upload, storageManager)) { uploadsStorageManager.removeUpload(upload.uploadId) continue } From 0a7c139fe798503f20ba91ba9b3973e4a5188aa3 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 25 Jul 2025 12:40:20 +0200 Subject: [PATCH 08/11] fix kt spotless Signed-off-by: alperozturk --- .../nextcloud/client/jobs/operation/FileOperationHelper.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 01834dc24507..1a621435340d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -42,10 +42,7 @@ class FileOperationHelper(private val user: User, private val context: Context) * with the lowercased version 0001.webp — in that case, this will return true. * ``` */ - fun isSameRemoteFileAlreadyPresent( - upload: OCUpload, - storageManager: FileDataStorageManager, - ): Boolean { + fun isSameRemoteFileAlreadyPresent(upload: OCUpload, storageManager: FileDataStorageManager): Boolean { val (lc, uc) = FileUtil.getRemotePathVariants(upload.remotePath) val localOCFile = storageManager.getFileByDecryptedRemotePath(lc) From 14b14152d87917eb7dfee675ed99c0d37902fc36 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 30 Jul 2025 09:30:32 +0200 Subject: [PATCH 09/11] better naming Signed-off-by: alperozturk --- .../client/jobs/operation/FileOperationHelper.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 1a621435340d..321fca4b5b2b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -45,11 +45,11 @@ class FileOperationHelper(private val user: User, private val context: Context) fun isSameRemoteFileAlreadyPresent(upload: OCUpload, storageManager: FileDataStorageManager): Boolean { val (lc, uc) = FileUtil.getRemotePathVariants(upload.remotePath) - val localOCFile = storageManager.getFileByDecryptedRemotePath(lc) + val remoteFile = storageManager.getFileByDecryptedRemotePath(lc) ?: storageManager.getFileByDecryptedRemotePath(uc) - if (localOCFile != null && localOCFile.remotePath.equals(upload.remotePath, ignoreCase = true)) { - if (isSameFileOnRemote(localOCFile, upload)) { + if (remoteFile != null && remoteFile.remotePath.equals(upload.remotePath, ignoreCase = true)) { + if (isSameFileOnRemote(remoteFile, upload)) { Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") return true } @@ -58,13 +58,13 @@ class FileOperationHelper(private val user: User, private val context: Context) return false } - fun isSameFileOnRemote(ocFile: OCFile, upload: OCUpload): Boolean { + fun isSameFileOnRemote(remoteFile: OCFile, upload: OCUpload): Boolean { val localFile = File(upload.localPath) if (!localFile.exists()) { return false } val localSize: Long = localFile.length() - return ocFile.fileLength == localSize + return remoteFile.fileLength == localSize } @Suppress("DEPRECATION") From 10ab58b6e62b51c4fcd7224c01f2b6766e6f6381 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 30 Jul 2025 09:47:24 +0200 Subject: [PATCH 10/11] better check Signed-off-by: alperozturk --- .../client/jobs/operation/FileOperationHelper.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 321fca4b5b2b..62375bab8d5e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -48,11 +48,9 @@ class FileOperationHelper(private val user: User, private val context: Context) val remoteFile = storageManager.getFileByDecryptedRemotePath(lc) ?: storageManager.getFileByDecryptedRemotePath(uc) - if (remoteFile != null && remoteFile.remotePath.equals(upload.remotePath, ignoreCase = true)) { - if (isSameFileOnRemote(remoteFile, upload)) { - Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") - return true - } + if (remoteFile != null && isSameFileOnRemote(remoteFile, upload)) { + Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") + return true } return false From 3b6f42e6c44624b0f9a866a9b608156b80d986bd Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 30 Jul 2025 10:15:17 +0200 Subject: [PATCH 11/11] better check Signed-off-by: alperozturk --- .../jobs/operation/FileOperationHelper.kt | 18 +++++------------- .../utils/extensions/OCUploadExtensions.kt | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt index 62375bab8d5e..dbcc2842f8b2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.jobs.operation import android.content.Context import com.nextcloud.client.account.User import com.nextcloud.utils.extensions.getErrorMessage +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.db.OCUpload @@ -24,7 +25,6 @@ import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import java.io.File class FileOperationHelper(private val user: User, private val context: Context) { companion object { @@ -45,10 +45,11 @@ class FileOperationHelper(private val user: User, private val context: Context) fun isSameRemoteFileAlreadyPresent(upload: OCUpload, storageManager: FileDataStorageManager): Boolean { val (lc, uc) = FileUtil.getRemotePathVariants(upload.remotePath) - val remoteFile = storageManager.getFileByDecryptedRemotePath(lc) - ?: storageManager.getFileByDecryptedRemotePath(uc) + val remoteFile = storageManager.run { + getFileByDecryptedRemotePath(lc) ?: getFileByDecryptedRemotePath(uc) + } - if (remoteFile != null && isSameFileOnRemote(remoteFile, upload)) { + if (upload.toFile()?.length() == remoteFile?.fileLength) { Log_OC.w(TAG, "Same file already exists due to lowercase/uppercase extension") return true } @@ -56,15 +57,6 @@ class FileOperationHelper(private val user: User, private val context: Context) return false } - fun isSameFileOnRemote(remoteFile: OCFile, upload: OCUpload): Boolean { - val localFile = File(upload.localPath) - if (!localFile.exists()) { - return false - } - val localSize: Long = localFile.length() - return remoteFile.fileLength == localSize - } - @Suppress("DEPRECATION") fun getRemoteFile(remotePath: String, client: OwnCloudClient): RemoteFile? { val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt index 6b2550df1e60..3e6bcf163058 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt @@ -8,7 +8,22 @@ package com.nextcloud.utils.extensions import com.owncloud.android.db.OCUpload +import java.io.File fun List.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() fun Array.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() + +@Suppress("ReturnCount") +fun OCUpload.toFile(): File? { + if (localPath.isNullOrEmpty()) { + return null + } + + val result = File(localPath) + if (!result.exists()) { + return null + } + + return result +}