From db867e39a57b49a943ae7edce473e190178e4c34 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 15:25:11 +0200 Subject: [PATCH 1/5] refactor(upload-file-operation): upload file resolver Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java --- .../operations/UploadFileOperation.java | 134 +++--------------- .../operations/upload/UploadFileResolver.kt | 104 ++++++++++++++ 2 files changed, 120 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt 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 7212caf8fea9..2424c8ab8cc2 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -63,11 +63,11 @@ import com.owncloud.android.operations.upload.RemoteFileExistence; import com.owncloud.android.operations.upload.UploadFileException; import com.owncloud.android.operations.upload.UploadFileOperationExtensionsKt; +import com.owncloud.android.operations.upload.UploadFileResolver; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.FileUtil; -import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.UriUtils; import com.owncloud.android.utils.theme.CapabilityUtils; @@ -95,11 +95,9 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.spec.InvalidParameterSpecException; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -168,36 +166,17 @@ public class UploadFileOperation extends SyncOperation { private final User user; private final OCUpload mUpload; - private final UploadsStorageManager uploadsStorageManager; + public final UploadsStorageManager uploadsStorageManager; private final ConnectivityService connectivityService; private final PowerManagementService powerManagementService; private volatile boolean encryptedAncestor; private OCFile duplicatedEncryptedFile; private AtomicBoolean missingPermissionThrown = new AtomicBoolean(false); + private final UploadFileResolver uploadFileResolver; public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) { - OCFile newFile = new OCFile(remotePath); - newFile.setStoragePath(localPath); - newFile.setLastSyncDateForProperties(0); - newFile.setLastSyncDateForData(0); - - // size - if (!TextUtils.isEmpty(localPath)) { - File localFile = new File(localPath); - newFile.setFileLength(localFile.length()); - newFile.setLastSyncDateForData(localFile.lastModified()); - } // don't worry about not assigning size, the problems with localPath - // are checked when the UploadFileOperation instance is created - - // MIME type - if (TextUtils.isEmpty(mimeType)) { - newFile.setMimeType(MimeTypeUtil.getBestMimeTypeByFilename(localPath)); - } else { - newFile.setMimeType(mimeType); - } - - return newFile; + return UploadFileResolver.Companion.obtainNewOCFileToUpload(remotePath, localPath, mimeType); } public UploadFileOperation(UploadsStorageManager uploadsStorageManager, @@ -281,6 +260,7 @@ public UploadFileOperation(UploadsStorageManager uploadsStorageManager, mIgnoringPowerSaveMode = mCreatedBy == CREATED_BY_USER; mFolderUnlockToken = upload.getFolderUnlockToken(); mDisableRetries = disableRetries; + uploadFileResolver = new UploadFileResolver(this); } public boolean isWifiRequired() { @@ -452,7 +432,7 @@ protected RemoteOperationResult run(OwnCloudClient client) { mPaused.set(false); mUploadStarted.set(true); - updateSize(0); + uploadFileResolver.updateSize(0); Log_OC.d(TAG, "file size set to 0KB before upload"); String remoteParentPath = new File(getRemotePath()).getParent(); @@ -564,7 +544,7 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare E2EClientData clientData = new E2EClientData(client, token, publicKey); - List fileNames = getCollidedFileNames(object); + List fileNames = uploadFileResolver.getCollidedFileNames(object); final var collisionResult = checkNameCollision(parentFile, client, fileNames, parentFile.isEncrypted()); if (collisionResult != null) { @@ -601,7 +581,7 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare channel = channelResult.getThird(); size = getChannelSize(channel); - updateSize(size); + uploadFileResolver.updateSize(size); setUploadOperationForE2E(token, e2eFiles.getEncryptedTempFile(), e2eData.getEncryptedFileName(), lastModifiedTimestamp, creationTimestamp, size); result = performE2EUpload(clientData); @@ -667,23 +647,6 @@ private DecryptedFolderMetadataFileV1 getDecryptedFolderMetadataV1(String public return metadata; } - private List getCollidedFileNames(Object object) { - List result = new ArrayList<>(); - - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - for (DecryptedFile file : metadata.getFiles().values()) { - result.add(file.getEncrypted().getFilename()); - } - } else if (object instanceof DecryptedFolderMetadataFile metadataFile) { - Map files = metadataFile.getMetadata().getFiles(); - for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file : files.values()) { - result.add(file.getFilename()); - } - } - - return result; - } - private String getEncryptedFileName(Object object) { String encryptedFileName = EncryptionUtils.generateUid(); @@ -1130,7 +1093,7 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { } final var formattedFileSize = Formatter.formatFileSize(mContext, size); - updateSize(size); + uploadFileResolver.updateSize(size); Log_OC.d(TAG, "file size set to " + formattedFileSize); final long serverMaxChunkSize = getCapabilities().getChunkedUploadMaxSize(); @@ -1228,14 +1191,6 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { return result; } - private void updateSize(long size) { - OCUpload ocUpload = uploadsStorageManager.getUploadById(getOCUploadId()); - if (ocUpload != null) { - ocUpload.setFileSize(size); - uploadsStorageManager.updateUpload(ocUpload); - } - } - private void logResult(RemoteOperationResult result, String sourcePath, String targetPath) { if (result.isSuccess()) { Log_OC.i(TAG, "Upload of " + sourcePath + " to " + targetPath + ": " + result.getLogMessage()); @@ -1296,7 +1251,11 @@ private RemoteOperationResult checkNameCollision(OCFile parentFile, case RENAME: mRemotePath = getNewAvailableRemotePath(client, mRemotePath, fileNames, encrypted); mWasRenamed = true; - createNewOCFile(mRemotePath); + + final var newFile = uploadFileResolver.createNewOCFile(mFile, mRemotePath); + mOldFile = mFile; + mFile = newFile; + Log_OC.d(TAG, "File renamed as " + mRemotePath); if (mRenameUploadListener != null) { mRenameUploadListener.onRenameUpload(); @@ -1448,7 +1407,7 @@ private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnClo if (result.isSuccess()) { OCFile parentDir = getStorageManager().getFileByPath(pathToGrant); if (parentDir == null) { - parentDir = createLocalFolder(pathToGrant); + parentDir = uploadFileResolver.createLocalFolder(pathToGrant); } if (parentDir != null) { result = new RemoteOperationResult<>(ResultCode.OK); @@ -1459,50 +1418,6 @@ private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnClo return result; } - private OCFile createLocalFolder(String remotePath) { - String parentPath = new File(remotePath).getParent(); - parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? - parentPath : parentPath + OCFile.PATH_SEPARATOR; - OCFile parent = getStorageManager().getFileByPath(parentPath); - if (parent == null) { - parent = createLocalFolder(parentPath); - } - if (parent != null) { - OCFile createdFolder = new OCFile(remotePath); - createdFolder.setMimeType(MimeType.DIRECTORY); - createdFolder.setParentId(parent.getFileId()); - getStorageManager().saveFile(createdFolder); - return createdFolder; - } - return null; - } - - - /** - * Create a new OCFile mFile with new remote path. This is required if nameCollisionPolicy==RENAME. New file is - * stored as mFile, original as mOldFile. - * - * @param newRemotePath new remote path - */ - private void createNewOCFile(String newRemotePath) { - // a new OCFile instance must be created for a new remote path - OCFile newFile = new OCFile(newRemotePath); - newFile.setCreationTimestamp(mFile.getCreationTimestamp()); - newFile.setFileLength(mFile.getFileLength()); - newFile.setMimeType(mFile.getMimeType()); - newFile.setModificationTimestamp(mFile.getModificationTimestamp()); - newFile.setModificationTimestampAtLastSyncForData( - mFile.getModificationTimestampAtLastSyncForData() - ); - newFile.setEtag(mFile.getEtag()); - newFile.setLastSyncDateForProperties(mFile.getLastSyncDateForProperties()); - newFile.setLastSyncDateForData(mFile.getLastSyncDateForData()); - newFile.setStoragePath(mFile.getStoragePath()); - newFile.setParentId(mFile.getParentId()); - mOldFile = mFile; - mFile = newFile; - } - /** * Returns a new and available (does not exists on the server) remotePath. This adds an incremental suffix. * @@ -1759,7 +1674,7 @@ private void saveUploadedFile(OwnCloudClient client) { ReadFileRemoteOperation operation = new ReadFileRemoteOperation(path); RemoteOperationResult result = operation.execute(client); if (result.isSuccess()) { - updateOCFile(file, (RemoteFile) result.getData().get(0)); + uploadFileResolver.updateOCFile(file, (RemoteFile) result.getData().get(0)); file.setLastSyncDateForProperties(syncDate); } else { Log_OC.e(TAG, "Error reading properties of file after successful upload; this is gonna hurt..."); @@ -1790,24 +1705,7 @@ private void saveUploadedFile(OwnCloudClient client) { task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, file.getRemoteId())); } - private void updateOCFile(OCFile file, RemoteFile remoteFile) { - file.setCreationTimestamp(remoteFile.getCreationTimestamp()); - file.setFileLength(remoteFile.getLength()); - file.setMimeType(remoteFile.getMimeType()); - file.setModificationTimestamp(remoteFile.getModifiedTimestamp()); - file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp()); - file.setEtag(remoteFile.getEtag()); - file.setEtagOnServer(remoteFile.getEtag()); - file.setRemoteId(remoteFile.getRemoteId()); - file.setPermissions(remoteFile.getPermissions()); - file.setUploadTimestamp(remoteFile.getUploadTimestamp()); - file.setPreviewAvailable(remoteFile.isHasPreview()); - file.setImageDimension(remoteFile.getImageDimension()); - file.setGeoLocation(remoteFile.getGeoLocation()); - } - public interface OnRenameListener { - void onRenameUpload(); } } diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt new file mode 100644 index 000000000000..fa8e0b6c817e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt @@ -0,0 +1,104 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.MimeTypeUtil +import java.io.File + +@Suppress("DEPRECATION") +class UploadFileResolver(private val operation: UploadFileOperation) { + + companion object { + fun obtainNewOCFileToUpload(remotePath: String?, localPath: String?, mimeType: String?): OCFile { + val newFile = OCFile(remotePath).apply { + setStoragePath(localPath) + lastSyncDateForProperties = 0 + lastSyncDateForData = 0 + } + + if (!localPath.isNullOrEmpty()) { + val localFile = File(localPath) + newFile.fileLength = localFile.length() + newFile.lastSyncDateForData = localFile.lastModified() + } + + if (mimeType.isNullOrEmpty()) { + newFile.mimeType = MimeTypeUtil.getBestMimeTypeByFilename(localPath) + } else { + newFile.mimeType = mimeType + } + + return newFile + } + } + + val Any?.collidedFileNames: List + get() = when (this) { + is DecryptedFolderMetadataFileV1 -> files.values.map { it.encrypted.filename } + is DecryptedFolderMetadataFile -> metadata.files.values.map { it.filename } + else -> emptyList() + } + + fun updateSize(size: Long) { + val storageManager = operation.uploadsStorageManager + val ocUpload = storageManager.getUploadById(operation.ocUploadId) + if (ocUpload != null) { + ocUpload.fileSize = size + storageManager.updateUpload(ocUpload) + } + } + + fun createNewOCFile(fileToBeUploaded: OCFile, newRemotePath: String): OCFile = OCFile(newRemotePath).apply { + creationTimestamp = fileToBeUploaded.creationTimestamp + fileLength = fileToBeUploaded.fileLength + mimeType = fileToBeUploaded.mimeType + modificationTimestamp = fileToBeUploaded.modificationTimestamp + modificationTimestampAtLastSyncForData = fileToBeUploaded.modificationTimestampAtLastSyncForData + etag = fileToBeUploaded.etag + lastSyncDateForProperties = fileToBeUploaded.lastSyncDateForProperties + lastSyncDateForData = fileToBeUploaded.lastSyncDateForData + storagePath = fileToBeUploaded.storagePath + parentId = fileToBeUploaded.parentId + } + + fun createLocalFolder(remotePath: String): OCFile? { + val parentPath = File(remotePath).parent + ?.let { if (it.endsWith(OCFile.PATH_SEPARATOR)) it else it + OCFile.PATH_SEPARATOR } + ?: return null + + val storageManager = operation.storageManager + val parent = storageManager.getFileByPath(parentPath) + ?: createLocalFolder(parentPath) + ?: return null + + return OCFile(remotePath).apply { + mimeType = MimeType.DIRECTORY + parentId = parent.fileId + }.also { storageManager.saveFile(it) } + } + + fun updateOCFile(file: OCFile, remoteFile: RemoteFile) { + file.creationTimestamp = remoteFile.creationTimestamp + file.fileLength = remoteFile.length + file.setMimeType(remoteFile.mimeType) + file.modificationTimestamp = remoteFile.modifiedTimestamp + file.modificationTimestampAtLastSyncForData = remoteFile.modifiedTimestamp + file.setEtag(remoteFile.etag) + file.setEtagOnServer(remoteFile.etag) + file.setRemoteId(remoteFile.remoteId) + file.setPermissions(remoteFile.permissions) + file.uploadTimestamp = remoteFile.uploadTimestamp + file.isPreviewAvailable = remoteFile.isHasPreview + } +} From 1270ead7a85901fec061b6880f2be4f3d3a32ba8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 8 Oct 2026 15:37:49 +0200 Subject: [PATCH 2/5] add tests Signed-off-by: alperozturk96 --- .../operations/UploadFileOperation.java | 8 +- .../operations/upload/UploadFileResolver.kt | 81 +++--- .../upload/UploadFileResolverTest.kt | 236 ++++++++++++++++++ 3 files changed, 278 insertions(+), 47 deletions(-) create mode 100644 app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt 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 2424c8ab8cc2..b9e9c42ee68d 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -166,7 +166,7 @@ public class UploadFileOperation extends SyncOperation { private final User user; private final OCUpload mUpload; - public final UploadsStorageManager uploadsStorageManager; + private final UploadsStorageManager uploadsStorageManager; private final ConnectivityService connectivityService; private final PowerManagementService powerManagementService; @@ -176,7 +176,7 @@ public class UploadFileOperation extends SyncOperation { private final UploadFileResolver uploadFileResolver; public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) { - return UploadFileResolver.Companion.obtainNewOCFileToUpload(remotePath, localPath, mimeType); + return UploadFileResolver.obtainNewOCFileToUpload(remotePath, localPath, mimeType); } public UploadFileOperation(UploadsStorageManager uploadsStorageManager, @@ -364,6 +364,10 @@ public long getOCUploadId() { return mOCUploadId; } + public UploadsStorageManager getUploadsStorageManager() { + return uploadsStorageManager; + } + public Set getDataTransferListeners() { return mDataTransferListeners; } diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt index fa8e0b6c817e..2bb4d2fab79e 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt @@ -16,46 +16,38 @@ import com.owncloud.android.utils.MimeType import com.owncloud.android.utils.MimeTypeUtil import java.io.File -@Suppress("DEPRECATION") class UploadFileResolver(private val operation: UploadFileOperation) { companion object { - fun obtainNewOCFileToUpload(remotePath: String?, localPath: String?, mimeType: String?): OCFile { - val newFile = OCFile(remotePath).apply { + @JvmStatic + fun obtainNewOCFileToUpload(remotePath: String?, localPath: String?, mimeType: String?): OCFile = + OCFile(remotePath).apply { setStoragePath(localPath) lastSyncDateForProperties = 0 lastSyncDateForData = 0 + if (!localPath.isNullOrEmpty()) { + val localFile = File(localPath) + fileLength = localFile.length() + lastSyncDateForData = localFile.lastModified() + } + setMimeType( + if (mimeType.isNullOrEmpty()) MimeTypeUtil.getBestMimeTypeByFilename(localPath) else mimeType + ) } - - if (!localPath.isNullOrEmpty()) { - val localFile = File(localPath) - newFile.fileLength = localFile.length() - newFile.lastSyncDateForData = localFile.lastModified() - } - - if (mimeType.isNullOrEmpty()) { - newFile.mimeType = MimeTypeUtil.getBestMimeTypeByFilename(localPath) - } else { - newFile.mimeType = mimeType - } - - return newFile - } } - val Any?.collidedFileNames: List - get() = when (this) { - is DecryptedFolderMetadataFileV1 -> files.values.map { it.encrypted.filename } - is DecryptedFolderMetadataFile -> metadata.files.values.map { it.filename } - else -> emptyList() - } + @Suppress("DEPRECATION") + fun getCollidedFileNames(metadata: Any?): List = when (metadata) { + is DecryptedFolderMetadataFileV1 -> metadata.files.values.map { it.encrypted.filename } + is DecryptedFolderMetadataFile -> metadata.metadata.files.values.map { it.filename } + else -> emptyList() + } fun updateSize(size: Long) { val storageManager = operation.uploadsStorageManager - val ocUpload = storageManager.getUploadById(operation.ocUploadId) - if (ocUpload != null) { - ocUpload.fileSize = size - storageManager.updateUpload(ocUpload) + storageManager.getUploadById(operation.ocUploadId)?.let { + it.fileSize = size + storageManager.updateUpload(it) } } @@ -73,13 +65,10 @@ class UploadFileResolver(private val operation: UploadFileOperation) { } fun createLocalFolder(remotePath: String): OCFile? { - val parentPath = File(remotePath).parent - ?.let { if (it.endsWith(OCFile.PATH_SEPARATOR)) it else it + OCFile.PATH_SEPARATOR } - ?: return null - val storageManager = operation.storageManager - val parent = storageManager.getFileByPath(parentPath) - ?: createLocalFolder(parentPath) + val parent = File(remotePath).parent + ?.let { if (it.endsWith(OCFile.PATH_SEPARATOR)) it else it + OCFile.PATH_SEPARATOR } + ?.let { storageManager.getFileByPath(it) ?: createLocalFolder(it) } ?: return null return OCFile(remotePath).apply { @@ -89,16 +78,18 @@ class UploadFileResolver(private val operation: UploadFileOperation) { } fun updateOCFile(file: OCFile, remoteFile: RemoteFile) { - file.creationTimestamp = remoteFile.creationTimestamp - file.fileLength = remoteFile.length - file.setMimeType(remoteFile.mimeType) - file.modificationTimestamp = remoteFile.modifiedTimestamp - file.modificationTimestampAtLastSyncForData = remoteFile.modifiedTimestamp - file.setEtag(remoteFile.etag) - file.setEtagOnServer(remoteFile.etag) - file.setRemoteId(remoteFile.remoteId) - file.setPermissions(remoteFile.permissions) - file.uploadTimestamp = remoteFile.uploadTimestamp - file.isPreviewAvailable = remoteFile.isHasPreview + file.apply { + creationTimestamp = remoteFile.creationTimestamp + fileLength = remoteFile.length + mimeType = remoteFile.mimeType + modificationTimestamp = remoteFile.modifiedTimestamp + modificationTimestampAtLastSyncForData = remoteFile.modifiedTimestamp + etag = remoteFile.etag + etagOnServer = remoteFile.etag + remoteId = remoteFile.remoteId + permissions = remoteFile.permissions + uploadTimestamp = remoteFile.uploadTimestamp + isPreviewAvailable = remoteFile.isHasPreview + } } } diff --git a/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt b/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt new file mode 100644 index 000000000000..ca82cc33014a --- /dev/null +++ b/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt @@ -0,0 +1,236 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.datamodel.e2e.v1.decrypted.Data +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata +import com.owncloud.android.db.OCUpload +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.utils.MimeType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile as DecryptedFileV1 + +@Suppress("MagicNumber") +class UploadFileResolverTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + private val uploadsStorageManager: UploadsStorageManager = mock() + private val fileDataStorageManager: FileDataStorageManager = mock() + private val operation: UploadFileOperation = mock() + private lateinit var resolver: UploadFileResolver + + @Before + fun setUp() { + whenever(operation.uploadsStorageManager).thenReturn(uploadsStorageManager) + whenever(operation.storageManager).thenReturn(fileDataStorageManager) + whenever(operation.ocUploadId).thenReturn(UPLOAD_ID) + resolver = UploadFileResolver(operation) + } + + @Test + fun obtainNewOCFileToUploadUsesLocalFileSizeAndModificationDate() { + val localFile = temporaryFolder.newFile("photo.jpg").apply { + writeBytes(ByteArray(42)) + setLastModified(1_700_000_000_000) + } + + val file = UploadFileResolver.obtainNewOCFileToUpload("/photo.jpg", localFile.absolutePath, "image/jpeg") + + assertEquals("/photo.jpg", file.remotePath) + assertEquals(localFile.absolutePath, file.storagePath) + assertEquals(42L, file.fileLength) + assertEquals(localFile.lastModified(), file.lastSyncDateForData) + assertEquals(0L, file.lastSyncDateForProperties) + assertEquals("image/jpeg", file.mimeType) + } + + @Test + fun obtainNewOCFileToUploadWithoutLocalPathKeepsDefaults() { + val file = UploadFileResolver.obtainNewOCFileToUpload("/photo.jpg", null, "image/jpeg") + + assertNull(file.storagePath) + assertEquals(0L, file.fileLength) + assertEquals(0L, file.lastSyncDateForData) + assertEquals("image/jpeg", file.mimeType) + } + + @Test + fun collidedFileNamesOfV1MetadataAreEncryptedFilenames() { + val files = FILE_NAMES.associateWith { name -> + DecryptedFileV1().apply { encrypted = Data().apply { filename = name } } + } + val metadata = DecryptedFolderMetadataFileV1().apply { setFiles(files) } + + assertEquals(FILE_NAMES, resolver.getCollidedFileNames(metadata)) + } + + @Test + fun collidedFileNamesOfV2MetadataAreFilenames() { + val files = FILE_NAMES.associateWith { name -> + DecryptedFile(filename = name, mimetype = "text/plain", nonce = "", authenticationTag = "", key = "") + }.toMutableMap() + val metadata = DecryptedFolderMetadataFile( + metadata = DecryptedMetadata(files = files, metadataKey = ByteArray(0)), + version = "2.0" + ) + + assertEquals(FILE_NAMES, resolver.getCollidedFileNames(metadata)) + } + + @Test + fun collidedFileNamesOfUnknownMetadataAreEmpty() { + assertTrue(resolver.getCollidedFileNames(null).isEmpty()) + assertTrue(resolver.getCollidedFileNames("metadata").isEmpty()) + } + + @Test + fun updateSizeStoresSizeOfExistingUpload() { + val upload: OCUpload = mock() + whenever(uploadsStorageManager.getUploadById(UPLOAD_ID)).thenReturn(upload) + + resolver.updateSize(1024) + + verify(upload).fileSize = 1024 + verify(uploadsStorageManager).updateUpload(upload) + } + + @Test + fun updateSizeIgnoresMissingUpload() { + whenever(uploadsStorageManager.getUploadById(UPLOAD_ID)).thenReturn(null) + + resolver.updateSize(1024) + + verify(uploadsStorageManager, never()).updateUpload(any()) + } + + @Test + fun createNewOCFileCopiesPropertiesToNewRemotePath() { + val original = OCFile("/photo.jpg").apply { + creationTimestamp = 1 + fileLength = 2 + mimeType = "image/jpeg" + modificationTimestamp = 3 + modificationTimestampAtLastSyncForData = 4 + etag = "etag" + lastSyncDateForProperties = 5 + lastSyncDateForData = 6 + storagePath = "/storage/photo.jpg" + parentId = 7 + } + + val renamed = resolver.createNewOCFile(original, "/photo (2).jpg") + + assertEquals("/photo (2).jpg", renamed.remotePath) + assertEquals(1L, renamed.creationTimestamp) + assertEquals(2L, renamed.fileLength) + assertEquals("image/jpeg", renamed.mimeType) + assertEquals(3L, renamed.modificationTimestamp) + assertEquals(4L, renamed.modificationTimestampAtLastSyncForData) + assertEquals("etag", renamed.etag) + assertEquals(5L, renamed.lastSyncDateForProperties) + assertEquals(6L, renamed.lastSyncDateForData) + assertEquals("/storage/photo.jpg", renamed.storagePath) + assertEquals(7L, renamed.parentId) + } + + @Test + fun createLocalFolderUsesExistingParent() { + val parent = OCFile("/a/").apply { fileId = 10 } + whenever(fileDataStorageManager.getFileByPath("/a/")).thenReturn(parent) + + val folder = resolver.createLocalFolder("/a/b/") + + assertNotNull(folder) + assertEquals("/a/b/", folder?.remotePath) + assertEquals(MimeType.DIRECTORY, folder?.mimeType) + assertEquals(10L, folder?.parentId) + verify(fileDataStorageManager).saveFile(folder) + } + + @Test + fun createLocalFolderCreatesMissingParents() { + val root = OCFile(OCFile.ROOT_PATH).apply { fileId = 1 } + whenever(fileDataStorageManager.getFileByPath(OCFile.ROOT_PATH)).thenReturn(root) + var nextId = 2L + doAnswer { + it.getArgument(0).fileId = nextId++ + true + }.whenever(fileDataStorageManager).saveFile(any()) + + val folder = resolver.createLocalFolder("/a/b/") + + assertEquals("/a/b/", folder?.remotePath) + assertEquals(2L, folder?.parentId) + verify(fileDataStorageManager).saveFile(argThat { remotePath == "/a/" && parentId == 1L }) + } + + @Test + fun createLocalFolderReturnsNullWithoutParentPath() { + assertNull(resolver.createLocalFolder(OCFile.ROOT_PATH)) + verify(fileDataStorageManager, never()).saveFile(any()) + } + + @Test + fun updateOCFileCopiesRemoteProperties() { + val remoteFile: RemoteFile = mock { + on { creationTimestamp } doReturn 1L + on { length } doReturn 2L + on { mimeType } doReturn "image/jpeg" + on { modifiedTimestamp } doReturn 3L + on { etag } doReturn "etag" + on { remoteId } doReturn "remote-id" + on { permissions } doReturn "RGDNVW" + on { uploadTimestamp } doReturn 4L + on { isHasPreview } doReturn true + } + val file = OCFile("/photo.jpg") + + resolver.updateOCFile(file, remoteFile) + + assertEquals(1L, file.creationTimestamp) + assertEquals(2L, file.fileLength) + assertEquals("image/jpeg", file.mimeType) + assertEquals(3L, file.modificationTimestamp) + assertEquals(3L, file.modificationTimestampAtLastSyncForData) + assertEquals("etag", file.etag) + assertEquals("etag", file.etagOnServer) + assertEquals("remote-id", file.remoteId) + assertEquals("RGDNVW", file.permissions) + assertEquals(4L, file.uploadTimestamp) + assertTrue(file.isPreviewAvailable) + } + + private companion object { + const val UPLOAD_ID = 5L + val FILE_NAMES = listOf("first.txt", "second.txt") + } +} From 29a3ed51a3e8486f74f5813a884e40560e754416 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 12 Oct 2026 12:47:01 +0200 Subject: [PATCH 3/5] update updateOCFile Signed-off-by: alperozturk96 --- .../owncloud/android/operations/upload/UploadFileResolver.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt index 2bb4d2fab79e..675c54cc2946 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt @@ -90,6 +90,8 @@ class UploadFileResolver(private val operation: UploadFileOperation) { permissions = remoteFile.permissions uploadTimestamp = remoteFile.uploadTimestamp isPreviewAvailable = remoteFile.isHasPreview + file.imageDimension = remoteFile.imageDimension + file.geoLocation = remoteFile.geoLocation } } } From db6ae7b7d183803f08a170fc91bc4cc834c6a056 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 12 Oct 2026 12:47:13 +0200 Subject: [PATCH 4/5] update updateOCFile Signed-off-by: alperozturk96 --- .../owncloud/android/operations/upload/UploadFileResolver.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt index 675c54cc2946..8a3df939cdc1 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileResolver.kt @@ -90,8 +90,8 @@ class UploadFileResolver(private val operation: UploadFileOperation) { permissions = remoteFile.permissions uploadTimestamp = remoteFile.uploadTimestamp isPreviewAvailable = remoteFile.isHasPreview - file.imageDimension = remoteFile.imageDimension - file.geoLocation = remoteFile.geoLocation + imageDimension = remoteFile.imageDimension + geoLocation = remoteFile.geoLocation } } } From 5a8619989ba46f5d375965ffeaa593873f44f889 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 12 Oct 2026 12:49:23 +0200 Subject: [PATCH 5/5] update tests Signed-off-by: alperozturk96 --- .../android/operations/upload/UploadFileResolverTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt b/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt index ca82cc33014a..f6d77b36e715 100644 --- a/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt +++ b/app/src/test/java/com/owncloud/android/operations/upload/UploadFileResolverTest.kt @@ -16,6 +16,8 @@ import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata import com.owncloud.android.db.OCUpload +import com.owncloud.android.lib.resources.files.model.GeoLocation +import com.owncloud.android.lib.resources.files.model.ImageDimension import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.utils.MimeType @@ -201,6 +203,8 @@ class UploadFileResolverTest { @Test fun updateOCFileCopiesRemoteProperties() { + val imageDimension = ImageDimension(640f, 480f) + val geoLocation = GeoLocation(52.52, 13.405) val remoteFile: RemoteFile = mock { on { creationTimestamp } doReturn 1L on { length } doReturn 2L @@ -211,6 +215,8 @@ class UploadFileResolverTest { on { permissions } doReturn "RGDNVW" on { uploadTimestamp } doReturn 4L on { isHasPreview } doReturn true + on { this.imageDimension } doReturn imageDimension + on { this.geoLocation } doReturn geoLocation } val file = OCFile("/photo.jpg") @@ -227,6 +233,8 @@ class UploadFileResolverTest { assertEquals("RGDNVW", file.permissions) assertEquals(4L, file.uploadTimestamp) assertTrue(file.isPreviewAvailable) + assertEquals(imageDimension, file.imageDimension) + assertEquals(geoLocation, file.geoLocation) } private companion object {