blob: 755a8022a952b7a9e0d70f5413d2c7ba7189c30d [file] [log] [blame]
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();
}
}
}