Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.ionic.libs</groupId>
<artifactId>ionfiletransfer-android</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -268,18 +272,16 @@ class IONFLTRController internal constructor(
): Pair<String, String>? {
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) {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -395,7 +398,7 @@ class IONFLTRController internal constructor(
emit(createUploadFileProgress(bytes = 0, total = 0))
return 0L
}

var totalBytesWritten: Long

connection.outputStream.use { connOutputStream ->
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ inline fun <R> 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
*/

Expand All @@ -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) ->
Expand All @@ -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
}
}

/**
* @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))

}