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..01d78e1d9959 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) @@ -102,7 +93,11 @@ class AutoUploadWorker( collectFileChangesFromContentObserverWork(contentUris) uploadFiles(syncedFolder) - Log_OC.d(TAG, "✅ ${syncedFolder.remotePath} finished checking files.") + // only update last scan time after uploading files + syncedFolder.lastScanTimestampMs = System.currentTimeMillis() + syncedFolderProvider.updateSyncFolder(syncedFolder) + + Log_OC.d(TAG, "✅ ${syncedFolder.remotePath} completed") Result.success() } catch (e: Exception) { Log_OC.e(TAG, "❌ failed: ${e.message}") @@ -226,20 +221,11 @@ class AutoUploadWorker( helper.insertEntries(syncedFolder, repository) } } - syncedFolder.lastScanTimestampMs = System.currentTimeMillis() - syncedFolderProvider.updateSyncFolder(syncedFolder) } } catch (e: Exception) { 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 +260,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 +282,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) @@ -337,7 +313,6 @@ class AutoUploadWorker( val result = operation.execute(client) fileUploadBroadcastManager.sendStarted(operation, context) - uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess) UploadErrorNotificationManager.handleResult( context, @@ -471,79 +446,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..17c3bddb4806 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 deleteAutoUploadAndUploadEntity(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") + deleteAutoUploadAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isQualifiedFolder(file.parent)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified folder): $path") + deleteAutoUploadAndUploadEntity(syncedFolder, path, entity) } else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.name)) { Log_OC.w(TAG, "Ignoring file for upload (unqualified file): $path") + deleteAutoUploadAndUploadEntity(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,21 @@ 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)) { + 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 } - if (fileSentForUpload) { - Log_OC.d(TAG, "File was sent for upload before but has changed, will re-upload: $localPath") + 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..75896f372d0b --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -0,0 +1,106 @@ +/* + * 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(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 + 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 + } + + val result = FileStorageUtils.getInstantUploadFilePath( + file, + resources.configuration.locales[0], + remoteFolder, + syncedFolder.localPath, + lastModificationTime, + useSubfolders, + subFolderRule + ) + + Log_OC.d(TAG, "auto upload remote path: $result") + + return result + } + + @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) + } + 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..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 @@ -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 @@ -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) { @@ -478,8 +478,13 @@ class FileUploadHelper { } } - @Suppress("MagicNumber") - fun isSameFileOnRemote(user: User, localFile: File, remotePath: String, context: Context): Boolean { + @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") + 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/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index ea0bac258db8..e64ca25ca296 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 @@ -256,10 +256,8 @@ class FileUploadWorker( ) val result = withContext(Dispatchers.IO) { - upload(operation, user, client) + upload(upload, operation, user, client) } - val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName) - uploadsStorageManager.updateStatus(entity, result.isSuccess) currentUploadFileOperation = null if (result.code == ResultCode.QUOTA_EXCEEDED) { @@ -330,6 +328,7 @@ class FileUploadWorker( @Suppress("TooGenericExceptionCaught", "DEPRECATION") private suspend fun upload( + upload: OCUpload, operation: UploadFileOperation, user: User, client: OwnCloudClient @@ -346,10 +345,17 @@ class FileUploadWorker( fileUploadBroadcastManager.sendStarted(operation, context) } catch (e: Exception) { Log_OC.e(TAG, "Error uploading", e) - result = RemoteOperationResult(e) + uploadsStorageManager.run { + uploadDao.getUploadById(upload.uploadId, user.accountName)?.let { entity -> + updateStatus( + entity, + UploadsStorageManager.UploadStatus.UPLOAD_FAILED + ) + } + } + result = RemoteOperationResult(e) } finally { if (!isStopped) { - uploadsStorageManager.updateDatabaseUploadResult(result, operation) UploadErrorNotificationManager.handleResult( context, notificationManager, diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt index 21aa3619b85d..efe129f8d765 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt @@ -78,7 +78,6 @@ class UploadTask( val client = clientProvider() uploadsStorageManager.updateDatabaseUploadStart(op) val result = op.execute(client) - uploadsStorageManager.updateDatabaseUploadResult(result, op) return Result(file, result.isSuccess) } } diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 1b979f23a433..f46ed83f14e8 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -392,7 +392,7 @@ public void disableDocumentsStorageProvider() { FilesSyncHelper.startAutoUploadForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager, new String[]{}, - true); + false); preferences.setLastAutoUploadOnStartTime(System.currentTimeMillis()); } } else if (event == Lifecycle.Event.ON_STOP) { diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index b339655b438c..244f87cecfe2 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -550,59 +550,53 @@ public void clearSuccessfulUploads() { } } - /** - * Updates the persistent upload database with upload result. - */ public void updateDatabaseUploadResult(RemoteOperationResult uploadResult, UploadFileOperation upload) { - // result: success or fail notification Log_OC.d(TAG, "updateDatabaseUploadResult uploadResult: " + uploadResult + " upload: " + upload); if (uploadResult.isCancelled()) { - removeUpload( - upload.getUser().getAccountName(), - upload.getRemotePath() - ); - } else { - String localPath = (FileUploadWorker.LOCAL_BEHAVIOUR_MOVE == upload.getLocalBehaviour()) - ? upload.getStoragePath() : null; - - if (uploadResult.isSuccess()) { - updateUploadStatus( - upload.getOCUploadId(), - UploadStatus.UPLOAD_SUCCEEDED, - UploadResult.UPLOADED, - upload.getRemotePath(), - localPath - ); - } else if (uploadResult.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT && - new FileUploadHelper().isSameFileOnRemote( - upload.getUser(), new File(upload.getStoragePath()), upload.getRemotePath(), upload.getContext())) { - - updateUploadStatus( - upload.getOCUploadId(), - UploadStatus.UPLOAD_SUCCEEDED, - UploadResult.SAME_FILE_CONFLICT, - upload.getRemotePath(), - localPath - ); - } else if (uploadResult.getCode() == RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND) { - updateUploadStatus( - upload.getOCUploadId(), - UploadStatus.UPLOAD_SUCCEEDED, - UploadResult.FILE_NOT_FOUND, - upload.getRemotePath(), - localPath - ); - } else if (uploadResult.getCode() != RemoteOperationResult.ResultCode.USER_CANCELLED){ - updateUploadStatus( - upload.getOCUploadId(), - UploadStatus.UPLOAD_FAILED, - UploadResult.fromOperationResult(uploadResult), - upload.getRemotePath(), - localPath - ); + Log_OC.w(TAG, "upload is cancelled, removing upload"); + removeUpload(upload.getUser().getAccountName(), upload.getRemotePath()); + return; + } + + String localPath = (upload.getLocalBehaviour() == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE) + ? upload.getStoragePath() : null; + + + Log_OC.d(TAG, "local behaviour: " + upload.getLocalBehaviour()); + Log_OC.d(TAG, "local path of upload: " + localPath); + + UploadStatus status = UploadStatus.UPLOAD_FAILED; + UploadResult result = UploadResult.fromOperationResult(uploadResult); + RemoteOperationResult.ResultCode code = uploadResult.getCode(); + + if (uploadResult.isSuccess()) { + status = UploadStatus.UPLOAD_SUCCEEDED; + result = UploadResult.UPLOADED; + } else if (code == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { + boolean isSame = new FileUploadHelper().isSameFileOnRemote( + upload.getUser(), new File(upload.getStoragePath()), upload.getRemotePath(), upload.getContext()); + + if (isSame) { + result = UploadResult.SAME_FILE_CONFLICT; + status = UploadStatus.UPLOAD_SUCCEEDED; + } else { + result = UploadResult.SYNC_CONFLICT; } + } else if (code == RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND) { + // upload status is SUCCEEDED because user cannot take action about it, it will always fail + status = UploadStatus.UPLOAD_SUCCEEDED; + result = UploadResult.FILE_NOT_FOUND; } + + Log_OC.d(TAG, String.format( + "Upload Finished [%s] | RemoteCode: %s | internalResult: %s | FinalStatus: %s | Path: %s", + uploadResult.isSuccess() ? "✅" : "❌", + code, + result.name(), + status, + upload.getRemotePath())); + updateUploadStatus(upload.getOCUploadId(), status, result, upload.getRemotePath(), localPath); } /** 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..efbf642eb0c1 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -83,7 +83,9 @@ import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -577,6 +579,9 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare result = new RemoteOperationResult<>(e); } finally { result = cleanupE2EUpload(fileLock, channel, e2eFiles, result, object, client, token); + + // update upload status + uploadsStorageManager.updateDatabaseUploadResult(result, this); } completeE2EUpload(result, e2eFiles, client); @@ -1005,24 +1010,17 @@ private RemoteOperationResult checkConditions(File originalFile) { } private RemoteOperationResult normalUpload(OwnCloudClient client) { - RemoteOperationResult result = null; + RemoteOperationResult result = null; File temporalFile = null; File originalFile = new File(mOriginalStoragePath); File expectedFile = null; - FileLock fileLock = null; - FileChannel channel = null; - - long size; try { - // check conditions result = checkConditions(originalFile); - if (result != null) { return result; } - // check name collision final var collisionResult = checkNameCollision(null, client, null, false); if (collisionResult != null) { result = collisionResult; @@ -1039,15 +1037,13 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { // Get the last modification date of the file from the file system long lastModifiedTimestamp = originalFile.lastModified() / 1000; - final Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile); - try { - channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } catch (FileNotFoundException e) { - // this basically means that the file is on SD card - // try to copy file to temporary dir if it doesn't exist + Path filePath = Paths.get(mFile.getStoragePath()); + + // file does not exists in storage + if (!Files.exists(filePath)) { + Log_OC.e(TAG, "file not found exception: normal upload, probably file in sd card"); String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + mFile.getRemotePath(); mFile.setStoragePath(temporalPath); @@ -1056,98 +1052,96 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { Files.deleteIfExists(Paths.get(temporalPath)); result = copy(originalFile, temporalFile); - if (result.isSuccess()) { - if (temporalFile.length() == originalFile.length()) { - channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } else { - result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); - } - } - } + if (!result.isSuccess()) return result; - try { - size = channel.size(); - } catch (Exception exception) { - Log_OC.e(TAG, "normalUpload, size cannot be determined from channel: " + exception); - size = new File(mFile.getStoragePath()).length(); + if (temporalFile.length() != originalFile.length()) { + result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); + } + filePath = temporalFile.toPath(); } - updateSize(size); + // file exists in storage + try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) { + FileLock fileLock = null; + 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) { + Log_OC.e(TAG, "shared lock overlap detected; proceeding safely."); + } - // perform the upload - if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { - boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); - - mUploadOperation = new ChunkedFileUploadRemoteOperation(mFile.getStoragePath(), - mFile.getRemotePath(), - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - creationTimestamp, - onWifiConnection, - mDisableRetries); - } else { - mUploadOperation = new UploadFileRemoteOperation(mFile.getStoragePath(), - mFile.getRemotePath(), - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - creationTimestamp, - mDisableRetries); - } + // determine size + long size; + try { + size = channel.size(); + } catch (IOException e) { + size = Files.size(filePath); + } + updateSize(size); + + // decide whether chunked or not + if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { + boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); + mUploadOperation = new ChunkedFileUploadRemoteOperation( + mFile.getStoragePath(), mFile.getRemotePath(), mFile.getMimeType(), + mFile.getEtagInConflict(), lastModifiedTimestamp, creationTimestamp, + onWifiConnection, mDisableRetries); + } else { + mUploadOperation = new UploadFileRemoteOperation( + mFile.getStoragePath(), mFile.getRemotePath(), mFile.getMimeType(), + mFile.getEtagInConflict(), lastModifiedTimestamp, creationTimestamp, + mDisableRetries); + } - /** - * Adds the onTransferProgress in FileUploadWorker - * {@link FileUploadWorker#onTransferProgress(long, long, long, String)()} - */ - for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { - mUploadOperation.addDataTransferProgressListener(mDataTransferListener); - } + /** + * Adds the onTransferProgress in FileUploadWorker + * {@link FileUploadWorker#onTransferProgress(long, long, long, String)()} + */ + for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { + mUploadOperation.addDataTransferProgressListener(mDataTransferListener); + } - if (mCancellationRequested.get()) { - throw new OperationCancelledException(); - } + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } - if (result.isSuccess() && mUploadOperation != null) { - result = mUploadOperation.execute(client); + // execute + if (result.isSuccess() && mUploadOperation != null) { + result = mUploadOperation.execute(client); + } - /// move local temporal file or original file to its corresponding + // move local temporal file or original file to its corresponding // location in the Nextcloud local folder if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { result = new RemoteOperationResult<>(ResultCode.SYNC_CONFLICT); } + + if (fileLock != null && fileLock.isValid()) { + fileLock.release(); + Log_OC.d(TAG ,"🔓" + "file lock released"); + } } } catch (FileNotFoundException e) { - Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore"); + Log_OC.e(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) { + Log_OC.e(TAG, "normal upload exception: ", e); result = new RemoteOperationResult<>(e); } finally { mUploadStarted.set(false); - if (fileLock != null) { - try { - fileLock.release(); - } catch (IOException e) { - Log_OC.e(TAG, "Failed to unlock file with path " + mOriginalStoragePath); - } - } - - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - Log_OC.w(TAG, "Failed to close file channel"); + // clean up temporal file if it exists + try { + if (temporalFile != null) { + if (temporalFile.exists() && !temporalFile.delete()) { + Log_OC.e(TAG, "Could not delete temporal file"); + } + } else { + Log_OC.d(TAG, "temporal file is null - internal storage is used instead of sd-card"); } - } - - if (temporalFile != null && !originalFile.equals(temporalFile)) { - boolean isTempFileDeleted = temporalFile.delete(); - Log_OC.d(TAG, "normalUpload, temp folder deletion: " + isTempFileDeleted); + } catch (Exception e) { + Log_OC.e(TAG, "an exception occurred during deletion of temporal file: ", e); } if (result == null) { @@ -1155,6 +1149,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { } logResult(result, mOriginalStoragePath, mRemotePath); + uploadsStorageManager.updateDatabaseUploadResult(result, this); } if (result.isSuccess()) { @@ -1163,11 +1158,6 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); } - // delete temporal file - if (temporalFile != null && temporalFile.exists() && !temporalFile.delete()) { - Log_OC.e(TAG, "Could not delete temporal file " + temporalFile.getAbsolutePath()); - } - return result; } @@ -1248,7 +1238,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 +1481,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(); } } 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