├── .gitignore ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── org │ └── rncloudfs │ ├── CopyToGoogleDriveTask.java │ ├── GoogleDriveApiClient.java │ ├── RNCloudFsModule.java │ └── RNCloudFsPackage.java ├── docs ├── getting-started.md ├── todo.md └── xcode.png ├── index.js ├── ios ├── .gitignore ├── RNCloudFs.h ├── RNCloudFs.m └── RNCloudFs.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ └── contents.xcworkspacedata ├── licesnse.txt ├── package.json └── react-native-cloud-fs.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | .idea 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-cloud-fs 3 | 4 | A react-native library for reading and writing files to _iCloud Drive_ (iOS) and _Google Drive_ (Android). 5 | 6 | [Getting started](./docs/getting-started.md) 7 | 8 | ## Usage 9 | ```javascript 10 | import RNCloudFs from 'react-native-cloud-fs'; 11 | ``` 12 | 13 | ### fileExists (options) 14 | Returns a promise which when resolved returns a boolean value indicating if the specified path already exists. 15 | 16 | ```javascript 17 | const destinationPath = "foo-bar/docs/info.pdf"; 18 | const scope = 'visible'; 19 | 20 | RNCloudFs.fileExists({ 21 | targetPath: destinationPath, 22 | scope: scope 23 | }) 24 | .then((exists) => { 25 | console.log(exists ? "this file exists" : "this file does not exist"); 26 | }) 27 | .catch((err) => { 28 | console.warn("it failed", err); 29 | }) 30 | ``` 31 | _targetPath_: a path 32 | 33 | _scope_: determines if the user-visible documents (`visible`) or the app-visible documents (`hidden`) are searched for the specified path 34 | 35 | ### copyToCloud (options) 36 | Copies the content of a file (or uri) to the target file system. The files will appear in a either a user visible 37 | directory, or a dectory that only the app can see. The directory is named after `destinationPath`. The directory 38 | hierarchy for the destination path will be created if it doesn't already exist. If the target file already exists 39 | it a new filename is chosen and returned when the promise is resolved. 40 | 41 | ```javascript 42 | const sourceUri = {uri: 'https://foo.com/bar.pdf'}; 43 | const destinationPath = "foo-bar/docs/info.pdf"; 44 | const mimeType = null; 45 | const scope = 'visible'; 46 | const update = false; 47 | 48 | RNCloudFs.copyToCloud({ 49 | sourcePath: sourceUri, 50 | targetPath: destinationPath, 51 | mimeType, 52 | scope, 53 | update 54 | }) 55 | .then((path) => { 56 | console.log("it worked", path); 57 | }) 58 | .catch((err) => { 59 | console.warn("it failed", err); 60 | }) 61 | ``` 62 | 63 | _sourceUri_: object with any uri or an **absolute** file path and optional http headers, e.g: 64 | * `{path: '/foo/bar/file.txt'}` 65 | * `{uri: 'file://foo/bar/file.txt'}` 66 | * `{uri: 'http://www.files.com/foo/bar/file.txt', 'http-headers': {user: 'foo', password: 'bar'}}` (http-headers are android only) 67 | * `{uri: 'content://media/external/images/media/296'}` (android only) 68 | * `{uri: 'assets-library://asset/asset.JPG?id=106E99A1-4F6A-45A2-B320-B0AD4A8E8473&ext=JPG'}` (iOS only) 69 | 70 | _targetPath_: a **relative** path including a filename under which the file will be placed, e.g: 71 | * `my-cloud-text-file.txt` 72 | * `foo/bar/my-cloud-text-file.txt` 73 | 74 | _mimeType_: a mime type to store the file with **or null** (android only) , e.g: 75 | * `text/plain` 76 | * `application/json` 77 | * `image/jpeg` 78 | 79 | _scope_: a string to specify if the user can access the document (`visible`) or not (`hidden`) 80 | 81 | _update_: a boolean to specify if we want to update an existing item instead of creating a new one 82 | 83 | ### listFiles (options) 84 | Lists files in a directory along with some file metadata. The scope determines if the file listing takes place in the app folder or the public user documents folder. 85 | 86 | ```javascript 87 | const path = "dirA/dirB"; 88 | const scope = 'hidden'; 89 | 90 | RNCloudFs.listFiles({targetPath: path, scope: scope}) 91 | .then((res) => { 92 | console.log("it worked", res); 93 | }) 94 | .catch((err) => { 95 | console.warn("it failed", err); 96 | }) 97 | ``` 98 | 99 | _targetPath_: a path representing a folder to list files from 100 | 101 | _scope_: a string to specify if the files are the user-visible documents (`visible`) or the app-visible documents (`hidden`) 102 | 103 | ### readFileContent (options) 104 | Reads a file content. The scope determines if the file listing takes place in the app folder or the public user documents folder. 105 | 106 | ```javascript 107 | const path = "dirA/dirB"; 108 | const scope = 'hidden'; 109 | 110 | RNCloudFs.readFileContent({targetPath: path, scope: scope}) 111 | .then((res) => { 112 | console.log("it worked", res); 113 | }) 114 | .catch((err) => { 115 | console.warn("it failed", err); 116 | }) 117 | ``` 118 | 119 | _targetPath_: a path representing a file which content you want to read 120 | 121 | _scope_: a string to specify if the files are the user-visible documents (`visible`) or the app-visible documents (`hidden`) 122 | 123 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Intellij 38 | *.iml 39 | .idea/workspace.xml 40 | 41 | # Keystore files 42 | *.jks 43 | 44 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.1.3' 7 | } 8 | } 9 | 10 | apply plugin: 'com.android.library' 11 | 12 | android { 13 | compileSdkVersion 23 14 | buildToolsVersion "23.0.1" 15 | 16 | defaultConfig { 17 | minSdkVersion 16 18 | targetSdkVersion 22 19 | versionCode 1 20 | versionName "1.0" 21 | ndk { 22 | abiFilters "armeabi-v7a", "x86" 23 | } 24 | } 25 | lintOptions { 26 | warning 'InvalidPackage' 27 | } 28 | } 29 | 30 | allprojects { 31 | repositories { 32 | jcenter() 33 | } 34 | } 35 | 36 | dependencies { 37 | provided 'com.google.android.gms:play-services-drive:+' 38 | provided 'com.facebook.react:react-native:+' 39 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Tue Nov 08 10:30:44 GMT 2016 16 | android.useDeprecatedNdk=true 17 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npomfret/react-native-cloud-fs/c0b467be748bdb561401414e1b44b0cdfcc93c4a/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/org/rncloudfs/CopyToGoogleDriveTask.java: -------------------------------------------------------------------------------- 1 | package org.rncloudfs; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.util.Log; 7 | 8 | import com.facebook.react.bridge.Promise; 9 | import com.google.android.gms.common.api.GoogleApiClient; 10 | import com.google.android.gms.drive.DriveFolder; 11 | 12 | import java.io.IOException; 13 | import java.util.List; 14 | 15 | import static org.rncloudfs.GoogleDriveApiClient.resolve; 16 | import static org.rncloudfs.RNCloudFsModule.TAG; 17 | 18 | public class CopyToGoogleDriveTask implements GoogleApiClient.ConnectionCallbacks { 19 | private RNCloudFsModule.SourceUri sourceUri; 20 | private final String outputPath; 21 | @Nullable 22 | private final String mimeType; 23 | private final Promise promise; 24 | private final GoogleDriveApiClient googleApiClient; 25 | private final boolean useDocumentsFolder; 26 | 27 | public CopyToGoogleDriveTask(RNCloudFsModule.SourceUri sourceUri, String outputPath, @Nullable String mimeType, Promise promise, GoogleDriveApiClient googleDriveApiClient, boolean useDocumentsFolder) { 28 | this.sourceUri = sourceUri; 29 | this.outputPath = outputPath; 30 | this.mimeType = mimeType; 31 | this.promise = promise; 32 | this.googleApiClient = googleDriveApiClient; 33 | this.useDocumentsFolder = useDocumentsFolder; 34 | } 35 | 36 | @Override 37 | public void onConnected(@Nullable Bundle bundle) { 38 | AsyncTask.execute(new Runnable() { 39 | @Override 40 | public void run() { 41 | List pathParts = resolve(outputPath); 42 | 43 | try { 44 | DriveFolder rootFolder = useDocumentsFolder ? googleApiClient.documentsFolder() : googleApiClient.appFolder(); 45 | createFileInFolders(rootFolder, pathParts, sourceUri); 46 | } catch (Exception e) { 47 | Log.e(TAG, "Failed to write " + outputPath, e); 48 | promise.reject("Failed copy '" + sourceUri.uri + "' to " + outputPath, e); 49 | } 50 | } 51 | }); 52 | 53 | googleApiClient.unregisterListener(this); 54 | } 55 | 56 | @Override 57 | public void onConnectionSuspended(int i) { 58 | 59 | } 60 | 61 | private void createFileInFolders(DriveFolder parentFolder, List pathParts, RNCloudFsModule.SourceUri sourceUri) { 62 | if (pathParts.size() > 1) 63 | parentFolder = googleApiClient.createFolders(parentFolder, pathParts.subList(0, pathParts.size() - 1)); 64 | 65 | try { 66 | String fileName = googleApiClient.createFile(parentFolder, sourceUri, pathParts.get(0), mimeType); 67 | promise.resolve(fileName); 68 | } catch (IOException e) { 69 | Log.e(TAG, "Failed to create file from " + sourceUri, e); 70 | promise.reject("Failed to read input", e); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /android/src/main/java/org/rncloudfs/GoogleDriveApiClient.java: -------------------------------------------------------------------------------- 1 | package org.rncloudfs; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.text.TextUtils; 8 | import android.util.Log; 9 | 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.bridge.WritableMap; 12 | import com.facebook.react.bridge.WritableNativeArray; 13 | import com.facebook.react.bridge.WritableNativeMap; 14 | import com.google.android.gms.common.api.GoogleApiClient; 15 | import com.google.android.gms.common.api.Result; 16 | import com.google.android.gms.drive.Drive; 17 | import com.google.android.gms.drive.DriveApi; 18 | import com.google.android.gms.drive.DriveContents; 19 | import com.google.android.gms.drive.DriveFolder; 20 | import com.google.android.gms.drive.Metadata; 21 | import com.google.android.gms.drive.MetadataBuffer; 22 | import com.google.android.gms.drive.MetadataChangeSet; 23 | 24 | import java.io.IOException; 25 | import java.io.OutputStream; 26 | import java.text.SimpleDateFormat; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Locale; 30 | import java.util.concurrent.atomic.AtomicBoolean; 31 | 32 | import static org.rncloudfs.RNCloudFsModule.TAG; 33 | 34 | public class GoogleDriveApiClient { 35 | private final GoogleApiClient googleApiClient; 36 | private ReactApplicationContext reactContext; 37 | 38 | public GoogleDriveApiClient(GoogleApiClient googleApiClient, ReactApplicationContext reactContext) { 39 | this.googleApiClient = googleApiClient; 40 | this.reactContext = reactContext; 41 | } 42 | 43 | //see https://developers.google.com/drive/android/appfolder 44 | public DriveFolder appFolder() { 45 | return Drive.DriveApi.getAppFolder(googleApiClient); 46 | } 47 | 48 | public synchronized DriveFolder documentsFolder() { 49 | DriveFolder rootFolder = Drive.DriveApi.getRootFolder(googleApiClient); 50 | String applicationName = getApplicationName(reactContext); 51 | 52 | if(fileExists(rootFolder, applicationName)) { 53 | return folder(rootFolder, applicationName); 54 | } else { 55 | DriveFolder.DriveFolderResult folder = createFolder(rootFolder, applicationName); 56 | return folder.getDriveFolder(); 57 | } 58 | } 59 | 60 | private static String getApplicationName(Context context) { 61 | ApplicationInfo applicationInfo = context.getApplicationInfo(); 62 | int stringId = applicationInfo.labelRes; 63 | return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId); 64 | } 65 | 66 | private DriveFolder.DriveFolderResult createFolder(DriveFolder parentFolder, String name) { 67 | MetadataChangeSet changeSet = new MetadataChangeSet.Builder() 68 | .setTitle(name) 69 | .build(); 70 | 71 | Log.i(TAG, "creating folder: " + name); 72 | return parentFolder.createFolder(googleApiClient, changeSet).await(); 73 | } 74 | 75 | @Nullable 76 | public DriveFolder folder(DriveFolder parentFolder, String name) { 77 | DriveApi.MetadataBufferResult childrenBuffer = parentFolder.listChildren(googleApiClient).await();//maybe queryChildren would be much better 78 | try { 79 | for (Metadata metadata : childrenBuffer.getMetadataBuffer()) { 80 | if (metadata.getTitle().equals(name)) { 81 | return metadata.isFolder() ? metadata.getDriveId().asDriveFolder() : null; 82 | } 83 | } 84 | } finally { 85 | childrenBuffer.release(); 86 | } 87 | return null; 88 | } 89 | 90 | private String listFiles(DriveFolder folder, List pathParts, FileVisitor fileVisitor) throws NotFoundException { 91 | List currentPath = new ArrayList<>(); 92 | currentPath.add("."); 93 | listFiles(currentPath, folder, pathParts, fileVisitor); 94 | return TextUtils.join("/", currentPath); 95 | } 96 | 97 | private void listFiles(List currentPath, DriveFolder folder, List pathParts, FileVisitor fileVisitor) throws NotFoundException { 98 | if (pathParts.isEmpty()) { 99 | listFiles(folder, fileVisitor); 100 | } else { 101 | String pathName = pathParts.remove(0); 102 | 103 | if(pathName.equals("..")) { 104 | DriveApi.MetadataBufferResult result = folder.listParents(googleApiClient).await(); 105 | try { 106 | for (Metadata metadata : result.getMetadataBuffer()) { 107 | if(metadata.isFolder()) { 108 | currentPath.remove(currentPath.size() - 1); 109 | listFiles(currentPath, metadata.getDriveId().asDriveFolder(), pathParts, fileVisitor); 110 | return; 111 | } 112 | } 113 | throw new IllegalStateException("No parent folder"); 114 | } finally { 115 | result.release(); 116 | } 117 | } 118 | 119 | DriveApi.MetadataBufferResult childrenBuffer = folder.listChildren(googleApiClient).await(); 120 | try { 121 | for (Metadata metadata : childrenBuffer.getMetadataBuffer()) { 122 | String fileName = metadata.getTitle(); 123 | if (pathName.equals(fileName)) { 124 | if(metadata.isFolder()) { 125 | currentPath.add(pathName); 126 | listFiles(currentPath, metadata.getDriveId().asDriveFolder(), pathParts, fileVisitor); 127 | return; 128 | } else { 129 | fileVisitor.fileMetadata(metadata); 130 | return; 131 | } 132 | } 133 | } 134 | 135 | throw new NotFoundException(pathName); 136 | } finally { 137 | childrenBuffer.release(); 138 | } 139 | } 140 | } 141 | 142 | private void listFiles(DriveFolder folder, FileVisitor fileVisitor) { 143 | DriveApi.MetadataBufferResult childrenBuffer = folder.listChildren(googleApiClient).await(); 144 | try { 145 | for (Metadata metadata : childrenBuffer.getMetadataBuffer()) { 146 | fileVisitor.fileMetadata(metadata); 147 | } 148 | } finally { 149 | childrenBuffer.release(); 150 | } 151 | } 152 | 153 | public DriveFolder createFolders(DriveFolder parentFolder, List pathParts) { 154 | if(pathParts.isEmpty()) 155 | return parentFolder; 156 | 157 | String name = pathParts.remove(0); 158 | 159 | DriveFolder folder = folder(parentFolder, name); 160 | 161 | if (folder == null) { 162 | DriveFolder.DriveFolderResult result = createFolder(parentFolder, name); 163 | 164 | Log.i(TAG, "Created folder '" + name + "'"); 165 | 166 | return createFolders(result.getDriveFolder(), pathParts); 167 | } else { 168 | Log.d(TAG, "Folder already exists '" + name + "'"); 169 | 170 | return createFolders(folder, pathParts); 171 | } 172 | } 173 | 174 | public boolean fileExists(boolean useDocumentsFolder, List pathParts) { 175 | List parentDirs = pathParts.size() > 1 ? pathParts.subList(0, pathParts.size() - 2) : new ArrayList(); 176 | final String filename = pathParts.get(pathParts.size() - 1); 177 | 178 | DriveFolder rootFolder = useDocumentsFolder ? documentsFolder() : appFolder(); 179 | 180 | final AtomicBoolean found = new AtomicBoolean(false); 181 | 182 | try { 183 | listFiles(rootFolder, parentDirs, new FileVisitor() { 184 | @Override 185 | public void fileMetadata(Metadata metadata) { 186 | if(!found.get()) { 187 | String title = metadata.getTitle(); 188 | if(title.equals(filename)) 189 | found.set(true); 190 | } 191 | } 192 | }); 193 | 194 | return found.get(); 195 | } catch (NotFoundException e) { 196 | return false; 197 | } 198 | } 199 | 200 | public void unregisterListener(GoogleApiClient.ConnectionCallbacks callbacks) { 201 | googleApiClient.unregisterConnectionCallbacks(callbacks); 202 | } 203 | 204 | private interface FileVisitor { 205 | void fileMetadata(Metadata metadata); 206 | } 207 | 208 | public String createFile(DriveFolder driveFolder, RNCloudFsModule.InputDataSource input, String filename) throws IOException { 209 | return createFile(driveFolder, input, filename, null); 210 | } 211 | 212 | public String createFile(DriveFolder driveFolder, RNCloudFsModule.InputDataSource input, String filename, String mimeType) throws IOException { 213 | int count = 1; 214 | 215 | String uniqueFilename = filename; 216 | while (fileExists(driveFolder, uniqueFilename)) { 217 | Log.w(TAG, "item already at location: " + filename); 218 | uniqueFilename = count + "." + filename; 219 | count++; 220 | } 221 | 222 | DriveApi.DriveContentsResult driveContentsResult = Drive.DriveApi.newDriveContents(googleApiClient).await(); 223 | 224 | if (!driveContentsResult.getStatus().isSuccess()) { 225 | throw new IllegalStateException("cannot create file"); 226 | } 227 | 228 | DriveContents driveContents = driveContentsResult.getDriveContents(); 229 | OutputStream outputStream = driveContents.getOutputStream(); 230 | input.copyToOutputStream(outputStream); 231 | outputStream.close(); 232 | 233 | MetadataChangeSet.Builder builder = new MetadataChangeSet.Builder() 234 | .setTitle(uniqueFilename); 235 | 236 | if (mimeType != null) { 237 | builder.setMimeType(mimeType); 238 | } 239 | 240 | DriveFolder.DriveFileResult driveFileResult = driveFolder.createFile(googleApiClient, builder.build(), driveContents).await(); 241 | 242 | if (!driveFileResult.getStatus().isSuccess()) { 243 | throw new IllegalStateException("cannot create file"); 244 | } 245 | 246 | Log.i(TAG, "Created a file '" + uniqueFilename); 247 | return uniqueFilename; 248 | } 249 | 250 | public boolean fileExists(DriveFolder driveFolder, String filename) { 251 | DriveApi.MetadataBufferResult childrenBuffer = driveFolder.listChildren(googleApiClient).await(); 252 | try { 253 | for (Metadata metadata : childrenBuffer.getMetadataBuffer()) { 254 | if (metadata.getTitle().equals(filename)) 255 | return true; 256 | } 257 | return false; 258 | } finally { 259 | childrenBuffer.release(); 260 | } 261 | } 262 | 263 | public WritableMap listFiles(boolean useDocumentsFolder, List paths) throws NotFoundException { 264 | WritableMap data = new WritableNativeMap(); 265 | 266 | final WritableNativeArray files = new WritableNativeArray(); 267 | 268 | final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); 269 | 270 | DriveFolder parentFolder = useDocumentsFolder ? documentsFolder() : appFolder(); 271 | 272 | String path = listFiles(parentFolder, paths, new FileVisitor() { 273 | @Override 274 | public void fileMetadata(Metadata metadata) { 275 | if (!metadata.isDataValid()) 276 | return; 277 | 278 | WritableNativeMap file = new WritableNativeMap(); 279 | 280 | file.putBoolean("isDirectory", metadata.isFolder()); 281 | file.putBoolean("isFile", !metadata.isFolder()); 282 | file.putString("name", metadata.getTitle()); 283 | file.putString("uri", metadata.getAlternateLink()); 284 | file.putString("lastModified", simpleDateFormat.format(metadata.getModifiedDate())); 285 | file.putInt("size", (int) metadata.getFileSize()); 286 | 287 | files.pushMap(file); 288 | } 289 | }); 290 | 291 | data.putString("path", path); 292 | data.putArray("files", files); 293 | 294 | return data; 295 | } 296 | 297 | @NonNull 298 | public static List resolve(String path) { 299 | List names = new ArrayList<>(); 300 | for (String pathPart : path.split("/")) { 301 | if (pathPart.equals(".") || pathPart.isEmpty()) { 302 | //ignore 303 | } else { 304 | names.add(pathPart); 305 | } 306 | } 307 | return names; 308 | } 309 | 310 | private static class NotFoundException extends Exception { 311 | public NotFoundException(String pathName) { 312 | super("not found: " + pathName); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /android/src/main/java/org/rncloudfs/RNCloudFsModule.java: -------------------------------------------------------------------------------- 1 | package org.rncloudfs; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.app.DialogFragment; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.IntentSender; 9 | import android.net.Uri; 10 | import android.os.AsyncTask; 11 | import android.os.Bundle; 12 | import android.support.annotation.NonNull; 13 | import android.support.annotation.Nullable; 14 | import android.util.Log; 15 | import android.webkit.MimeTypeMap; 16 | 17 | import com.facebook.react.bridge.ActivityEventListener; 18 | import com.facebook.react.bridge.LifecycleEventListener; 19 | import com.facebook.react.bridge.Promise; 20 | import com.facebook.react.bridge.ReactApplicationContext; 21 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 22 | import com.facebook.react.bridge.ReactMethod; 23 | import com.facebook.react.bridge.ReadableMap; 24 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 25 | import com.facebook.react.bridge.WritableMap; 26 | import com.google.android.gms.common.ConnectionResult; 27 | import com.google.android.gms.common.GoogleApiAvailability; 28 | import com.google.android.gms.common.api.GoogleApiClient; 29 | import com.google.android.gms.drive.Drive; 30 | import com.google.android.gms.drive.DriveFolder; 31 | 32 | import java.io.File; 33 | import java.io.FileInputStream; 34 | import java.io.IOException; 35 | import java.io.InputStream; 36 | import java.io.OutputStream; 37 | import java.net.HttpURLConnection; 38 | import java.net.URL; 39 | import java.util.List; 40 | 41 | import static android.app.Activity.RESULT_OK; 42 | import static org.rncloudfs.GoogleDriveApiClient.resolve; 43 | 44 | public class RNCloudFsModule extends ReactContextBaseJavaModule implements GoogleApiClient.OnConnectionFailedListener, LifecycleEventListener, ActivityEventListener { 45 | public static final String TAG = "RNCloudFs"; 46 | 47 | private static final int REQUEST_CODE_RESOLUTION = 3; 48 | private static final int REQUEST_RESOLVE_ERROR = 1001; 49 | private static final String DIALOG_ERROR = "dialog_error"; 50 | private boolean isResolvingError = false; 51 | 52 | private final ReactApplicationContext reactContext; 53 | private GoogleApiClient googleApiClient; 54 | 55 | public RNCloudFsModule(ReactApplicationContext reactContext) { 56 | super(reactContext); 57 | this.reactContext = reactContext; 58 | 59 | reactContext.addLifecycleEventListener(this); 60 | reactContext.addActivityEventListener(this); 61 | } 62 | 63 | /** 64 | * android only method. Just here to test the connection logic 65 | */ 66 | @ReactMethod 67 | public void reset(final Promise promise) { 68 | final GoogleApiClient googleApiClient = this.googleApiClient; 69 | 70 | connect(new GoogleApiClient.ConnectionCallbacks() { 71 | @Override 72 | public void onConnected(@Nullable Bundle bundle) { 73 | googleApiClient.clearDefaultAccountAndReconnect(); 74 | googleApiClient.unregisterConnectionCallbacks(this); 75 | promise.resolve(null); 76 | } 77 | 78 | @Override 79 | public void onConnectionSuspended(int i) { 80 | 81 | } 82 | }); 83 | } 84 | 85 | @ReactMethod 86 | public void createFile(ReadableMap options, final Promise promise) { 87 | if (!options.hasKey("targetPath")) { 88 | promise.reject("error", "targetPath not specified"); 89 | } 90 | final String path = options.getString("targetPath"); 91 | 92 | if (!options.hasKey("content")) { 93 | promise.reject("error", "content not specified"); 94 | } 95 | final String content = options.getString("content"); 96 | 97 | final boolean useDocumentsFolder = options.hasKey("scope") ? options.getString("scope").toLowerCase().equals("visible") : true; 98 | 99 | connect(new CreateFileTask(path, promise, useDocumentsFolder, content)); 100 | } 101 | 102 | @ReactMethod 103 | public void fileExists(ReadableMap options, final Promise promise) { 104 | if (!options.hasKey("targetPath")) { 105 | promise.reject("error", "targetPath not specified"); 106 | } 107 | String path = options.getString("targetPath"); 108 | 109 | boolean useDocumentsFolder = !options.hasKey("scope") || options.getString("scope").toLowerCase().equals("visible"); 110 | 111 | connect(new FileExistsTask(useDocumentsFolder, path, promise)); 112 | } 113 | 114 | @ReactMethod 115 | public void listFiles(ReadableMap options, final Promise promise) { 116 | if (!options.hasKey("targetPath")) { 117 | promise.reject("error", "targetPath not specified"); 118 | } 119 | String path = options.getString("targetPath"); 120 | 121 | boolean useDocumentsFolder = !options.hasKey("scope") || options.getString("scope").toLowerCase().equals("visible"); 122 | 123 | connect(new ListFilesTask(useDocumentsFolder, path, promise)); 124 | } 125 | 126 | /** 127 | * Copy the source into the google drive database 128 | */ 129 | @ReactMethod 130 | public void copyToCloud(ReadableMap options, final Promise promise) { 131 | try { 132 | if (!options.hasKey("sourcePath")) { 133 | promise.reject("error", "sourcePath not specified"); 134 | } 135 | ReadableMap source = options.getMap("sourcePath"); 136 | String uriOrPath = source.hasKey("uri") ? source.getString("uri") : null; 137 | 138 | if (uriOrPath == null) { 139 | uriOrPath = source.hasKey("path") ? source.getString("path") : null; 140 | } 141 | 142 | if (uriOrPath == null) { 143 | promise.reject("no path", "no source uri or path was specified"); 144 | return; 145 | } 146 | 147 | if (!options.hasKey("targetPath")) { 148 | promise.reject("error", "targetPath not specified"); 149 | } 150 | final String destinationPath = options.getString("targetPath"); 151 | 152 | String mimeType = null; 153 | if (options.hasKey("mimetype")) { 154 | mimeType = options.getString("mimetype"); 155 | } 156 | 157 | boolean useDocumentsFolder = options.hasKey("scope") ? options.getString("scope").toLowerCase().equals("visible") : true; 158 | 159 | SourceUri sourceUri = new SourceUri(uriOrPath, source.hasKey("headers") ? source.getMap("headers") : null); 160 | 161 | String actualMimeType; 162 | if (mimeType == null) { 163 | actualMimeType = guessMimeType(uriOrPath); 164 | } else { 165 | actualMimeType = null; 166 | } 167 | 168 | connect(new CopyToGoogleDriveTask( 169 | sourceUri, 170 | destinationPath, 171 | actualMimeType, 172 | promise, 173 | new GoogleDriveApiClient(this.googleApiClient, reactContext), 174 | useDocumentsFolder) 175 | ); 176 | 177 | } catch (Exception e) { 178 | Log.e(TAG, "Failed to copy", e); 179 | promise.reject("Failed to copy", e); 180 | } 181 | } 182 | 183 | @Nullable 184 | private static String guessMimeType(String url) { 185 | String extension = MimeTypeMap.getFileExtensionFromUrl(url); 186 | if (extension != null) { 187 | return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 188 | } else { 189 | return null; 190 | } 191 | } 192 | 193 | @Override 194 | public void onHostResume() { 195 | if (this.googleApiClient != null) 196 | googleApiClient.connect(); 197 | } 198 | 199 | @Override 200 | public void onHostPause() { 201 | if (this.googleApiClient != null) 202 | this.googleApiClient.disconnect(); 203 | } 204 | 205 | @Override 206 | public void onHostDestroy() { 207 | if (this.googleApiClient != null) 208 | this.googleApiClient.disconnect(); 209 | } 210 | 211 | @Override 212 | public void onNewIntent(Intent intent) { 213 | System.out.println("RNCloudFsModule.onNewIntent"); 214 | } 215 | 216 | @Override 217 | public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { 218 | if (requestCode == REQUEST_CODE_RESOLUTION && resultCode == RESULT_OK) 219 | this.googleApiClient.connect(); 220 | else if (requestCode == REQUEST_RESOLVE_ERROR) { 221 | isResolvingError = false; 222 | 223 | if (resultCode == RESULT_OK) { 224 | // Make sure the app is not already connected or attempting to connect 225 | if (!googleApiClient.isConnecting() && !googleApiClient.isConnected()) { 226 | googleApiClient.connect(); 227 | } 228 | } 229 | } 230 | } 231 | 232 | @Override 233 | public void onConnectionFailed(ConnectionResult result) { 234 | if (isResolvingError) { 235 | // Already attempting to resolve an error. 236 | } else if (result.hasResolution()) { 237 | try { 238 | isResolvingError = true; 239 | result.startResolutionForResult(getCurrentActivity(), REQUEST_RESOLVE_ERROR); 240 | } catch (IntentSender.SendIntentException e) { 241 | // There was an error with the resolution intent. Try again. 242 | googleApiClient.connect(); 243 | } 244 | } else { 245 | showErrorDialog(result.getErrorCode()); 246 | isResolvingError = true; 247 | } 248 | } 249 | 250 | private void showErrorDialog(int errorCode) { 251 | ErrorDialogFragment dialogFragment = new ErrorDialogFragment(); 252 | Bundle args = new Bundle(); 253 | args.putInt(DIALOG_ERROR, errorCode); 254 | 255 | dialogFragment.setArguments(args); 256 | dialogFragment.show(getCurrentActivity().getFragmentManager(), "errordialog"); 257 | } 258 | 259 | /* Called from ErrorDialogFragment when the dialog is dismissed. */ 260 | public void onDialogDismissed() { 261 | isResolvingError = false; 262 | } 263 | 264 | /* A fragment to display an error dialog */ 265 | public static class ErrorDialogFragment extends DialogFragment { 266 | public ErrorDialogFragment() { 267 | } 268 | 269 | @Override 270 | public Dialog onCreateDialog(Bundle savedInstanceState) { 271 | int errorCode = this.getArguments().getInt(DIALOG_ERROR); 272 | return GoogleApiAvailability.getInstance().getErrorDialog(this.getActivity(), errorCode, REQUEST_RESOLVE_ERROR); 273 | } 274 | 275 | @Override 276 | public void onDismiss(DialogInterface dialog) { 277 | Activity activity = getActivity(); 278 | 279 | //todo - call back to onDialogDismissed 280 | } 281 | } 282 | 283 | public interface InputDataSource { 284 | void copyToOutputStream(OutputStream output) throws IOException; 285 | } 286 | 287 | public class SourceUri implements InputDataSource { 288 | public final String uri; 289 | @Nullable 290 | private final ReadableMap httpHeaders; 291 | 292 | private SourceUri(String uri, @Nullable ReadableMap httpHeaders) { 293 | this.uri = uri; 294 | this.httpHeaders = httpHeaders; 295 | } 296 | 297 | private InputStream read() throws IOException { 298 | if (uri.startsWith("/") || uri.startsWith("file:/")) { 299 | String path = uri.replaceFirst("^file\\:/+", "/"); 300 | File file = new File(path); 301 | return new FileInputStream(file); 302 | } else if (uri.startsWith("content://")) { 303 | return RNCloudFsModule.this.reactContext.getContentResolver().openInputStream(Uri.parse(uri)); 304 | } else { 305 | HttpURLConnection conn = (HttpURLConnection) new URL(uri).openConnection(); 306 | 307 | if (httpHeaders != null) { 308 | ReadableMapKeySetIterator readableMapKeySetIterator = httpHeaders.keySetIterator(); 309 | while (readableMapKeySetIterator.hasNextKey()) { 310 | String key = readableMapKeySetIterator.nextKey(); 311 | if (key == null) 312 | continue; 313 | String value = httpHeaders.getString(key); 314 | if (value == null) 315 | continue; 316 | conn.setRequestProperty(key, value); 317 | } 318 | } 319 | 320 | conn.setRequestMethod("GET"); 321 | 322 | return conn.getInputStream(); 323 | } 324 | } 325 | 326 | public void copyToOutputStream(OutputStream output) throws IOException { 327 | InputStream input = read(); 328 | if (input == null) 329 | throw new IllegalStateException("Cannot read " + uri); 330 | 331 | try { 332 | byte[] buffer = new byte[256]; 333 | int bytesRead; 334 | while ((bytesRead = input.read(buffer)) != -1) { 335 | output.write(buffer, 0, bytesRead); 336 | } 337 | } finally { 338 | input.close(); 339 | } 340 | } 341 | } 342 | 343 | @NonNull 344 | private void connect(GoogleApiClient.ConnectionCallbacks listener) { 345 | if (googleApiClient == null) { 346 | synchronized (this) { 347 | googleApiClient = new GoogleApiClient.Builder(reactContext) 348 | .addApi(Drive.API) 349 | .addScope(Drive.SCOPE_FILE) 350 | .addScope(Drive.SCOPE_APPFOLDER) 351 | .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { 352 | @Override 353 | public void onConnected(@Nullable Bundle bundle) { 354 | Log.i(TAG, "Google client API connected"); 355 | } 356 | 357 | @Override 358 | public void onConnectionSuspended(int i) { 359 | //what to do here?? 360 | Log.w(TAG, "Google client API suspended: " + i); 361 | } 362 | }) 363 | .addOnConnectionFailedListener(this) 364 | .build(); 365 | } 366 | } 367 | 368 | this.googleApiClient.registerConnectionCallbacks(listener); 369 | this.googleApiClient.connect(); 370 | } 371 | 372 | @Override 373 | public String getName() { 374 | return "RNCloudFs"; 375 | } 376 | 377 | private class ListFilesTask implements GoogleApiClient.ConnectionCallbacks { 378 | private final boolean useDocumentsFolder; 379 | private final String path; 380 | private final Promise promise; 381 | 382 | public ListFilesTask(boolean useDocumentsFolder, String path, Promise promise) { 383 | this.useDocumentsFolder = useDocumentsFolder; 384 | this.path = path; 385 | this.promise = promise; 386 | } 387 | 388 | @Override 389 | public void onConnected(@Nullable Bundle bundle) { 390 | final GoogleDriveApiClient googleDriveApiClient = new GoogleDriveApiClient(RNCloudFsModule.this.googleApiClient, reactContext); 391 | 392 | AsyncTask.execute(new Runnable() { 393 | @Override 394 | public void run() { 395 | try { 396 | WritableMap data = googleDriveApiClient.listFiles(useDocumentsFolder, resolve(path)); 397 | promise.resolve(data); 398 | } catch (Exception e) { 399 | promise.reject("error", e); 400 | } 401 | } 402 | }); 403 | 404 | googleDriveApiClient.unregisterListener(this); 405 | } 406 | 407 | @Override 408 | public void onConnectionSuspended(int i) { 409 | Log.w(TAG, "Google client API suspended: " + i); 410 | } 411 | } 412 | 413 | private class CreateFileTask implements GoogleApiClient.ConnectionCallbacks { 414 | private final String path; 415 | private final Promise promise; 416 | private final boolean useDocumentsFolder; 417 | private final String content; 418 | 419 | public CreateFileTask(String path, Promise promise, boolean useDocumentsFolder, String content) { 420 | this.path = path; 421 | this.promise = promise; 422 | this.useDocumentsFolder = useDocumentsFolder; 423 | this.content = content; 424 | } 425 | 426 | @Override 427 | public void onConnected(@Nullable Bundle bundle) { 428 | final GoogleDriveApiClient googleDriveApiClient = new GoogleDriveApiClient(RNCloudFsModule.this.googleApiClient, reactContext); 429 | 430 | AsyncTask.execute(new Runnable() { 431 | @Override 432 | public void run() { 433 | try { 434 | 435 | List pathParts = resolve(path); 436 | if (pathParts.size() == 0) { 437 | promise.reject("error", "no filename specified"); 438 | return; 439 | } 440 | 441 | DriveFolder parentFolder = useDocumentsFolder ? googleDriveApiClient.documentsFolder() : googleDriveApiClient.appFolder(); 442 | if (pathParts.size() > 1) { 443 | List parentDirs = pathParts.subList(0, pathParts.size() - 1); 444 | parentFolder = googleDriveApiClient.createFolders(parentFolder, parentDirs); 445 | } 446 | 447 | String filename = pathParts.get(pathParts.size() - 1); 448 | 449 | String outputFilename = googleDriveApiClient.createFile(parentFolder, new InputDataSource() { 450 | @Override 451 | public void copyToOutputStream(OutputStream output) throws IOException { 452 | output.write(content.getBytes("UTF-8")); 453 | } 454 | }, filename); 455 | 456 | promise.resolve(outputFilename); 457 | } catch (Exception e) { 458 | promise.reject("error", e); 459 | } 460 | 461 | } 462 | }); 463 | 464 | googleDriveApiClient.unregisterListener(this); 465 | } 466 | 467 | @Override 468 | public void onConnectionSuspended(int i) { 469 | Log.w(TAG, "Google client API suspended: " + i); 470 | } 471 | } 472 | 473 | private class FileExistsTask implements GoogleApiClient.ConnectionCallbacks { 474 | private final boolean useDocumentsFolder; 475 | private final String path; 476 | private final Promise promise; 477 | 478 | public FileExistsTask(boolean useDocumentsFolder, String path, Promise promise) { 479 | this.useDocumentsFolder = useDocumentsFolder; 480 | this.path = path; 481 | this.promise = promise; 482 | } 483 | 484 | @Override 485 | public void onConnected(@Nullable Bundle bundle) { 486 | final GoogleDriveApiClient googleDriveApiClient = new GoogleDriveApiClient(RNCloudFsModule.this.googleApiClient, reactContext); 487 | 488 | AsyncTask.execute(new Runnable() { 489 | @Override 490 | public void run() { 491 | try { 492 | boolean fileExists = googleDriveApiClient.fileExists(useDocumentsFolder, resolve(path)); 493 | promise.resolve(fileExists); 494 | } catch (Exception e) { 495 | promise.reject("error", e); 496 | } 497 | } 498 | }); 499 | 500 | googleDriveApiClient.unregisterListener(this); 501 | } 502 | 503 | @Override 504 | public void onConnectionSuspended(int i) { 505 | Log.w(TAG, "Google client API suspended: " + i); 506 | } 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /android/src/main/java/org/rncloudfs/RNCloudFsPackage.java: -------------------------------------------------------------------------------- 1 | package org.rncloudfs; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class RNCloudFsPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNCloudFsModule(reactContext)); 17 | } 18 | 19 | @Override 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | An example project can be found at [react-native-cloud-fs-example](https://github.com/npomfret/react-native-cloud-fs-example). 4 | 5 | This package is not released to npm (yet)... 6 | 7 | npm install react-native-cloud-fs@https://github.com/npomfret/react-native-cloud-fs.git --save 8 | react-native link react-native-cloud-fs 9 | 10 | ### iOS 11 | 12 | On the device, make sure iCloud Drive is enabled. And it's helpful to have the iClound Drive app available. 13 | 14 | In xCode... 15 | 16 | * Add the following to `ios/app-name/Info.plist` (replacing _app-name_ and _package-name_ as appropriate): 17 | 18 | ```xml 19 | NSUbiquitousContainers 20 | 21 | iCloud.package-name 22 | 23 | NSUbiquitousContainerIsDocumentScopePublic 24 | 25 | NSUbiquitousContainerSupportedFolderLevels 26 | One 27 | NSUbiquitousContainerName 28 | app-name 29 | 30 | 31 | ``` 32 | 33 | * Enable iCloud: 34 | 35 | ![alt tag](./xcode.png) 36 | 37 | ### Android 38 | 39 | Enable Google Drive API: 40 | 41 | It's complicated! Here's a [video](https://www.youtube.com/watch?v=RezC1XP6jcs&feature=youtu.be&t=3m55s) of someone doing a similar thing for the Google Drive API demo. 42 | 43 | - Create a [new project](https://console.developers.google.com/apis/dashboard) for your app (if you don't already have one) 44 | - Under `Credentials`, choose `Create Credentials` > `OAth client ID` 45 | - Choose `Android` and enter a name 46 | - enter your SHA1 fingerprint (use the keytool to find it, eg: `keytool -exportcert -keystore path-to-debug-or-production-keystore -list -v`) 47 | - enter your package name (found in your manifest file) 48 | - copy the _OAuth client ID_ 49 | - Click Library, choose `Drive API` and enable it 50 | - Click `Drive UI Integration` 51 | - add the mandatory application icons 52 | - under `Drive integration` > `Authentication` 53 | - check `Automatically show OAuth 2.0 consent screen when users open my application from Google Drive` and enter your _OAuth client ID_ 54 | - enter an _Open URL_ 55 | 56 | Add the following to your `app/build.gradle` in the `dependecies` section (you can change the version to suit your application): 57 | 58 | compile ('com.google.android.gms:play-services-drive:10.2.0') { 59 | force = true; 60 | } 61 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | # todo 2 | 3 | ## In progress 4 | 5 | ## API 6 | 7 | * get user to sign into icloud: https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/CloudKitQuickStart/CreatingaSchemabySavingRecords/CreatingaSchemabySavingRecords.html#//apple_ref/doc/uid/TP40014987-CH3-SW8 8 | * download docs from icloud? https://github.com/iRareMedia/iCloudDocumentSync/blob/master/iCloud/iCloud.m#L751 9 | * send event when the files change 10 | * add method: `moveFile` / `renameFile` 11 | * add method: `createCouldDirectory` 12 | * add method: `copyFromCloud` 13 | * single file 14 | * to local file system 15 | * put (to rest endpoint) 16 | * post (mutipart form post) 17 | * add method: `deleteFromCloud` 18 | * single file 19 | * entire directory 20 | * option to save images to 21 | * ios photos 22 | * android clound images..? 23 | * `copyToCloud` 24 | * add option to overwrite existing file 25 | * add option to fail silently if the file already exists 26 | * add optional http headers (done for android) 27 | * check what happens (and fix) if a destination path contains non-filename safe characters ('#', '<', '$', '+', '%', '>', '!', '`', '&', '*', '‘', '|', '{', '?', '“', '=', '}', '/', ':', '\\', '@') [source](http://www.mtu.edu/umc/services/digital/writing/characters-avoid/). _iOS only_ 28 | * sensible & descriptive error messages for all error scenarios 29 | * add method: `searchForCloudFiles` 30 | 31 | ## Other implementations 32 | 33 | * Dropbox 34 | * google drive on iOS 35 | -------------------------------------------------------------------------------- /docs/xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npomfret/react-native-cloud-fs/c0b467be748bdb561401414e1b44b0cdfcc93c4a/docs/xcode.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { NativeModules } from 'react-native'; 4 | 5 | const { RNCloudFs } = NativeModules; 6 | 7 | export default RNCloudFs; 8 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Xcode template 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | -------------------------------------------------------------------------------- /ios/RNCloudFs.h: -------------------------------------------------------------------------------- 1 | 2 | #if __has_include() 3 | #import 4 | #else 5 | #import "RCTBridgeModule.h" 6 | #endif 7 | 8 | @interface RNCloudFs : NSObject 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/RNCloudFs.m: -------------------------------------------------------------------------------- 1 | 2 | #import "RNCloudFs.h" 3 | #import 4 | #if __has_include() 5 | #import 6 | #else 7 | #import "RCTBridgeModule.h" 8 | #endif 9 | #import "RCTEventDispatcher.h" 10 | #import "RCTUtils.h" 11 | #import 12 | #import "RCTLog.h" 13 | 14 | @implementation RNCloudFs 15 | 16 | - (dispatch_queue_t)methodQueue 17 | { 18 | return dispatch_queue_create("RNCloudFs.queue", DISPATCH_QUEUE_SERIAL); 19 | } 20 | 21 | RCT_EXPORT_MODULE() 22 | 23 | //see https://developer.apple.com/library/content/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html 24 | 25 | RCT_EXPORT_METHOD(createFile:(NSDictionary *) options 26 | resolver:(RCTPromiseResolveBlock)resolve 27 | rejecter:(RCTPromiseRejectBlock)reject) { 28 | 29 | NSString *destinationPath = [options objectForKey:@"targetPath"]; 30 | NSString *content = [options objectForKey:@"content"]; 31 | NSString *scope = [options objectForKey:@"scope"]; 32 | bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; 33 | 34 | NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; 35 | 36 | NSError *error; 37 | [content writeToFile:tempFile atomically:YES encoding:NSUTF8StringEncoding error:&error]; 38 | if(error) { 39 | return reject(@"error", error.description, nil); 40 | } 41 | 42 | [self moveToICloudDirectory:documentsFolder :tempFile :destinationPath :false :resolve :reject]; 43 | } 44 | 45 | RCT_EXPORT_METHOD(fileExists:(NSDictionary *)options 46 | resolver:(RCTPromiseResolveBlock)resolve 47 | rejecter:(RCTPromiseRejectBlock)reject) { 48 | 49 | NSString *destinationPath = [options objectForKey:@"targetPath"]; 50 | NSString *scope = [options objectForKey:@"scope"]; 51 | bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; 52 | 53 | NSFileManager* fileManager = [NSFileManager defaultManager]; 54 | 55 | NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; 56 | 57 | if (ubiquityURL) { 58 | NSURL* dir = [ubiquityURL URLByAppendingPathComponent:destinationPath]; 59 | NSString* dirPath = [dir.path stringByStandardizingPath]; 60 | 61 | bool exists = [fileManager fileExistsAtPath:dirPath]; 62 | 63 | return resolve(@(exists)); 64 | } else { 65 | RCTLogTrace(@"Could not retrieve a ubiquityURL"); 66 | return reject(@"error", [NSString stringWithFormat:@"could access iCloud drive '%@'", destinationPath], nil); 67 | } 68 | } 69 | 70 | RCT_EXPORT_METHOD(listFiles:(NSDictionary *)options 71 | resolver:(RCTPromiseResolveBlock)resolve 72 | rejecter:(RCTPromiseRejectBlock)reject) { 73 | 74 | NSString *destinationPath = [options objectForKey:@"targetPath"]; 75 | NSString *scope = [options objectForKey:@"scope"]; 76 | bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; 77 | 78 | NSFileManager* fileManager = [NSFileManager defaultManager]; 79 | 80 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 81 | [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZ"]; 82 | 83 | NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; 84 | 85 | if (ubiquityURL) { 86 | NSURL* target = [ubiquityURL URLByAppendingPathComponent:destinationPath]; 87 | 88 | NSMutableArray *fileData = [NSMutableArray new]; 89 | 90 | NSError *error = nil; 91 | 92 | BOOL isDirectory; 93 | [fileManager fileExistsAtPath:[target path] isDirectory:&isDirectory]; 94 | 95 | NSURL *dirPath; 96 | NSArray *contents; 97 | if(isDirectory) { 98 | contents = [fileManager contentsOfDirectoryAtPath:[target path] error:&error]; 99 | dirPath = target; 100 | } else { 101 | contents = @[[target lastPathComponent]]; 102 | dirPath = [target URLByDeletingLastPathComponent]; 103 | } 104 | 105 | if(error) { 106 | return reject(@"error", error.description, nil); 107 | } 108 | 109 | [contents enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) { 110 | NSURL *fileUrl = [dirPath URLByAppendingPathComponent:object]; 111 | 112 | NSError *error; 113 | NSDictionary *attributes = [fileManager attributesOfItemAtPath:[fileUrl path] error:&error]; 114 | if(error) { 115 | RCTLogTrace(@"problem getting attributes for %@", [fileUrl path]); 116 | //skip this one 117 | return; 118 | } 119 | 120 | NSFileAttributeType type = [attributes objectForKey:NSFileType]; 121 | 122 | bool isDir = type == NSFileTypeDirectory; 123 | bool isFile = type == NSFileTypeRegular; 124 | 125 | if(!isDir && !isFile) 126 | return; 127 | 128 | NSDate* modDate = [attributes objectForKey:NSFileModificationDate]; 129 | 130 | NSURL *shareUrl = [fileManager URLForPublishingUbiquitousItemAtURL:fileUrl expirationDate:nil error:&error]; 131 | 132 | [fileData addObject:@{ 133 | @"name": object, 134 | @"path": [fileUrl path], 135 | @"uri": shareUrl ? [shareUrl absoluteString] : [NSNull null], 136 | @"size": [attributes objectForKey:NSFileSize], 137 | @"lastModified": [dateFormatter stringFromDate:modDate], 138 | @"isDirectory": @(isDir), 139 | @"isFile": @(isFile) 140 | }]; 141 | }]; 142 | 143 | if (error) { 144 | return reject(@"error", [NSString stringWithFormat:@"could not copy to iCloud drive '%@'", destinationPath], error); 145 | } 146 | 147 | NSString *relativePath = [[dirPath path] stringByReplacingOccurrencesOfString:[ubiquityURL path] withString:@"."]; 148 | 149 | return resolve(@{ 150 | @"files": fileData, 151 | @"path": relativePath 152 | }); 153 | 154 | } else { 155 | NSLog(@"Could not retrieve a ubiquityURL"); 156 | return reject(@"error", [NSString stringWithFormat:@"could not copy to iCloud drive '%@'", destinationPath], nil); 157 | } 158 | } 159 | 160 | RCT_EXPORT_METHOD(copyToCloud:(NSDictionary *)options 161 | resolver:(RCTPromiseResolveBlock)resolve 162 | rejecter:(RCTPromiseRejectBlock)reject) { 163 | 164 | // mimeType is ignored for iOS 165 | NSDictionary *source = [options objectForKey:@"sourcePath"]; 166 | NSString *destinationPath = [options objectForKey:@"targetPath"]; 167 | NSString *scope = [options objectForKey:@"scope"]; 168 | BOOL update = [[options objectForKey:@"update"] boolValue]; 169 | bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; 170 | 171 | NSFileManager* fileManager = [NSFileManager defaultManager]; 172 | 173 | NSString *sourceUri = [source objectForKey:@"uri"]; 174 | if(!sourceUri) { 175 | sourceUri = [source objectForKey:@"path"]; 176 | } 177 | 178 | if([sourceUri hasPrefix:@"assets-library"]){ 179 | ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; 180 | 181 | [library assetForURL:[NSURL URLWithString:sourceUri] resultBlock:^(ALAsset *asset) { 182 | 183 | ALAssetRepresentation *rep = [asset defaultRepresentation]; 184 | 185 | Byte *buffer = (Byte*)malloc(rep.size); 186 | NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil]; 187 | NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; 188 | 189 | if (data) { 190 | NSString *filename = [sourceUri lastPathComponent]; 191 | NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; 192 | [data writeToFile:tempFile atomically:YES]; 193 | [self moveToICloudDirectory:documentsFolder :tempFile :destinationPath :update :resolve :reject]; 194 | } else { 195 | RCTLogTrace(@"source file does not exist %@", sourceUri); 196 | return reject(@"error", [NSString stringWithFormat:@"failed to copy asset '%@'", sourceUri], nil); 197 | } 198 | } failureBlock:^(NSError *error) { 199 | RCTLogTrace(@"source file does not exist %@", sourceUri); 200 | return reject(@"error", error.description, nil); 201 | }]; 202 | } else if ([sourceUri hasPrefix:@"file:/"] || [sourceUri hasPrefix:@"/"]) { 203 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^file:/+" options:NSRegularExpressionCaseInsensitive error:nil]; 204 | NSString *modifiedSourceUri = [regex stringByReplacingMatchesInString:sourceUri options:0 range:NSMakeRange(0, [sourceUri length]) withTemplate:@"/"]; 205 | 206 | if ([fileManager fileExistsAtPath:modifiedSourceUri isDirectory:nil]) { 207 | NSURL *sourceURL = [NSURL fileURLWithPath:modifiedSourceUri]; 208 | 209 | // todo: figure out how to *copy* to icloud drive 210 | // ...setUbiquitous will move the file instead of copying it, so as a work around lets copy it to a tmp file first 211 | NSString *filename = [sourceUri lastPathComponent]; 212 | NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; 213 | 214 | NSError *error; 215 | 216 | if(update && [fileManager fileExistsAtPath:tempFile isDirectory:nil]) { 217 | // in update mode, if a temp file already exists at destination path, we need to delete it to avoid errors 218 | [fileManager removeItemAtPath:tempFile error:&error]; 219 | } 220 | 221 | [fileManager copyItemAtPath:[sourceURL path] toPath:tempFile error:&error]; 222 | if(error) { 223 | return reject(@"error", error.description, nil); 224 | } 225 | 226 | [self moveToICloudDirectory:documentsFolder :tempFile :destinationPath :update :resolve :reject]; 227 | } else { 228 | NSLog(@"source file does not exist %@", sourceUri); 229 | return reject(@"error", [NSString stringWithFormat:@"no such file or directory, open '%@'", sourceUri], nil); 230 | } 231 | } else { 232 | NSURL *url = [NSURL URLWithString:sourceUri]; 233 | NSData *urlData = [NSData dataWithContentsOfURL:url]; 234 | 235 | if (urlData) { 236 | NSString *filename = [sourceUri lastPathComponent]; 237 | NSString *tempFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; 238 | [urlData writeToFile:tempFile atomically:YES]; 239 | [self moveToICloudDirectory:documentsFolder :tempFile :destinationPath :update :resolve :reject]; 240 | } else { 241 | RCTLogTrace(@"source file does not exist %@", sourceUri); 242 | return reject(@"error", [NSString stringWithFormat:@"cannot download '%@'", sourceUri], nil); 243 | } 244 | } 245 | } 246 | 247 | RCT_EXPORT_METHOD(readFileContent:(NSDictionary *)options 248 | resolver:(RCTPromiseResolveBlock)resolve 249 | rejecter:(RCTPromiseRejectBlock)reject) { 250 | 251 | NSString *destinationPath = [options objectForKey:@"targetPath"]; 252 | NSString *scope = [options objectForKey:@"scope"]; 253 | bool documentsFolder = !scope || [scope caseInsensitiveCompare:@"visible"] == NSOrderedSame; 254 | 255 | NSFileManager* fileManager = [NSFileManager defaultManager]; 256 | 257 | NSURL *ubiquityURL = documentsFolder ? [self icloudDocumentsDirectory] : [self icloudDirectory]; 258 | 259 | if (ubiquityURL) { 260 | NSURL* dir = [ubiquityURL URLByAppendingPathComponent:destinationPath]; 261 | NSString* dirPath = [dir.path stringByStandardizingPath]; 262 | 263 | NSString *content = [NSString stringWithContentsOfFile:dirPath encoding:NSUTF8StringEncoding error:nil]; 264 | 265 | RCTLogTrace(@"Has read file content from %@", dir); 266 | 267 | return resolve(content); 268 | } else { 269 | NSLog(@"Could not retrieve a ubiquityURL"); 270 | return reject(@"error", [NSString stringWithFormat:@"could not get from iCloud drive '%@'", destinationPath], nil); 271 | } 272 | } 273 | 274 | - (void) moveToICloudDirectory:(bool) documentsFolder :(NSString *)tempFile :(NSString *)destinationPath :(bool) update 275 | :(RCTPromiseResolveBlock)resolver 276 | :(RCTPromiseRejectBlock)rejecter { 277 | 278 | if(documentsFolder) { 279 | NSURL *ubiquityURL = [self icloudDocumentsDirectory]; 280 | [self moveToICloud:ubiquityURL :tempFile :destinationPath :update :resolver :rejecter]; 281 | } else { 282 | NSURL *ubiquityURL = [self icloudDirectory]; 283 | [self moveToICloud:ubiquityURL :tempFile :destinationPath :update :resolver :rejecter]; 284 | } 285 | } 286 | 287 | - (void) moveToICloud:(NSURL *)ubiquityURL :(NSString *)tempFile :(NSString *)destinationPath :(bool) update 288 | :(RCTPromiseResolveBlock)resolver 289 | :(RCTPromiseRejectBlock)rejecter { 290 | 291 | 292 | NSString * destPath = destinationPath; 293 | while ([destPath hasPrefix:@"/"]) { 294 | destPath = [destPath substringFromIndex:1]; 295 | } 296 | 297 | RCTLogTrace(@"Moving file %@ to %@", tempFile, destPath); 298 | 299 | NSFileManager* fileManager = [NSFileManager defaultManager]; 300 | 301 | if (ubiquityURL) { 302 | 303 | NSURL* targetFile = [ubiquityURL URLByAppendingPathComponent:destPath]; 304 | NSURL *dir = [targetFile URLByDeletingLastPathComponent]; 305 | NSString *name = [targetFile lastPathComponent]; 306 | 307 | NSURL* uniqueFile = targetFile; 308 | 309 | if(!update) { 310 | int count = 1; 311 | while([fileManager fileExistsAtPath:uniqueFile.path]) { 312 | NSString *uniqueName = [NSString stringWithFormat:@"%i.%@", count, name]; 313 | uniqueFile = [dir URLByAppendingPathComponent:uniqueName]; 314 | count++; 315 | } 316 | } 317 | 318 | RCTLogTrace(@"Target file: %@", uniqueFile.path); 319 | 320 | if (![fileManager fileExistsAtPath:dir.path]) { 321 | [fileManager createDirectoryAtURL:dir withIntermediateDirectories:YES attributes:nil error:nil]; 322 | } 323 | 324 | NSError *error; 325 | 326 | if(update && [fileManager fileExistsAtPath:uniqueFile.path isDirectory:nil]) { 327 | // in update mode, if a icloud file already exists at destination path, we need to delete it to avoid errors 328 | [fileManager removeItemAtPath:uniqueFile.path error:&error]; 329 | } 330 | 331 | [fileManager setUbiquitous:YES itemAtURL:[NSURL fileURLWithPath:tempFile] destinationURL:uniqueFile error:&error]; 332 | if(error) { 333 | return rejecter(@"error", error.description, nil); 334 | } 335 | 336 | [fileManager removeItemAtPath:tempFile error:&error]; 337 | 338 | return resolver(uniqueFile.path); 339 | } else { 340 | NSError *error; 341 | [fileManager removeItemAtPath:tempFile error:&error]; 342 | 343 | return rejecter(@"error", [NSString stringWithFormat:@"could not copy '%@' to iCloud drive", tempFile], nil); 344 | } 345 | } 346 | 347 | - (NSURL *)icloudDocumentsDirectory { 348 | NSFileManager* fileManager = [NSFileManager defaultManager]; 349 | NSURL *rootDirectory = [[self icloudDirectory] URLByAppendingPathComponent:@"Documents"]; 350 | 351 | if (rootDirectory) { 352 | if (![fileManager fileExistsAtPath:rootDirectory.path isDirectory:nil]) { 353 | RCTLogTrace(@"Creating documents directory: %@", rootDirectory.path); 354 | [fileManager createDirectoryAtURL:rootDirectory withIntermediateDirectories:YES attributes:nil error:nil]; 355 | } 356 | } 357 | 358 | return rootDirectory; 359 | } 360 | 361 | - (NSURL *)icloudDirectory { 362 | NSFileManager* fileManager = [NSFileManager defaultManager]; 363 | NSURL *rootDirectory = [fileManager URLForUbiquityContainerIdentifier:nil]; 364 | return rootDirectory; 365 | } 366 | 367 | - (NSURL *)localPathForResource:(NSString *)resource ofType:(NSString *)type { 368 | NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 369 | NSString *resourcePath = [[documentsDirectory stringByAppendingPathComponent:resource] stringByAppendingPathExtension:type]; 370 | return [NSURL fileURLWithPath:resourcePath]; 371 | } 372 | 373 | @end 374 | -------------------------------------------------------------------------------- /ios/RNCloudFs.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3E7B58A1CC2AC0600A0062D /* RNCloudFs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNCloudFs.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libRNCloudFs.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCloudFs.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B3E7B5881CC2AC0600A0062D /* RNCloudFs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCloudFs.h; sourceTree = ""; }; 28 | B3E7B5891CC2AC0600A0062D /* RNCloudFs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCloudFs.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libRNCloudFs.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | B3E7B5881CC2AC0600A0062D /* RNCloudFs.h */, 54 | B3E7B5891CC2AC0600A0062D /* RNCloudFs.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 58B511DA1A9E6C8500147676 /* RNCloudFs */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNCloudFs" */; 65 | buildPhases = ( 66 | 58B511D71A9E6C8500147676 /* Sources */, 67 | 58B511D81A9E6C8500147676 /* Frameworks */, 68 | 58B511D91A9E6C8500147676 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = RNCloudFs; 75 | productName = RCTDataManager; 76 | productReference = 134814201AA4EA6300B7C361 /* libRNCloudFs.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 58B511D31A9E6C8500147676 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0610; 86 | ORGANIZATIONNAME = Facebook; 87 | TargetAttributes = { 88 | 58B511DA1A9E6C8500147676 = { 89 | CreatedOnToolsVersion = 6.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNCloudFs" */; 94 | compatibilityVersion = "Xcode 3.2"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 58B511D21A9E6C8500147676; 101 | productRefGroup = 58B511D21A9E6C8500147676; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 58B511DA1A9E6C8500147676 /* RNCloudFs */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 58B511D71A9E6C8500147676 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | B3E7B58A1CC2AC0600A0062D /* RNCloudFs.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 58B511ED1A9E6C8500147676 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_CONSTANT_CONVERSION = YES; 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 133 | CLANG_WARN_EMPTY_BODY = YES; 134 | CLANG_WARN_ENUM_CONVERSION = YES; 135 | CLANG_WARN_INT_CONVERSION = YES; 136 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 137 | CLANG_WARN_UNREACHABLE_CODE = YES; 138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 139 | COPY_PHASE_STRIP = NO; 140 | ENABLE_STRICT_OBJC_MSGSEND = YES; 141 | GCC_C_LANGUAGE_STANDARD = gnu99; 142 | GCC_DYNAMIC_NO_PIC = NO; 143 | GCC_OPTIMIZATION_LEVEL = 0; 144 | GCC_PREPROCESSOR_DEFINITIONS = ( 145 | "DEBUG=1", 146 | "$(inherited)", 147 | ); 148 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 149 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 150 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 151 | GCC_WARN_UNDECLARED_SELECTOR = YES; 152 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 153 | GCC_WARN_UNUSED_FUNCTION = YES; 154 | GCC_WARN_UNUSED_VARIABLE = YES; 155 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 156 | MTL_ENABLE_DEBUG_INFO = YES; 157 | ONLY_ACTIVE_ARCH = YES; 158 | SDKROOT = iphoneos; 159 | }; 160 | name = Debug; 161 | }; 162 | 58B511EE1A9E6C8500147676 /* Release */ = { 163 | isa = XCBuildConfiguration; 164 | buildSettings = { 165 | ALWAYS_SEARCH_USER_PATHS = NO; 166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 167 | CLANG_CXX_LIBRARY = "libc++"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_WARN_BOOL_CONVERSION = YES; 171 | CLANG_WARN_CONSTANT_CONVERSION = YES; 172 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 173 | CLANG_WARN_EMPTY_BODY = YES; 174 | CLANG_WARN_ENUM_CONVERSION = YES; 175 | CLANG_WARN_INT_CONVERSION = YES; 176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 177 | CLANG_WARN_UNREACHABLE_CODE = YES; 178 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 179 | COPY_PHASE_STRIP = YES; 180 | ENABLE_NS_ASSERTIONS = NO; 181 | ENABLE_STRICT_OBJC_MSGSEND = YES; 182 | GCC_C_LANGUAGE_STANDARD = gnu99; 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 190 | MTL_ENABLE_DEBUG_INFO = NO; 191 | SDKROOT = iphoneos; 192 | VALIDATE_PRODUCT = YES; 193 | }; 194 | name = Release; 195 | }; 196 | 58B511F01A9E6C8500147676 /* Debug */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | HEADER_SEARCH_PATHS = ( 200 | "$(inherited)", 201 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 202 | "$(SRCROOT)/../../../React/**", 203 | "$(SRCROOT)/../../react-native/React/**", 204 | ); 205 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 206 | OTHER_LDFLAGS = "-ObjC"; 207 | PRODUCT_NAME = RNCloudFs; 208 | SKIP_INSTALL = YES; 209 | }; 210 | name = Debug; 211 | }; 212 | 58B511F11A9E6C8500147676 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | HEADER_SEARCH_PATHS = ( 216 | "$(inherited)", 217 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 218 | "$(SRCROOT)/../../../React/**", 219 | "$(SRCROOT)/../../react-native/React/**", 220 | ); 221 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 222 | OTHER_LDFLAGS = "-ObjC"; 223 | PRODUCT_NAME = RNCloudFs; 224 | SKIP_INSTALL = YES; 225 | }; 226 | name = Release; 227 | }; 228 | /* End XCBuildConfiguration section */ 229 | 230 | /* Begin XCConfigurationList section */ 231 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNCloudFs" */ = { 232 | isa = XCConfigurationList; 233 | buildConfigurations = ( 234 | 58B511ED1A9E6C8500147676 /* Debug */, 235 | 58B511EE1A9E6C8500147676 /* Release */, 236 | ); 237 | defaultConfigurationIsVisible = 0; 238 | defaultConfigurationName = Release; 239 | }; 240 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNCloudFs" */ = { 241 | isa = XCConfigurationList; 242 | buildConfigurations = ( 243 | 58B511F01A9E6C8500147676 /* Debug */, 244 | 58B511F11A9E6C8500147676 /* Release */, 245 | ); 246 | defaultConfigurationIsVisible = 0; 247 | defaultConfigurationName = Release; 248 | }; 249 | /* End XCConfigurationList section */ 250 | }; 251 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 252 | } 253 | -------------------------------------------------------------------------------- /ios/RNCloudFs.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /licesnse.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-cloud-fs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react-native" 11 | ], 12 | "author": "", 13 | "license": "" 14 | } -------------------------------------------------------------------------------- /react-native-cloud-fs.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = package['name'] 7 | s.version = package['version'] 8 | s.summary = 'iCloud integration' 9 | s.license = 'MIT' 10 | s.homepage = 'https://github.com/npomfret/react-native-cloud-fs' 11 | s.authors = 'npomfret' 12 | s.platforms = { :ios => "9.0", :tvos => "9.2" } 13 | s.source = { :git => 'https://github.com/npomfret/react-native-cloud-fs.git' } 14 | s.source_files = 'ios/**/*.{h,m}' 15 | s.requires_arc = true 16 | s.dependency 'React' 17 | end 18 | --------------------------------------------------------------------------------