| package org.apache.cordova.file; |
| |
| 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.nio.channels.FileChannel; |
| |
| import org.apache.cordova.CordovaInterface; |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import android.util.Base64; |
| import android.net.Uri; |
| |
| public class LocalFilesystem implements Filesystem { |
| |
| private String name; |
| private String fsRoot; |
| private CordovaInterface cordova; |
| |
| public LocalFilesystem(CordovaInterface cordova, String name, String fsRoot) { |
| this.name = name; |
| this.fsRoot = fsRoot; |
| this.cordova = cordova; |
| } |
| |
| @Override |
| public String filesystemPathForURL(LocalFilesystemURL url) { |
| String path = this.fsRoot + url.fullPath; |
| if (path.endsWith("/")) { |
| path = path.substring(0, path.length()-1); |
| } |
| return path; |
| } |
| |
| private String fullPathForFilesystemPath(String absolutePath) { |
| if (absolutePath != null && absolutePath.startsWith(this.fsRoot)) { |
| return absolutePath.substring(this.fsRoot.length()); |
| } |
| return null; |
| } |
| |
| public static JSONObject makeEntryForPath(String path, int fsType, Boolean isDir) throws JSONException { |
| JSONObject entry = new JSONObject(); |
| |
| int end = path.endsWith("/") ? 1 : 0; |
| String[] parts = path.substring(0,path.length()-end).split("/"); |
| String name = parts[parts.length-1]; |
| entry.put("isFile", !isDir); |
| entry.put("isDirectory", isDir); |
| entry.put("name", name); |
| entry.put("fullPath", path); |
| // The file system can't be specified, as it would lead to an infinite loop, |
| // but the filesystem type can |
| entry.put("filesystem", fsType); |
| |
| return entry; |
| |
| } |
| |
| public JSONObject makeEntryForFile(File file, int fsType) throws JSONException { |
| String path = this.fullPathForFilesystemPath(file.getAbsolutePath()); |
| if (path != null) { |
| return makeEntryForPath(path, fsType, file.isDirectory()); |
| } |
| return null; |
| } |
| |
| @Override |
| public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException { |
| File fp = null; |
| fp = new File(this.fsRoot + inputURL.fullPath); //TODO: Proper fs.join |
| |
| if (!fp.exists()) { |
| throw new FileNotFoundException(); |
| } |
| if (!fp.canRead()) { |
| throw new IOException(); |
| } |
| try { |
| JSONObject entry = new JSONObject(); |
| entry.put("isFile", fp.isFile()); |
| entry.put("isDirectory", fp.isDirectory()); |
| entry.put("name", fp.getName()); |
| entry.put("fullPath", inputURL.fullPath); |
| // The file system can't be specified, as it would lead to an infinite loop. |
| // But we can specify the type of FS, and the rest can be reconstructed in JS. |
| entry.put("filesystem", inputURL.filesystemType); |
| return entry; |
| } catch (JSONException e) { |
| throw new IOException(); |
| } |
| } |
| |
| @Override |
| public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, |
| 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"); |
| } |
| |
| LocalFilesystemURL requestedURL = new LocalFilesystemURL(Uri.withAppendedPath(inputURL.URL, fileName)); |
| |
| File fp = new File(this.filesystemPathForURL(requestedURL)); |
| |
| 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 makeEntryForPath(requestedURL.fullPath, requestedURL.filesystemType, directory); |
| } |
| |
| @Override |
| public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException { |
| |
| File fp = new File(filesystemPathForURL(inputURL)); |
| |
| // 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(); |
| } |
| |
| @Override |
| public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException { |
| File directory = new File(filesystemPathForURL(inputURL)); |
| return removeDirRecursively(directory); |
| } |
| |
| protected 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; |
| } |
| } |
| |
| @Override |
| public JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { |
| File fp = new File(filesystemPathForURL(inputURL)); |
| |
| 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()) { |
| try { |
| entries.put(makeEntryForPath(fullPathForFilesystemPath(files[i].getAbsolutePath()), inputURL.filesystemType, files[i].isDirectory())); |
| } catch (JSONException e) { |
| } |
| } |
| } |
| } |
| |
| return entries; |
| } |
| |
| @Override |
| public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { |
| File file = new File(filesystemPathForURL(inputURL)); |
| |
| if (!file.exists()) { |
| throw new FileNotFoundException("File at " + inputURL.URL + " does not exist."); |
| } |
| |
| JSONObject metadata = new JSONObject(); |
| try { |
| metadata.put("size", file.length()); |
| metadata.put("type", FileHelper.getMimeType(file.getAbsolutePath(), cordova)); |
| metadata.put("name", file.getName()); |
| metadata.put("fullPath", inputURL.fullPath); |
| metadata.put("lastModifiedDate", file.lastModified()); |
| } catch (JSONException e) { |
| return null; |
| } |
| return metadata; |
| } |
| |
| @Override |
| public JSONObject getParentForLocalURL(LocalFilesystemURL inputURL) throws IOException { |
| LocalFilesystemURL newURL = new LocalFilesystemURL(inputURL.URL); |
| |
| if (!("".equals(inputURL.fullPath) || "/".equals(inputURL.fullPath))) { |
| int end = inputURL.fullPath.endsWith("/") ? 1 : 0; |
| int lastPathStartsAt = inputURL.fullPath.lastIndexOf('/', inputURL.fullPath.length()-end)+1; |
| newURL.fullPath = newURL.fullPath.substring(0,lastPathStartsAt); |
| } |
| return getEntryForLocalURL(newURL); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| |
| /** |
| * 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, int fsType) 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 makeEntryForFile(destFile, fsType); |
| } |
| |
| /** |
| * 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, int fsType) 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, fsType); |
| } else { |
| copyFile(file, destination, fsType); |
| } |
| } |
| |
| return makeEntryForFile(destinationDir, fsType); |
| } |
| |
| /** |
| * 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, int fsType) 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 makeEntryForFile(destFile, fsType); |
| } |
| |
| /** |
| * 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, int fsType) 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, fsType); |
| if (destinationDir.exists()) { |
| removeDirRecursively(srcDir); |
| } else { |
| throw new IOException("moved failed"); |
| } |
| } |
| |
| return makeEntryForFile(destinationDir, fsType); |
| } |
| |
| |
| @Override |
| public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName, |
| Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException { |
| |
| |
| String newFileName = this.filesystemPathForURL(srcURL); |
| String newParent = this.filesystemPathForURL(destURL); |
| |
| |
| 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"); |
| } |
| |
| |
| if (LocalFilesystem.class.isInstance(srcFs)) { |
| |
| /* Same FS, we can shortcut with NSFileManager operations */ |
| |
| |
| 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"); |
| } |
| |
| // 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, destURL.filesystemType); |
| } else { |
| return copyDirectory(source, destination, destURL.filesystemType); |
| } |
| } else { |
| if (move) { |
| JSONObject newFileEntry = moveFile(source, destination, destURL.filesystemType); |
| |
| /* // If we've moved a file given its content URI, we need to clean up. |
| // TODO: Move this to where it belongs, in cross-fs mv code below. |
| if (srcURL.URL.getScheme().equals("content")) { |
| notifyDelete(fileName); |
| } |
| */ |
| return newFileEntry; |
| } else { |
| return copyFile(source, destination, destURL.filesystemType); |
| } |
| } |
| |
| |
| } else { |
| /* // Need to copy the hard way |
| srcFs.readFileAtURL(srcURL, 0, -1, new ReadFileCallback() { |
| void run(data, mimetype, errorcode) { |
| if (data != null) { |
| //write data to file |
| // send success message -- call original callback? |
| } |
| // error |
| } |
| }); |
| return null; // Async, will return later |
| */ |
| } |
| return null; |
| } |
| |
| @Override |
| public void readFileAtURL(LocalFilesystemURL inputURL, int start, int end, |
| ReadFileCallback readFileCallback) throws IOException { |
| |
| int numBytesToRead = end - start; |
| byte[] bytes = new byte[numBytesToRead]; |
| String contentType; |
| |
| File file = new File(this.filesystemPathForURL(inputURL)); |
| contentType = FileHelper.getMimeTypeForExtension(file.getAbsolutePath()); |
| |
| InputStream inputStream = new FileInputStream(file); |
| int numBytesRead = 0; |
| try { |
| if (start > 0) { |
| inputStream.skip(start); |
| } |
| while (numBytesToRead > 0 && (numBytesRead = inputStream.read(bytes, numBytesRead, numBytesToRead)) >= 0) { |
| numBytesToRead -= numBytesRead; |
| } |
| } finally { |
| inputStream.close(); |
| } |
| readFileCallback.handleData(bytes, contentType); |
| } |
| |
| @Override |
| public long writeToFileAtURL(LocalFilesystemURL inputURL, String data, |
| int offset, boolean isBinary) throws IOException, NoModificationAllowedException { |
| |
| boolean append = false; |
| if (offset > 0) { |
| this.truncateFileAtURL(inputURL, offset); |
| append = true; |
| } |
| |
| byte[] rawData; |
| if (isBinary) { |
| rawData = Base64.decode(data, Base64.DEFAULT); |
| } else { |
| rawData = data.getBytes(); |
| } |
| ByteArrayInputStream in = new ByteArrayInputStream(rawData); |
| try |
| { |
| byte buff[] = new byte[rawData.length]; |
| FileOutputStream out = new FileOutputStream(this.filesystemPathForURL(inputURL), append); |
| try { |
| in.read(buff, 0, buff.length); |
| out.write(buff, 0, rawData.length); |
| out.flush(); |
| } finally { |
| // Always close the output |
| out.close(); |
| } |
| } |
| catch (NullPointerException e) |
| { |
| // This is a bug in the Android implementation of the Java Stack |
| NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString()); |
| throw realException; |
| } |
| |
| return rawData.length; |
| } |
| |
| @Override |
| public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException { |
| File file = new File(filesystemPathForURL(inputURL)); |
| |
| if (!file.exists()) { |
| throw new FileNotFoundException("File at " + inputURL.URL + " does not exist."); |
| } |
| |
| RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw"); |
| try { |
| if (raf.length() >= size) { |
| FileChannel channel = raf.getChannel(); |
| channel.truncate(size); |
| return size; |
| } |
| |
| return raf.length(); |
| } finally { |
| raf.close(); |
| } |
| |
| |
| } |
| |
| |
| } |