diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ebec4..b66e7f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.0.3 + +### 2026-01-08 + +- Fix uploading files with params. + ## 1.0.2 ### 2025-12-22 diff --git a/pom.xml b/pom.xml index 9d46eb2..3581ec6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ionfiletransfer-android - 1.0.2 + 1.0.3 \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt index 961ad72..b568c9a 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt @@ -126,7 +126,9 @@ class IONFLTRController internal constructor( fileHelper.createParentDirectories(targetFile) // Setup connection - val connection = connectionHelper.setupConnection(options.url, options.httpOptions) + val connection = connectionHelper.setupConnection(options.url, options.httpOptions).let { + connectionHelper.appendParamsToUrl(options.url, it, options.httpOptions, useChunkedMode = false) + } return Pair(targetFile, connection) } @@ -250,7 +252,9 @@ class IONFLTRController internal constructor( val file = fileHelper.getFileToUploadInfo(options.filePath) // Setup connection - val connection = connectionHelper.setupConnection(options.url, options.httpOptions) + val connection = connectionHelper.setupConnection(options.url, options.httpOptions).let { + connectionHelper.appendParamsToUrl(options.url, it, options.httpOptions, options.chunkedMode) + } return Pair(file, connection) } @@ -268,18 +272,16 @@ class IONFLTRController internal constructor( ): Pair? { var multiPartUpload = false // Set content type if not already set - if (!options.httpOptions.headers.containsKey("Content-Type")) { + if (connectionHelper.useMultipartFormData(options.httpOptions) && !useChunkedMode) { + multiPartUpload = true + connection.setRequestProperty( + "Content-Type", + "multipart/form-data; boundary=$BOUNDARY" + ) + } else if (!options.httpOptions.headers.containsKey("Content-Type")) { val mimeType = options.mimeType ?: fileHelper.getMimeType(options.filePath) ?: "application/octet-stream" - if (isPostOrPutMethod(options.httpOptions.method)) { - multiPartUpload = true - connection.setRequestProperty( - "Content-Type", - "multipart/form-data; boundary=$BOUNDARY" - ) - } else { - connection.setRequestProperty("Content-Type", mimeType) - } + connection.setRequestProperty("Content-Type", mimeType) } if (useChunkedMode) { @@ -314,8 +316,9 @@ class IONFLTRController internal constructor( val boundary = "$LINE_START$BOUNDARY$LINE_END" val beforeData = buildString { - // Write additional form parameters if any - options.formParams?.forEach { (key, value) -> + // Write form parameters using the available attributes + val allParams = (options.formParams ?: emptyMap()) + options.httpOptions.params + allParams.forEach { (key, value) -> append(boundary) val paramHeader = "Content-Disposition: form-data; name=\"$key\"$LINE_END$LINE_END" val paramValue = "$value$LINE_END" @@ -395,7 +398,7 @@ class IONFLTRController internal constructor( emit(createUploadFileProgress(bytes = 0, total = 0)) return 0L } - + var totalBytesWritten: Long connection.outputStream.use { connOutputStream -> @@ -446,14 +449,4 @@ class IONFLTRController internal constructor( ) ) } - - /** - * Checks if the HTTP method is either POST or PUT. - * - * @param method The HTTP method to check - * @return True if the method is POST or PUT, false otherwise - */ - private fun isPostOrPutMethod(method: String): Boolean { - return method.equals("POST", ignoreCase = true) || method.equals("PUT", ignoreCase = true) - } } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt index 03ed379..e5a71fe 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt @@ -26,7 +26,7 @@ inline fun HttpURLConnection.use(block: (HttpURLConnection) -> R): R { * Extension function to assert that an HTTP response was successful (2xx status code). * If the response was not successful, throws an IONFLTRException.HttpError with details * from the error stream. - * + * * @throws IONFLTRException.HttpError if the response code is not in the 200-299 range */ @@ -46,27 +46,46 @@ fun HttpURLConnection.assertSuccessHttpResponse() { /** * Helper class for setting up HTTP connections with proper configuration. */ -class IONFLTRConnectionHelper { +internal class IONFLTRConnectionHelper { /** * Sets up the HTTP connection with the provided options. */ fun setupConnection(urlString: String, httpOptions: IONFLTRTransferHttpOptions): HttpURLConnection { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection - + // Set method connection.requestMethod = httpOptions.method - + // Set timeouts connection.connectTimeout = httpOptions.connectTimeout connection.readTimeout = httpOptions.readTimeout - + // Set headers httpOptions.headers.forEach { (key, value) -> connection.setRequestProperty(key, value) } - - // Set parameters + + // Set redirect handling + connection.instanceFollowRedirects = !httpOptions.disableRedirects + + // Set SSL factory if provided + if (httpOptions.sslSocketFactory != null && connection is javax.net.ssl.HttpsURLConnection) { + connection.sslSocketFactory = httpOptions.sslSocketFactory + } + + return connection + } + + /** + * append params to url, if applicable + */ + fun appendParamsToUrl( + urlString: String, + connection: HttpURLConnection, + httpOptions: IONFLTRTransferHttpOptions, + useChunkedMode: Boolean + ): HttpURLConnection { if (httpOptions.params.isNotEmpty()) { val paramString = buildString { httpOptions.params.forEach { (key, values) -> @@ -82,27 +101,22 @@ class IONFLTRConnectionHelper { } } } - - if (httpOptions.method.equals("GET", ignoreCase = true)) { + + // for HTTP requests that aren't for multipart/form-data, params will be written in the request body. + if (!useMultipartFormData(httpOptions) || useChunkedMode) { val separator = if (urlString.contains("?")) "&" else "?" val newUrl = URL("$urlString$separator$paramString") return newUrl.openConnection() as HttpURLConnection - } else { - connection.doOutput = true - connection.outputStream.use { os -> - os.write(paramString.toByteArray()) - } } } - - // Set redirect handling - connection.instanceFollowRedirects = !httpOptions.disableRedirects - - // Set SSL factory if provided - if (httpOptions.sslSocketFactory != null && connection is javax.net.ssl.HttpsURLConnection) { - connection.sslSocketFactory = httpOptions.sslSocketFactory - } - return connection } -} \ No newline at end of file + + /** + * @return true for any HTTP request that isn't a multipart/form-data + */ + internal fun useMultipartFormData(options: IONFLTRTransferHttpOptions): Boolean = + !options.headers.containsKey("Content-Type") && + (options.method.equals("POST", ignoreCase = true) || options.method.equals("PUT", ignoreCase = true)) + +}