From 0f666b21cbf225b482eb409fdecc66e8e7cfc345 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 11:54:16 +0100 Subject: [PATCH 1/6] fix(auto-upload): file detection Signed-off-by: alperozturk96 --- .../client/database/dao/FileSystemDao.kt | 4 + .../client/database/dao/UploadDao.kt | 4 +- .../client/jobs/BackgroundJobFactory.kt | 2 +- .../jobs/autoUpload/AutoUploadWorker.kt | 103 +----------------- .../jobs/autoUpload/FileSystemRepository.kt | 54 ++++++--- .../jobs/autoUpload/SyncFolderHelper.kt | 102 +++++++++++++++++ .../client/jobs/upload/FileUploadHelper.kt | 2 +- 7 files changed, 153 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt index 91a03044afde..af363b743283 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt @@ -8,6 +8,7 @@ package com.nextcloud.client.database.dao import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -19,6 +20,9 @@ interface FileSystemDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(filesystemEntity: FilesystemEntity) + @Delete + fun delete(entity: FilesystemEntity) + @Query( """ DELETE FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt index 7f09f4aa0543..2e1d86d726d5 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -41,7 +41,7 @@ interface UploadDao { "WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName " + "AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath" ) - fun deleteByAccountAndRemotePath(remotePath: String, accountName: String) + fun deleteByRemotePathAndAccountName(remotePath: String, accountName: String) @Query( "SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + @@ -51,7 +51,7 @@ interface UploadDao { ) fun getUploadById(id: Long, accountName: String): UploadEntity? - @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(entity: UploadEntity): Long @Query( 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 4b1dd1d659b2..8b6012c0e8db 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -179,7 +179,7 @@ class BackgroundJobFactory @Inject constructor( powerManagementService = powerManagementService, syncedFolderProvider = syncedFolderProvider, backgroundJobManager = backgroundJobManager.get(), - repository = FileSystemRepository(dao = database.fileSystemDao(), context), + repository = FileSystemRepository(dao = database.fileSystemDao(), uploadsStorageManager, context), viewThemeUtils = viewThemeUtils.get(), localBroadcastManager = localBroadcastManager.get() ) 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 af38b417a2b5..f2af22abf14e 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 @@ -9,8 +9,6 @@ package com.nextcloud.client.jobs.autoUpload import android.app.Notification import android.content.Context -import android.content.res.Resources -import androidx.exifinterface.media.ExifInterface import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo @@ -25,13 +23,11 @@ import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService -import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.utils.extensions.isNonRetryable import com.nextcloud.utils.extensions.updateStatus import com.owncloud.android.R import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.datamodel.FileDataStorageManager -import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager @@ -44,16 +40,10 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.SettingsActivity -import com.owncloud.android.utils.FileStorageUtils -import com.owncloud.android.utils.MimeType import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -import java.text.ParsePosition -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone @Suppress("LongParameterList", "TooManyFunctions", "TooGenericExceptionCaught") class AutoUploadWorker( @@ -79,6 +69,7 @@ class AutoUploadWorker( } private val helper = AutoUploadHelper() + private val syncFolderHelper = SyncFolderHelper(context) private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager) private lateinit var syncedFolder: SyncedFolder private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID) @@ -233,13 +224,6 @@ class AutoUploadWorker( Log_OC.d(TAG, "Exception collectFileChangesFromContentObserverWork: $e") } - private fun prepareDateFormat(): SimpleDateFormat { - val currentLocale = context.resources.configuration.locales[0] - return SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale).apply { - timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) - } - } - private fun getUserOrReturn(syncedFolder: SyncedFolder): User? { val optionalUser = userAccountManager.getUser(syncedFolder.account) if (!optionalUser.isPresent) { @@ -274,13 +258,10 @@ class AutoUploadWorker( @Suppress("LongMethod", "DEPRECATION", "TooGenericExceptionCaught") private suspend fun uploadFiles(syncedFolder: SyncedFolder) = withContext(Dispatchers.IO) { - val dateFormat = prepareDateFormat() val user = getUserOrReturn(syncedFolder) ?: return@withContext val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) val client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(ocAccount, context) - val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light) - val currentLocale = context.resources.configuration.locales[0] trySetForeground() updateNotification() @@ -299,14 +280,7 @@ class AutoUploadWorker( filePathsWithIds.forEachIndexed { batchIndex, (path, id) -> val file = File(path) val localPath = file.absolutePath - val remotePath = getRemotePath( - file, - syncedFolder, - dateFormat, - lightVersion, - context.resources, - currentLocale - ) + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, file) try { val entityResult = getEntityResult(user, localPath, remotePath) @@ -471,79 +445,6 @@ class AutoUploadWorker( FileDataStorageManager(user, context.contentResolver) ) - private fun getRemotePath( - file: File, - syncedFolder: SyncedFolder, - sFormatter: SimpleDateFormat, - lightVersion: Boolean, - resources: Resources, - currentLocale: Locale - ): String { - val lastModificationTime = calculateLastModificationTime(file, syncedFolder, sFormatter) - - val (remoteFolder, useSubfolders, subFolderRule) = if (lightVersion) { - Triple( - resources.getString(R.string.syncedFolder_remote_folder), - resources.getBoolean(R.bool.syncedFolder_light_use_subfolders), - SubFolderRule.YEAR_MONTH - ) - } else { - Triple( - syncedFolder.remotePath, - syncedFolder.isSubfolderByDate, - syncedFolder.subfolderRule - ) - } - - return FileStorageUtils.getInstantUploadFilePath( - file, - currentLocale, - remoteFolder, - syncedFolder.localPath, - lastModificationTime, - useSubfolders, - subFolderRule - ) - } - - private fun hasExif(file: File): Boolean { - val mimeType = FileStorageUtils.getMimeTypeFromName(file.absolutePath) - return MimeType.JPEG.equals(mimeType, ignoreCase = true) || MimeType.TIFF.equals(mimeType, ignoreCase = true) - } - - @Suppress("NestedBlockDepth") - private fun calculateLastModificationTime( - file: File, - syncedFolder: SyncedFolder, - formatter: SimpleDateFormat - ): Long { - var lastModificationTime = file.lastModified() - if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) { - Log_OC.d(TAG, "calculateLastModificationTime exif found") - - @Suppress("TooGenericExceptionCaught") - try { - val exifInterface = ExifInterface(file.absolutePath) - val exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME) - if (!exifDate.isNullOrBlank()) { - val pos = ParsePosition(0) - val dateTime = formatter.parse(exifDate, pos) - if (dateTime != null) { - lastModificationTime = dateTime.time - Log_OC.w(TAG, "calculateLastModificationTime calculatedTime is: $lastModificationTime") - } else { - Log_OC.w(TAG, "calculateLastModificationTime dateTime is empty") - } - } else { - Log_OC.w(TAG, "calculateLastModificationTime exifDate is empty") - } - } catch (e: Exception) { - Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage) - } - } - return lastModificationTime - } - private fun sendUploadFinishEvent(operation: UploadFileOperation, result: RemoteOperationResult<*>) { fileUploadBroadcastManager.sendFinished( operation, diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index f9c12b36b9a1..b50da8c76b2c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -15,19 +15,37 @@ import com.nextcloud.client.database.entity.FilesystemEntity import com.nextcloud.utils.extensions.shouldSkipFile import com.nextcloud.utils.extensions.toFile import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.SyncedFolderUtils import java.io.File import java.util.zip.CRC32 @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "MagicNumber", "ReturnCount") -class FileSystemRepository(private val dao: FileSystemDao, private val context: Context) { +class FileSystemRepository( + private val dao: FileSystemDao, + private val uploadsStorageManager: UploadsStorageManager, + private val context: Context +) { + private val syncFolderHelper = SyncFolderHelper(context) companion object { private const val TAG = "FilesystemRepository" const val BATCH_SIZE = 50 } + fun deleteAutoUploadEntityAndUploadEntity(syncedFolder: SyncedFolder, localPath: String, entity: FilesystemEntity) { + Log_OC.d(TAG, "deleting auto upload entity and upload entity") + + val file = File(localPath) + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, file) + uploadsStorageManager.uploadDao.deleteByRemotePathAndAccountName( + remotePath = remotePath, + accountName = syncedFolder.account + ) + dao.delete(entity) + } + suspend fun deleteByLocalPathAndId(path: String, id: Int) { dao.deleteByLocalPathAndId(path, id) } @@ -39,20 +57,23 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: val entities = dao.getAutoUploadFilesEntities(syncedFolderId, BATCH_SIZE, lastId) val filtered = mutableListOf>() - entities.forEach { - it.localPath?.let { path -> + entities.forEach { entity -> + entity.localPath?.let { path -> val file = File(path) if (!file.exists()) { Log_OC.w(TAG, "Ignoring file for upload (doesn't exist): $path") + deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isQualifiedFolder(file.parent)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified folder): $path") + deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.name)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified file): $path") + deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) } else { Log_OC.d(TAG, "Adding path to upload: $path") - if (it.id != null) { - filtered.add(path to it.id) + if (entity.id != null) { + filtered.add(path to entity.id) } else { Log_OC.w(TAG, "cant adding path to upload, id is null") } @@ -160,22 +181,29 @@ class FileSystemRepository(private val dao: FileSystemDao, private val context: } val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString()) - val fileSentForUpload = (entity != null && entity.fileSentForUpload == 1) - if (fileSentForUpload) { - Log_OC.d(TAG, "File was sent for upload, checking if it changed...") - } val fileModified = (lastModified ?: file.lastModified()) - if (syncedFolder.shouldSkipFile(file, fileModified, creationTime, fileSentForUpload)) { + if (fileModified <= 0L) { + Log_OC.d(TAG, "file is deleted, skipping: $localPath") + entity?.let { + deleteAutoUploadEntityAndUploadEntity(syncedFolder, localPath, entity) + } return } - if (fileSentForUpload) { - Log_OC.d(TAG, "File was sent for upload before but has changed, will re-upload: $localPath") + val hasNotChanged = entity?.fileModified == fileModified + val fileSentForUpload = entity?.fileSentForUpload == 1 + + if (hasNotChanged && fileSentForUpload) { + Log_OC.d(TAG, "File hasn't changed since last scan. skipping: $localPath") + return } - val crc = getFileChecksum(file) + if (syncedFolder.shouldSkipFile(file, fileModified, creationTime, fileSentForUpload)) { + return + } + val crc = getFileChecksum(file) val newEntity = FilesystemEntity( id = entity?.id, localPath = localPath, diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt new file mode 100644 index 000000000000..6100487f6210 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -0,0 +1,102 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.content.Context +import androidx.exifinterface.media.ExifInterface +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.R +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.MimeType +import java.io.File +import java.text.ParsePosition +import java.text.SimpleDateFormat +import java.util.TimeZone + +class SyncFolderHelper(context: Context) { + + private val resources = context.resources + private val isLightVersion = resources.getBoolean(R.bool.syncedFolder_light) + + companion object { + private const val TAG = "SyncFolderHelper" + } + + fun getAutoUploadRemotePath(syncedFolder: SyncedFolder, file: File): String { + val lastModificationTime = calculateLastModificationTime(file, syncedFolder) + + val remoteFolder: String + val useSubfolders: Boolean + val subFolderRule: SubFolderRule + + if (isLightVersion) { + remoteFolder = resources.getString(R.string.syncedFolder_remote_folder) + useSubfolders = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders) + subFolderRule = SubFolderRule.YEAR_MONTH + } else { + remoteFolder = syncedFolder.remotePath + useSubfolders = syncedFolder.isSubfolderByDate + subFolderRule = syncedFolder.subfolderRule + } + + return FileStorageUtils.getInstantUploadFilePath( + file, + resources.configuration.locales[0], + remoteFolder, + syncedFolder.localPath, + lastModificationTime, + useSubfolders, + subFolderRule + ) + } + + @Suppress("NestedBlockDepth") + private fun calculateLastModificationTime(file: File, syncedFolder: SyncedFolder): Long { + val currentLocale = resources.configuration.locales[0] + val formatter = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale).apply { + timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) + } + var lastModificationTime = file.lastModified() + if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) { + Log_OC.d(TAG, "calculateLastModificationTime exif found") + + @Suppress("TooGenericExceptionCaught") + try { + val exifInterface = ExifInterface(file.absolutePath) + val exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME) + if (!exifDate.isNullOrBlank()) { + val pos = ParsePosition(0) + val dateTime = formatter.parse(exifDate, pos) + if (dateTime != null) { + lastModificationTime = dateTime.time + Log_OC.w( + TAG, + "calculateLastModificationTime calculatedTime is: $lastModificationTime" + ) + } else { + Log_OC.w(TAG, "calculateLastModificationTime dateTime is empty") + } + } else { + Log_OC.w(TAG, "calculateLastModificationTime exifDate is empty") + } + } catch (e: Exception) { + Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage) + } + } + return lastModificationTime + } + + private fun hasExif(file: File): Boolean { + val mimeType = FileStorageUtils.getMimeTypeFromName(file.absolutePath) + return mimeType.equals(MimeType.JPEG, ignoreCase = true) || + mimeType.equals(MimeType.TIFF, ignoreCase = 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 809d24c8e66d..5ef42d5aa2ea 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 @@ -261,7 +261,7 @@ class FileUploadHelper { } fun removeFileUpload(remotePath: String, accountName: String) { - uploadsStorageManager.uploadDao.deleteByAccountAndRemotePath(remotePath, accountName) + uploadsStorageManager.uploadDao.deleteByRemotePathAndAccountName(remotePath, accountName) } fun updateUploadStatus(remotePath: String, accountName: String, status: UploadStatus) { From 60f734bb44c01f6a090f58218ca6d8fc4f266ee4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 11:55:22 +0100 Subject: [PATCH 2/6] fix(auto-upload): file detection Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt index 6100487f6210..bf0d549774b8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -47,7 +47,7 @@ class SyncFolderHelper(context: Context) { subFolderRule = syncedFolder.subfolderRule } - return FileStorageUtils.getInstantUploadFilePath( + val result = FileStorageUtils.getInstantUploadFilePath( file, resources.configuration.locales[0], remoteFolder, @@ -56,6 +56,10 @@ class SyncFolderHelper(context: Context) { useSubfolders, subFolderRule ) + + Log_OC.d(TAG, "auto upload remote path: $result") + + return result } @Suppress("NestedBlockDepth") From 372e78eb96f7e4e2bc6cd36374dead5ad2bba6bb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 11:56:36 +0100 Subject: [PATCH 3/6] fix(auto-upload): file detection Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt index bf0d549774b8..990ad440990c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -21,16 +21,15 @@ import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.TimeZone -class SyncFolderHelper(context: Context) { - - private val resources = context.resources - private val isLightVersion = resources.getBoolean(R.bool.syncedFolder_light) +class SyncFolderHelper(private val context: Context) { companion object { private const val TAG = "SyncFolderHelper" } fun getAutoUploadRemotePath(syncedFolder: SyncedFolder, file: File): String { + val resources = context.resources + val isLightVersion = resources.getBoolean(R.bool.syncedFolder_light) val lastModificationTime = calculateLastModificationTime(file, syncedFolder) val remoteFolder: String @@ -64,6 +63,7 @@ class SyncFolderHelper(context: Context) { @Suppress("NestedBlockDepth") private fun calculateLastModificationTime(file: File, syncedFolder: SyncedFolder): Long { + val resources = context.resources val currentLocale = resources.configuration.locales[0] val formatter = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale).apply { timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) From 7a8e90a182022e088a5d0f530e28312cd4a8e82a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 13:28:47 +0100 Subject: [PATCH 4/6] fix(auto-upload): file lock unlock Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 9 ++- .../operations/UploadFileOperation.java | 81 +++++++++++++++---- 2 files changed, 73 insertions(+), 17 deletions(-) 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 5ef42d5aa2ea..9478850e1be0 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 @@ -19,9 +19,9 @@ import com.nextcloud.client.device.BatteryStatus import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation -import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.utils.extensions.getUploadIds import com.owncloud.android.MainApp import com.owncloud.android.R @@ -479,7 +479,12 @@ class FileUploadHelper { } @Suppress("MagicNumber") - fun isSameFileOnRemote(user: User, localFile: File, remotePath: String, context: Context): Boolean { + fun isSameFileOnRemote(user: User?, localFile: File?, remotePath: String?, context: Context?): Boolean { + if (user == null || localFile == null || remotePath == null || context == null) { + Log_OC.e(TAG,"cannot compare remote and local file") + return false + } + // Compare remote file to local file val localLastModifiedTimestamp = localFile.lastModified() / 1000 // remote file timestamp in milli not micro sec val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) 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 fa8b2a13c72a..652b4bb24c7f 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1011,6 +1011,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { File expectedFile = null; FileLock fileLock = null; FileChannel channel = null; + FileInputStream fileInputStream = null; long size; @@ -1043,9 +1044,19 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { final Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile); try { - channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel(); - fileLock = channel.tryLock(); + fileInputStream = new FileInputStream(mFile.getStoragePath()); + channel = fileInputStream.getChannel(); + try { + // request a shared lock instead of exclusive one, since we are just reading file + fileLock = channel.tryLock(0L, Long.MAX_VALUE, true); + Log_OC.d(TAG ,"file locked"); + } catch (OverlappingFileLockException e) { + // if another thread has the lock, current thread can still read the file. + Log_OC.e(TAG, "shared lock overlap detected; proceeding safely."); + } } catch (FileNotFoundException e) { + Log_OC.e(TAG, "file not found exception: normal upload"); + // this basically means that the file is on SD card // try to copy file to temporary dir if it doesn't exist String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + @@ -1058,8 +1069,13 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { if (result.isSuccess()) { if (temporalFile.length() == originalFile.length()) { - channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel(); - fileLock = channel.tryLock(); + try { + fileInputStream = new FileInputStream(temporalFile.getAbsolutePath()); + channel = fileInputStream.getChannel(); + fileLock = channel.tryLock(0L, Long.MAX_VALUE, true); + } catch (OverlappingFileLockException ex) { + Log_OC.e(TAG, "shared lock overlap detected; proceeding safely."); + } } else { result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); } @@ -1067,7 +1083,11 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { } try { - size = channel.size(); + if (channel != null && channel.isOpen()) { + size = channel.size(); + } else { + size = new File(mFile.getStoragePath()).length(); + } } catch (Exception exception) { Log_OC.e(TAG, "normalUpload, size cannot be determined from channel: " + exception); size = new File(mFile.getStoragePath()).length(); @@ -1121,28 +1141,42 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { } catch (FileNotFoundException e) { Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore"); result = new RemoteOperationResult<>(ResultCode.LOCAL_FILE_NOT_FOUND); - } catch (OverlappingFileLockException e) { - Log_OC.d(TAG, "Overlapping file lock exception"); - result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); } catch (Exception e) { result = new RemoteOperationResult<>(e); } finally { mUploadStarted.set(false); - if (fileLock != null) { + if (fileLock != null && fileLock.isValid()) { try { fileLock.release(); - } catch (IOException e) { - Log_OC.e(TAG, "Failed to unlock file with path " + mOriginalStoragePath); + Log_OC.d(TAG ,"file lock released"); + } catch (IOException ignored) { + Log_OC.e(TAG, "failed to unlock file with path " + mOriginalStoragePath); } + } else { + Log_OC.e(TAG, "file lock is null"); } if (channel != null) { try { channel.close(); - } catch (IOException e) { - Log_OC.w(TAG, "Failed to close file channel"); + Log_OC.d(TAG ,"file channel closed"); + } catch (IOException ignored) { + Log_OC.e(TAG, "failed to close file channel"); } + } else { + Log_OC.e(TAG, "channel is null"); + } + + if (fileInputStream != null) { + try { + fileInputStream.close(); + Log_OC.d(TAG ,"file input stream closed"); + } catch (IOException ignored) { + Log_OC.e(TAG, "failed to close file input stream"); + } + } else { + Log_OC.e(TAG, "file input stream is null"); } if (temporalFile != null && !originalFile.equals(temporalFile)) { @@ -1248,7 +1282,24 @@ private RemoteOperationResult checkNameCollision(OCFile parentFile, break; case ASK_USER: Log_OC.d(TAG, "Name collision; asking the user what to do"); - return new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + + // check if its real SYNC_CONFLICT + boolean isSameFileOnRemote = false; + if (mFile != null) { + String localPath = mFile.getStoragePath(); + + if (localPath != null) { + File localFile = new File(localPath); + isSameFileOnRemote = FileUploadHelper.Companion.instance() + .isSameFileOnRemote(user, localFile, mRemotePath, mContext); + } + } + + if (isSameFileOnRemote) { + return new RemoteOperationResult<>(ResultCode.OK); + } else { + return new RemoteOperationResult<>(ResultCode.SYNC_CONFLICT); + } } } @@ -1474,7 +1525,7 @@ private static boolean existsFile(OwnCloudClient client, return false; } else { ExistenceCheckRemoteOperation existsOperation = new ExistenceCheckRemoteOperation(remotePath, false); - RemoteOperationResult result = existsOperation.execute(client); + final var result = existsOperation.execute(client); return result.isSuccess(); } } From 6498ece4d56fdc361269fa7c2bef190b134a9614 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 13:32:50 +0100 Subject: [PATCH 5/6] fix codacy Signed-off-by: alperozturk96 --- .../com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt | 2 +- .../java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt index 990ad440990c..75896f372d0b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -46,7 +46,7 @@ class SyncFolderHelper(private val context: Context) { subFolderRule = syncedFolder.subfolderRule } - val result = FileStorageUtils.getInstantUploadFilePath( + val result = FileStorageUtils.getInstantUploadFilePath( file, resources.configuration.locales[0], remoteFolder, 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 9478850e1be0..5bb40b30969d 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 @@ -478,10 +478,10 @@ class FileUploadHelper { } } - @Suppress("MagicNumber") + @Suppress("MagicNumber", "ReturnCount", "ComplexCondition") fun isSameFileOnRemote(user: User?, localFile: File?, remotePath: String?, context: Context?): Boolean { if (user == null || localFile == null || remotePath == null || context == null) { - Log_OC.e(TAG,"cannot compare remote and local file") + Log_OC.e(TAG, "cannot compare remote and local file") return false } From a15d1c4bb8d46266aa9a3c9e4e6feb0e58b6cadb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 6 Feb 2026 14:26:45 +0100 Subject: [PATCH 6/6] fix test Signed-off-by: alperozturk96 --- .../client/jobs/autoUpload/FileSystemRepository.kt | 10 +++++----- .../android/operations/UploadFileOperation.java | 12 ++++++------ .../owncloud/android/utils/AutoUploadHelperTest.kt | 4 +++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index b50da8c76b2c..d4804134f466 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -34,7 +34,7 @@ class FileSystemRepository( const val BATCH_SIZE = 50 } - fun deleteAutoUploadEntityAndUploadEntity(syncedFolder: SyncedFolder, localPath: String, entity: FilesystemEntity) { + fun deleteAutoUploadAndUploadEntity(syncedFolder: SyncedFolder, localPath: String, entity: FilesystemEntity) { Log_OC.d(TAG, "deleting auto upload entity and upload entity") val file = File(localPath) @@ -62,13 +62,13 @@ class FileSystemRepository( val file = File(path) if (!file.exists()) { Log_OC.w(TAG, "Ignoring file for upload (doesn't exist): $path") - deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) + deleteAutoUploadAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isQualifiedFolder(file.parent)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified folder): $path") - deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) + deleteAutoUploadAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.name)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified file): $path") - deleteAutoUploadEntityAndUploadEntity(syncedFolder, path, entity) + deleteAutoUploadAndUploadEntity(syncedFolder, path, entity) } else { Log_OC.d(TAG, "Adding path to upload: $path") @@ -186,7 +186,7 @@ class FileSystemRepository( if (fileModified <= 0L) { Log_OC.d(TAG, "file is deleted, skipping: $localPath") entity?.let { - deleteAutoUploadEntityAndUploadEntity(syncedFolder, localPath, entity) + deleteAutoUploadAndUploadEntity(syncedFolder, localPath, entity) } return } 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 652b4bb24c7f..1f4a6b611fa7 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1049,7 +1049,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { try { // request a shared lock instead of exclusive one, since we are just reading file fileLock = channel.tryLock(0L, Long.MAX_VALUE, true); - Log_OC.d(TAG ,"file locked"); + Log_OC.d(TAG ,"🔒" + "file locked"); } catch (OverlappingFileLockException e) { // if another thread has the lock, current thread can still read the file. Log_OC.e(TAG, "shared lock overlap detected; proceeding safely."); @@ -1089,7 +1089,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { size = new File(mFile.getStoragePath()).length(); } } catch (Exception exception) { - Log_OC.e(TAG, "normalUpload, size cannot be determined from channel: " + exception); + Log_OC.e(TAG, "size cannot be determined from channel: " + exception); size = new File(mFile.getStoragePath()).length(); } @@ -1149,7 +1149,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { if (fileLock != null && fileLock.isValid()) { try { fileLock.release(); - Log_OC.d(TAG ,"file lock released"); + Log_OC.d(TAG ,"🔓" + "file lock released"); } catch (IOException ignored) { Log_OC.e(TAG, "failed to unlock file with path " + mOriginalStoragePath); } @@ -1160,7 +1160,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { if (channel != null) { try { channel.close(); - Log_OC.d(TAG ,"file channel closed"); + Log_OC.d(TAG ,"📢" + "file channel closed"); } catch (IOException ignored) { Log_OC.e(TAG, "failed to close file channel"); } @@ -1171,7 +1171,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { if (fileInputStream != null) { try { fileInputStream.close(); - Log_OC.d(TAG ,"file input stream closed"); + Log_OC.d(TAG ,"📝" + "file input stream closed"); } catch (IOException ignored) { Log_OC.e(TAG, "failed to close file input stream"); } @@ -1181,7 +1181,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { if (temporalFile != null && !originalFile.equals(temporalFile)) { boolean isTempFileDeleted = temporalFile.delete(); - Log_OC.d(TAG, "normalUpload, temp folder deletion: " + isTempFileDeleted); + Log_OC.d(TAG, "temp folder deletion: " + isTempFileDeleted); } if (result == null) { diff --git a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt index a7aff50a8cc3..5eb368139729 100644 --- a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt @@ -15,6 +15,7 @@ import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.utils.extensions.shouldSkipFile import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.UploadsStorageManager import io.mockk.clearAllMocks import io.mockk.mockk import org.junit.After @@ -36,6 +37,7 @@ class AutoUploadHelperTest { private val mockContext: Context = mockk(relaxed = true) private lateinit var repo: FileSystemRepository + private val mockUploadsStorageManager: UploadsStorageManager = mockk(relaxed = true) @Before fun setup() { @@ -43,7 +45,7 @@ class AutoUploadHelperTest { tempDir.mkdirs() assertTrue("Failed to create temp directory", tempDir.exists()) - repo = FileSystemRepository(mockDao, mockContext) + repo = FileSystemRepository(mockDao, mockUploadsStorageManager, mockContext) } @After