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;
}
}