diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt index bb68dfcfd153..79197a12a034 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt @@ -115,9 +115,9 @@ class ContactsBackupIT : AbstractOnServerIT() { fail("ocFile.storagePath cannot be null") } - assertTrue(DownloadFileOperation(user, ocFile, targetContext).execute(client).isSuccess) + assertTrue(DownloadFileOperation(user, ocFile!!, targetContext).execute(client).isSuccess) - val file = ocFile?.storagePath?.let { File(it) } + val file = ocFile.storagePath?.let { File(it) } if (file == null) { fail("file cannot be null") } diff --git a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt index e5c3591597c4..7298142d89df 100644 --- a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt @@ -17,6 +17,7 @@ import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.utils.FileStorageUtils import org.junit.After import org.junit.Assert +import org.junit.Assert.fail import org.junit.Test import kotlin.io.path.Path import kotlin.io.path.exists @@ -72,11 +73,17 @@ class DownloadIT : AbstractOnServerIT() { var file1 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") var file2 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") - val operation1 = DownloadFileOperation(user, file1, targetContext) + if (file1 == null) { + fail("file 1 cannot be null") + } + val operation1 = DownloadFileOperation(user, file1!!, targetContext) val operation1Result = operation1.execute(client) Assert.assertTrue(operation1Result.isSuccess) - val operation2 = DownloadFileOperation(user, file2, targetContext) + if (file2 == null) { + fail("file 2 cannot be null") + } + val operation2 = DownloadFileOperation(user, file2!!, targetContext) val operation2Result = operation2.execute(client) Assert.assertTrue(operation2Result.isSuccess) diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt index b8708dfd5b26..2da0e9eec5b6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt @@ -74,8 +74,8 @@ class DownloadTask( lastSyncDateForProperties = syncDate lastSyncDateForData = syncDate isUpdateThumbnailNeeded = true - modificationTimestamp = op.modificationTimestamp - modificationTimestampAtLastSyncForData = op.modificationTimestamp + modificationTimestamp = op.getModificationTimestamp() + modificationTimestampAtLastSyncForData = op.getModificationTimestamp() etag = op.etag mimeType = op.mimeType storagePath = op.savePath diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt index 240fda49ef75..f9bbcd71ce5a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt @@ -79,8 +79,8 @@ class FileDownloadHelper { lastSyncDateForProperties = syncDate lastSyncDateForData = syncDate isUpdateThumbnailNeeded = true - modificationTimestamp = currentDownload?.modificationTimestamp ?: 0L - modificationTimestampAtLastSyncForData = currentDownload?.modificationTimestamp ?: 0L + modificationTimestamp = currentDownload?.getModificationTimestamp() ?: 0L + modificationTimestampAtLastSyncForData = currentDownload?.getModificationTimestamp() ?: 0L etag = currentDownload?.etag mimeType = currentDownload?.mimeType storagePath = currentDownload?.savePath diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index d9febd6e48b3..0a4657a9fd56 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -175,6 +175,11 @@ class FileDownloadWorker( val requestedDownloads: AbstractList = Vector() + val user = user ?: run { + Log_OC.e(TAG, "user cannot be null") + return requestedDownloads + } + return try { files.forEach { file -> val operation = DownloadFileOperation( @@ -190,7 +195,7 @@ class FileDownloadWorker( operation.addDownloadDataTransferProgressListener(this) operation.addDownloadDataTransferProgressListener(downloadProgressListener) val (downloadKey, _) = pendingDownloads.putIfAbsent( - user?.accountName, + user.accountName, file.remotePath, operation ) ?: Pair(null, null) diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java deleted file mode 100644 index 84ed094efda2..000000000000 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Andy Scherzinger - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2015 María Asensio Valverde - * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) - */ -package com.owncloud.android.operations; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.webkit.MimeTypeMap; - -import com.nextcloud.client.account.User; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.utils.extensions.ContextExtensionsKt; -import com.nextcloud.utils.extensions.OwnCloudClientExtensionsKt; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -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.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; -import com.owncloud.android.lib.common.operations.OperationCancelledException; -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.DownloadFileRemoteOperation; -import com.owncloud.android.utils.EncryptionUtils; -import com.owncloud.android.utils.FileExportUtils; -import com.owncloud.android.utils.FileStorageUtils; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.file.Files; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.crypto.Cipher; - -import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; - -/** - * Remote DownloadOperation performing the download of a file to an ownCloud server - */ -public class DownloadFileOperation extends RemoteOperation { - private static final String TAG = DownloadFileOperation.class.getSimpleName(); - - private User user; - private OCFile file; - private String behaviour; - private String etag = ""; - private String activityName; - private String packageName; - private DownloadType downloadType; - - private final WeakReference context; - - // CHECK: Is this still needed after conversion from Foreground Services to Worker? - private Set dataTransferListeners = new HashSet<>(); - - private long modificationTimestamp; - private DownloadFileRemoteOperation downloadOperation; - private final AtomicBoolean cancellationRequested = new AtomicBoolean(false); - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - - public DownloadFileOperation(User user, - OCFile file, - String behaviour, - String activityName, - String packageName, - Context context, - DownloadType downloadType) { - if (user == null) { - throw new IllegalArgumentException("Illegal null user in DownloadFileOperation " + - "creation"); - } - if (file == null) { - throw new IllegalArgumentException("Illegal null file in DownloadFileOperation " + - "creation"); - } - - this.user = user; - this.file = file; - this.behaviour = behaviour; - this.activityName = activityName; - this.packageName = packageName; - this.context = new WeakReference<>(context); - this.downloadType = downloadType; - } - - public DownloadFileOperation(User user, OCFile file, Context context) { - this(user, file, null, null, null, context, DownloadType.DOWNLOAD); - } - - public boolean isMatching(String accountName, long fileId) { - return getFile().getFileId() == fileId && getUser().getAccountName().equals(accountName); - } - - public void cancelMatchingOperation(String accountName, long fileId) { - if (isMatching(accountName, fileId)) { - cancel(); - } - } - - public String getSavePath() { - if (file.getStoragePath() != null) { - File parentFile = new File(file.getStoragePath()).getParentFile(); - if (parentFile != null && !parentFile.exists()) { - try { - Files.createDirectories(parentFile.toPath()); - } catch (IOException e) { - return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file); - } - } - File path = new File(file.getStoragePath()); // re-downloads should be done over the original file - if (path.canWrite() || parentFile != null && parentFile.canWrite()) { - return path.getAbsolutePath(); - } - } - return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file); - } - - public String getTmpPath() { - return FileStorageUtils.getTemporalPath(user.getAccountName()) + file.getRemotePath(); - } - - public String getTmpFolder() { - return FileStorageUtils.getTemporalPath(user.getAccountName()); - } - - public String getRemotePath() { - return file.getRemotePath(); - } - - public String getMimeType() { - String mimeType = file.getMimeType(); - if (TextUtils.isEmpty(mimeType)) { - try { - mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension( - file.getRemotePath().substring( - file.getRemotePath().lastIndexOf('.') + 1)); - } catch (IndexOutOfBoundsException e) { - Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " + - file.getRemotePath()); - } - } - if (mimeType == null) { - mimeType = "application/octet-stream"; - } - return mimeType; - } - - public long getSize() { - return file.getFileLength(); - } - - public long getModificationTimestamp() { - return modificationTimestamp > 0 ? modificationTimestamp : file.getModificationTimestamp(); - } - - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - /// perform the download - synchronized(cancellationRequested) { - if (cancellationRequested.get()) { - return new RemoteOperationResult<>(new OperationCancelledException()); - } - } - - final var isValidExtFilename = FileStorageUtils.isValidExtFilename(file.getFileName()); - if (!isValidExtFilename) { - mainThreadHandler.post(() -> ContextExtensionsKt.showToast(context.get(), R.string.download_download_invalid_local_file_name)); - return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME); - } - - Context operationContext = context.get(); - if (operationContext == null) { - return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.UNKNOWN_ERROR); - } - - RemoteOperationResult result; - File newFile = null; - boolean moved; - - /// download will be performed to a temporal file, then moved to the final location - File tmpFile = new File(getTmpPath()); - - String tmpFolder = getTmpFolder(); - - downloadOperation = new DownloadFileRemoteOperation(file.getRemotePath(), tmpFolder); - - if (downloadType == DownloadType.DOWNLOAD) { - dataTransferListeners.forEach(downloadOperation::addDatatransferProgressListener); - } - - NextcloudClient nextcloudClient = OwnCloudClientExtensionsKt.toNextcloudClient(client, operationContext); - result = downloadOperation.execute(nextcloudClient); - - - - if (result.isSuccess()) { - modificationTimestamp = downloadOperation.getModificationTimestamp(); - etag = downloadOperation.getEtag(); - - if (downloadType == DownloadType.DOWNLOAD) { - newFile = new File(getSavePath()); - - if (!newFile.getParentFile().exists() && !newFile.getParentFile().mkdirs()) { - Log_OC.e(TAG, "Unable to create parent folder " + newFile.getParentFile().getAbsolutePath()); - } - } - - // decrypt file - if (file.isEncrypted()) { - FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(user, operationContext.getContentResolver()); - - OCFile parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.getParentRemotePath()); - - Object object = EncryptionUtils.downloadFolderMetadata(parent, - client, - operationContext, - user); - - if (object == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - String keyString; - String nonceString; - String authenticationTagString; - if (object instanceof DecryptedFolderMetadataFile) { - DecryptedFile decryptedFile = ((DecryptedFolderMetadataFile) object) - .getMetadata() - .getFiles() - .get(file.getEncryptedFileName()); - - if (decryptedFile == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - keyString = decryptedFile.getKey(); - nonceString = decryptedFile.getNonce(); - authenticationTagString = decryptedFile.getAuthenticationTag(); - } else { - com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile = - ((DecryptedFolderMetadataFileV1) object) - .getFiles() - .get(file.getEncryptedFileName()); - - if (decryptedFile == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - keyString = decryptedFile.getEncrypted().getKey(); - nonceString = decryptedFile.getInitializationVector(); - authenticationTagString = decryptedFile.getAuthenticationTag(); - } - - byte[] key = decodeStringToBase64Bytes(keyString); - byte[] iv = decodeStringToBase64Bytes(nonceString); - - try { - Cipher cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv); - EncryptionUtils.decryptFile(cipher, tmpFile, newFile, authenticationTagString, new ArbitraryDataProviderImpl(operationContext), user); - } catch (Exception e) { - return new RemoteOperationResult(e); - } - } - - if (downloadType == DownloadType.DOWNLOAD && !file.isEncrypted()) { - moved = tmpFile.renameTo(newFile); - boolean isLastModifiedSet = newFile.setLastModified(file.getModificationTimestamp()); - Log_OC.d(TAG, "Last modified set: " + isLastModifiedSet); - if (!moved) { - result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED); - } - } else if (downloadType == DownloadType.EXPORT) { - new FileExportUtils().exportFile(file.getFileName(), - file.getMimeType(), - operationContext.getContentResolver(), - null, - tmpFile); - if (!tmpFile.delete()) { - Log_OC.e(TAG, "Deletion of " + tmpFile.getAbsolutePath() + " failed!"); - } - } - } - - Log_OC.i(TAG, "Download of " + file.getRemotePath() + " to " + getSavePath() + ": " + - result.getLogMessage()); - - return result; - } - - public void cancel() { - cancellationRequested.set(true); // atomic set; there is no need of synchronizing it - if (downloadOperation != null) { - downloadOperation.cancel(); - } - } - - - public void addDownloadDataTransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (dataTransferListeners) { - dataTransferListeners.add(listener); - } - } - - public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (dataTransferListeners) { - dataTransferListeners.remove(listener); - } - } - - public User getUser() { - return this.user; - } - - public OCFile getFile() { - return this.file; - } - - public String getBehaviour() { - return this.behaviour; - } - - public String getEtag() { - return this.etag; - } - - public String getActivityName() { - return this.activityName; - } - - public String getPackageName() { - return this.packageName; - } - - public DownloadType getDownloadType() { - return downloadType; - } - - public void setDownloadType(DownloadType type) { - downloadType = type; - } -} diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt new file mode 100644 index 000000000000..a68112e097db --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt @@ -0,0 +1,266 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Andy Scherzinger + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2015 María Asensio Valverde + * SPDX-FileCopyrightText: 2012 David A. Velasco + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.operations + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.webkit.MimeTypeMap +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.showToast +import com.nextcloud.utils.extensions.toNextcloudClient +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +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.common.OwnCloudClient +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener +import com.owncloud.android.lib.common.operations.OperationCancelledException +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.DownloadFileRemoteOperation +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.FileExportUtils +import com.owncloud.android.utils.FileStorageUtils +import java.io.File +import java.io.IOException +import java.lang.ref.WeakReference +import java.nio.file.Files +import java.util.concurrent.atomic.AtomicBoolean +import javax.crypto.Cipher + +@Suppress("LongParameterList", "ReturnCount") +class DownloadFileOperation( + val user: User, + val file: OCFile, + val behaviour: String?, + val activityName: String?, + val packageName: String?, + context: Context?, + var downloadType: DownloadType? +) : RemoteOperation() { + + var etag: String? = "" + private set + + private val context = WeakReference(context) + private val dataTransferListeners: MutableSet = HashSet() + private var timestampForModification: Long = 0 + private val cancellationRequested = AtomicBoolean(false) + private val mainThreadHandler = Handler(Looper.getMainLooper()) + + constructor(user: User, file: OCFile, context: Context?) : this( + user, + file, + null, + null, + null, + context, + DownloadType.DOWNLOAD + ) + + fun isMatching(accountName: String?, fileId: Long) = file.fileId == fileId && user.accountName == accountName + + fun cancelMatchingOperation(accountName: String?, fileId: Long) { + if (isMatching(accountName, fileId)) cancel() + } + + val savePath: String + get() { + file.storagePath?.let { storagePath -> + val parentFile = File(storagePath).parentFile + parentFile?.takeIf { !it.exists() }?.runCatching { + Files.createDirectories(toPath()) + }?.onFailure { + return FileStorageUtils.getDefaultSavePathFor(user.accountName, file) + } + val path = File(storagePath) + if (path.canWrite() || parentFile?.canWrite() == true) return path.absolutePath + } + return FileStorageUtils.getDefaultSavePathFor(user.accountName, file) + } + + val tmpPath: String get() = FileStorageUtils.getTemporalPath(user.accountName) + file.remotePath + val tmpFolder: String get() = FileStorageUtils.getTemporalPath(user.accountName) + val remotePath: String get() = file.remotePath + + val mimeType: String + get() { + var mimeType = file.mimeType + if (mimeType.isNullOrEmpty()) { + runCatching { + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + file.remotePath.substring(file.remotePath.lastIndexOf('.') + 1) + ) + }.onFailure { + Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: ${file.remotePath}") + } + } + return mimeType ?: "application/octet-stream" + } + + val size: Long get() = file.fileLength + + fun getModificationTimestamp() = + if (timestampForModification > 0) timestampForModification else file.modificationTimestamp + + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun run(client: OwnCloudClient): RemoteOperationResult { + synchronized(cancellationRequested) { + if (cancellationRequested.get()) return RemoteOperationResult(OperationCancelledException()) + } + + if (!FileStorageUtils.isValidExtFilename(file.fileName)) { + mainThreadHandler.post { context.get()?.showToast(R.string.download_download_invalid_local_file_name) } + return RemoteOperationResult(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME) + } + + val operationContext = context.get() + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR) + + val tmpFile = File(tmpPath) + val (downloadOp, downloadResult) = executeDownload(client, operationContext) + + if (!downloadResult.isSuccess) return downloadResult + + timestampForModification = downloadOp.modificationTimestamp + etag = downloadOp.etag + + if (file.isEncrypted) { + handleDecryption(client, operationContext, tmpFile)?.let { return it } + } + + val result = when (downloadType) { + DownloadType.DOWNLOAD if !file.isEncrypted -> handleFileMove(tmpFile, downloadResult) + DownloadType.EXPORT -> handleExport(operationContext, tmpFile, downloadResult) + else -> downloadResult + } + + Log_OC.i(TAG, "Download of ${file.remotePath} to $savePath: ${result.logMessage}") + return result + } + + private data class DownloadResult( + val operation: DownloadFileRemoteOperation, + val result: RemoteOperationResult + ) + + @Suppress("UNCHECKED_CAST") + private fun executeDownload(client: OwnCloudClient, operationContext: Context): DownloadResult { + val operation = DownloadFileRemoteOperation(file.remotePath, tmpFolder).also { op -> + if (downloadType == DownloadType.DOWNLOAD) { + dataTransferListeners.forEach { op.addDatatransferProgressListener(it) } + } + } + val result = operation.execute(client.toNextcloudClient(operationContext)) as RemoteOperationResult + return DownloadResult(operation, result) + } + + private data class EncryptionKeys(val key: String?, val nonce: String?, val authTag: String?) + + private fun extractEncryptionKeys(metadata: Any): EncryptionKeys? = when (metadata) { + is DecryptedFolderMetadataFile -> metadata.metadata.files[file.encryptedFileName]?.let { + EncryptionKeys(it.key, it.nonce, it.authenticationTag) + } + + is DecryptedFolderMetadataFileV1 -> metadata.files[file.encryptedFileName]?.let { + EncryptionKeys(it.encrypted.key, it.initializationVector, it.authenticationTag) + } + + else -> null + } + + @Suppress("DEPRECATION") + private fun handleDecryption( + client: OwnCloudClient, + operationContext: Context, + tmpFile: File + ): RemoteOperationResult? { + val fileDataStorageManager = FileDataStorageManager(user, operationContext.contentResolver) + val parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.parentRemotePath) + val metadata = EncryptionUtils.downloadFolderMetadata(parent, client, operationContext, user) + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + + val (keyString, nonceString, authenticationTagString) = extractEncryptionKeys(metadata) + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + + val key = EncryptionUtils.decodeStringToBase64Bytes(keyString) + val iv = EncryptionUtils.decodeStringToBase64Bytes(nonceString) + + return runCatching { + val cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv) + EncryptionUtils.decryptFile( + cipher, + tmpFile, + File(savePath), + authenticationTagString, + ArbitraryDataProviderImpl(operationContext), + user + ) + }.fold( + onSuccess = { null }, + onFailure = { RemoteOperationResult(it as? Exception ?: Exception(it)) } + ) + } + + private fun handleFileMove(tmpFile: File, currentResult: RemoteOperationResult): RemoteOperationResult { + val newFile = File(savePath) + newFile.parentFile?.takeIf { !it.exists() }?.also { + if (!it.mkdirs()) Log_OC.e(TAG, "Unable to create parent folder ${it.absolutePath}") + } + + val tempFilePath = tmpFile.toPath() + val newFilePath = newFile.toPath() + + Log_OC.d(TAG, "trying to move: $tempFilePath to $newFilePath") + + return try { + Files.move(tempFilePath, newFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING) + newFile.setLastModified(file.modificationTimestamp) + .also { Log_OC.d(TAG, "Last modified set: $it") } + currentResult + } catch (e: IOException) { + Log_OC.e(TAG, "Failed to move file to ${newFile.absolutePath}: ${e.message}") + RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED) + } + } + + private fun handleExport( + operationContext: Context, + tmpFile: File, + currentResult: RemoteOperationResult + ): RemoteOperationResult { + FileExportUtils().exportFile(file.fileName, file.mimeType, operationContext.contentResolver, null, tmpFile) + if (!tmpFile.delete()) Log_OC.e(TAG, "Deletion of ${tmpFile.absolutePath} failed!") + return currentResult + } + + fun cancel() { + cancellationRequested.set(true) + } + + fun addDownloadDataTransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { dataTransferListeners.add(listener) } + } + + fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { dataTransferListeners.remove(listener) } + } + + companion object { + private val TAG = DownloadFileOperation::class.java.simpleName + } +}