diff --git a/android/src/main/java/com/nitrofs/FileDownloader.kt b/android/src/main/java/com/nitrofs/FileDownloader.kt index 9b9b4a5..635b903 100644 --- a/android/src/main/java/com/nitrofs/FileDownloader.kt +++ b/android/src/main/java/com/nitrofs/FileDownloader.kt @@ -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 @@ -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() + private val progressCallbacks = ConcurrentHashMap Unit)>() + private val fileDeferreds = ConcurrentHashMap>() + 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() + 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 } } \ No newline at end of file diff --git a/android/src/main/java/com/nitrofs/HybridNitroFS.kt b/android/src/main/java/com/nitrofs/HybridNitroFS.kt index a341839..e9d783d 100755 --- a/android/src/main/java/com/nitrofs/HybridNitroFS.kt +++ b/android/src/main/java/com/nitrofs/HybridNitroFS.kt @@ -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 @@ -180,7 +181,7 @@ class HybridNitroFS: HybridNitroFSSpec() { file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((Double, Double) -> Unit)? - ): Promise { + ): Promise { return Promise.async(ioScope) { try { nitroFsImpl.uploadFile(file, uploadOptions, onProgress) @@ -190,12 +191,23 @@ class HybridNitroFS: HybridNitroFSSpec() { } } } + + override fun cancelUpload(jobId: String): Promise { + 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 { + ): Promise { return Promise.async(ioScope) { try { nitroFsImpl.downloadFile(serverUrl, destinationPath, onProgress) @@ -205,6 +217,17 @@ class HybridNitroFS: HybridNitroFSSpec() { } } } + + override fun cancelDownload(jobId: String): Promise { + 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" diff --git a/android/src/main/java/com/nitrofs/NitroFSImpl.kt b/android/src/main/java/com/nitrofs/NitroFSImpl.kt index 5790205..ca1d69f 100644 --- a/android/src/main/java/com/nitrofs/NitroFSImpl.kt +++ b/android/src/main/java/com/nitrofs/NitroFSImpl.kt @@ -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 @@ -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 { diff --git a/android/src/main/java/com/nitrofs/NitroFileUploader.kt b/android/src/main/java/com/nitrofs/NitroFileUploader.kt index 244852d..7a4d04e 100644 --- a/android/src/main/java/com/nitrofs/NitroFileUploader.kt +++ b/android/src/main/java/com/nitrofs/NitroFileUploader.kt @@ -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() + private val progressCallbacks = ConcurrentHashMap 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 { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4b4305f..99052f7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,7 +38,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - NitroFS (0.7.0): + - NitroFS (0.8.0): - boost - DoubleConversion - fast_float @@ -2647,7 +2647,7 @@ SPEC CHECKSUMS: glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: e7491a2038f2618c8cd444ed411a6deb350a3742 NitroDocumentPicker: 3f7adcb535ed9ac19a92a65c7228da559227ffdb - NitroFS: 5d5ad45cd2351ea71bbdc7f5bb45857fc1219e08 + NitroFS: 61a09bcd2314341d3ad3db444ba75a57e6facef6 NitroModules: edd5870885e786b0f2119836cf47e8b28d5b9c1f RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 0735ab4f6b3ec93a7f98187b5da74d7916e2cf4c diff --git a/ios/HybridNitroFs.swift b/ios/HybridNitroFs.swift index 7e85b64..4a2a809 100755 --- a/ios/HybridNitroFs.swift +++ b/ios/HybridNitroFs.swift @@ -160,10 +160,10 @@ class HybridNitroFS: HybridNitroFSSpec { file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((_ uploadedBytes: Double, _ totalBytes: Double) -> Void)? - ) throws -> Promise{ + ) throws -> Promise{ return .async { [unowned self] in do { - try await self.nitroFSImpl.uploadFile( + return try await self.nitroFSImpl.uploadFile( file: file, uploadOptions: uploadOptions, onProgress: onProgress @@ -175,7 +175,13 @@ class HybridNitroFS: HybridNitroFSSpec { } } - func downloadFile(serverUrl: String, destinationPath: String, onProgress: ((Double, Double) -> Void)?) throws -> NitroModules.Promise { + func cancelUpload(jobId: String) throws -> Promise { + return .async { [unowned self] in + return self.nitroFSImpl.cancelUpload(jobId: jobId) + } + } + + func downloadFile(serverUrl: String, destinationPath: String, onProgress: ((Double, Double) -> Void)?) throws -> NitroModules.Promise { return .async { [unowned self] in do { return try await self.nitroFSImpl.downloadFile( @@ -184,10 +190,16 @@ class HybridNitroFS: HybridNitroFSSpec { onProgress: onProgress ) } catch { - os_log("failed to upload file: \(error.localizedDescription)") + os_log("failed to download file: \(error.localizedDescription)") throw error } } } + + func cancelDownload(jobId: String) throws -> NitroModules.Promise { + return .async { [unowned self] in + return self.nitroFSImpl.cancelDownload(jobId: jobId) + } + } } diff --git a/ios/NitroFSFileDownloader.swift b/ios/NitroFSFileDownloader.swift index 55a1110..76ad99f 100644 --- a/ios/NitroFSFileDownloader.swift +++ b/ios/NitroFSFileDownloader.swift @@ -6,13 +6,17 @@ // import Foundation +import NitroModules final class NitroFSFileDownloader: NSObject { private weak var fileManager: FileManager? - private var downloadTask: URLSessionDownloadTask? - private var onProgress: ((Double, Double) -> Void)? - private var continuation: CheckedContinuation? - private var destinationPath: String? + private var downloadTasks: [String: URLSessionDownloadTask] = [:] + private var downloadSessions: [String: URLSession] = [:] + private var progressCallbacks: [String: ((Double, Double) -> Void)] = [:] + private var destinationPaths: [String: String] = [:] + private var taskToJobId: [Int: String] = [:] + private var fileContinuations: [String: CheckedContinuation] = [:] + private let taskQueue = DispatchQueue(label: "com.nitrofs.downloader") init(fileManager: FileManager) { self.fileManager = fileManager @@ -23,13 +27,12 @@ final class NitroFSFileDownloader: NSObject { _ serverUrl: String, _ destinationPath: String, onProgress: ((Double, Double) -> Void)? = nil - ) async throws -> NitroFile { + ) async throws -> NitroDownloadResult { guard fileManager != nil else { throw NitroFSError.unavailable(message: "FileManager is not available") } - self.onProgress = onProgress - self.destinationPath = destinationPath + let jobId = UUID().uuidString let request = try makeRequest(serverUrl: serverUrl) @@ -38,17 +41,50 @@ final class NitroFSFileDownloader: NSObject { config.requestCachePolicy = .reloadIgnoringLocalCacheData return URLSession(configuration: config, delegate: self, delegateQueue: .main) }() - - return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation - downloadTask = session.downloadTask(with: request) - downloadTask?.resume() + let downloadTask = session.downloadTask(with: request) + + let file = try await withCheckedThrowingContinuation { continuation in + // Use sync to ensure state is initialized before downloadTask.resume() is called + // This prevents race condition where delegate callbacks fire before state is set up + taskQueue.sync { + self.downloadTasks[jobId] = downloadTask + self.downloadSessions[jobId] = session + self.taskToJobId[downloadTask.taskIdentifier] = jobId + self.destinationPaths[jobId] = destinationPath + self.fileContinuations[jobId] = continuation + if let onProgress = onProgress { + self.progressCallbacks[jobId] = onProgress + } + } + downloadTask.resume() } + + return NitroDownloadResult(jobId: jobId, file: file) } - func cancelDownload() { - downloadTask?.cancel() + func cancelDownload(jobId: String) -> Bool { + var cancelled = false + taskQueue.sync { [weak self] in + guard let self = self else { return } + if let task = self.downloadTasks[jobId] { + task.cancel() + self.taskToJobId.removeValue(forKey: task.taskIdentifier) + self.downloadTasks.removeValue(forKey: jobId) + self.progressCallbacks.removeValue(forKey: jobId) + self.destinationPaths.removeValue(forKey: jobId) + if let continuation = self.fileContinuations[jobId] { + continuation.resume(throwing: NitroFSError.networkError(message: "Download cancelled")) + self.fileContinuations.removeValue(forKey: jobId) + } + cancelled = true + } + if let session = self.downloadSessions[jobId] { + session.invalidateAndCancel() + self.downloadSessions.removeValue(forKey: jobId) + } + } + return cancelled } // MARK: - Private Methods @@ -69,7 +105,7 @@ final class NitroFSFileDownloader: NSObject { location: URL, response: URLResponse, downloadTask: URLSessionDownloadTask - ) throws -> NitroFile { + ) throws -> NitroFile? { guard let fileManager else { throw NitroFSError.unavailable(message: "FileManager is not available") } @@ -82,8 +118,17 @@ final class NitroFSFileDownloader: NSObject { throw NitroFSError.networkError(message: "HTTP Error: \(response.statusCode)") } - guard let destinationPath = self.destinationPath else { - throw NitroFSError.networkError(message: "Destination path not set") + var jobId: String? + var destinationPath: String? + + taskQueue.sync { + jobId = self.taskToJobId[downloadTask.taskIdentifier] + destinationPath = jobId.flatMap { self.destinationPaths[$0] } + } + + guard let destinationPath = destinationPath else { + // Download was cancelled or jobId not found + return nil } let destinationURL = URL(fileURLWithPath: destinationPath) @@ -112,7 +157,22 @@ extension NitroFSFileDownloader: URLSessionDownloadDelegate { downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { - guard let continuation = self.continuation else { return } + var jobId: String? + var continuation: CheckedContinuation? + + taskQueue.sync { + jobId = self.taskToJobId[downloadTask.taskIdentifier] + if let jobId = jobId { + continuation = self.fileContinuations[jobId] + } + } + + guard let jobId = jobId, let continuation = continuation else { return } + + // Remove continuation immediately to prevent double-resume from didCompleteWithError + taskQueue.sync { + self.fileContinuations.removeValue(forKey: jobId) + } do { let file = try handleDownloadCompletion( @@ -120,11 +180,23 @@ extension NitroFSFileDownloader: URLSessionDownloadDelegate { response: downloadTask.response ?? URLResponse(), downloadTask: downloadTask ) - continuation.resume(returning: file) + if let file = file { + continuation.resume(returning: file) + } else { + continuation.resume(throwing: NitroFSError.networkError(message: "Download was cancelled")) + } } catch { continuation.resume(throwing: error) } - self.continuation = nil + + taskQueue.async { + self.downloadTasks.removeValue(forKey: jobId) + self.downloadSessions.removeValue(forKey: jobId) + self.progressCallbacks.removeValue(forKey: jobId) + self.destinationPaths.removeValue(forKey: jobId) + self.taskToJobId.removeValue(forKey: downloadTask.taskIdentifier) + } + session.finishTasksAndInvalidate() } @@ -136,8 +208,16 @@ extension NitroFSFileDownloader: URLSessionDownloadDelegate { totalBytesExpectedToWrite: Int64 ) { guard totalBytesExpectedToWrite > 0 else { return } - DispatchQueue.main.async { [weak self] in - self?.onProgress?(Double(totalBytesWritten), Double(totalBytesExpectedToWrite)) + + taskQueue.async { [weak self] in + guard let self = self, + let jobId = self.taskToJobId[downloadTask.taskIdentifier], + let onProgress = self.progressCallbacks[jobId] else { + return + } + DispatchQueue.main.async { + onProgress(Double(totalBytesWritten), Double(totalBytesExpectedToWrite)) + } } } @@ -146,9 +226,32 @@ extension NitroFSFileDownloader: URLSessionDownloadDelegate { task: URLSessionTask, didCompleteWithError error: Error? ) { - if let error { - continuation?.resume(throwing: error) - continuation = nil + var jobId: String? + var continuation: CheckedContinuation? + + taskQueue.sync { + jobId = self.taskToJobId[task.taskIdentifier] + if let jobId = jobId { + continuation = self.fileContinuations[jobId] + } + } + + guard let jobId = jobId else { return } + + if let error = error, let continuation = continuation { + // Remove continuation immediately to prevent double-resume + taskQueue.sync { + self.fileContinuations.removeValue(forKey: jobId) + } + + continuation.resume(throwing: error) + taskQueue.async { + self.downloadTasks.removeValue(forKey: jobId) + self.downloadSessions.removeValue(forKey: jobId) + self.progressCallbacks.removeValue(forKey: jobId) + self.destinationPaths.removeValue(forKey: jobId) + self.taskToJobId.removeValue(forKey: task.taskIdentifier) + } session.finishTasksAndInvalidate() } } diff --git a/ios/NitroFSFileUploader.swift b/ios/NitroFSFileUploader.swift index 0a88c7b..f29664c 100644 --- a/ios/NitroFSFileUploader.swift +++ b/ios/NitroFSFileUploader.swift @@ -9,7 +9,12 @@ import Foundation final class NitroFSFileUploader: NSObject, URLSessionDataDelegate { weak var fileManager: FileManager? - private var onProgress: ((Double, Double) -> Void)? + private var uploadTasks: [String: URLSessionUploadTask] = [:] + private var uploadSessions: [String: URLSession] = [:] + private var progressCallbacks: [String: ((Double, Double) -> Void)] = [:] + private var taskToJobId: [Int: String] = [:] + private var uploadContinuations: [String: CheckedContinuation] = [:] + private let taskQueue = DispatchQueue(label: "com.nitrofs.uploader") init(fileManager: FileManager) { super.init() @@ -20,13 +25,13 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate { file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((Double, Double) -> Void)? = nil - ) async throws { - self.onProgress = onProgress + ) async throws -> String { guard let uploadURL = URL(string: uploadOptions.url) else { throw NitroFSError.networkError(message: "Invalid URL") } + let jobId = UUID().uuidString let fieldName = uploadOptions.field ?? "file" let boundary = UUID().uuidString let fileURL = URL(fileURLWithPath: file.path) @@ -42,31 +47,55 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate { request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - return try await withCheckedThrowingContinuation { [weak self] continuation in - let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) + let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) - let task = session.uploadTask(with: request, fromFile: multipartFile) { _, response, error in - defer { - try? FileManager.default.removeItem(at: multipartFile) - session.finishTasksAndInvalidate() - } + let task = session.uploadTask(with: request, fromFile: multipartFile) - if let error = error { - continuation.resume(throwing: error) - return - } - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - continuation.resume(throwing: NitroFSError.networkError(message: "Invalid server response")) - return + // Use sync to ensure state is initialized before task.resume() is called + // This prevents race condition where delegate callbacks fire before state is set up + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + taskQueue.sync { + self.uploadTasks[jobId] = task + self.uploadSessions[jobId] = session + self.taskToJobId[task.taskIdentifier] = jobId + self.uploadContinuations[jobId] = continuation + if let onProgress = onProgress { + self.progressCallbacks[jobId] = onProgress } - - continuation.resume() } - task.resume() } + + // Cleanup multipart file after completion + defer { + try? FileManager.default.removeItem(at: multipartFile) + } + + return jobId + } + + func cancelUpload(jobId: String) -> Bool { + var cancelled = false + taskQueue.sync { [weak self] in + guard let self = self else { return } + if let task = self.uploadTasks[jobId] { + task.cancel() + // Resume continuation with cancellation error if it exists + if let continuation = self.uploadContinuations[jobId] { + continuation.resume(throwing: NitroFSError.networkError(message: "Upload cancelled")) + self.uploadContinuations.removeValue(forKey: jobId) + } + self.taskToJobId.removeValue(forKey: task.taskIdentifier) + self.uploadTasks.removeValue(forKey: jobId) + self.progressCallbacks.removeValue(forKey: jobId) + cancelled = true + } + if let session = self.uploadSessions[jobId] { + session.invalidateAndCancel() + self.uploadSessions.removeValue(forKey: jobId) + } + } + return cancelled } } @@ -137,10 +166,71 @@ extension NitroFSFileUploader { extension NitroFSFileUploader: URLSessionTaskDelegate { func urlSession(_ _: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - DispatchQueue.main.async { - self.onProgress?(Double(totalBytesSent), Double(totalBytesExpectedToSend)) + taskQueue.async { [weak self] in + guard let self = self, + let jobId = self.taskToJobId[task.taskIdentifier], + let onProgress = self.progressCallbacks[jobId] else { + return + } + DispatchQueue.main.async { + onProgress(Double(totalBytesSent), Double(totalBytesExpectedToSend)) + } } } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error? + ) { + var jobId: String? + var continuation: CheckedContinuation? + + taskQueue.sync { + jobId = self.taskToJobId[task.taskIdentifier] + if let jobId = jobId { + continuation = self.uploadContinuations[jobId] + } + } + + guard let jobId = jobId else { return } + + // Remove continuation immediately to prevent double-resume + taskQueue.sync { + self.uploadContinuations.removeValue(forKey: jobId) + } + + if let error = error { + // Handle cancellation separately + if (error as NSError).code == NSURLErrorCancelled { + continuation?.resume(throwing: NitroFSError.networkError(message: "Upload cancelled")) + } else { + continuation?.resume(throwing: error) + } + } else { + // Check HTTP response status + if let httpResponse = task.response as? HTTPURLResponse { + guard (200...299).contains(httpResponse.statusCode) else { + continuation?.resume(throwing: NitroFSError.networkError( + message: "Upload failed with status code: \(httpResponse.statusCode)" + )) + return + } + } + // Success - resume continuation + continuation?.resume() + } + + // Cleanup + taskQueue.async { + self.uploadTasks.removeValue(forKey: jobId) + self.uploadSessions.removeValue(forKey: jobId) + self.progressCallbacks.removeValue(forKey: jobId) + self.taskToJobId.removeValue(forKey: task.taskIdentifier) + } + + session.finishTasksAndInvalidate() + } } // MARK: - OutputStream Extension diff --git a/ios/NitroFSImpl.swift b/ios/NitroFSImpl.swift index 3fbb3db..13c5bf8 100644 --- a/ios/NitroFSImpl.swift +++ b/ios/NitroFSImpl.swift @@ -6,6 +6,7 @@ // import Foundation +import NitroModules import os class NitroFSImpl { @@ -204,38 +205,53 @@ class NitroFSImpl { return pathURL.lastPathComponent } + private var fileUploader: NitroFSFileUploader? + private var fileDownloader: NitroFSFileDownloader? + func uploadFile( file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((_ uploadedBytes: Double, _ totalBytes: Double) -> Void)? - ) async throws { + ) async throws -> String { guard let fileManager else { throw NitroFSError.unavailable(message: "Failed to upload file. FileManager is unavailable") } - let fileUploader = NitroFSFileUploader(fileManager: fileManager) - try await fileUploader.uploadFile( + if fileUploader == nil { + fileUploader = NitroFSFileUploader(fileManager: fileManager) + } + return try await fileUploader!.uploadFile( file: file, uploadOptions: uploadOptions, onProgress: onProgress ) } + func cancelUpload(jobId: String) -> Bool { + return fileUploader?.cancelUpload(jobId: jobId) ?? false + } + func downloadFile( serverUrl: String, destinationPath: String, onProgress: ((Double, Double) -> Void)? - ) async throws -> NitroFile { + ) async throws -> NitroDownloadResult { guard let fileManager else { throw NitroFSError.unavailable(message: "Failed to download file. FileManager is unavailable") } - let fileDownloader = NitroFSFileDownloader(fileManager: fileManager) - return try await fileDownloader.downloadFile( + if fileDownloader == nil { + fileDownloader = NitroFSFileDownloader(fileManager: fileManager) + } + return try await fileDownloader!.downloadFile( serverUrl, destinationPath, onProgress: onProgress ) } + func cancelDownload(jobId: String) -> Bool { + return fileDownloader?.cancelDownload(jobId: jobId) ?? false + } + private func getEncoding(nitroEncoding: NitroFileEncoding) -> String.Encoding { switch(nitroEncoding) { case .utf8: diff --git a/nitrogen/generated/android/c++/JHybridNitroFSSpec.cpp b/nitrogen/generated/android/c++/JHybridNitroFSSpec.cpp index b85cb2d..778e2a5 100644 --- a/nitrogen/generated/android/c++/JHybridNitroFSSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridNitroFSSpec.cpp @@ -11,6 +11,8 @@ namespace margelo::nitro::nitrofs { struct NitroFileStat; } // Forward declaration of `NitroFile` to properly resolve imports. namespace margelo::nitro::nitrofs { struct NitroFile; } +// Forward declaration of `NitroDownloadResult` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroDownloadResult; } // Forward declaration of `NitroFileEncoding` to properly resolve imports. namespace margelo::nitro::nitrofs { enum class NitroFileEncoding; } // Forward declaration of `NitroUploadOptions` to properly resolve imports. @@ -26,6 +28,8 @@ namespace margelo::nitro::nitrofs { enum class NitroUploadMethod; } #include "NitroFile.hpp" #include #include "JNitroFile.hpp" +#include "NitroDownloadResult.hpp" +#include "JNitroDownloadResult.hpp" #include "NitroFileEncoding.hpp" #include "JNitroFileEncoding.hpp" #include "NitroUploadOptions.hpp" @@ -287,13 +291,14 @@ namespace margelo::nitro::nitrofs { auto __result = method(_javaPart, jni::make_jstring(path)); return __result->toStdString(); } - std::shared_ptr> JHybridNitroFSSpec::uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) { + std::shared_ptr> JHybridNitroFSSpec::uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) { static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* file */, jni::alias_ref /* uploadOptions */, jni::alias_ref /* onProgress */)>("uploadFile_cxx"); auto __result = method(_javaPart, JNitroFile::fromCpp(file), JNitroUploadOptions::fromCpp(uploadOptions), onProgress.has_value() ? JFunc_void_double_double_cxx::fromCpp(onProgress.value()) : nullptr); return [&]() { - auto __promise = Promise::create(); - __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) { - __promise->resolve(); + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); }); __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { jni::JniException __jniError(__throwable); @@ -302,13 +307,29 @@ namespace margelo::nitro::nitrofs { return __promise; }(); } - std::shared_ptr> JHybridNitroFSSpec::downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) { + std::shared_ptr> JHybridNitroFSSpec::cancelUpload(const std::string& jobId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* jobId */)>("cancelUpload"); + auto __result = method(_javaPart, jni::make_jstring(jobId)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNitroFSSpec::downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) { static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* serverUrl */, jni::alias_ref /* destinationPath */, jni::alias_ref /* onProgress */)>("downloadFile_cxx"); auto __result = method(_javaPart, jni::make_jstring(serverUrl), jni::make_jstring(destinationPath), onProgress.has_value() ? JFunc_void_double_double_cxx::fromCpp(onProgress.value()) : nullptr); return [&]() { - auto __promise = Promise::create(); + auto __promise = Promise::create(); __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { - auto __result = jni::static_ref_cast(__boxedResult); + auto __result = jni::static_ref_cast(__boxedResult); __promise->resolve(__result->toCpp()); }); __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { @@ -318,5 +339,21 @@ namespace margelo::nitro::nitrofs { return __promise; }(); } + std::shared_ptr> JHybridNitroFSSpec::cancelDownload(const std::string& jobId) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* jobId */)>("cancelDownload"); + auto __result = method(_javaPart, jni::make_jstring(jobId)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(static_cast(__result->value())); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } } // namespace margelo::nitro::nitrofs diff --git a/nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp b/nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp index 0a5762f..b76713a 100644 --- a/nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridNitroFSSpec.hpp @@ -74,8 +74,10 @@ namespace margelo::nitro::nitrofs { std::string dirname(const std::string& path) override; std::string basename(const std::string& path) override; std::string extname(const std::string& path) override; - std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) override; - std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) override; + std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) override; + std::shared_ptr> cancelUpload(const std::string& jobId) override; + std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) override; + std::shared_ptr> cancelDownload(const std::string& jobId) override; private: friend HybridBase; diff --git a/nitrogen/generated/android/c++/JNitroDownloadResult.hpp b/nitrogen/generated/android/c++/JNitroDownloadResult.hpp new file mode 100644 index 0000000..9448904 --- /dev/null +++ b/nitrogen/generated/android/c++/JNitroDownloadResult.hpp @@ -0,0 +1,63 @@ +/// +/// JNitroDownloadResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "NitroDownloadResult.hpp" + +#include "JNitroFile.hpp" +#include "NitroFile.hpp" +#include + +namespace margelo::nitro::nitrofs { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "NitroDownloadResult" and the the Kotlin data class "NitroDownloadResult". + */ + struct JNitroDownloadResult final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitrofs/NitroDownloadResult;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct NitroDownloadResult by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + NitroDownloadResult toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldJobId = clazz->getField("jobId"); + jni::local_ref jobId = this->getFieldValue(fieldJobId); + static const auto fieldFile = clazz->getField("file"); + jni::local_ref file = this->getFieldValue(fieldFile); + return NitroDownloadResult( + jobId->toStdString(), + file->toCpp() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const NitroDownloadResult& value) { + using JSignature = JNitroDownloadResult(jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.jobId), + JNitroFile::fromCpp(value.file) + ); + } + }; + +} // namespace margelo::nitro::nitrofs diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt index 822f7b2..a55a9df 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/HybridNitroFSSpec.kt @@ -128,23 +128,31 @@ abstract class HybridNitroFSSpec: HybridObject() { @Keep abstract fun extname(path: String): String - abstract fun uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((uploadedBytes: Double, totalBytes: Double) -> Unit)?): Promise + abstract fun uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((uploadedBytes: Double, totalBytes: Double) -> Unit)?): Promise @DoNotStrip @Keep - private fun uploadFile_cxx(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: Func_void_double_double?): Promise { + private fun uploadFile_cxx(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: Func_void_double_double?): Promise { val __result = uploadFile(file, uploadOptions, onProgress?.let { it }) return __result } - abstract fun downloadFile(serverUrl: String, destinationPath: String, onProgress: ((downloadedBytes: Double, totalBytes: Double) -> Unit)?): Promise + @DoNotStrip + @Keep + abstract fun cancelUpload(jobId: String): Promise + + abstract fun downloadFile(serverUrl: String, destinationPath: String, onProgress: ((downloadedBytes: Double, totalBytes: Double) -> Unit)?): Promise @DoNotStrip @Keep - private fun downloadFile_cxx(serverUrl: String, destinationPath: String, onProgress: Func_void_double_double?): Promise { + private fun downloadFile_cxx(serverUrl: String, destinationPath: String, onProgress: Func_void_double_double?): Promise { val __result = downloadFile(serverUrl, destinationPath, onProgress?.let { it }) return __result } + + @DoNotStrip + @Keep + abstract fun cancelDownload(jobId: String): Promise private external fun initHybrid(): HybridData diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/NitroDownloadResult.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/NitroDownloadResult.kt new file mode 100644 index 0000000..8ae20ac --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofs/NitroDownloadResult.kt @@ -0,0 +1,41 @@ +/// +/// NitroDownloadResult.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitrofs + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "NitroDownloadResult". + */ +@DoNotStrip +@Keep +data class NitroDownloadResult( + @DoNotStrip + @Keep + val jobId: String, + @DoNotStrip + @Keep + val file: NitroFile +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(jobId: String, file: NitroFile): NitroDownloadResult { + return NitroDownloadResult(jobId, file) + } + } +} diff --git a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.cpp b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.cpp index 961ca67..ae5bbeb 100644 --- a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.cpp +++ b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.cpp @@ -69,10 +69,10 @@ namespace margelo::nitro::nitrofs::bridge::swift { }; } - // pragma MARK: std::function - Func_void_NitroFile create_Func_void_NitroFile(void* NON_NULL swiftClosureWrapper) noexcept { - auto swiftClosure = NitroFS::Func_void_NitroFile::fromUnsafe(swiftClosureWrapper); - return [swiftClosure = std::move(swiftClosure)](const NitroFile& result) mutable -> void { + // pragma MARK: std::function + Func_void_NitroDownloadResult create_Func_void_NitroDownloadResult(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroFS::Func_void_NitroDownloadResult::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const NitroDownloadResult& result) mutable -> void { swiftClosure.call(result); }; } diff --git a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.hpp index 5ad6404..70a573b 100644 --- a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Bridge.hpp @@ -10,6 +10,8 @@ // Forward declarations of C++ defined types // Forward declaration of `HybridNitroFSSpec` to properly resolve imports. namespace margelo::nitro::nitrofs { class HybridNitroFSSpec; } +// Forward declaration of `NitroDownloadResult` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroDownloadResult; } // Forward declaration of `NitroFileStat` to properly resolve imports. namespace margelo::nitro::nitrofs { struct NitroFileStat; } // Forward declaration of `NitroFile` to properly resolve imports. @@ -23,6 +25,7 @@ namespace NitroFS { class HybridNitroFSSpec_cxx; } // Include C++ defined types #include "HybridNitroFSSpec.hpp" +#include "NitroDownloadResult.hpp" #include "NitroFile.hpp" #include "NitroFileStat.hpp" #include "NitroUploadMethod.hpp" @@ -312,38 +315,38 @@ namespace margelo::nitro::nitrofs::bridge::swift { return *optional; } - // pragma MARK: std::shared_ptr> + // pragma MARK: std::shared_ptr> /** - * Specialized version of `std::shared_ptr>`. + * Specialized version of `std::shared_ptr>`. */ - using std__shared_ptr_Promise_NitroFile__ = std::shared_ptr>; - inline std::shared_ptr> create_std__shared_ptr_Promise_NitroFile__() noexcept { - return Promise::create(); + using std__shared_ptr_Promise_NitroDownloadResult__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_NitroDownloadResult__() noexcept { + return Promise::create(); } - inline PromiseHolder wrap_std__shared_ptr_Promise_NitroFile__(std::shared_ptr> promise) noexcept { - return PromiseHolder(std::move(promise)); + inline PromiseHolder wrap_std__shared_ptr_Promise_NitroDownloadResult__(std::shared_ptr> promise) noexcept { + return PromiseHolder(std::move(promise)); } - // pragma MARK: std::function + // pragma MARK: std::function /** - * Specialized version of `std::function`. + * Specialized version of `std::function`. */ - using Func_void_NitroFile = std::function; + using Func_void_NitroDownloadResult = std::function; /** - * Wrapper class for a `std::function`, this can be used from Swift. + * Wrapper class for a `std::function`, this can be used from Swift. */ - class Func_void_NitroFile_Wrapper final { + class Func_void_NitroDownloadResult_Wrapper final { public: - explicit Func_void_NitroFile_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} - inline void call(NitroFile result) const noexcept { + explicit Func_void_NitroDownloadResult_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(NitroDownloadResult result) const noexcept { _function->operator()(result); } private: - std::unique_ptr> _function; + std::unique_ptr> _function; } SWIFT_NONCOPYABLE; - Func_void_NitroFile create_Func_void_NitroFile(void* NON_NULL swiftClosureWrapper) noexcept; - inline Func_void_NitroFile_Wrapper wrap_Func_void_NitroFile(Func_void_NitroFile value) noexcept { - return Func_void_NitroFile_Wrapper(std::move(value)); + Func_void_NitroDownloadResult create_Func_void_NitroDownloadResult(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_NitroDownloadResult_Wrapper wrap_Func_void_NitroDownloadResult(Func_void_NitroDownloadResult value) noexcept { + return Func_void_NitroDownloadResult_Wrapper(std::move(value)); } // pragma MARK: std::optional> @@ -427,13 +430,13 @@ namespace margelo::nitro::nitrofs::bridge::swift { return Result::withError(error); } - // pragma MARK: Result>> - using Result_std__shared_ptr_Promise_NitroFile___ = Result>>; - inline Result_std__shared_ptr_Promise_NitroFile___ create_Result_std__shared_ptr_Promise_NitroFile___(const std::shared_ptr>& value) noexcept { - return Result>>::withValue(value); + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_NitroDownloadResult___ = Result>>; + inline Result_std__shared_ptr_Promise_NitroDownloadResult___ create_Result_std__shared_ptr_Promise_NitroDownloadResult___(const std::shared_ptr>& value) noexcept { + return Result>>::withValue(value); } - inline Result_std__shared_ptr_Promise_NitroFile___ create_Result_std__shared_ptr_Promise_NitroFile___(const std::exception_ptr& error) noexcept { - return Result>>::withError(error); + inline Result_std__shared_ptr_Promise_NitroDownloadResult___ create_Result_std__shared_ptr_Promise_NitroDownloadResult___(const std::exception_ptr& error) noexcept { + return Result>>::withError(error); } } // namespace margelo::nitro::nitrofs::bridge::swift diff --git a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Umbrella.hpp index 16ec4d2..0b034b0 100644 --- a/nitrogen/generated/ios/NitroFS-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/NitroFS-Swift-Cxx-Umbrella.hpp @@ -10,6 +10,8 @@ // Forward declarations of C++ defined types // Forward declaration of `HybridNitroFSSpec` to properly resolve imports. namespace margelo::nitro::nitrofs { class HybridNitroFSSpec; } +// Forward declaration of `NitroDownloadResult` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroDownloadResult; } // Forward declaration of `NitroFileEncoding` to properly resolve imports. namespace margelo::nitro::nitrofs { enum class NitroFileEncoding; } // Forward declaration of `NitroFileStat` to properly resolve imports. @@ -23,6 +25,7 @@ namespace margelo::nitro::nitrofs { struct NitroUploadOptions; } // Include C++ defined types #include "HybridNitroFSSpec.hpp" +#include "NitroDownloadResult.hpp" #include "NitroFile.hpp" #include "NitroFileEncoding.hpp" #include "NitroFileStat.hpp" diff --git a/nitrogen/generated/ios/c++/HybridNitroFSSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridNitroFSSpecSwift.hpp index 1ffb5e4..b0ad5c3 100644 --- a/nitrogen/generated/ios/c++/HybridNitroFSSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridNitroFSSpecSwift.hpp @@ -22,6 +22,8 @@ namespace margelo::nitro::nitrofs { struct NitroFile; } namespace margelo::nitro::nitrofs { struct NitroUploadOptions; } // Forward declaration of `NitroUploadMethod` to properly resolve imports. namespace margelo::nitro::nitrofs { enum class NitroUploadMethod; } +// Forward declaration of `NitroDownloadResult` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroDownloadResult; } #include #include @@ -33,6 +35,7 @@ namespace margelo::nitro::nitrofs { enum class NitroUploadMethod; } #include "NitroUploadMethod.hpp" #include #include +#include "NitroDownloadResult.hpp" #include "NitroFS-Swift-Cxx-Umbrella.hpp" @@ -213,7 +216,7 @@ namespace margelo::nitro::nitrofs { auto __value = std::move(__result.value()); return __value; } - inline std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) override { + inline std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) override { auto __result = _swiftPart.uploadFile(std::forward(file), std::forward(uploadOptions), onProgress); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); @@ -221,7 +224,15 @@ namespace margelo::nitro::nitrofs { auto __value = std::move(__result.value()); return __value; } - inline std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) override { + inline std::shared_ptr> cancelUpload(const std::string& jobId) override { + auto __result = _swiftPart.cancelUpload(jobId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) override { auto __result = _swiftPart.downloadFile(serverUrl, destinationPath, onProgress); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); @@ -229,6 +240,14 @@ namespace margelo::nitro::nitrofs { auto __value = std::move(__result.value()); return __value; } + inline std::shared_ptr> cancelDownload(const std::string& jobId) override { + auto __result = _swiftPart.cancelDownload(jobId); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } private: NitroFS::HybridNitroFSSpec_cxx _swiftPart; diff --git a/nitrogen/generated/ios/swift/Func_void_NitroFile.swift b/nitrogen/generated/ios/swift/Func_void_NitroDownloadResult.swift similarity index 61% rename from nitrogen/generated/ios/swift/Func_void_NitroFile.swift rename to nitrogen/generated/ios/swift/Func_void_NitroDownloadResult.swift index 61c0bfd..64c203b 100644 --- a/nitrogen/generated/ios/swift/Func_void_NitroFile.swift +++ b/nitrogen/generated/ios/swift/Func_void_NitroDownloadResult.swift @@ -1,5 +1,5 @@ /// -/// Func_void_NitroFile.swift +/// Func_void_NitroDownloadResult.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro /// Copyright © 2025 Marc Rousavy @ Margelo @@ -9,20 +9,20 @@ import NitroModules /** - * Wraps a Swift `(_ value: NitroFile) -> Void` as a class. + * Wraps a Swift `(_ value: NitroDownloadResult) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. */ -public final class Func_void_NitroFile { +public final class Func_void_NitroDownloadResult { public typealias bridge = margelo.nitro.nitrofs.bridge.swift - private let closure: (_ value: NitroFile) -> Void + private let closure: (_ value: NitroDownloadResult) -> Void - public init(_ closure: @escaping (_ value: NitroFile) -> Void) { + public init(_ closure: @escaping (_ value: NitroDownloadResult) -> Void) { self.closure = closure } @inline(__always) - public func call(value: NitroFile) -> Void { + public func call(value: NitroDownloadResult) -> Void { self.closure(value) } @@ -36,12 +36,12 @@ public final class Func_void_NitroFile { } /** - * Casts an unsafe pointer to a `Func_void_NitroFile`. - * The pointer has to be a retained opaque `Unmanaged`. + * Casts an unsafe pointer to a `Func_void_NitroDownloadResult`. + * The pointer has to be a retained opaque `Unmanaged`. * This removes one strong reference from the object! */ @inline(__always) - public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_NitroFile { - return Unmanaged.fromOpaque(pointer).takeRetainedValue() + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_NitroDownloadResult { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() } } diff --git a/nitrogen/generated/ios/swift/HybridNitroFSSpec.swift b/nitrogen/generated/ios/swift/HybridNitroFSSpec.swift index 23f4d97..184216f 100644 --- a/nitrogen/generated/ios/swift/HybridNitroFSSpec.swift +++ b/nitrogen/generated/ios/swift/HybridNitroFSSpec.swift @@ -35,8 +35,10 @@ public protocol HybridNitroFSSpec_protocol: HybridObject { func dirname(path: String) throws -> String func basename(path: String) throws -> String func extname(path: String) throws -> String - func uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((_ uploadedBytes: Double, _ totalBytes: Double) -> Void)?) throws -> Promise - func downloadFile(serverUrl: String, destinationPath: String, onProgress: ((_ downloadedBytes: Double, _ totalBytes: Double) -> Void)?) throws -> Promise + func uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: ((_ uploadedBytes: Double, _ totalBytes: Double) -> Void)?) throws -> Promise + func cancelUpload(jobId: String) throws -> Promise + func downloadFile(serverUrl: String, destinationPath: String, onProgress: ((_ downloadedBytes: Double, _ totalBytes: Double) -> Void)?) throws -> Promise + func cancelDownload(jobId: String) throws -> Promise } public extension HybridNitroFSSpec_protocol { diff --git a/nitrogen/generated/ios/swift/HybridNitroFSSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridNitroFSSpec_cxx.swift index ed378ee..d898831 100644 --- a/nitrogen/generated/ios/swift/HybridNitroFSSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridNitroFSSpec_cxx.swift @@ -405,7 +405,7 @@ open class HybridNitroFSSpec_cxx { } @inline(__always) - public final func uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: bridge.std__optional_std__function_void_double____uploadedBytes_____double____totalBytes______) -> bridge.Result_std__shared_ptr_Promise_void___ { + public final func uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress: bridge.std__optional_std__function_void_double____uploadedBytes_____double____totalBytes______) -> bridge.Result_std__shared_ptr_Promise_std__string___ { do { let __result = try self.__implementation.uploadFile(file: file, uploadOptions: uploadOptions, onProgress: { () -> ((_ uploadedBytes: Double, _ totalBytes: Double) -> Void)? in if bridge.has_value_std__optional_std__function_void_double____uploadedBytes_____double____totalBytes______(onProgress) { @@ -420,23 +420,42 @@ open class HybridNitroFSSpec_cxx { return nil } }()) - let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in - let __promise = bridge.create_std__shared_ptr_Promise_void__() - let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) __result - .then({ __result in __promiseHolder.resolve() }) + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) return __promise }() - return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp) + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) } catch (let __error) { let __exceptionPtr = __error.toCpp() - return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func cancelUpload(jobId: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.cancelUpload(jobId: String(jobId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } @inline(__always) - public final func downloadFile(serverUrl: std.string, destinationPath: std.string, onProgress: bridge.std__optional_std__function_void_double____downloadedBytes_____double____totalBytes______) -> bridge.Result_std__shared_ptr_Promise_NitroFile___ { + public final func downloadFile(serverUrl: std.string, destinationPath: std.string, onProgress: bridge.std__optional_std__function_void_double____downloadedBytes_____double____totalBytes______) -> bridge.Result_std__shared_ptr_Promise_NitroDownloadResult___ { do { let __result = try self.__implementation.downloadFile(serverUrl: String(serverUrl), destinationPath: String(destinationPath), onProgress: { () -> ((_ downloadedBytes: Double, _ totalBytes: Double) -> Void)? in if bridge.has_value_std__optional_std__function_void_double____downloadedBytes_____double____totalBytes______(onProgress) { @@ -451,18 +470,37 @@ open class HybridNitroFSSpec_cxx { return nil } }()) - let __resultCpp = { () -> bridge.std__shared_ptr_Promise_NitroFile__ in - let __promise = bridge.create_std__shared_ptr_Promise_NitroFile__() - let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_NitroFile__(__promise) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_NitroDownloadResult__ in + let __promise = bridge.create_std__shared_ptr_Promise_NitroDownloadResult__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_NitroDownloadResult__(__promise) __result .then({ __result in __promiseHolder.resolve(__result) }) .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) return __promise }() - return bridge.create_Result_std__shared_ptr_Promise_NitroFile___(__resultCpp) + return bridge.create_Result_std__shared_ptr_Promise_NitroDownloadResult___(__resultCpp) } catch (let __error) { let __exceptionPtr = __error.toCpp() - return bridge.create_Result_std__shared_ptr_Promise_NitroFile___(__exceptionPtr) + return bridge.create_Result_std__shared_ptr_Promise_NitroDownloadResult___(__exceptionPtr) + } + } + + @inline(__always) + public final func cancelDownload(jobId: std.string) -> bridge.Result_std__shared_ptr_Promise_bool___ { + do { + let __result = try self.__implementation.cancelDownload(jobId: String(jobId)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in + let __promise = bridge.create_std__shared_ptr_Promise_bool__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr) } } } diff --git a/nitrogen/generated/ios/swift/NitroDownloadResult.swift b/nitrogen/generated/ios/swift/NitroDownloadResult.swift new file mode 100644 index 0000000..674e95f --- /dev/null +++ b/nitrogen/generated/ios/swift/NitroDownloadResult.swift @@ -0,0 +1,46 @@ +/// +/// NitroDownloadResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `NitroDownloadResult`, backed by a C++ struct. + */ +public typealias NitroDownloadResult = margelo.nitro.nitrofs.NitroDownloadResult + +public extension NitroDownloadResult { + private typealias bridge = margelo.nitro.nitrofs.bridge.swift + + /** + * Create a new instance of `NitroDownloadResult`. + */ + init(jobId: String, file: NitroFile) { + self.init(std.string(jobId), file) + } + + var jobId: String { + @inline(__always) + get { + return String(self.__jobId) + } + @inline(__always) + set { + self.__jobId = std.string(newValue) + } + } + + var file: NitroFile { + @inline(__always) + get { + return self.__file + } + @inline(__always) + set { + self.__file = newValue + } + } +} diff --git a/nitrogen/generated/shared/c++/HybridNitroFSSpec.cpp b/nitrogen/generated/shared/c++/HybridNitroFSSpec.cpp index f26b426..f2fc654 100644 --- a/nitrogen/generated/shared/c++/HybridNitroFSSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridNitroFSSpec.cpp @@ -36,7 +36,9 @@ namespace margelo::nitro::nitrofs { prototype.registerHybridMethod("basename", &HybridNitroFSSpec::basename); prototype.registerHybridMethod("extname", &HybridNitroFSSpec::extname); prototype.registerHybridMethod("uploadFile", &HybridNitroFSSpec::uploadFile); + prototype.registerHybridMethod("cancelUpload", &HybridNitroFSSpec::cancelUpload); prototype.registerHybridMethod("downloadFile", &HybridNitroFSSpec::downloadFile); + prototype.registerHybridMethod("cancelDownload", &HybridNitroFSSpec::cancelDownload); }); } diff --git a/nitrogen/generated/shared/c++/HybridNitroFSSpec.hpp b/nitrogen/generated/shared/c++/HybridNitroFSSpec.hpp index a2c7cce..5ac90e1 100644 --- a/nitrogen/generated/shared/c++/HybridNitroFSSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridNitroFSSpec.hpp @@ -21,6 +21,8 @@ namespace margelo::nitro::nitrofs { struct NitroFileStat; } namespace margelo::nitro::nitrofs { struct NitroFile; } // Forward declaration of `NitroUploadOptions` to properly resolve imports. namespace margelo::nitro::nitrofs { struct NitroUploadOptions; } +// Forward declaration of `NitroDownloadResult` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroDownloadResult; } #include #include @@ -31,6 +33,7 @@ namespace margelo::nitro::nitrofs { struct NitroUploadOptions; } #include "NitroUploadOptions.hpp" #include #include +#include "NitroDownloadResult.hpp" namespace margelo::nitro::nitrofs { @@ -83,8 +86,10 @@ namespace margelo::nitro::nitrofs { virtual std::string dirname(const std::string& path) = 0; virtual std::string basename(const std::string& path) = 0; virtual std::string extname(const std::string& path) = 0; - virtual std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) = 0; - virtual std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) = 0; + virtual std::shared_ptr> uploadFile(const NitroFile& file, const NitroUploadOptions& uploadOptions, const std::optional>& onProgress) = 0; + virtual std::shared_ptr> cancelUpload(const std::string& jobId) = 0; + virtual std::shared_ptr> downloadFile(const std::string& serverUrl, const std::string& destinationPath, const std::optional>& onProgress) = 0; + virtual std::shared_ptr> cancelDownload(const std::string& jobId) = 0; protected: // Hybrid Setup diff --git a/nitrogen/generated/shared/c++/NitroDownloadResult.hpp b/nitrogen/generated/shared/c++/NitroDownloadResult.hpp new file mode 100644 index 0000000..a93b776 --- /dev/null +++ b/nitrogen/generated/shared/c++/NitroDownloadResult.hpp @@ -0,0 +1,81 @@ +/// +/// NitroDownloadResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `NitroFile` to properly resolve imports. +namespace margelo::nitro::nitrofs { struct NitroFile; } + +#include +#include "NitroFile.hpp" + +namespace margelo::nitro::nitrofs { + + /** + * A struct which can be represented as a JavaScript object (NitroDownloadResult). + */ + struct NitroDownloadResult { + public: + std::string jobId SWIFT_PRIVATE; + NitroFile file SWIFT_PRIVATE; + + public: + NitroDownloadResult() = default; + explicit NitroDownloadResult(std::string jobId, NitroFile file): jobId(jobId), file(file) {} + }; + +} // namespace margelo::nitro::nitrofs + +namespace margelo::nitro { + + // C++ NitroDownloadResult <> JS NitroDownloadResult (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitrofs::NitroDownloadResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitrofs::NitroDownloadResult( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "jobId")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "file")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitrofs::NitroDownloadResult& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "jobId", JSIConverter::toJSI(runtime, arg.jobId)); + obj.setProperty(runtime, "file", JSIConverter::toJSI(runtime, arg.file)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "jobId"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "file"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/src/specs/nitro-fs.nitro.ts b/src/specs/nitro-fs.nitro.ts index a11b282..f6c0907 100755 --- a/src/specs/nitro-fs.nitro.ts +++ b/src/specs/nitro-fs.nitro.ts @@ -1,6 +1,6 @@ import { type HybridObject } from 'react-native-nitro-modules' -import type { NitroFile, NitroFileEncoding, NitroFileStat, NitroUploadOptions } from '../type' +import type { NitroDownloadResult, NitroFile, NitroFileEncoding, NitroFileStat, NitroUploadOptions } from '../type' export interface NitroFS extends HybridObject<{ ios: 'swift', android: 'kotlin' }> { /** @@ -119,12 +119,22 @@ export interface NitroFS extends HybridObject<{ ios: 'swift', android: 'kotlin' * 'X-Filename': 'test.txt', * }, * } - * await NitroFS.uploadFile(options, (uploadedBytes, totalBytes) => { + * const jobId = await NitroFS.uploadFile(options, (uploadedBytes, totalBytes) => { * console.log(`Uploading ${uploadedBytes / totalBytes * 100}%`) * }) + * // To cancel: NitroFS.cancelUpload(jobId) * ``` */ - uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress?: (uploadedBytes: number, totalBytes: number) => void): Promise + uploadFile(file: NitroFile, uploadOptions: NitroUploadOptions, onProgress?: (uploadedBytes: number, totalBytes: number) => void): Promise + /** + * Cancel an upload operation + * ```typescript + * const jobId = await NitroFS.uploadFile(...) + * const cancelled = await NitroFS.cancelUpload(jobId) + * // Returns true if the upload was cancelled, false if jobId not found + * ``` + */ + cancelUpload(jobId: string): Promise /** * Upload multiple files to the file system * ```typescript @@ -142,11 +152,21 @@ export interface NitroFS extends HybridObject<{ ios: 'swift', android: 'kotlin' * ```typescript * const serverUrl = 'https://example.com/download' * const destinationPath = NitroFS.DOWNLOAD_DIR + '/file.txt' - * const file = await NitroFS.downloadFile(serverUrl, destinationPath, (downloadedBytes, totalBytes) => { + * const { jobId, file } = await NitroFS.downloadFile(serverUrl, destinationPath, (downloadedBytes, totalBytes) => { * console.log(`Downloading ${downloadedBytes / totalBytes * 100}%`) * }) - * console.log(file) // { name: 'file.txt', mimeType: 'text/plain', path: 'file.txt' } + * // To cancel before completion: NitroFS.cancelDownload(jobId) + * // File is available once download completes + * ``` + */ + downloadFile(serverUrl: string, destinationPath: string, onProgress?: (downloadedBytes: number, totalBytes: number) => void): Promise + /** + * Cancel a download operation + * ```typescript + * const { jobId } = await NitroFS.downloadFile(...) + * const cancelled = await NitroFS.cancelDownload(jobId) + * // Returns true if the download was cancelled, false if jobId not found * ``` */ - downloadFile(serverUrl: string, destinationPath: string, onProgress?: (downloadedBytes: number, totalBytes: number) => void): Promise + cancelDownload(jobId: string): Promise } diff --git a/src/type.ts b/src/type.ts index 1ae8a31..7d8b4b5 100644 --- a/src/type.ts +++ b/src/type.ts @@ -46,3 +46,14 @@ export type NitroFileStat = { isFile: boolean isDirectory: boolean } + +export type NitroDownloadResult = { + /** + * The job ID for cancelling the download operation + */ + jobId: string + /** + * The downloaded file (available once download completes) + */ + file: NitroFile +} \ No newline at end of file