Skip to content

Commit 88bf43f

Browse files
committed
feat: add Android and TS code for raw request
1 parent 1049b4e commit 88bf43f

File tree

8 files changed

+317
-2
lines changed

8 files changed

+317
-2
lines changed

android/src/main/java/io/deckers/blob_courier/BlobCourierModule.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import io.deckers.blob_courier.react.CongestionAvoidingProgressNotifierFactory
3131
import io.deckers.blob_courier.react.processUnexpectedError
3232
import io.deckers.blob_courier.react.processUnexpectedException
3333
import io.deckers.blob_courier.react.toReactMap
34+
import io.deckers.blob_courier.send.BlobSender
35+
import io.deckers.blob_courier.send.SenderParameterFactory
3436
import io.deckers.blob_courier.upload.BlobUploader
3537
import io.deckers.blob_courier.upload.UploaderParameterFactory
3638
import java.net.UnknownHostException
@@ -127,6 +129,43 @@ class BlobCourierModule(private val reactContext: ReactApplicationContext) :
127129
li("Called fetchBlob")
128130
}
129131

132+
@ReactMethod
133+
fun sendBlob(input: ReadableMap, promise: Promise) {
134+
li("Calling sendBlob")
135+
thread {
136+
try {
137+
SenderParameterFactory()
138+
.fromInput(input)
139+
.fold(::Failure, ::Success)
140+
.fmap(
141+
BlobSender(
142+
reactContext,
143+
createHttpClient(),
144+
createProgressFactory(reactContext)
145+
)::send
146+
)
147+
.map { it.toReactMap() }
148+
.`do`(
149+
{ f ->
150+
lv("Something went wrong during send (code=${f.code},message=${f.message})")
151+
promise.reject(f.code, f.message)
152+
},
153+
promise::resolve
154+
)
155+
} catch (e: UnknownHostException) {
156+
lv("Unknown host", e)
157+
promise.reject(ERROR_UNKNOWN_HOST, e)
158+
} catch (e: Exception) {
159+
le("Unexpected exception", e)
160+
promise.reject(ERROR_UNEXPECTED_EXCEPTION, processUnexpectedException(e).message)
161+
} catch (e: Error) {
162+
le("Unexpected error", e)
163+
promise.reject(ERROR_UNEXPECTED_ERROR, processUnexpectedError(e).message)
164+
}
165+
}
166+
li("Called sendBlob")
167+
}
168+
130169
@ReactMethod
131170
fun uploadBlob(input: ReadableMap, promise: Promise) {
132171
li("Calling uploadBlob")

android/src/main/java/io/deckers/blob_courier/upload/InputStreamRequestBody.kt renamed to android/src/main/java/io/deckers/blob_courier/common/InputStreamRequestBody.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MPL-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
7-
package io.deckers.blob_courier.upload
7+
package io.deckers.blob_courier.common
88

99
import android.content.ContentResolver
1010
import android.net.Uri
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) Ely Deckers.
3+
*
4+
* This source code is licensed under the MPL-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
package io.deckers.blob_courier.send
8+
9+
import android.content.Context
10+
import io.deckers.blob_courier.cancel.registerCancellationHandler
11+
import io.deckers.blob_courier.common.ERROR_CANCELED_EXCEPTION
12+
import io.deckers.blob_courier.common.ERROR_UNEXPECTED_ERROR
13+
import io.deckers.blob_courier.common.ERROR_UNEXPECTED_EXCEPTION
14+
import io.deckers.blob_courier.common.Failure
15+
import io.deckers.blob_courier.common.Logger
16+
import io.deckers.blob_courier.common.Result
17+
import io.deckers.blob_courier.common.Success
18+
import io.deckers.blob_courier.common.createErrorFromThrowable
19+
import io.deckers.blob_courier.common.mapHeadersToMap
20+
import io.deckers.blob_courier.progress.BlobCourierProgressRequest
21+
import io.deckers.blob_courier.progress.ProgressNotifierFactory
22+
import okhttp3.OkHttpClient
23+
import okhttp3.Request
24+
import java.io.IOException
25+
26+
private val TAG = BlobSender::class.java.name
27+
28+
private val logger = Logger(TAG)
29+
private fun li(m: String) = logger.i(m)
30+
31+
class BlobSender(
32+
private val context: Context,
33+
private val httpClient: OkHttpClient,
34+
private val progressNotifierFactory: ProgressNotifierFactory
35+
) {
36+
37+
fun send(senderParameters: SenderParameters): Result<Map<String, Any>> {
38+
li("Starting unmanaged send")
39+
40+
val requestBody = BlobCourierProgressRequest(
41+
senderParameters.toRequestBody(context.contentResolver),
42+
progressNotifierFactory.create(senderParameters.taskId)
43+
)
44+
45+
val requestBuilder = Request.Builder()
46+
.url(senderParameters.uri)
47+
.method(senderParameters.method, requestBody)
48+
.apply {
49+
senderParameters.headers.forEach { e: Map.Entry<String, String> ->
50+
addHeader(e.key, e.value)
51+
}
52+
}
53+
.build()
54+
55+
val sendRequestCall = httpClient.newCall(requestBuilder)
56+
57+
try {
58+
registerCancellationHandler(context, senderParameters.taskId, sendRequestCall)
59+
60+
val response = sendRequestCall.execute()
61+
62+
val responseBody = response.body()?.string().orEmpty()
63+
64+
li("Finished unmanaged send")
65+
66+
return Success(
67+
mapOf(
68+
"response" to mapOf(
69+
"code" to response.code(),
70+
"data" to if (senderParameters.returnResponse) responseBody else "",
71+
"headers" to mapHeadersToMap(response.headers())
72+
)
73+
)
74+
)
75+
} catch (e: IOException) {
76+
if (sendRequestCall.isCanceled) {
77+
return Failure(createErrorFromThrowable(ERROR_CANCELED_EXCEPTION, e))
78+
}
79+
80+
return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_EXCEPTION, e))
81+
} catch (e: Exception) {
82+
return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_EXCEPTION, e))
83+
} catch (e: Error) {
84+
return Failure(createErrorFromThrowable(ERROR_UNEXPECTED_ERROR, e))
85+
}
86+
}
87+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Copyright (c) Ely Deckers.
3+
*
4+
* This source code is licensed under the MPL-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
package io.deckers.blob_courier.send
8+
9+
import android.content.ContentResolver
10+
import android.net.Uri
11+
import com.facebook.react.bridge.ReadableMap
12+
import io.deckers.blob_courier.common.DEFAULT_MIME_TYPE
13+
import io.deckers.blob_courier.common.DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
14+
import io.deckers.blob_courier.common.DEFAULT_UPLOAD_METHOD
15+
import io.deckers.blob_courier.common.InputStreamRequestBody
16+
import io.deckers.blob_courier.common.PARAMETER_ABSOLUTE_FILE_PATH
17+
import io.deckers.blob_courier.common.PARAMETER_HEADERS
18+
import io.deckers.blob_courier.common.PARAMETER_METHOD
19+
import io.deckers.blob_courier.common.PARAMETER_MIME_TYPE
20+
import io.deckers.blob_courier.common.PARAMETER_SETTINGS_PROGRESS_INTERVAL
21+
import io.deckers.blob_courier.common.PARAMETER_TASK_ID
22+
import io.deckers.blob_courier.common.PARAMETER_URL
23+
import io.deckers.blob_courier.common.PROVIDED_PARAMETERS
24+
import io.deckers.blob_courier.common.ValidationResult
25+
import io.deckers.blob_courier.common.ValidationSuccess
26+
import io.deckers.blob_courier.common.filterHeaders
27+
import io.deckers.blob_courier.common.getMapInt
28+
import io.deckers.blob_courier.common.hasRequiredStringField
29+
import io.deckers.blob_courier.common.ifNone
30+
import io.deckers.blob_courier.common.isNotNull
31+
import io.deckers.blob_courier.common.maybe
32+
import io.deckers.blob_courier.common.right
33+
import io.deckers.blob_courier.common.testKeep
34+
import io.deckers.blob_courier.common.validationContext
35+
import io.deckers.blob_courier.upload.FilePayload
36+
import io.deckers.blob_courier.upload.StringPayload
37+
import io.deckers.blob_courier.upload.UploaderParameters
38+
import okhttp3.MediaType
39+
import okhttp3.MultipartBody
40+
import okhttp3.RequestBody
41+
import java.net.URL
42+
43+
private const val PARAMETER_RETURN_RESPONSE = "returnResponse"
44+
45+
data class RequiredParameters(
46+
val taskId: String,
47+
val url: String,
48+
val absoluteFilePath: String,
49+
)
50+
51+
data class SenderParameters(
52+
val absoluteFilePath: Uri,
53+
val mediaType: String,
54+
val method: String,
55+
val headers: Map<String, String>,
56+
val progressInterval: Int,
57+
val returnResponse: Boolean,
58+
val taskId: String,
59+
val uri: URL,
60+
)
61+
62+
fun SenderParameters.toRequestBody(contentResolver: ContentResolver): RequestBody =
63+
InputStreamRequestBody(
64+
mediaType.let(MediaType::parse)
65+
?: MediaType.get(DEFAULT_MIME_TYPE),
66+
contentResolver,
67+
absoluteFilePath
68+
)
69+
70+
private fun verifyRequiredParametersProvided(input: ReadableMap):
71+
ValidationResult<RequiredParameters> =
72+
validationContext(input, isNotNull(PROVIDED_PARAMETERS))
73+
.fmap(testKeep(hasRequiredStringField(PARAMETER_ABSOLUTE_FILE_PATH)))
74+
.fmap(testKeep(hasRequiredStringField(PARAMETER_TASK_ID)))
75+
.fmap(testKeep(hasRequiredStringField(PARAMETER_URL)))
76+
.fmap { (_, validatedParameters) ->
77+
val (url, rest) = validatedParameters
78+
val (taskId, rest2) = rest
79+
val (absoluteFilePath, _) = rest2
80+
81+
ValidationSuccess(RequiredParameters(taskId, url, absoluteFilePath))
82+
}
83+
84+
class SenderParameterFactory {
85+
fun fromInput(input: ReadableMap): ValidationResult<SenderParameters> =
86+
verifyRequiredParametersProvided(input)
87+
.fmap {
88+
val (taskId, url, absoluteFilePath) = it
89+
90+
val mediaType = maybe(input.getString(PARAMETER_MIME_TYPE)).ifNone(DEFAULT_MIME_TYPE)
91+
val method = maybe(input.getString(PARAMETER_METHOD)).ifNone(DEFAULT_UPLOAD_METHOD)
92+
93+
val unfilteredHeaders =
94+
input.getMap(PARAMETER_HEADERS)?.toHashMap() ?: emptyMap<String, Any>()
95+
96+
val headers = filterHeaders(unfilteredHeaders)
97+
98+
val returnResponse =
99+
input.hasKey(PARAMETER_RETURN_RESPONSE) && input.getBoolean(PARAMETER_RETURN_RESPONSE)
100+
101+
val progressInterval =
102+
getMapInt(
103+
input,
104+
PARAMETER_SETTINGS_PROGRESS_INTERVAL,
105+
DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
106+
)
107+
108+
right(SenderParameters(
109+
Uri.parse(absoluteFilePath),
110+
mediaType,
111+
method,
112+
headers,
113+
progressInterval,
114+
returnResponse,
115+
taskId,
116+
URL(url),
117+
))
118+
}
119+
}

android/src/main/java/io/deckers/blob_courier/upload/UploaderParameterFactory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import io.deckers.blob_courier.common.DEFAULT_MIME_TYPE
1515
import io.deckers.blob_courier.common.DEFAULT_PROGRESS_TIMEOUT_MILLISECONDS
1616
import io.deckers.blob_courier.common.DEFAULT_UPLOAD_METHOD
1717
import io.deckers.blob_courier.common.Either
18+
import io.deckers.blob_courier.common.InputStreamRequestBody
1819
import io.deckers.blob_courier.common.PARAMETER_ABSOLUTE_FILE_PATH
1920
import io.deckers.blob_courier.common.PARAMETER_FILENAME
2021
import io.deckers.blob_courier.common.PARAMETER_HEADERS

android/src/test/java/io/deckers/blob_courier/BlobCourierModuleTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import io.deckers.blob_courier.common.isNotNull
3131
import io.deckers.blob_courier.common.isNotNullOrEmptyString
3232
import io.deckers.blob_courier.common.validate
3333
import io.deckers.blob_courier.react.toReactMap
34-
import io.deckers.blob_courier.upload.InputStreamRequestBody
34+
import io.deckers.blob_courier.common.InputStreamRequestBody
3535
import io.deckers.blob_courier.upload.UploaderParameterFactory
3636
import io.deckers.blob_courier.upload.toMultipartBody
3737
import io.mockk.every

src/ExposedTypes.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ export declare interface BlobUploadRequest
104104
readonly multipartName?: string;
105105
}
106106

107+
export declare interface BlobSendRequest
108+
extends BlobBaseRequest,
109+
BlobRequestMimeType,
110+
BlobRequestMethod,
111+
BlobRequestReturnResponse {
112+
readonly absoluteFilePath: string;
113+
}
114+
107115
export declare interface BlobMultipartBaseRequest
108116
extends BlobBaseRequest,
109117
BlobRequestMethod,
@@ -162,4 +170,6 @@ export type BlobFetchInput = BlobFetchRequest &
162170
AndroidFetchSettings &
163171
IOSFetchSettings;
164172

173+
export type BlobSendInput = BlobSendRequest & BlobRequestSettings;
174+
165175
export type BlobUploadInput = BlobUploadRequest & BlobRequestSettings;

0 commit comments

Comments
 (0)