From c944457e2d25a9be92bae05850ca617e101f9bfb Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Thu, 6 Feb 2025 12:35:05 +0100 Subject: [PATCH 1/7] feat: wire cells sdk integration (#WPB-15743) --- .../wire/kalium/logic/data/cells/CellNode.kt | 50 ++++++ .../kalium/logic/data/cells/PreCheckResult.kt | 23 +++ gradle/libs.versions.toml | 4 + .../kalium/logic/feature/UserSessionScope.kt | 4 + .../logic/feature/cells/CellsRepository.kt | 117 +++++++++++++ .../kalium/logic/feature/cells/CellsScope.kt | 78 +++++++++ .../cells/usecase/CancelDraftUseCase.kt | 40 +++++ .../cells/usecase/DeleteCellFileUseCase.kt | 41 +++++ .../cells/usecase/GetCellFilesUseCase.kt | 40 +++++ .../cells/usecase/PublishDraftUseCase.kt | 40 +++++ .../cells/usecase/UploadToCellUseCase.kt | 80 +++++++++ .../api/unbound/cells/GetFilesResponseDTO.kt | 32 ++++ .../api/unbound/cells/PreCheckResultDTO.kt | 23 +++ network/build.gradle.kts | 3 + .../aws/AwsProgressListenerInterceptor.kt | 142 ++++++++++++++++ .../network/cells/aws/CellsAwsClientJvm.kt | 159 ++++++++++++++++++ .../network/cells/aws/MetadataHeaders.kt | 24 +++ .../com/wire/kalium/network/cells/CellsApi.kt | 144 ++++++++++++++++ .../network/cells/aws/CellsAwsClient.kt | 27 +++ .../network/cells/aws/CellsCredentials.kt | 24 +++ .../UnboundNetworkContainer.kt | 10 ++ .../wire/kalium/network/utils/NetworkUtils.kt | 34 ++++ settings.gradle.kts | 15 ++ 23 files changed, 1154 insertions(+) create mode 100644 data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt create mode 100644 data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt create mode 100644 network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt create mode 100644 network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt create mode 100644 network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt create mode 100644 network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt create mode 100644 network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt create mode 100644 network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt create mode 100644 network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt create mode 100644 network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt new file mode 100644 index 00000000000..22b2e6c6d49 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.cells + +import com.wire.kalium.network.api.unbound.cells.CellNodeDTO + +data class CellNode( + val uuid: String, + val versionId: String, + val path: String, + val eTag: String? = null, + val type: String? = null, + val isRecycleBin: Boolean = false, + val isDraft: Boolean = false, +) + +fun CellNodeDTO.toModel() = CellNode( + uuid = uuid, + versionId = versionId, + path = path, + eTag = eTag, + type = type, + isRecycleBin = isRecycleBin, + isDraft = isDraft, +) + +fun CellNode.toDto() = CellNodeDTO( + uuid = uuid, + versionId = versionId, + path = path, + eTag = eTag, + type = type, + isRecycleBin = isRecycleBin, + isDraft = isDraft, +) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt new file mode 100644 index 00000000000..2cd56fb5657 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt @@ -0,0 +1,23 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.cells + +sealed interface PreCheckResult { + data object Success : PreCheckResult + data class FileExists(val suggestedFilename: String) : PreCheckResult +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c775d1c944..3992da7cbe6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,6 +68,7 @@ jmh = "1.37" jmhReport = "0.9.6" xerialDriver = "3.45.3.0" kotlinx-io = "0.5.3" +cells-sdk = "0.1.1-alpha01" [plugins] # Home-made convention plugins @@ -226,3 +227,6 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } micrometer = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" } slf4js = { module = "org.slf4j:slf4j-simple", version.ref = "slf4js" } + +# cells +wire-cells-sdk = { module = "com.wire:cells-sdk-kmp", version.ref = "cells-sdk" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 14bdb75e474..a0ade651a1b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -185,6 +185,7 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProviderImpl import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCaseImpl +import com.wire.kalium.logic.feature.cells.CellsScope import com.wire.kalium.logic.feature.client.ClientScope import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCaseImpl @@ -888,6 +889,9 @@ class UserSessionScope internal constructor( globalPreferences, ) + val cells: CellsScope + get() = CellsScope(globalScope) + val persistMessage: PersistMessageUseCase get() = PersistMessageUseCaseImpl(messageRepository, userId, NotificationEventsManagerImpl) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt new file mode 100644 index 00000000000..6e43824239e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt @@ -0,0 +1,117 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.data.cells.PreCheckResult +import com.wire.kalium.logic.data.cells.toDto +import com.wire.kalium.logic.data.cells.toModel +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.network.cells.CellsApi +import com.wire.kalium.network.cells.aws.CellsAwsClient +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.withContext +import okio.Path + +interface CellsRepository { + suspend fun preCheck(node: CellNode): Either + suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either + suspend fun getFiles(cellName: String): Either> + suspend fun deleteFile(node: CellNode): Either + suspend fun cancelDraft(node: CellNode): Either + suspend fun publishDraft(node: CellNode): Either +} + +internal class CellsDataSource internal constructor( + private val cellsApi: CellsApi, + private val awsClient: CellsAwsClient, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : CellsRepository { + + override suspend fun preCheck(node: CellNode): Either { + return withContext(dispatchers.io) { + wrapApiRequest { + cellsApi.preCheck(node.path) + }.map { result -> + if (result.fileExists) { + PreCheckResult.FileExists(result.suggestedPath ?: node.path) + } else { + PreCheckResult.Success + } + } + } + } + + @Suppress("TooGenericExceptionCaught") + override suspend fun uploadFile( + path: Path, + node: CellNode, + onProgressUpdate: (Long) -> Unit, + ): Either = withContext(dispatchers.io) { + try { + awsClient.upload(path, node.toDto(), onProgressUpdate) + Either.Right(Unit) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + kaliumLogger.e("Failed to upload file", e) + Either.Left(NetworkFailure.ServerMiscommunication(e)) + } + } + + override suspend fun getFiles(cellName: String): Either> = + withContext(dispatchers.io) { + wrapApiRequest { + cellsApi.getFiles(cellName) + }.map { response -> + response.nodes + .filterNot { it.isRecycleBin } + .map { it.toModel() } + } + } + + override suspend fun deleteFile(node: CellNode): Either { + return withContext(dispatchers.io) { + wrapApiRequest { + cellsApi.delete(node.toDto()) + } + } + } + + override suspend fun publishDraft(node: CellNode): Either { + return withContext(dispatchers.io) { + wrapApiRequest { + cellsApi.publishDraft(node.toDto()) + } + } + } + + override suspend fun cancelDraft(node: CellNode): Either { + return withContext(dispatchers.io) { + wrapApiRequest { + cellsApi.cancelDraft(node.toDto()) + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt new file mode 100644 index 00000000000..5e9ee6591d4 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt @@ -0,0 +1,78 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells + +import com.wire.kalium.logic.GlobalKaliumScope +import com.wire.kalium.logic.feature.cells.usecase.CancelDraftUseCase +import com.wire.kalium.logic.feature.cells.usecase.CancelDraftUseCaseImpl +import com.wire.kalium.logic.feature.cells.usecase.DeleteCellFileUseCase +import com.wire.kalium.logic.feature.cells.usecase.DeleteCellFileUseCaseImpl +import com.wire.kalium.logic.feature.cells.usecase.GetCellFilesUseCase +import com.wire.kalium.logic.feature.cells.usecase.GetCellFilesUseCaseImpl +import com.wire.kalium.logic.feature.cells.usecase.PublishDraftUseCase +import com.wire.kalium.logic.feature.cells.usecase.PublishDraftUseCaseImpl +import com.wire.kalium.logic.feature.cells.usecase.UploadToCellUseCase +import com.wire.kalium.logic.feature.cells.usecase.UploadToCellUseCaseImpl +import com.wire.kalium.network.cells.aws.CellsAwsClient +import com.wire.kalium.network.cells.aws.CellsCredentials +import com.wire.kalium.network.cells.aws.cellsAwsClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +class CellsScope internal constructor( + private val globalScope: GlobalKaliumScope, +) : CoroutineScope { + + override val coroutineContext: CoroutineContext = SupervisorJob() + + // Temporary hardcoded credentials and server URL + private val cellClientCredentials: CellsCredentials + get() = CellsCredentials( + serverUrl = "https://service.zeta.pydiocells.com", + accessToken = "", + gatewaySecret = "", + ) + + private val cellAwsClient: CellsAwsClient + get() = cellsAwsClient(cellClientCredentials) + + private val cellsRepository: CellsRepository + get() = CellsDataSource( + cellsApi = globalScope.unboundNetworkContainer.cellsApi(cellClientCredentials), + awsClient = cellAwsClient + ) + + val getCellFiles: GetCellFilesUseCase + get() = GetCellFilesUseCaseImpl(cellsRepository) + + val uploadToCell: UploadToCellUseCase + get() = UploadToCellUseCaseImpl( + scope = this, + cellsRepository = cellsRepository + ) + + val deleteFromCell: DeleteCellFileUseCase + get() = DeleteCellFileUseCaseImpl(cellsRepository) + + val cancelDraft: CancelDraftUseCase + get() = CancelDraftUseCaseImpl(cellsRepository) + + val publishDraft: PublishDraftUseCase + get() = PublishDraftUseCaseImpl(cellsRepository) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt new file mode 100644 index 00000000000..cf489449018 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells.usecase + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.feature.cells.CellsRepository +import com.wire.kalium.logic.functional.Either + +interface CancelDraftUseCase { + /** + * Cancels the draft of the cell node. + * @param node Cell node to cancel the draft for. + * @return Either. Result of the operation. + */ + suspend operator fun invoke(node: CellNode): Either +} + +internal class CancelDraftUseCaseImpl( + private val cellsRepository: CellsRepository +) : CancelDraftUseCase { + override suspend operator fun invoke(node: CellNode): Either { + return cellsRepository.cancelDraft(node) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt new file mode 100644 index 00000000000..5b106f60d6b --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells.usecase + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.feature.cells.CellsRepository +import com.wire.kalium.logic.functional.Either + +interface DeleteCellFileUseCase { + /** + * Delete a file from the cell. + * Note: Delete operation is asynchronous on Cells Server. Actual delete does not happen immediately. + * @param node The node of the file to delete. + * @return Either a [NetworkFailure] or [Unit]. + */ + suspend operator fun invoke(node: CellNode): Either +} + +internal class DeleteCellFileUseCaseImpl( + private val cellsRepository: CellsRepository +) : DeleteCellFileUseCase { + override suspend operator fun invoke(node: CellNode): Either { + return cellsRepository.deleteFile(node) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt new file mode 100644 index 00000000000..b0e0edab216 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells.usecase + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.feature.cells.CellsRepository +import com.wire.kalium.logic.functional.Either + +interface GetCellFilesUseCase { + /** + * Get the list of files in the cell. + * @param cellName the name of the cell. + * @return the list of files in the cell or [NetworkFailure]. + */ + suspend operator fun invoke(cellName: String): Either> +} + +internal class GetCellFilesUseCaseImpl( + private val cellsRepository: CellsRepository +) : GetCellFilesUseCase { + override suspend operator fun invoke(cellName: String): Either> { + return cellsRepository.getFiles(cellName) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt new file mode 100644 index 00000000000..08d33b447c9 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells.usecase + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.feature.cells.CellsRepository +import com.wire.kalium.logic.functional.Either + +interface PublishDraftUseCase { + /** + * Publish the draft in the cell. + * @param node the cell node. + * @return [NetworkFailure] or [Unit]. + */ + suspend operator fun invoke(node: CellNode): Either +} + +internal class PublishDraftUseCaseImpl( + private val cellsRepository: CellsRepository +) : PublishDraftUseCase { + override suspend operator fun invoke(node: CellNode): Either { + return cellsRepository.publishDraft(node) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt new file mode 100644 index 00000000000..b1ddc7baef7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt @@ -0,0 +1,80 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.cells.usecase + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.cells.CellNode +import com.wire.kalium.logic.data.cells.PreCheckResult +import com.wire.kalium.logic.feature.cells.CellsRepository +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.left +import com.wire.kalium.logic.functional.map +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import okio.Path + +interface UploadToCellUseCase { + /** + * Uploads a file to the cell. + * This use case will check if the file already exists in the cell and if it does, it will update the file name + * with name suggested by Cells SDK. + * @param path Path to the file to upload. + * @param node New cell node for the file. Must have a valid UUID, VersionId and Path. + * @param size Size of the file in bytes. + * @param progress Callback to report the upload progress. + * @return Deferred>. Deferred result of the upload. Can be cancelled to cancel upload. + */ + suspend operator fun invoke( + path: Path, + node: CellNode, + size: Long, + progress: (Float) -> Unit, + ): Deferred> +} + +internal class UploadToCellUseCaseImpl( + private val cellsRepository: CellsRepository, + private val scope: CoroutineScope, +) : UploadToCellUseCase { + + override suspend operator fun invoke( + path: Path, + node: CellNode, + size: Long, + progress: (Float) -> Unit, + ): Deferred> = + cellsRepository.preCheck(node).fold( + fnR = { result -> + val checkedNode = when (result) { + is PreCheckResult.FileExists -> node.copy(path = result.suggestedFilename) + PreCheckResult.Success -> node + } + scope.async { + cellsRepository.uploadFile( + path = path, + node = checkedNode, + onProgressUpdate = { uploaded -> progress(uploaded.toFloat() / size) } + ).map { checkedNode } + } + }, + fnL = { CompletableDeferred(it.left()) }, + ) +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt new file mode 100644 index 00000000000..1693b357b78 --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.unbound.cells + +data class GetFilesResponseDTO( + val nodes: List +) + +data class CellNodeDTO( + val uuid: String, + val versionId: String, + val path: String, + val eTag: String? = null, + val type: String? = null, + val isRecycleBin: Boolean = false, + val isDraft: Boolean = false, +) diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt new file mode 100644 index 00000000000..2c95ab353cc --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt @@ -0,0 +1,23 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.unbound.cells + +data class PreCheckResultDTO( + val fileExists: Boolean = false, + val suggestedPath: String? = null, +) diff --git a/network/build.gradle.kts b/network/build.gradle.kts index fce0b545667..4c5c91b6465 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -68,6 +68,8 @@ kotlin { // UUIDs implementation(libs.benAsherUUID) + + implementation(libs.wire.cells.sdk) } } val commonTest by getting { @@ -96,6 +98,7 @@ kotlin { addCommonKotlinJvmSourceDir() dependencies { implementation(libs.ktor.okHttp) + implementation(awssdk.services.s3) } } val appleMain by getting { diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt new file mode 100644 index 00000000000..7fd304a4d5a --- /dev/null +++ b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt @@ -0,0 +1,142 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells.aws + +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.http.response.HttpResponse +import aws.smithy.kotlin.runtime.io.SdkBuffer +import aws.smithy.kotlin.runtime.io.SdkByteReadChannel +import aws.smithy.kotlin.runtime.io.SdkSource +import aws.smithy.kotlin.runtime.io.readAll + +internal open class AwsProgressListenerInterceptor( + private val progressListener: (Long) -> Unit +) : HttpInterceptor { + fun convertBodyWithProgressUpdates(httpBody: HttpBody): HttpBody { + return when (httpBody) { + is HttpBody.ChannelContent -> { + SdkByteReadChannelWithProgressUpdates( + httpBody, + progressListener + ) + } + is HttpBody.SourceContent -> { + SourceContentWithProgressUpdates( + httpBody, + progressListener + ) + } + is HttpBody.Bytes -> { + httpBody + } + is HttpBody.Empty -> { + httpBody + } + } + } + + internal class SourceContentWithProgressUpdates( + private val sourceContent: SourceContent, + private val progressListener: (Long) -> Unit + ) : HttpBody.SourceContent() { + private val delegate = sourceContent.readFrom() + private var uploaded = 0L + override val contentLength: Long? + get() = sourceContent.contentLength + + override fun readFrom(): SdkSource { + return object : SdkSource { + override fun close() { + delegate.close() + } + + override fun read(sink: SdkBuffer, limit: Long): Long { + return delegate.read(sink, limit).also { + if (it > 0) { + uploaded += it + progressListener(uploaded) + } + } + } + } + } + } + + internal class SdkByteReadChannelWithProgressUpdates( + private val httpBody: ChannelContent, + private val progressListener: (Long) -> Unit + ) : HttpBody.ChannelContent() { + val delegate = httpBody.readFrom() + private var uploaded = 0L + override val contentLength: Long? + get() = httpBody.contentLength + override fun readFrom(): SdkByteReadChannel { + return object : SdkByteReadChannel by delegate { + override val availableForRead: Int + get() = delegate.availableForRead + + override val isClosedForRead: Boolean + get() = delegate.isClosedForRead + + override val isClosedForWrite: Boolean + get() = delegate.isClosedForWrite + + override fun cancel(cause: Throwable?): Boolean { + return delegate.cancel(cause) + } + + override suspend fun read(sink: SdkBuffer, limit: Long): Long { + return delegate.readAll(sink).also { + if (it > 0) { + uploaded += it + progressListener(uploaded) + } + } + } + } + } + } + + internal class DownloadProgressListenerInterceptor( + progressListener: (Long) -> Unit + ) : AwsProgressListenerInterceptor(progressListener) { + override suspend fun modifyBeforeDeserialization( + context: ProtocolResponseInterceptorContext + ): HttpResponse { + val body = convertBodyWithProgressUpdates(context.protocolResponse.body) + return HttpResponse(context.protocolResponse.status, context.protocolResponse.headers, body) + } + } + + internal class UploadProgressListenerInterceptor( + progressListener: (Long) -> Unit + ) : AwsProgressListenerInterceptor(progressListener) { + override suspend fun modifyBeforeTransmit( + context: ProtocolRequestInterceptorContext + ): HttpRequest { + val builder = context.protocolRequest.toBuilder() + builder.body = convertBodyWithProgressUpdates(builder.body) + return builder.build() + } + } +} diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt new file mode 100644 index 00000000000..297f10ed701 --- /dev/null +++ b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt @@ -0,0 +1,159 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells.aws + +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.services.s3.S3Client +import aws.sdk.kotlin.services.s3.completeMultipartUpload +import aws.sdk.kotlin.services.s3.createMultipartUpload +import aws.sdk.kotlin.services.s3.model.CompletedMultipartUpload +import aws.sdk.kotlin.services.s3.model.CompletedPart +import aws.sdk.kotlin.services.s3.putObject +import aws.sdk.kotlin.services.s3.uploadPart +import aws.sdk.kotlin.services.s3.withConfig +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.content.ByteStream +import aws.smithy.kotlin.runtime.content.asByteStream +import aws.smithy.kotlin.runtime.net.url.Url +import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import okhttp3.internal.http2.Header +import okio.Path +import java.io.RandomAccessFile +import java.nio.ByteBuffer + +actual fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient = CellsAwsClientJvm(credentials) + +private class CellsAwsClientJvm( + private val credentials: CellsCredentials +) : CellsAwsClient { + + private companion object { + const val DEFAULT_REGION = "us-east-1" + const val DEFAULT_BUCKET_NAME = "io" + const val MAX_REGULAR_UPLOAD_SIZE = 100 * 1024 * 1024L + const val MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024 + } + + private val s3Client: S3Client by lazy { buildS3Client() } + + private fun buildS3Client() = with(credentials) { + S3Client { + region = DEFAULT_REGION + enableAwsChunked = false + credentialsProvider = StaticCredentialsProvider( + Credentials( + accessKeyId = accessToken, + secretAccessKey = gatewaySecret, + ) + ) + endpointUrl = Url.parse(serverUrl) + } + } + + override suspend fun upload(path: Path, node: CellNodeDTO, onProgressUpdate: (Long) -> Unit) { + val length = path.toFile().length() + if (length > MAX_REGULAR_UPLOAD_SIZE) { + uploadMultipart(path, node, onProgressUpdate) + } else { + uploadRegular(path, node, onProgressUpdate) + } + } + + private suspend fun uploadRegular(path: Path, node: CellNodeDTO, onProgressUpdate: (Long) -> Unit) { + withS3Client(uploadProgressListener = onProgressUpdate) { + putObject { + bucket = DEFAULT_BUCKET_NAME + key = node.path + metadata = node.createDraftNodeMetaData() + body = path.toFile().asByteStream() + } + } + } + + private suspend fun uploadMultipart(path: Path, node: CellNodeDTO, onProgressUpdate: (Long) -> Unit) { + val buffer = ByteBuffer.allocate(MULTIPART_CHUNK_SIZE) + var number = 1 + val completed = mutableListOf() + withS3Client { + val requestId = createMultipartUpload { + bucket = DEFAULT_BUCKET_NAME + key = node.path + }.uploadId + RandomAccessFile(path.toFile(), "r").use { file -> + val fileSize = file.length() + var position = 0L + while (position < fileSize) { + file.seek(position) + val bytesRead = file.channel.read(buffer) + onProgressUpdate(position + bytesRead) + buffer.flip() + val partData = ByteArray(bytesRead) + buffer.get(partData, 0, bytesRead) + val response = uploadPart { + bucket = DEFAULT_BUCKET_NAME + key = node.path + uploadId = requestId + partNumber = number + contentLength = bytesRead.toLong() + body = ByteStream.fromBytes(partData) + } + completed.add( + CompletedPart { + partNumber = number + eTag = response.eTag + } + ) + buffer.clear() + position += bytesRead + number++ + } + } + completeMultipartUpload { + bucket = DEFAULT_BUCKET_NAME + key = node.path + uploadId = requestId + multipartUpload = CompletedMultipartUpload { + parts = completed + } + } + } + } + + private suspend fun withS3Client( + uploadProgressListener: ((Long) -> Unit)? = null, + downloadProgressListener: ((Long) -> Unit)? = null, + block: suspend S3Client.() -> T, + ): T = + s3Client.withConfig { + if (uploadProgressListener != null) { + Header.TARGET_PATH + interceptors.add(AwsProgressListenerInterceptor.UploadProgressListenerInterceptor(uploadProgressListener)) + } + if (downloadProgressListener != null) { + interceptors.add(AwsProgressListenerInterceptor.DownloadProgressListenerInterceptor(downloadProgressListener)) + } + }.use { + block(it) + } +} + +private fun CellNodeDTO.createDraftNodeMetaData() = mapOf( + MetadataHeaders.DRAFT_MODE to "true", + MetadataHeaders.CREATE_RESOURCE_UUID to uuid, + MetadataHeaders.CREATE_VERSION_ID to versionId, +) diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt new file mode 100644 index 00000000000..c2bff330365 --- /dev/null +++ b/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells.aws + +internal object MetadataHeaders { + const val DRAFT_MODE = "draft_mode" + const val CREATE_RESOURCE_UUID = "create-resource-uuid" + const val CREATE_VERSION_ID = "create-version-id" +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt new file mode 100644 index 00000000000..cb0d324b46c --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt @@ -0,0 +1,144 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells + +import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi +import com.wire.kalium.cells.sdk.kmp.model.RestActionParameters +import com.wire.kalium.cells.sdk.kmp.model.RestCreateCheckRequest +import com.wire.kalium.cells.sdk.kmp.model.RestIncomingNode +import com.wire.kalium.cells.sdk.kmp.model.RestLookupRequest +import com.wire.kalium.cells.sdk.kmp.model.RestNode +import com.wire.kalium.cells.sdk.kmp.model.RestNodeCollection +import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocator +import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocators +import com.wire.kalium.cells.sdk.kmp.model.RestNodeVersionsFilter +import com.wire.kalium.cells.sdk.kmp.model.RestPromoteParameters +import com.wire.kalium.cells.sdk.kmp.model.RestVersionCollection +import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import com.wire.kalium.network.api.unbound.cells.GetFilesResponseDTO +import com.wire.kalium.network.api.unbound.cells.PreCheckResultDTO +import com.wire.kalium.network.cells.aws.CellsCredentials +import com.wire.kalium.network.session.installAuth +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.network.utils.mapSuccess +import com.wire.kalium.network.utils.wrapCellsResponse +import io.ktor.client.HttpClient +import io.ktor.client.plugins.auth.providers.BearerAuthProvider +import io.ktor.client.plugins.auth.providers.BearerTokens + +interface CellsApi { + suspend fun getFiles(cellName: String): NetworkResponse + suspend fun delete(node: CellNodeDTO): NetworkResponse + suspend fun cancelDraft(node: CellNodeDTO): NetworkResponse + suspend fun publishDraft(node: CellNodeDTO): NetworkResponse + suspend fun preCheck(path: String): NetworkResponse +} + +internal class CellsApiImpl( + credentials: CellsCredentials, + httpClient: HttpClient +) : CellsApi { + + private var nodeServiceApi: NodeServiceApi = NodeServiceApi( + baseUrl = "${credentials.serverUrl}/a", + httpClient = httpClient.config { + installAuth( + BearerAuthProvider( + loadTokens = { BearerTokens(credentials.accessToken, "") }, + refreshTokens = { null }, + realm = null + ) + ) + } + ) + + override suspend fun getFiles(cellName: String): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.lookup( + RestLookupRequest( + locators = RestNodeLocators(listOf(RestNodeLocator(path = "$cellName/*"))) + ) + ) + }.mapSuccess { response -> response.toDto() } + + override suspend fun delete(node: CellNodeDTO): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.performAction( + name = NodeServiceApi.NamePerformAction.delete, + parameters = RestActionParameters( + nodes = listOf(RestNodeLocator(path = node.path)) + ) + ) + }.mapSuccess {} + + override suspend fun publishDraft(node: CellNodeDTO): NetworkResponse = + getNodeDraftVersions(node).mapSuccess { response -> + wrapCellsResponse { + val version = response.versions?.firstOrNull() ?: error("Draft version not found") + nodeServiceApi.promoteVersion(node.uuid, version.versionId, RestPromoteParameters(publish = true)) + } + } + + override suspend fun cancelDraft(node: CellNodeDTO): NetworkResponse = + getNodeDraftVersions(node).mapSuccess { response -> + wrapCellsResponse { + val version = response.versions?.firstOrNull() ?: error("Draft version not found") + nodeServiceApi.deleteVersion(node.uuid, version.versionId) + } + } + + private suspend fun getNodeDraftVersions(node: CellNodeDTO): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.nodeVersions(node.uuid, RestNodeVersionsFilter()) + } + + override suspend fun preCheck(path: String): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.createCheck( + RestCreateCheckRequest( + inputs = listOf(RestIncomingNode(locator = RestNodeLocator(path))), + findAvailablePath = true + ) + ) + }.mapSuccess { response -> + response.results?.firstOrNull()?.let { + PreCheckResultDTO( + fileExists = it.exists ?: false, + suggestedPath = it.nextPath, + ) + } ?: PreCheckResultDTO() + } +} + +private fun RestNode.isDraft(): Boolean { + return userMetadata?.firstOrNull { it.namespace == "usermeta-draft" }?.jsonValue == "true" +} + +private fun RestNodeCollection.toDto() = GetFilesResponseDTO( + nodes = nodes?.map { node -> + CellNodeDTO( + uuid = node.uuid, + versionId = node.versionMeta?.versionId ?: "", + path = node.path, + type = node.type?.name ?: "", + eTag = node.storageETag, + isRecycleBin = node.isRecycleBin ?: false, + isDraft = node.isDraft(), + ) + } ?: emptyList() +) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt new file mode 100644 index 00000000000..1b905dbb976 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt @@ -0,0 +1,27 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells.aws + +import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import okio.Path + +interface CellsAwsClient { + suspend fun upload(path: Path, node: CellNodeDTO, onProgressUpdate: (Long) -> Unit) +} + +expect fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt new file mode 100644 index 00000000000..01432fa4071 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.cells.aws + +data class CellsCredentials( + val serverUrl: String, + val accessToken: String, + val gatewaySecret: String, +) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt index b82e6b1dc70..c1172ec16b1 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt @@ -21,8 +21,11 @@ package com.wire.kalium.network.networkContainer import com.wire.kalium.network.UnboundNetworkClient import com.wire.kalium.network.api.base.unbound.acme.ACMEApi import com.wire.kalium.network.api.base.unbound.acme.ACMEApiImpl +import com.wire.kalium.network.cells.CellsApi +import com.wire.kalium.network.cells.CellsApiImpl import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApi import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApiImpl +import com.wire.kalium.network.cells.aws.CellsCredentials import com.wire.kalium.network.clearTextTrafficEngine import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.session.CertificatePinning @@ -31,6 +34,8 @@ import io.ktor.client.engine.HttpClientEngine interface UnboundNetworkContainer { val serverConfigApi: ServerConfigApi val acmeApi: ACMEApi + + fun cellsApi(credentials: CellsCredentials): CellsApi } private interface UnboundNetworkClientProvider { @@ -95,4 +100,9 @@ class UnboundNetworkContainerCommon( unboundNetworkClient, unboundClearTextTrafficNetworkClient ) + + override fun cellsApi(credentials: CellsCredentials): CellsApi = CellsApiImpl( + credentials = credentials, + httpClient = unboundNetworkClient.httpClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt index f23846f27c2..51c72e5a3c6 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt @@ -34,6 +34,7 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.URLProtocol import io.ktor.http.Url import io.ktor.http.isSuccess +import kotlinx.coroutines.CancellationException import kotlinx.serialization.SerializationException internal fun HttpRequestBuilder.setWSSUrl(baseUrl: Url, vararg path: String) { @@ -277,6 +278,39 @@ suspend fun wrapFederationResponse( } } +/** + * For Cells Sdk calls. + * Wraps a producer of [HttpResponse] and attempts to parse the server response based on the [BodyType]. + * @return - Successful response (HTTP Status Codes from 200 to 299): + * a [NetworkResponse.Success] with the expected [BodyType] will be returned. + * + * - Unsuccessful response (any other HTTP Status Code): + * a [NetworkResponse.Error] with a [KaliumException]. + * + * - Exceptions failure to reach server or parse response: + * a [NetworkResponse.Error] containing a [KaliumException.GenericError] + * + */ +suspend inline fun wrapCellsResponse( + performRequest: () -> com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse +): NetworkResponse = + try { + + val response = performRequest() + val status = HttpStatusCode.fromValue(response.status) + + if (status.isSuccess()) { + NetworkResponse.Success(response.body(), emptyMap(), response.status) + } else { + NetworkResponse.Error(KaliumException.ServerError(ErrorResponse(response.status, "", ""))) + } + + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + NetworkResponse.Error(KaliumException.GenericError(e)) + } + /** * Due to the "shared" status code limitations nature of some endpoints. * We need to first delegate to status code based exceptions and if parse fails go for federated error, diff --git a/settings.gradle.kts b/settings.gradle.kts index 5349f730705..52536a42b04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,3 +39,18 @@ pluginManagement { mavenCentral() } } + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("awssdk") { + from("aws.sdk.kotlin:version-catalog:1.3.112") + } + } +} From 30735f5a4b2934d218d15ff66cea2e030e3e7be2 Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Thu, 6 Feb 2025 18:20:15 +0100 Subject: [PATCH 2/7] feat: cells module (#WPB-15743) --- cells/.gitignore | 1 + cells/build.gradle.kts | 53 ++++++++++++++++++ .../data}/AwsProgressListenerInterceptor.kt | 2 +- .../kalium/cells/data}/CellsAwsClientJvm.kt | 7 ++- .../kalium/cells/data}/MetadataHeaders.kt | 2 +- .../com/wire/kalium}/cells/CellsScope.kt | 56 +++++++++++-------- .../com/wire/kalium/cells/data}/CellsApi.kt | 56 ++++++++++--------- .../wire/kalium/cells/data}/CellsAwsClient.kt | 9 +-- .../wire/kalium/cells/data/CellsDataSource.kt | 32 +++++------ .../kalium/cells/data/model/CellNodeDTO.kt | 25 +++++++-- .../cells/data/model/GetFilesResponseDTO.kt | 30 ++++++++++ .../cells/data/model}/PreCheckResultDTO.kt | 4 +- .../kalium/cells/domain/CellsRepository.kt | 33 +++++++++++ .../kalium/cells/domain/model/CellNode.kt | 8 +-- .../cells/domain/model}/CellsCredentials.kt | 4 +- .../cells/domain/model}/PreCheckResult.kt | 8 +-- .../domain}/usecase/CancelDraftUseCase.kt | 10 ++-- .../domain}/usecase/DeleteCellFileUseCase.kt | 10 ++-- .../domain}/usecase/GetCellFilesUseCase.kt | 10 ++-- .../domain}/usecase/PublishDraftUseCase.kt | 10 ++-- .../domain}/usecase/UploadToCellUseCase.kt | 12 ++-- .../kalium/logic/feature/UserSessionScope.kt | 4 -- network/build.gradle.kts | 3 - .../UnboundNetworkContainer.kt | 13 ++--- .../wire/kalium/network/utils/NetworkUtils.kt | 36 +----------- 25 files changed, 266 insertions(+), 172 deletions(-) create mode 100644 cells/.gitignore create mode 100644 cells/build.gradle.kts rename {network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws => cells/src/androidMain/kotlin/com/wire/kalium/cells/data}/AwsProgressListenerInterceptor.kt (99%) rename {network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws => cells/src/androidMain/kotlin/com/wire/kalium/cells/data}/CellsAwsClientJvm.kt (95%) rename {network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws => cells/src/androidMain/kotlin/com/wire/kalium/cells/data}/MetadataHeaders.kt (95%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature => cells/src/commonMain/kotlin/com/wire/kalium}/cells/CellsScope.kt (51%) rename {network/src/commonMain/kotlin/com/wire/kalium/network/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/data}/CellsApi.kt (78%) rename {network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws => cells/src/commonMain/kotlin/com/wire/kalium/cells/data}/CellsAwsClient.kt (75%) rename logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt => cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt (76%) rename data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt => cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt (64%) create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/GetFilesResponseDTO.kt rename {network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model}/PreCheckResultDTO.kt (90%) create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt rename network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt (86%) rename {network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model}/CellsCredentials.kt (90%) rename {data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model}/PreCheckResult.kt (76%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain}/usecase/CancelDraftUseCase.kt (82%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain}/usecase/DeleteCellFileUseCase.kt (82%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain}/usecase/GetCellFilesUseCase.kt (81%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain}/usecase/PublishDraftUseCase.kt (81%) rename {logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells => cells/src/commonMain/kotlin/com/wire/kalium/cells/domain}/usecase/UploadToCellUseCase.kt (90%) diff --git a/cells/.gitignore b/cells/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/cells/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/cells/build.gradle.kts b/cells/build.gradle.kts new file mode 100644 index 00000000000..437c87cff4c --- /dev/null +++ b/cells/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +plugins { + id(libs.plugins.android.library.get().pluginId) + id(libs.plugins.kotlin.multiplatform.get().pluginId) + id(libs.plugins.kalium.library.get().pluginId) +} + +kaliumLibrary { + multiplatform { enableJs.set(false) } +} + +kotlin { + explicitApi() + sourceSets { + commonMain { + dependencies { + implementation(project(":network")) + implementation(project(":util")) + implementation(project(":logic")) + implementation(libs.coroutines.core) + implementation(libs.ktor.authClient) + implementation(libs.okio.core) + implementation(libs.wire.cells.sdk) + } + } + commonTest { + dependencies { + } + } + androidMain { + dependencies { + implementation(libs.ktor.okHttp) + implementation(awssdk.services.s3) + } + } + } +} diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt similarity index 99% rename from network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt rename to cells/src/androidMain/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt index 7fd304a4d5a..62bb01e0641 100644 --- a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/AwsProgressListenerInterceptor.kt +++ b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells.aws +package com.wire.kalium.cells.data import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt similarity index 95% rename from network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt rename to cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt index 297f10ed701..7473eb08a24 100644 --- a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClientJvm.kt +++ b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells.aws +package com.wire.kalium.cells.data import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider import aws.sdk.kotlin.services.s3.S3Client @@ -30,13 +30,14 @@ import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.content.asByteStream import aws.smithy.kotlin.runtime.net.url.Url -import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import com.wire.kalium.cells.data.model.CellNodeDTO +import com.wire.kalium.cells.domain.model.CellsCredentials import okhttp3.internal.http2.Header import okio.Path import java.io.RandomAccessFile import java.nio.ByteBuffer -actual fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient = CellsAwsClientJvm(credentials) +internal actual fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient = CellsAwsClientJvm(credentials) private class CellsAwsClientJvm( private val credentials: CellsCredentials diff --git a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt similarity index 95% rename from network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt rename to cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt index c2bff330365..7fabefea040 100644 --- a/network/src/androidMain/kotlin/com/wire/kalium/network/cells/aws/MetadataHeaders.kt +++ b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells.aws +package com.wire.kalium.cells.data internal object MetadataHeaders { const val DRAFT_MODE = "draft_mode" diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt similarity index 51% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt index 5e9ee6591d4..add457c8268 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsScope.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt @@ -15,27 +15,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells +package com.wire.kalium.cells +import com.wire.kalium.cells.data.CellsApi +import com.wire.kalium.cells.data.CellsApiImpl +import com.wire.kalium.cells.data.CellsAwsClient +import com.wire.kalium.cells.data.CellsDataSource +import com.wire.kalium.cells.data.cellsAwsClient +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellsCredentials +import com.wire.kalium.cells.domain.usecase.CancelDraftUseCase +import com.wire.kalium.cells.domain.usecase.CancelDraftUseCaseImpl +import com.wire.kalium.cells.domain.usecase.DeleteCellFileUseCase +import com.wire.kalium.cells.domain.usecase.DeleteCellFileUseCaseImpl +import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCase +import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCaseImpl +import com.wire.kalium.cells.domain.usecase.PublishDraftUseCase +import com.wire.kalium.cells.domain.usecase.PublishDraftUseCaseImpl +import com.wire.kalium.cells.domain.usecase.UploadToCellUseCase +import com.wire.kalium.cells.domain.usecase.UploadToCellUseCaseImpl import com.wire.kalium.logic.GlobalKaliumScope -import com.wire.kalium.logic.feature.cells.usecase.CancelDraftUseCase -import com.wire.kalium.logic.feature.cells.usecase.CancelDraftUseCaseImpl -import com.wire.kalium.logic.feature.cells.usecase.DeleteCellFileUseCase -import com.wire.kalium.logic.feature.cells.usecase.DeleteCellFileUseCaseImpl -import com.wire.kalium.logic.feature.cells.usecase.GetCellFilesUseCase -import com.wire.kalium.logic.feature.cells.usecase.GetCellFilesUseCaseImpl -import com.wire.kalium.logic.feature.cells.usecase.PublishDraftUseCase -import com.wire.kalium.logic.feature.cells.usecase.PublishDraftUseCaseImpl -import com.wire.kalium.logic.feature.cells.usecase.UploadToCellUseCase -import com.wire.kalium.logic.feature.cells.usecase.UploadToCellUseCaseImpl -import com.wire.kalium.network.cells.aws.CellsAwsClient -import com.wire.kalium.network.cells.aws.CellsCredentials -import com.wire.kalium.network.cells.aws.cellsAwsClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlin.coroutines.CoroutineContext -class CellsScope internal constructor( +public class CellsScope( private val globalScope: GlobalKaliumScope, ) : CoroutineScope { @@ -45,34 +49,40 @@ class CellsScope internal constructor( private val cellClientCredentials: CellsCredentials get() = CellsCredentials( serverUrl = "https://service.zeta.pydiocells.com", - accessToken = "", - gatewaySecret = "", + accessToken = "mBzSPjZ1qH7weLqHlNK9_W5HNUN0zdESyvhL4KqlhhM.0TUuMHKucKMCfC337jaUof-gdjODmCj2gGML5INWc8w", + gatewaySecret = "gatewaysecret", ) private val cellAwsClient: CellsAwsClient get() = cellsAwsClient(cellClientCredentials) + private val cellsApi: CellsApi + get() = CellsApiImpl( + credentials = cellClientCredentials, + httpClient = globalScope.unboundNetworkContainer.cellsClient + ) + private val cellsRepository: CellsRepository get() = CellsDataSource( - cellsApi = globalScope.unboundNetworkContainer.cellsApi(cellClientCredentials), + cellsApi = cellsApi, awsClient = cellAwsClient ) - val getCellFiles: GetCellFilesUseCase + public val getCellFiles: GetCellFilesUseCase get() = GetCellFilesUseCaseImpl(cellsRepository) - val uploadToCell: UploadToCellUseCase + public val uploadToCell: UploadToCellUseCase get() = UploadToCellUseCaseImpl( scope = this, cellsRepository = cellsRepository ) - val deleteFromCell: DeleteCellFileUseCase + public val deleteFromCell: DeleteCellFileUseCase get() = DeleteCellFileUseCaseImpl(cellsRepository) - val cancelDraft: CancelDraftUseCase + public val cancelDraft: CancelDraftUseCase get() = CancelDraftUseCaseImpl(cellsRepository) - val publishDraft: PublishDraftUseCase + public val publishDraft: PublishDraftUseCase get() = PublishDraftUseCaseImpl(cellsRepository) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt similarity index 78% rename from network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt index cb0d324b46c..13a34db19bc 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/CellsApi.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt @@ -15,33 +15,36 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells +package com.wire.kalium.cells.data +import com.wire.kalium.cells.data.model.CellNodeDTO +import com.wire.kalium.cells.data.model.GetFilesResponseDTO +import com.wire.kalium.cells.data.model.PreCheckResultDTO +import com.wire.kalium.cells.data.model.toDto +import com.wire.kalium.cells.domain.model.CellsCredentials import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi import com.wire.kalium.cells.sdk.kmp.model.RestActionParameters import com.wire.kalium.cells.sdk.kmp.model.RestCreateCheckRequest import com.wire.kalium.cells.sdk.kmp.model.RestIncomingNode import com.wire.kalium.cells.sdk.kmp.model.RestLookupRequest -import com.wire.kalium.cells.sdk.kmp.model.RestNode -import com.wire.kalium.cells.sdk.kmp.model.RestNodeCollection import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocator import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocators import com.wire.kalium.cells.sdk.kmp.model.RestNodeVersionsFilter import com.wire.kalium.cells.sdk.kmp.model.RestPromoteParameters import com.wire.kalium.cells.sdk.kmp.model.RestVersionCollection -import com.wire.kalium.network.api.unbound.cells.CellNodeDTO -import com.wire.kalium.network.api.unbound.cells.GetFilesResponseDTO -import com.wire.kalium.network.api.unbound.cells.PreCheckResultDTO -import com.wire.kalium.network.cells.aws.CellsCredentials +import com.wire.kalium.network.api.model.ErrorResponse +import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.session.installAuth import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.mapSuccess -import com.wire.kalium.network.utils.wrapCellsResponse import io.ktor.client.HttpClient import io.ktor.client.plugins.auth.providers.BearerAuthProvider import io.ktor.client.plugins.auth.providers.BearerTokens +import io.ktor.http.HttpStatusCode +import io.ktor.http.isSuccess +import kotlinx.coroutines.CancellationException -interface CellsApi { +internal interface CellsApi { suspend fun getFiles(cellName: String): NetworkResponse suspend fun delete(node: CellNodeDTO): NetworkResponse suspend fun cancelDraft(node: CellNodeDTO): NetworkResponse @@ -125,20 +128,23 @@ internal class CellsApiImpl( } } -private fun RestNode.isDraft(): Boolean { - return userMetadata?.firstOrNull { it.namespace == "usermeta-draft" }?.jsonValue == "true" -} +@Suppress("TooGenericExceptionCaught") +private suspend inline fun wrapCellsResponse( + performRequest: () -> com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse +): NetworkResponse = + try { + + val response = performRequest() + val status = HttpStatusCode.fromValue(response.status) + + if (status.isSuccess()) { + NetworkResponse.Success(response.body(), emptyMap(), response.status) + } else { + NetworkResponse.Error(KaliumException.ServerError(ErrorResponse(response.status, "", ""))) + } -private fun RestNodeCollection.toDto() = GetFilesResponseDTO( - nodes = nodes?.map { node -> - CellNodeDTO( - uuid = node.uuid, - versionId = node.versionMeta?.versionId ?: "", - path = node.path, - type = node.type?.name ?: "", - eTag = node.storageETag, - isRecycleBin = node.isRecycleBin ?: false, - isDraft = node.isDraft(), - ) - } ?: emptyList() -) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + NetworkResponse.Error(KaliumException.GenericError(e)) + } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsAwsClient.kt similarity index 75% rename from network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsAwsClient.kt index 1b905dbb976..3fac4dcb3e3 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsAwsClient.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsAwsClient.kt @@ -15,13 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells.aws +package com.wire.kalium.cells.data -import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import com.wire.kalium.cells.data.model.CellNodeDTO +import com.wire.kalium.cells.domain.model.CellsCredentials import okio.Path -interface CellsAwsClient { +internal interface CellsAwsClient { suspend fun upload(path: Path, node: CellNodeDTO, onProgressUpdate: (Long) -> Unit) } -expect fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient +internal expect fun cellsAwsClient(credentials: CellsCredentials): CellsAwsClient diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt similarity index 76% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt index 6e43824239e..19f6c404c00 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/CellsRepository.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt @@ -15,34 +15,23 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells +package com.wire.kalium.cells.data +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.data.model.toDto +import com.wire.kalium.cells.data.model.toModel +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.domain.model.PreCheckResult import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.data.cells.PreCheckResult -import com.wire.kalium.logic.data.cells.toDto -import com.wire.kalium.logic.data.cells.toModel import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.map -import com.wire.kalium.logic.kaliumLogger -import com.wire.kalium.logic.wrapApiRequest -import com.wire.kalium.network.cells.CellsApi -import com.wire.kalium.network.cells.aws.CellsAwsClient +import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withContext import okio.Path -interface CellsRepository { - suspend fun preCheck(node: CellNode): Either - suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either - suspend fun getFiles(cellName: String): Either> - suspend fun deleteFile(node: CellNode): Either - suspend fun cancelDraft(node: CellNode): Either - suspend fun publishDraft(node: CellNode): Either -} - internal class CellsDataSource internal constructor( private val cellsApi: CellsApi, private val awsClient: CellsAwsClient, @@ -75,7 +64,6 @@ internal class CellsDataSource internal constructor( } catch (e: CancellationException) { throw e } catch (e: Exception) { - kaliumLogger.e("Failed to upload file", e) Either.Left(NetworkFailure.ServerMiscommunication(e)) } } @@ -115,3 +103,9 @@ internal class CellsDataSource internal constructor( } } } + +internal inline fun wrapApiRequest(networkCall: () -> NetworkResponse): Either = + when (val result = networkCall()) { + is NetworkResponse.Success -> Either.Right(result.value) + is NetworkResponse.Error -> Either.Left(NetworkFailure.ServerMiscommunication(result.kException)) + } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt similarity index 64% rename from data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt index 22b2e6c6d49..ba56d703b58 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/CellNode.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt @@ -15,11 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.data.cells +package com.wire.kalium.cells.data.model -import com.wire.kalium.network.api.unbound.cells.CellNodeDTO +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.sdk.kmp.model.RestNode -data class CellNode( +internal data class CellNodeDTO( val uuid: String, val versionId: String, val path: String, @@ -29,7 +30,7 @@ data class CellNode( val isDraft: Boolean = false, ) -fun CellNodeDTO.toModel() = CellNode( +internal fun CellNodeDTO.toModel() = CellNode( uuid = uuid, versionId = versionId, path = path, @@ -39,7 +40,7 @@ fun CellNodeDTO.toModel() = CellNode( isDraft = isDraft, ) -fun CellNode.toDto() = CellNodeDTO( +internal fun CellNode.toDto() = CellNodeDTO( uuid = uuid, versionId = versionId, path = path, @@ -48,3 +49,17 @@ fun CellNode.toDto() = CellNodeDTO( isRecycleBin = isRecycleBin, isDraft = isDraft, ) + +internal fun RestNode.toDto() = CellNodeDTO( + uuid = uuid, + versionId = versionMeta?.versionId ?: "", + path = path, + type = type?.name ?: "", + eTag = storageETag, + isRecycleBin = isRecycleBin ?: false, + isDraft = isDraft(), +) + +private fun RestNode.isDraft(): Boolean { + return userMetadata?.firstOrNull { it.namespace == "usermeta-draft" }?.jsonValue == "true" +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/GetFilesResponseDTO.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/GetFilesResponseDTO.kt new file mode 100644 index 00000000000..dd90110eada --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/GetFilesResponseDTO.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.data.model + +import com.wire.kalium.cells.sdk.kmp.model.RestNodeCollection + +internal data class GetFilesResponseDTO( + val nodes: List +) + +internal fun RestNodeCollection.toDto() = GetFilesResponseDTO( + nodes = nodes?.map { node -> + node.toDto() + } ?: emptyList() +) diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt similarity index 90% rename from network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt index 2c95ab353cc..f8675bf4ff3 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/PreCheckResultDTO.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.api.unbound.cells +package com.wire.kalium.cells.data.model -data class PreCheckResultDTO( +internal data class PreCheckResultDTO( val fileExists: Boolean = false, val suggestedPath: String? = null, ) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt new file mode 100644 index 00000000000..dd097e8299a --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt @@ -0,0 +1,33 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.domain.model.PreCheckResult +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.functional.Either +import okio.Path + +internal interface CellsRepository { + suspend fun preCheck(node: CellNode): Either + suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either + suspend fun getFiles(cellName: String): Either> + suspend fun deleteFile(node: CellNode): Either + suspend fun cancelDraft(node: CellNode): Either + suspend fun publishDraft(node: CellNode): Either +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt similarity index 86% rename from network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt index 1693b357b78..a3209684bb0 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/unbound/cells/GetFilesResponseDTO.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt @@ -15,13 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.api.unbound.cells +package com.wire.kalium.cells.domain.model -data class GetFilesResponseDTO( - val nodes: List -) - -data class CellNodeDTO( +public data class CellNode( val uuid: String, val versionId: String, val path: String, diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellsCredentials.kt similarity index 90% rename from network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellsCredentials.kt index 01432fa4071..c0caf9fae0b 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/cells/aws/CellsCredentials.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellsCredentials.kt @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.network.cells.aws +package com.wire.kalium.cells.domain.model -data class CellsCredentials( +internal data class CellsCredentials( val serverUrl: String, val accessToken: String, val gatewaySecret: String, diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt similarity index 76% rename from data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt index 2cd56fb5657..17b581c5e9a 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/cells/PreCheckResult.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.data.cells +package com.wire.kalium.cells.domain.model -sealed interface PreCheckResult { - data object Success : PreCheckResult - data class FileExists(val suggestedFilename: String) : PreCheckResult +public sealed interface PreCheckResult { + public data object Success : PreCheckResult + public data class FileExists(val suggestedFilename: String) : PreCheckResult } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt similarity index 82% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt index cf489449018..83cec9b169c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/CancelDraftUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells.usecase +package com.wire.kalium.cells.domain.usecase +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.feature.cells.CellsRepository import com.wire.kalium.logic.functional.Either -interface CancelDraftUseCase { +public interface CancelDraftUseCase { /** * Cancels the draft of the cell node. * @param node Cell node to cancel the draft for. * @return Either. Result of the operation. */ - suspend operator fun invoke(node: CellNode): Either + public suspend operator fun invoke(node: CellNode): Either } internal class CancelDraftUseCaseImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt similarity index 82% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt index 5b106f60d6b..fd8a9605244 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/DeleteCellFileUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt @@ -15,21 +15,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells.usecase +package com.wire.kalium.cells.domain.usecase +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.feature.cells.CellsRepository import com.wire.kalium.logic.functional.Either -interface DeleteCellFileUseCase { +public interface DeleteCellFileUseCase { /** * Delete a file from the cell. * Note: Delete operation is asynchronous on Cells Server. Actual delete does not happen immediately. * @param node The node of the file to delete. * @return Either a [NetworkFailure] or [Unit]. */ - suspend operator fun invoke(node: CellNode): Either + public suspend operator fun invoke(node: CellNode): Either } internal class DeleteCellFileUseCaseImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt similarity index 81% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt index b0e0edab216..945cb489e7b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/GetCellFilesUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells.usecase +package com.wire.kalium.cells.domain.usecase +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.feature.cells.CellsRepository import com.wire.kalium.logic.functional.Either -interface GetCellFilesUseCase { +public interface GetCellFilesUseCase { /** * Get the list of files in the cell. * @param cellName the name of the cell. * @return the list of files in the cell or [NetworkFailure]. */ - suspend operator fun invoke(cellName: String): Either> + public suspend operator fun invoke(cellName: String): Either> } internal class GetCellFilesUseCaseImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt similarity index 81% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt index 08d33b447c9..cde3be3b072 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/PublishDraftUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells.usecase +package com.wire.kalium.cells.domain.usecase +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.feature.cells.CellsRepository import com.wire.kalium.logic.functional.Either -interface PublishDraftUseCase { +public interface PublishDraftUseCase { /** * Publish the draft in the cell. * @param node the cell node. * @return [NetworkFailure] or [Unit]. */ - suspend operator fun invoke(node: CellNode): Either + public suspend operator fun invoke(node: CellNode): Either } internal class PublishDraftUseCaseImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt similarity index 90% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt index b1ddc7baef7..c1f0828f265 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/cells/usecase/UploadToCellUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.cells.usecase +package com.wire.kalium.cells.domain.usecase +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.domain.model.PreCheckResult import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.cells.CellNode -import com.wire.kalium.logic.data.cells.PreCheckResult -import com.wire.kalium.logic.feature.cells.CellsRepository import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.left @@ -31,7 +31,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import okio.Path -interface UploadToCellUseCase { +public interface UploadToCellUseCase { /** * Uploads a file to the cell. * This use case will check if the file already exists in the cell and if it does, it will update the file name @@ -42,7 +42,7 @@ interface UploadToCellUseCase { * @param progress Callback to report the upload progress. * @return Deferred>. Deferred result of the upload. Can be cancelled to cancel upload. */ - suspend operator fun invoke( + public suspend operator fun invoke( path: Path, node: CellNode, size: Long, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index a0ade651a1b..14bdb75e474 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -185,7 +185,6 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProviderImpl import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCaseImpl -import com.wire.kalium.logic.feature.cells.CellsScope import com.wire.kalium.logic.feature.client.ClientScope import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCaseImpl @@ -889,9 +888,6 @@ class UserSessionScope internal constructor( globalPreferences, ) - val cells: CellsScope - get() = CellsScope(globalScope) - val persistMessage: PersistMessageUseCase get() = PersistMessageUseCaseImpl(messageRepository, userId, NotificationEventsManagerImpl) diff --git a/network/build.gradle.kts b/network/build.gradle.kts index 4c5c91b6465..fce0b545667 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -68,8 +68,6 @@ kotlin { // UUIDs implementation(libs.benAsherUUID) - - implementation(libs.wire.cells.sdk) } } val commonTest by getting { @@ -98,7 +96,6 @@ kotlin { addCommonKotlinJvmSourceDir() dependencies { implementation(libs.ktor.okHttp) - implementation(awssdk.services.s3) } } val appleMain by getting { diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt index c1172ec16b1..98d1ff60176 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnboundNetworkContainer.kt @@ -21,21 +21,18 @@ package com.wire.kalium.network.networkContainer import com.wire.kalium.network.UnboundNetworkClient import com.wire.kalium.network.api.base.unbound.acme.ACMEApi import com.wire.kalium.network.api.base.unbound.acme.ACMEApiImpl -import com.wire.kalium.network.cells.CellsApi -import com.wire.kalium.network.cells.CellsApiImpl import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApi import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApiImpl -import com.wire.kalium.network.cells.aws.CellsCredentials import com.wire.kalium.network.clearTextTrafficEngine import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.session.CertificatePinning +import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine interface UnboundNetworkContainer { val serverConfigApi: ServerConfigApi val acmeApi: ACMEApi - - fun cellsApi(credentials: CellsCredentials): CellsApi + val cellsClient: HttpClient } private interface UnboundNetworkClientProvider { @@ -101,8 +98,6 @@ class UnboundNetworkContainerCommon( unboundClearTextTrafficNetworkClient ) - override fun cellsApi(credentials: CellsCredentials): CellsApi = CellsApiImpl( - credentials = credentials, - httpClient = unboundNetworkClient.httpClient - ) + override val cellsClient: HttpClient + get() = unboundNetworkClient.httpClient } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt index 51c72e5a3c6..3d07f794892 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/NetworkUtils.kt @@ -34,7 +34,6 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.URLProtocol import io.ktor.http.Url import io.ktor.http.isSuccess -import kotlinx.coroutines.CancellationException import kotlinx.serialization.SerializationException internal fun HttpRequestBuilder.setWSSUrl(baseUrl: Url, vararg path: String) { @@ -120,7 +119,7 @@ internal fun String.splitSetCookieHeader(): List { * @return A new [NetworkResponse.Success] with the mapped result, * or [NetworkResponse.Error] if it was never a success to begin with */ -internal inline fun NetworkResponse.mapSuccess(mapping: ((T) -> U)): NetworkResponse = +inline fun NetworkResponse.mapSuccess(mapping: ((T) -> U)): NetworkResponse = if (isSuccessful()) { NetworkResponse.Success(mapping(this.value), this.headers, this.httpCode) } else { @@ -278,39 +277,6 @@ suspend fun wrapFederationResponse( } } -/** - * For Cells Sdk calls. - * Wraps a producer of [HttpResponse] and attempts to parse the server response based on the [BodyType]. - * @return - Successful response (HTTP Status Codes from 200 to 299): - * a [NetworkResponse.Success] with the expected [BodyType] will be returned. - * - * - Unsuccessful response (any other HTTP Status Code): - * a [NetworkResponse.Error] with a [KaliumException]. - * - * - Exceptions failure to reach server or parse response: - * a [NetworkResponse.Error] containing a [KaliumException.GenericError] - * - */ -suspend inline fun wrapCellsResponse( - performRequest: () -> com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse -): NetworkResponse = - try { - - val response = performRequest() - val status = HttpStatusCode.fromValue(response.status) - - if (status.isSuccess()) { - NetworkResponse.Success(response.body(), emptyMap(), response.status) - } else { - NetworkResponse.Error(KaliumException.ServerError(ErrorResponse(response.status, "", ""))) - } - - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - NetworkResponse.Error(KaliumException.GenericError(e)) - } - /** * Due to the "shared" status code limitations nature of some endpoints. * We need to first delegate to status code based exceptions and if parse fails go for federated error, From e5c584f3ce82366f1ac19a501df9e6a2395efd86 Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Mon, 10 Feb 2025 08:17:17 +0100 Subject: [PATCH 3/7] feat: refactoring upload (#WPB-15743) --- cells/build.gradle.kts | 1 + .../wire/kalium/cells/data/MetadataHeaders.kt | 2 +- .../com/wire/kalium/cells/CellsScope.kt | 17 ++- .../cells/data/CellUploadManagerImpl.kt | 138 ++++++++++++++++++ .../com/wire/kalium/cells/data/CellsApi.kt | 11 +- .../wire/kalium/cells/data/CellsDataSource.kt | 6 +- .../kalium/cells/data/model/CellNodeDTO.kt | 8 + .../cells/data/model/PreCheckResultDTO.kt | 2 +- .../kalium/cells/domain/CellUploadManager.kt | 72 +++++++++ .../kalium/cells/domain/CellsRepository.kt | 2 +- .../kalium/cells/domain/model/CellNode.kt | 2 + .../cells/domain/model/PreCheckResult.kt | 2 +- .../domain/usecase/UploadToCellUseCase.kt | 80 ---------- 13 files changed, 245 insertions(+), 98 deletions(-) create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt delete mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt diff --git a/cells/build.gradle.kts b/cells/build.gradle.kts index 437c87cff4c..e697afa3729 100644 --- a/cells/build.gradle.kts +++ b/cells/build.gradle.kts @@ -36,6 +36,7 @@ kotlin { implementation(libs.coroutines.core) implementation(libs.ktor.authClient) implementation(libs.okio.core) + implementation(libs.benAsherUUID) implementation(libs.wire.cells.sdk) } } diff --git a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt index 7fabefea040..8e7e9d213b1 100644 --- a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt +++ b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt @@ -18,7 +18,7 @@ package com.wire.kalium.cells.data internal object MetadataHeaders { - const val DRAFT_MODE = "draft_mode" + const val DRAFT_MODE = "draft-mode" const val CREATE_RESOURCE_UUID = "create-resource-uuid" const val CREATE_VERSION_ID = "create-version-id" } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt index add457c8268..2d0add413a4 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt @@ -17,11 +17,13 @@ */ package com.wire.kalium.cells +import com.wire.kalium.cells.data.CellUploadManagerImpl import com.wire.kalium.cells.data.CellsApi import com.wire.kalium.cells.data.CellsApiImpl import com.wire.kalium.cells.data.CellsAwsClient import com.wire.kalium.cells.data.CellsDataSource import com.wire.kalium.cells.data.cellsAwsClient +import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellsCredentials import com.wire.kalium.cells.domain.usecase.CancelDraftUseCase @@ -32,8 +34,6 @@ import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCase import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCaseImpl import com.wire.kalium.cells.domain.usecase.PublishDraftUseCase import com.wire.kalium.cells.domain.usecase.PublishDraftUseCaseImpl -import com.wire.kalium.cells.domain.usecase.UploadToCellUseCase -import com.wire.kalium.cells.domain.usecase.UploadToCellUseCaseImpl import com.wire.kalium.logic.GlobalKaliumScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -68,15 +68,16 @@ public class CellsScope( awsClient = cellAwsClient ) + public val uploadManager: CellUploadManager by lazy { + CellUploadManagerImpl( + repository = cellsRepository, + uploadScope = this, + ) + } + public val getCellFiles: GetCellFilesUseCase get() = GetCellFilesUseCaseImpl(cellsRepository) - public val uploadToCell: UploadToCellUseCase - get() = UploadToCellUseCaseImpl( - scope = this, - cellsRepository = cellsRepository - ) - public val deleteFromCell: DeleteCellFileUseCase get() = DeleteCellFileUseCaseImpl(cellsRepository) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt new file mode 100644 index 00000000000..a2a8b63dc9b --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt @@ -0,0 +1,138 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.data + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.cells.domain.CellUploadEvent +import com.wire.kalium.cells.domain.CellUploadInfo +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.domain.model.PreCheckResult +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import okio.Path + +internal class CellUploadManagerImpl internal constructor( + private val repository: CellsRepository, + private val uploadScope: CoroutineScope, +) : CellUploadManager { + + private val uploads = mutableMapOf() + + override suspend fun upload( + assetPath: Path, + assetSize: Long, + destNodePath: String, + ): Either = + repository.preCheck(destNodePath).map { result -> + + val path = when (result) { + is PreCheckResult.FileExists -> result.nextPath + is PreCheckResult.Success -> destNodePath + } + + CellNode( + uuid = uuid4().toString(), + versionId = uuid4().toString(), + path = path, + size = assetSize, + isDraft = true, + ).also { + startUpload(assetPath, it) + } + } + + private fun startUpload(assetPath: Path, node: CellNode) { + + val uploadEventsFlow = MutableSharedFlow() + + val uploadJob = uploadScope.launch { + repository.uploadFile( + path = assetPath, + node = node, + onProgressUpdate = { updateUploadProgress(node.uuid, it) } + ) + .onSuccess { + uploads.remove(node.uuid) + uploadEventsFlow.emit(CellUploadEvent.UploadCompleted) + } + .onFailure { + updateJobInfo(node.uuid) { copy(uploadFiled = true) } + uploadEventsFlow.emit(CellUploadEvent.UploadError) + } + } + + uploads[node.uuid] = UploadInfo( + node = node, + job = uploadJob, + events = uploadEventsFlow, + ) + } + + private fun updateUploadProgress(nodeUuid: String, uploaded: Long) { + uploads[nodeUuid]?.let { info -> + val progress = info.node.size?.let { uploaded.toFloat() / it } ?: 0f + updateJobInfo(info.node.uuid) { copy(progress = progress) } + uploadScope.launch { + info.events.emit(CellUploadEvent.UploadProgress(progress)) + } + } + } + + override fun cancelUpload(nodeUuid: String) { + uploads[nodeUuid]?.run { + job.cancel() + uploads.remove(nodeUuid) + } + } + + override fun observeUpload(nodeUuid: String): Flow? { + return uploads[nodeUuid]?.events?.asSharedFlow() + } + + override fun getUploadInfo(nodeUuid: String): CellUploadInfo? { + return uploads[nodeUuid]?.run { + CellUploadInfo( + progress = progress, + uploadFiled = uploadFiled, + ) + } + } + + private fun updateJobInfo(uuid: String, block: UploadInfo.() -> UploadInfo) { + uploads[uuid]?.let { uploads[uuid] = block(it) } + } +} + +private data class UploadInfo( + val node: CellNode, + val job: Job, + val events: MutableSharedFlow, + val progress: Float = 0f, + val uploadFiled: Boolean = false, +) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt index 13a34db19bc..87215ef17bd 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt @@ -57,8 +57,12 @@ internal class CellsApiImpl( httpClient: HttpClient ) : CellsApi { + private companion object { + const val API_VERSION = "v2" + } + private var nodeServiceApi: NodeServiceApi = NodeServiceApi( - baseUrl = "${credentials.serverUrl}/a", + baseUrl = "${credentials.serverUrl}/$API_VERSION", httpClient = httpClient.config { installAuth( BearerAuthProvider( @@ -74,7 +78,8 @@ internal class CellsApiImpl( wrapCellsResponse { nodeServiceApi.lookup( RestLookupRequest( - locators = RestNodeLocators(listOf(RestNodeLocator(path = "$cellName/*"))) + locators = RestNodeLocators(listOf(RestNodeLocator(path = "$cellName/*"))), + sortField = "Modified" ) ) }.mapSuccess { response -> response.toDto() } @@ -122,7 +127,7 @@ internal class CellsApiImpl( response.results?.firstOrNull()?.let { PreCheckResultDTO( fileExists = it.exists ?: false, - suggestedPath = it.nextPath, + nextPath = it.nextPath, ) } ?: PreCheckResultDTO() } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt index 19f6c404c00..a62d3b340b0 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt @@ -38,13 +38,13 @@ internal class CellsDataSource internal constructor( private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : CellsRepository { - override suspend fun preCheck(node: CellNode): Either { + override suspend fun preCheck(nodePath: String): Either { return withContext(dispatchers.io) { wrapApiRequest { - cellsApi.preCheck(node.path) + cellsApi.preCheck(nodePath) }.map { result -> if (result.fileExists) { - PreCheckResult.FileExists(result.suggestedPath ?: node.path) + PreCheckResult.FileExists(result.nextPath ?: nodePath) } else { PreCheckResult.Success } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt index ba56d703b58..aef9ebeb077 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/CellNodeDTO.kt @@ -24,6 +24,8 @@ internal data class CellNodeDTO( val uuid: String, val versionId: String, val path: String, + val modified: Long?, + val size: Long?, val eTag: String? = null, val type: String? = null, val isRecycleBin: Boolean = false, @@ -34,6 +36,8 @@ internal fun CellNodeDTO.toModel() = CellNode( uuid = uuid, versionId = versionId, path = path, + modified = modified, + size = size, eTag = eTag, type = type, isRecycleBin = isRecycleBin, @@ -44,6 +48,8 @@ internal fun CellNode.toDto() = CellNodeDTO( uuid = uuid, versionId = versionId, path = path, + modified = modified, + size = size, eTag = eTag, type = type, isRecycleBin = isRecycleBin, @@ -54,6 +60,8 @@ internal fun RestNode.toDto() = CellNodeDTO( uuid = uuid, versionId = versionMeta?.versionId ?: "", path = path, + modified = modified?.toLong(), + size = propertySize?.toLong(), type = type?.name ?: "", eTag = storageETag, isRecycleBin = isRecycleBin ?: false, diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt index f8675bf4ff3..3233199324b 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/model/PreCheckResultDTO.kt @@ -19,5 +19,5 @@ package com.wire.kalium.cells.data.model internal data class PreCheckResultDTO( val fileExists: Boolean = false, - val suggestedPath: String? = null, + val nextPath: String? = null, ) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt new file mode 100644 index 00000000000..b1591ef7ea1 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt @@ -0,0 +1,72 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.functional.Either +import kotlinx.coroutines.flow.Flow +import okio.Path + +public interface CellUploadManager { + /** + * Starts file upload to the cell. Returns immediately after pre-checking the file name and returns the new node. + * The upload is done in the background and can be observed with [observeUpload] function. + * @param assetPath path to the file to upload + * @param assetSize size of the file to upload + * @param destNodePath path to the node where the file should be uploaded (cellName/fileName) + * @return [Either] with the new [CellNode] or [NetworkFailure] + */ + public suspend fun upload(assetPath: Path, assetSize: Long, destNodePath: String): Either + + /** + * Observe upload events for the node with [nodeUuid]. + * @param nodeUuid UUID of the node to observe + * @return [Flow] of [CellUploadEvent] or null if the node is not being uploaded + */ + public fun observeUpload(nodeUuid: String): Flow? + + /** + * Cancel upload of the node with [nodeUuid]. + * @param nodeUuid UUID of the node to cancel + */ + public fun cancelUpload(nodeUuid: String) + + /** + * Get upload info for the node with [nodeUuid]. + * @param nodeUuid UUID of the node to get info for + * @return [CellUploadInfo] or null if the node is not being uploaded + */ + public fun getUploadInfo(nodeUuid: String): CellUploadInfo? +} + +/** + * Information about the upload of the file. + * @param progress upload progress + * @param uploadFiled true if the upload failed + */ +public data class CellUploadInfo( + val progress: Float = 0f, + val uploadFiled: Boolean = false, +) + +public sealed interface CellUploadEvent { + public data class UploadProgress(val progress: Float) : CellUploadEvent + public data object UploadCompleted : CellUploadEvent + public data object UploadError : CellUploadEvent +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt index dd097e8299a..6574a61e648 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt @@ -24,7 +24,7 @@ import com.wire.kalium.logic.functional.Either import okio.Path internal interface CellsRepository { - suspend fun preCheck(node: CellNode): Either + suspend fun preCheck(nodePath: String): Either suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either suspend fun getFiles(cellName: String): Either> suspend fun deleteFile(node: CellNode): Either diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt index a3209684bb0..1f8d4bad159 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/CellNode.kt @@ -21,6 +21,8 @@ public data class CellNode( val uuid: String, val versionId: String, val path: String, + val modified: Long? = null, + val size: Long? = null, val eTag: String? = null, val type: String? = null, val isRecycleBin: Boolean = false, diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt index 17b581c5e9a..456358a69cc 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt @@ -19,5 +19,5 @@ package com.wire.kalium.cells.domain.model public sealed interface PreCheckResult { public data object Success : PreCheckResult - public data class FileExists(val suggestedFilename: String) : PreCheckResult + public data class FileExists(val nextPath: String) : PreCheckResult } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt deleted file mode 100644 index c1f0828f265..00000000000 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/UploadToCellUseCase.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cells.domain.usecase - -import com.wire.kalium.cells.domain.CellsRepository -import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.cells.domain.model.PreCheckResult -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.fold -import com.wire.kalium.logic.functional.left -import com.wire.kalium.logic.functional.map -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import okio.Path - -public interface UploadToCellUseCase { - /** - * Uploads a file to the cell. - * This use case will check if the file already exists in the cell and if it does, it will update the file name - * with name suggested by Cells SDK. - * @param path Path to the file to upload. - * @param node New cell node for the file. Must have a valid UUID, VersionId and Path. - * @param size Size of the file in bytes. - * @param progress Callback to report the upload progress. - * @return Deferred>. Deferred result of the upload. Can be cancelled to cancel upload. - */ - public suspend operator fun invoke( - path: Path, - node: CellNode, - size: Long, - progress: (Float) -> Unit, - ): Deferred> -} - -internal class UploadToCellUseCaseImpl( - private val cellsRepository: CellsRepository, - private val scope: CoroutineScope, -) : UploadToCellUseCase { - - override suspend operator fun invoke( - path: Path, - node: CellNode, - size: Long, - progress: (Float) -> Unit, - ): Deferred> = - cellsRepository.preCheck(node).fold( - fnR = { result -> - val checkedNode = when (result) { - is PreCheckResult.FileExists -> node.copy(path = result.suggestedFilename) - PreCheckResult.Success -> node - } - scope.async { - cellsRepository.uploadFile( - path = path, - node = checkedNode, - onProgressUpdate = { uploaded -> progress(uploaded.toFloat() / size) } - ).map { checkedNode } - } - }, - fnL = { CompletableDeferred(it.left()) }, - ) -} From 9554550bd6b81705646135c8f550731eaa35076f Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Mon, 10 Feb 2025 14:41:58 +0100 Subject: [PATCH 4/7] feat: adding headers for multipart upload (#WPB-15743) --- .../kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt index 7473eb08a24..554ebee11b1 100644 --- a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt +++ b/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt @@ -94,6 +94,7 @@ private class CellsAwsClientJvm( val requestId = createMultipartUpload { bucket = DEFAULT_BUCKET_NAME key = node.path + metadata = node.createDraftNodeMetaData() }.uploadId RandomAccessFile(path.toFile(), "r").use { file -> val fileSize = file.length() From 42282ad0104c2633396e17c90edcfe70d4cc0c9b Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Wed, 12 Feb 2025 14:14:33 +0100 Subject: [PATCH 5/7] update cells module after kalium refactoring (#WPB-15743) --- cells/build.gradle.kts | 2 +- .../kotlin/com/wire/kalium/cells/CellsScope.kt | 6 +++--- .../wire/kalium/cells/data/CellUploadManagerImpl.kt | 10 +++++----- .../com/wire/kalium/cells/data/CellsDataSource.kt | 6 +++--- .../com/wire/kalium/cells/domain/CellUploadManager.kt | 4 ++-- .../com/wire/kalium/cells/domain/CellsRepository.kt | 4 ++-- .../kalium/cells/domain/usecase/CancelDraftUseCase.kt | 4 ++-- .../cells/domain/usecase/DeleteCellFileUseCase.kt | 4 ++-- .../kalium/cells/domain/usecase/GetCellFilesUseCase.kt | 4 ++-- .../kalium/cells/domain/usecase/PublishDraftUseCase.kt | 4 ++-- logic/build.gradle.kts | 1 + .../com/wire/kalium/logic/feature/UserSessionScope.kt | 4 ++++ 12 files changed, 29 insertions(+), 24 deletions(-) diff --git a/cells/build.gradle.kts b/cells/build.gradle.kts index e697afa3729..b07ecb2cede 100644 --- a/cells/build.gradle.kts +++ b/cells/build.gradle.kts @@ -30,9 +30,9 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":common")) implementation(project(":network")) implementation(project(":util")) - implementation(project(":logic")) implementation(libs.coroutines.core) implementation(libs.ktor.authClient) implementation(libs.okio.core) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt index 2d0add413a4..67df6946cdd 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt @@ -34,13 +34,13 @@ import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCase import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCaseImpl import com.wire.kalium.cells.domain.usecase.PublishDraftUseCase import com.wire.kalium.cells.domain.usecase.PublishDraftUseCaseImpl -import com.wire.kalium.logic.GlobalKaliumScope +import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlin.coroutines.CoroutineContext public class CellsScope( - private val globalScope: GlobalKaliumScope, + private val cellsClient: HttpClient, ) : CoroutineScope { override val coroutineContext: CoroutineContext = SupervisorJob() @@ -59,7 +59,7 @@ public class CellsScope( private val cellsApi: CellsApi get() = CellsApiImpl( credentials = cellClientCredentials, - httpClient = globalScope.unboundNetworkContainer.cellsClient + httpClient = cellsClient ) private val cellsRepository: CellsRepository diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt index a2a8b63dc9b..c8cbfe9d3fe 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt @@ -24,11 +24,11 @@ import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.cells.domain.model.PreCheckResult -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.map -import com.wire.kalium.logic.functional.onFailure -import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.map +import com.wire.kalium.common.functional.onFailure +import com.wire.kalium.common.functional.onSuccess import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt index a62d3b340b0..ba9b7d275d7 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt @@ -22,9 +22,9 @@ import com.wire.kalium.cells.data.model.toDto import com.wire.kalium.cells.data.model.toModel import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.cells.domain.model.PreCheckResult -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.map +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.map import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt index b1591ef7ea1..caee833305e 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt @@ -18,8 +18,8 @@ package com.wire.kalium.cells.domain import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either import kotlinx.coroutines.flow.Flow import okio.Path diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt index 6574a61e648..b137f2b4dfe 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt @@ -19,8 +19,8 @@ package com.wire.kalium.cells.domain import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.cells.domain.model.PreCheckResult -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either import okio.Path internal interface CellsRepository { diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt index 83cec9b169c..d714c2ef62c 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt @@ -19,8 +19,8 @@ package com.wire.kalium.cells.domain.usecase import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either public interface CancelDraftUseCase { /** diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt index fd8a9605244..da74684328f 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt @@ -19,8 +19,8 @@ package com.wire.kalium.cells.domain.usecase import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either public interface DeleteCellFileUseCase { /** diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt index 945cb489e7b..2bb1e3f0844 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt @@ -19,8 +19,8 @@ package com.wire.kalium.cells.domain.usecase import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either public interface GetCellFilesUseCase { /** diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt index cde3be3b072..f07673677a4 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt @@ -19,8 +19,8 @@ package com.wire.kalium.cells.domain.usecase import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.functional.Either +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either public interface PublishDraftUseCase { /** diff --git a/logic/build.gradle.kts b/logic/build.gradle.kts index a7bffdef0c5..ea08a0ec5d0 100644 --- a/logic/build.gradle.kts +++ b/logic/build.gradle.kts @@ -46,6 +46,7 @@ kotlin { api(project(":logger")) api(project(":calling")) implementation(project(":util")) + implementation(project(":cells")) // coroutines implementation(libs.coroutines.core) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index b855903cbbb..2138ffd2f64 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature +import com.wire.kalium.cells.CellsScope import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logger.obfuscateId import com.wire.kalium.common.error.CoreFailure @@ -2179,6 +2180,9 @@ class UserSessionScope internal constructor( InCallReactionsDataSource() } + val cells: CellsScope + get() = CellsScope(globalScope.unboundNetworkContainer.cellsClient) + /** * This will start subscribers of observable work per user session, as long as the user is logged in. * When the user logs out, this work will be canceled. From b36f2cbf92e44c9bccabcf83af7e505dd38f6b6f Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Fri, 14 Feb 2025 17:11:09 +0100 Subject: [PATCH 6/7] feat: persist attachment drafts in the database (#WPB-15743) --- cells/build.gradle.kts | 2 + .../com/wire/kalium/cells/CellsScope.kt | 53 +++++++---- .../cells/data/CellUploadManagerImpl.kt | 12 ++- .../com/wire/kalium/cells/data/CellsApi.kt | 69 ++++++++++---- .../wire/kalium/cells/data/CellsDataSource.kt | 63 ++++++------- .../data/MessageAttachmentDraftDataSource.kt | 80 ++++++++++++++++ .../kalium/cells/domain/CellUploadManager.kt | 14 ++- .../kalium/cells/domain/CellsRepository.kt | 6 +- .../MessageAttachmentDraftRepository.kt | 36 +++++++ .../cells/domain/model/AttachmentDraft.kt | 32 +++++++ .../cells/domain/model/PreCheckResult.kt | 4 + .../usecase/AddAttachmentDraftUseCase.kt | 93 +++++++++++++++++++ .../domain/usecase/CancelDraftUseCase.kt | 40 -------- .../domain/usecase/DeleteCellFileUseCase.kt | 41 -------- .../domain/usecase/GetCellFilesUseCase.kt | 40 -------- .../usecase/ObserveAttachmentDraftsUseCase.kt | 59 ++++++++++++ .../domain/usecase/ObserveCellFilesUseCase.kt | 74 +++++++++++++++ .../usecase/PublishAttachmentsUseCase.kt | 67 +++++++++++++ .../domain/usecase/PublishDraftUseCase.kt | 40 -------- .../usecase/RemoveAttachmentDraftUseCase.kt | 69 ++++++++++++++ .../kalium/logic/feature/UserSessionScope.kt | 10 +- .../logic/feature/message/MessageScope.kt | 3 + .../feature/message/SendTextMessageUseCase.kt | 17 +++- .../message/SendTextMessageCaseTest.kt | 5 + .../persistence/MessageAttachmentDraft.sq | 54 +++++++++++ .../src/commonMain/db_user/migrations/99.sqm | 13 +++ .../MessageAttachmentDraftDao.kt | 40 ++++++++ .../MessageAttachmentDraftDaoImpl.kt | 72 ++++++++++++++ .../MessageAttachmentDraftEntity.kt | 31 +++++++ .../MessageAttachmentDraftMapper.kt | 43 +++++++++ .../wire/kalium/persistence/db/TableMapper.kt | 5 + .../persistence/db/UserDatabaseBuilder.kt | 8 +- 32 files changed, 956 insertions(+), 239 deletions(-) create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/MessageAttachmentDraftRepository.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCase.kt delete mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt delete mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt delete mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCase.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt delete mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCase.kt create mode 100644 persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAttachmentDraft.sq create mode 100644 persistence/src/commonMain/db_user/migrations/99.sqm create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDao.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftEntity.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftMapper.kt diff --git a/cells/build.gradle.kts b/cells/build.gradle.kts index b07ecb2cede..24a8088500e 100644 --- a/cells/build.gradle.kts +++ b/cells/build.gradle.kts @@ -32,7 +32,9 @@ kotlin { dependencies { implementation(project(":common")) implementation(project(":network")) + implementation(project(":data")) implementation(project(":util")) + implementation(project(":persistence")) implementation(libs.coroutines.core) implementation(libs.ktor.authClient) implementation(libs.okio.core) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt index 67df6946cdd..ff721b9ec0a 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt @@ -22,18 +22,24 @@ import com.wire.kalium.cells.data.CellsApi import com.wire.kalium.cells.data.CellsApiImpl import com.wire.kalium.cells.data.CellsAwsClient import com.wire.kalium.cells.data.CellsDataSource +import com.wire.kalium.cells.data.MessageAttachmentDraftDataSource import com.wire.kalium.cells.data.cellsAwsClient import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository import com.wire.kalium.cells.domain.model.CellsCredentials -import com.wire.kalium.cells.domain.usecase.CancelDraftUseCase -import com.wire.kalium.cells.domain.usecase.CancelDraftUseCaseImpl -import com.wire.kalium.cells.domain.usecase.DeleteCellFileUseCase -import com.wire.kalium.cells.domain.usecase.DeleteCellFileUseCaseImpl -import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCase -import com.wire.kalium.cells.domain.usecase.GetCellFilesUseCaseImpl -import com.wire.kalium.cells.domain.usecase.PublishDraftUseCase -import com.wire.kalium.cells.domain.usecase.PublishDraftUseCaseImpl +import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCaseImpl +import com.wire.kalium.cells.domain.usecase.ObserveAttachmentDraftsUseCase +import com.wire.kalium.cells.domain.usecase.ObserveAttachmentDraftsUseCaseImpl +import com.wire.kalium.cells.domain.usecase.ObserveCellFilesUseCase +import com.wire.kalium.cells.domain.usecase.ObserveCellFilesUseCaseImpl +import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase +import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCaseImpl +import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCaseImpl +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftDao import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -41,15 +47,22 @@ import kotlin.coroutines.CoroutineContext public class CellsScope( private val cellsClient: HttpClient, + private val attachmentDraftDao: MessageAttachmentDraftDao, + private val conversationsDAO: ConversationDAO, ) : CoroutineScope { + internal companion object { + // Temporary hardcoded root cell + const val ROOT_CELL = "wire-cells-android" + } + override val coroutineContext: CoroutineContext = SupervisorJob() // Temporary hardcoded credentials and server URL private val cellClientCredentials: CellsCredentials get() = CellsCredentials( serverUrl = "https://service.zeta.pydiocells.com", - accessToken = "mBzSPjZ1qH7weLqHlNK9_W5HNUN0zdESyvhL4KqlhhM.0TUuMHKucKMCfC337jaUof-gdjODmCj2gGML5INWc8w", + accessToken = "", gatewaySecret = "gatewaysecret", ) @@ -68,6 +81,9 @@ public class CellsScope( awsClient = cellAwsClient ) + private val messageAttachmentsDraftRepository: MessageAttachmentDraftRepository + get() = MessageAttachmentDraftDataSource(attachmentDraftDao) + public val uploadManager: CellUploadManager by lazy { CellUploadManagerImpl( repository = cellsRepository, @@ -75,15 +91,18 @@ public class CellsScope( ) } - public val getCellFiles: GetCellFilesUseCase - get() = GetCellFilesUseCaseImpl(cellsRepository) + public val addAttachment: AddAttachmentDraftUseCase + get() = AddAttachmentDraftUseCaseImpl(uploadManager, messageAttachmentsDraftRepository, this) + + public val removeAttachment: RemoveAttachmentDraftUseCase + get() = RemoveAttachmentDraftUseCaseImpl(uploadManager, messageAttachmentsDraftRepository, cellsRepository) - public val deleteFromCell: DeleteCellFileUseCase - get() = DeleteCellFileUseCaseImpl(cellsRepository) + public val observeAttachments: ObserveAttachmentDraftsUseCase + get() = ObserveAttachmentDraftsUseCaseImpl(messageAttachmentsDraftRepository, uploadManager) - public val cancelDraft: CancelDraftUseCase - get() = CancelDraftUseCaseImpl(cellsRepository) + public val publishAttachments: PublishAttachmentsUseCase + get() = PublishAttachmentsUseCaseImpl(cellsRepository, messageAttachmentsDraftRepository) - public val publishDraft: PublishDraftUseCase - get() = PublishDraftUseCaseImpl(cellsRepository) + public val observeFiles: ObserveCellFilesUseCase + get() = ObserveCellFilesUseCaseImpl(conversationsDAO, cellsRepository) } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt index c8cbfe9d3fe..c156d70dcfb 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellUploadManagerImpl.kt @@ -31,6 +31,7 @@ import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -104,9 +105,10 @@ internal class CellUploadManagerImpl internal constructor( } } - override fun cancelUpload(nodeUuid: String) { + override suspend fun cancelUpload(nodeUuid: String) { uploads[nodeUuid]?.run { - job.cancel() + events.emit(CellUploadEvent.UploadCancelled) + job.cancelAndJoin() uploads.remove(nodeUuid) } } @@ -119,11 +121,15 @@ internal class CellUploadManagerImpl internal constructor( return uploads[nodeUuid]?.run { CellUploadInfo( progress = progress, - uploadFiled = uploadFiled, + uploadFailed = uploadFiled, ) } } + override fun isUploading(nodeUuid: String): Boolean { + return uploads.containsKey(nodeUuid) + } + private fun updateJobInfo(uuid: String, block: UploadInfo.() -> UploadInfo) { uploads[uuid]?.let { uploads[uuid] = block(it) } } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt index 87215ef17bd..ef0e5096372 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt @@ -23,6 +23,7 @@ import com.wire.kalium.cells.data.model.PreCheckResultDTO import com.wire.kalium.cells.data.model.toDto import com.wire.kalium.cells.domain.model.CellsCredentials import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi +import com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse import com.wire.kalium.cells.sdk.kmp.model.RestActionParameters import com.wire.kalium.cells.sdk.kmp.model.RestCreateCheckRequest import com.wire.kalium.cells.sdk.kmp.model.RestIncomingNode @@ -31,6 +32,9 @@ import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocator import com.wire.kalium.cells.sdk.kmp.model.RestNodeLocators import com.wire.kalium.cells.sdk.kmp.model.RestNodeVersionsFilter import com.wire.kalium.cells.sdk.kmp.model.RestPromoteParameters +import com.wire.kalium.cells.sdk.kmp.model.RestPublicLinkRequest +import com.wire.kalium.cells.sdk.kmp.model.RestShareLink +import com.wire.kalium.cells.sdk.kmp.model.RestShareLinkAccessType import com.wire.kalium.cells.sdk.kmp.model.RestVersionCollection import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException @@ -46,14 +50,16 @@ import kotlinx.coroutines.CancellationException internal interface CellsApi { suspend fun getFiles(cellName: String): NetworkResponse + suspend fun getFiles(cellNames: List): NetworkResponse suspend fun delete(node: CellNodeDTO): NetworkResponse - suspend fun cancelDraft(node: CellNodeDTO): NetworkResponse - suspend fun publishDraft(node: CellNodeDTO): NetworkResponse + suspend fun cancelDraft(nodeUuid: String, versionUuid: String): NetworkResponse + suspend fun publishDraft(nodeUuid: String): NetworkResponse suspend fun preCheck(path: String): NetworkResponse + suspend fun createPublicUrl(uuid: String, fileName: String): NetworkResponse } internal class CellsApiImpl( - credentials: CellsCredentials, + private val credentials: CellsCredentials, httpClient: HttpClient ) : CellsApi { @@ -84,6 +90,20 @@ internal class CellsApiImpl( ) }.mapSuccess { response -> response.toDto() } + override suspend fun getFiles(cellNames: List): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.lookup( + RestLookupRequest( + locators = RestNodeLocators( + cellNames.map { + RestNodeLocator(path = "$it/*") + } + ), + sortField = "Modified" + ) + ) + }.mapSuccess { response -> response.toDto() } + override suspend fun delete(node: CellNodeDTO): NetworkResponse = wrapCellsResponse { nodeServiceApi.performAction( @@ -94,25 +114,22 @@ internal class CellsApiImpl( ) }.mapSuccess {} - override suspend fun publishDraft(node: CellNodeDTO): NetworkResponse = - getNodeDraftVersions(node).mapSuccess { response -> + override suspend fun publishDraft(uuid: String): NetworkResponse = + getNodeDraftVersions(uuid).mapSuccess { response -> wrapCellsResponse { val version = response.versions?.firstOrNull() ?: error("Draft version not found") - nodeServiceApi.promoteVersion(node.uuid, version.versionId, RestPromoteParameters(publish = true)) + nodeServiceApi.promoteVersion(uuid, version.versionId, RestPromoteParameters(publish = true)) } } - override suspend fun cancelDraft(node: CellNodeDTO): NetworkResponse = - getNodeDraftVersions(node).mapSuccess { response -> - wrapCellsResponse { - val version = response.versions?.firstOrNull() ?: error("Draft version not found") - nodeServiceApi.deleteVersion(node.uuid, version.versionId) - } - } + override suspend fun cancelDraft(nodeUuid: String, versionUuid: String): NetworkResponse = + wrapCellsResponse { + nodeServiceApi.deleteVersion(nodeUuid, versionUuid) + }.mapSuccess {} - private suspend fun getNodeDraftVersions(node: CellNodeDTO): NetworkResponse = + private suspend fun getNodeDraftVersions(uuid: String): NetworkResponse = wrapCellsResponse { - nodeServiceApi.nodeVersions(node.uuid, RestNodeVersionsFilter()) + nodeServiceApi.nodeVersions(uuid, RestNodeVersionsFilter()) } override suspend fun preCheck(path: String): NetworkResponse = @@ -131,11 +148,31 @@ internal class CellsApiImpl( ) } ?: PreCheckResultDTO() } + + override suspend fun createPublicUrl(uuid: String, fileName: String): NetworkResponse { + return wrapCellsResponse { + nodeServiceApi.createPublicLink( + uuid = uuid, + publicLinkRequest = RestPublicLinkRequest( + link = RestShareLink( + label = fileName, + permissions = listOf( + RestShareLinkAccessType.Preview, + RestShareLinkAccessType.Download, + ) + ), + ) + ) + }.mapSuccess { response -> + response.linkUrl?.let { "${credentials.serverUrl}$it" } ?: error("Link URL not found") + } + } + } @Suppress("TooGenericExceptionCaught") private suspend inline fun wrapCellsResponse( - performRequest: () -> com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse + performRequest: () -> HttpResponse ): NetworkResponse = try { diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt index ba9b7d275d7..a6bedd0032c 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt @@ -23,9 +23,9 @@ import com.wire.kalium.cells.data.model.toModel import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.cells.domain.model.PreCheckResult import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.error.wrapApiRequest import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.map -import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.CancellationException @@ -69,43 +69,40 @@ internal class CellsDataSource internal constructor( } override suspend fun getFiles(cellName: String): Either> = - withContext(dispatchers.io) { - wrapApiRequest { - cellsApi.getFiles(cellName) - }.map { response -> - response.nodes - .filterNot { it.isRecycleBin } - .map { it.toModel() } - } + wrapApiRequest { + cellsApi.getFiles(cellName) + }.map { response -> + response.nodes + .filterNot { it.isRecycleBin } + .map { it.toModel() } } - override suspend fun deleteFile(node: CellNode): Either { - return withContext(dispatchers.io) { - wrapApiRequest { - cellsApi.delete(node.toDto()) - } + override suspend fun deleteFile(node: CellNode): Either = + wrapApiRequest { + cellsApi.delete(node.toDto()) } - } - override suspend fun publishDraft(node: CellNode): Either { - return withContext(dispatchers.io) { - wrapApiRequest { - cellsApi.publishDraft(node.toDto()) - } + override suspend fun publishDraft(nodeUuid: String): Either = + wrapApiRequest { + cellsApi.publishDraft(nodeUuid) } - } - override suspend fun cancelDraft(node: CellNode): Either { - return withContext(dispatchers.io) { - wrapApiRequest { - cellsApi.cancelDraft(node.toDto()) - } + override suspend fun getFiles(cellNames: List): Either> = + wrapApiRequest { + cellsApi.getFiles(cellNames) + }.map { response -> + response.nodes + .filterNot { it.isRecycleBin } + .map { it.toModel() } } - } -} -internal inline fun wrapApiRequest(networkCall: () -> NetworkResponse): Either = - when (val result = networkCall()) { - is NetworkResponse.Success -> Either.Right(result.value) - is NetworkResponse.Error -> Either.Left(NetworkFailure.ServerMiscommunication(result.kException)) - } + override suspend fun cancelDraft(nodeUuid: String, versionUuid: String): Either = + wrapApiRequest { + cellsApi.cancelDraft(nodeUuid, versionUuid) + } + + override suspend fun getPublicUrl(nodeUuid: String, fileName: String): Either = + wrapApiRequest { + cellsApi.createPublicUrl(nodeUuid, fileName) + } +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt new file mode 100644 index 00000000000..31677ea0106 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt @@ -0,0 +1,80 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.data + +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.common.error.wrapStorageRequest +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftDao +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class MessageAttachmentDraftDataSource internal constructor( + private val messageAttachmentDao: MessageAttachmentDraftDao +) : MessageAttachmentDraftRepository { + + override suspend fun add(conversationId: QualifiedID, node: CellNode, dataPath: String) = wrapStorageRequest { + messageAttachmentDao.addAttachment( + uuid = node.uuid, + versionId = node.versionId, + conversationId = QualifiedIDEntity(conversationId.value, conversationId.domain), + fileName = node.path.substringAfterLast("/"), + fileSize = node.size ?: 0, + dataPath = dataPath, + nodePath = node.path, + status = AttachmentUploadStatus.UPLOADING.name + ) + } + + override suspend fun observe(conversationId: QualifiedID): Flow> = + messageAttachmentDao.observeAttachments(QualifiedIDEntity(conversationId.value, conversationId.domain)) + .map { list -> list.map { it.toModel() } } + + override suspend fun updateStatus(uuid: String, status: AttachmentUploadStatus) = wrapStorageRequest { + messageAttachmentDao.updateUploadStatus(uuid, status.name) + } + + override suspend fun remove(uuid: String) = wrapStorageRequest { + messageAttachmentDao.deleteAttachment(uuid) + } + + override suspend fun get(uuid: String) = wrapStorageRequest { + messageAttachmentDao.getAttachment(uuid)?.toModel() + } + + override suspend fun getAll(conversationId: ConversationId) = + wrapStorageRequest { + messageAttachmentDao.getAttachments( + QualifiedIDEntity(conversationId.value, conversationId.domain) + ).map { it.toModel() } + } +} + +private fun MessageAttachmentDraftEntity.toModel() = AttachmentDraft( + uuid = uuid, + versionId = versionId, + fileName = fileName, + fileSize = fileSize, + uploadStatus = AttachmentUploadStatus.valueOf(uploadStatus), +) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt index caee833305e..8b5564aef38 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellUploadManager.kt @@ -45,7 +45,7 @@ public interface CellUploadManager { * Cancel upload of the node with [nodeUuid]. * @param nodeUuid UUID of the node to cancel */ - public fun cancelUpload(nodeUuid: String) + public suspend fun cancelUpload(nodeUuid: String) /** * Get upload info for the node with [nodeUuid]. @@ -53,20 +53,28 @@ public interface CellUploadManager { * @return [CellUploadInfo] or null if the node is not being uploaded */ public fun getUploadInfo(nodeUuid: String): CellUploadInfo? + + /** + * Check if the node with [nodeUuid] is being uploaded. + * @param nodeUuid UUID of the node to check + * @return true if the node is being uploaded + */ + public fun isUploading(nodeUuid: String): Boolean } /** * Information about the upload of the file. * @param progress upload progress - * @param uploadFiled true if the upload failed + * @param uploadFailed true if the upload failed */ public data class CellUploadInfo( val progress: Float = 0f, - val uploadFiled: Boolean = false, + val uploadFailed: Boolean = false, ) public sealed interface CellUploadEvent { public data class UploadProgress(val progress: Float) : CellUploadEvent public data object UploadCompleted : CellUploadEvent public data object UploadError : CellUploadEvent + public data object UploadCancelled : CellUploadEvent } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt index b137f2b4dfe..b4223d4ee0b 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt @@ -27,7 +27,9 @@ internal interface CellsRepository { suspend fun preCheck(nodePath: String): Either suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either suspend fun getFiles(cellName: String): Either> + suspend fun getFiles(cellNames: List): Either> suspend fun deleteFile(node: CellNode): Either - suspend fun cancelDraft(node: CellNode): Either - suspend fun publishDraft(node: CellNode): Either + suspend fun cancelDraft(nodeUuid: String, versionUuid: String): Either + suspend fun publishDraft(nodeUuid: String): Either + suspend fun getPublicUrl(nodeUuid: String, fileName: String): Either } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/MessageAttachmentDraftRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/MessageAttachmentDraftRepository.kt new file mode 100644 index 00000000000..7f75ade6308 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/MessageAttachmentDraftRepository.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import kotlinx.coroutines.flow.Flow + +internal interface MessageAttachmentDraftRepository { + suspend fun add(conversationId: QualifiedID, node: CellNode, dataPath: String): Either + suspend fun get(uuid: String): Either + suspend fun getAll(conversationId: ConversationId): Either> + suspend fun observe(conversationId: QualifiedID): Flow> + suspend fun updateStatus(uuid: String, status: AttachmentUploadStatus): Either + suspend fun remove(uuid: String): Either +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt new file mode 100644 index 00000000000..6fb6a12098e --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.model + +public data class AttachmentDraft( + val uuid: String, + val versionId: String, + val fileName: String, + val fileSize: Long, + val uploadStatus: AttachmentUploadStatus, +) + +public enum class AttachmentUploadStatus { + UPLOADING, + UPLOADED, + FAILED, +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt index 456358a69cc..102cd5e8dba 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/PreCheckResult.kt @@ -17,6 +17,10 @@ */ package com.wire.kalium.cells.domain.model +/** + * Represents the result of a pre-check operation to determine if file + * with the given path and name already exists on the Cell server. + */ public sealed interface PreCheckResult { public data object Success : PreCheckResult public data class FileExists(val nextPath: String) : PreCheckResult diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCase.kt new file mode 100644 index 00000000000..f52bc672998 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCase.kt @@ -0,0 +1,93 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.CellsScope.Companion.ROOT_CELL +import com.wire.kalium.cells.domain.CellUploadEvent +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus.FAILED +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus.UPLOADED +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.map +import com.wire.kalium.common.functional.onSuccess +import com.wire.kalium.logic.data.id.QualifiedID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import okio.Path + +public interface AddAttachmentDraftUseCase { + /** + * Adds an attachment draft to the conversation and starts attachment upload. + * + * @param conversationId ID of the conversation + * @param fileName name of the attachment + * @param assetPath path to the attachment asset + * @param assetSize size of the attachment asset + * @return [Either] with [Unit] or [NetworkFailure] + */ + public suspend operator fun invoke( + conversationId: QualifiedID, + fileName: String, + assetPath: Path, + assetSize: Long, + ): Either +} + +internal class AddAttachmentDraftUseCaseImpl internal constructor( + private val uploadManager: CellUploadManager, + private val repository: MessageAttachmentDraftRepository, + private val scope: CoroutineScope, +) : AddAttachmentDraftUseCase { + + override suspend fun invoke( + conversationId: QualifiedID, + fileName: String, + assetPath: Path, + assetSize: Long, + ): Either { + + val destNodePath = "$ROOT_CELL/$conversationId/$fileName" + + return uploadManager.upload(assetPath, assetSize, destNodePath).map { node -> + persistDraftNode(conversationId, assetPath, node).onSuccess { + scope.launch observer@{ + uploadManager.observeUpload(node.uuid)?.collectLatest { event -> + when (event) { + CellUploadEvent.UploadCompleted -> repository.updateStatus(node.uuid, UPLOADED) + CellUploadEvent.UploadError -> repository.updateStatus(node.uuid, FAILED) + CellUploadEvent.UploadCancelled -> this@observer.cancel() + is CellUploadEvent.UploadProgress -> {} + } + } + } + } + } + } + + private suspend fun persistDraftNode(conversationId: QualifiedID, assetPath: Path, node: CellNode) = + repository.add( + conversationId = conversationId, + node = node, + dataPath = assetPath.toString(), + ) +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt deleted file mode 100644 index d714c2ef62c..00000000000 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/CancelDraftUseCase.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cells.domain.usecase - -import com.wire.kalium.cells.domain.CellsRepository -import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.common.error.NetworkFailure -import com.wire.kalium.common.functional.Either - -public interface CancelDraftUseCase { - /** - * Cancels the draft of the cell node. - * @param node Cell node to cancel the draft for. - * @return Either. Result of the operation. - */ - public suspend operator fun invoke(node: CellNode): Either -} - -internal class CancelDraftUseCaseImpl( - private val cellsRepository: CellsRepository -) : CancelDraftUseCase { - override suspend operator fun invoke(node: CellNode): Either { - return cellsRepository.cancelDraft(node) - } -} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt deleted file mode 100644 index da74684328f..00000000000 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/DeleteCellFileUseCase.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cells.domain.usecase - -import com.wire.kalium.cells.domain.CellsRepository -import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.common.error.NetworkFailure -import com.wire.kalium.common.functional.Either - -public interface DeleteCellFileUseCase { - /** - * Delete a file from the cell. - * Note: Delete operation is asynchronous on Cells Server. Actual delete does not happen immediately. - * @param node The node of the file to delete. - * @return Either a [NetworkFailure] or [Unit]. - */ - public suspend operator fun invoke(node: CellNode): Either -} - -internal class DeleteCellFileUseCaseImpl( - private val cellsRepository: CellsRepository -) : DeleteCellFileUseCase { - override suspend operator fun invoke(node: CellNode): Either { - return cellsRepository.deleteFile(node) - } -} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt deleted file mode 100644 index 2bb1e3f0844..00000000000 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/GetCellFilesUseCase.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cells.domain.usecase - -import com.wire.kalium.cells.domain.CellsRepository -import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.common.error.NetworkFailure -import com.wire.kalium.common.functional.Either - -public interface GetCellFilesUseCase { - /** - * Get the list of files in the cell. - * @param cellName the name of the cell. - * @return the list of files in the cell or [NetworkFailure]. - */ - public suspend operator fun invoke(cellName: String): Either> -} - -internal class GetCellFilesUseCaseImpl( - private val cellsRepository: CellsRepository -) : GetCellFilesUseCase { - override suspend operator fun invoke(cellName: String): Either> { - return cellsRepository.getFiles(cellName) - } -} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCase.kt new file mode 100644 index 00000000000..8743527cbb3 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCase.kt @@ -0,0 +1,59 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.common.functional.getOrNull +import com.wire.kalium.logic.data.id.QualifiedID +import kotlinx.coroutines.flow.Flow + +public interface ObserveAttachmentDraftsUseCase { + /** + * Observe draft attachments for the given conversation. + * + * @param conversationId ID of the conversation + * @return [Flow] of [List] of [AttachmentDraft] + */ + public suspend operator fun invoke(conversationId: QualifiedID): Flow> +} + +internal class ObserveAttachmentDraftsUseCaseImpl internal constructor( + private val repository: MessageAttachmentDraftRepository, + private val uploadManager: CellUploadManager, +) : ObserveAttachmentDraftsUseCase { + + override suspend fun invoke(conversationId: QualifiedID): Flow> { + removeStaleUploads(conversationId) + return repository.observe(conversationId) + } + + /** + * Remove uploads that are in UPLOADING status but not being uploaded. + */ + private suspend fun removeStaleUploads(conversationId: QualifiedID) { + repository.getAll(conversationId).getOrNull() + ?.filter { it.uploadStatus == AttachmentUploadStatus.UPLOADING } + ?.filterNot { uploadManager.isUploading(it.uuid) } + ?.onEach { + repository.remove(it.uuid) + } + } +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt new file mode 100644 index 00000000000..727d56c118f --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.CellsScope.Companion.ROOT_CELL +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.common.functional.getOrElse +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +public interface ObserveCellFilesUseCase { + /** + * Observe files from all conversations. + * + * @return [Flow] of [List] of [ConversationFiles] + */ + public suspend operator fun invoke(): Flow> +} + +internal class ObserveCellFilesUseCaseImpl( + private val conversationsDAO: ConversationDAO, + private val cellsRepository: CellsRepository, +) : ObserveCellFilesUseCase { + + override suspend operator fun invoke(): Flow> { + return conversationsDAO.getAllConversationDetails(fromArchive = false, filter = ConversationFilterEntity.ALL).map { conversations -> + +// val files = cellsRepository.getFiles(conversations.map { "${ROOT_CELL}/${it.id}" }) +// .getOrElse { emptyList() } +// .groupBy { it.path.substringBeforeLast("/") } +// .map { group -> +// val conversation = conversations.first { it.id.toString() == group.key.substringAfterLast("/") } +// ConversationFiles( +// conversationId = QualifiedID(conversation.id.value, conversation.id.domain), +// conversationTitle = conversation.name ?: "", +// files = group.value +// ) +// } + + conversations.map { conversation -> + ConversationFiles( + conversationId = QualifiedID(conversation.id.value, conversation.id.domain), + conversationTitle = conversation.name ?: "", + files = cellsRepository.getFiles("${ROOT_CELL}/${conversation.id}").getOrElse { emptyList() } + ) + }.filter { it.files.isNotEmpty() } + } + } +} +public data class ConversationFiles( + val conversationId: ConversationId, + val conversationTitle: String, + val files: List +) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt new file mode 100644 index 00000000000..1d9538b79a2 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt @@ -0,0 +1,67 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.getOrNull +import com.wire.kalium.common.functional.onFailure +import com.wire.kalium.common.functional.onSuccess +import com.wire.kalium.logic.data.id.ConversationId + +public interface PublishAttachmentsUseCase { + /** + * For TESTING purposes only. + * Use case for publishing all draft attachments and creating public URLs. + */ + public suspend operator fun invoke(conversationId: ConversationId): Either> +} + +internal class PublishAttachmentsUseCaseImpl internal constructor( + private val cellsRepository: CellsRepository, + private val repository: MessageAttachmentDraftRepository, +) : PublishAttachmentsUseCase { + + @Suppress("ReturnCount") + override suspend fun invoke(conversationId: ConversationId): Either> { + + val publicUrls = mutableListOf() + val attachments = repository.getAll(conversationId).getOrNull() + + attachments?.forEach { attachment -> + + cellsRepository.publishDraft(attachment.uuid).onFailure { + return Either.Left(it) + } + + cellsRepository.getPublicUrl(attachment.uuid, attachment.fileName) + .onSuccess { + publicUrls.add(it) + } + .onFailure { + return Either.Left(it) + } + + repository.remove(attachment.uuid) + } + + return Either.Right(publicUrls.toList()) + } +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt deleted file mode 100644 index f07673677a4..00000000000 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishDraftUseCase.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cells.domain.usecase - -import com.wire.kalium.cells.domain.CellsRepository -import com.wire.kalium.cells.domain.model.CellNode -import com.wire.kalium.common.error.NetworkFailure -import com.wire.kalium.common.functional.Either - -public interface PublishDraftUseCase { - /** - * Publish the draft in the cell. - * @param node the cell node. - * @return [NetworkFailure] or [Unit]. - */ - public suspend operator fun invoke(node: CellNode): Either -} - -internal class PublishDraftUseCaseImpl( - private val cellsRepository: CellsRepository -) : PublishDraftUseCase { - override suspend operator fun invoke(node: CellNode): Either { - return cellsRepository.publishDraft(node) - } -} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCase.kt new file mode 100644 index 00000000000..5d0807d402e --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCase.kt @@ -0,0 +1,69 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.flatMap +import com.wire.kalium.common.functional.left +import com.wire.kalium.common.functional.onSuccess + +public interface RemoveAttachmentDraftUseCase { + /** + * Removes the draft attachment from conversation. + * If the attachment is in the process of uploading, the upload will be cancelled. + * If the attachment is already uploaded, the attachment draft will be removed from the server. + * + * @param uuid UUID of the attachment + * @return [Either] with [Unit] or [CoreFailure] + */ + public suspend operator fun invoke(uuid: String): Either +} + +internal class RemoveAttachmentDraftUseCaseImpl internal constructor( + private val uploadManager: CellUploadManager, + private val attachmentsRepository: MessageAttachmentDraftRepository, + private val cellsRepository: CellsRepository, +) : RemoveAttachmentDraftUseCase { + + override suspend fun invoke(uuid: String): Either = + attachmentsRepository.get(uuid).flatMap { attachment -> + when (attachment?.uploadStatus) { + AttachmentUploadStatus.UPLOADING -> cancelAttachmentUpload(uuid) + AttachmentUploadStatus.UPLOADED -> removeAttachmentDraft(attachment) + AttachmentUploadStatus.FAILED -> attachmentsRepository.remove(uuid) + null -> StorageFailure.DataNotFound.left() + } + } + + private suspend fun cancelAttachmentUpload(uuid: String): Either { + uploadManager.cancelUpload(uuid) + return attachmentsRepository.remove(uuid) + } + + private suspend fun removeAttachmentDraft(attachment: AttachmentDraft) = + cellsRepository.cancelDraft(attachment.uuid, attachment.versionId).onSuccess { + attachmentsRepository.remove(attachment.uuid) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 2138ffd2f64..fc1577efb88 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -1880,6 +1880,7 @@ class UserSessionScope internal constructor( staleEpochVerifier, legalHoldHandler, observeFileSharingStatus, + cells.publishAttachments, this, userScopedLogger, ) @@ -2180,8 +2181,13 @@ class UserSessionScope internal constructor( InCallReactionsDataSource() } - val cells: CellsScope - get() = CellsScope(globalScope.unboundNetworkContainer.cellsClient) + val cells: CellsScope by lazy { + CellsScope( + cellsClient = globalScope.unboundNetworkContainer.cellsClient, + attachmentDraftDao = userStorage.database.messageAttachmentDraftDao, + conversationsDAO = userStorage.database.conversationDAO, + ) + } /** * This will start subscribers of observable work per user session, as long as the user is logged in. diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index 2a6129225bf..e411098ad31 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.message +import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.data.asset.AssetRepository @@ -126,6 +127,7 @@ class MessageScope internal constructor( private val staleEpochVerifier: StaleEpochVerifier, private val legalHoldHandler: LegalHoldHandler, private val observeFileSharingStatusUseCase: ObserveFileSharingStatusUseCase, + private val publishAttachmentsUseCase: PublishAttachmentsUseCase, private val scope: CoroutineScope, kaliumLogger: KaliumLogger, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, @@ -234,6 +236,7 @@ class MessageScope internal constructor( messageSendFailureHandler = messageSendFailureHandler, userPropertyRepository = userPropertyRepository, selfDeleteTimer = observeSelfDeletingMessages, + publishAttachmentsUseCase = publishAttachmentsUseCase, scope = scope ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageUseCase.kt index e835097898f..52e343cae19 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageUseCase.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature.message import com.benasher44.uuid.uuid4 +import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase import com.wire.kalium.cryptography.utils.AES256Key import com.wire.kalium.cryptography.utils.generateRandomAES256Key import com.wire.kalium.common.error.CoreFailure @@ -37,6 +38,7 @@ import com.wire.kalium.logic.data.message.linkpreview.MessageLinkPreview import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.flatMap +import com.wire.kalium.common.functional.getOrElse import com.wire.kalium.common.functional.getOrNull import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.logger.kaliumLogger @@ -63,6 +65,7 @@ class SendTextMessageUseCase internal constructor( private val messageSendFailureHandler: MessageSendFailureHandler, private val userPropertyRepository: UserPropertyRepository, private val selfDeleteTimer: ObserveSelfDeletionTimerSettingsForConversationUseCase, + private val publishAttachmentsUseCase: PublishAttachmentsUseCase, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl, private val scope: CoroutineScope ) { @@ -86,11 +89,23 @@ class SendTextMessageUseCase internal constructor( val previews = uploadLinkPreviewImages(linkPreviews) + val attachments = publishAttachmentsUseCase.invoke(conversationId).getOrElse { emptyList() } + + val textWithAttachments = if (attachments.isNotEmpty()) { + buildString { + append(text) + appendLine() + attachments.forEach { appendLine(it) } + } + } else { + text + } + provideClientId().flatMap { clientId -> val message = Message.Regular( id = generatedMessageUuid, content = MessageContent.Text( - value = text, + value = textWithAttachments, linkPreviews = previews, mentions = mentions, quotedMessageReference = quotedMessageId?.let { quotedMessageId -> diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt index 14a4dc99d43..7f9c16d6035 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.message +import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase import com.wire.kalium.cryptography.utils.SHA256Key import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.data.asset.AssetRepository @@ -311,6 +312,9 @@ class SendTextMessageCaseTest { @Mock val observeSelfDeletionTimerSettingsForConversation = mock(ObserveSelfDeletionTimerSettingsForConversationUseCase::class) + @Mock + val publishAttachmentsUseCase = mock(PublishAttachmentsUseCase::class) + suspend fun withSendMessageSuccess() = apply { coEvery { messageSender.sendMessage(any(), any()) @@ -376,6 +380,7 @@ class SendTextMessageCaseTest { messageSendFailureHandler, userPropertyRepository, observeSelfDeletionTimerSettingsForConversation, + publishAttachmentsUseCase, scope = coroutineScope, dispatchers = coroutineScope.testKaliumDispatcher ) diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAttachmentDraft.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAttachmentDraft.sq new file mode 100644 index 00000000000..7b6c0ef07b5 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAttachmentDraft.sq @@ -0,0 +1,54 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Int; +import kotlin.String; + +CREATE TABLE MessageAttachmentDraft ( + attachment_id TEXT NOT NULL, + version_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + file_name TEXT NOT NULL, + file_size INTEGER NOT NULL, + data_path TEXT NOT NULL, + node_path TEXT NOT NULL, + upload_status TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (attachment_id) +); + +getDraft: +SELECT * FROM MessageAttachmentDraft WHERE attachment_id = ?; + +getDrafts: +SELECT * FROM MessageAttachmentDraft WHERE conversation_id = ?; + +upsertDraft: +INSERT INTO MessageAttachmentDraft( + attachment_id, + version_id, + conversation_id, + file_name, + file_size, + data_path, + node_path, + upload_status +) VALUES(?,?,?,?,?,?,?,?) +ON CONFLICT(attachment_id) DO UPDATE SET + version_id = excluded.version_id, + conversation_id = excluded.conversation_id, + file_name = excluded.file_name, + file_size = excluded.file_size, + data_path = excluded.data_path, + node_path = excluded.node_path, + upload_status = excluded.upload_status; + +updateUploadStatus: +UPDATE MessageAttachmentDraft SET upload_status = ? WHERE attachment_id = ?; + +deleteDraft: +DELETE FROM MessageAttachmentDraft WHERE attachment_id = ?; + +selectChanges: +SELECT changes(); diff --git a/persistence/src/commonMain/db_user/migrations/99.sqm b/persistence/src/commonMain/db_user/migrations/99.sqm new file mode 100644 index 00000000000..1451e36cb6a --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/99.sqm @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS MessageAttachmentDraft ( + attachment_id TEXT NOT NULL, + verison_id TEXT NOT NULL, + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + file_name TEXT NOT NULL, + file_size INTEGER NOT NULL, + data_path TEXT NOT NULL, + node_path TEXT NOT NULL, + upload_status TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (attachment_id) +); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDao.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDao.kt new file mode 100644 index 00000000000..c060d5675a9 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDao.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.messageattachment + +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import kotlinx.coroutines.flow.Flow + +@Suppress("LongParameterList") +interface MessageAttachmentDraftDao { + suspend fun getAttachment(uuid: String): MessageAttachmentDraftEntity? + suspend fun getAttachments(conversationId: QualifiedIDEntity): List + suspend fun deleteAttachment(uuid: String) + suspend fun observeAttachments(conversationId: QualifiedIDEntity): Flow> + suspend fun addAttachment( + uuid: String, + versionId: String, + conversationId: QualifiedIDEntity, + fileName: String, + fileSize: Long, + dataPath: String, + nodePath: String, + status: String, + ) + suspend fun updateUploadStatus(uuid: String, status: String) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt new file mode 100644 index 00000000000..5128707ed15 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt @@ -0,0 +1,72 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.messageattachment + +import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.MessageAttachmentDraftQueries +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftMapper.toDao +import com.wire.kalium.persistence.util.mapToList +import kotlinx.coroutines.flow.Flow + +internal class MessageAttachmentDraftDaoImpl internal constructor( + private val queries: MessageAttachmentDraftQueries, +) : MessageAttachmentDraftDao { + + override suspend fun addAttachment( + uuid: String, + versionId: String, + conversationId: QualifiedIDEntity, + fileName: String, + fileSize: Long, + dataPath: String, + nodePath: String, + status: String, + ) { + queries.upsertDraft( + attachment_id = uuid, + version_id = versionId, + conversation_id = conversationId, + file_name = fileName, + file_size = fileSize, + data_path = dataPath, + node_path = nodePath, + upload_status = status, + ) + } + + override suspend fun updateUploadStatus(uuid: String, status: String) { + queries.updateUploadStatus(status, uuid) + } + + override suspend fun getAttachments(conversationId: QualifiedIDEntity): List { + return queries.getDrafts(conversationId, ::toDao).executeAsList() + } + + override suspend fun observeAttachments(conversationId: QualifiedIDEntity): Flow> { + return queries.getDrafts(conversationId, ::toDao).asFlow().mapToList() + } + + override suspend fun getAttachment(uuid: String): MessageAttachmentDraftEntity? { + return queries.getDraft(uuid, ::toDao).executeAsOneOrNull() + } + + override suspend fun deleteAttachment(uuid: String) { + queries.deleteDraft(uuid) + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftEntity.kt new file mode 100644 index 00000000000..24572add138 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftEntity.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.messageattachment + +import com.wire.kalium.persistence.dao.ConversationIDEntity + +data class MessageAttachmentDraftEntity( + val uuid: String, + val versionId: String, + val conversationId: ConversationIDEntity, + val fileName: String, + val fileSize: Long, + val dataPath: String, + val nodePath: String, + val uploadStatus: String, +) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftMapper.kt new file mode 100644 index 00000000000..93bd667f650 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftMapper.kt @@ -0,0 +1,43 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.messageattachment + +import com.wire.kalium.persistence.dao.QualifiedIDEntity + +@Suppress("LongParameterList") +internal data object MessageAttachmentDraftMapper { + fun toDao( + attachmentId: String, + versionId: String, + conversationId: QualifiedIDEntity, + fileName: String, + fileSize: Long, + dataPath: String, + nodePath: String, + uploadStatus: String, + ) = MessageAttachmentDraftEntity( + uuid = attachmentId, + versionId = versionId, + conversationId = conversationId, + fileName = fileName, + fileSize = fileSize, + dataPath = dataPath, + nodePath = nodePath, + uploadStatus = uploadStatus, + ) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index f58f4f2b279..0301df668d7 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -33,6 +33,7 @@ import com.wire.kalium.persistence.Member import com.wire.kalium.persistence.Message import com.wire.kalium.persistence.MessageAssetContent import com.wire.kalium.persistence.MessageAssetTransferStatus +import com.wire.kalium.persistence.MessageAttachmentDraft import com.wire.kalium.persistence.MessageConversationChangedContent import com.wire.kalium.persistence.MessageConversationLocationContent import com.wire.kalium.persistence.MessageConversationProtocolChangedContent @@ -281,4 +282,8 @@ internal object TableMapper { val conversationFolderAdapter = ConversationFolder.Adapter( folder_typeAdapter = EnumColumnAdapter() ) + + val messageAttachmentDraftAdapter = MessageAttachmentDraft.Adapter( + conversation_idAdapter = QualifiedIDAdapter, + ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 0396b04f778..a2bc50f5206 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -71,6 +71,8 @@ import com.wire.kalium.persistence.dao.message.MessageDAOImpl import com.wire.kalium.persistence.dao.message.MessageMetadataDAO import com.wire.kalium.persistence.dao.message.MessageMetadataDAOImpl import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAOImpl +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftDao +import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftDaoImpl import com.wire.kalium.persistence.dao.newclient.NewClientDAO import com.wire.kalium.persistence.dao.newclient.NewClientDAOImpl import com.wire.kalium.persistence.dao.reaction.ReactionDAO @@ -165,7 +167,8 @@ class UserDatabaseBuilder internal constructor( MessageDraftAdapter = TableMapper.messageDraftsAdapter, LastMessageAdapter = TableMapper.lastMessageAdapter, LabeledConversationAdapter = TableMapper.labeledConversationAdapter, - ConversationFolderAdapter = TableMapper.conversationFolderAdapter + ConversationFolderAdapter = TableMapper.conversationFolderAdapter, + MessageAttachmentDraftAdapter = TableMapper.messageAttachmentDraftAdapter, ) init { @@ -318,6 +321,9 @@ class UserDatabaseBuilder internal constructor( queriesContext ) + val messageAttachmentDraftDao: MessageAttachmentDraftDao + get() = MessageAttachmentDraftDaoImpl(database.messageAttachmentDraftQueries) + val debugExtension: DebugExtension get() = DebugExtension( sqlDriver = sqlDriver, From 5b71d2e134228442660bfbb26d1075f21e78e6fd Mon Sep 17 00:00:00 2001 From: "sergei.bakhtiarov" Date: Mon, 17 Feb 2025 15:07:41 +0100 Subject: [PATCH 7/7] feat: unit tests and refactoring (#WPB-15743) --- cells/build.gradle.kts | 36 ++- .../data/AwsProgressListenerInterceptor.kt | 0 .../kalium/cells/data/CellsAwsClientJvm.kt | 0 .../wire/kalium/cells/data/MetadataHeaders.kt | 0 .../com/wire/kalium/cells/CellsScope.kt | 17 +- .../data/{CellsApi.kt => CellsApiImpl.kt} | 58 +---- .../wire/kalium/cells/data/CellsDataSource.kt | 10 +- .../data/MessageAttachmentDraftDataSource.kt | 1 + .../com/wire/kalium/cells/domain/CellsApi.kt | 32 +++ .../kalium/cells/domain/CellsRepository.kt | 1 - .../kalium/cells/domain/NodeServiceBuilder.kt | 60 +++++ .../cells/domain/model/AttachmentDraft.kt | 1 + .../domain/usecase/ObserveCellFilesUseCase.kt | 14 +- .../usecase/PublishAttachmentsUseCase.kt | 4 +- .../cells/domain/CellUploadManagerTest.kt | 241 ++++++++++++++++++ .../wire/kalium/cells/domain/CellsApiTest.kt | 111 ++++++++ .../usecase/AddAttachmentDraftUseCaseTest.kt | 221 ++++++++++++++++ .../ObserveAttachmentDraftsUseCaseTest.kt | 84 ++++++ .../RemoveAttachmentDraftUseCaseTest.kt | 183 +++++++++++++ 19 files changed, 989 insertions(+), 85 deletions(-) rename cells/src/{androidMain => commonJvmAndroid}/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt (100%) rename cells/src/{androidMain => commonJvmAndroid}/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt (100%) rename cells/src/{androidMain => commonJvmAndroid}/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt (100%) rename cells/src/commonMain/kotlin/com/wire/kalium/cells/data/{CellsApi.kt => CellsApiImpl.kt} (70%) create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsApi.kt create mode 100644 cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/NodeServiceBuilder.kt create mode 100644 cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellUploadManagerTest.kt create mode 100644 cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellsApiTest.kt create mode 100644 cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCaseTest.kt create mode 100644 cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCaseTest.kt create mode 100644 cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCaseTest.kt diff --git a/cells/build.gradle.kts b/cells/build.gradle.kts index 24a8088500e..81c295f76fc 100644 --- a/cells/build.gradle.kts +++ b/cells/build.gradle.kts @@ -18,6 +18,7 @@ plugins { id(libs.plugins.android.library.get().pluginId) id(libs.plugins.kotlin.multiplatform.get().pluginId) + alias(libs.plugins.ksp) id(libs.plugins.kalium.library.get().pluginId) } @@ -28,7 +29,7 @@ kaliumLibrary { kotlin { explicitApi() sourceSets { - commonMain { + val commonMain by getting { dependencies { implementation(project(":common")) implementation(project(":network")) @@ -42,11 +43,32 @@ kotlin { implementation(libs.wire.cells.sdk) } } - commonTest { + val commonTest by getting { dependencies { + // coroutines + implementation(libs.coroutines.test) + implementation(libs.turbine) + // ktor test + implementation(libs.ktor.mock) + // mocks + implementation(libs.mockative.runtime) + implementation(libs.okio.test) } } - androidMain { + + fun org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.addCommonKotlinJvmSourceDir() { + kotlin.srcDir("src/commonJvmAndroid/kotlin") + } + + val jvmMain by getting { + addCommonKotlinJvmSourceDir() + dependencies { + implementation(libs.ktor.okHttp) + implementation(awssdk.services.s3) + } + } + val androidMain by getting { + addCommonKotlinJvmSourceDir() dependencies { implementation(libs.ktor.okHttp) implementation(awssdk.services.s3) @@ -54,3 +76,11 @@ kotlin { } } } + +dependencies { + configurations + .filter { it.name.startsWith("ksp") && it.name.contains("Test") } + .forEach { + add(it.name, libs.mockative.processor) + } +} diff --git a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt b/cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt similarity index 100% rename from cells/src/androidMain/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt rename to cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/AwsProgressListenerInterceptor.kt diff --git a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt b/cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt similarity index 100% rename from cells/src/androidMain/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt rename to cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/CellsAwsClientJvm.kt diff --git a/cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt b/cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt similarity index 100% rename from cells/src/androidMain/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt rename to cells/src/commonJvmAndroid/kotlin/com/wire/kalium/cells/data/MetadataHeaders.kt diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt index ff721b9ec0a..08848be4a2a 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/CellsScope.kt @@ -18,15 +18,16 @@ package com.wire.kalium.cells import com.wire.kalium.cells.data.CellUploadManagerImpl -import com.wire.kalium.cells.data.CellsApi import com.wire.kalium.cells.data.CellsApiImpl import com.wire.kalium.cells.data.CellsAwsClient import com.wire.kalium.cells.data.CellsDataSource import com.wire.kalium.cells.data.MessageAttachmentDraftDataSource import com.wire.kalium.cells.data.cellsAwsClient import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.CellsApi import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.NodeServiceBuilder import com.wire.kalium.cells.domain.model.CellsCredentials import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCaseImpl @@ -38,6 +39,7 @@ import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCaseImpl import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCase import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCaseImpl +import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.messageattachment.MessageAttachmentDraftDao import io.ktor.client.HttpClient @@ -69,11 +71,14 @@ public class CellsScope( private val cellAwsClient: CellsAwsClient get() = cellsAwsClient(cellClientCredentials) + private val nodeServiceApi: NodeServiceApi + get() = NodeServiceBuilder + .withHttpClient(cellsClient) + .withCredentials(cellClientCredentials) + .build() + private val cellsApi: CellsApi - get() = CellsApiImpl( - credentials = cellClientCredentials, - httpClient = cellsClient - ) + get() = CellsApiImpl(nodeServiceApi = nodeServiceApi) private val cellsRepository: CellsRepository get() = CellsDataSource( @@ -101,7 +106,7 @@ public class CellsScope( get() = ObserveAttachmentDraftsUseCaseImpl(messageAttachmentsDraftRepository, uploadManager) public val publishAttachments: PublishAttachmentsUseCase - get() = PublishAttachmentsUseCaseImpl(cellsRepository, messageAttachmentsDraftRepository) + get() = PublishAttachmentsUseCaseImpl(cellsRepository, messageAttachmentsDraftRepository, cellClientCredentials) public val observeFiles: ObserveCellFilesUseCase get() = ObserveCellFilesUseCaseImpl(conversationsDAO, cellsRepository) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApiImpl.kt similarity index 70% rename from cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt rename to cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApiImpl.kt index ef0e5096372..b50bc894122 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApi.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsApiImpl.kt @@ -21,7 +21,7 @@ import com.wire.kalium.cells.data.model.CellNodeDTO import com.wire.kalium.cells.data.model.GetFilesResponseDTO import com.wire.kalium.cells.data.model.PreCheckResultDTO import com.wire.kalium.cells.data.model.toDto -import com.wire.kalium.cells.domain.model.CellsCredentials +import com.wire.kalium.cells.domain.CellsApi import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi import com.wire.kalium.cells.sdk.kmp.infrastructure.HttpResponse import com.wire.kalium.cells.sdk.kmp.model.RestActionParameters @@ -38,48 +38,16 @@ import com.wire.kalium.cells.sdk.kmp.model.RestShareLinkAccessType import com.wire.kalium.cells.sdk.kmp.model.RestVersionCollection import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException -import com.wire.kalium.network.session.installAuth import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.mapSuccess -import io.ktor.client.HttpClient -import io.ktor.client.plugins.auth.providers.BearerAuthProvider -import io.ktor.client.plugins.auth.providers.BearerTokens import io.ktor.http.HttpStatusCode import io.ktor.http.isSuccess import kotlinx.coroutines.CancellationException -internal interface CellsApi { - suspend fun getFiles(cellName: String): NetworkResponse - suspend fun getFiles(cellNames: List): NetworkResponse - suspend fun delete(node: CellNodeDTO): NetworkResponse - suspend fun cancelDraft(nodeUuid: String, versionUuid: String): NetworkResponse - suspend fun publishDraft(nodeUuid: String): NetworkResponse - suspend fun preCheck(path: String): NetworkResponse - suspend fun createPublicUrl(uuid: String, fileName: String): NetworkResponse -} - internal class CellsApiImpl( - private val credentials: CellsCredentials, - httpClient: HttpClient + private val nodeServiceApi: NodeServiceApi, ) : CellsApi { - private companion object { - const val API_VERSION = "v2" - } - - private var nodeServiceApi: NodeServiceApi = NodeServiceApi( - baseUrl = "${credentials.serverUrl}/$API_VERSION", - httpClient = httpClient.config { - installAuth( - BearerAuthProvider( - loadTokens = { BearerTokens(credentials.accessToken, "") }, - refreshTokens = { null }, - realm = null - ) - ) - } - ) - override suspend fun getFiles(cellName: String): NetworkResponse = wrapCellsResponse { nodeServiceApi.lookup( @@ -90,20 +58,6 @@ internal class CellsApiImpl( ) }.mapSuccess { response -> response.toDto() } - override suspend fun getFiles(cellNames: List): NetworkResponse = - wrapCellsResponse { - nodeServiceApi.lookup( - RestLookupRequest( - locators = RestNodeLocators( - cellNames.map { - RestNodeLocator(path = "$it/*") - } - ), - sortField = "Modified" - ) - ) - }.mapSuccess { response -> response.toDto() } - override suspend fun delete(node: CellNodeDTO): NetworkResponse = wrapCellsResponse { nodeServiceApi.performAction( @@ -114,11 +68,11 @@ internal class CellsApiImpl( ) }.mapSuccess {} - override suspend fun publishDraft(uuid: String): NetworkResponse = - getNodeDraftVersions(uuid).mapSuccess { response -> + override suspend fun publishDraft(nodeUuid: String): NetworkResponse = + getNodeDraftVersions(nodeUuid).mapSuccess { response -> wrapCellsResponse { val version = response.versions?.firstOrNull() ?: error("Draft version not found") - nodeServiceApi.promoteVersion(uuid, version.versionId, RestPromoteParameters(publish = true)) + nodeServiceApi.promoteVersion(nodeUuid, version.versionId, RestPromoteParameters(publish = true)) } } @@ -164,7 +118,7 @@ internal class CellsApiImpl( ) ) }.mapSuccess { response -> - response.linkUrl?.let { "${credentials.serverUrl}$it" } ?: error("Link URL not found") + response.linkUrl ?: error("Link URL not found") } } diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt index a6bedd0032c..faafdcc2e6f 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/CellsDataSource.kt @@ -20,6 +20,7 @@ package com.wire.kalium.cells.data import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.data.model.toDto import com.wire.kalium.cells.data.model.toModel +import com.wire.kalium.cells.domain.CellsApi import com.wire.kalium.cells.domain.model.CellNode import com.wire.kalium.cells.domain.model.PreCheckResult import com.wire.kalium.common.error.NetworkFailure @@ -87,15 +88,6 @@ internal class CellsDataSource internal constructor( cellsApi.publishDraft(nodeUuid) } - override suspend fun getFiles(cellNames: List): Either> = - wrapApiRequest { - cellsApi.getFiles(cellNames) - }.map { response -> - response.nodes - .filterNot { it.isRecycleBin } - .map { it.toModel() } - } - override suspend fun cancelDraft(nodeUuid: String, versionUuid: String): Either = wrapApiRequest { cellsApi.cancelDraft(nodeUuid, versionUuid) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt index 31677ea0106..67baf7ed830 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/data/MessageAttachmentDraftDataSource.kt @@ -75,6 +75,7 @@ private fun MessageAttachmentDraftEntity.toModel() = AttachmentDraft( uuid = uuid, versionId = versionId, fileName = fileName, + localFilePath = dataPath, fileSize = fileSize, uploadStatus = AttachmentUploadStatus.valueOf(uploadStatus), ) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsApi.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsApi.kt new file mode 100644 index 00000000000..e8d314c3290 --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsApi.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.data.model.CellNodeDTO +import com.wire.kalium.cells.data.model.GetFilesResponseDTO +import com.wire.kalium.cells.data.model.PreCheckResultDTO +import com.wire.kalium.network.utils.NetworkResponse + +internal interface CellsApi { + suspend fun preCheck(path: String): NetworkResponse + suspend fun cancelDraft(nodeUuid: String, versionUuid: String): NetworkResponse + suspend fun publishDraft(nodeUuid: String): NetworkResponse + suspend fun delete(node: CellNodeDTO): NetworkResponse + suspend fun getFiles(cellName: String): NetworkResponse + suspend fun createPublicUrl(uuid: String, fileName: String): NetworkResponse +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt index b4223d4ee0b..912ca21b08a 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/CellsRepository.kt @@ -27,7 +27,6 @@ internal interface CellsRepository { suspend fun preCheck(nodePath: String): Either suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either suspend fun getFiles(cellName: String): Either> - suspend fun getFiles(cellNames: List): Either> suspend fun deleteFile(node: CellNode): Either suspend fun cancelDraft(nodeUuid: String, versionUuid: String): Either suspend fun publishDraft(nodeUuid: String): Either diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/NodeServiceBuilder.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/NodeServiceBuilder.kt new file mode 100644 index 00000000000..01bfd30bd1a --- /dev/null +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/NodeServiceBuilder.kt @@ -0,0 +1,60 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.domain.model.CellsCredentials +import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi +import com.wire.kalium.network.session.installAuth +import io.ktor.client.HttpClient +import io.ktor.client.plugins.auth.providers.BearerAuthProvider +import io.ktor.client.plugins.auth.providers.BearerTokens + +internal object NodeServiceBuilder { + + private const val API_VERSION = "v2" + + private var httpClient: HttpClient? = null + private var baseUrl: String? = null + private var accessToken: String? = null + + fun withCredentials(credentials: CellsCredentials): NodeServiceBuilder { + baseUrl = "${credentials.serverUrl}/$API_VERSION" + accessToken = credentials.accessToken + return this + } + + fun withHttpClient(httpClient: HttpClient): NodeServiceBuilder { + this.httpClient = httpClient + return this + } + + fun build(): NodeServiceApi { + return NodeServiceApi( + baseUrl = baseUrl ?: error("Base URL is not set"), + httpClient = httpClient?.config { + installAuth( + BearerAuthProvider( + loadTokens = { BearerTokens(accessToken ?: error("Access token not set"), "") }, + refreshTokens = { null }, + realm = null + ) + ) + } ?: error("HttpClient is not set") + ) + } +} diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt index 6fb6a12098e..653a16a54f3 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/model/AttachmentDraft.kt @@ -21,6 +21,7 @@ public data class AttachmentDraft( val uuid: String, val versionId: String, val fileName: String, + val localFilePath: String, val fileSize: Long, val uploadStatus: AttachmentUploadStatus, ) diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt index 727d56c118f..db5faf0b498 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/ObserveCellFilesUseCase.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.map public interface ObserveCellFilesUseCase { /** + * For TESTING purposes only. * Observe files from all conversations. * * @return [Flow] of [List] of [ConversationFiles] @@ -44,19 +45,6 @@ internal class ObserveCellFilesUseCaseImpl( override suspend operator fun invoke(): Flow> { return conversationsDAO.getAllConversationDetails(fromArchive = false, filter = ConversationFilterEntity.ALL).map { conversations -> - -// val files = cellsRepository.getFiles(conversations.map { "${ROOT_CELL}/${it.id}" }) -// .getOrElse { emptyList() } -// .groupBy { it.path.substringBeforeLast("/") } -// .map { group -> -// val conversation = conversations.first { it.id.toString() == group.key.substringAfterLast("/") } -// ConversationFiles( -// conversationId = QualifiedID(conversation.id.value, conversation.id.domain), -// conversationTitle = conversation.name ?: "", -// files = group.value -// ) -// } - conversations.map { conversation -> ConversationFiles( conversationId = QualifiedID(conversation.id.value, conversation.id.domain), diff --git a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt index 1d9538b79a2..acec246c409 100644 --- a/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt +++ b/cells/src/commonMain/kotlin/com/wire/kalium/cells/domain/usecase/PublishAttachmentsUseCase.kt @@ -19,6 +19,7 @@ package com.wire.kalium.cells.domain.usecase import com.wire.kalium.cells.domain.CellsRepository import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.CellsCredentials import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.getOrNull @@ -37,6 +38,7 @@ public interface PublishAttachmentsUseCase { internal class PublishAttachmentsUseCaseImpl internal constructor( private val cellsRepository: CellsRepository, private val repository: MessageAttachmentDraftRepository, + private val credentials: CellsCredentials, ) : PublishAttachmentsUseCase { @Suppress("ReturnCount") @@ -53,7 +55,7 @@ internal class PublishAttachmentsUseCaseImpl internal constructor( cellsRepository.getPublicUrl(attachment.uuid, attachment.fileName) .onSuccess { - publicUrls.add(it) + publicUrls.add("${credentials.serverUrl}$it") } .onFailure { return Either.Left(it) diff --git a/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellUploadManagerTest.kt b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellUploadManagerTest.kt new file mode 100644 index 00000000000..12a8e9e10e6 --- /dev/null +++ b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellUploadManagerTest.kt @@ -0,0 +1,241 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import app.cash.turbine.test +import com.wire.kalium.cells.CellsScope +import com.wire.kalium.cells.data.CellUploadManagerImpl +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.cells.domain.model.PreCheckResult +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either +import com.wire.kalium.common.functional.getOrFail +import com.wire.kalium.common.functional.getOrNull +import com.wire.kalium.common.functional.isLeft +import com.wire.kalium.common.functional.left +import com.wire.kalium.common.functional.right +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import okio.Path +import okio.Path.Companion.toPath +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class CellUploadManagerTest { + + private companion object { + val assetPath = "path".toPath() + const val assetSize = 1000L + const val fileName = "testfile.test" + const val suggestedFileName = "testfile-1.test" + val destNodePath = "${CellsScope.ROOT_CELL}/$fileName" + val suggestedDestNodePath = "${CellsScope.ROOT_CELL}/$suggestedFileName" + } + + @Test + fun `given new file pre-check new node returned` () = runTest { + + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadSuccess() + .arrange() + + val result = uploadManager.upload(assetPath, assetSize, destNodePath).getOrNull() + + assertEquals(destNodePath, result?.path) + } + + @Test + fun `given existing file pre-check new node with suggested name returned` () = runTest { + + val (_, uploadManager) = Arrangement() + .withPreCheckFileExists(suggestedDestNodePath) + .withUploadSuccess() + .arrange() + + val result = uploadManager.upload(assetPath, assetSize, destNodePath).getOrNull() + + assertEquals(suggestedDestNodePath, result?.path) + } + + @Test + fun `given pre-check failure error is returned` () = runTest { + + val (_, uploadManager) = Arrangement() + .withPreCheckFailed() + .withUploadSuccess() + .arrange() + + val result = uploadManager.upload(assetPath, assetSize, destNodePath) + + assertTrue { result.isLeft() } + } + + @Test + fun `given success pre-check file upload is started` () = runTest { + val (arrangement, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadSuccess() + .arrange() + + uploadManager.upload(assetPath, assetSize, destNodePath) + + advanceTimeBy(1) + + coVerify { + arrangement.repository.uploadFile(any(), any(), any()) + }.wasInvoked(once) + } + + @Test + fun `given success upload upload complete event is emitted` () = runTest { + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadSuccess() + .arrange() + + val node = uploadManager.upload(assetPath, assetSize, destNodePath).getOrFail { error("") } + + uploadManager.observeUpload(node.uuid)?.test { + val uploadEvent = awaitItem() + assertEquals(CellUploadEvent.UploadCompleted, uploadEvent) + } + } + + @Test + fun `given success upload upload job is removed from upload manager` () = runTest { + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadSuccess() + .arrange() + + val node = uploadManager.upload(assetPath, assetSize, destNodePath).getOrFail { error("") } + + advanceTimeBy(1) + + assertFalse(uploadManager.isUploading(node.uuid)) + } + + @Test + fun `given failed upload upload error event is emitted` () = runTest { + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadFailed() + .arrange() + + val node = uploadManager.upload(assetPath, assetSize, destNodePath).getOrFail { error("") } + + uploadManager.observeUpload(node.uuid)?.test { + val uploadEvent = awaitItem() + assertEquals(CellUploadEvent.UploadError, uploadEvent) + } + } + + @Test + fun `given failed upload upload job error flag is set` () = runTest { + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadFailed() + .arrange() + + val node = uploadManager.upload(assetPath, assetSize, destNodePath).getOrFail { error("") } + + advanceTimeBy(1) + + assertTrue(uploadManager.getUploadInfo(node.uuid)?.uploadFailed == true) + } + + @Test + fun `given upload progress is updated then progress event is emitted` () = runTest { + val (_, uploadManager) = Arrangement() + .withPreCheckSuccess() + .withUploadSuccess() + .arrange(TestRepository()) + + val node = uploadManager.upload(assetPath, assetSize, destNodePath).getOrFail { error("") } + + uploadManager.observeUpload(node.uuid)?.test { + val uploadEvent = awaitItem() + assertEquals(CellUploadEvent.UploadProgress(0.5f), uploadEvent) + } + } + + private class Arrangement(val uploadScope: CoroutineScope) { + + @Mock + val repository = mock(CellsRepository::class) + + suspend fun withPreCheckFileExists(suggestedName: String) = apply { + coEvery { repository.preCheck(any()) }.returns(PreCheckResult.FileExists(suggestedName).right()) + } + + suspend fun withPreCheckSuccess() = apply { + coEvery { repository.preCheck(any()) }.returns(PreCheckResult.Success.right()) + } + + suspend fun withPreCheckFailed() = apply { + coEvery { repository.preCheck(any()) }.returns(NetworkFailure.NoNetworkConnection(IllegalStateException("test")).left()) + } + + suspend fun withUploadSuccess() = apply { + coEvery { repository.uploadFile(any(), any(), any()) }.returns(Unit.right()) + } + + suspend fun withUploadFailed() = apply { + coEvery { repository.uploadFile(any(), any(), any()) }.returns( + NetworkFailure.NoNetworkConnection(IllegalStateException("test")).left() + ) + } + + fun arrange(cellRepo: CellsRepository = repository) = this to CellUploadManagerImpl( + repository = cellRepo, + uploadScope = uploadScope + ) + } + + private fun TestScope.Arrangement() = Arrangement(this.backgroundScope) +} + +private class TestRepository : CellsRepository { + + override suspend fun uploadFile(path: Path, node: CellNode, onProgressUpdate: (Long) -> Unit): Either { + onProgressUpdate(500) + delay(100) + return Unit.right() + } + + override suspend fun preCheck(nodePath: String) = PreCheckResult.Success.right() + override suspend fun getFiles(cellName: String) = emptyList().right() + override suspend fun deleteFile(node: CellNode) = Unit.right() + override suspend fun cancelDraft(nodeUuid: String, versionUuid: String) = Unit.right() + override suspend fun publishDraft(nodeUuid: String) = Unit.right() + override suspend fun getPublicUrl(nodeUuid: String, fileName: String) = "".right() +} diff --git a/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellsApiTest.kt b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellsApiTest.kt new file mode 100644 index 00000000000..d5389c9b037 --- /dev/null +++ b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/CellsApiTest.kt @@ -0,0 +1,111 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain + +import com.wire.kalium.cells.data.CellsApiImpl +import com.wire.kalium.cells.sdk.kmp.api.NodeServiceApi +import com.wire.kalium.cells.sdk.kmp.model.RestCheckResult +import com.wire.kalium.cells.sdk.kmp.model.RestCreateCheckResponse +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.HttpRequestData +import io.ktor.http.HeadersImpl +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.core.toByteArray +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class CellsApiTest { + + @Test + fun `given new file pre-check then success is returned`() = runTest { + + val (_, cellApi) = Arrangement() + .withHttpClient( + Json.encodeToString( + RestCreateCheckResponse( + results = listOf(RestCheckResult(exists = false)) + ) + ).toByteArray() + ) + .arrange() + + val result = cellApi.preCheck("path") + + // TODO: Fix issue with response deserialization +// assertEquals(PreCheckResultDTO(fileExists = false), result) + } + + @Test + fun `given existing file pre-check then existing file is returned`() = runTest { + + } + + @Test + fun `given pre-check failure then failure is returned`() = runTest { + + } + + private class Arrangement() { + + var httpClient: HttpClient = HttpClient(createMockEngine( + ByteArray(0), + HttpStatusCode.OK + )) + + val nodeService = NodeServiceApi("", httpClient) + + fun withHttpClient(body: ByteArray, statusCode: HttpStatusCode = HttpStatusCode.OK) = apply { + httpClient = HttpClient(createMockEngine(body, statusCode)) { + install(ContentNegotiation) { + json() + } + } + } + + fun arrange() = this to CellsApiImpl(nodeService) + } +} + +private fun createMockEngine( + responseBody: ByteArray, + statusCode: HttpStatusCode, + assertion: (HttpRequestData.() -> Unit) = {}, + headers: Map? = null +): MockEngine { + val newHeaders: Map> = (headers?.let { + headers.mapValues { listOf(it.value) } + } ?: run { + mapOf(HttpHeaders.ContentType to "application/json").mapValues { listOf(it.value) } + }) + + return MockEngine { request -> + request.assertion() + respond( + content = responseBody, + status = statusCode, + headers = HeadersImpl(newHeaders) + ) + } +} diff --git a/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCaseTest.kt b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCaseTest.kt new file mode 100644 index 00000000000..6466a041b10 --- /dev/null +++ b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/AddAttachmentDraftUseCaseTest.kt @@ -0,0 +1,221 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.CellsScope +import com.wire.kalium.cells.domain.CellUploadEvent +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.cells.domain.model.CellNode +import com.wire.kalium.common.functional.right +import com.wire.kalium.logic.data.id.ConversationId +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import okio.Path.Companion.toPath +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AddAttachmentDraftUseCaseTest { + + private companion object { + val conversationId = ConversationId("1", "test") + val assetPath = "path".toPath() + const val assetSize = 1000L + const val fileName = "testfile.test" + val destNodePath = "${CellsScope.ROOT_CELL}/$conversationId/$fileName" + } + + @Test + fun `given valid request upload manager is called`() = runTest { + val (arrangement, useCase) = Arrangement() + .withSuccessAdd() + .withSuccessPreCheck() + .arrange() + + useCase(conversationId, fileName, assetPath, assetSize) + + coVerify { + arrangement.uploadManager.upload( + assetPath = assetPath, + assetSize = assetSize, + destNodePath = destNodePath, + ) + }.wasInvoked(once) + } + + @Test + fun `given success pre-check attachment is persisted`() = runTest { + val (arrangement, useCase) = Arrangement() + .withSuccessAdd() + .withSuccessPreCheck() + .arrange() + + useCase(conversationId, fileName, assetPath, assetSize) + + coVerify { + arrangement.repository.add( + conversationId = conversationId, + node = testNode, + dataPath = assetPath.toString(), + ) + }.wasInvoked(once) + } + + @Test + fun `given success attachment persist upload events observer is started`() = runTest { + val (arrangement, useCase) = Arrangement(this.backgroundScope) + .withSuccessAdd() + .withSuccessPreCheck() + .withUploadEvents() + .arrange() + + useCase(conversationId, fileName, assetPath, assetSize) + + advanceTimeBy(1) + + verify { + arrangement.uploadManager.observeUpload(any()) + }.wasInvoked(once) + } + + @Test + fun `given upload complete event upload status is updated`() = runTest { + val (arrangement, useCase) = Arrangement(this.backgroundScope) + .withSuccessAdd() + .withSuccessPreCheck() + .withSuccessUpdate() + .withUploadCompleteEvent() + .arrange() + + useCase(conversationId, fileName, assetPath, assetSize) + + advanceTimeBy(1) + + coVerify { + arrangement.repository.updateStatus(testNode.uuid, AttachmentUploadStatus.UPLOADED) + }.wasInvoked(once) + } + + @Test + fun `given upload error event upload status is updated`() = runTest { + val (arrangement, useCase) = Arrangement(this.backgroundScope) + .withSuccessAdd() + .withSuccessPreCheck() + .withSuccessUpdate() + .withUploadErrorEvent() + .arrange() + + useCase(conversationId, fileName, assetPath, assetSize) + + advanceTimeBy(1) + + coVerify { + arrangement.repository.updateStatus(testNode.uuid, AttachmentUploadStatus.FAILED) + }.wasInvoked(once) + } + + private class Arrangement(val useCaseScope: CoroutineScope = TestScope()) { + + @Mock + val uploadManager = mock(CellUploadManager::class) + + @Mock + val repository = mock(MessageAttachmentDraftRepository::class) + + val uploadEventsFlow = MutableSharedFlow() + + suspend fun withSuccessPreCheck() = apply { + coEvery { + uploadManager.upload( + assetPath = any(), + assetSize = any(), + destNodePath = any() + ) + }.returns(testNode.right()) + } + + suspend fun withSuccessAdd() = apply { + coEvery { + repository.add( + conversationId = any(), + node = any(), + dataPath = any() + ) + }.returns(Unit.right()) + } + + suspend fun withSuccessUpdate() = apply { + coEvery { + repository.updateStatus( + uuid = any(), + status = any() + ) + }.returns(Unit.right()) + } + + fun withUploadEvents() = apply { + every { + uploadManager.observeUpload(any()) + }.returns(uploadEventsFlow) + } + + fun withUploadCompleteEvent() = apply { + every { + uploadManager.observeUpload(any()) + }.returns(flowOf(CellUploadEvent.UploadCompleted)) + } + + fun withUploadErrorEvent() = apply { + every { + uploadManager.observeUpload(any()) + }.returns(flowOf(CellUploadEvent.UploadError)) + } + + fun arrange() = this to AddAttachmentDraftUseCaseImpl( + uploadManager = uploadManager, + repository = repository, + scope = useCaseScope, + ) + } +} + +private val testNode = CellNode( + uuid = "uuid", + versionId = "versionId", + path = "path", + modified = 0, + size = 0, + eTag = "eTag", + type = "type", + isRecycleBin = false, + isDraft = false +) diff --git a/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCaseTest.kt b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCaseTest.kt new file mode 100644 index 00000000000..08e65d5338b --- /dev/null +++ b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/ObserveAttachmentDraftsUseCaseTest.kt @@ -0,0 +1,84 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.common.functional.right +import com.wire.kalium.logic.data.id.ConversationId +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class ObserveAttachmentDraftsUseCaseTest { + + @Test + fun `test stale uploads are removed`() = runTest { + + val (arrangement, useCase) = Arrangement() + .withUploadManager() + .withRepository() + .arrange() + + useCase.invoke(ConversationId("1", "test")) + + coVerify { + arrangement.repository.remove("1") + }.wasInvoked(once) + } + + private class Arrangement { + @Mock + val repository = mock(MessageAttachmentDraftRepository::class) + + @Mock + val uploadManager = mock(CellUploadManager::class) + + fun withUploadManager() = apply { + every { uploadManager.isUploading(any()) }.returns(false) + } + + suspend fun withRepository() = apply { + coEvery { repository.getAll(any()) }.returns(listOf( + AttachmentDraft( + uuid = "1", + versionId = "1", + uploadStatus = AttachmentUploadStatus.UPLOADING, + fileName = "", + localFilePath = "", + fileSize = 1, + ) + ).right()) + + coEvery { repository.remove(any()) }.returns(Unit.right()) + + coEvery { repository.observe(any()) }.returns(emptyFlow()) + } + + fun arrange() = this to ObserveAttachmentDraftsUseCaseImpl(repository, uploadManager) + } +} diff --git a/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCaseTest.kt b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCaseTest.kt new file mode 100644 index 00000000000..78be3eda850 --- /dev/null +++ b/cells/src/commonTest/kotlin/com/wire/kalium/cells/domain/usecase/RemoveAttachmentDraftUseCaseTest.kt @@ -0,0 +1,183 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cells.domain.usecase + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.CellsRepository +import com.wire.kalium.cells.domain.MessageAttachmentDraftRepository +import com.wire.kalium.cells.domain.model.AttachmentDraft +import com.wire.kalium.cells.domain.model.AttachmentUploadStatus +import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.common.functional.left +import com.wire.kalium.common.functional.right +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class RemoveAttachmentDraftUseCaseTest { + + @Test + fun `given attachment with UPLOADING status when removing then upload is cancelled`() = runTest { + val uuid = uuid4().toString() + val (arrangement, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withAttachment(uuid, AttachmentUploadStatus.UPLOADING) + .arrange() + + removeAttachment(uuid) + + coVerify { + arrangement.uploadManager.cancelUpload(uuid) + }.wasInvoked(once) + } + + @Test + fun `given attachment with UPLOADING status when removing then attachment is removed`() = runTest { + val uuid = uuid4().toString() + val (arrangement, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withAttachment(uuid, AttachmentUploadStatus.UPLOADING) + .arrange() + + removeAttachment(uuid) + + coVerify { + arrangement.attachmentsRepository.remove(uuid) + }.wasInvoked(once) + } + + @Test + fun `given attachment with UPLOADED status when removing then attachment draft is cancelled`() = runTest { + val uuid = uuid4().toString() + val (arrangement, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withAttachment(uuid, AttachmentUploadStatus.UPLOADED) + .arrange() + + removeAttachment(uuid) + + coVerify { + arrangement.cellsRepository.cancelDraft(uuid, "") + }.wasInvoked(once) + } + + @Test + fun `given attachment with UPLOADED status when removing then attachment is removed`() = runTest { + val uuid = uuid4().toString() + val (arrangement, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withAttachment(uuid, AttachmentUploadStatus.UPLOADED) + .arrange() + + removeAttachment(uuid) + + coVerify { + arrangement.attachmentsRepository.remove(uuid) + }.wasInvoked(once) + } + + @Test + fun `given attachment with FAILED status when removing then attachment is removed`() = runTest { + val uuid = uuid4().toString() + val (arrangement, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withAttachment(uuid, AttachmentUploadStatus.FAILED) + .arrange() + + removeAttachment(uuid) + + coVerify { + arrangement.attachmentsRepository.remove(uuid) + }.wasInvoked(once) + } + + @Test + fun `given attachment not found when removing then error is returned`() = runTest { + val uuid = uuid4().toString() + val (_, removeAttachment) = Arrangement() + .withRepository() + .withUploadManager() + .withCellsRepository() + .withNoAttachment() + .arrange() + + val result = removeAttachment(uuid) + + assertEquals(StorageFailure.DataNotFound.left(), result) + } + + private class Arrangement { + + @Mock + val attachmentsRepository = mock(MessageAttachmentDraftRepository::class) + + @Mock + val cellsRepository = mock(CellsRepository::class) + + @Mock + val uploadManager = mock(CellUploadManager::class) + + suspend fun withRepository() = apply { + coEvery { attachmentsRepository.remove(any()) }.returns(Unit.right()) + } + + suspend fun withCellsRepository() = apply { + coEvery { cellsRepository.cancelDraft(any(), any()) }.returns(Unit.right()) + } + + suspend fun withUploadManager() = apply { + coEvery { uploadManager.cancelUpload(any()) }.returns(Unit) + } + + suspend fun withAttachment(uuid: String, status: AttachmentUploadStatus) = apply { + coEvery { attachmentsRepository.get(any()) }.returns( + AttachmentDraft( + uuid = uuid, + versionId = "", + fileName = "", + localFilePath = "", + fileSize = 1, + uploadStatus = status + ).right() + ) + } + + suspend fun withNoAttachment() = apply { + coEvery { attachmentsRepository.get(any()) }.returns(null.right()) + } + + fun arrange() = this to RemoveAttachmentDraftUseCaseImpl(uploadManager, attachmentsRepository, cellsRepository) + } +}