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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/nextcloud/client/di/AppModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,7 +62,8 @@ class BackgroundJobFactory @Inject constructor(
private val viewThemeUtils: Provider<ViewThemeUtils>,
private val localBroadcastManager: Provider<LocalBroadcastManager>,
private val generatePdfUseCase: GeneratePDFUseCase,
private val syncedFolderProvider: SyncedFolderProvider
private val syncedFolderProvider: SyncedFolderProvider,
private val fileOperationHelper: FileOperationHelper
) : WorkerFactory() {

@SuppressLint("NewApi")
Expand Down Expand Up @@ -108,6 +110,7 @@ class BackgroundJobFactory @Inject constructor(
accountManager.user,
context,
connectivityService,
fileOperationHelper,
viewThemeUtils.get(),
params
)
Expand Down Expand Up @@ -230,6 +233,7 @@ class BackgroundJobFactory @Inject constructor(
localBroadcastManager.get(),
backgroundJobManager.get(),
preferences,
fileOperationHelper,
context,
params
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -42,10 +39,12 @@ import kotlin.coroutines.suspendCoroutine

private typealias OfflineOperationResult = Pair<RemoteOperationResult<*>?, 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) {
Expand Down Expand Up @@ -126,10 +125,10 @@ class OfflineOperationsWorker(
return@withContext null
}

val remoteFile = getRemoteFile(path)
val remoteFile = fileOperationHelper.getRemoteFile(path, client)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OfflineOperationWorker was updated to use the logic in FileUtil to prevent code duplication and unify similar functionality.

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()) {
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,77 @@ 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
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

class FileOperationHelper(
private val user: User,
private val context: Context,
private val fileDataStorageManager: FileDataStorageManager
) {
class FileOperationHelper(private val user: User, private val context: Context) {
companion object {
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(upload: OCUpload, storageManager: FileDataStorageManager): Boolean {
val (lc, uc) = FileUtil.getRemotePathVariants(upload.remotePath)

val remoteFile = storageManager.run {
getFileByDecryptedRemotePath(lc) ?: getFileByDecryptedRemotePath(uc)
}

if (upload.toFile()?.length() == remoteFile?.fileLength) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tobiasKaminsky @ZetaTom

I need to compare a local file intended for upload with an existing remote file to determine if they are exactly the same. While file size can be used as a basic check, I’m looking for a more reliable method.

Using eTags is not feasible in this case, as the file to be uploaded (OCUpload) does not have an eTag yet. Similarly, relying on the modification timestamp is unreliable, since it can change due to non-content-related operations such as permission updates or file copying.

At this point, I have a java.io.File object and an OCFile instance, and I need to determine whether they represent the same file content.

Any suggestions for a more robust comparison method beyond file size?

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,
storageManager: FileDataStorageManager,
onlyLocalCopy: Boolean,
inBackground: Boolean,
client: OwnCloudClient
Expand All @@ -44,7 +94,7 @@ class FileOperationHelper(
user,
inBackground,
context,
fileDataStorageManager
storageManager
)
}
val operationResult = operation.await()
Expand Down
Loading
Loading