diff options
Diffstat (limited to 'plugins/org.apache.cordova.file/src')
13 files changed, 5684 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.file/src/android/DirectoryManager.java b/plugins/org.apache.cordova.file/src/android/DirectoryManager.java new file mode 100644 index 00000000..bcc005b2 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/DirectoryManager.java @@ -0,0 +1,133 @@ +/* + 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.os.Environment; +import android.os.StatFs; + +import java.io.File; + +/** + * This class provides file directory utilities. + * All file operations are performed on the SD card. + * + * It is used by the FileUtils class. + */ +public class DirectoryManager { + + @SuppressWarnings("unused") + private static final String LOG_TAG = "DirectoryManager"; + + /** + * Determine if a file or directory exists. + * @param name The name of the file to check. + * @return T=exists, F=not found + */ + public static boolean testFileExists(String name) { + boolean status; + + // If SD card exists + if ((testSaveLocationExists()) && (!name.equals(""))) { + File path = Environment.getExternalStorageDirectory(); + File newPath = constructFilePaths(path.toString(), name); + status = newPath.exists(); + } + // If no SD card + else { + status = false; + } + return status; + } + + /** + * Get the free disk space + * + * @return Size in KB or -1 if not available + */ + public static long getFreeDiskSpace(boolean checkInternal) { + String status = Environment.getExternalStorageState(); + long freeSpace = 0; + + // If SD card exists + if (status.equals(Environment.MEDIA_MOUNTED)) { + freeSpace = freeSpaceCalculation(Environment.getExternalStorageDirectory().getPath()); + } + else if (checkInternal) { + freeSpace = freeSpaceCalculation("/"); + } + // If no SD card and we haven't been asked to check the internal directory then return -1 + else { + return -1; + } + + return freeSpace; + } + + /** + * Given a path return the number of free KB + * + * @param path to the file system + * @return free space in KB + */ + private static long freeSpaceCalculation(String path) { + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + return availableBlocks * blockSize / 1024; + } + + /** + * Determine if SD card exists. + * + * @return T=exists, F=not found + */ + public static boolean testSaveLocationExists() { + String sDCardStatus = Environment.getExternalStorageState(); + boolean status; + + // If SD card is mounted + if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) { + status = true; + } + + // If no SD card + else { + status = false; + } + return status; + } + + /** + * Create a new file object from two file paths. + * + * @param file1 Base file path + * @param file2 Remaining file path + * @return File object + */ + private static File constructFilePaths (String file1, String file2) { + File newPath; + if (file2.startsWith(file1)) { + newPath = new File(file2); + } + else { + newPath = new File(file1 + "/" + file2); + } + return newPath; + } +} diff --git a/plugins/org.apache.cordova.file/src/android/EncodingException.java b/plugins/org.apache.cordova.file/src/android/EncodingException.java new file mode 100644 index 00000000..e9e1653b --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/EncodingException.java @@ -0,0 +1,29 @@ +/* + 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; + +@SuppressWarnings("serial") +public class EncodingException extends Exception { + + public EncodingException(String message) { + super(message); + } + +} diff --git a/plugins/org.apache.cordova.file/src/android/FileExistsException.java b/plugins/org.apache.cordova.file/src/android/FileExistsException.java new file mode 100644 index 00000000..5c4d83dc --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/FileExistsException.java @@ -0,0 +1,29 @@ +/* + 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; + +@SuppressWarnings("serial") +public class FileExistsException extends Exception { + + public FileExistsException(String msg) { + super(msg); + } + +} diff --git a/plugins/org.apache.cordova.file/src/android/FileHelper.java b/plugins/org.apache.cordova.file/src/android/FileHelper.java new file mode 100644 index 00000000..867f128c --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/FileHelper.java @@ -0,0 +1,158 @@ +/* + 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.webkit.MimeTypeMap; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.LOG; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +public class FileHelper { + private static final String LOG_TAG = "FileUtils"; + private static final String _DATA = "_data"; + + /** + * Returns the real path of the given URI string. + * If the given URI string represents a content:// URI, the real path is retrieved from the media store. + * + * @param uriString the URI string of the audio/image/video + * @param cordova the current application context + * @return the full path to the file + */ + @SuppressWarnings("deprecation") + public static String getRealPath(String uriString, CordovaInterface cordova) { + String realPath = null; + + if (uriString.startsWith("content://")) { + String[] proj = { _DATA }; + Cursor cursor = cordova.getActivity().managedQuery(Uri.parse(uriString), proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(_DATA); + cursor.moveToFirst(); + realPath = cursor.getString(column_index); + if (realPath == null) { + LOG.e(LOG_TAG, "Could get real path for URI string %s", uriString); + } + } else if (uriString.startsWith("file://")) { + realPath = uriString.substring(7); + if (realPath.startsWith("/android_asset/")) { + LOG.e(LOG_TAG, "Cannot get real path for URI string %s because it is a file:///android_asset/ URI.", uriString); + realPath = null; + } + } else { + realPath = uriString; + } + + return realPath; + } + + /** + * Returns the real path of the given URI. + * If the given URI is a content:// URI, the real path is retrieved from the media store. + * + * @param uri the URI of the audio/image/video + * @param cordova the current application context + * @return the full path to the file + */ + public static String getRealPath(Uri uri, CordovaInterface cordova) { + return FileHelper.getRealPath(uri.toString(), cordova); + } + + /** + * Returns an input stream based on given URI string. + * + * @param uriString the URI string from which to obtain the input stream + * @param cordova the current application context + * @return an input stream into the data at the given URI or null if given an invalid URI string + * @throws IOException + */ + public static InputStream getInputStreamFromUriString(String uriString, CordovaInterface cordova) throws IOException { + if (uriString.startsWith("content")) { + Uri uri = Uri.parse(uriString); + return cordova.getActivity().getContentResolver().openInputStream(uri); + } else if (uriString.startsWith("file://")) { + int question = uriString.indexOf("?"); + if (question > -1) { + uriString = uriString.substring(0,question); + } + if (uriString.startsWith("file:///android_asset/")) { + Uri uri = Uri.parse(uriString); + String relativePath = uri.getPath().substring(15); + return cordova.getActivity().getAssets().open(relativePath); + } else { + return new FileInputStream(getRealPath(uriString, cordova)); + } + } else { + return new FileInputStream(getRealPath(uriString, cordova)); + } + } + + /** + * Removes the "file://" prefix from the given URI string, if applicable. + * If the given URI string doesn't have a "file://" prefix, it is returned unchanged. + * + * @param uriString the URI string to operate on + * @return a path without the "file://" prefix + */ + public static String stripFileProtocol(String uriString) { + if (uriString.startsWith("file://")) { + uriString = uriString.substring(7); + } + return uriString; + } + + public static String getMimeTypeForExtension(String path) { + String extension = path; + int lastDot = extension.lastIndexOf('.'); + if (lastDot != -1) { + extension = extension.substring(lastDot + 1); + } + // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185). + extension = extension.toLowerCase(Locale.getDefault()); + if (extension.equals("3ga")) { + return "audio/3gpp"; + } + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + + /** + * Returns the mime type of the data specified by the given URI string. + * + * @param uriString the URI string of the data + * @return the mime type of the specified data + */ + public static String getMimeType(String uriString, CordovaInterface cordova) { + String mimeType = null; + + Uri uri = Uri.parse(uriString); + if (uriString.startsWith("content://")) { + mimeType = cordova.getActivity().getContentResolver().getType(uri); + } else { + mimeType = getMimeTypeForExtension(uri.getPath()); + } + + return mimeType; + } +} 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(); + } + } +} diff --git a/plugins/org.apache.cordova.file/src/android/InvalidModificationException.java b/plugins/org.apache.cordova.file/src/android/InvalidModificationException.java new file mode 100644 index 00000000..8f6bec59 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/InvalidModificationException.java @@ -0,0 +1,30 @@ +/* + 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; + +@SuppressWarnings("serial") +public class InvalidModificationException extends Exception { + + public InvalidModificationException(String message) { + super(message); + } + +} diff --git a/plugins/org.apache.cordova.file/src/android/NoModificationAllowedException.java b/plugins/org.apache.cordova.file/src/android/NoModificationAllowedException.java new file mode 100644 index 00000000..627eafb5 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/NoModificationAllowedException.java @@ -0,0 +1,29 @@ +/* + 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; + +@SuppressWarnings("serial") +public class NoModificationAllowedException extends Exception { + + public NoModificationAllowedException(String message) { + super(message); + } + +} diff --git a/plugins/org.apache.cordova.file/src/android/TypeMismatchException.java b/plugins/org.apache.cordova.file/src/android/TypeMismatchException.java new file mode 100644 index 00000000..1315f9a9 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/android/TypeMismatchException.java @@ -0,0 +1,30 @@ +/* + 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; + +@SuppressWarnings("serial") +public class TypeMismatchException extends Exception { + + public TypeMismatchException(String message) { + super(message); + } + +} diff --git a/plugins/org.apache.cordova.file/src/blackberry10/index.js b/plugins/org.apache.cordova.file/src/blackberry10/index.js new file mode 100644 index 00000000..914d9663 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/blackberry10/index.js @@ -0,0 +1,10 @@ +module.exports = { + setSandbox : function (success, fail, args, env) { + require("lib/webview").setSandbox(JSON.parse(decodeURIComponent(args[0]))); + new PluginResult(args, env).noResult(false); + }, + + isSandboxed : function (success, fail, args, env) { + new PluginResult(args, env).ok(require("lib/webview").getSandbox() === "1"); + } +}; diff --git a/plugins/org.apache.cordova.file/src/ios/CDVFile.h b/plugins/org.apache.cordova.file/src/ios/CDVFile.h new file mode 100644 index 00000000..54d25f08 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/ios/CDVFile.h @@ -0,0 +1,107 @@ +/* + 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. + */ + +#import <Foundation/Foundation.h> +#import <Cordova/CDVPlugin.h> + +enum CDVFileError { + NO_ERROR = 0, + NOT_FOUND_ERR = 1, + SECURITY_ERR = 2, + ABORT_ERR = 3, + NOT_READABLE_ERR = 4, + ENCODING_ERR = 5, + NO_MODIFICATION_ALLOWED_ERR = 6, + INVALID_STATE_ERR = 7, + SYNTAX_ERR = 8, + INVALID_MODIFICATION_ERR = 9, + QUOTA_EXCEEDED_ERR = 10, + TYPE_MISMATCH_ERR = 11, + PATH_EXISTS_ERR = 12 +}; +typedef int CDVFileError; + +enum CDVFileSystemType { + TEMPORARY = 0, + PERSISTENT = 1 +}; +typedef int CDVFileSystemType; + +extern NSString* const kCDVAssetsLibraryPrefix; + +@interface CDVFile : CDVPlugin { + NSString* appDocsPath; + NSString* appLibraryPath; + NSString* appTempPath; + NSString* persistentPath; + NSString* temporaryPath; + + BOOL userHasAllowed; +} +- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath; +- (NSString*)getAppPath:(NSString*)pathFragment; +// -(NSString*) getFullPath: (NSString*)pathFragment; +- (void)requestFileSystem:(CDVInvokedUrlCommand*)command; +- (NSDictionary*)getDirectoryEntry:(NSString*)fullPath isDirectory:(BOOL)isDir; +- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command; +- (void)getDirectory:(CDVInvokedUrlCommand*)command; +- (void)getFile:(CDVInvokedUrlCommand*)command; +- (void)getParent:(CDVInvokedUrlCommand*)command; +- (void)getMetadata:(CDVInvokedUrlCommand*)command; +- (void)removeRecursively:(CDVInvokedUrlCommand*)command; +- (void)remove:(CDVInvokedUrlCommand*)command; +- (CDVPluginResult*)doRemove:(NSString*)fullPath; +- (void)copyTo:(CDVInvokedUrlCommand*)command; +- (void)moveTo:(CDVInvokedUrlCommand*)command; +- (BOOL)canCopyMoveSrc:(NSString*)src ToDestination:(NSString*)dest; +- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy; +// - (void) toURI:(CDVInvokedUrlCommand*)command; +- (void)getFileMetadata:(CDVInvokedUrlCommand*)command; +- (void)readEntries:(CDVInvokedUrlCommand*)command; + +- (void)readAsText:(CDVInvokedUrlCommand*)command; +- (void)readAsDataURL:(CDVInvokedUrlCommand*)command; +- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command; +- (NSString*)getMimeTypeFromPath:(NSString*)fullPath; +- (void)write:(CDVInvokedUrlCommand*)command; +- (void)testFileExists:(CDVInvokedUrlCommand*)command; +- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command; +// - (void) createDirectory:(CDVInvokedUrlCommand*)command; +// - (void) deleteDirectory:(CDVInvokedUrlCommand*)command; +// - (void) deleteFile:(CDVInvokedUrlCommand*)command; +- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command; +- (void)truncate:(CDVInvokedUrlCommand*)command; + +// - (BOOL) fileExists:(NSString*)fileName; +// - (BOOL) directoryExists:(NSString*)dirName; +- (void)writeToFile:(NSString*)fileName withData:(NSData*)data append:(BOOL)shouldAppend callback:(NSString*)callbackId; +- (void)writeToFile:(NSString*)fileName withString:(NSString*)data encoding:(NSStringEncoding)encoding append:(BOOL)shouldAppend callback:(NSString*)callbackId; +- (unsigned long long)truncateFile:(NSString*)filePath atPosition:(unsigned long long)pos; + +@property (nonatomic, strong) NSString* appDocsPath; +@property (nonatomic, strong) NSString* appLibraryPath; +@property (nonatomic, strong) NSString* appTempPath; +@property (nonatomic, strong) NSString* persistentPath; +@property (nonatomic, strong) NSString* temporaryPath; +@property BOOL userHasAllowed; + +@end + +#define kW3FileTemporary @"temporary" +#define kW3FilePersistent @"persistent" diff --git a/plugins/org.apache.cordova.file/src/ios/CDVFile.m b/plugins/org.apache.cordova.file/src/ios/CDVFile.m new file mode 100644 index 00000000..6b2602d3 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/ios/CDVFile.m @@ -0,0 +1,1417 @@ +/* + 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. + */ + +#import "CDVFile.h" +#import <Cordova/CDV.h> +#import <AssetsLibrary/ALAsset.h> +#import <AssetsLibrary/ALAssetRepresentation.h> +#import <AssetsLibrary/ALAssetsLibrary.h> +#import <MobileCoreServices/MobileCoreServices.h> +#import <sys/xattr.h> + +extern NSString * const NSURLIsExcludedFromBackupKey __attribute__((weak_import)); + +#ifndef __IPHONE_5_1 + NSString* const NSURLIsExcludedFromBackupKey = @"NSURLIsExcludedFromBackupKey"; +#endif + +NSString* const kCDVAssetsLibraryPrefix = @"assets-library://"; + +@implementation CDVFile + +@synthesize appDocsPath, appLibraryPath, appTempPath, persistentPath, temporaryPath, userHasAllowed; + +- (id)initWithWebView:(UIWebView*)theWebView +{ + self = (CDVFile*)[super initWithWebView:theWebView]; + if (self) { + // get the documents directory path + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + self.appDocsPath = [paths objectAtIndex:0]; + + paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + self.appLibraryPath = [paths objectAtIndex:0]; + + self.appTempPath = [NSTemporaryDirectory()stringByStandardizingPath]; // remove trailing slash from NSTemporaryDirectory() + + self.persistentPath = [NSString stringWithFormat:@"/%@", [self.appDocsPath lastPathComponent]]; + self.temporaryPath = [NSString stringWithFormat:@"/%@", [self.appTempPath lastPathComponent]]; + // NSLog(@"docs: %@ - temp: %@", self.appDocsPath, self.appTempPath); + } + + return self; +} + +- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath +{ + NSFileManager* fMgr = [[NSFileManager alloc] init]; + + NSError* __autoreleasing pError = nil; + + NSDictionary* pDict = [fMgr attributesOfFileSystemForPath:appPath error:&pError]; + NSNumber* pNumAvail = (NSNumber*)[pDict objectForKey:NSFileSystemFreeSize]; + + return pNumAvail; +} + +// figure out if the pathFragment represents a persistent of temporary directory and return the full application path. +// returns nil if path is not persistent or temporary +- (NSString*)getAppPath:(NSString*)pathFragment +{ + NSString* appPath = nil; + NSRange rangeP = [pathFragment rangeOfString:self.persistentPath]; + NSRange rangeT = [pathFragment rangeOfString:self.temporaryPath]; + + if ((rangeP.location != NSNotFound) && (rangeT.location != NSNotFound)) { + // we found both in the path, return whichever one is first + if (rangeP.length < rangeT.length) { + appPath = self.appDocsPath; + } else { + appPath = self.appTempPath; + } + } else if (rangeP.location != NSNotFound) { + appPath = self.appDocsPath; + } else if (rangeT.location != NSNotFound) { + appPath = self.appTempPath; + } + return appPath; +} + +/* get the full path to this resource + * IN + * NSString* pathFragment - full Path from File or Entry object (includes system path info) + * OUT + * NSString* fullPath - full iOS path to this resource, nil if not found + */ + +/* Was here in order to NOT have to return full path, but W3C synchronous DirectoryEntry.toURI() killed that idea since I can't call into iOS to + * resolve full URI. Leaving this code here in case W3C spec changes. +-(NSString*) getFullPath: (NSString*)pathFragment +{ + return pathFragment; + NSString* fullPath = nil; + NSString *appPath = [ self getAppPath: pathFragment]; + if (appPath){ + + // remove last component from appPath + NSRange range = [appPath rangeOfString:@"/" options: NSBackwardsSearch]; + NSString* newPath = [appPath substringToIndex:range.location]; + // add pathFragment to get test Path + fullPath = [newPath stringByAppendingPathComponent:pathFragment]; + } + return fullPath; +} */ + +/* Request the File System info + * + * IN: + * arguments[0] - type (number as string) + * TEMPORARY = 0, PERSISTENT = 1; + * arguments[1] - size + * + * OUT: + * Dictionary representing FileSystem object + * name - the human readable directory name + * root = DirectoryEntry object + * bool isDirectory + * bool isFile + * string name + * string fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference !! + */ + +- (void)requestFileSystem:(CDVInvokedUrlCommand*)command +{ + NSArray* arguments = command.arguments; + + // arguments + NSString* strType = [arguments objectAtIndex:0]; + unsigned long long size = [[arguments objectAtIndex:1] longLongValue]; + + int type = [strType intValue]; + CDVPluginResult* result = nil; + + if (type > 1) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:NOT_FOUND_ERR]; + NSLog(@"iOS only supports TEMPORARY and PERSISTENT file systems"); + } else { + // NSString* fullPath = [NSString stringWithFormat:@"/%@", (type == 0 ? [self.appTempPath lastPathComponent] : [self.appDocsPath lastPathComponent])]; + NSString* fullPath = (type == 0 ? self.appTempPath : self.appDocsPath); + // check for avail space for size request + NSNumber* pNumAvail = [self checkFreeDiskSpace:fullPath]; + // NSLog(@"Free space: %@", [NSString stringWithFormat:@"%qu", [ pNumAvail unsignedLongLongValue ]]); + if (pNumAvail && ([pNumAvail unsignedLongLongValue] < size)) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:QUOTA_EXCEEDED_ERR]; + } else { + NSMutableDictionary* fileSystem = [NSMutableDictionary dictionaryWithCapacity:2]; + [fileSystem setObject:(type == TEMPORARY ? kW3FileTemporary : kW3FilePersistent) forKey:@"name"]; + NSDictionary* dirEntry = [self getDirectoryEntry:fullPath isDirectory:YES]; + [fileSystem setObject:dirEntry forKey:@"root"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* Creates a dictionary representing an Entry Object + * + * IN: + * NSString* fullPath of the entry + * FileSystem type + * BOOL isDirectory - YES if this is a directory, NO if is a file + * OUT: + * NSDictionary* + Entry object + * bool as NSNumber isDirectory + * bool as NSNumber isFile + * NSString* name - last part of path + * NSString* fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference FileSystem contains DirectoryEntry which contains FileSystem.....!! + */ +- (NSDictionary*)getDirectoryEntry:(NSString*)fullPath isDirectory:(BOOL)isDir +{ + NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:4]; + NSString* lastPart = [fullPath lastPathComponent]; + + [dirEntry setObject:[NSNumber numberWithBool:!isDir] forKey:@"isFile"]; + [dirEntry setObject:[NSNumber numberWithBool:isDir] forKey:@"isDirectory"]; + // NSURL* fileUrl = [NSURL fileURLWithPath:fullPath]; + // [dirEntry setObject: [fileUrl absoluteString] forKey: @"fullPath"]; + [dirEntry setObject:fullPath forKey:@"fullPath"]; + [dirEntry setObject:lastPart forKey:@"name"]; + + return dirEntry; +} + +/* + * Given a URI determine the File System information associated with it and return an appropriate W3C entry object + * IN + * NSString* fileURI - currently requires full file URI + * OUT + * Entry object + * bool isDirectory + * bool isFile + * string name + * string fullPath + * fileSystem = FileSystem object - !! ignored because creates circular reference FileSystem contains DirectoryEntry which contains FileSystem.....!! + */ +- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* inputUri = [command.arguments objectAtIndex:0]; + + // don't know if string is encoded or not so unescape + NSString* cleanUri = [inputUri stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + // now escape in order to create URL + NSString* strUri = [cleanUri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSURL* testUri = [NSURL URLWithString:strUri]; + CDVPluginResult* result = nil; + + if (!testUri) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR]; + } else if ([testUri isFileURL]) { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSString* path = [testUri path]; + // NSLog(@"url path: %@", path); + BOOL isDir = NO; + // see if exists and is file or dir + BOOL bExists = [fileMgr fileExistsAtPath:path isDirectory:&isDir]; + if (bExists) { + // see if it contains docs path or temp path + NSString* foundFullPath = nil; + if ([path hasPrefix:self.appDocsPath]) { + foundFullPath = self.appDocsPath; + } else if ([path hasPrefix:self.appTempPath]) { + foundFullPath = self.appTempPath; + } + + if (foundFullPath == nil) { + // error SECURITY_ERR - not one of the two paths types supported + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:SECURITY_ERR]; + } else { + NSDictionary* fileSystem = [self getDirectoryEntry:path isDirectory:isDir]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } + } else { + // return NOT_FOUND_ERR + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + } else if ([strUri hasPrefix:@"assets-library://"]) { + NSDictionary* fileSystem = [self getDirectoryEntry:strUri isDirectory:NO]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileSystem]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:ENCODING_ERR]; + } + + if (result != nil) { + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } +} + +/* Part of DirectoryEntry interface, creates or returns the specified directory + * IN: + * NSString* fullPath - full path for this directory + * NSString* path - directory to be created/returned; may be full path or relative path + * NSDictionary* - Flags object + * boolean as NSNumber create - + * if create is true and directory does not exist, create dir and return directory entry + * if create is true and exclusive is true and directory does exist, return error + * if create is false and directory does not exist, return error + * if create is false and the path represents a file, return error + * boolean as NSNumber exclusive - used in conjunction with create + * if exclusive is true and create is true - specifies failure if directory already exists + * + * + */ +- (void)getDirectory:(CDVInvokedUrlCommand*)command +{ + NSMutableArray* arguments = [NSMutableArray arrayWithArray:command.arguments]; + NSMutableDictionary* options = nil; + + if ([arguments count] >= 3) { + options = [arguments objectAtIndex:2 withDefault:nil]; + } + // add getDir to options and call getFile() + if (options != nil) { + options = [NSMutableDictionary dictionaryWithDictionary:options]; + } else { + options = [NSMutableDictionary dictionaryWithCapacity:1]; + } + [options setObject:[NSNumber numberWithInt:1] forKey:@"getDir"]; + if ([arguments count] >= 3) { + [arguments replaceObjectAtIndex:2 withObject:options]; + } else { + [arguments addObject:options]; + } + CDVInvokedUrlCommand* subCommand = + [[CDVInvokedUrlCommand alloc] initWithArguments:arguments + callbackId:command.callbackId + className:command.className + methodName:command.methodName]; + + [self getFile:subCommand]; +} + +/* Part of DirectoryEntry interface, creates or returns the specified file + * IN: + * NSString* fullPath - full path for this file + * NSString* path - file to be created/returned; may be full path or relative path + * NSDictionary* - Flags object + * boolean as NSNumber create - + * if create is true and file does not exist, create file and return File entry + * if create is true and exclusive is true and file does exist, return error + * if create is false and file does not exist, return error + * if create is false and the path represents a directory, return error + * boolean as NSNumber exclusive - used in conjunction with create + * if exclusive is true and create is true - specifies failure if file already exists + * + * + */ +- (void)getFile:(CDVInvokedUrlCommand*)command +{ + // arguments are URL encoded + NSString* fullPath = [command.arguments objectAtIndex:0]; + NSString* requestedPath = [command.arguments objectAtIndex:1]; + NSDictionary* options = [command.arguments objectAtIndex:2 withDefault:nil]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"getFile not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + BOOL bDirRequest = NO; + BOOL create = NO; + BOOL exclusive = NO; + int errorCode = 0; // !!! risky - no error code currently defined for 0 + + if ([options valueForKeyIsNumber:@"create"]) { + create = [(NSNumber*)[options valueForKey:@"create"] boolValue]; + } + if ([options valueForKeyIsNumber:@"exclusive"]) { + exclusive = [(NSNumber*)[options valueForKey:@"exclusive"] boolValue]; + } + + if ([options valueForKeyIsNumber:@"getDir"]) { + // this will not exist for calls directly to getFile but will have been set by getDirectory before calling this method + bDirRequest = [(NSNumber*)[options valueForKey:@"getDir"] boolValue]; + } + // see if the requested path has invalid characters - should we be checking for more than just ":"? + if ([requestedPath rangeOfString:@":"].location != NSNotFound) { + errorCode = ENCODING_ERR; + } else { + // was full or relative path provided? + NSRange range = [requestedPath rangeOfString:fullPath]; + BOOL bIsFullPath = range.location != NSNotFound; + + NSString* reqFullPath = nil; + + if (!bIsFullPath) { + reqFullPath = [fullPath stringByAppendingPathComponent:requestedPath]; + } else { + reqFullPath = requestedPath; + } + + // NSLog(@"reqFullPath = %@", reqFullPath); + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir; + BOOL bExists = [fileMgr fileExistsAtPath:reqFullPath isDirectory:&bIsDir]; + if (bExists && (create == NO) && (bIsDir == !bDirRequest)) { + // path exists and is of requested type - return TYPE_MISMATCH_ERR + errorCode = TYPE_MISMATCH_ERR; + } else if (!bExists && (create == NO)) { + // path does not exist and create is false - return NOT_FOUND_ERR + errorCode = NOT_FOUND_ERR; + } else if (bExists && (create == YES) && (exclusive == YES)) { + // file/dir already exists and exclusive and create are both true - return PATH_EXISTS_ERR + errorCode = PATH_EXISTS_ERR; + } else { + // if bExists and create == YES - just return data + // if bExists and create == NO - just return data + // if !bExists and create == YES - create and return data + BOOL bSuccess = YES; + NSError __autoreleasing* pError = nil; + if (!bExists && (create == YES)) { + if (bDirRequest) { + // create the dir + bSuccess = [fileMgr createDirectoryAtPath:reqFullPath withIntermediateDirectories:NO attributes:nil error:&pError]; + } else { + // create the empty file + bSuccess = [fileMgr createFileAtPath:reqFullPath contents:nil attributes:nil]; + } + } + if (!bSuccess) { + errorCode = ABORT_ERR; + if (pError) { + NSLog(@"error creating directory: %@", [pError localizedDescription]); + } + } else { + // NSLog(@"newly created file/dir (%@) exists: %d", reqFullPath, [fileMgr fileExistsAtPath:reqFullPath]); + // file existed or was created + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:reqFullPath isDirectory:bDirRequest]]; + } + } // are all possible conditions met? + } + + if (errorCode > 0) { + // create error callback + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * Look up the parent Entry containing this Entry. + * If this Entry is the root of its filesystem, its parent is itself. + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * NSMutableDictionary* options + * empty + */ +- (void)getParent:(CDVInvokedUrlCommand*)command +{ + // arguments are URL encoded + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // we don't (yet?) support getting the parent of an asset + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_READABLE_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + NSString* newPath = nil; + + if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + // return self + newPath = fullPath; + } else { + // since this call is made from an existing Entry object - the parent should already exist so no additional error checking + // remove last component and return Entry + NSRange range = [fullPath rangeOfString:@"/" options:NSBackwardsSearch]; + newPath = [fullPath substringToIndex:range.location]; + } + + if (newPath) { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir; + BOOL bExists = [fileMgr fileExistsAtPath:newPath isDirectory:&bIsDir]; + if (bExists) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:newPath isDirectory:bIsDir]]; + } + } + if (!result) { + // invalid path or file does not exist + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * get MetaData of entry + * Currently MetaData only includes modificationTime. + */ +- (void)getMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + __block CDVPluginResult* result = nil; + + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Retrieve the metadata and send it off. + NSDate* date = [asset valueForProperty:ALAssetPropertyDate]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:[date timeIntervalSince1970] * 1000]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + // TODO(maxw): Consider making this a class variable since it's the same every time. + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:argPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } + + NSString* testPath = argPath; // [self getFullPath: argPath]; + + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* __autoreleasing error = nil; + + NSDictionary* fileAttribs = [fileMgr attributesOfItemAtPath:testPath error:&error]; + + if (fileAttribs) { + NSDate* modDate = [fileAttribs fileModificationDate]; + if (modDate) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:[modDate timeIntervalSince1970] * 1000]; + } + } else { + // didn't get fileAttribs + CDVFileError errorCode = ABORT_ERR; + NSLog(@"error getting metadata: %@", [error localizedDescription]); + if ([error code] == NSFileNoSuchFileError) { + errorCode = NOT_FOUND_ERR; + } + // log [NSNumber numberWithDouble: theMessage] objCtype to see what it returns + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode]; + } + if (!result) { + // invalid path or file does not exist + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* + * set MetaData of entry + * Currently we only support "com.apple.MobileBackup" (boolean) + */ +- (void)setMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* filePath = [command.arguments objectAtIndex:0]; + NSDictionary* options = [command.arguments objectAtIndex:1 withDefault:nil]; + CDVPluginResult* result = nil; + BOOL ok = NO; + + // setMetadata doesn't make sense for asset library files + if (![filePath hasPrefix:kCDVAssetsLibraryPrefix]) { + // we only care about this iCloud key for now. + // set to 1/true to skip backup, set to 0/false to back it up (effectively removing the attribute) + NSString* iCloudBackupExtendedAttributeKey = @"com.apple.MobileBackup"; + id iCloudBackupExtendedAttributeValue = [options objectForKey:iCloudBackupExtendedAttributeKey]; + + if ((iCloudBackupExtendedAttributeValue != nil) && [iCloudBackupExtendedAttributeValue isKindOfClass:[NSNumber class]]) { + if (IsAtLeastiOSVersion(@"5.1")) { + NSURL* url = [NSURL fileURLWithPath:filePath]; + NSError* __autoreleasing error = nil; + + ok = [url setResourceValue:[NSNumber numberWithBool:[iCloudBackupExtendedAttributeValue boolValue]] forKey:NSURLIsExcludedFromBackupKey error:&error]; + } else { // below 5.1 (deprecated - only really supported in 5.01) + u_int8_t value = [iCloudBackupExtendedAttributeValue intValue]; + if (value == 0) { // remove the attribute (allow backup, the default) + ok = (removexattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], 0) == 0); + } else { // set the attribute (skip backup) + ok = (setxattr([filePath fileSystemRepresentation], [iCloudBackupExtendedAttributeKey cStringUsingEncoding:NSUTF8StringEncoding], &value, sizeof(value), 0, 0) == 0); + } + } + } + } + + if (ok) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* removes the directory or file entry + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * + * returns NO_MODIFICATION_ALLOWED_ERR if is top level directory or no permission to delete dir + * returns INVALID_MODIFICATION_ERR if is non-empty dir or asset library file + * returns NOT_FOUND_ERR if file or dir is not found +*/ +- (void)remove:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + CDVPluginResult* result = nil; + CDVFileError errorCode = 0; // !! 0 not currently defined + + // return error for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + errorCode = INVALID_MODIFICATION_ERR; + } else if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + // error if try to remove top level (documents or tmp) dir + errorCode = NO_MODIFICATION_ALLOWED_ERR; + } else { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir = NO; + BOOL bExists = [fileMgr fileExistsAtPath:fullPath isDirectory:&bIsDir]; + if (!bExists) { + errorCode = NOT_FOUND_ERR; + } + if (bIsDir && ([[fileMgr contentsOfDirectoryAtPath:fullPath error:nil] count] != 0)) { + // dir is not empty + errorCode = INVALID_MODIFICATION_ERR; + } + } + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } else { + // perform actual remove + result = [self doRemove:fullPath]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* recursively removes the directory + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * + * returns NO_MODIFICATION_ALLOWED_ERR if is top level directory or no permission to delete dir + * returns NOT_FOUND_ERR if file or dir is not found + */ +- (void)removeRecursively:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"removeRecursively not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + + // error if try to remove top level (documents or tmp) dir + if ([fullPath isEqualToString:self.appDocsPath] || [fullPath isEqualToString:self.appTempPath]) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + } else { + result = [self doRemove:fullPath]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* remove the file or directory (recursively) + * IN: + * NSString* fullPath - the full path to the file or directory to be removed + * NSString* callbackId + * called from remove and removeRecursively - check all pubic api specific error conditions (dir not empty, etc) before calling + */ + +- (CDVPluginResult*)doRemove:(NSString*)fullPath +{ + CDVPluginResult* result = nil; + BOOL bSuccess = NO; + NSError* __autoreleasing pError = nil; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + + @try { + bSuccess = [fileMgr removeItemAtPath:fullPath error:&pError]; + if (bSuccess) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + // see if we can give a useful error + CDVFileError errorCode = ABORT_ERR; + NSLog(@"error getting metadata: %@", [pError localizedDescription]); + if ([pError code] == NSFileNoSuchFileError) { + errorCode = NOT_FOUND_ERR; + } else if ([pError code] == NSFileWriteNoPermissionError) { + errorCode = NO_MODIFICATION_ALLOWED_ERR; + } + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + } @catch(NSException* e) { // NSInvalidArgumentException if path is . or .. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:SYNTAX_ERR]; + } + + return result; +} + +- (void)copyTo:(CDVInvokedUrlCommand*)command +{ + [self doCopyMove:command isCopy:YES]; +} + +- (void)moveTo:(CDVInvokedUrlCommand*)command +{ + [self doCopyMove:command isCopy:NO]; +} + +/** + * Helper function to 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. + * + * IN: + * NSString* srcDir + * NSString* destinationDir + * OUT: + * YES copy/ move is allows + * NO move is onto itself + */ +- (BOOL)canCopyMoveSrc:(NSString*)src ToDestination:(NSString*)dest +{ + // This weird test is to determine if we are copying or moving a directory into itself. + // Copy /Documents/myDir to /Documents/myDir-backup is okay but + // Copy /Documents/myDir to /Documents/myDir/backup not okay + BOOL copyOK = YES; + NSRange range = [dest rangeOfString:src]; + + if (range.location != NSNotFound) { + NSRange testRange = {range.length - 1, ([dest length] - range.length)}; + NSRange resultRange = [dest rangeOfString:@"/" options:0 range:testRange]; + if (resultRange.location != NSNotFound) { + copyOK = NO; + } + } + return copyOK; +} + +/* Copy/move a file or directory to a new location + * IN: + * NSArray* arguments + * 0 - NSString* fullPath of entry + * 1 - NSString* newName the new name of the entry, defaults to the current name + * NSMutableDictionary* options - DirectoryEntry to which to copy the entry + * BOOL - bCopy YES if copy, NO if move + * + */ +- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy +{ + NSArray* arguments = command.arguments; + + // arguments + NSString* srcFullPath = [arguments objectAtIndex:0]; + NSString* destRootPath = [arguments objectAtIndex:1]; + // optional argument + NSString* newName = ([arguments count] > 2) ? [arguments objectAtIndex:2] : [srcFullPath lastPathComponent]; // use last component from appPath if new name not provided + + __block CDVPluginResult* result = nil; + CDVFileError errCode = 0; // !! Currently 0 is not defined, use this to signal error !! + + /*NSString* destRootPath = nil; + NSString* key = @"fullPath"; + if([options valueForKeyIsString:key]){ + destRootPath = [options objectForKey:@"fullPath"]; + }*/ + + if (!destRootPath) { + // no destination provided + errCode = NOT_FOUND_ERR; + } else if ([newName rangeOfString:@":"].location != NSNotFound) { + // invalid chars in new name + errCode = ENCODING_ERR; + } else { + NSString* newFullPath = [destRootPath stringByAppendingPathComponent:newName]; + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + if ([newFullPath isEqualToString:srcFullPath]) { + // source and destination can not be the same + errCode = INVALID_MODIFICATION_ERR; + } else if ([srcFullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + if (bCopy) { + // Copying (as opposed to moving) an assets library file is okay. + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and try to copy it over. + if (![fileMgr fileExistsAtPath:destRootPath]) { + // The destination directory doesn't exist. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } else if ([fileMgr fileExistsAtPath:newFullPath]) { + // A file already exists at the destination path. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:PATH_EXISTS_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + // We're good to go! Write the file to the new destination. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [data writeToFile:newFullPath atomically:YES]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self getDirectoryEntry:newFullPath isDirectory:NO]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:srcFullPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + // Moving an assets library file is not doable, since we can't remove it. + errCode = INVALID_MODIFICATION_ERR; + } + } else { + BOOL bSrcIsDir = NO; + BOOL bDestIsDir = NO; + BOOL bNewIsDir = NO; + BOOL bSrcExists = [fileMgr fileExistsAtPath:srcFullPath isDirectory:&bSrcIsDir]; + BOOL bDestExists = [fileMgr fileExistsAtPath:destRootPath isDirectory:&bDestIsDir]; + BOOL bNewExists = [fileMgr fileExistsAtPath:newFullPath isDirectory:&bNewIsDir]; + if (!bSrcExists || !bDestExists) { + // the source or the destination root does not exist + errCode = NOT_FOUND_ERR; + } else if (bSrcIsDir && (bNewExists && !bNewIsDir)) { + // can't copy/move dir to file + errCode = INVALID_MODIFICATION_ERR; + } else { // no errors yet + NSError* __autoreleasing error = nil; + BOOL bSuccess = NO; + if (bCopy) { + if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFullPath] /*[newFullPath hasPrefix:srcFullPath]*/) { + // can't copy dir into self + errCode = INVALID_MODIFICATION_ERR; + } else if (bNewExists) { + // the full destination should NOT already exist if a copy + errCode = PATH_EXISTS_ERR; + } else { + bSuccess = [fileMgr copyItemAtPath:srcFullPath toPath:newFullPath error:&error]; + } + } else { // move + // iOS requires that destination must not exist before calling moveTo + // is W3C INVALID_MODIFICATION_ERR error if destination dir exists and has contents + // + if (!bSrcIsDir && (bNewExists && bNewIsDir)) { + // can't move a file to directory + errCode = INVALID_MODIFICATION_ERR; + } else if (bSrcIsDir && ![self canCopyMoveSrc:srcFullPath ToDestination:newFullPath]) { // [newFullPath hasPrefix:srcFullPath]){ + // can't move a dir into itself + errCode = INVALID_MODIFICATION_ERR; + } else if (bNewExists) { + if (bNewIsDir && ([[fileMgr contentsOfDirectoryAtPath:newFullPath error:NULL] count] != 0)) { + // can't move dir to a dir that is not empty + errCode = INVALID_MODIFICATION_ERR; + newFullPath = nil; // so we won't try to move + } else { + // remove destination so can perform the moveItemAtPath + bSuccess = [fileMgr removeItemAtPath:newFullPath error:NULL]; + if (!bSuccess) { + errCode = INVALID_MODIFICATION_ERR; // is this the correct error? + newFullPath = nil; + } + } + } else if (bNewIsDir && [newFullPath hasPrefix:srcFullPath]) { + // can't move a directory inside itself or to any child at any depth; + errCode = INVALID_MODIFICATION_ERR; + newFullPath = nil; + } + + if (newFullPath != nil) { + bSuccess = [fileMgr moveItemAtPath:srcFullPath toPath:newFullPath error:&error]; + } + } + if (bSuccess) { + // should verify it is there and of the correct type??? + NSDictionary* newEntry = [self getDirectoryEntry:newFullPath isDirectory:bSrcIsDir]; // should be the same type as source + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry]; + } else { + errCode = INVALID_MODIFICATION_ERR; // catch all + if (error) { + if (([error code] == NSFileReadUnknownError) || ([error code] == NSFileReadTooLargeError)) { + errCode = NOT_READABLE_ERR; + } else if ([error code] == NSFileWriteOutOfSpaceError) { + errCode = QUOTA_EXCEEDED_ERR; + } else if ([error code] == NSFileWriteNoPermissionError) { + errCode = NO_MODIFICATION_ALLOWED_ERR; + } + } + } + } + } + } + if (errCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errCode]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +/* return the URI to the entry + * IN: + * NSArray* arguments + * 0 - NSString* fullPath of entry + * 1 - desired mime type of entry - ignored - always returns file:// + */ + +/* Not needed since W3C toURI is synchronous. Leaving code here for now in case W3C spec changes..... +- (void) toURI:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSString* argPath = [command.arguments objectAtIndex:0]; + PluginResult* result = nil; + NSString* jsString = nil; + + NSString* fullPath = [self getFullPath: argPath]; + if (fullPath) { + // do we need to make sure the file actually exists? + // create file uri + NSString* strUri = [fullPath stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; + NSURL* fileUrl = [NSURL fileURLWithPath:strUri]; + if (fileUrl) { + result = [PluginResult resultWithStatus:CDVCommandStatus_OK messageAsString: [fileUrl absoluteString]]; + jsString = [result toSuccessCallbackString:callbackId]; + } // else NOT_FOUND_ERR + } + if(!jsString) { + // was error + result = [PluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt: NOT_FOUND_ERR cast: @"window.localFileSystem._castError"]; + jsString = [result toErrorCallbackString:callbackId]; + } + + [self writeJavascript:jsString]; +}*/ +- (void)getFileMetadata:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + __block CDVPluginResult* result = nil; + + NSString* fullPath = argPath; // [self getFullPath: argPath]; + + if (fullPath) { + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Populate the dictionary and send it off. + NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5]; + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:[assetRepresentation size]] forKey:@"size"]; + [fileInfo setObject:argPath forKey:@"fullPath"]; + NSString* filename = [assetRepresentation filename]; + [fileInfo setObject:filename forKey:@"name"]; + [fileInfo setObject:[self getMimeTypeFromPath:filename] forKey:@"type"]; + NSDate* creationDate = [asset valueForProperty:ALAssetPropertyDate]; + NSNumber* msDate = [NSNumber numberWithDouble:[creationDate timeIntervalSince1970] * 1000]; + [fileInfo setObject:msDate forKey:@"lastModifiedDate"]; + + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } else { + // We couldn't find the asset. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:argPath] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + BOOL bIsDir = NO; + // make sure it exists and is not a directory + BOOL bExists = [fileMgr fileExistsAtPath:fullPath isDirectory:&bIsDir]; + if (!bExists || bIsDir) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } else { + // create dictionary of file info + NSError* __autoreleasing error = nil; + NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:&error]; + NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5]; + [fileInfo setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"]; + [fileInfo setObject:argPath forKey:@"fullPath"]; + [fileInfo setObject:@"" forKey:@"type"]; // can't easily get the mimetype unless create URL, send request and read response so skipping + [fileInfo setObject:[argPath lastPathComponent] forKey:@"name"]; + NSDate* modDate = [fileAttrs fileModificationDate]; + NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000]; + [fileInfo setObject:msDate forKey:@"lastModifiedDate"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]; + } + } + } + if (!result) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_INSTANTIATION_EXCEPTION]; + } + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)readEntries:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* fullPath = [command.arguments objectAtIndex:0]; + + // return unsupported result for assets-library URLs + if ([fullPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"readEntries not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + + NSFileManager* fileMgr = [[NSFileManager alloc] init]; + NSError* __autoreleasing error = nil; + NSArray* contents = [fileMgr contentsOfDirectoryAtPath:fullPath error:&error]; + + if (contents) { + NSMutableArray* entries = [NSMutableArray arrayWithCapacity:1]; + if ([contents count] > 0) { + // create an Entry (as JSON) for each file/dir + for (NSString* name in contents) { + // see if is dir or file + NSString* entryPath = [fullPath stringByAppendingPathComponent:name]; + BOOL bIsDir = NO; + [fileMgr fileExistsAtPath:entryPath isDirectory:&bIsDir]; + NSDictionary* entryDict = [self getDirectoryEntry:entryPath isDirectory:bIsDir]; + [entries addObject:entryDict]; + } + } + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:entries]; + } else { + // assume not found but could check error for more specific error conditions + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)readFileWithPath:(NSString*)path start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback +{ + if (path == nil) { + callback(nil, nil, SYNTAX_ERR); + } else { + [self.commandDelegate runInBackground:^ { + if ([path hasPrefix:kCDVAssetsLibraryPrefix]) { + // In this case, we need to use an asynchronous method to retrieve the file. + // Because of this, we can't just assign to `result` and send it at the end of the method. + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it off. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType); + + callback(data, MIMEType, NO_ERROR); + } else { + callback(nil, nil, NOT_FOUND_ERR); + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + NSLog(@"Error: %@", error); + callback(nil, nil, SECURITY_ERR); + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:path] resultBlock:resultBlock failureBlock:failureBlock]; + } else { + NSString* mimeType = [self getMimeTypeFromPath:path]; + if (mimeType == nil) { + mimeType = @"*/*"; + } + NSFileHandle* file = [NSFileHandle fileHandleForReadingAtPath:path]; + if (start > 0) { + [file seekToFileOffset:start]; + } + + NSData* readData; + if (end < 0) { + readData = [file readDataToEndOfFile]; + } else { + readData = [file readDataOfLength:(end - start)]; + } + + [file closeFile]; + + callback(readData, mimeType, readData != nil ? NO_ERROR : NOT_FOUND_ERR); + } + }]; + } +} + +/* read and return file data + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* encoding + * 2 - NSString* start + * 3 - NSString* end + */ +- (void)readAsText:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* path = [command argumentAtIndex:0]; + NSString* encoding = [command argumentAtIndex:1]; + NSInteger start = [[command argumentAtIndex:2] integerValue]; + NSInteger end = [[command argumentAtIndex:3] integerValue]; + + // TODO: implement + if ([@"UTF-8" caseInsensitiveCompare : encoding] != NSOrderedSame) { + NSLog(@"Only UTF-8 encodings are currently supported by readAsText"); + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ENCODING_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + NSString* str = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSUTF8StringEncoding freeWhenDone:NO]; + // Check that UTF8 conversion did not fail. + if (str != nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str]; + result.associatedObject = data; + } else { + errorCode = ENCODING_ERR; + } + } + if (result == nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* Read content of text file and return as base64 encoded data url. + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* start + * 2 - NSString* end + * + * Determines the mime type from the file extension, returns ENCODING_ERR if mimetype can not be determined. + */ + +- (void)readAsDataURL:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + // TODO: Would be faster to base64 encode directly to the final string. + NSString* output = [NSString stringWithFormat:@"data:%@;base64,%@", mimeType, [data base64EncodedString]]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:output]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* Read content of text file and return as an arraybuffer + * IN: + * NSArray* arguments + * 0 - NSString* fullPath + * 1 - NSString* start + * 2 - NSString* end + */ + +- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:data]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +- (void)readAsBinaryString:(CDVInvokedUrlCommand*)command +{ + NSString* path = [command argumentAtIndex:0]; + NSInteger start = [[command argumentAtIndex:1] integerValue]; + NSInteger end = [[command argumentAtIndex:2] integerValue]; + + [self readFileWithPath:path start:start end:end callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) { + CDVPluginResult* result = nil; + if (data != nil) { + NSString* payload = [[NSString alloc] initWithBytesNoCopy:(void*)[data bytes] length:[data length] encoding:NSASCIIStringEncoding freeWhenDone:NO]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:payload]; + result.associatedObject = data; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode]; + } + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }]; +} + +/* helper function to get the mimeType from the file extension + * IN: + * NSString* fullPath - filename (may include path) + * OUT: + * NSString* the mime type as type/subtype. nil if not able to determine + */ +- (NSString*)getMimeTypeFromPath:(NSString*)fullPath +{ + NSString* mimeType = nil; + + if (fullPath) { + CFStringRef typeId = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fullPath pathExtension], NULL); + if (typeId) { + mimeType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(typeId, kUTTagClassMIMEType); + if (!mimeType) { + // special case for m4a + if ([(__bridge NSString*)typeId rangeOfString : @"m4a-audio"].location != NSNotFound) { + mimeType = @"audio/mp4"; + } else if ([[fullPath pathExtension] rangeOfString:@"wav"].location != NSNotFound) { + mimeType = @"audio/wav"; + } + } + CFRelease(typeId); + } + } + return mimeType; +} + +- (void)truncate:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + unsigned long long pos = (unsigned long long)[[command.arguments objectAtIndex:1] longLongValue]; + + // assets-library files can't be truncated + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSString* appFile = argPath; // [self getFullPath:argPath]; + + unsigned long long newPos = [self truncateFile:appFile atPosition:pos]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:newPos]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (unsigned long long)truncateFile:(NSString*)filePath atPosition:(unsigned long long)pos +{ + unsigned long long newPos = 0UL; + + NSFileHandle* file = [NSFileHandle fileHandleForWritingAtPath:filePath]; + + if (file) { + [file truncateFileAtOffset:(unsigned long long)pos]; + newPos = [file offsetInFile]; + [file synchronizeFile]; + [file closeFile]; + } + return newPos; +} + +/* write + * IN: + * NSArray* arguments + * 0 - NSString* file path to write to + * 1 - NSString* or NSData* data to write + * 2 - NSNumber* position to begin writing + */ +- (void)write:(CDVInvokedUrlCommand*)command +{ + NSString* callbackId = command.callbackId; + NSArray* arguments = command.arguments; + + // arguments + NSString* argPath = [arguments objectAtIndex:0]; + id argData = [arguments objectAtIndex:1]; + unsigned long long pos = (unsigned long long)[[arguments objectAtIndex:2] longLongValue]; + + // text can't be written into assets-library files + if ([argPath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSString* fullPath = argPath; // [self getFullPath:argPath]; + + [self truncateFile:fullPath atPosition:pos]; + + if ([argData isKindOfClass:[NSString class]]) { + [self writeToFile:fullPath withString:argData encoding:NSUTF8StringEncoding append:YES callback:callbackId]; + } else if ([argData isKindOfClass:[NSData class]]) { + [self writeToFile:fullPath withData:argData append:YES callback:callbackId]; + } else { + CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Invalid parameter type"]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + +} + +- (void)writeToFile:(NSString*)filePath withData:(NSData*)encData append:(BOOL)shouldAppend callback:(NSString*)callbackId +{ + CDVPluginResult* result = nil; + CDVFileError errCode = INVALID_MODIFICATION_ERR; + int bytesWritten = 0; + + if (filePath) { + NSOutputStream* fileStream = [NSOutputStream outputStreamToFileAtPath:filePath append:shouldAppend]; + if (fileStream) { + NSUInteger len = [encData length]; + [fileStream open]; + + bytesWritten = [fileStream write:[encData bytes] maxLength:len]; + + [fileStream close]; + if (bytesWritten > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:bytesWritten]; + // } else { + // can probably get more detailed error info via [fileStream streamError] + // errCode already set to INVALID_MODIFICATION_ERR; + // bytesWritten = 0; // may be set to -1 on error + } + } // else fileStream not created return INVALID_MODIFICATION_ERR + } else { + // invalid filePath + errCode = NOT_FOUND_ERR; + } + if (!result) { + // was an error + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode]; + } + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)writeToFile:(NSString*)filePath withString:(NSString*)stringData encoding:(NSStringEncoding)encoding append:(BOOL)shouldAppend callback:(NSString*)callbackId +{ + [self writeToFile:filePath withData:[stringData dataUsingEncoding:encoding allowLossyConversion:YES] append:shouldAppend callback:callbackId]; +} + +- (void)testFileExists:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + // Get the file manager + NSFileManager* fMgr = [NSFileManager defaultManager]; + NSString* appFile = argPath; // [ self getFullPath: argPath]; + + BOOL bExists = [fMgr fileExistsAtPath:appFile]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(bExists ? 1 : 0)]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command +{ + // arguments + NSString* argPath = [command.arguments objectAtIndex:0]; + + // Get the file manager + NSFileManager* fMgr = [[NSFileManager alloc] init]; + NSString* appFile = argPath; // [self getFullPath: argPath]; + BOOL bIsDir = NO; + BOOL bExists = [fMgr fileExistsAtPath:appFile isDirectory:&bIsDir]; + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:((bExists && bIsDir) ? 1 : 0)]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +// Returns number of bytes available via callback +- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command +{ + // no arguments + + NSNumber* pNumAvail = [self checkFreeDiskSpace:self.appDocsPath]; + + NSString* strFreeSpace = [NSString stringWithFormat:@"%qu", [pNumAvail unsignedLongLongValue]]; + // NSLog(@"Free space is %@", strFreeSpace ); + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:strFreeSpace]; + + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +@end diff --git a/plugins/org.apache.cordova.file/src/windows8/FileProxy.js b/plugins/org.apache.cordova.file/src/windows8/FileProxy.js new file mode 100644 index 00000000..1445ae70 --- /dev/null +++ b/plugins/org.apache.cordova.file/src/windows8/FileProxy.js @@ -0,0 +1,845 @@ +/* + * + * 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. + * +*/ + +var cordova = require('cordova'); +var Entry = require('./Entry'), + File = require('./File'), + FileEntry = require('./FileEntry'), + FileError = require('./FileError'), + DirectoryEntry = require('./DirectoryEntry'), + Flags = require('./Flags'), + FileSystem = require('./FileSystem'), + LocalFileSystem = require('./LocalFileSystem'); + +module.exports = { + + getFileMetadata:function(win,fail,args) { + var fullPath = args[0]; + + Windows.Storage.StorageFile.getFileFromPathAsync(fullPath).done( + function (storageFile) { + storageFile.getBasicPropertiesAsync().then( + function (basicProperties) { + win(new File(storageFile.name, storageFile.path, storageFile.fileType, basicProperties.dateModified, basicProperties.size)); + }, function () { + fail && fail(FileError.NOT_READABLE_ERR); + } + ); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + getMetadata:function(success,fail,args) { + var fullPath = args[0]; + + var dealFile = function (sFile) { + Windows.Storage.StorageFile.getFileFromPathAsync(fullPath).then( + function (storageFile) { + return storageFile.getBasicPropertiesAsync(); + }, + function () { + fail && fail(FileError.NOT_READABLE_ERR); + } + // get the basic properties of the file. + ).then( + function (basicProperties) { + success(basicProperties.dateModified); + }, + function () { + fail && fail(FileError.NOT_READABLE_ERR); + } + ); + }; + + var dealFolder = function (sFolder) { + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (storageFolder) { + return storageFolder.getBasicPropertiesAsync(); + }, + function () { + fail && fail(FileError.NOT_READABLE_ERR); + } + // get the basic properties of the folder. + ).then( + function (basicProperties) { + success(basicProperties.dateModified); + }, + function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }; + + Windows.Storage.StorageFile.getFileFromPathAsync(fullPath).then( + // the path is file. + function (sFile) { + dealFile(sFile); + }, + // the path is folder + function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (sFolder) { + dealFolder(sFolder); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + ); + }, + + getParent:function(win,fail,args) { // ["fullPath"] + var fullPath = args[0]; + + var storageFolderPer = Windows.Storage.ApplicationData.current.localFolder; + var storageFolderTem = Windows.Storage.ApplicationData.current.temporaryFolder; + + if (fullPath == storageFolderPer.path) { + win(new DirectoryEntry(storageFolderPer.name, storageFolderPer.path)); + return; + } else if (fullPath == storageFolderTem.path) { + win(new DirectoryEntry(storageFolderTem.name, storageFolderTem.path)); + return; + } + var splitArr = fullPath.split(new RegExp(/\/|\\/g)); + + var popItem = splitArr.pop(); + + var result = new DirectoryEntry(popItem, fullPath.substr(0, fullPath.length - popItem.length - 1)); + Windows.Storage.StorageFolder.getFolderFromPathAsync(result.fullPath).done( + function () { win(result); }, + function () { fail && fail(FileError.INVALID_STATE_ERR); } + ); + }, + + readAsText:function(win,fail,args) { + var fileName = args[0]; + var enc = args[1]; + + Windows.Storage.StorageFile.getFileFromPathAsync(fileName).done( + function (storageFile) { + var value = Windows.Storage.Streams.UnicodeEncoding.utf8; + if (enc == 'Utf16LE' || enc == 'utf16LE') { + value = Windows.Storage.Streams.UnicodeEncoding.utf16LE; + }else if (enc == 'Utf16BE' || enc == 'utf16BE') { + value = Windows.Storage.Streams.UnicodeEncoding.utf16BE; + } + Windows.Storage.FileIO.readTextAsync(storageFile, value).done( + function (fileContent) { + win(fileContent); + }, + function () { + fail && fail(FileError.ENCODING_ERR); + } + ); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + readAsDataURL:function(win,fail,args) { + var fileName = args[0]; + + + Windows.Storage.StorageFile.getFileFromPathAsync(fileName).then( + function (storageFile) { + Windows.Storage.FileIO.readBufferAsync(storageFile).done( + function (buffer) { + var strBase64 = Windows.Security.Cryptography.CryptographicBuffer.encodeToBase64String(buffer); + //the method encodeToBase64String will add "77u/" as a prefix, so we should remove it + if(String(strBase64).substr(0,4) == "77u/") { + strBase64 = strBase64.substr(4); + } + var mediaType = storageFile.contentType; + var result = "data:" + mediaType + ";base64," + strBase64; + win(result); + } + ); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + getDirectory:function(win,fail,args) { + var fullPath = args[0]; + var path = args[1]; + var options = args[2]; + + var flag = ""; + if (options !== null) { + flag = new Flags(options.create, options.exclusive); + } else { + flag = new Flags(false, false); + } + + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (storageFolder) { + if (flag.create === true && flag.exclusive === true) { + storageFolder.createFolderAsync(path, Windows.Storage.CreationCollisionOption.failIfExists).done( + function (storageFolder) { + win(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, function () { + fail && fail(FileError.PATH_EXISTS_ERR); + } + ); + } else if (flag.create === true && flag.exclusive === false) { + storageFolder.createFolderAsync(path, Windows.Storage.CreationCollisionOption.openIfExists).done( + function (storageFolder) { + win(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + } + ); + } else if (flag.create === false) { + if (/\?|\\|\*|\||\"|<|>|\:|\//g.test(path)) { + fail && fail(FileError.ENCODING_ERR); + return; + } + + storageFolder.getFolderAsync(path).done( + function (storageFolder) { + win(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + remove:function(win,fail,args) { + var fullPath = args[0]; + + Windows.Storage.StorageFile.getFileFromPathAsync(fullPath).then( + function (sFile) { + Windows.Storage.StorageFile.getFileFromPathAsync(fullPath).done(function (storageFile) { + storageFile.deleteAsync().done(win, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + + }); + }); + }, + function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (sFolder) { + var removeEntry = function () { + var storageFolderTop = null; + + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (storageFolder) { + // FileSystem root can't be removed! + var storageFolderPer = Windows.Storage.ApplicationData.current.localFolder; + var storageFolderTem = Windows.Storage.ApplicationData.current.temporaryFolder; + if (fullPath == storageFolderPer.path || fullPath == storageFolderTem.path) { + fail && fail(FileError.NO_MODIFICATION_ALLOWED_ERR); + return; + } + storageFolderTop = storageFolder; + return storageFolder.createFileQuery().getFilesAsync(); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + + } + // check sub-files. + ).then(function (fileList) { + if (fileList) { + if (fileList.length === 0) { + return storageFolderTop.createFolderQuery().getFoldersAsync(); + } else { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + } + } + // check sub-folders. + }).then(function (folderList) { + if (folderList) { + if (folderList.length === 0) { + storageFolderTop.deleteAsync().done(win, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + + }); + } else { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + } + } + + }); + }; + removeEntry(); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + ); + }, + + removeRecursively:function(successCallback,fail,args) { + var fullPath = args[0]; + + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).done(function (storageFolder) { + var storageFolderPer = Windows.Storage.ApplicationData.current.localFolder; + var storageFolderTem = Windows.Storage.ApplicationData.current.temporaryFolder; + + if (storageFolder.path == storageFolderPer.path || storageFolder.path == storageFolderTem.path) { + fail && fail(FileError.NO_MODIFICATION_ALLOWED_ERR); + return; + } + + var removeFolders = function (path) { + return new WinJS.Promise(function (complete) { + var filePromiseArr = []; + var storageFolderTop = null; + Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then( + function (storageFolder) { + var fileListPromise = storageFolder.createFileQuery().getFilesAsync(); + + storageFolderTop = storageFolder; + return fileListPromise; + } + // remove all the files directly under the folder. + ).then(function (fileList) { + if (fileList !== null) { + for (var i = 0; i < fileList.length; i++) { + var filePromise = fileList[i].deleteAsync(); + filePromiseArr.push(filePromise); + } + } + WinJS.Promise.join(filePromiseArr).then(function () { + var folderListPromise = storageFolderTop.createFolderQuery().getFoldersAsync(); + return folderListPromise; + // remove empty folders. + }).then(function (folderList) { + var folderPromiseArr = []; + if (folderList.length !== 0) { + for (var j = 0; j < folderList.length; j++) { + + folderPromiseArr.push(removeFolders(folderList[j].path)); + } + WinJS.Promise.join(folderPromiseArr).then(function () { + storageFolderTop.deleteAsync().then(complete); + }); + } else { + storageFolderTop.deleteAsync().then(complete); + } + }, function () { }); + }, function () { }); + }); + }; + removeFolders(storageFolder.path).then(function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(storageFolder.path).then( + function () {}, + function () { + if (typeof successCallback !== 'undefined' && successCallback !== null) { successCallback(); } + }); + }); + }); + }, + + getFile:function(win,fail,args) { + var fullPath = args[0]; + var path = args[1]; + var options = args[2]; + + var flag = ""; + if (options !== null) { + flag = new Flags(options.create, options.exclusive); + } else { + flag = new Flags(false, false); + } + + Windows.Storage.StorageFolder.getFolderFromPathAsync(fullPath).then( + function (storageFolder) { + if (flag.create === true && flag.exclusive === true) { + storageFolder.createFileAsync(path, Windows.Storage.CreationCollisionOption.failIfExists).done( + function (storageFile) { + win(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + fail && fail(FileError.PATH_EXISTS_ERR); + } + ); + } else if (flag.create === true && flag.exclusive === false) { + storageFolder.createFileAsync(path, Windows.Storage.CreationCollisionOption.openIfExists).done( + function (storageFile) { + win(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + } + ); + } else if (flag.create === false) { + if (/\?|\\|\*|\||\"|<|>|\:|\//g.test(path)) { + fail && fail(FileError.ENCODING_ERR); + return; + } + storageFolder.getFileAsync(path).done( + function (storageFile) { + win(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + readEntries:function(win,fail,args) { // ["fullPath"] + var path = args[0]; + + var result = []; + + Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then(function (storageFolder) { + var promiseArr = []; + var index = 0; + promiseArr[index++] = storageFolder.createFileQuery().getFilesAsync().then(function (fileList) { + if (fileList !== null) { + for (var i = 0; i < fileList.length; i++) { + result.push(new FileEntry(fileList[i].name, fileList[i].path)); + } + } + }); + promiseArr[index++] = storageFolder.createFolderQuery().getFoldersAsync().then(function (folderList) { + if (folderList !== null) { + for (var j = 0; j < folderList.length; j++) { + result.push(new FileEntry(folderList[j].name, folderList[j].path)); + } + } + }); + WinJS.Promise.join(promiseArr).then(function () { + win(result); + }); + + }, function () { fail && fail(FileError.NOT_FOUND_ERR); }); + }, + + write:function(win,fail,args) { + var fileName = args[0]; + var text = args[1]; + var position = args[2]; + + Windows.Storage.StorageFile.getFileFromPathAsync(fileName).done( + function (storageFile) { + Windows.Storage.FileIO.writeTextAsync(storageFile,text,Windows.Storage.Streams.UnicodeEncoding.utf8).done( + function() { + win(String(text).length); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + } + ); + }, function() { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + }, + + truncate:function(win,fail,args) { // ["fileName","size"] + var fileName = args[0]; + var size = args[1]; + + Windows.Storage.StorageFile.getFileFromPathAsync(fileName).done(function(storageFile){ + //the current length of the file. + var leng = 0; + + storageFile.getBasicPropertiesAsync().then(function (basicProperties) { + leng = basicProperties.size; + if (Number(size) >= leng) { + win(this.length); + return; + } + if (Number(size) >= 0) { + Windows.Storage.FileIO.readTextAsync(storageFile, Windows.Storage.Streams.UnicodeEncoding.utf8).then(function (fileContent) { + fileContent = fileContent.substr(0, size); + var fullPath = storageFile.path; + var name = storageFile.name; + var entry = new Entry(true, false, name, fullPath); + var parentPath = ""; + var successCallBack = function (entry) { + parentPath = entry.fullPath; + storageFile.deleteAsync().then(function () { + return Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath); + }).then(function (storageFolder) { + storageFolder.createFileAsync(name).then(function (newStorageFile) { + Windows.Storage.FileIO.writeTextAsync(newStorageFile, fileContent).done(function () { + win(String(fileContent).length); + }, function () { + fail && fail(FileError.NO_MODIFICATION_ALLOWED_ERR); + }); + }); + }); + }; + entry.getParent(successCallBack, null); + }, function () { fail && fail(FileError.NOT_FOUND_ERR); }); + } + }); + }, function () { fail && fail(FileError.NOT_FOUND_ERR); }); + }, + + copyTo:function(success,fail,args) { // ["fullPath","parent", "newName"] + var srcPath = args[0]; + var parentFullPath = args[1]; + var name = args[2]; + + //name can't be invalid + if (/\?|\\|\*|\||\"|<|>|\:|\//g.test(name)) { + fail && fail(FileError.ENCODING_ERR); + return; + } + // copy + var copyFiles = ""; + Windows.Storage.StorageFile.getFileFromPathAsync(srcPath).then( + function (sFile) { + copyFiles = function (srcPath, parentPath) { + var storageFileTop = null; + Windows.Storage.StorageFile.getFileFromPathAsync(srcPath).then(function (storageFile) { + storageFileTop = storageFile; + return Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath); + }, function () { + + fail && fail(FileError.NOT_FOUND_ERR); + }).then(function (storageFolder) { + storageFileTop.copyAsync(storageFolder, name, Windows.Storage.NameCollisionOption.failIfExists).then(function (storageFile) { + + success(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + + fail && fail(FileError.INVALID_MODIFICATION_ERR); + }); + }, function () { + + fail && fail(FileError.NOT_FOUND_ERR); + }); + }; + var copyFinish = function (srcPath, parentPath) { + copyFiles(srcPath, parentPath); + }; + copyFinish(srcPath, parentFullPath); + }, + function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(srcPath).then( + function (sFolder) { + copyFiles = function (srcPath, parentPath) { + var coreCopy = function (storageFolderTop, complete) { + storageFolderTop.createFolderQuery().getFoldersAsync().then(function (folderList) { + var folderPromiseArr = []; + if (folderList.length === 0) { complete(); } + else { + Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath).then(function (storageFolderTarget) { + var tempPromiseArr = []; + var index = 0; + for (var j = 0; j < folderList.length; j++) { + tempPromiseArr[index++] = storageFolderTarget.createFolderAsync(folderList[j].name).then(function (targetFolder) { + folderPromiseArr.push(copyFiles(folderList[j].path, targetFolder.path)); + }); + } + WinJS.Promise.join(tempPromiseArr).then(function () { + WinJS.Promise.join(folderPromiseArr).then(complete); + }); + }); + } + }); + }; + + return new WinJS.Promise(function (complete) { + var storageFolderTop = null; + var filePromiseArr = []; + var fileListTop = null; + Windows.Storage.StorageFolder.getFolderFromPathAsync(srcPath).then(function (storageFolder) { + storageFolderTop = storageFolder; + return storageFolder.createFileQuery().getFilesAsync(); + }).then(function (fileList) { + fileListTop = fileList; + if (fileList) { + return Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath); + } + }).then(function (targetStorageFolder) { + for (var i = 0; i < fileListTop.length; i++) { + filePromiseArr.push(fileListTop[i].copyAsync(targetStorageFolder)); + } + WinJS.Promise.join(filePromiseArr).then(function () { + coreCopy(storageFolderTop, complete); + }); + }); + }); + }; + var copyFinish = function (srcPath, parentPath) { + Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath).then(function (storageFolder) { + storageFolder.createFolderAsync(name, Windows.Storage.CreationCollisionOption.openIfExists).then(function (newStorageFolder) { + //can't copy onto itself + if (srcPath == newStorageFolder.path) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + //can't copy into itself + if (srcPath == parentPath) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + copyFiles(srcPath, newStorageFolder.path).then(function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(newStorageFolder.path).done( + function (storageFolder) { + success(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, + function () { fail && fail(FileError.NOT_FOUND_ERR); } + ); + }); + }, function () { fail && fail(FileError.INVALID_MODIFICATION_ERR); }); + }, function () { fail && fail(FileError.INVALID_MODIFICATION_ERR); }); + }; + copyFinish(srcPath, parentFullPath); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + ); + }, + + moveTo:function(success,fail,args) { + var srcPath = args[0]; + var parentFullPath = args[1]; + var name = args[2]; + + + //name can't be invalid + if (/\?|\\|\*|\||\"|<|>|\:|\//g.test(name)) { + fail && fail(FileError.ENCODING_ERR); + return; + } + + var moveFiles = ""; + Windows.Storage.StorageFile.getFileFromPathAsync(srcPath).then( + function (sFile) { + moveFiles = function (srcPath, parentPath) { + var storageFileTop = null; + Windows.Storage.StorageFile.getFileFromPathAsync(srcPath).then(function (storageFile) { + storageFileTop = storageFile; + return Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + }).then(function (storageFolder) { + storageFileTop.moveAsync(storageFolder, name, Windows.Storage.NameCollisionOption.replaceExisting).then(function () { + success(new FileEntry(name, storageFileTop.path)); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + }); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + }); + }; + var moveFinish = function (srcPath, parentPath) { + //can't copy onto itself + if (srcPath == parentPath + "\\" + name) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + moveFiles(srcPath, parentFullPath); + }; + moveFinish(srcPath, parentFullPath); + }, + function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(srcPath).then( + function (sFolder) { + moveFiles = function (srcPath, parentPath) { + var coreMove = function (storageFolderTop, complete) { + storageFolderTop.createFolderQuery().getFoldersAsync().then(function (folderList) { + var folderPromiseArr = []; + if (folderList.length === 0) { + // If failed, we must cancel the deletion of folders & files.So here wo can't delete the folder. + complete(); + } + else { + Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath).then(function (storageFolderTarget) { + var tempPromiseArr = []; + var index = 0; + for (var j = 0; j < folderList.length; j++) { + tempPromiseArr[index++] = storageFolderTarget.createFolderAsync(folderList[j].name).then(function (targetFolder) { + folderPromiseArr.push(moveFiles(folderList[j].path, targetFolder.path)); + }); + } + WinJS.Promise.join(tempPromiseArr).then(function () { + WinJS.Promise.join(folderPromiseArr).then(complete); + }); + }); + } + }); + }; + return new WinJS.Promise(function (complete) { + var storageFolderTop = null; + Windows.Storage.StorageFolder.getFolderFromPathAsync(srcPath).then(function (storageFolder) { + storageFolderTop = storageFolder; + return storageFolder.createFileQuery().getFilesAsync(); + }).then(function (fileList) { + var filePromiseArr = []; + Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath).then(function (dstStorageFolder) { + if (fileList) { + for (var i = 0; i < fileList.length; i++) { + filePromiseArr.push(fileList[i].moveAsync(dstStorageFolder)); + } + } + WinJS.Promise.join(filePromiseArr).then(function () { + coreMove(storageFolderTop, complete); + }, function () { }); + }); + }); + }); + }; + var moveFinish = function (srcPath, parentPath) { + var originFolderTop = null; + Windows.Storage.StorageFolder.getFolderFromPathAsync(srcPath).then(function (originFolder) { + originFolderTop = originFolder; + return Windows.Storage.StorageFolder.getFolderFromPathAsync(parentPath); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + }).then(function (storageFolder) { + return storageFolder.createFolderAsync(name, Windows.Storage.CreationCollisionOption.openIfExists); + }, function () { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + }).then(function (newStorageFolder) { + //can't move onto directory that is not empty + newStorageFolder.createFileQuery().getFilesAsync().then(function (fileList) { + newStorageFolder.createFolderQuery().getFoldersAsync().then(function (folderList) { + if (fileList.length !== 0 || folderList.length !== 0) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + //can't copy onto itself + if (srcPath == newStorageFolder.path) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + //can't copy into itself + if (srcPath == parentPath) { + fail && fail(FileError.INVALID_MODIFICATION_ERR); + return; + } + moveFiles(srcPath, newStorageFolder.path).then(function () { + var successCallback = function () { + success(new DirectoryEntry(name, newStorageFolder.path)); + }; + var temp = new DirectoryEntry(originFolderTop.name, originFolderTop.path).removeRecursively(successCallback, fail); + + }, function () { console.log("error!"); }); + }); + }); + }, function () { fail && fail(FileError.INVALID_MODIFICATION_ERR); }); + + }; + moveFinish(srcPath, parentFullPath); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + ); + }, + tempFileSystem:null, + + persistentFileSystem:null, + + requestFileSystem:function(win,fail,args) { + var type = args[0]; + var size = args[1]; + + var filePath = ""; + var result = null; + var fsTypeName = ""; + + switch (type) { + case LocalFileSystem.TEMPORARY: + filePath = Windows.Storage.ApplicationData.current.temporaryFolder.path; + fsTypeName = "temporary"; + break; + case LocalFileSystem.PERSISTENT: + filePath = Windows.Storage.ApplicationData.current.localFolder.path; + fsTypeName = "persistent"; + break; + } + + var MAX_SIZE = 10000000000; + if (size > MAX_SIZE) { + fail && fail(FileError.QUOTA_EXCEEDED_ERR); + return; + } + + var fileSystem = new FileSystem(fsTypeName, new DirectoryEntry(fsTypeName, filePath)); + result = fileSystem; + win(result); + }, + + resolveLocalFileSystemURI:function(success,fail,args) { + var uri = args[0]; + + var path = uri; + + // support for file name with parameters + if (/\?/g.test(path)) { + path = String(path).split("?")[0]; + } + + // support for encodeURI + if (/\%5/g.test(path)) { + path = decodeURI(path); + } + + // support for special path start with file:/// + if (path.substr(0, 8) == "file:///") { + path = Windows.Storage.ApplicationData.current.localFolder.path + "\\" + String(path).substr(8).split("/").join("\\"); + Windows.Storage.StorageFile.getFileFromPathAsync(path).then( + function (storageFile) { + success(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then( + function (storageFolder) { + success(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, function () { + fail && fail(FileError.NOT_FOUND_ERR); + } + ); + } + ); + } else { + Windows.Storage.StorageFile.getFileFromPathAsync(path).then( + function (storageFile) { + success(new FileEntry(storageFile.name, storageFile.path)); + }, function () { + Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then( + function (storageFolder) { + success(new DirectoryEntry(storageFolder.name, storageFolder.path)); + }, function () { + fail && fail(FileError.ENCODING_ERR); + } + ); + } + ); + } + } + +}; + +require("cordova/windows8/commandProxy").add("File",module.exports); diff --git a/plugins/org.apache.cordova.file/src/wp/File.cs b/plugins/org.apache.cordova.file/src/wp/File.cs new file mode 100644 index 00000000..4fc61dde --- /dev/null +++ b/plugins/org.apache.cordova.file/src/wp/File.cs @@ -0,0 +1,1676 @@ +/* + Licensed 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.IsolatedStorage; +using System.Runtime.Serialization; +using System.Security; +using System.Text; +using System.Windows; +using System.Windows.Resources; + +namespace WPCordovaClassLib.Cordova.Commands +{ + /// <summary> + /// Provides access to isolated storage + /// </summary> + public class File : BaseCommand + { + // Error codes + public const int NOT_FOUND_ERR = 1; + public const int SECURITY_ERR = 2; + public const int ABORT_ERR = 3; + public const int NOT_READABLE_ERR = 4; + public const int ENCODING_ERR = 5; + public const int NO_MODIFICATION_ALLOWED_ERR = 6; + public const int INVALID_STATE_ERR = 7; + public const int SYNTAX_ERR = 8; + public const int INVALID_MODIFICATION_ERR = 9; + public const int QUOTA_EXCEEDED_ERR = 10; + public const int TYPE_MISMATCH_ERR = 11; + public const int PATH_EXISTS_ERR = 12; + + // File system options + public const int TEMPORARY = 0; + public const int PERSISTENT = 1; + public const int RESOURCE = 2; + public const int APPLICATION = 3; + + /// <summary> + /// Temporary directory name + /// </summary> + private readonly string TMP_DIRECTORY_NAME = "tmp"; + + /// <summary> + /// Represents error code for callback + /// </summary> + [DataContract] + public class ErrorCode + { + /// <summary> + /// Error code + /// </summary> + [DataMember(IsRequired = true, Name = "code")] + public int Code { get; set; } + + /// <summary> + /// Creates ErrorCode object + /// </summary> + public ErrorCode(int code) + { + this.Code = code; + } + } + + /// <summary> + /// Represents File action options. + /// </summary> + [DataContract] + public class FileOptions + { + /// <summary> + /// File path + /// </summary> + /// + private string _fileName; + [DataMember(Name = "fileName")] + public string FilePath + { + get + { + return this._fileName; + } + + set + { + int index = value.IndexOfAny(new char[] { '#', '?' }); + this._fileName = index > -1 ? value.Substring(0, index) : value; + } + } + + /// <summary> + /// Full entryPath + /// </summary> + [DataMember(Name = "fullPath")] + public string FullPath { get; set; } + + /// <summary> + /// Directory name + /// </summary> + [DataMember(Name = "dirName")] + public string DirectoryName { get; set; } + + /// <summary> + /// Path to create file/directory + /// </summary> + [DataMember(Name = "path")] + public string Path { get; set; } + + /// <summary> + /// The encoding to use to encode the file's content. Default is UTF8. + /// </summary> + [DataMember(Name = "encoding")] + public string Encoding { get; set; } + + /// <summary> + /// Uri to get file + /// </summary> + /// + private string _uri; + [DataMember(Name = "uri")] + public string Uri + { + get + { + return this._uri; + } + + set + { + int index = value.IndexOfAny(new char[] { '#', '?' }); + this._uri = index > -1 ? value.Substring(0, index) : value; + } + } + + /// <summary> + /// Size to truncate file + /// </summary> + [DataMember(Name = "size")] + public long Size { get; set; } + + /// <summary> + /// Data to write in file + /// </summary> + [DataMember(Name = "data")] + public string Data { get; set; } + + /// <summary> + /// Position the writing starts with + /// </summary> + [DataMember(Name = "position")] + public int Position { get; set; } + + /// <summary> + /// Type of file system requested + /// </summary> + [DataMember(Name = "type")] + public int FileSystemType { get; set; } + + /// <summary> + /// New file/directory name + /// </summary> + [DataMember(Name = "newName")] + public string NewName { get; set; } + + /// <summary> + /// Destination directory to copy/move file/directory + /// </summary> + [DataMember(Name = "parent")] + public string Parent { get; set; } + + /// <summary> + /// Options for getFile/getDirectory methods + /// </summary> + [DataMember(Name = "options")] + public CreatingOptions CreatingOpt { get; set; } + + /// <summary> + /// Creates options object with default parameters + /// </summary> + public FileOptions() + { + this.SetDefaultValues(new StreamingContext()); + } + + /// <summary> + /// Initializes default values for class fields. + /// Implemented in separate method because default constructor is not invoked during deserialization. + /// </summary> + /// <param name="context"></param> + [OnDeserializing()] + public void SetDefaultValues(StreamingContext context) + { + this.Encoding = "UTF-8"; + this.FilePath = ""; + this.FileSystemType = -1; + } + } + + /// <summary> + /// Stores image info + /// </summary> + [DataContract] + public class FileMetadata + { + [DataMember(Name = "fileName")] + public string FileName { get; set; } + + [DataMember(Name = "fullPath")] + public string FullPath { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "lastModifiedDate")] + public string LastModifiedDate { get; set; } + + [DataMember(Name = "size")] + public long Size { get; set; } + + public FileMetadata(string filePath) + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new FileNotFoundException("File doesn't exist"); + } + else if (!isoFile.FileExists(filePath)) + { + // attempt to get it from the resources + if (filePath.IndexOf("www") == 0) + { + Uri fileUri = new Uri(filePath, UriKind.Relative); + StreamResourceInfo streamInfo = Application.GetResourceStream(fileUri); + if (streamInfo != null) + { + this.Size = streamInfo.Stream.Length; + this.FileName = filePath.Substring(filePath.LastIndexOf("/") + 1); + this.FullPath = filePath; + } + } + else + { + throw new FileNotFoundException("File doesn't exist"); + } + } + else + { + //TODO get file size the other way if possible + using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, FileAccess.Read, isoFile)) + { + this.Size = stream.Length; + } + this.FullPath = filePath; + this.FileName = System.IO.Path.GetFileName(filePath); + this.LastModifiedDate = isoFile.GetLastWriteTime(filePath).DateTime.ToString(); + } + this.Type = MimeTypeMapper.GetMimeType(this.FileName); + } + } + } + + /// <summary> + /// Represents file or directory modification metadata + /// </summary> + [DataContract] + public class ModificationMetadata + { + /// <summary> + /// Modification time + /// </summary> + [DataMember] + public string modificationTime { get; set; } + } + + /// <summary> + /// Represents file or directory entry + /// </summary> + [DataContract] + public class FileEntry + { + + /// <summary> + /// File type + /// </summary> + [DataMember(Name = "isFile")] + public bool IsFile { get; set; } + + /// <summary> + /// Directory type + /// </summary> + [DataMember(Name = "isDirectory")] + public bool IsDirectory { get; set; } + + /// <summary> + /// File/directory name + /// </summary> + [DataMember(Name = "name")] + public string Name { get; set; } + + /// <summary> + /// Full path to file/directory + /// </summary> + [DataMember(Name = "fullPath")] + public string FullPath { get; set; } + + public bool IsResource { get; set; } + + public static FileEntry GetEntry(string filePath, bool bIsRes=false) + { + FileEntry entry = null; + try + { + entry = new FileEntry(filePath, bIsRes); + + } + catch (Exception ex) + { + Debug.WriteLine("Exception in GetEntry for filePath :: " + filePath + " " + ex.Message); + } + return entry; + } + + /// <summary> + /// Creates object and sets necessary properties + /// </summary> + /// <param name="filePath"></param> + public FileEntry(string filePath, bool bIsRes = false) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException(); + } + + if(filePath.Contains(" ")) + { + Debug.WriteLine("FilePath with spaces :: " + filePath); + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + IsResource = bIsRes; + IsFile = isoFile.FileExists(filePath); + IsDirectory = isoFile.DirectoryExists(filePath); + if (IsFile) + { + this.Name = Path.GetFileName(filePath); + } + else if (IsDirectory) + { + this.Name = this.GetDirectoryName(filePath); + if (string.IsNullOrEmpty(Name)) + { + this.Name = "/"; + } + } + else + { + if (IsResource) + { + this.Name = Path.GetFileName(filePath); + } + else + { + throw new FileNotFoundException(); + } + } + + try + { + this.FullPath = filePath.Replace('\\', '/'); // new Uri(filePath).LocalPath; + } + catch (Exception) + { + this.FullPath = filePath; + } + } + } + + /// <summary> + /// Extracts directory name from path string + /// Path should refer to a directory, for example \foo\ or /foo. + /// </summary> + /// <param name="path"></param> + /// <returns></returns> + private string GetDirectoryName(string path) + { + if (String.IsNullOrEmpty(path)) + { + return path; + } + + string[] split = path.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 1) + { + return null; + } + else + { + return split[split.Length - 1]; + } + } + } + + + /// <summary> + /// Represents info about requested file system + /// </summary> + [DataContract] + public class FileSystemInfo + { + /// <summary> + /// file system type + /// </summary> + [DataMember(Name = "name", IsRequired = true)] + public string Name { get; set; } + + /// <summary> + /// Root directory entry + /// </summary> + [DataMember(Name = "root", EmitDefaultValue = false)] + public FileEntry Root { get; set; } + + /// <summary> + /// Creates class instance + /// </summary> + /// <param name="name"></param> + /// <param name="rootEntry"> Root directory</param> + public FileSystemInfo(string name, FileEntry rootEntry = null) + { + Name = name; + Root = rootEntry; + } + } + + [DataContract] + public class CreatingOptions + { + /// <summary> + /// Create file/directory if is doesn't exist + /// </summary> + [DataMember(Name = "create")] + public bool Create { get; set; } + + /// <summary> + /// Generate an exception if create=true and file/directory already exists + /// </summary> + [DataMember(Name = "exclusive")] + public bool Exclusive { get; set; } + + + } + + // returns null value if it fails. + private string[] getOptionStrings(string options) + { + string[] optStings = null; + try + { + optStings = JSON.JsonHelper.Deserialize<string[]>(options); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), CurrentCommandCallbackId); + } + return optStings; + } + + /// <summary> + /// Gets amount of free space available for Isolated Storage + /// </summary> + /// <param name="options">No options is needed for this method</param> + public void getFreeDiskSpace(string options) + { + string callbackId = getOptionStrings(options)[0]; + + try + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, isoFile.AvailableFreeSpace), callbackId); + } + } + catch (IsolatedStorageException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + /// <summary> + /// Check if file exists + /// </summary> + /// <param name="options">File path</param> + public void testFileExists(string options) + { + IsDirectoryOrFileExist(options, false); + } + + /// <summary> + /// Check if directory exists + /// </summary> + /// <param name="options">directory name</param> + public void testDirectoryExists(string options) + { + IsDirectoryOrFileExist(options, true); + } + + /// <summary> + /// Check if file or directory exist + /// </summary> + /// <param name="options">File path/Directory name</param> + /// <param name="isDirectory">Flag to recognize what we should check</param> + public void IsDirectoryOrFileExist(string options, bool isDirectory) + { + string[] args = getOptionStrings(options); + string callbackId = args[1]; + FileOptions fileOptions = JSON.JsonHelper.Deserialize<FileOptions>(args[0]); + string filePath = args[0]; + + if (fileOptions == null) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + } + + try + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + bool isExist; + if (isDirectory) + { + isExist = isoFile.DirectoryExists(fileOptions.DirectoryName); + } + else + { + isExist = isoFile.FileExists(fileOptions.FilePath); + } + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, isExist), callbackId); + } + } + catch (IsolatedStorageException) // default handler throws INVALID_MODIFICATION_ERR + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + } + + } + + public void readAsDataURL(string options) + { + string[] optStrings = getOptionStrings(options); + string filePath = optStrings[0]; + int startPos = int.Parse(optStrings[1]); + int endPos = int.Parse(optStrings[2]); + string callbackId = optStrings[3]; + + if (filePath != null) + { + try + { + string base64URL = null; + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoFile.FileExists(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + string mimeType = MimeTypeMapper.GetMimeType(filePath); + + using (IsolatedStorageFileStream stream = isoFile.OpenFile(filePath, FileMode.Open, FileAccess.Read)) + { + string base64String = GetFileContent(stream); + base64URL = "data:" + mimeType + ";base64," + base64String; + } + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, base64URL), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + } + + public void readAsArrayBuffer(string options) + { + string[] optStrings = getOptionStrings(options); + string filePath = optStrings[0]; + int startPos = int.Parse(optStrings[1]); + int endPos = int.Parse(optStrings[2]); + string callbackId = optStrings[3]; + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR), callbackId); + } + + public void readAsBinaryString(string options) + { + string[] optStrings = getOptionStrings(options); + string filePath = optStrings[0]; + int startPos = int.Parse(optStrings[1]); + int endPos = int.Parse(optStrings[2]); + string callbackId = optStrings[3]; + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR), callbackId); + } + + public void readAsText(string options) + { + string[] optStrings = getOptionStrings(options); + string filePath = optStrings[0]; + string encStr = optStrings[1]; + int startPos = int.Parse(optStrings[2]); + int endPos = int.Parse(optStrings[3]); + string callbackId = optStrings[4]; + + try + { + string text = ""; + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoFile.FileExists(filePath)) + { + readResourceAsText(options); + return; + } + Encoding encoding = Encoding.GetEncoding(encStr); + + using (TextReader reader = new StreamReader(isoFile.OpenFile(filePath, FileMode.Open, FileAccess.Read), encoding)) + { + text = reader.ReadToEnd(); + if (startPos < 0) + { + startPos = Math.Max(text.Length + startPos, 0); + } + else if (startPos > 0) + { + startPos = Math.Min(text.Length, startPos); + } + + if (endPos > 0) + { + endPos = Math.Min(text.Length, endPos); + } + else if (endPos < 0) + { + endPos = Math.Max(endPos + text.Length, 0); + } + + + text = text.Substring(startPos, endPos - startPos); + + } + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, text), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + /// <summary> + /// Reads application resource as a text + /// </summary> + /// <param name="options">Path to a resource</param> + public void readResourceAsText(string options) + { + string[] optStrings = getOptionStrings(options); + string pathToResource = optStrings[0]; + string encStr = optStrings[1]; + int start = int.Parse(optStrings[2]); + int endMarker = int.Parse(optStrings[3]); + string callbackId = optStrings[4]; + + try + { + if (pathToResource.StartsWith("/")) + { + pathToResource = pathToResource.Remove(0, 1); + } + + var resource = Application.GetResourceStream(new Uri(pathToResource, UriKind.Relative)); + + if (resource == null) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + + string text; + StreamReader streamReader = new StreamReader(resource.Stream); + text = streamReader.ReadToEnd(); + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, text), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + public void truncate(string options) + { + string[] optStrings = getOptionStrings(options); + + string filePath = optStrings[0]; + int size = int.Parse(optStrings[1]); + string callbackId = optStrings[2]; + + try + { + long streamLength = 0; + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoFile.FileExists(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + + using (FileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, FileAccess.ReadWrite, isoFile)) + { + if (0 <= size && size <= stream.Length) + { + stream.SetLength(size); + } + streamLength = stream.Length; + } + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, streamLength), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + //write:[filePath,data,position,isBinary,callbackId] + public void write(string options) + { + string[] optStrings = getOptionStrings(options); + + string filePath = optStrings[0]; + string data = optStrings[1]; + int position = int.Parse(optStrings[2]); + bool isBinary = bool.Parse(optStrings[3]); + string callbackId = optStrings[4]; + + try + { + if (string.IsNullOrEmpty(data)) + { + Debug.WriteLine("Expected some data to be send in the write command to {0}", filePath); + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + // create the file if not exists + if (!isoFile.FileExists(filePath)) + { + var file = isoFile.CreateFile(filePath); + file.Close(); + } + + using (FileStream stream = new IsolatedStorageFileStream(filePath, FileMode.Open, FileAccess.ReadWrite, isoFile)) + { + if (0 <= position && position <= stream.Length) + { + stream.SetLength(position); + } + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Seek(0, SeekOrigin.End); + writer.Write(data.ToCharArray()); + } + } + } + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, data.Length), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + /// <summary> + /// Look up metadata about this entry. + /// </summary> + /// <param name="options">filePath to entry</param> + public void getMetadata(string options) + { + string[] optStings = getOptionStrings(options); + string filePath = optStings[0]; + string callbackId = optStings[1]; + + if (filePath != null) + { + try + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (isoFile.FileExists(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, + new ModificationMetadata() { modificationTime = isoFile.GetLastWriteTime(filePath).DateTime.ToString() }), callbackId); + } + else if (isoFile.DirectoryExists(filePath)) + { + string modTime = isoFile.GetLastWriteTime(filePath).DateTime.ToString(); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new ModificationMetadata() { modificationTime = modTime }), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + + } + } + catch (IsolatedStorageException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + + } + + + /// <summary> + /// Returns a File that represents the current state of the file that this FileEntry represents. + /// </summary> + /// <param name="filePath">filePath to entry</param> + /// <returns></returns> + public void getFileMetadata(string options) + { + string[] optStings = getOptionStrings(options); + string filePath = optStings[0]; + string callbackId = optStings[1]; + + if (filePath != null) + { + try + { + FileMetadata metaData = new FileMetadata(filePath); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, metaData), callbackId); + } + catch (IsolatedStorageException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_READABLE_ERR), callbackId); + } + } + } + } + + /// <summary> + /// Look up the parent DirectoryEntry containing this Entry. + /// If this Entry is the root of IsolatedStorage, its parent is itself. + /// </summary> + /// <param name="options"></param> + public void getParent(string options) + { + string[] optStings = getOptionStrings(options); + string filePath = optStings[0]; + string callbackId = optStings[1]; + + if (filePath != null) + { + try + { + if (string.IsNullOrEmpty(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION),callbackId); + return; + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + FileEntry entry; + + if (isoFile.FileExists(filePath) || isoFile.DirectoryExists(filePath)) + { + + + string path = this.GetParentDirectory(filePath); + entry = FileEntry.GetEntry(path); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, entry),callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR),callbackId); + } + + } + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR),callbackId); + } + } + } + } + + public void remove(string options) + { + string[] args = getOptionStrings(options); + string filePath = args[0]; + string callbackId = args[1]; + + if (filePath != null) + { + try + { + if (filePath == "/" || filePath == "" || filePath == @"\") + { + throw new Exception("Cannot delete root file system") ; + } + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (isoFile.FileExists(filePath)) + { + isoFile.DeleteFile(filePath); + } + else + { + if (isoFile.DirectoryExists(filePath)) + { + isoFile.DeleteDirectory(filePath); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR),callbackId); + return; + } + } + DispatchCommandResult(new PluginResult(PluginResult.Status.OK),callbackId); + } + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR),callbackId); + } + } + } + } + + public void removeRecursively(string options) + { + string[] args = getOptionStrings(options); + string filePath = args[0]; + string callbackId = args[1]; + + if (filePath != null) + { + if (string.IsNullOrEmpty(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION),callbackId); + } + else + { + if (removeDirRecursively(filePath, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK), callbackId); + } + } + } + } + + public void readEntries(string options) + { + string[] args = getOptionStrings(options); + string filePath = args[0]; + string callbackId = args[1]; + + if (filePath != null) + { + try + { + if (string.IsNullOrEmpty(filePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION),callbackId); + return; + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (isoFile.DirectoryExists(filePath)) + { + string path = File.AddSlashToDirectory(filePath); + List<FileEntry> entries = new List<FileEntry>(); + string[] files = isoFile.GetFileNames(path + "*"); + string[] dirs = isoFile.GetDirectoryNames(path + "*"); + foreach (string file in files) + { + entries.Add(FileEntry.GetEntry(path + file)); + } + foreach (string dir in dirs) + { + entries.Add(FileEntry.GetEntry(path + dir + "/")); + } + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, entries),callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR),callbackId); + } + } + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR),callbackId); + } + } + } + } + + public void requestFileSystem(string options) + { + // TODO: try/catch + string[] optVals = getOptionStrings(options); + //FileOptions fileOptions = new FileOptions(); + int fileSystemType = int.Parse(optVals[0]); + double size = double.Parse(optVals[1]); + string callbackId = optVals[2]; + + + IsolatedStorageFile.GetUserStoreForApplication(); + + if (size > (10 * 1024 * 1024)) // 10 MB, compier will clean this up! + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, QUOTA_EXCEEDED_ERR), callbackId); + return; + } + + try + { + if (size != 0) + { + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + long availableSize = isoFile.AvailableFreeSpace; + if (size > availableSize) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, QUOTA_EXCEEDED_ERR), callbackId); + return; + } + } + } + + if (fileSystemType == PERSISTENT) + { + // TODO: this should be in it's own folder to prevent overwriting of the app assets, which are also in ISO + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new FileSystemInfo("persistent", FileEntry.GetEntry("/"))), callbackId); + } + else if (fileSystemType == TEMPORARY) + { + using (IsolatedStorageFile isoStorage = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoStorage.FileExists(TMP_DIRECTORY_NAME)) + { + isoStorage.CreateDirectory(TMP_DIRECTORY_NAME); + } + } + + string tmpFolder = "/" + TMP_DIRECTORY_NAME + "/"; + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new FileSystemInfo("temporary", FileEntry.GetEntry(tmpFolder))), callbackId); + } + else if (fileSystemType == RESOURCE) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new FileSystemInfo("resource")), callbackId); + } + else if (fileSystemType == APPLICATION) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new FileSystemInfo("application")), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR), callbackId); + } + + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR), callbackId); + } + } + } + + public void resolveLocalFileSystemURI(string options) + { + + string[] optVals = getOptionStrings(options); + string uri = optVals[0].Split('?')[0]; + string callbackId = optVals[1]; + + if (uri != null) + { + // a single '/' is valid, however, '/someDir' is not, but '/tmp//somedir' and '///someDir' are valid + if (uri.StartsWith("/") && uri.IndexOf("//") < 0 && uri != "/") + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ENCODING_ERR), callbackId); + return; + } + try + { + // fix encoded spaces + string path = Uri.UnescapeDataString(uri); + + FileEntry uriEntry = FileEntry.GetEntry(path); + if (uriEntry != null) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, uriEntry), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR), callbackId); + } + } + } + } + + public void copyTo(string options) + { + TransferTo(options, false); + } + + public void moveTo(string options) + { + TransferTo(options, true); + } + + public void getFile(string options) + { + GetFileOrDirectory(options, false); + } + + public void getDirectory(string options) + { + GetFileOrDirectory(options, true); + } + + #region internal functionality + + /// <summary> + /// Retrieves the parent directory name of the specified path, + /// </summary> + /// <param name="path">Path</param> + /// <returns>Parent directory name</returns> + private string GetParentDirectory(string path) + { + if (String.IsNullOrEmpty(path) || path == "/") + { + return "/"; + } + + if (path.EndsWith(@"/") || path.EndsWith(@"\")) + { + return this.GetParentDirectory(Path.GetDirectoryName(path)); + } + + string result = Path.GetDirectoryName(path); + if (result == null) + { + result = "/"; + } + + return result; + } + + private bool removeDirRecursively(string fullPath,string callbackId) + { + try + { + if (fullPath == "/") + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR),callbackId); + return false; + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (isoFile.DirectoryExists(fullPath)) + { + string tempPath = File.AddSlashToDirectory(fullPath); + string[] files = isoFile.GetFileNames(tempPath + "*"); + if (files.Length > 0) + { + foreach (string file in files) + { + isoFile.DeleteFile(tempPath + file); + } + } + string[] dirs = isoFile.GetDirectoryNames(tempPath + "*"); + if (dirs.Length > 0) + { + foreach (string dir in dirs) + { + if (!removeDirRecursively(tempPath + dir, callbackId)) + { + return false; + } + } + } + isoFile.DeleteDirectory(fullPath); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR),callbackId); + } + } + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR),callbackId); + return false; + } + } + return true; + } + + private bool CanonicalCompare(string pathA, string pathB) + { + string a = pathA.Replace("//", "/"); + string b = pathB.Replace("//", "/"); + + return a.Equals(b, StringComparison.OrdinalIgnoreCase); + } + + /* + * copyTo:["fullPath","parent", "newName"], + * moveTo:["fullPath","parent", "newName"], + */ + private void TransferTo(string options, bool move) + { + // TODO: try/catch + string[] optStrings = getOptionStrings(options); + string fullPath = optStrings[0]; + string parent = optStrings[1]; + string newFileName = optStrings[2]; + string callbackId = optStrings[3]; + + char[] invalids = Path.GetInvalidPathChars(); + + if (newFileName.IndexOfAny(invalids) > -1 || newFileName.IndexOf(":") > -1 ) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ENCODING_ERR), callbackId); + return; + } + + try + { + if ((parent == null) || (string.IsNullOrEmpty(parent)) || (string.IsNullOrEmpty(fullPath))) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + + string parentPath = File.AddSlashToDirectory(parent); + string currentPath = fullPath; + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + bool isFileExist = isoFile.FileExists(currentPath); + bool isDirectoryExist = isoFile.DirectoryExists(currentPath); + bool isParentExist = isoFile.DirectoryExists(parentPath); + + if ( ( !isFileExist && !isDirectoryExist ) || !isParentExist ) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + string newName; + string newPath; + if (isFileExist) + { + newName = (string.IsNullOrEmpty(newFileName)) + ? Path.GetFileName(currentPath) + : newFileName; + + newPath = Path.Combine(parentPath, newName); + + // sanity check .. + // cannot copy file onto itself + if (CanonicalCompare(newPath,currentPath)) //(parent + newFileName)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, INVALID_MODIFICATION_ERR), callbackId); + return; + } + else if (isoFile.DirectoryExists(newPath)) + { + // there is already a folder with the same name, operation is not allowed + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, INVALID_MODIFICATION_ERR), callbackId); + return; + } + else if (isoFile.FileExists(newPath)) + { // remove destination file if exists, in other case there will be exception + isoFile.DeleteFile(newPath); + } + + if (move) + { + isoFile.MoveFile(currentPath, newPath); + } + else + { + isoFile.CopyFile(currentPath, newPath, true); + } + } + else + { + newName = (string.IsNullOrEmpty(newFileName)) + ? currentPath + : newFileName; + + newPath = Path.Combine(parentPath, newName); + + if (move) + { + // remove destination directory if exists, in other case there will be exception + // target directory should be empty + if (!newPath.Equals(currentPath) && isoFile.DirectoryExists(newPath)) + { + isoFile.DeleteDirectory(newPath); + } + + isoFile.MoveDirectory(currentPath, newPath); + } + else + { + CopyDirectory(currentPath, newPath, isoFile); + } + } + FileEntry entry = FileEntry.GetEntry(newPath); + if (entry != null) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, entry), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + } + + } + catch (Exception ex) + { + if (!this.HandleException(ex, callbackId)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR), callbackId); + } + } + } + + private bool HandleException(Exception ex, string cbId="") + { + bool handled = false; + string callbackId = String.IsNullOrEmpty(cbId) ? this.CurrentCommandCallbackId : cbId; + if (ex is SecurityException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, SECURITY_ERR), callbackId); + handled = true; + } + else if (ex is FileNotFoundException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + handled = true; + } + else if (ex is ArgumentException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ENCODING_ERR), callbackId); + handled = true; + } + else if (ex is IsolatedStorageException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, INVALID_MODIFICATION_ERR), callbackId); + handled = true; + } + else if (ex is DirectoryNotFoundException) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + handled = true; + } + return handled; + } + + private void CopyDirectory(string sourceDir, string destDir, IsolatedStorageFile isoFile) + { + string path = File.AddSlashToDirectory(sourceDir); + + bool bExists = isoFile.DirectoryExists(destDir); + + if (!bExists) + { + isoFile.CreateDirectory(destDir); + } + + destDir = File.AddSlashToDirectory(destDir); + + string[] files = isoFile.GetFileNames(path + "*"); + + if (files.Length > 0) + { + foreach (string file in files) + { + isoFile.CopyFile(path + file, destDir + file,true); + } + } + string[] dirs = isoFile.GetDirectoryNames(path + "*"); + if (dirs.Length > 0) + { + foreach (string dir in dirs) + { + CopyDirectory(path + dir, destDir + dir, isoFile); + } + } + } + + private void GetFileOrDirectory(string options, bool getDirectory) + { + FileOptions fOptions = new FileOptions(); + string[] args = getOptionStrings(options); + + fOptions.FullPath = args[0]; + fOptions.Path = args[1]; + + string callbackId = args[3]; + + try + { + fOptions.CreatingOpt = JSON.JsonHelper.Deserialize<CreatingOptions>(args[2]); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION), callbackId); + return; + } + + try + { + if ((string.IsNullOrEmpty(fOptions.Path)) || (string.IsNullOrEmpty(fOptions.FullPath))) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + + string path; + + if (fOptions.Path.Split(':').Length > 2) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ENCODING_ERR), callbackId); + return; + } + + try + { + path = Path.Combine(fOptions.FullPath + "/", fOptions.Path); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ENCODING_ERR), callbackId); + return; + } + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + bool isFile = isoFile.FileExists(path); + bool isDirectory = isoFile.DirectoryExists(path); + bool create = (fOptions.CreatingOpt == null) ? false : fOptions.CreatingOpt.Create; + bool exclusive = (fOptions.CreatingOpt == null) ? false : fOptions.CreatingOpt.Exclusive; + if (create) + { + if (exclusive && (isoFile.FileExists(path) || isoFile.DirectoryExists(path))) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, PATH_EXISTS_ERR), callbackId); + return; + } + + // need to make sure the parent exists + // it is an error to create a directory whose immediate parent does not yet exist + // see issue: https://issues.apache.org/jira/browse/CB-339 + string[] pathParts = path.Split('/'); + string builtPath = pathParts[0]; + for (int n = 1; n < pathParts.Length - 1; n++) + { + builtPath += "/" + pathParts[n]; + if (!isoFile.DirectoryExists(builtPath)) + { + Debug.WriteLine(String.Format("Error :: Parent folder \"{0}\" does not exist, when attempting to create \"{1}\"",builtPath,path)); + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + return; + } + } + + if ((getDirectory) && (!isDirectory)) + { + isoFile.CreateDirectory(path); + } + else + { + if ((!getDirectory) && (!isFile)) + { + + IsolatedStorageFileStream fileStream = isoFile.CreateFile(path); + fileStream.Close(); + } + } + } + else // (not create) + { + if ((!isFile) && (!isDirectory)) + { + if (path.IndexOf("//www") == 0) + { + Uri fileUri = new Uri(path.Remove(0,2), UriKind.Relative); + StreamResourceInfo streamInfo = Application.GetResourceStream(fileUri); + if (streamInfo != null) + { + FileEntry _entry = FileEntry.GetEntry(fileUri.OriginalString,true); + + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, _entry), callbackId); + + //using (BinaryReader br = new BinaryReader(streamInfo.Stream)) + //{ + // byte[] data = br.ReadBytes((int)streamInfo.Stream.Length); + + //} + + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + + + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + return; + } + if (((getDirectory) && (!isDirectory)) || ((!getDirectory) && (!isFile))) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, TYPE_MISMATCH_ERR), callbackId); + return; + } + } + FileEntry entry = FileEntry.GetEntry(path); + if (entry != null) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, entry), callbackId); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NOT_FOUND_ERR), callbackId); + } + } + } + catch (Exception ex) + { + if (!this.HandleException(ex)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, NO_MODIFICATION_ALLOWED_ERR), callbackId); + } + } + } + + private static string AddSlashToDirectory(string dirPath) + { + if (dirPath.EndsWith("/")) + { + return dirPath; + } + else + { + return dirPath + "/"; + } + } + + /// <summary> + /// Returns file content in a form of base64 string + /// </summary> + /// <param name="stream">File stream</param> + /// <returns>Base64 representation of the file</returns> + private string GetFileContent(Stream stream) + { + int streamLength = (int)stream.Length; + byte[] fileData = new byte[streamLength + 1]; + stream.Read(fileData, 0, streamLength); + stream.Close(); + return Convert.ToBase64String(fileData); + } + + #endregion + + } +} |
