Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add raw http body request #141

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
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
39 changes: 39 additions & 0 deletions android/src/main/java/io/deckers/blob_courier/BlobCourierModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import io.deckers.blob_courier.react.CongestionAvoidingProgressNotifierFactory
import io.deckers.blob_courier.react.processUnexpectedError
import io.deckers.blob_courier.react.processUnexpectedException
import io.deckers.blob_courier.react.toReactMap
import io.deckers.blob_courier.send.BlobSender
import io.deckers.blob_courier.send.SenderParameterFactory
import io.deckers.blob_courier.upload.BlobUploader
import io.deckers.blob_courier.upload.UploaderParameterFactory
import java.net.UnknownHostException
Expand Down Expand Up @@ -134,6 +136,43 @@ class BlobCourierModule(private val reactContext: ReactApplicationContext) :
li("Called fetchBlob")
}

@ReactMethod
fun sendBlob(input: ReadableMap, promise: Promise) {
li("Calling sendBlob")
thread {
try {
SenderParameterFactory()
.fromInput(input)
.fold(::Failure, ::Success)
.fmap(
BlobSender(
reactContext,
createHttpClient(),
createProgressFactory(reactContext)
)::send
)
.map { it.toReactMap() }
.`do`(
{ f ->
lv("Something went wrong during send (code=${f.code},message=${f.message})")
promise.reject(f.code, f.message)
},
promise::resolve
)
} catch (e: UnknownHostException) {
lv("Unknown host", e)
promise.reject(ERROR_UNKNOWN_HOST, e)
} catch (e: Exception) {
le("Unexpected exception", e)
promise.reject(ERROR_UNEXPECTED_EXCEPTION, processUnexpectedException(e).message)
} catch (e: Error) {
le("Unexpected error", e)
promise.reject(ERROR_UNEXPECTED_ERROR, processUnexpectedError(e).message)
}
}
li("Called sendBlob")
}

@ReactMethod
fun uploadBlob(input: ReadableMap, promise: Promise) {
li("Calling uploadBlob")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MPL-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
package io.deckers.blob_courier.upload
package io.deckers.blob_courier.common

import android.content.ContentResolver
import android.net.Uri
Expand Down
87 changes: 87 additions & 0 deletions android/src/main/java/io/deckers/blob_courier/send/BlobSender.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright (c) Ely Deckers.
*
* This source code is licensed under the MPL-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
package io.deckers.blob_courier.send

import android.content.Context
import io.deckers.blob_courier.cancel.registerCancellationHandler
import io.deckers.blob_courier.common.ERROR_CANCELED_EXCEPTION
import io.deckers.blob_courier.common.ERROR_UNEXPECTED_ERROR
import io.deckers.blob_courier.common.ERROR_UNEXPECTED_EXCEPTION
import io.deckers.blob_courier.common.Failure
import io.deckers.blob_courier.common.Logger
import io.deckers.blob_courier.common.Result
import io.deckers.blob_courier.common.Success
import io.deckers.blob_courier.common.createErrorFromThrowable
import io.deckers.blob_courier.common.mapHeadersToMap
import io.deckers.blob_courier.progress.BlobCourierProgressRequest
import io.deckers.blob_courier.progress.ProgressNotifierFactory
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException

private val TAG = BlobSender::class.java.name

private val logger = Logger(TAG)
private fun li(m: String) = logger.i(m)

class BlobSender(
private val context: Context,
private val httpClient: OkHttpClient,
private val progressNotifierFactory: ProgressNotifierFactory
) {

fun send(senderParameters: SenderParameters): Result<Map<String, Any>> {
li("Starting unmanaged send")

val requestBody = BlobCourierProgressRequest(
senderParameters.toRequestBody(context.contentResolver),
progressNotifierFactory.create(senderParameters.taskId)
)

val requestBuilder = Request.Builder()
.url(senderParameters.uri)
.method(senderParameters.method, requestBody)
.apply {
senderParameters.headers.forEach { e: Map.Entry<String, String> ->
addHeader(e.key, e.value)
}
}
.build()

val sendRequestCall = httpClient.newCall(requestBuilder)

try {
registerCancellationHandler(context, senderParameters.taskId, sendRequestCall)

val response = sendRequestCall.execute()

val responseBody = response.body?.string().orEmpty()

li("Finished unmanaged send")

return Success(
mapOf(
"response" to mapOf(
"code" to response.code,
"data" to if (senderParameters.returnResponse) responseBody else "",
"headers" to mapHeadersToMap(response.headers)
)
)
)
} catch (e: IOException) {
if (sendRequestCall.isCanceled()) {
return Failure(createErrorFromThrowable(ERROR_CANCELED_EXCEPTION, e))
}

return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_EXCEPTION, e))
} catch (e: Exception) {
return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_EXCEPTION, e))
} catch (e: Error) {
return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_ERROR, e))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) Ely Deckers.
*
* This source code is licensed under the MPL-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
package io.deckers.blob_courier.send

import android.content.ContentResolver
import android.net.Uri
import com.facebook.react.bridge.ReadableMap
import io.deckers.blob_courier.common.DEFAULT_MIME_TYPE
import io.deckers.blob_courier.common.DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
import io.deckers.blob_courier.common.DEFAULT_UPLOAD_METHOD
import io.deckers.blob_courier.common.InputStreamRequestBody
import io.deckers.blob_courier.common.PARAMETER_ABSOLUTE_FILE_PATH
import io.deckers.blob_courier.common.PARAMETER_HEADERS
import io.deckers.blob_courier.common.PARAMETER_METHOD
import io.deckers.blob_courier.common.PARAMETER_MIME_TYPE
import io.deckers.blob_courier.common.PARAMETER_SETTINGS_PROGRESS_INTERVAL
import io.deckers.blob_courier.common.PARAMETER_TASK_ID
import io.deckers.blob_courier.common.PARAMETER_URL
import io.deckers.blob_courier.common.PROVIDED_PARAMETERS
import io.deckers.blob_courier.common.ValidationResult
import io.deckers.blob_courier.common.ValidationSuccess
import io.deckers.blob_courier.common.filterHeaders
import io.deckers.blob_courier.common.getMapInt
import io.deckers.blob_courier.common.hasRequiredStringField
import io.deckers.blob_courier.common.ifNone
import io.deckers.blob_courier.common.isNotNull
import io.deckers.blob_courier.common.maybe
import io.deckers.blob_courier.common.right
import io.deckers.blob_courier.common.testKeep
import io.deckers.blob_courier.common.validationContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import java.net.URL

private const val PARAMETER_RETURN_RESPONSE = "returnResponse"

data class RequiredParameters(
val taskId: String,
val url: String,
val absoluteFilePath: String,
)

data class SenderParameters(
val absoluteFilePath: Uri,
val mediaType: String,
val method: String,
val headers: Map<String, String>,
val progressInterval: Int,
val returnResponse: Boolean,
val taskId: String,
val uri: URL,
)

fun SenderParameters.toRequestBody(contentResolver: ContentResolver): RequestBody =
InputStreamRequestBody(
mediaType.toMediaTypeOrNull()
?: DEFAULT_MIME_TYPE.toMediaType(),
contentResolver,
absoluteFilePath
)

private fun verifyRequiredParametersProvided(input: ReadableMap):
ValidationResult<RequiredParameters> =
validationContext(input, isNotNull(PROVIDED_PARAMETERS))
.fmap(testKeep(hasRequiredStringField(PARAMETER_ABSOLUTE_FILE_PATH)))
.fmap(testKeep(hasRequiredStringField(PARAMETER_TASK_ID)))
.fmap(testKeep(hasRequiredStringField(PARAMETER_URL)))
.fmap { (_, validatedParameters) ->
val (url, rest) = validatedParameters
val (taskId, rest2) = rest
val (absoluteFilePath, _) = rest2

ValidationSuccess(RequiredParameters(taskId, url, absoluteFilePath))
}

class SenderParameterFactory {
fun fromInput(input: ReadableMap): ValidationResult<SenderParameters> =
verifyRequiredParametersProvided(input)
.fmap {
val (taskId, url, absoluteFilePath) = it

val mediaType = maybe(input.getString(PARAMETER_MIME_TYPE)).ifNone(DEFAULT_MIME_TYPE)
val method = maybe(input.getString(PARAMETER_METHOD)).ifNone(DEFAULT_UPLOAD_METHOD)

val unfilteredHeaders =
input.getMap(PARAMETER_HEADERS)?.toHashMap() ?: emptyMap<String, Any>()

val headers = filterHeaders(unfilteredHeaders)

val returnResponse =
input.hasKey(PARAMETER_RETURN_RESPONSE) && input.getBoolean(PARAMETER_RETURN_RESPONSE)

val progressInterval =
getMapInt(
input,
PARAMETER_SETTINGS_PROGRESS_INTERVAL,
DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
)

right(SenderParameters(
Uri.parse(absoluteFilePath),
mediaType,
method,
headers,
progressInterval,
returnResponse,
taskId,
URL(url),
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.deckers.blob_courier.common.DEFAULT_MIME_TYPE
import io.deckers.blob_courier.common.DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
import io.deckers.blob_courier.common.DEFAULT_UPLOAD_METHOD
import io.deckers.blob_courier.common.Either
import io.deckers.blob_courier.common.InputStreamRequestBody
import io.deckers.blob_courier.common.PARAMETER_ABSOLUTE_FILE_PATH
import io.deckers.blob_courier.common.PARAMETER_FILENAME
import io.deckers.blob_courier.common.PARAMETER_HEADERS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import io.deckers.blob_courier.common.validate
import io.deckers.blob_courier.react.CongestionAvoidingProgressNotifier
import io.deckers.blob_courier.react.CongestionAvoidingProgressNotifierFactory
import io.deckers.blob_courier.react.toReactMap
import io.deckers.blob_courier.upload.InputStreamRequestBody
import io.deckers.blob_courier.common.InputStreamRequestBody
import io.deckers.blob_courier.upload.UploaderParameterFactory
import io.deckers.blob_courier.upload.toMultipartBody
import io.mockk.EqMatcher
Expand Down
3 changes: 3 additions & 0 deletions ios/BlobCourier.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ @interface RCT_EXTERN_MODULE(BlobCourier, NSObject)
RCT_EXTERN_METHOD(fetchBlob:(NSDictionary *)input
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sendBlob:(NSDictionary *)input
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(uploadBlob:(NSDictionary *)input
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
Expand Down
29 changes: 29 additions & 0 deletions ios/BlobCourier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ open class BlobCourier: NSObject {
}
}

@objc(sendBlob:withResolver:withRejecter:)
func sendBlob(
input: NSDictionary,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
DispatchQueue.global(qos: .background).async {
do {
let errorOrParameters = SenderParameterFactory.fromInput(input: input)

if case .failure(let error) = errorOrParameters { reject(error.code, error.message, error.error) }
guard case .success(let parameters) = errorOrParameters else { return }

let result = BlobSender.sendBlobFromValidatedParameters(parameters: parameters)

switch result {
case .success(let success):
resolve(success)
case .failure(let error):
reject(error.code, error.message, error.error)
}
} catch {
let unexpectedError = Errors.createUnexpectedError(error: error)

reject(unexpectedError.code, unexpectedError.message, unexpectedError.error)
}
}
}

@objc(uploadBlob:withResolver:withRejecter:)
func uploadBlob(
input: NSDictionary,
Expand Down
Loading