diff --git a/README.md b/README.md index 80962c7d..1ac14834 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,101 @@ await RNFS.scanFile('FilePath', Date, Date) ``` * Scan the file using [Media Scanner](https://developer.android.com/reference/android/media/MediaScannerConnection). +# MediaStore -### Constants +### RNFS2 can now interact with the MediaStore on Android. This allows you to add, delete, and update media files in the MediaStore. + +### Inspiration for this feature came from [react-native-blob-util](https://github.com/RonRadtke/react-native-blob-util/wiki/MediaStore/) + +### This feature is only available on Android targeting API 29 or higher. And may require the following permissions: + +```xml + + + + + + + + + + + + +``` + +## Available Methods + +### `createMediaFile` + +* Creates a new media file in the MediaStore with the given `mimeType`. This will not create a file on the filesystem, but will create a reference in the MediaStore. + +```ts +// createMediaFile(fileDescriptor: FileDescriptor, mediatype: MediaCollections): Promise + +const fileDescriptor = { name: 'sample', parentFolder: 'MyAppFolder', mimeType: 'image/png' } + +const contentURI = await RNFS.MediaStore.createMediaFile(fileDescriptor, RNFS.MediaStore.MEDIA_IMAGE) +``` + +### `writeToMediaFile` + +* Writes data to a media file in the MediaStore with the given `mimeType`. + +```ts +// writeToMediaFile((uri: string, path: string): Promise + +await RNFS.MediaStore.writeToMediaFile('content://media/external/images/media/123', '/path/to/image/imageToWrite.png') +``` + +### `copyToMediaStore` + +* Copies the file at `filepath` to the MediaStore with the given `mimeType`. + +```ts +// copyToMediaStore(fileDescriptor: filedescriptor, mediatype: MediaCollections, path: string): Promise + +const fileDescriptor = { name: 'sample', parentFolder: 'MyAppFolder', mimeType: 'image/png' } + +const contentURI = await RNFS.MediaStore.copyToMediaStore(fileDescriptor, RNFS.MediaStore.MEDIA_IMAGE, '/path/to/image/imageToCopy.png') +``` + +### `existsInMediaStore` + +* Checks if the media file at `uri` exists in the MediaStore. + +```ts +// existsInMediaStore(uri: string): Promise + +await RNFS.MediaStore.existsInMediaStore('content://media/external/images/media/123') +``` + +### `deleteFromMediaStore` + +* Deletes the media file at `uri` from the MediaStore. + +```ts +// deleteFromMediaStore(uri: string): Promise + +await RNFS.MediaStore.deleteFromMediaStore('content://media/external/images/media/123') +``` + +## FileDescriptor +```ts +type FileDescriptor = { + name: string; + parentFolder: string; + mimeType: string +}; +``` + +## MediaStore Collections + * `MediaStore.MEDIA_AUDIO` - Audio media collection + * `MediaStore.MEDIA_IMAGE` - Image media collection + * `MediaStore.MEDIA_VIDEO` - Video media collection + * `MediaStore.MEDIA_DOWNLOAD` - Download media collection + +## Constants #### Common * `CachesDirectoryPath` - Absolute path to cache directory. diff --git a/android/src/main/java/com/rnfs2/RNFSFileTransformer.java b/android/src/main/java/com/rnfs2/RNFSFileTransformer.java new file mode 100644 index 00000000..a95bc32b --- /dev/null +++ b/android/src/main/java/com/rnfs2/RNFSFileTransformer.java @@ -0,0 +1,10 @@ +package com.rnfs2; + +public class RNFSFileTransformer { + public interface FileTransformer { + public byte[] onWriteFile(byte[] data); + public byte[] onReadFile(byte[] data); + } + + public static RNFSFileTransformer.FileTransformer sharedFileTransformer; +} diff --git a/android/src/main/java/com/rnfs2/RNFSMediaStoreManager.java b/android/src/main/java/com/rnfs2/RNFSMediaStoreManager.java new file mode 100644 index 00000000..dc07aee1 --- /dev/null +++ b/android/src/main/java/com/rnfs2/RNFSMediaStoreManager.java @@ -0,0 +1,397 @@ +package com.rnfs2; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; + +import com.rnfs2.Utils.FileDescription; +import com.rnfs2.Utils.MediaStoreQuery; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@ReactModule(name = RNFSMediaStoreManager.MODULE_NAME) +public class RNFSMediaStoreManager extends ReactContextBaseJavaModule { + + static final String MODULE_NAME = "RNFSMediaStoreManager"; + private final ReactApplicationContext reactContext; + + public enum MediaType { + Audio, + Image, + Video, + Download, + } + + private static final String RNFSMediaStoreTypeAudio = MediaType.Audio.toString(); + private static final String RNFSMediaStoreTypeImage = MediaType.Image.toString(); + private static final String RNFSMediaStoreTypeVideo = MediaType.Video.toString(); + private static final String RNFSMediaStoreTypeDownload = MediaType.Download.toString(); + + public RNFSMediaStoreManager(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + public String getName() { + return MODULE_NAME; + } + + private static Uri getMediaUri(MediaType mt) { + Uri res = null; + if (mt == MediaType.Audio) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Video) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Image) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } else { + res = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + } else if (mt == MediaType.Download) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + res = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + } + } + + return res; + } + + private static String getRelativePath(MediaType mt, ReactApplicationContext ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (mt == MediaType.Audio) { + return Environment.DIRECTORY_MUSIC; + } + + if (mt == MediaType.Video) { + return Environment.DIRECTORY_MOVIES; + } + + if (mt == MediaType.Image) { + return Environment.DIRECTORY_PICTURES; + } + + if (mt == MediaType.Download) { + return Environment.DIRECTORY_DOWNLOADS; + } + + return Environment.DIRECTORY_DOWNLOADS; + } else { + // throw error not supported + return null; + } + } + + @ReactMethod + public void createMediaFile(ReadableMap filedata, String mediaType, Promise promise) { + if (!(filedata.hasKey("name") && filedata.hasKey("parentFolder") && filedata.hasKey("mimeType"))) { + promise.reject("RNFS2.createMediaFile", "Invalid filedata: " + filedata.toString()); + return; + } + + if (mediaType == null) { + promise.reject("RNFS2.createMediaFile", "Invalid mediatype"); + } + + FileDescription file = new FileDescription(filedata.getString("name"), filedata.getString("mimeType"), filedata.getString("parentFolder")); + Uri res = createNewMediaFile(file, MediaType.valueOf(mediaType), promise, reactContext); + + if (res != null) { + promise.resolve(res.toString()); + } else { + promise.reject("RNFS2.createMediaFile", "File could not be created"); + } + } + + @ReactMethod + public void writeToMediaFile(String fileUri, String path, boolean transformFile, Promise promise) { + boolean res = writeToMediaFile(Uri.parse(fileUri), path, transformFile, promise, reactContext); + if (res) { + promise.resolve("Success"); + } + } + + @ReactMethod + public void copyToMediaStore(ReadableMap filedata, String mediaType, String path, Promise promise) { + if (!(filedata.hasKey("name") && filedata.hasKey("parentFolder") && filedata.hasKey("mimeType"))) { + promise.reject("RNFS2.copyToMediaStore", "Invalid filedata: " + filedata.toString()); + return; + } + + if (mediaType == null) { + promise.reject("RNFS2.copyToMediaStore", "Invalid mediatype"); + return; + } + + if (path == null) { + promise.reject("RNFS2.copyToMediaStore", "Invalid path"); + return; + } + + // check if file at path exists first before proceeding on creating the media file + try { + File file = new File(path); + if (!file.exists()) { + promise.reject("RNFS2.copyToMediaStore", "No such file ('" + path + "')"); + return; + } + } catch (Exception e) { + promise.reject("RNFS2.copyToMediaStore", "Error opening file: " + e.getMessage()); + return; + } + + FileDescription file = new FileDescription(filedata.getString("name"), filedata.getString("mimeType"), filedata.getString("parentFolder")); + Uri fileuri = createNewMediaFile(file, MediaType.valueOf(mediaType), promise, reactContext); + + if (fileuri == null) { + promise.reject("RNFS2.copyToMediaStore", "File could not be created"); + return; + } + + boolean res = writeToMediaFile(fileuri, path, false, promise, reactContext); + if (res) { + promise.resolve(fileuri.toString()); + } + } + + @ReactMethod + public void query(ReadableMap query, Promise promise) { + try { + MediaStoreQuery mediaStoreQuery = new MediaStoreQuery(query.getString("uri"), query.getString("fileName"), query.getString("relativePath"), query.getString("mediaType")); + WritableMap queryResult = query(mediaStoreQuery, promise, reactContext); + promise.resolve(queryResult); + } catch (Exception e) { + promise.reject("RNFS2.query", "Error checking file existence: " + e.getMessage()); + } + } + + @ReactMethod + public void delete(String fileUri, Promise promise) { + try { + Uri uri = Uri.parse(fileUri); + ContentResolver resolver = reactContext.getContentResolver(); + int res = resolver.delete(uri, null, null); + if (res > 0) { + promise.resolve(true); + } else { + promise.resolve(false); + } + } catch (Exception e) { + promise.reject("RNFS2.delete", "Error deleting file: " + e.getMessage()); + } + } + + private Uri createNewMediaFile(FileDescription file, MediaType mediaType, Promise promise, ReactApplicationContext ctx) { + // Add a specific media item. + Context appCtx = reactContext.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + + ContentValues fileDetails = new ContentValues(); + String relativePath = getRelativePath(mediaType, ctx); + String mimeType = file.mimeType; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + fileDetails.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); + fileDetails.put(MediaStore.MediaColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); + fileDetails.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); + fileDetails.put(MediaStore.MediaColumns.DISPLAY_NAME, file.name); + fileDetails.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath + '/' + file.parentFolder); + + Uri mediauri = getMediaUri(mediaType); + + try { + // Keeps a handle to the new file's URI in case we need to modify it later. + return resolver.insert(mediauri, fileDetails); + } catch (Exception e) { + return null; + } + } else { + // throw error not supported + promise.reject("RNFS2.createNewMediaFile", "Android version not supported"); + } + + return null; + } + + private boolean writeToMediaFile(Uri fileUri, String filePath, boolean transformFile, Promise promise, ReactApplicationContext ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + Context appCtx = ctx.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + + // write data + OutputStream stream = null; + Uri uri = null; + + try { + ParcelFileDescriptor descr; + try { + assert fileUri != null; + descr = appCtx.getContentResolver().openFileDescriptor(fileUri, "w"); + assert descr != null; + + File src = new File(filePath); + + if (!src.exists()) { + promise.reject("ENOENT", "No such file ('" + filePath + "')"); + return false; + } + + FileInputStream fin = new FileInputStream(src); + FileOutputStream out = new FileOutputStream(descr.getFileDescriptor()); + + if (transformFile) { + // in order to transform file, we must load the entire file onto memory + int length = (int) src.length(); + byte[] bytes = new byte[length]; + fin.read(bytes); + if (RNFSFileTransformer.sharedFileTransformer == null) { + throw new IllegalStateException("Write to media file with transform was specified but the shared file transformer is not set"); + } + byte[] transformedBytes = RNFSFileTransformer.sharedFileTransformer.onWriteFile(bytes); + out.write(transformedBytes); + } else { + byte[] buf = new byte[1024 * 10]; + int read; + + while ((read = fin.read(buf)) > 0) { + out.write(buf, 0, read); + } + } + + fin.close(); + out.close(); + descr.close(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new IOException("Failed to get output stream.")); + return false; + } + + stream = resolver.openOutputStream(fileUri); + if (stream == null) { + promise.reject(new IOException("Failed to get output stream.")); + return false; + } + } catch (IOException e) { + // Don't leave an orphan entry in the MediaStore + resolver.delete(uri, null, null); + promise.reject(e); + return false; + } finally { + if (stream != null) { + stream.close(); + } + } + } catch (IOException e) { + promise.reject("RNFS2.createMediaFile", "Cannot write to file, file might not exist"); + return false; + } + + return true; + } else { + // throw error not supported + promise.reject("RNFS2.createMediaFile", "Android version not supported"); + return false; + } + } + + private WritableMap query(MediaStoreQuery query, Promise promise, ReactApplicationContext ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Cursor cursor = null; + try { + Context appCtx = ctx.getApplicationContext(); + ContentResolver resolver = appCtx.getContentResolver(); + WritableMap queryResultsMap = Arguments.createMap(); + + Uri mediaURI = !Objects.equals(query.uri, "") ? Uri.parse(query.uri) : getMediaUri(MediaType.valueOf(query.mediaType)); + String[] projection = {MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.RELATIVE_PATH}; + + String selection = null; + String[] selectionArgs = null; + + if (Objects.equals(query.uri, "")) { + String relativePath = getRelativePath(MediaType.valueOf(query.mediaType), ctx); + selection = MediaStore.MediaColumns.DISPLAY_NAME + " = ? AND " + MediaStore.MediaColumns.RELATIVE_PATH + " = ?"; + selectionArgs = new String[]{query.fileName, relativePath + '/' + query.relativePath + '/'}; + } + + // query the media store + cursor = resolver.query(mediaURI, projection, selection, selectionArgs, null); + + if (cursor != null && cursor.moveToFirst()) { + int idColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID); + long id = cursor.getLong(idColumnIndex); + + Uri contentUri = Uri.withAppendedPath(mediaURI, String.valueOf(id)); + + queryResultsMap.putString("contentUri", contentUri.toString()); + + promise.resolve(queryResultsMap); + } else { + promise.resolve(null); + } + + return queryResultsMap; + } catch (Exception e) { + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } else { + // throw error not supported + promise.reject("RNFS2.exists", "Android version not supported"); + return null; + } + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + + constants.put(RNFSMediaStoreTypeAudio, RNFSMediaStoreTypeAudio); + constants.put(RNFSMediaStoreTypeImage, RNFSMediaStoreTypeImage); + constants.put(RNFSMediaStoreTypeVideo, RNFSMediaStoreTypeVideo); + constants.put(RNFSMediaStoreTypeDownload, RNFSMediaStoreTypeDownload); + + return constants; + } +} diff --git a/android/src/main/java/com/rnfs2/RNFSPackage.java b/android/src/main/java/com/rnfs2/RNFSPackage.java index 2c85dbb1..12301525 100644 --- a/android/src/main/java/com/rnfs2/RNFSPackage.java +++ b/android/src/main/java/com/rnfs2/RNFSPackage.java @@ -15,7 +15,10 @@ public class RNFSPackage implements ReactPackage { @NonNull @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new RNFSManager(reactContext)); + return Arrays.asList( + new RNFSManager(reactContext), + new RNFSMediaStoreManager(reactContext) + ); } @NonNull diff --git a/android/src/main/java/com/rnfs2/Utils/FileDescription.java b/android/src/main/java/com/rnfs2/Utils/FileDescription.java new file mode 100644 index 00000000..eaae7250 --- /dev/null +++ b/android/src/main/java/com/rnfs2/Utils/FileDescription.java @@ -0,0 +1,17 @@ +package com.rnfs2.Utils; + +public class FileDescription { + public String name; + public String parentFolder; + public String mimeType; + + public FileDescription(String n, String mT, String pF) { + name = n; + parentFolder = pF != null ? pF : ""; + mimeType = mT; + } + + public String getFullPath() { + return parentFolder + "/" + MimeType.getFullFileName(name, mimeType); + } +} diff --git a/android/src/main/java/com/rnfs2/Utils/MediaStoreQuery.java b/android/src/main/java/com/rnfs2/Utils/MediaStoreQuery.java new file mode 100644 index 00000000..02909978 --- /dev/null +++ b/android/src/main/java/com/rnfs2/Utils/MediaStoreQuery.java @@ -0,0 +1,15 @@ +package com.rnfs2.Utils; + +public class MediaStoreQuery { + public String uri; + public String fileName; + public String relativePath; + public String mediaType; + + public MediaStoreQuery(String contentURI, String contentFileName, String contentRelativePath, String contentMediaType) { + uri = contentURI != null ? contentURI : ""; + fileName = contentFileName != null ? contentFileName : ""; + relativePath = contentRelativePath != null ? contentRelativePath : ""; + mediaType = contentMediaType; + } +} diff --git a/android/src/main/java/com/rnfs2/Utils/MimeType.java b/android/src/main/java/com/rnfs2/Utils/MimeType.java new file mode 100644 index 00000000..8a8308fa --- /dev/null +++ b/android/src/main/java/com/rnfs2/Utils/MimeType.java @@ -0,0 +1,93 @@ +package com.rnfs2.Utils; + +import android.webkit.MimeTypeMap; + +public class MimeType { + static String UNKNOWN = "*/*"; + static String BINARY_FILE = "application/octet-stream"; + static String IMAGE = "image/*"; + static String AUDIO = "audio/*"; + static String VIDEO = "video/*"; + static String TEXT = "text/*"; + static String FONT = "font/*"; + static String APPLICATION = "application/*"; + static String CHEMICAL = "chemical/*"; + static String MODEL = "model/*"; + + /** + * * Given `name` = `ABC` AND `mimeType` = `video/mp4`, then return `ABC.mp4` + * * Given `name` = `ABC` AND `mimeType` = `null`, then return `ABC` + * * Given `name` = `ABC.mp4` AND `mimeType` = `video/mp4`, then return `ABC.mp4` + * + * @param name can have file extension or not + */ + + public static String getFullFileName(String name, String mimeType) { + // Prior to API 29, MimeType.BINARY_FILE has no file extension + String ext = MimeType.getExtensionFromMimeType(mimeType); + if ((ext == null || ext.isEmpty()) || name.endsWith("." + ext)) return name; + else { + String fn = name + "." + ext; + if (fn.endsWith(".")) return stripEnd(fn, "."); + else return fn; + } + } + + /** + * Some mime types return no file extension on older API levels. This function adds compatibility accross API levels. + * + * @see this.getExtensionFromMimeTypeOrFileName + */ + + public static String getExtensionFromMimeType(String mimeType) { + if (mimeType != null) { + if (mimeType.equals(BINARY_FILE)) return "bin"; + else return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + } else return ""; + } + + /** + * @see this.getExtensionFromMimeType + */ + public static String getExtensionFromMimeTypeOrFileName(String mimeType, String filename) { + if (mimeType == null || mimeType.equals(UNKNOWN)) return substringAfterLast(filename, "."); + else return getExtensionFromMimeType(mimeType); + } + + /** + * Some file types return no mime type on older API levels. This function adds compatibility across API levels. + */ + public static String getMimeTypeFromExtension(String fileExtension) { + if (fileExtension.equals("bin")) return BINARY_FILE; + else { + String mt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + if (mt != null) return mt; + else return UNKNOWN; + } + } + + public static String stripEnd(String str, String stripChars) { + if (str == null || stripChars == null) { + return str; + } + int end = str.length(); + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != -1) { + end--; + } + return str.substring(0, end); + } + + public static String substringAfterLast(String str, String separator) { + if (str == null) { + return null; + } else if (str.isEmpty()) { + return ""; + } else { + int pos = str.lastIndexOf(separator); + if (pos == -1 || pos == str.length() - 1) { + return ""; + } + return str.substring(pos + 1); + } + } +} diff --git a/example/App/example3.tsx b/example/App/example3.tsx new file mode 100644 index 00000000..38df3494 --- /dev/null +++ b/example/App/example3.tsx @@ -0,0 +1,225 @@ +import React, {useState} from 'react'; +import {Image} from 'react-native'; +import RNFS from 'react-native-fs2'; + +import {StyleSheet, Text, View, Button, Platform, ActivityIndicator, PermissionsAndroid} from 'react-native'; +import {getTestFolder} from './utils'; + +const DUMMY_IMAGE = + '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'; + +const Example = () => { + const [runningAction, setRunningAction] = useState(false); + const [result, setResult] = useState(''); + const [imageURI, setImageURI] = useState(''); + + const copyImageToMediaStore = async ( + imagePath: string, + imageFileName: string, + folderName = 'RNFSExample3Folder', + prefix = '', + overwrite = false, + ) => { + if (Platform.OS === 'android') { + // Check if file already exist on MediaStore + const contentExists = await RNFS.MediaStore.queryMediaStore({ + uri: '', + fileName: `${prefix}${imageFileName}`, + relativePath: folderName, + mediaType: RNFS.MediaStore.MEDIA_IMAGE, + }); + + if (contentExists) { + // if overwrite flag is true then we replace the file with the new one + if (overwrite) { + // overwrite + await RNFS.MediaStore.writeToMediaFile(contentExists.contentUri, imagePath); + } + + return contentExists.contentUri; + } + + const contentURI = await RNFS.MediaStore.copyToMediaStore( + { + name: `${prefix}${imageFileName}`, + parentFolder: folderName, + mimeType: 'image/png', + }, + RNFS.MediaStore.MEDIA_IMAGE, + imagePath, + ); + + return contentURI; + } + + /** + * USE DocumentDirectory on iOS and DownloadDirectory on android + * + * Note: + * DocumentDirectory not accessible on non-rooted android devices + */ + const targetDirectory = getTestFolder(); + + /** + * Create UploadedImages folder if it does not exist + */ + await RNFS.mkdir(`${targetDirectory}/${folderName}`); + + // generate destination path + const destinationFolder = `${targetDirectory}/${folderName}`; + const destinationPath = `${destinationFolder}/${prefix}${imageFileName}`; + + // Check if file already exist on the destination path + const fileExist = await RNFS.exists(destinationPath); + if (fileExist) { + // if overwrite flag is true then we replace the file with the new one + if (overwrite) { + try { + // attempt to delete existing file + await RNFS.unlink(destinationPath); + } catch {} + + // copy file to destination path + await RNFS.copyFile(imagePath, destinationPath); + + return destinationPath; + } + + return destinationPath; + } + + // get file stat to ensure file is not corrupted + // and to add another layer of check if RNFS.exists() fails to return the true value + try { + const fileStat = await RNFS.stat(destinationPath); + if (fileStat?.size > 0 && fileStat?.isFile()) { + return destinationPath; + } + } catch (err) { + console.error('File does not exist', err); + } + + // otherwise copy file to destination path + await RNFS.copyFile(imagePath, destinationPath); + + return destinationPath; + }; + + /** + * Methods + */ + const executeExample = async () => { + if (Platform.OS === 'android') { + const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES, { + title: 'RNFS2 Storage Permission', + message: 'RNFS2 Example App needs read/write access to your phone storage to save images', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + }); + + if (granted !== PermissionsAndroid.RESULTS.GRANTED) { + /** + * Android 13 does not need this permission anymore, so don't throw error + */ + if (typeof Platform.Version === 'number' && Platform.Version < 33) { + throw new Error('Permission denied'); + } + } + } + + let runStatus = ''; + try { + const dummyImagePath = `${RNFS.DocumentDirectoryPath}/dummyImage2.png`; + const dummyImageFile = await RNFS.exists(dummyImagePath); + + if (!dummyImageFile) { + await RNFS.writeFile(dummyImagePath, DUMMY_IMAGE, 'base64'); + } + + const contentURI = await copyImageToMediaStore( + dummyImagePath, + 'dummyImage2.png', + 'RNFSExample3Folder', + 'prefix', + true, + ); + + setImageURI(contentURI); + + const mediaStat = await RNFS.stat(contentURI); + + runStatus = `${runStatus}\n- "Media File" stat: ${JSON.stringify(mediaStat)}`; + setResult(runStatus); + } catch (err) { + setResult(`${runStatus}\n- Error Running Example`); + console.error(err); + } finally { + setRunningAction(false); + } + }; + + return ( + + Example #3 + This example will: + + + - Add an entry to MediaStore.Image + + + - access the image and displays it below + - Note: running the example again will overwrite existing media file + + + {runningAction && } + {result} + + + {!!imageURI && } + + +