diff --git a/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/File.kt b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/File.kt
new file mode 100644
index 00000000..ea90b483
--- /dev/null
+++ b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/File.kt
@@ -0,0 +1,31 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2
+
+interface File {
+ val id: String
+ val path: String
+ val size: Long
+ val mimeType: String? get() = null
+
+ // Local
+ val thumbnailPath: String? get() = null
+ val isFolder: Boolean get() = false
+ // utils
+ val name get() = path.substringAfterLast("/")
+}
diff --git a/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/Transfer.kt b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/Transfer.kt
new file mode 100644
index 00000000..67195b13
--- /dev/null
+++ b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/interfaces/transfers/v2/Transfer.kt
@@ -0,0 +1,39 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2
+
+import com.infomaniak.multiplatform_swisstransfer.common.models.TransferDirection
+import com.infomaniak.multiplatform_swisstransfer.common.models.TransferStatus
+
+interface Transfer {
+ val id: String
+ val senderEmail: String
+ val title: String?
+ val message: String?
+ val createdAt: Long
+ val expiresAt: Long
+ val files: List get() = emptyList()
+ val totalSize: Long
+
+ //region Only local
+ val password: String? get() = null
+ val transferDirection: TransferDirection? get() = null
+ val transferStatus: TransferStatus? get() = null
+ val recipientsEmails: Set get() = emptySet()
+ //endregion
+}
diff --git a/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/utils/ApiEnvironment.kt b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/utils/ApiEnvironment.kt
index 56f69e2a..906b5bfb 100644
--- a/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/utils/ApiEnvironment.kt
+++ b/STCommon/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/common/utils/ApiEnvironment.kt
@@ -17,9 +17,13 @@
*/
package com.infomaniak.multiplatform_swisstransfer.common.utils
-sealed class ApiEnvironment(val baseUrl: String) {
+sealed class ApiEnvironment(val baseUrl: String, val baseUrlV2: String) {
// Those urls are duplicated with the ones we have in Android so don't forget to change them also in Android
- data object Preprod : ApiEnvironment("https://swisstransfer-legacy.preprod.dev.infomaniak.ch")
- data object Prod : ApiEnvironment("https://www.swisstransfer.com")
- data class Custom(private val url: String) : ApiEnvironment(url)
+ data object Preprod : ApiEnvironment(
+ baseUrl = "https://swisstransfer-legacy.preprod.dev.infomaniak.ch",
+ baseUrlV2 = "https://swisstransfer.preprod.dev.infomaniak.ch"
+ )
+
+ data object Prod : ApiEnvironment("https://www.swisstransfer.com", "https://swisstransfer.infomaniak.com")
+ data class Custom(private val url: String, private val urlV2: String) : ApiEnvironment(url, urlV2)
}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProvider.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProvider.kt
index e600b9d2..956051ba 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProvider.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProvider.kt
@@ -22,9 +22,11 @@ import com.infomaniak.multiplatform_swisstransfer.common.interfaces.CrashReportI
import com.infomaniak.multiplatform_swisstransfer.common.interfaces.CrashReportLevel
import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException
import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.ApiErrorException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.ApiV2ErrorException
import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.UnexpectedApiErrorFormatException
import com.infomaniak.multiplatform_swisstransfer.network.exceptions.NetworkException
import com.infomaniak.multiplatform_swisstransfer.network.models.ApiError
+import com.infomaniak.multiplatform_swisstransfer.network.models.ApiResponseForError
import com.infomaniak.multiplatform_swisstransfer.network.utils.getRequestContextId
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
@@ -40,6 +42,7 @@ import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.contentLength
import io.ktor.serialization.kotlinx.json.json
+import io.ktor.util.network.UnresolvedAddressException
import io.ktor.utils.io.CancellationException
import kotlinx.io.IOException
import kotlinx.serialization.json.Json
@@ -88,7 +91,7 @@ class ApiClientProvider internal constructor(
}
install(HttpRequestRetry) {
retryOnExceptionIf(maxRetries = MAX_RETRY) { _, cause ->
- cause.isNetworkException()
+ cause.isRetryableNetworkException()
}
delayMillis { retry ->
retry * 500L
@@ -103,17 +106,24 @@ class ApiClientProvider internal constructor(
if (statusCode >= 300) {
val bodyResponse = response.bodyAsText()
- val apiError = runCatching {
- json.decodeFromString(bodyResponse)
- }.getOrElse {
- throw UnexpectedApiErrorFormatException(statusCode, bodyResponse, null, requestContextId)
+ runCatching {
+ if (bodyResponse.isFromApiV2()) {
+ val error = json.decodeFromString(bodyResponse).error
+ throw ApiV2ErrorException(error.code, error.description, requestContextId)
+ } else {
+ val error = json.decodeFromString(bodyResponse)
+ throw ApiErrorException(error.errorCode, error.message, requestContextId)
+ }
+ }.getOrElse { exception ->
+ if (exception is ApiException) throw exception
+ throw UnexpectedApiErrorFormatException(statusCode, bodyResponse, exception, requestContextId)
}
- throw ApiErrorException(apiError.errorCode, apiError.message, requestContextId)
}
}
handleResponseExceptionWithRequest { cause, request ->
when (cause) {
- is IOException -> throw NetworkException("Network error: ${cause.message}")
+ is UnresolvedAddressException,
+ is IOException -> throw NetworkException("Network error: ${cause.message}", cause)
is ApiException, is CancellationException -> throw cause
else -> {
val response = runCatching { request.call.response }.getOrNull()
@@ -151,9 +161,12 @@ class ApiClientProvider internal constructor(
)
}
- private fun Throwable.isNetworkException() = this is IOException
+ private fun Throwable.isRetryableNetworkException() = this is IOException
companion object {
private const val MAX_RETRY = 3
+
+ //TODO[API-V2]: Delete once the v1 api has been removed
+ private fun String.isFromApiV2() = contains("\"result\": \"error\"")
}
}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/ApiException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/ApiException.kt
index 5203bfb4..87f4b83c 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/ApiException.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/ApiException.kt
@@ -49,6 +49,22 @@ sealed class ApiException(
requestContextId: String,
) : ApiException(errorMessage, null, requestContextId)
+ /**
+ * Thrown when an API(v2) call fails due to an error identified by a specific error code.
+ *
+ * This exception is used to represent errors returned by an API, with an associated error code
+ * and message describing the problem.
+ *
+ * @property code The specific error code returned by the API.
+ * @property errorMessage The detailed error message explaining the cause of the failure.
+ * @param requestContextId The request context id send by the backend to track the call
+ */
+ open class ApiV2ErrorException(
+ val code: String,
+ val description: String,
+ requestContextId: String,
+ ) : ApiException(description, null, requestContextId)
+
/**
* Thrown when an API call returns an error in an unexpected format that cannot be parsed.
*
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/FetchTransferException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/FetchTransferException.kt
index 82c4a395..5f37824a 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/FetchTransferException.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/FetchTransferException.kt
@@ -89,6 +89,11 @@ sealed class FetchTransferException(
class WrongPasswordFetchTransferException(requestContextId: String) :
FetchTransferException(401, "Wrong password for this Transfer", requestContextId)
+ class TransferCancelledException(requestContextId: String) :
+ FetchTransferException(409, "Transfer cancelled", requestContextId)
+
+ class DownloadLimitReached(requestContextId: String) : FetchTransferException(429, "Download limit reached", requestContextId)
+
companion object {
private const val ERROR_VIRUS_CHECK = "wait_virus_check"
@@ -129,6 +134,26 @@ sealed class FetchTransferException(
errorCode == 404 -> NotFoundFetchTransferException(requestContextId)
else -> this
}
+
+ /**
+ * Extension function to convert an instance of [ApiException.ApiV2ErrorException] to a specific [FetchTransferException]
+ * based on its error code.
+ *
+ * Useful to translate some correctly formatted api error exceptions as our custom type of errors we handle everywhere.
+ *
+ * @receiver An instance of [ApiException.ApiV2ErrorException].
+ * @return An instance of [FetchTransferException] or the original [ApiException.ApiV2ErrorException] if we cannot map it
+ * to a [FetchTransferException].
+ */
+ internal fun ApiV2ErrorException.toFetchTransferException() = when (code) {
+ "access_denied" -> PasswordNeededFetchTransferException(requestContextId)
+ "invalid_password" -> WrongPasswordFetchTransferException(requestContextId)
+ "object_not_found" -> NotFoundFetchTransferException(requestContextId)
+ "transfer_cancelled" -> TransferCancelledException(requestContextId)
+ "transfer_expired" -> ExpiredDateFetchTransferException(requestContextId)
+ "download_limit_reached" -> DownloadLimitReached(requestContextId)
+ else -> this
+ }
}
}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/NetworkException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/NetworkException.kt
index a624c4c7..01d5ba1c 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/NetworkException.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/NetworkException.kt
@@ -22,4 +22,4 @@ package com.infomaniak.multiplatform_swisstransfer.network.exceptions
*
* @param message A detailed message describing the network error.
*/
-class NetworkException(message: String) : Exception(message)
+class NetworkException(message: String, cause: Throwable?) : Exception(message, cause)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/TooManyRequestException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/TooManyRequestException.kt
new file mode 100644
index 00000000..0b6564f6
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/TooManyRequestException.kt
@@ -0,0 +1,24 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.exceptions
+
+class TooManyRequestException(requestContextId: String) : ApiException(
+ errorMessage = "",
+ cause = null,
+ requestContextId = requestContextId
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UnauthorizedException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UnauthorizedException.kt
new file mode 100644
index 00000000..a209b36e
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UnauthorizedException.kt
@@ -0,0 +1,30 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.exceptions
+
+/**
+ * Exception thrown when an API request fails due to missing or invalid Bearer token authorization.
+ *
+ * This exception indicates that the request requires a valid Bearer token for authentication,
+ * but either no token was provided or the token was invalid/expired.
+ */
+class UnauthorizedException(requestContextId: String) : ApiException(
+ errorMessage = "",
+ cause = null,
+ requestContextId = requestContextId
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UploadErrorsException.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UploadErrorsException.kt
new file mode 100644
index 00000000..e992b3dc
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/exceptions/UploadErrorsException.kt
@@ -0,0 +1,38 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.exceptions
+
+sealed class UploadErrorsException(requestContextId: String) : ApiException.ApiV2ErrorException(
+ code = "",
+ description = "",
+ requestContextId = requestContextId
+) {
+
+ class NotFoundException(requestContextId: String) : UploadErrorsException(requestContextId = requestContextId)
+ class TransferCancelled(requestContextId: String) : UploadErrorsException(requestContextId = requestContextId)
+ class TransferExpired(requestContextId: String) : UploadErrorsException(requestContextId = requestContextId)
+ class TransferFailed(requestContextId: String) : UploadErrorsException(requestContextId = requestContextId)
+}
+
+internal fun ApiException.ApiV2ErrorException.toUploadErrorsException() = when (code) {
+ "object_not_found" -> UploadErrorsException.NotFoundException(requestContextId)
+ "transfer_cancelled" -> UploadErrorsException.TransferCancelled(requestContextId)
+ "transfer_expired" -> UploadErrorsException.TransferExpired(requestContextId)
+ "transfer_failed" -> UploadErrorsException.TransferFailed(requestContextId)
+ else -> this
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiErrorV2.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiErrorV2.kt
new file mode 100644
index 00000000..f9bf106d
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiErrorV2.kt
@@ -0,0 +1,26 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ApiErrorV2(
+ val code: String,
+ val description: String,
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseForError.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseForError.kt
new file mode 100644
index 00000000..56aa51a3
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseForError.kt
@@ -0,0 +1,26 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ApiResponseForError(
+ val responseStatus: ApiResponseStatus = ApiResponseStatus.ERROR,
+ val error: ApiErrorV2
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseV2Success.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseV2Success.kt
new file mode 100644
index 00000000..7463acc3
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/ApiResponseV2Success.kt
@@ -0,0 +1,36 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ApiResponseV2Success(
+ val result: Status,
+ val data: T,
+) {
+ @Serializable
+ enum class Status {
+ @SerialName("success")
+ SUCCESS,
+ @SerialName("asynchronous")
+ ASYNCHRONOUS,
+ ;
+ }
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/FileApi.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/FileApi.kt
new file mode 100644
index 00000000..a655db76
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/FileApi.kt
@@ -0,0 +1,31 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.transfer.v2
+
+import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.File
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FileApi(
+ override val id: String,
+ override val path: String,
+ override val size: Long,
+ @SerialName("mime_type")
+ override val mimeType: String? = null,
+) : File
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/TransferApi.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/TransferApi.kt
new file mode 100644
index 00000000..5c6224be
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/transfer/v2/TransferApi.kt
@@ -0,0 +1,38 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.transfer.v2
+
+import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.Transfer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TransferApi(
+ override val id: String,
+ @SerialName("sender_email")
+ override val senderEmail: String,
+ override val title: String? = null,
+ override val message: String? = null,
+ @SerialName("created_at")
+ override val createdAt: Long,
+ @SerialName("expires_at")
+ override val expiresAt: Long,
+ override val files: List,
+ @SerialName("total_size")
+ override val totalSize: Long,
+) : Transfer
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/ChunkEtag.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/ChunkEtag.kt
new file mode 100644
index 00000000..246aa2ac
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/ChunkEtag.kt
@@ -0,0 +1,28 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ChunkEtag(
+ val etag: String,
+ @SerialName("chunk_index")
+ val chunkIndex: Int, // Start with index 1
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/CreateTransfer.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/CreateTransfer.kt
new file mode 100644
index 00000000..1c746842
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/CreateTransfer.kt
@@ -0,0 +1,36 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CreateTransfer(
+ val title: String? = null,
+ val message: String? = null,
+ val password: String? = null,
+ val language: String,
+ @SerialName("expires_in_days")
+ val expiresInDays: Int,
+ @SerialName("max_download")
+ val maxDownload: Int,
+ val files: List,
+ val recipients: List
+)
+
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/TransferFile.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/TransferFile.kt
new file mode 100644
index 00000000..e2636c53
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/TransferFile.kt
@@ -0,0 +1,29 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TransferFile(
+ val path: String,
+ val size: Long,
+ @SerialName("mime_type")
+ val mimeType: String = "" // Empty otherwise
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/UploadTransferStatus.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/UploadTransferStatus.kt
new file mode 100644
index 00000000..1e26b9d1
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/request/v2/UploadTransferStatus.kt
@@ -0,0 +1,24 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2
+
+internal enum class UploadTransferStatus(val apiValue: String) {
+ Completed("completed"),
+ Cancelled("cancelled"),
+ Failed("failed"),
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/response/v2/PresignedUrlResponse.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/response/v2/PresignedUrlResponse.kt
new file mode 100644
index 00000000..7bf4cf74
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/models/upload/response/v2/PresignedUrlResponse.kt
@@ -0,0 +1,25 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2026 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.models.upload.response.v2
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PresignedUrlResponse(
+ val url: String
+)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/TransferV2Repository.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/TransferV2Repository.kt
new file mode 100644
index 00000000..7ef3d6e0
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/TransferV2Repository.kt
@@ -0,0 +1,156 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.repositories
+
+import com.infomaniak.multiplatform_swisstransfer.common.exceptions.UnknownException
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+import com.infomaniak.multiplatform_swisstransfer.network.ApiClientProvider
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.ApiV2ErrorException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.UnexpectedApiErrorFormatException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.Companion.toFetchTransferException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.DownloadLimitReached
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.ExpiredDateFetchTransferException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.NotFoundFetchTransferException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.PasswordNeededFetchTransferException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.TransferCancelledException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.FetchTransferException.WrongPasswordFetchTransferException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.NetworkException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UnauthorizedException
+import com.infomaniak.multiplatform_swisstransfer.network.models.ApiResponseV2Success
+import com.infomaniak.multiplatform_swisstransfer.network.models.transfer.v2.TransferApi
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.response.v2.PresignedUrlResponse
+import com.infomaniak.multiplatform_swisstransfer.network.requests.v2.TransferRequest
+import io.ktor.client.HttpClient
+import kotlinx.serialization.json.Json
+import kotlin.coroutines.cancellation.CancellationException
+
+class TransferV2Repository internal constructor(private val transferRequest: TransferRequest) {
+
+ // for Obj-C https://youtrack.jetbrains.com/issue/KT-68288/KMP-Support-Kotlin-default-parameters-into-Swift-default-parameters-and-Objective-C-somehow-possibly-a-JvmOverloads-like
+ constructor(environment: ApiEnvironment, token: () -> String) : this(ApiClientProvider(), environment, token)
+ constructor(
+ apiClientProvider: ApiClientProvider = ApiClientProvider(),
+ environment: ApiEnvironment,
+ token: () -> String,
+ ) : this(
+ environment = environment,
+ json = apiClientProvider.json,
+ httpClient = apiClientProvider.httpClient,
+ token = token,
+ )
+
+ internal constructor(
+ environment: ApiEnvironment,
+ json: Json,
+ httpClient: HttpClient,
+ token: () -> String,
+ ) : this(TransferRequest(environment, json, httpClient, token))
+
+ @Throws(
+ CancellationException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ NetworkException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ PasswordNeededFetchTransferException::class,
+ WrongPasswordFetchTransferException::class,
+ NotFoundFetchTransferException::class,
+ TransferCancelledException::class,
+ ExpiredDateFetchTransferException::class,
+ DownloadLimitReached::class,
+ )
+ suspend fun getTransferByLinkUUID(
+ linkUUID: String,
+ password: String?
+ ): TransferApi = withTransferErrorHandling {
+ transferRequest.getTransfer(linkUUID, password).data
+ }
+
+ @Throws(
+ CancellationException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ NetworkException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ PasswordNeededFetchTransferException::class,
+ WrongPasswordFetchTransferException::class,
+ NotFoundFetchTransferException::class,
+ TransferCancelledException::class,
+ ExpiredDateFetchTransferException::class,
+ DownloadLimitReached::class,
+ )
+ suspend fun getTransferByUrl(url: String, password: String?): TransferApi {
+ return getTransferByLinkUUID(extractUUID(url), password)
+ }
+
+ @Throws(
+ CancellationException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ NetworkException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ PasswordNeededFetchTransferException::class,
+ WrongPasswordFetchTransferException::class,
+ NotFoundFetchTransferException::class,
+ TransferCancelledException::class,
+ ExpiredDateFetchTransferException::class,
+ DownloadLimitReached::class,
+ )
+ suspend fun presignedDownloadUrl(
+ linkId: String,
+ fileId: String,
+ password: String?
+ ): String = withTransferErrorHandling {
+ transferRequest.presignedDownloadUrl(linkId, fileId, password).data.url
+ }
+
+ @Throws(
+ CancellationException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ NetworkException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ PasswordNeededFetchTransferException::class,
+ WrongPasswordFetchTransferException::class,
+ NotFoundFetchTransferException::class,
+ TransferCancelledException::class,
+ ExpiredDateFetchTransferException::class,
+ DownloadLimitReached::class,
+ )
+ suspend fun delete(
+ transferId: String,
+ ): Boolean = withTransferErrorHandling {
+ transferRequest.deleteTransfer(transferId)
+ }
+
+
+ internal fun extractUUID(url: String) = url.substringAfterLast("/")
+
+ private suspend fun withTransferErrorHandling(block: suspend () -> T) = runCatching {
+ block()
+ }.getOrElse { exception ->
+ throw when (exception) {
+ is ApiV2ErrorException -> exception.toFetchTransferException()
+ else -> exception
+ }
+ }
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/UploadV2Repository.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/UploadV2Repository.kt
new file mode 100644
index 00000000..45162d60
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/repositories/UploadV2Repository.kt
@@ -0,0 +1,222 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.repositories
+
+import com.infomaniak.multiplatform_swisstransfer.common.exceptions.UnknownException
+import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.Transfer
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+import com.infomaniak.multiplatform_swisstransfer.network.ApiClientProvider
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.ApiV2ErrorException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException.UnexpectedApiErrorFormatException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.NetworkException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.TooManyRequestException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UnauthorizedException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UploadErrorsException.NotFoundException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UploadErrorsException.TransferCancelled
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UploadErrorsException.TransferExpired
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.UploadErrorsException.TransferFailed
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.toUploadErrorsException
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.ChunkEtag
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.CreateTransfer
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.UploadTransferStatus
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.response.v2.PresignedUrlResponse
+import com.infomaniak.multiplatform_swisstransfer.network.requests.v2.UploadRequest
+import io.ktor.client.HttpClient
+import io.ktor.http.content.OutgoingContent
+import kotlinx.serialization.json.Json
+import kotlin.coroutines.cancellation.CancellationException
+
+class UploadV2Repository internal constructor(private val uploadRequest: UploadRequest) {
+
+ // for Obj-C https://youtrack.jetbrains.com/issue/KT-68288/KMP-Support-Kotlin-default-parameters-into-Swift-default-parameters-and-Objective-C-somehow-possibly-a-JvmOverloads-like
+ constructor(environment: ApiEnvironment, token: () -> String) : this(ApiClientProvider(), environment, token)
+ constructor(
+ apiClientProvider: ApiClientProvider = ApiClientProvider(),
+ environment: ApiEnvironment,
+ token: () -> String,
+ ) : this(
+ environment = environment,
+ json = apiClientProvider.json,
+ httpClient = apiClientProvider.httpClient,
+ token = token,
+ )
+
+ internal constructor(
+ environment: ApiEnvironment,
+ json: Json,
+ httpClient: HttpClient,
+ token: () -> String,
+ ) : this(UploadRequest(environment, json, httpClient, token))
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ TooManyRequestException::class,
+ )
+ suspend fun createTransfer(createTransfer: CreateTransfer): Transfer {
+ return withUploadErrorHandling {
+ uploadRequest.createTransfer(createTransfer).data
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ NotFoundException::class,
+ )
+ suspend fun getPresignedUploadUrl(transferId: String, fileId: String): PresignedUrlResponse {
+ return withUploadErrorHandling {
+ uploadRequest.getPresignedUploadUrl(transferId, fileId).data
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ NotFoundException::class,
+ )
+ suspend fun getPresignedUploadChunkUrl(
+ transferId: String,
+ fileId: String,
+ chunkIndex: Int,
+ ): PresignedUrlResponse {
+ return withUploadErrorHandling {
+ uploadRequest.getPresignedUploadChunkUrl(transferId, fileId, chunkIndex).data
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ NotFoundException::class,
+ TransferCancelled::class,
+ )
+ suspend fun finalizeTransferFile(transferId: String, fileId: String, etags: List): Boolean {
+ return withUploadErrorHandling {
+ uploadRequest.finalizeTransferFile(transferId, fileId, etags)
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ TooManyRequestException::class,
+ NotFoundException::class,
+ TransferExpired::class,
+ TransferCancelled::class,
+ )
+ suspend fun finalizeTransfer(transferId: String): Boolean {
+ return withUploadErrorHandling {
+ uploadRequest.updateTransferStatus(transferId, UploadTransferStatus.Completed)
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ TooManyRequestException::class,
+ NotFoundException::class,
+ TransferExpired::class,
+ TransferCancelled::class,
+ TransferFailed::class,
+ )
+ suspend fun cancelTransfer(transferId: String): Boolean {
+ return withUploadErrorHandling {
+ uploadRequest.updateTransferStatus(transferId, UploadTransferStatus.Cancelled)
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ ApiV2ErrorException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ UnauthorizedException::class,
+ TooManyRequestException::class,
+ NotFoundException::class,
+ TransferExpired::class,
+ TransferCancelled::class,
+ TransferFailed::class,
+ )
+ suspend fun markTransferAsFailed(transferId: String): Boolean {
+ return withUploadErrorHandling {
+ uploadRequest.updateTransferStatus(transferId, UploadTransferStatus.Failed)
+ }
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ )
+ suspend fun uploadFile(
+ cephUrl: String,
+ data: OutgoingContent.WriteChannelContent,
+ onUpload: suspend (bytesSentTotal: Long, chunkSize: Long) -> Unit,
+ ) = withUploadErrorHandling {
+ uploadRequest.uploadFile(cephUrl, data, onUpload)
+ }
+
+ @Throws(
+ CancellationException::class,
+ NetworkException::class,
+ UnexpectedApiErrorFormatException::class,
+ UnknownException::class,
+ )
+ suspend fun uploadChunkToEtag(
+ cephUrl: String,
+ data: OutgoingContent.WriteChannelContent,
+ onUpload: suspend (bytesSentTotal: Long, chunkSize: Long) -> Unit,
+ ): String? = withUploadErrorHandling {
+ uploadRequest.uploadChunk(cephUrl, data, onUpload)
+ }
+
+ private suspend fun withUploadErrorHandling(block: suspend () -> T) = runCatching {
+ block()
+ }.getOrElse { exception ->
+ throw when (exception) {
+ is ApiV2ErrorException if exception.code == "not_authorized" -> UnauthorizedException(exception.requestContextId)
+ is ApiV2ErrorException if exception.code == "too_many_request" -> TooManyRequestException(exception.requestContextId)
+ is ApiV2ErrorException -> exception.toUploadErrorsException()
+ else -> exception
+ }
+ }
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/BaseRequest.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/BaseRequest.kt
index b6581584..1267e47d 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/BaseRequest.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/BaseRequest.kt
@@ -21,14 +21,17 @@ import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
import com.infomaniak.multiplatform_swisstransfer.network.utils.ApiRoutes
import com.infomaniak.multiplatform_swisstransfer.network.utils.decode
import io.ktor.client.HttpClient
+import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.headers
+import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HeadersBuilder
+import io.ktor.http.HttpHeaders
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.contentType
@@ -38,13 +41,27 @@ internal open class BaseRequest(
protected val environment: ApiEnvironment,
protected val json: Json,
protected val httpClient: HttpClient,
+ private val token: () -> String,
) {
protected fun createUrl(path: String, vararg queries: Pair): Url {
- val baseUrl = Url(ApiRoutes.apiBaseUrl(environment) + path)
- return URLBuilder(baseUrl).apply {
- queries.forEach { parameters.append(it.first, it.second) }
- }.build()
+ return createUrl(ApiRoutes.apiBaseUrl(environment), path, queries)
+ }
+
+ protected fun createV2Url(path: String, vararg queries: Pair): Url {
+ return createUrl(ApiRoutes.apiBaseUrlV2(environment), path, queries)
+ }
+
+ private fun createUrl(
+ baseUrl: String,
+ path: String,
+ queries: Array>
+ ): Url = URLBuilder(Url(baseUrl + path)).apply {
+ queries.forEach { parameters.append(it.first, it.second) }
+ }.build()
+
+ protected fun HeadersBuilder.appendBearer() {
+ append(HttpHeaders.Authorization, "Bearer ${token()}")
}
protected suspend inline fun get(
@@ -76,6 +93,13 @@ internal open class BaseRequest(
}.decode()
}
+ protected suspend inline fun patch(url: Url, data: Any?, httpClient: HttpClient = this.httpClient): R {
+ return httpClient.patch(url) {
+ contentType(ContentType.Application.Json)
+ setBody(data)
+ }.decode()
+ }
+
protected suspend inline fun delete(url: Url, httpClient: HttpClient = this.httpClient): R {
return httpClient.delete(url) {}.decode()
}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/TransferRequest.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/TransferRequest.kt
index 2a3a9b1a..57ea851f 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/TransferRequest.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/TransferRequest.kt
@@ -40,7 +40,7 @@ internal class TransferRequest(
environment: ApiEnvironment,
json: Json,
httpClient: HttpClient,
-) : BaseRequest(environment, json, httpClient) {
+) : BaseRequest(environment, json, httpClient, token = { "" }) {
@OptIn(ExperimentalEncodingApi::class)
suspend fun getTransfer(linkUUID: String, password: String? = null): ApiResponse {
@@ -48,7 +48,7 @@ internal class TransferRequest(
url = createUrl(ApiRoutes.getTransfer(linkUUID)),
appendHeaders = {
if (password?.isNotEmpty() == true) {
- append(HttpHeaders.Authorization, Base64.Default.encode(password.encodeToByteArray()))
+ append(HttpHeaders.Authorization, Base64.encode(password.encodeToByteArray()))
}
}
)
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/UploadRequest.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/UploadRequest.kt
index add5a404..d3168813 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/UploadRequest.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/UploadRequest.kt
@@ -37,7 +37,6 @@ import io.ktor.http.ContentType
import io.ktor.http.Url
import io.ktor.http.content.OutgoingContent
import io.ktor.http.contentType
-import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -45,7 +44,7 @@ internal class UploadRequest(
environment: ApiEnvironment,
json: Json,
httpClient: HttpClient,
-) : BaseRequest(environment, json, httpClient) {
+) : BaseRequest(environment, json, httpClient, token = { "" }) {
suspend fun initUpload(
initUploadBody: InitUploadBody,
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/TransferRequest.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/TransferRequest.kt
new file mode 100644
index 00000000..b9fd1d2e
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/TransferRequest.kt
@@ -0,0 +1,71 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.requests.v2
+
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+import com.infomaniak.multiplatform_swisstransfer.network.models.ApiResponseV2Success
+import com.infomaniak.multiplatform_swisstransfer.network.models.transfer.v2.TransferApi
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.response.v2.PresignedUrlResponse
+import com.infomaniak.multiplatform_swisstransfer.network.requests.BaseRequest
+import com.infomaniak.multiplatform_swisstransfer.network.utils.ApiRoutes
+import io.ktor.client.HttpClient
+import io.ktor.client.request.delete
+import io.ktor.http.HeadersBuilder
+import io.ktor.http.isSuccess
+import kotlinx.serialization.json.Json
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+internal class TransferRequest(
+ environment: ApiEnvironment,
+ json: Json,
+ httpClient: HttpClient,
+ token: () -> String,
+) : BaseRequest(environment, json, httpClient, token) {
+
+ suspend fun getTransfer(linkUUID: String, password: String? = null): ApiResponseV2Success {
+ return get(
+ url = createV2Url(ApiRoutes.getTransfer(linkUUID)),
+ appendHeaders = {
+ appendBearer()
+ appendPasswordIfNeeded(password)
+ }
+ )
+ }
+
+ suspend fun presignedDownloadUrl(
+ linkId: String,
+ fileId: String,
+ password: String?
+ ): ApiResponseV2Success = get(
+ url = createV2Url(ApiRoutes.presignedDownloadUrl(linkId, fileId)),
+ appendHeaders = { appendPasswordIfNeeded(password) }
+ )
+
+ suspend fun deleteTransfer(transferId: String): Boolean {
+ val response = httpClient.delete(
+ url = createV2Url(ApiRoutes.v2DeleteTransfer(transferId)),
+ )
+ return response.status.isSuccess()
+ }
+
+ private fun HeadersBuilder.appendPasswordIfNeeded(password: String?) {
+ if (password?.isNotEmpty() == true) {
+ append("Transfer-Password", password)
+ }
+ }
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/UploadRequest.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/UploadRequest.kt
new file mode 100644
index 00000000..78cde2a4
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/requests/v2/UploadRequest.kt
@@ -0,0 +1,123 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.requests.v2
+
+import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.Transfer
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+import com.infomaniak.multiplatform_swisstransfer.network.models.ApiResponseV2Success
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.ChunkEtag
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.CreateTransfer
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.request.v2.UploadTransferStatus
+import com.infomaniak.multiplatform_swisstransfer.network.models.upload.response.v2.PresignedUrlResponse
+import com.infomaniak.multiplatform_swisstransfer.network.requests.BaseRequest
+import com.infomaniak.multiplatform_swisstransfer.network.utils.ApiRoutes
+import com.infomaniak.multiplatform_swisstransfer.network.utils.longTimeout
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.onUpload
+import io.ktor.client.plugins.retry
+import io.ktor.client.request.headers
+import io.ktor.client.request.patch
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.content.OutgoingContent
+import io.ktor.http.contentType
+import io.ktor.http.isSuccess
+import kotlinx.serialization.json.Json
+
+internal class UploadRequest(
+ environment: ApiEnvironment,
+ json: Json,
+ httpClient: HttpClient,
+ token: () -> String,
+) : BaseRequest(environment, json, httpClient, token) {
+
+ suspend fun createTransfer(createTransfer: CreateTransfer): ApiResponseV2Success {
+ val nullableJson = Json(json) {
+ explicitNulls = false
+ }
+ return post(
+ url = createV2Url(ApiRoutes.createTransfer()),
+ data = nullableJson.encodeToString(createTransfer),
+ appendHeaders = { appendBearer() }
+ )
+ }
+
+ suspend fun getPresignedUploadUrl(transferId: String, fileId: String): ApiResponseV2Success {
+ return post(
+ url = createV2Url(ApiRoutes.uploadDirectly(transferId, fileId)),
+ data = null,
+ )
+ }
+
+ suspend fun getPresignedUploadChunkUrl(
+ transferId: String,
+ fileId: String,
+ chunkIndex: Int,
+ ): ApiResponseV2Success {
+ return post(
+ url = createV2Url(ApiRoutes.uploadChunk(transferId, fileId, chunkIndex)),
+ data = null,
+ )
+ }
+
+ suspend fun finalizeTransferFile(transferId: String, fileId: String, etags: List): Boolean {
+ val response = httpClient.patch(createV2Url(ApiRoutes.uploadDirectly(transferId, fileId))) {
+ contentType(ContentType.Application.Json)
+ setBody(etags)
+ }
+ return response.status.isSuccess()
+ }
+
+ suspend fun updateTransferStatus(transferId: String, status: UploadTransferStatus): Boolean {
+ val response = httpClient.patch(createV2Url(ApiRoutes.finishTransfer(transferId))) {
+ headers { appendBearer() }
+ contentType(ContentType.Application.Json)
+ setBody(mapOf("status" to status.apiValue))
+ }
+ return response.status.isSuccess()
+ }
+
+ suspend fun uploadFile(
+ cephUrl: String,
+ data: OutgoingContent.WriteChannelContent,
+ onUpload: suspend (bytesSentTotal: Long, chunkSize: Long) -> Unit,
+ ) {
+ httpClient.post(urlString = cephUrl) {
+ retry { noRetry() }
+ longTimeout()
+ setBody(data)
+ onUpload { bytesSentTotal, contentLength -> onUpload(bytesSentTotal, contentLength ?: 0) }
+ }
+ }
+
+ suspend fun uploadChunk(
+ cephUrl: String,
+ data: OutgoingContent.WriteChannelContent,
+ onUpload: suspend (bytesSentTotal: Long, chunkSize: Long) -> Unit,
+ ): String? {
+ val response = httpClient.post(urlString = cephUrl) {
+ retry { noRetry() }
+ longTimeout()
+ setBody(data)
+ onUpload { bytesSentTotal, contentLength -> onUpload(bytesSentTotal, contentLength ?: 0) }
+ }
+
+ return if (response.status.isSuccess()) response.headers["Etag"] else null
+ }
+}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/ApiRoutes.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/ApiRoutes.kt
index d4e926cc..14d0e89c 100644
--- a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/ApiRoutes.kt
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/ApiRoutes.kt
@@ -22,14 +22,17 @@ import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
internal object ApiRoutes {
fun apiBaseUrl(environment: ApiEnvironment) = "${environment.baseUrl}/api/"
+ fun apiBaseUrlV2(environment: ApiEnvironment) = "${environment.baseUrlV2}/api/1/"
//region Transfer
fun getTransfer(linkUUID: String): String = "links/$linkUUID"
fun disableLinks(): String = "disableLinks"
+ fun presignedDownloadUrl(linkId: String, fileId: String) = "links/$linkId/files/$fileId"
+ fun v2DeleteTransfer(transferId: String) = "transfers/$transferId"
fun generateDownloadToken() = "generateDownloadToken"
- //endRegion
+ //endregion
//region Upload
const val initUpload = "mobile/containers"
@@ -37,5 +40,13 @@ internal object ApiRoutes {
const val resendEmailCode = "$verifyEmailCode/resend"
const val finishUpload = "uploadComplete"
const val cancelUpload = "cancelUpload"
+
+ fun createTransfer() = "transfers"
+ fun uploadDirectly(transferId: String, fileId: String) = "transfers/$transferId/files/$fileId"
+ fun uploadChunk(transferId: String, fileId: String, chunkIndex: Int): String {
+ return "${uploadDirectly(transferId, fileId)}/chunks/$chunkIndex"
+ }
+
+ fun finishTransfer(transferId: String) = "transfers/$transferId"
//endregion
}
diff --git a/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/SharedApiV2Routes.kt b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/SharedApiV2Routes.kt
new file mode 100644
index 00000000..da285d9f
--- /dev/null
+++ b/STNetwork/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/network/utils/SharedApiV2Routes.kt
@@ -0,0 +1,25 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network.utils
+
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+
+object SharedApiV2Routes {
+
+ fun shareTransfer(environment: ApiEnvironment, linkUUID: String): String = "${environment.baseUrlV2}/d/$linkUUID"
+}
diff --git a/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProviderTest.kt b/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProviderTest.kt
index 2ed26384..0d886fca 100644
--- a/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProviderTest.kt
+++ b/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/ApiClientProviderTest.kt
@@ -18,6 +18,7 @@
package com.infomaniak.multiplatform_swisstransfer.network
import com.infomaniak.multiplatform_swisstransfer.network.exceptions.ApiException
+import com.infomaniak.multiplatform_swisstransfer.network.exceptions.NetworkException
import com.infomaniak.multiplatform_swisstransfer.network.models.ApiError
import com.infomaniak.multiplatform_swisstransfer.network.utils.CONTENT_REQUEST_ID_HEADER
import com.infomaniak.multiplatform_swisstransfer.network.utils.decode
@@ -31,10 +32,12 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.http.headersOf
+import io.ktor.util.network.UnresolvedAddressException
import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.encodeToString
+import kotlinx.io.IOException
import kotlinx.serialization.json.Json
import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.expect
class ApiClientProviderTest {
@@ -73,6 +76,42 @@ class ApiClientProviderTest {
}
}
+ @Test
+ fun unresolvedAddressExceptionConvertedToNetworkException() {
+ val apiClientProvider = ApiClientProvider(
+ createFailingEngine(UnresolvedAddressException()),
+ userAgent = TEST_USER_AGENT
+ )
+
+ val result = runCatching {
+ runBlocking {
+ post(apiClientProvider = apiClientProvider, url = Url("http://invalid-hostname.com"), data = null)
+ }
+ }
+
+ val exception = result.exceptionOrNull()
+ assertEquals(NetworkException::class, exception!!::class)
+ assertEquals(UnresolvedAddressException::class, exception.cause!!::class)
+ }
+
+ @Test
+ fun ioExceptionConvertedToNetworkException() {
+ val apiClientProvider = ApiClientProvider(
+ createFailingEngine(IOException("Connection refused")),
+ userAgent = TEST_USER_AGENT
+ )
+
+ val result = runCatching {
+ runBlocking {
+ post(apiClientProvider = apiClientProvider, url = Url("http://localhost:8080"), data = null)
+ }
+ }
+
+ val exception = result.exceptionOrNull()
+ assertEquals(NetworkException::class, exception!!::class)
+ assertEquals(IOException::class, exception.cause!!::class)
+ }
+
private suspend inline fun post(apiClientProvider: ApiClientProvider, url: Url, data: Any?): R {
return apiClientProvider.httpClient.post(url) {
contentType(ContentType.Application.Json)
@@ -91,6 +130,10 @@ class ApiClientProviderTest {
)
}
+ private fun createFailingEngine(throwable: Throwable) = MockEngine { _ ->
+ throw throwable
+ }
+
companion object {
private const val TEST_USER_AGENT = "Ktor client test"
diff --git a/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/TransferV2RepositoryTest.kt b/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/TransferV2RepositoryTest.kt
new file mode 100644
index 00000000..0f837875
--- /dev/null
+++ b/STNetwork/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/network/TransferV2RepositoryTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Infomaniak SwissTransfer - Multiplatform
+ * Copyright (C) 2024 Infomaniak Network SA
+ *
+ * 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 .
+ */
+package com.infomaniak.multiplatform_swisstransfer.network
+
+import com.infomaniak.multiplatform_swisstransfer.common.utils.ApiEnvironment
+import com.infomaniak.multiplatform_swisstransfer.network.models.ApiResponseV2Success
+import com.infomaniak.multiplatform_swisstransfer.network.models.transfer.v2.TransferApi
+import com.infomaniak.multiplatform_swisstransfer.network.repositories.TransferV2Repository
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.MissingFieldException
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class TransferV2RepositoryTest {
+
+ private val apiClientProvider = ApiClientProvider()
+ private val transferRepository = TransferV2Repository(
+ apiClientProvider = apiClientProvider,
+ environment = ApiEnvironment.Preprod,
+ token = { "dummy_token" }
+ ) // TODO: Use mock client
+
+ @Test
+ fun canExtractLinkUUIDFromUrl() {
+ val url = "https://www.swisstransfer.com/d/fa7d299d-1001-4668-83a4-2a9b61aa59e8"
+ val result = transferRepository.extractUUID(url)
+ assertEquals("fa7d299d-1001-4668-83a4-2a9b61aa59e8", result)
+ }
+
+ @Test
+ fun canParseEmptyMimeTypeToNull() {
+ val data = """
+ {
+ "result": "success",
+ "data": {
+ "id": "c96350da-4d8c-4c43-aa6a-40f2d2b31eac",
+ "sender_email": "example@ik.me",
+ "title": "Coucou",
+ "message": "Voici ce que tu m'as demandé",
+ "created_at": 1771324513,
+ "expires_at": 1771324513,
+ "files": [
+ {
+ "id": "2e085789-43cb-48f7-ab1f-b7322fbf5367",
+ "path": "app-release (1).aab",
+ "size": 26070874,
+ "mimeType": ""
+ }
+ ],
+ "total_size": 26070874
+ }
+ }
+ """.trimIndent()
+
+ val responseData = apiClientProvider.json.decodeFromString>(data).data
+ assertNotNull(responseData)
+
+ val file = responseData.files.firstOrNull()
+ assertNotNull(file)
+
+ assertNull(file.mimeType)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ @Test
+ fun throwWhenApiResponseWithError() {
+ val data = """
+ {
+ "result": "success",
+ "data": {
+ "type": "need_password",
+ "message": "Transfer need a password"
+ }
+ }
+ """.trimIndent()
+
+ assertFailsWith(MissingFieldException::class) {
+ apiClientProvider.json.decodeFromString>(data)
+ }
+ }
+}