Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.media.MediaScannerConnection
import android.os.Build
import android.os.Environment
import androidx.core.net.toUri
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.PermissionState
import com.getcapacitor.Plugin
Expand Down Expand Up @@ -155,7 +156,8 @@ class FileTransferPlugin : Plugin() {
val options = IONFLTRDownloadOptions(
url = url,
filePath = filePath,
httpOptions = httpOptions
httpOptions = httpOptions,
body = getRequestBody(call, httpOptions)
)

controller.downloadFile(options)
Expand Down Expand Up @@ -185,6 +187,13 @@ class FileTransferPlugin : Plugin() {

val response = JSObject().apply {
put("path", filePath)
result.data.headers?.let { headers ->
val headersObject = JSObject()
for (header in headers) {
headersObject.put(header.key, JSArray(header.value))
}
put("headers", headersObject);
}
}
call.resolve(response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.capacitorjs.plugins.filetransfer

import android.os.Build
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.JSValue
import com.getcapacitor.PluginCall
import io.ionic.libs.ionfiletransferlib.model.IONFLTRTransferHttpOptions
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Base64

// This file is basically a conversion of https://github.com/ionic-team/capacitor/blob/main/android/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java#L192
// to Kotlin and returning a ByteArray instead of directly writing into the output stream

fun getRequestBody(call: PluginCall, http: IONFLTRTransferHttpOptions): ByteArray? {
val contentType = http.headers["Content-Type"]
if (contentType.isNullOrBlank()) return null

val method = http.method
val isHttpMutate =
method == "DELETE" || method == "PATCH" || method == "POST" || method == "PUT"
if (!isHttpMutate) return null

val body = JSValue(call, "data")
val bodyType = call.getString("dataType")

if (contentType.contains("application/json")) {
return stringToRequestBody(body.toString())
} else if (bodyType != null && bodyType == "file") {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Base64.getDecoder().decode(body.toString())
} else {
android.util.Base64.decode(body.toString(), android.util.Base64.DEFAULT)
}
} else if (contentType.contains("application/x-www-form-urlencoded")) {
try {
val obj = body.toJSObject()
return objectToRequestBody(obj)
} catch (e: Exception) {
return stringToRequestBody(body.toString())
}
} else if (bodyType != null && bodyType == "formData") {
return formDataToRequestBody(contentType, body.toJSArray())
} else {
return stringToRequestBody(body.toString())
}
}

fun stringToRequestBody(from: String): ByteArray {
return from.toByteArray(Charsets.UTF_8)
}

fun objectToRequestBody(from: JSObject): ByteArray {
val bytes = ByteArrayOutputStream()
DataOutputStream(bytes).use { os ->
val keys = from.keys()
for (key in keys) {
val d = from.get(key)
os.writeBytes(URLEncoder.encode(key, "UTF-8"))
os.writeBytes("=")
os.writeBytes(URLEncoder.encode(d.toString(), "UTF-8"))

if (keys.hasNext()) {
os.writeBytes("&")
}
}
}
return bytes.toByteArray()
}

fun formDataToRequestBody(contentType: String, entries: JSArray): ByteArray {
val bytes = ByteArrayOutputStream()
DataOutputStream(bytes).use { os ->
val boundary = contentType.split(";")[1].split("=")[1]
val lineEnd = "\r\n"
val twoHyphens = "--"

for (e in entries.toList<Any>()) {
if (e is JSONObject) {
val type = e.getString("type")
val key = e.getString("key")
val value = e.getString("value")
if (type == "string") {
os.writeBytes(twoHyphens + boundary + lineEnd)
os.writeBytes("Content-Disposition: form-data; name=\"$key\"$lineEnd$lineEnd")
os.write(value.toByteArray(StandardCharsets.UTF_8))
os.writeBytes(lineEnd)
} else if (type == "base64File") {
val fileName = e.getString("fileName")
val fileContentType = e.getString("contentType")

os.writeBytes(twoHyphens + boundary + lineEnd)
os.writeBytes("Content-Disposition: form-data; name=\"$key\"; filename=\"$fileName\"$lineEnd")
os.writeBytes("Content-Type: $fileContentType$lineEnd")
os.writeBytes("Content-Transfer-Encoding: binary$lineEnd")
os.writeBytes(lineEnd)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
os.write(Base64.getDecoder().decode(value))
} else {
os.write(android.util.Base64.decode(value, android.util.Base64.DEFAULT))
}

os.writeBytes(lineEnd)
}
}
}
}
return bytes.toByteArray()
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
@objc func downloadFile(_ call: CAPPluginCall) {
do {
let prepData = try validateAndPrepare(call: call, action: .download)
var httpOptions = prepData.httpOptions
let reqData = try getRequestData(call: call, options: &httpOptions)

try manager.downloadFile(
fromServerURL: prepData.serverURL,
toFileURL: prepData.fileURL,
withHttpOptions: prepData.httpOptions
withHttpOptions: httpOptions,
body: reqData
).sink(
receiveCompletion: handleCompletion(call: call, source: prepData.serverURL.absoluteString, target: prepData.fileURL.absoluteString),
receiveValue: handleReceiveValue(
Expand Down Expand Up @@ -225,7 +228,7 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
let result: JSObject = {
switch type {
case .download:
return ["path": path]
return ["path": path, "headers": data.headers as JSObject]
case .upload:
return [
"bytesSent": data.totalBytes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import Capacitor
import IONFileTransferLib

// Basically a reimplementation of certain parts of CapacitorUrlRequest
// that does not need to be used with the latter.

public enum RequestBodyError: Error {
case serializationError(String?)
}

public struct RequestData {
let body: Data
let additionalHeaders: [String: String]

init(body: Data) {
self.body = body
self.additionalHeaders = [:]
}
init(body: Data, additionalHeaders: [String: String]) {
self.body = body
self.additionalHeaders = additionalHeaders
}
}

public func getRequestData(
call: CAPPluginCall,
options: inout IONFLTRHttpOptions
) throws -> Data? {
guard let body = call.options["data"] as? JSValue else {
return nil
}
let bodyType = call.getString("dataType")
guard let contentType = options.headers["Content-Type"] else {
return nil
}
guard let reqData = try getRequestData(body, contentType, bodyType) else {
return nil
}

options.headers.merge(reqData.additionalHeaders, uniquingKeysWith: { _, new in new })

return reqData.body
}

public func getRequestData(
_ body: JSValue,
_ contentType: String,
_ dataType: String? = nil
) throws -> RequestData? {
if dataType == "file" {
guard let stringData = body as? String else {
throw RequestBodyError.serializationError(
"[ data ] argument could not be parsed as string"
)
}
guard let data = Data(base64Encoded: stringData) else {return nil}
return RequestData(body: data)
} else if dataType == "formData" {
return try getRequestDataFromFormData(body, contentType)
}

// If data can be parsed directly as a string, return that without processing.
if let strVal = try? getRequestDataAsString(body) {
return strVal
} else if contentType.contains("application/json") {
return try getRequestDataAsJson(body)
} else if contentType.contains("application/x-www-form-urlencoded") {
return try getRequestDataAsFormUrlEncoded(body)
} else if contentType.contains("multipart/form-data") {
return try getRequestDataAsMultipartFormData(body, contentType)
} else {
throw RequestBodyError.serializationError(
"[ data ] argument could not be parsed for content type [ \(contentType) ]"
)
}
}

public func getRequestDataAsString(_ data: JSValue) throws -> RequestData {
guard let stringData = data as? String else {
throw RequestBodyError.serializationError(
"[ data ] argument could not be parsed as string"
)
}
return RequestData(body: Data(stringData.utf8))
}

public func getRequestDataFromFormData(_ data: JSValue, _ contentType: String)
throws -> RequestData?
{
guard let list = data as? JSArray else {
// Throw, other data types explicitly not supported.
throw RequestBodyError.serializationError(
"Data must be an array for FormData"
)
}
var requestHeaders: [String: String] = [:]
var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="),
let contentBoundary = contentType.components(separatedBy: "=").last
{
boundary = contentBoundary
} else {
let contentType = "multipart/form-data; boundary=\(boundary)"
requestHeaders["Content-Type"] = contentType
}
for entry in list {
guard let item = entry as? [String: String] else {
throw RequestBodyError.serializationError(
"Data must be an array for FormData"
)
}

let type = item["type"]
let key = item["key"]
let value = item["value"]!

if type == "base64File" {
let fileName = item["fileName"]
let fileContentType = item["contentType"]

data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append(
"Content-Disposition: form-data; name=\"\(key!)\"; filename=\"\(fileName!)\"\r\n"
.data(using: .utf8)!
)
data.append(
"Content-Type: \(fileContentType!)\r\n".data(using: .utf8)!
)
data.append(
"Content-Transfer-Encoding: binary\r\n".data(using: .utf8)!
)
data.append("\r\n".data(using: .utf8)!)

data.append(Data(base64Encoded: value)!)

data.append("\r\n".data(using: .utf8)!)
} else if type == "string" {
data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append(
"Content-Disposition: form-data; name=\"\(key!)\"\r\n".data(
using: .utf8
)!
)
data.append("\r\n".data(using: .utf8)!)
data.append(value.data(using: .utf8)!)
data.append("\r\n".data(using: .utf8)!)
}
}
data.append("--\(boundary)--\r\n".data(using: .utf8)!)

return RequestData.init(body: data, additionalHeaders: requestHeaders)
}

public func getRequestDataAsJson(_ data: JSValue) throws -> RequestData? {
// We need to check if the JSON is valid before attempting to serialize, as JSONSerialization.data will not throw an exception that can be caught, and will cause the application to crash if it fails.
if JSONSerialization.isValidJSONObject(data) {
return RequestData(body: try JSONSerialization.data(withJSONObject: data))
} else {
throw RequestBodyError.serializationError("[ data ] argument for request of content-type [ application/json ] must be serializable to JSON")
}
}

public func getRequestDataAsFormUrlEncoded(_ data: JSValue) throws -> RequestData? {
var components = URLComponents()
components.queryItems = []

guard let obj = data as? JSObject else {
// Throw, other data types explicitly not supported
throw RequestBodyError.serializationError("[ data ] argument for request with content-type [ multipart/form-data ] may only be a plain javascript object")
}

let allowed = CharacterSet(charactersIn: "-._*").union(.alphanumerics)

obj.keys.forEach { (key: String) in
let value = obj[key] as? String ?? ""
components.queryItems?.append(URLQueryItem(name: key.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+") ?? key, value: value.addingPercentEncoding(withAllowedCharacters: allowed)?.replacingOccurrences(of: "%20", with: "+")))
}

if components.query != nil {
return RequestData(body: Data(components.query!.utf8))
}

return nil
}

public func getRequestDataAsMultipartFormData(_ data: JSValue, _ contentType: String) throws -> RequestData {
guard let obj = data as? JSObject else {
// Throw, other data types explicitly not supported.
throw RequestBodyError.serializationError("[ data ] argument for request with content-type [ application/x-www-form-urlencoded ] may only be a plain javascript object")
}

var additionalHeaders: [String: String] = [:]

let strings: [String: String] = obj.compactMapValues { any in
any as? String
}

var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
boundary = contentBoundary
} else {
let contentType = "multipart/form-data; boundary=\(boundary)"
additionalHeaders["Content-Type"] = contentType
}
strings.forEach { key, value in
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
data.append(value.data(using: .utf8)!)
}
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)

return RequestData(body: data, additionalHeaders: additionalHeaders)
}