diff options
| author | PliablePixels <pliablepixels@gmail.com> | 2015-06-24 18:47:42 -0400 |
|---|---|---|
| committer | PliablePixels <pliablepixels@gmail.com> | 2015-06-24 18:47:42 -0400 |
| commit | 855a0e8ddc273b58066530a1b55a946021dfc56e (patch) | |
| tree | 26550033e855a31a265fc2da4da3df0cc2733dc1 /plugins/org.apache.cordova.file/src/android/FileUtils.java | |
| parent | d442629aa825aab6bc55ab6be19e3aba060867fe (diff) | |
Cleaned up code, commented, preparing for HTTPS via CordovaHTTP
Diffstat (limited to 'plugins/org.apache.cordova.file/src/android/FileUtils.java')
| -rwxr-xr-x | plugins/org.apache.cordova.file/src/android/FileUtils.java | 1191 |
1 files changed, 1191 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.file/src/android/FileUtils.java b/plugins/org.apache.cordova.file/src/android/FileUtils.java new file mode 100755 index 00000000..9a9452fb --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/FileUtils.java @@ -0,0 +1,1191 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.cordova.file; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Log; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.PluginResult; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.channels.FileChannel; + +/** + * This class provides SD card file and directory services to JavaScript. + * Only files on the SD card can be accessed. + */ +public class FileUtils extends CordovaPlugin { + private static final String LOG_TAG = "FileUtils"; + + public static int NOT_FOUND_ERR = 1; + public static int SECURITY_ERR = 2; + public static int ABORT_ERR = 3; + + public static int NOT_READABLE_ERR = 4; + public static int ENCODING_ERR = 5; + public static int NO_MODIFICATION_ALLOWED_ERR = 6; + public static int INVALID_STATE_ERR = 7; + public static int SYNTAX_ERR = 8; + public static int INVALID_MODIFICATION_ERR = 9; + public static int QUOTA_EXCEEDED_ERR = 10; + public static int TYPE_MISMATCH_ERR = 11; + public static int PATH_EXISTS_ERR = 12; + + public static int TEMPORARY = 0; + public static int PERSISTENT = 1; + public static int RESOURCE = 2; + public static int APPLICATION = 3; + + private interface FileOp { + void run( ) throws Exception; + } + + /** + * Executes the request and returns whether the action was valid. + * + * @param action The action to execute. + * @param args JSONArray of arguments for the plugin. + * @param callbackContext The callback context used when calling back into JavaScript. + * @return True if the action was valid, false otherwise. + */ + public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException { + if (action.equals("testSaveLocationExists")) { + threadhelper( new FileOp( ){ + public void run() { + boolean b = DirectoryManager.testSaveLocationExists(); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + },callbackContext); + } + else if (action.equals("getFreeDiskSpace")) { + threadhelper( new FileOp( ){ + public void run() { + long l = DirectoryManager.getFreeDiskSpace(false); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l)); + } + },callbackContext); + } + else if (action.equals("testFileExists")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + boolean b = DirectoryManager.testFileExists(fname); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + }, callbackContext); + } + else if (action.equals("testDirectoryExists")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + boolean b = DirectoryManager.testFileExists(fname); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b)); + } + }, callbackContext); + } + else if (action.equals("readAsText")) { + final String encoding = args.getString(1); + final int start = args.getInt(2); + final int end = args.getInt(3); + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + readFileAs(fname, start, end, callbackContext, encoding, PluginResult.MESSAGE_TYPE_STRING); + } + }, callbackContext); + } + else if (action.equals("readAsDataURL")) { + final int start = args.getInt(1); + final int end = args.getInt(2); + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + readFileAs(fname, start, end, callbackContext, null, -1); + } + }, callbackContext); + } + else if (action.equals("readAsArrayBuffer")) { + final int start = args.getInt(1); + final int end = args.getInt(2); + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_ARRAYBUFFER); + } + },callbackContext); + } + else if (action.equals("readAsBinaryString")) { + final int start = args.getInt(1); + final int end = args.getInt(2); + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() { + readFileAs(fname, start, end, callbackContext, null, PluginResult.MESSAGE_TYPE_BINARYSTRING); + } + }, callbackContext); + } + else if (action.equals("write")) { + final String fname=args.getString(0); + final String data=args.getString(1); + final int offset=args.getInt(2); + final Boolean isBinary=args.getBoolean(3); + threadhelper( new FileOp( ){ + public void run() throws FileNotFoundException, IOException, NoModificationAllowedException { + long fileSize = write(fname, data, offset, isBinary); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize)); + } + }, callbackContext); + } + else if (action.equals("truncate")) { + final String fname=args.getString(0); + final int offset=args.getInt(1); + threadhelper( new FileOp( ){ + public void run( ) throws FileNotFoundException, IOException, NoModificationAllowedException { + long fileSize = truncateFile(fname, offset); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize)); + } + }, callbackContext); + } + else if (action.equals("requestFileSystem")) { + final int fstype=args.getInt(0); + final long size = args.optLong(1); + threadhelper( new FileOp( ){ + public void run() throws IOException, JSONException { + if (size != 0 && size > (DirectoryManager.getFreeDiskSpace(true) * 1024)) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR)); + } else { + JSONObject obj = requestFileSystem(fstype); + callbackContext.success(obj); + } + } + }, callbackContext); + } + else if (action.equals("resolveLocalFileSystemURI")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws IOException, JSONException { + JSONObject obj = resolveLocalFileSystemURI(fname); + callbackContext.success(obj); + } + },callbackContext); + } + else if (action.equals("getMetadata")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws FileNotFoundException, JSONException { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, getMetadata(fname))); + } + }, callbackContext); + } + else if (action.equals("getFileMetadata")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws FileNotFoundException, JSONException { + JSONObject obj = getFileMetadata(fname); + callbackContext.success(obj); + } + },callbackContext); + } + else if (action.equals("getParent")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws JSONException { + JSONObject obj = getParent(fname); + callbackContext.success(obj); + } + },callbackContext); + } + else if (action.equals("getDirectory")) { + final String dirname=args.getString(0); + final String fname=args.getString(1); + threadhelper( new FileOp( ){ + public void run() throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + JSONObject obj = getFile(dirname, fname, args.optJSONObject(2), true); + callbackContext.success(obj); + } + },callbackContext); + } + else if (action.equals("getFile")) { + final String dirname=args.getString(0); + final String fname=args.getString(1); + threadhelper( new FileOp( ){ + public void run() throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + JSONObject obj = getFile(dirname, fname, args.optJSONObject(2), false); + callbackContext.success(obj); + } + },callbackContext); + } + else if (action.equals("remove")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws NoModificationAllowedException, InvalidModificationException { + boolean success= remove(fname); + if (success) { + notifyDelete(fname); + callbackContext.success(); + } else { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } + } + },callbackContext); + } + else if (action.equals("removeRecursively")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws FileExistsException { + boolean success = removeRecursively(fname); + if (success) { + callbackContext.success(); + } else { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } + } + },callbackContext); + } + else if (action.equals("moveTo")) { + final String fname=args.getString(0); + final String newParent=args.getString(1); + final String newName=args.getString(2); + threadhelper( new FileOp( ){ + public void run() throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + JSONObject entry = transferTo(fname, newParent, newName, true); + callbackContext.success(entry); + } + },callbackContext); + } + else if (action.equals("copyTo")) { + final String fname=args.getString(0); + final String newParent=args.getString(1); + final String newName=args.getString(2); + threadhelper( new FileOp( ){ + public void run() throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + JSONObject entry = transferTo(fname, newParent, newName, false); + callbackContext.success(entry); + } + },callbackContext); + } + else if (action.equals("readEntries")) { + final String fname=args.getString(0); + threadhelper( new FileOp( ){ + public void run() throws FileNotFoundException, JSONException { + JSONArray entries = readEntries(fname); + callbackContext.success(entries); + } + },callbackContext); + } + else { + return false; + } + return true; + } + + /* helper to execute functions async and handle the result codes + * + */ + private void threadhelper(final FileOp f, final CallbackContext callbackContext){ + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + f.run(); + } catch ( Exception e) { + e.printStackTrace(); + if( e instanceof EncodingException){ + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof FileNotFoundException) { + callbackContext.error(FileUtils.NOT_FOUND_ERR); + } else if(e instanceof FileExistsException) { + callbackContext.error(FileUtils.PATH_EXISTS_ERR); + } else if(e instanceof NoModificationAllowedException ) { + callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR); + } else if(e instanceof InvalidModificationException ) { + callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR); + } else if(e instanceof MalformedURLException ) { + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof IOException ) { + callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR); + } else if(e instanceof EncodingException ) { + callbackContext.error(FileUtils.ENCODING_ERR); + } else if(e instanceof TypeMismatchException ) { + callbackContext.error(FileUtils.TYPE_MISMATCH_ERR); + } + } + } + }); + } + + /** + * Need to check to see if we need to clean up the content store + * + * @param filePath the path to check + */ + private void notifyDelete(String filePath) { + String newFilePath = FileHelper.getRealPath(filePath, cordova); + try { + this.cordova.getActivity().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + MediaStore.Images.Media.DATA + " = ?", + new String[] { newFilePath }); + } catch (UnsupportedOperationException t) { + // Was seeing this on the File mobile-spec tests on 4.0.3 x86 emulator. + // The ContentResolver applies only when the file was registered in the + // first case, which is generally only the case with images. + } + } + + /** + * Allows the user to look up the Entry for a file or directory referred to by a local URI. + * + * @param url of the file/directory to look up + * @return a JSONObject representing a Entry from the filesystem + * @throws MalformedURLException if the url is not valid + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the user can't read the file + * @throws JSONException + */ + @SuppressWarnings("deprecation") + private JSONObject resolveLocalFileSystemURI(String url) throws IOException, JSONException { + String decoded = URLDecoder.decode(url, "UTF-8"); + + File fp = null; + + // Handle the special case where you get an Android content:// uri. + if (decoded.startsWith("content:")) { + Cursor cursor = this.cordova.getActivity().managedQuery(Uri.parse(decoded), new String[] { MediaStore.Images.Media.DATA }, null, null, null); + // Note: MediaStore.Images/Audio/Video.Media.DATA is always "_data" + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + fp = new File(cursor.getString(column_index)); + } else { + // Test to see if this is a valid URL first + @SuppressWarnings("unused") + URL testUrl = new URL(decoded); + + if (decoded.startsWith("file://")) { + int questionMark = decoded.indexOf("?"); + if (questionMark < 0) { + fp = new File(decoded.substring(7, decoded.length())); + } else { + fp = new File(decoded.substring(7, questionMark)); + } + } else { + fp = new File(decoded); + } + } + + if (!fp.exists()) { + throw new FileNotFoundException(); + } + if (!fp.canRead()) { + throw new IOException(); + } + return getEntry(fp); + } + + /** + * Read the list of files from this directory. + * + * @param fileName the directory to read from + * @return a JSONArray containing JSONObjects that represent Entry objects. + * @throws FileNotFoundException if the directory is not found. + * @throws JSONException + */ + private JSONArray readEntries(String fileName) throws FileNotFoundException, JSONException { + File fp = createFileObject(fileName); + + if (!fp.exists()) { + // The directory we are listing doesn't exist so we should fail. + throw new FileNotFoundException(); + } + + JSONArray entries = new JSONArray(); + + if (fp.isDirectory()) { + File[] files = fp.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].canRead()) { + entries.put(getEntry(files[i])); + } + } + } + + return entries; + } + + /** + * A setup method that handles the move/copy of files/directories + * + * @param fileName to be copied/moved + * @param newParent is the location where the file will be copied/moved to + * @param newName for the file directory to be called, if null use existing file name + * @param move if false do a copy, if true do a move + * @return a Entry object + * @throws NoModificationAllowedException + * @throws IOException + * @throws InvalidModificationException + * @throws EncodingException + * @throws JSONException + * @throws FileExistsException + */ + private JSONObject transferTo(String fileName, String newParent, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException { + String newFileName = FileHelper.getRealPath(fileName, cordova); + newParent = FileHelper.getRealPath(newParent, cordova); + + // Check for invalid file name + if (newName != null && newName.contains(":")) { + throw new EncodingException("Bad file name"); + } + + File source = new File(newFileName); + + if (!source.exists()) { + // The file/directory we are copying doesn't exist so we should fail. + throw new FileNotFoundException("The source does not exist"); + } + + File destinationDir = new File(newParent); + if (!destinationDir.exists()) { + // The destination does not exist so we should fail. + throw new FileNotFoundException("The source does not exist"); + } + + // Figure out where we should be copying to + File destination = createDestination(newName, source, destinationDir); + + //Log.d(LOG_TAG, "Source: " + source.getAbsolutePath()); + //Log.d(LOG_TAG, "Destin: " + destination.getAbsolutePath()); + + // Check to see if source and destination are the same file + if (source.getAbsolutePath().equals(destination.getAbsolutePath())) { + throw new InvalidModificationException("Can't copy a file onto itself"); + } + + if (source.isDirectory()) { + if (move) { + return moveDirectory(source, destination); + } else { + return copyDirectory(source, destination); + } + } else { + if (move) { + JSONObject newFileEntry = moveFile(source, destination); + + // If we've moved a file given its content URI, we need to clean up. + if (fileName.startsWith("content://")) { + notifyDelete(fileName); + } + + return newFileEntry; + } else { + return copyFile(source, destination); + } + } + } + + /** + * Creates the destination File object based on name passed in + * + * @param newName for the file directory to be called, if null use existing file name + * @param fp represents the source file + * @param destination represents the destination file + * @return a File object that represents the destination + */ + private File createDestination(String newName, File fp, File destination) { + File destFile = null; + + // I know this looks weird but it is to work around a JSON bug. + if ("null".equals(newName) || "".equals(newName)) { + newName = null; + } + + if (newName != null) { + destFile = new File(destination.getAbsolutePath() + File.separator + newName); + } else { + destFile = new File(destination.getAbsolutePath() + File.separator + fp.getName()); + } + return destFile; + } + + /** + * Copy a file + * + * @param srcFile file to be copied + * @param destFile destination to be copied to + * @return a FileEntry object + * @throws IOException + * @throws InvalidModificationException + * @throws JSONException + */ + private JSONObject copyFile(File srcFile, File destFile) throws IOException, InvalidModificationException, JSONException { + // Renaming a file to an existing directory should fail + if (destFile.exists() && destFile.isDirectory()) { + throw new InvalidModificationException("Can't rename a file to a directory"); + } + + copyAction(srcFile, destFile); + + return getEntry(destFile); + } + + /** + * Moved this code into it's own method so moveTo could use it when the move is across file systems + */ + private void copyAction(File srcFile, File destFile) + throws FileNotFoundException, IOException { + FileInputStream istream = new FileInputStream(srcFile); + FileOutputStream ostream = new FileOutputStream(destFile); + FileChannel input = istream.getChannel(); + FileChannel output = ostream.getChannel(); + + try { + input.transferTo(0, input.size(), output); + } finally { + istream.close(); + ostream.close(); + input.close(); + output.close(); + } + } + + /** + * Copy a directory + * + * @param srcDir directory to be copied + * @param destinationDir destination to be copied to + * @return a DirectoryEntry object + * @throws JSONException + * @throws IOException + * @throws NoModificationAllowedException + * @throws InvalidModificationException + */ + private JSONObject copyDirectory(File srcDir, File destinationDir) throws JSONException, IOException, NoModificationAllowedException, InvalidModificationException { + // Renaming a file to an existing directory should fail + if (destinationDir.exists() && destinationDir.isFile()) { + throw new InvalidModificationException("Can't rename a file to a directory"); + } + + // Check to make sure we are not copying the directory into itself + if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) { + throw new InvalidModificationException("Can't copy itself into itself"); + } + + // See if the destination directory exists. If not create it. + if (!destinationDir.exists()) { + if (!destinationDir.mkdir()) { + // If we can't create the directory then fail + throw new NoModificationAllowedException("Couldn't create the destination directory"); + } + } + + + for (File file : srcDir.listFiles()) { + File destination = new File(destinationDir.getAbsoluteFile() + File.separator + file.getName()); + if (file.isDirectory()) { + copyDirectory(file, destination); + } else { + copyFile(file, destination); + } + } + + return getEntry(destinationDir); + } + + /** + * Check to see if the user attempted to copy an entry into its parent without changing its name, + * or attempted to copy a directory into a directory that it contains directly or indirectly. + * + * @param srcDir + * @param destinationDir + * @return + */ + private boolean isCopyOnItself(String src, String dest) { + + // This weird test is to determine if we are copying or moving a directory into itself. + // Copy /sdcard/myDir to /sdcard/myDir-backup is okay but + // Copy /sdcard/myDir to /sdcard/myDir/backup should throw an INVALID_MODIFICATION_ERR + if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) { + return true; + } + + return false; + } + + /** + * Move a file + * + * @param srcFile file to be copied + * @param destFile destination to be copied to + * @return a FileEntry object + * @throws IOException + * @throws InvalidModificationException + * @throws JSONException + */ + private JSONObject moveFile(File srcFile, File destFile) throws IOException, JSONException, InvalidModificationException { + // Renaming a file to an existing directory should fail + if (destFile.exists() && destFile.isDirectory()) { + throw new InvalidModificationException("Can't rename a file to a directory"); + } + + // Try to rename the file + if (!srcFile.renameTo(destFile)) { + // Trying to rename the file failed. Possibly because we moved across file system on the device. + // Now we have to do things the hard way + // 1) Copy all the old file + // 2) delete the src file + copyAction(srcFile, destFile); + if (destFile.exists()) { + srcFile.delete(); + } else { + throw new IOException("moved failed"); + } + } + + return getEntry(destFile); + } + + /** + * Move a directory + * + * @param srcDir directory to be copied + * @param destinationDir destination to be copied to + * @return a DirectoryEntry object + * @throws JSONException + * @throws IOException + * @throws InvalidModificationException + * @throws NoModificationAllowedException + * @throws FileExistsException + */ + private JSONObject moveDirectory(File srcDir, File destinationDir) throws IOException, JSONException, InvalidModificationException, NoModificationAllowedException, FileExistsException { + // Renaming a file to an existing directory should fail + if (destinationDir.exists() && destinationDir.isFile()) { + throw new InvalidModificationException("Can't rename a file to a directory"); + } + + // Check to make sure we are not copying the directory into itself + if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) { + throw new InvalidModificationException("Can't move itself into itself"); + } + + // If the destination directory already exists and is empty then delete it. This is according to spec. + if (destinationDir.exists()) { + if (destinationDir.list().length > 0) { + throw new InvalidModificationException("directory is not empty"); + } + } + + // Try to rename the directory + if (!srcDir.renameTo(destinationDir)) { + // Trying to rename the directory failed. Possibly because we moved across file system on the device. + // Now we have to do things the hard way + // 1) Copy all the old files + // 2) delete the src directory + copyDirectory(srcDir, destinationDir); + if (destinationDir.exists()) { + removeDirRecursively(srcDir); + } else { + throw new IOException("moved failed"); + } + } + + return getEntry(destinationDir); + } + + /** + * Deletes a directory and all of its contents, if any. In the event of an error + * [e.g. trying to delete a directory that contains a file that cannot be removed], + * some of the contents of the directory may be deleted. + * It is an error to attempt to delete the root directory of a filesystem. + * + * @param filePath the directory to be removed + * @return a boolean representing success of failure + * @throws FileExistsException + */ + private boolean removeRecursively(String filePath) throws FileExistsException { + File fp = createFileObject(filePath); + + // You can't delete the root directory. + if (atRootDirectory(filePath)) { + return false; + } + + return removeDirRecursively(fp); + } + + /** + * Loops through a directory deleting all the files. + * + * @param directory to be removed + * @return a boolean representing success of failure + * @throws FileExistsException + */ + private boolean removeDirRecursively(File directory) throws FileExistsException { + if (directory.isDirectory()) { + for (File file : directory.listFiles()) { + removeDirRecursively(file); + } + } + + if (!directory.delete()) { + throw new FileExistsException("could not delete: " + directory.getName()); + } else { + return true; + } + } + + /** + * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty. + * It is an error to attempt to delete the root directory of a filesystem. + * + * @param filePath file or directory to be removed + * @return a boolean representing success of failure + * @throws NoModificationAllowedException + * @throws InvalidModificationException + */ + private boolean remove(String filePath) throws NoModificationAllowedException, InvalidModificationException { + File fp = createFileObject(filePath); + + // You can't delete the root directory. + if (atRootDirectory(filePath)) { + throw new NoModificationAllowedException("You can't delete the root directory"); + } + + // You can't delete a directory that is not empty + if (fp.isDirectory() && fp.list().length > 0) { + throw new InvalidModificationException("You can't delete a directory that is not empty."); + } + + return fp.delete(); + } + + /** + * Creates or looks up a file. + * + * @param dirPath base directory + * @param fileName file/directory to lookup or create + * @param options specify whether to create or not + * @param directory if true look up directory, if false look up file + * @return a Entry object + * @throws FileExistsException + * @throws IOException + * @throws TypeMismatchException + * @throws EncodingException + * @throws JSONException + */ + private JSONObject getFile(String dirPath, String fileName, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException { + boolean create = false; + boolean exclusive = false; + if (options != null) { + create = options.optBoolean("create"); + if (create) { + exclusive = options.optBoolean("exclusive"); + } + } + + // Check for a ":" character in the file to line up with BB and iOS + if (fileName.contains(":")) { + throw new EncodingException("This file has a : in it's name"); + } + + File fp = createFileObject(dirPath, fileName); + + if (create) { + if (exclusive && fp.exists()) { + throw new FileExistsException("create/exclusive fails"); + } + if (directory) { + fp.mkdir(); + } else { + fp.createNewFile(); + } + if (!fp.exists()) { + throw new FileExistsException("create fails"); + } + } + else { + if (!fp.exists()) { + throw new FileNotFoundException("path does not exist"); + } + if (directory) { + if (fp.isFile()) { + throw new TypeMismatchException("path doesn't exist or is file"); + } + } else { + if (fp.isDirectory()) { + throw new TypeMismatchException("path doesn't exist or is directory"); + } + } + } + + // Return the directory + return getEntry(fp); + } + + /** + * If the path starts with a '/' just return that file object. If not construct the file + * object from the path passed in and the file name. + * + * @param dirPath root directory + * @param fileName new file name + * @return + */ + private File createFileObject(String dirPath, String fileName) { + File fp = null; + if (fileName.startsWith("/")) { + fp = new File(fileName); + } else { + dirPath = FileHelper.getRealPath(dirPath, cordova); + fp = new File(dirPath + File.separator + fileName); + } + return fp; + } + + /** + * Look up the parent DirectoryEntry containing this Entry. + * If this Entry is the root of its filesystem, its parent is itself. + * + * @param filePath + * @return + * @throws JSONException + */ + private JSONObject getParent(String filePath) throws JSONException { + filePath = FileHelper.getRealPath(filePath, cordova); + + if (atRootDirectory(filePath)) { + return getEntry(filePath); + } + return getEntry(new File(filePath).getParent()); + } + + /** + * Checks to see if we are at the root directory. Useful since we are + * not allow to delete this directory. + * + * @param filePath to directory + * @return true if we are at the root, false otherwise. + */ + private boolean atRootDirectory(String filePath) { + filePath = FileHelper.getRealPath(filePath, cordova); + + if (filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache") || + filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath()) || + filePath.equals("/data/data/" + cordova.getActivity().getPackageName())) { + return true; + } + return false; + } + + /** + * Create a File object from the passed in path + * + * @param filePath + * @return + */ + private File createFileObject(String filePath) { + filePath = FileHelper.getRealPath(filePath, cordova); + + File file = new File(filePath); + return file; + } + + /** + * Look up metadata about this entry. + * + * @param filePath to entry + * @return a long + * @throws FileNotFoundException + */ + private long getMetadata(String filePath) throws FileNotFoundException { + File file = createFileObject(filePath); + + if (!file.exists()) { + throw new FileNotFoundException("Failed to find file in getMetadata"); + } + + return file.lastModified(); + } + + /** + * Returns a File that represents the current state of the file that this FileEntry represents. + * + * @param filePath to entry + * @return returns a JSONObject represent a W3C File object + * @throws FileNotFoundException + * @throws JSONException + */ + private JSONObject getFileMetadata(String filePath) throws FileNotFoundException, JSONException { + File file = createFileObject(filePath); + + if (!file.exists()) { + throw new FileNotFoundException("File: " + filePath + " does not exist."); + } + + JSONObject metadata = new JSONObject(); + metadata.put("size", file.length()); + metadata.put("type", FileHelper.getMimeType(filePath, cordova)); + metadata.put("name", file.getName()); + metadata.put("fullPath", filePath); + metadata.put("lastModifiedDate", file.lastModified()); + + return metadata; + } + + /** + * Requests a filesystem in which to store application data. + * + * @param type of file system requested + * @return a JSONObject representing the file system + * @throws IOException + * @throws JSONException + */ + private JSONObject requestFileSystem(int type) throws IOException, JSONException { + JSONObject fs = new JSONObject(); + if (type == TEMPORARY) { + File fp; + fs.put("name", "temporary"); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + fp = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/"); + // Create the cache dir if it doesn't exist. + fp.mkdirs(); + fs.put("root", getEntry(Environment.getExternalStorageDirectory().getAbsolutePath() + + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/")); + } else { + fp = new File("/data/data/" + cordova.getActivity().getPackageName() + "/cache/"); + // Create the cache dir if it doesn't exist. + fp.mkdirs(); + fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName() + "/cache/")); + } + } + else if (type == PERSISTENT) { + fs.put("name", "persistent"); + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + fs.put("root", getEntry(Environment.getExternalStorageDirectory())); + } else { + fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName())); + } + } + else { + throw new IOException("No filesystem of type requested"); + } + + return fs; + } + + /** + * Returns a JSON object representing the given File. + * + * @param file the File to convert + * @return a JSON representation of the given File + * @throws JSONException + */ + public static JSONObject getEntry(File file) throws JSONException { + JSONObject entry = new JSONObject(); + + entry.put("isFile", file.isFile()); + entry.put("isDirectory", file.isDirectory()); + entry.put("name", file.getName()); + entry.put("fullPath", "file://" + file.getAbsolutePath()); + // The file system can't be specified, as it would lead to an infinite loop. + // entry.put("filesystem", null); + + return entry; + } + + /** + * Returns a JSON Object representing a directory on the device's file system + * + * @param path to the directory + * @return + * @throws JSONException + */ + private JSONObject getEntry(String path) throws JSONException { + return getEntry(new File(path)); + } + + /** + * Read the contents of a file. + * This is done in a background thread; the result is sent to the callback. + * + * @param filename The name of the file. + * @param start Start position in the file. + * @param end End position to stop at (exclusive). + * @param callbackContext The context through which to send the result. + * @param encoding The encoding to return contents as. Typical value is UTF-8. (see http://www.iana.org/assignments/character-sets) + * @param resultType The desired type of data to send to the callback. + * @return Contents of file. + */ + public void readFileAs(final String filename, final int start, final int end, final CallbackContext callbackContext, final String encoding, final int resultType) { + this.cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + byte[] bytes = readAsBinaryHelper(filename, start, end); + + PluginResult result; + switch (resultType) { + case PluginResult.MESSAGE_TYPE_STRING: + result = new PluginResult(PluginResult.Status.OK, new String(bytes, encoding)); + break; + case PluginResult.MESSAGE_TYPE_ARRAYBUFFER: + result = new PluginResult(PluginResult.Status.OK, bytes); + break; + case PluginResult.MESSAGE_TYPE_BINARYSTRING: + result = new PluginResult(PluginResult.Status.OK, bytes, true); + break; + default: // Base64. + String contentType = FileHelper.getMimeType(filename, cordova); + byte[] base64 = Base64.encode(bytes, Base64.NO_WRAP); + String s = "data:" + contentType + ";base64," + new String(base64, "US-ASCII"); + result = new PluginResult(PluginResult.Status.OK, s); + } + + callbackContext.sendPluginResult(result); + } catch (FileNotFoundException e) { + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR)); + } catch (IOException e) { + Log.d(LOG_TAG, e.getLocalizedMessage()); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, NOT_READABLE_ERR)); + } + } + }); + } + + /** + * Read the contents of a file as binary. + * This is done synchronously; the result is returned. + * + * @param filename The name of the file. + * @param start Start position in the file. + * @param end End position to stop at (exclusive). + * @return Contents of the file as a byte[]. + * @throws IOException + */ + private byte[] readAsBinaryHelper(String filename, int start, int end) throws IOException { + int numBytesToRead = end - start; + byte[] bytes = new byte[numBytesToRead]; + InputStream inputStream = FileHelper.getInputStreamFromUriString(filename, cordova); + int numBytesRead = 0; + + if (start > 0) { + inputStream.skip(start); + } + + while (numBytesToRead > 0 && (numBytesRead = inputStream.read(bytes, numBytesRead, numBytesToRead)) >= 0) { + numBytesToRead -= numBytesRead; + } + + return bytes; + } + + /** + * Write contents of file. + * + * @param filename The name of the file. + * @param data The contents of the file. + * @param offset The position to begin writing the file. + * @param isBinary True if the file contents are base64-encoded binary data + * @throws FileNotFoundException, IOException + * @throws NoModificationAllowedException + */ + /**/ + public long write(String filename, String data, int offset, boolean isBinary) throws FileNotFoundException, IOException, NoModificationAllowedException { + if (filename.startsWith("content://")) { + throw new NoModificationAllowedException("Couldn't write to file given its content URI"); + } + + filename = FileHelper.getRealPath(filename, cordova); + + boolean append = false; + if (offset > 0) { + this.truncateFile(filename, offset); + append = true; + } + + byte[] rawData; + if (isBinary) { + rawData = Base64.decode(data, Base64.DEFAULT); + } else { + rawData = data.getBytes(); + } + ByteArrayInputStream in = new ByteArrayInputStream(rawData); + try + { + FileOutputStream out = new FileOutputStream(filename, append); + byte buff[] = new byte[rawData.length]; + in.read(buff, 0, buff.length); + out.write(buff, 0, rawData.length); + out.flush(); + out.close(); + } + catch (NullPointerException e) + { + // This is a bug in the Android implementation of the Java Stack + NoModificationAllowedException realException = new NoModificationAllowedException(filename); + throw realException; + } + + return rawData.length; + } + + /** + * Truncate the file to size + * + * @param filename + * @param size + * @throws FileNotFoundException, IOException + * @throws NoModificationAllowedException + */ + private long truncateFile(String filename, long size) throws FileNotFoundException, IOException, NoModificationAllowedException { + if (filename.startsWith("content://")) { + throw new NoModificationAllowedException("Couldn't truncate file given its content URI"); + } + + filename = FileHelper.getRealPath(filename, cordova); + + RandomAccessFile raf = new RandomAccessFile(filename, "rw"); + try { + if (raf.length() >= size) { + FileChannel channel = raf.getChannel(); + channel.truncate(size); + return size; + } + + return raf.length(); + } finally { + raf.close(); + } + } +} |
