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.OutputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; 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 extends Filesystem { private String fsRoot; private CordovaInterface cordova; public LocalFilesystem(String name, CordovaInterface cordova, String fsRoot) { this.name = name; this.fsRoot = fsRoot; this.cordova = cordova; } public String filesystemPathForFullPath(String fullPath) { String path = new File(this.fsRoot, fullPath).toString(); int questionMark = path.indexOf("?"); if (questionMark >= 0) { path = path.substring(0, questionMark); } if (path.endsWith("/")) { path = path.substring(0, path.length()-1); } return path; } @Override public String filesystemPathForURL(LocalFilesystemURL url) { return filesystemPathForFullPath(url.fullPath); } private String fullPathForFilesystemPath(String absolutePath) { if (absolutePath != null && absolutePath.startsWith(this.fsRoot)) { return absolutePath.substring(this.fsRoot.length()); } return null; } protected LocalFilesystemURL URLforFullPath(String fullPath) { if (fullPath != null) { if (fullPath.startsWith("/")) { return new LocalFilesystemURL(LocalFilesystemURL.FILESYSTEM_PROTOCOL + "://localhost/"+this.name+fullPath); } return new LocalFilesystemURL(LocalFilesystemURL.FILESYSTEM_PROTOCOL + "://localhost/"+this.name+"/"+fullPath); } return null; } @Override public LocalFilesystemURL URLforFilesystemPath(String path) { return this.URLforFullPath(this.fullPathForFilesystemPath(path)); } protected String normalizePath(String rawPath) { // If this is an absolute path, trim the leading "/" and replace it later boolean isAbsolutePath = rawPath.startsWith("/"); if (isAbsolutePath) { rawPath = rawPath.substring(1); } ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/"))); for (int index = 0; index < components.size(); ++index) { if (components.get(index).equals("..")) { components.remove(index); if (index > 0) { components.remove(index-1); --index; } } } StringBuilder normalizedPath = new StringBuilder(); for(String component: components) { normalizedPath.append("/"); normalizedPath.append(component); } if (isAbsolutePath) { return normalizedPath.toString(); } else { return normalizedPath.toString().substring(1); } } @Override public JSONObject makeEntryForFile(File file) throws JSONException { String path = this.fullPathForFilesystemPath(file.getAbsolutePath()); if (path != null) { return makeEntryForPath(path, this.name, file.isDirectory(), Uri.fromFile(file).toString()); } return null; } @Override public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException { File fp = new File(filesystemPathForURL(inputURL)); 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 name of the FS, and the rest can be reconstructed // in JS. entry.put("filesystemName", inputURL.filesystemName); // Backwards compatibility entry.put("filesystem", "temporary".equals(name) ? 0 : 1); entry.put("nativeURL", Uri.fromFile(fp).toString()); return entry; } catch (JSONException e) { throw new IOException(); } } @Override public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path, 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 (path.contains(":")) { throw new EncodingException("This path has an invalid \":\" in it."); } LocalFilesystemURL requestedURL; // Check whether the supplied path is absolute or relative if (path.startsWith("/")) { requestedURL = URLforFilesystemPath(path); } else { requestedURL = URLforFullPath(normalizePath(inputURL.fullPath + "/" + path)); } 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.filesystemName, directory, Uri.fromFile(fp).toString()); } @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.filesystemName, files[i].isDirectory(), Uri.fromFile(files[i]).toString())); } 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 { // Ensure that directories report a size of 0 metadata.put("size", file.isDirectory() ? 0 : 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; } /** * 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; } /** * 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 makeEntryForFile(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 makeEntryForFile(destinationDir); } /** * 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 makeEntryForFile(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 makeEntryForFile(destinationDir); } @Override public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName, Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException { // Check to see if the destination directory exists 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 */ // Figure out where we should be copying to final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL); String srcFilesystemPath = srcFs.filesystemPathForURL(srcURL); File sourceFile = new File(srcFilesystemPath); String destFilesystemPath = this.filesystemPathForURL(destinationURL); File destinationFile = new File(destFilesystemPath); if (!sourceFile.exists()) { // The file/directory we are copying doesn't exist so we should fail. throw new FileNotFoundException("The source does not exist"); } // Check to see if source and destination are the same file if (sourceFile.getAbsolutePath().equals(destinationFile.getAbsolutePath())) { throw new InvalidModificationException("Can't copy a file onto itself"); } if (sourceFile.isDirectory()) { if (move) { return moveDirectory(sourceFile, destinationFile); } else { return copyDirectory(sourceFile, destinationFile); } } else { if (move) { return moveFile(sourceFile, destinationFile); } else { return copyFile(sourceFile, destinationFile); } } } else { // Need to copy the hard way return super.copyFileToURL(destURL, newName, srcFs, srcURL, move); } } @Override public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end, ReadFileCallback readFileCallback) throws IOException { File file = new File(this.filesystemPathForURL(inputURL)); String contentType = FileHelper.getMimeTypeForExtension(file.getAbsolutePath()); if (end < 0) { end = file.length(); } long numBytesToRead = end - start; InputStream rawInputStream = new FileInputStream(file); try { if (start > 0) { rawInputStream.skip(start); } LimitedInputStream inputStream = new LimitedInputStream(rawInputStream, numBytesToRead); readFileCallback.handleData(inputStream, contentType); } finally { rawInputStream.close(); } } @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(); } } @Override public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) { String path = filesystemPathForURL(inputURL); File file = new File(path); return file.exists(); } @Override OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws FileNotFoundException { String path = filesystemPathForURL(inputURL); File file = new File(path); FileOutputStream os = new FileOutputStream(file); return os; } }