Skip to content
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
102 changes: 77 additions & 25 deletions android/src/main/java/com/nitrofs/FileDownloader.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nitrofs

import android.util.Log
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import io.ktor.client.HttpClient
import io.ktor.client.call.body
Expand All @@ -12,50 +13,101 @@ import io.ktor.http.isSuccess
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.io.File

class FileDownloader {
private val downloadScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val downloadJobs = ConcurrentHashMap<String, Job>()
private val progressCallbacks = ConcurrentHashMap<String, ((Double, Double) -> Unit)>()
private val fileDeferreds = ConcurrentHashMap<String, CompletableDeferred<NitroFile>>()

suspend fun downloadFile(
serverUrl: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
): NitroFile? {
var contentType = ""
): NitroDownloadResult {
val jobId = UUID.randomUUID().toString()
val outputFile = File(destinationPath)
outputFile.parentFile?.mkdirs()

val client = HttpClient(OkHttp)

val fileDeferred = CompletableDeferred<NitroFile>()
fileDeferreds[jobId] = fileDeferred

client.use { it
it.prepareGet(serverUrl) {
method = HttpMethod.Get
onDownload { totalBytesSent, contentLength ->
if (totalBytesSent > 0 && contentLength != null){
onProgress?.let {
withContext(Dispatchers.Main) {
onProgress.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
if (onProgress != null) {
progressCallbacks[jobId] = onProgress
}

val client = HttpClient(OkHttp)
val job = downloadScope.launch {
try {
client.use { httpClient ->
httpClient.prepareGet(serverUrl) {
method = HttpMethod.Get
onDownload { totalBytesSent, contentLength ->
if (totalBytesSent > 0 && contentLength != null) {
progressCallbacks[jobId]?.let { callback ->
withContext(Dispatchers.Main) {
callback.invoke(totalBytesSent.toDouble(), contentLength.toDouble())
}
}
}
}
}.execute { response ->
Log.d("TAG", "${response.status.isSuccess()} ${response.status.value} $serverUrl")
if (!response.status.isSuccess()) {
throw RuntimeException("HTTP ${response.status.value}: Failed to download file")
}
val contentType = response.headers["Content-Type"] ?: "application/octet-stream"
val channel: ByteReadChannel = response.body()
channel.copyAndClose(outputFile.writeChannel())

// Complete the deferred with the file
val nitroFile = NitroFile(
name = outputFile.name,
path = outputFile.absolutePath,
mimeType = contentType
)
fileDeferred.complete(nitroFile)
}
}
}.execute { response ->
Log.d("TAG", "${response.status.isSuccess()} ${response.status.value} $serverUrl")
if (!response.status.isSuccess()) {
throw RuntimeException("HTTP ${response.status.value}: Failed to download file")
}
contentType = response.headers["Content-Type"] ?: "application/octet-stream"
val channel: ByteReadChannel = response.body()
channel.copyAndClose(outputFile.writeChannel())
} catch (e: Exception) {
fileDeferred.completeExceptionally(e)
} finally {
downloadJobs.remove(jobId)
progressCallbacks.remove(jobId)
fileDeferreds.remove(jobId)
}
}

return NitroFile(
name = outputFile.name,
path = outputFile.absolutePath,
mimeType = contentType
)
downloadJobs[jobId] = job

// Wait for download to complete and get the file
val file = fileDeferred.await()

return NitroDownloadResult(jobId = jobId, file = file)
}

fun cancelDownload(jobId: String): Boolean {
val job = downloadJobs[jobId]
if (job != null) {
job.cancel()
downloadJobs.remove(jobId)
progressCallbacks.remove(jobId)
fileDeferreds[jobId]?.cancel()
fileDeferreds.remove(jobId)
return true
}
return false
}
}
27 changes: 25 additions & 2 deletions android/src/main/java/com/nitrofs/HybridNitroFS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise
import com.margelo.nitro.nitrofs.HybridNitroFSSpec
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import com.margelo.nitro.nitrofs.NitroFileEncoding
import com.margelo.nitro.nitrofs.NitroFileStat
Expand Down Expand Up @@ -180,7 +181,7 @@ class HybridNitroFS: HybridNitroFSSpec() {
file: NitroFile,
uploadOptions: NitroUploadOptions,
onProgress: ((Double, Double) -> Unit)?
): Promise<Unit> {
): Promise<String> {
return Promise.async(ioScope) {
try {
nitroFsImpl.uploadFile(file, uploadOptions, onProgress)
Expand All @@ -190,12 +191,23 @@ class HybridNitroFS: HybridNitroFSSpec() {
}
}
}

override fun cancelUpload(jobId: String): Promise<Boolean> {
return Promise.async {
try {
nitroFsImpl.cancelUpload(jobId)
} catch (e: Exception) {
Log.e(TAG, "Error cancelling upload: ${e.message}")
throw Error(e)
}
}
}

override fun downloadFile(
serverUrl: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
): Promise<NitroFile> {
): Promise<NitroDownloadResult> {
return Promise.async(ioScope) {
try {
nitroFsImpl.downloadFile(serverUrl, destinationPath, onProgress)
Expand All @@ -205,6 +217,17 @@ class HybridNitroFS: HybridNitroFSSpec() {
}
}
}

override fun cancelDownload(jobId: String): Promise<Boolean> {
return Promise.async {
try {
nitroFsImpl.cancelDownload(jobId)
} catch (e: Exception) {
Log.e(TAG, "Error cancelling download: ${e.message}")
throw Error(e)
}
}
}

companion object {
const val TAG = "NitroFS"
Expand Down
22 changes: 13 additions & 9 deletions android/src/main/java/com/nitrofs/NitroFSImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
import com.facebook.react.bridge.ReactApplicationContext
import com.margelo.nitro.nitrofs.NitroDownloadResult
import com.margelo.nitro.nitrofs.NitroFile
import com.margelo.nitro.nitrofs.NitroFileEncoding
import com.margelo.nitro.nitrofs.NitroFileStat
Expand Down Expand Up @@ -336,26 +337,29 @@ class NitroFSImpl(val context: ReactApplicationContext) {
suspend fun uploadFile(file: NitroFile,
uploadOptions: NitroUploadOptions,
onProgress: ((Double, Double) -> Unit)?
) {
): String {
val nitroFile = File(file.path)
nitroFileUploader.handleUpload(nitroFile, uploadOptions, onProgress)
return nitroFileUploader.handleUpload(nitroFile, uploadOptions, onProgress)
}

fun cancelUpload(jobId: String): Boolean {
return nitroFileUploader.cancelUpload(jobId)
}

suspend fun downloadFile(
serverUrl: String,
destinationPath: String,
onProgress: ((Double, Double) -> Unit)?
): NitroFile {
val file = fileDownloader.downloadFile(
): NitroDownloadResult {
return fileDownloader.downloadFile(
serverUrl,
destinationPath,
onProgress
)
if (file != null) {
return file
} else {
throw RuntimeException("Failed to download file from: $serverUrl")
}
}

fun cancelDownload(jobId: String): Boolean {
return fileDownloader.cancelDownload(jobId)
}

fun getFileEncoding(encoding: NitroFileEncoding): Charset {
Expand Down
82 changes: 59 additions & 23 deletions android/src/main/java/com/nitrofs/NitroFileUploader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,83 @@ import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.utils.io.streams.asInput
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.io.File

class NitroFileUploader {
private val uploadScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val uploadJobs = ConcurrentHashMap<String, Job>()
private val progressCallbacks = ConcurrentHashMap<String, ((Double, Double) -> Unit)>()

suspend fun handleUpload(
file: File,
uploadOptions: NitroUploadOptions,
onProgress: ((Double, Double) -> Unit)?
) {
): String {
val jobId = UUID.randomUUID().toString()
val totalBytes = file.length()
val client = HttpClient(OkHttp)

if (onProgress != null) {
progressCallbacks[jobId] = onProgress
}

client.use { it
it.submitFormWithBinaryData(
url = uploadOptions.url,
formData = formData {
appendInput(
key = uploadOptions.field ?: "file",
headers = Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"")
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.toString())
},
size = totalBytes,
val client = HttpClient(OkHttp)
val job = uploadScope.launch {
try {
client.use { httpClient ->
httpClient.submitFormWithBinaryData(
url = uploadOptions.url,
formData = formData {
appendInput(
key = uploadOptions.field ?: "file",
headers = Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"")
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.toString())
},
size = totalBytes,
) {
file.inputStream().asInput()
}
}
) {
file.inputStream().asInput()
}
}
){
method = getMethod(uploadOptions.method)
onUpload { totalBytesSent, totalBytes ->
if (totalBytesSent > 0 && totalBytes != null) {
onProgress?.let {
withContext(Dispatchers.Main) {
it.invoke(totalBytesSent.toDouble(), totalBytes.toDouble())
method = getMethod(uploadOptions.method)
onUpload { totalBytesSent, totalBytes ->
if (totalBytesSent > 0 && totalBytes != null) {
progressCallbacks[jobId]?.let { callback ->
withContext(Dispatchers.Main) {
callback.invoke(totalBytesSent.toDouble(), totalBytes.toDouble())
}
}
}
}
}
}
} finally {
uploadJobs.remove(jobId)
progressCallbacks.remove(jobId)
}
}

uploadJobs[jobId] = job
return jobId
}

fun cancelUpload(jobId: String): Boolean {
val job = uploadJobs[jobId]
if (job != null) {
job.cancel()
uploadJobs.remove(jobId)
progressCallbacks.remove(jobId)
return true
}
return false
}

fun getMethod(method: NitroUploadMethod?): HttpMethod {
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroFS (0.7.0):
- NitroFS (0.8.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2647,7 +2647,7 @@ SPEC CHECKSUMS:
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: e7491a2038f2618c8cd444ed411a6deb350a3742
NitroDocumentPicker: 3f7adcb535ed9ac19a92a65c7228da559227ffdb
NitroFS: 5d5ad45cd2351ea71bbdc7f5bb45857fc1219e08
NitroFS: 61a09bcd2314341d3ad3db444ba75a57e6facef6
NitroModules: edd5870885e786b0f2119836cf47e8b28d5b9c1f
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: 0735ab4f6b3ec93a7f98187b5da74d7916e2cf4c
Expand Down
Loading
Loading