From 364b84b072194c45fa216860adcdfe01ba324976 Mon Sep 17 00:00:00 2001 From: Numan <36044436+numandev1@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:54:53 +0500 Subject: [PATCH] feat: add cancelUpload and AbortController for backgroundUpload (#238) 1. cancelUpload 2. cancelUpload all 3. AbortController --- README.md | 60 ++++++++++ android/build.gradle | 4 +- .../reactnativecompressor/CompressorModule.kt | 5 + .../Utils/HttpCallManager.kt | 41 +++++++ .../reactnativecompressor/Utils/Uploader.kt | 103 ++++++++++++------ android/src/oldarch/CompressorSpec.kt | 1 + .../project.pbxproj | 8 ++ example/ios/Gemfile.lock | 1 + example/ios/Podfile.lock | 4 +- example/src/Screens/Main/index.tsx | 6 +- example/src/Screens/Video/index.tsx | 48 +++++++- example/src/Screens/index.tsx | 6 +- exampleExpo/src/Screens/Main/index.tsx | 6 +- exampleExpo/src/Screens/index.tsx | 6 +- ios/Compressor.mm | 5 +- ios/CompressorManager.swift | 5 + ios/Utils/Uploader.swift | 19 +++- ios/Utils/UrlTaskManager.swift | 58 ++++++++++ src/Spec/NativeCompressor.ts | 2 + src/index.tsx | 2 + src/utils/Uploader.tsx | 19 +++- 21 files changed, 353 insertions(+), 56 deletions(-) create mode 100644 android/src/main/java/com/reactnativecompressor/Utils/HttpCallManager.kt create mode 100644 ios/Utils/UrlTaskManager.swift diff --git a/README.md b/README.md index 394c33e..25e5701 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,53 @@ const uploadResult = await backgroundUpload( ); ``` +### Cancel Background Upload +for cancellation Upload, there is two ways +1. by calling, cancelUpload function +2. by calling abort function + +##### cancelUpload (support single and all) +```js +import { cancelUpload, backgroundUpload } from 'react-native-compressor'; + +// if we will call without passing any param then it will remove last pushed uploading +cancelUpload() + +// if you pass true as second param then it will cancel all the uploading +cancelUpload("",true) + +// if there is multiple files are uploading, and you wanna cancel specific uploading then you pass specific video id like this +let videoId='' +const uploadResult = await backgroundUpload( + url, + fileUrl, + { httpMethod: 'PUT', getCancellationId: (cancellationId) =>(videoId = cancellationId), }, + (written, total) => { + console.log(written, total); + } +); +cancelUpload(videoId) +``` + +##### cancel by calling abort +```js +import { backgroundUpload } from 'react-native-compressor'; + +const abortSignalRef = useRef(new AbortController()); + +const uploadResult = await backgroundUpload( + url, + fileUrl, + { httpMethod: 'PUT' }, + (written, total) => { + console.log(written, total); + }, + abortSignalRef.current.signal +); + +abortSignalRef.current?.abort(); // this will cancel uploading +``` + ### Download File ```js @@ -493,12 +540,25 @@ export declare type UploaderOptions = ( ) & { headers?: Record; httpMethod?: UploaderHttpMethod; + getCancellationId?: (cancellationId: string) => void; }; ``` **Note:** some of the uploader code is borrowed from [Expo](https://github.com/expo/expo) I tested file uploader on this backend [Nodejs-File-Uploader](https://github.com/numandev1/nodejs-file-uploader) +### Cancel Background Upload +for cancellation Upload, there is two ways, you can use one of it +- ##### cancelUpload: ( uuid?: string, shouldCancelAll?: boolean) => void + 1. If we call without passing any param then it will remove the last pushed uploading + 2. If you pass true as the second param then it will cancel all the uploading + 3. if there is multiple files are uploading, and you wanna cancel specific uploading then you pass a specific video ID like this + +- ##### we can use [AbortController](https://github.com/facebook/react-native/blob/255fef5263afdf9933ba2f8a3dbcbca39ea9928a/packages/react-native/types/modules/globals.d.ts#L531) in backgroundUpload [Usage](#cancel-background-upload) + `const abortSignalRef = useRef(new AbortController());` + + `abortSignalRef.current?.abort();` + ### Download - ##### download: ( fileUrl: string, downloadProgress?: (progress: number) => void, progressDivider?: number ) => Promise< string > diff --git a/android/build.gradle b/android/build.gradle index 78b8fce..2948c83 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -78,8 +78,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } sourceSets { diff --git a/android/src/main/java/com/reactnativecompressor/CompressorModule.kt b/android/src/main/java/com/reactnativecompressor/CompressorModule.kt index 3a4a65e..c878392 100644 --- a/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +++ b/android/src/main/java/com/reactnativecompressor/CompressorModule.kt @@ -125,6 +125,11 @@ class CompressorModule(private val reactContext: ReactApplicationContext) : Comp uploader.upload(fileUrl, options, reactContext, promise) } + @ReactMethod + override fun cancelUpload(uuid: String,shouldCancelAll:Boolean) { + uploader.cancelUpload(uuid,shouldCancelAll) + } + @ReactMethod override fun download( fileUrl: String, diff --git a/android/src/main/java/com/reactnativecompressor/Utils/HttpCallManager.kt b/android/src/main/java/com/reactnativecompressor/Utils/HttpCallManager.kt new file mode 100644 index 0000000..462df8d --- /dev/null +++ b/android/src/main/java/com/reactnativecompressor/Utils/HttpCallManager.kt @@ -0,0 +1,41 @@ +package com.reactnativecompressor.Utils + +import okhttp3.Call + +class HttpCallManager { + private var resumableCalls: MutableMap = HashMap() + + fun registerTask(call: Call, uuid: String) { + resumableCalls[uuid] = call + } + + fun taskForId(uuid: String): Call? { + return resumableCalls[uuid] + } + + // will use in future + fun downloadTaskForId(uuid: String): Call? { + return taskForId(uuid) + } + + fun uploadTaskForId(uuid: String): Call? { + return taskForId(uuid) + } + + fun taskPop(): Call? { + val lastUuid = resumableCalls.keys.lastOrNull() + val lastCall = resumableCalls.remove(lastUuid) + return lastCall + } + + fun unregisterTask(uuid: String) { + resumableCalls.remove(uuid) + } + + fun cancelAllTasks() { + for ((_, call) in resumableCalls) { + call?.cancel() + } + resumableCalls.clear() + } +} diff --git a/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt b/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt index 9629a0a..8fd7c6a 100644 --- a/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt +++ b/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt @@ -26,36 +26,45 @@ import java.net.URLConnection import java.util.concurrent.TimeUnit class Uploader(private val reactContext: ReactApplicationContext) { - val TAG = "asyncTaskUploader" - var client: OkHttpClient? = null - val MIN_EVENT_DT_MS: Long = 100 - - fun upload(fileUriString: String, _options: ReadableMap, reactContext: ReactApplicationContext, promise: Promise) { - val options:UploaderOptions=convertReadableMapToUploaderOptions(_options) - val url = options.url - val uuid = options.uuid - val progressListener: CountingRequestListener = object : CountingRequestListener { - private var mLastUpdate: Long = -1 - override fun onProgress(bytesWritten: Long, contentLength: Long) { - val currentTime = System.currentTimeMillis() - - // Throttle events. Sending too many events will block the JS event loop. - // Make sure to send the last event when we're at 100%. - if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || bytesWritten == contentLength) { - mLastUpdate = currentTime - EventEmitterHandler.sendUploadProgressEvent(bytesWritten,contentLength,uuid) - } + val TAG = "asyncTaskUploader" + var client: OkHttpClient? = null + val MIN_EVENT_DT_MS: Long = 100 + val httpCallManager = HttpCallManager() + + fun upload( + fileUriString: String, + _options: ReadableMap, + reactContext: ReactApplicationContext, + promise: Promise + ) { + val options: UploaderOptions = convertReadableMapToUploaderOptions(_options) + val url = options.url + val uuid = options.uuid + val progressListener: CountingRequestListener = object : CountingRequestListener { + private var mLastUpdate: Long = -1 + override fun onProgress(bytesWritten: Long, contentLength: Long) { + val currentTime = System.currentTimeMillis() + + // Throttle events. Sending too many events will block the JS event loop. + // Make sure to send the last event when we're at 100%. + if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || bytesWritten == contentLength) { + mLastUpdate = currentTime + EventEmitterHandler.sendUploadProgressEvent(bytesWritten, contentLength, uuid) } } - val request = createUploadRequest( - url, fileUriString, options - ) { requestBody -> CountingRequestBody(requestBody, progressListener) } - - okHttpClient?.let { - it.newCall(request).enqueue(object : Callback { + } + val request = createUploadRequest( + url, fileUriString, options + ) { requestBody -> CountingRequestBody(requestBody, progressListener) } + + okHttpClient?.let { + val call = it.newCall(request) + httpCallManager.registerTask(call,uuid) + call.enqueue( + object : Callback { override fun onFailure(call: Call, e: IOException) { Log.e(TAG, e.message.toString()) - promise.reject(TAG, e.message, e) + promise.reject(TAG, e.message) } override fun onResponse(call: Call, response: Response) { @@ -67,12 +76,12 @@ class Uploader(private val reactContext: ReactApplicationContext) { promise.resolve(param) } }) - } ?: run { - promise.reject(UploaderOkHttpNullException()) - } - + } ?: run { + promise.reject(UploaderOkHttpNullException()) } + } + @get:Synchronized private val okHttpClient: OkHttpClient? get() { @@ -87,7 +96,12 @@ class Uploader(private val reactContext: ReactApplicationContext) { } @Throws(IOException::class) - private fun createUploadRequest(url: String, fileUriString: String, options: UploaderOptions, decorator: RequestBodyDecorator): Request { + private fun createUploadRequest( + url: String, + fileUriString: String, + options: UploaderOptions, + decorator: RequestBodyDecorator + ): Request { val fileUri = Uri.parse(slashifyFilePath(fileUriString)) fileUri.checkIfFileExists() @@ -101,7 +115,11 @@ class Uploader(private val reactContext: ReactApplicationContext) { } @SuppressLint("NewApi") - private fun createRequestBody(options: UploaderOptions, decorator: RequestBodyDecorator, file: File): RequestBody { + private fun createRequestBody( + options: UploaderOptions, + decorator: RequestBodyDecorator, + file: File + ): RequestBody { return when (options.uploadType) { UploadType.BINARY_CONTENT -> { val mimeType: String? = if (options.mimeType?.isNotEmpty() == true) { @@ -122,7 +140,11 @@ class Uploader(private val reactContext: ReactApplicationContext) { val mimeType: String = options.mimeType ?: URLConnection.guessContentTypeFromName(file.name) val fieldName = options.fieldName ?: file.name - bodyBuilder.addFormDataPart(fieldName, file.name, decorator.decorate(file.asRequestBody(mimeType.toMediaTypeOrNull()))) + bodyBuilder.addFormDataPart( + fieldName, + file.name, + decorator.decorate(file.asRequestBody(mimeType.toMediaTypeOrNull())) + ) bodyBuilder.build() } } @@ -175,4 +197,19 @@ class Uploader(private val reactContext: ReactApplicationContext) { } return responseHeaders } + + fun cancelUpload(uuid:String,shouldCancelAll:Boolean) { + if(shouldCancelAll) + { + httpCallManager.cancelAllTasks() + } + else if(uuid=="") + { + httpCallManager.taskPop()?.cancel() + } + else + { + httpCallManager.uploadTaskForId(uuid)?.cancel() + } + } } diff --git a/android/src/oldarch/CompressorSpec.kt b/android/src/oldarch/CompressorSpec.kt index 77275dc..18ca452 100644 --- a/android/src/oldarch/CompressorSpec.kt +++ b/android/src/oldarch/CompressorSpec.kt @@ -23,6 +23,7 @@ abstract class CompressorSpec(context: ReactApplicationContext?) : ReactContextB abstract fun compress(fileUrl: String, optionMap: ReadableMap, promise: Promise) abstract fun cancelCompression(uuid: String) abstract fun upload(fileUrl: String, options: ReadableMap, promise: Promise) + abstract fun cancelUpload(uuid: String, shouldCancelAll:Boolean) abstract fun download(fileUrl: String, options: ReadableMap, promise: Promise) abstract fun activateBackgroundTask(options: ReadableMap, promise: Promise) diff --git a/example/ios/CompressorExample.xcodeproj/project.pbxproj b/example/ios/CompressorExample.xcodeproj/project.pbxproj index 40821a0..433f8d5 100644 --- a/example/ios/CompressorExample.xcodeproj/project.pbxproj +++ b/example/ios/CompressorExample.xcodeproj/project.pbxproj @@ -485,7 +485,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = CompressorExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -500,6 +503,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = CompressorExample; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -512,7 +516,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = CompressorExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -526,6 +533,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = CompressorExample; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/example/ios/Gemfile.lock b/example/ios/Gemfile.lock index 8aff6af..438f8e2 100644 --- a/example/ios/Gemfile.lock +++ b/example/ios/Gemfile.lock @@ -86,6 +86,7 @@ GEM rexml (~> 3.2.4) PLATFORMS + arm64-darwin-22 x86_64-darwin-19 x86_64-darwin-20 x86_64-darwin-22 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bee0dcb..a1bb53e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -880,7 +880,7 @@ PODS: - React-Codegen - React-RCTFabric - ReactCommon/turbomodule/core - - react-native-compressor (1.8.16): + - react-native-compressor (1.8.17): - hermes-engine - RCT-Folly (= 2021.07.22.00) - RCTRequired @@ -1377,7 +1377,7 @@ SPEC CHECKSUMS: React-jsinspector: 194e32c6aab382d88713ad3dd0025c5f5c4ee072 React-logger: cebf22b6cf43434e471dc561e5911b40ac01d289 react-native-cameraroll: 5d9523136a929b58f092fd7f0a9a13367a4b46e3 - react-native-compressor: 3ad769f5bac56d7337df076a1c62e74292f7136d + react-native-compressor: 8b6e302c4531f93aeaabd9658495bf9855adf3bd react-native-document-picker: c9ac93d7b511413f4a0ed61c92ff6c7b1bcf4f94 react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb react-native-image-picker: 9b4b1d0096500050cbdabf8f4fd00b771065d983 diff --git a/example/src/Screens/Main/index.tsx b/example/src/Screens/Main/index.tsx index 36c4847..b778e46 100644 --- a/example/src/Screens/Main/index.tsx +++ b/example/src/Screens/Main/index.tsx @@ -1,4 +1,4 @@ -import { StackNavigationProp } from '@react-navigation/stack'; +import { type StackNavigationProp } from '@react-navigation/stack'; import React from 'react'; import { FlatList, @@ -7,9 +7,11 @@ import { Pressable, ScrollView, View, + type StyleProp, + type ViewStyle, } from 'react-native'; -import { SCREENS, Screens } from '..'; +import { SCREENS, type Screens } from '..'; type RootStackParams = { Home: undefined } & { [key: string]: undefined }; type MainScreenProps = { diff --git a/example/src/Screens/Video/index.tsx b/example/src/Screens/Video/index.tsx index e4543c2..ad0c650 100644 --- a/example/src/Screens/Video/index.tsx +++ b/example/src/Screens/Video/index.tsx @@ -1,9 +1,18 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, Text, Button, Image, Alert, Platform } from 'react-native'; +import { + View, + Text, + Button, + Image, + Alert, + Platform, + BackHandler, +} from 'react-native'; import { Video, getRealPath, backgroundUpload, + cancelUpload, UploadType, UploaderHttpMethod, createVideoThumbnail, @@ -17,12 +26,16 @@ import { getFileInfo } from '../../Utils'; import ProgressBar from '../../Components/ProgressBar'; import type { ProgressBarRafType } from '../../Components/ProgressBar'; // const DOMAIN = 'http://localhost:8080'; -const DOMAIN = 'http://192.168.1.3:8080'; +const DOMAIN = 'http://192.168.1.5:8080'; const uploadPutRequest = `${DOMAIN}/upload/putRequestFile.mov`; +const uploadPutRequest1 = `${DOMAIN}/upload/putRequestFile1.mov`; const uploadPostRequest = `${DOMAIN}/upload`; // const uploadPostRequestFail = `${DOMAIN}/uploadFail`; +let counter1 = 0; export default function App() { + const cancellationIdForUploadRef = useRef(''); const progressRef = useRef(); + const abortSignalRef = useRef(new AbortController()); const cancellationIdRef = useRef(''); const [sourceVideo, setSourceVideo] = useState(); const [sourceSize, setSourceSize] = useState(); @@ -36,6 +49,7 @@ export default function App() { const [backgroundMode, setBackgroundMode] = useState(false); useEffect(() => { + counter1 = -1; if (!sourceVideo) return; createVideoThumbnail(sourceVideo, {}) .then((response) => setSourceVideoThumbnail(response.path)) @@ -67,7 +81,7 @@ export default function App() { if (doingSomething) { let counter = 1; const timer = setInterval(() => { - console.log(counter, ' Doing Simething', new Date()); + console.log(counter, ' Doing Something', new Date()); counter += 1; }, 500); return () => { @@ -77,6 +91,16 @@ export default function App() { return undefined; }, [doingSomething]); + useEffect(() => { + const handler = () => { + abortSignalRef.current?.abort(); + return true; + }; + BackHandler.addEventListener('hardwareBackPress', handler); + + return () => BackHandler.removeEventListener('hardwareBackPress', handler); + }, []); + const selectVideo = async () => { try { ImagePicker.launchImageLibrary( @@ -203,7 +227,8 @@ export default function App() { (written, total) => { progressRef.current?.setProgress(written / total); console.log(written, total); - } + }, + abortSignalRef.current.signal ); console.log(result, 'result'); @@ -222,18 +247,22 @@ export default function App() { const headers = { Authorization: `Bearer ABCABC`, }; + counter1++; const result = await backgroundUpload( - uploadPutRequest, + counter1 % 2 == 0 ? uploadPutRequest : uploadPutRequest1, localFileUrl, { uploadType: UploadType.BINARY_CONTENT, httpMethod: UploaderHttpMethod.PUT, headers, + getCancellationId: (cancellationId) => + (cancellationIdForUploadRef.current = cancellationId), }, (written, total) => { progressRef.current?.setProgress(written / total); console.log(written, total); - } + }, + abortSignalRef.current.signal ); console.log(result, 'result'); @@ -244,6 +273,11 @@ export default function App() { } }; + const cancelUploader = () => { + console.log('cancelUploader', cancellationIdForUploadRef.current); + cancelUpload(cancellationIdForUploadRef.current); + }; + const onCompressVideofromCameraoll = async () => { const photos = await CameraRoll.getPhotos({ first: 1, @@ -293,6 +327,8 @@ export default function App() { title="Upload(Put)" onPress={() => uploadByPutRequest('actual')} /> + +