Cordova plugin file und file-transfer geadded

This commit is contained in:
2016-01-08 00:06:37 +01:00
parent 23a833a31b
commit 36c7e2c8e2
234 changed files with 49402 additions and 81 deletions

View File

@@ -0,0 +1,283 @@
/*
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.content.res.AssetManager;
import android.net.Uri;
import android.util.Log;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashMap;
import java.util.Map;
public class AssetFilesystem extends Filesystem {
private final AssetManager assetManager;
// A custom gradle hook creates the cdvasset.manifest file, which speeds up asset listing a tonne.
// See: http://stackoverflow.com/questions/16911558/android-assetmanager-list-incredibly-slow
private static Object listCacheLock = new Object();
private static boolean listCacheFromFile;
private static Map<String, String[]> listCache;
private static Map<String, Long> lengthCache;
private void lazyInitCaches() {
synchronized (listCacheLock) {
if (listCache == null) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(assetManager.open("cdvasset.manifest"));
listCache = (Map<String, String[]>) ois.readObject();
lengthCache = (Map<String, Long>) ois.readObject();
listCacheFromFile = true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// Asset manifest won't exist if the gradle hook isn't set up correctly.
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
}
}
}
if (listCache == null) {
Log.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow.");
listCache = new HashMap<String, String[]>();
}
}
}
}
private String[] listAssets(String assetPath) throws IOException {
if (assetPath.startsWith("/")) {
assetPath = assetPath.substring(1);
}
lazyInitCaches();
String[] ret = listCache.get(assetPath);
if (ret == null) {
if (listCacheFromFile) {
ret = new String[0];
} else {
ret = assetManager.list(assetPath);
listCache.put(assetPath, ret);
}
}
return ret;
}
private long getAssetSize(String assetPath) throws FileNotFoundException {
if (assetPath.startsWith("/")) {
assetPath = assetPath.substring(1);
}
lazyInitCaches();
if (lengthCache != null) {
Long ret = lengthCache.get(assetPath);
if (ret == null) {
throw new FileNotFoundException("Asset not found: " + assetPath);
}
return ret;
}
CordovaResourceApi.OpenForReadResult offr = null;
try {
offr = resourceApi.openForRead(nativeUriForFullPath(assetPath));
long length = offr.length;
if (length < 0) {
// available() doesn't always yield the file size, but for assets it does.
length = offr.inputStream.available();
}
return length;
} catch (IOException e) {
throw new FileNotFoundException("File not found: " + assetPath);
} finally {
if (offr != null) {
try {
offr.inputStream.close();
} catch (IOException e) {
}
}
}
}
public AssetFilesystem(AssetManager assetManager, CordovaResourceApi resourceApi) {
super(Uri.parse("file:///android_asset/"), "assets", resourceApi);
this.assetManager = assetManager;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
return nativeUriForFullPath(inputURL.path);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"file".equals(inputURL.getScheme())) {
return null;
}
File f = new File(inputURL.getPath());
// Removes and duplicate /s (e.g. file:///a//b/c)
Uri resolvedUri = Uri.fromFile(f);
String rootUriNoTrailingSlash = rootUri.getEncodedPath();
rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
return null;
}
String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
// Strip leading slash
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);
if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
if (isDirectory(subPath) || inputURL.getPath().endsWith("/")) {
// Add trailing / for directories.
b.appendEncodedPath("");
}
return LocalFilesystemURL.parse(b.build());
}
private boolean isDirectory(String assetPath) {
try {
return listAssets(assetPath).length != 0;
} catch (IOException e) {
return false;
}
}
@Override
public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
String pathNoSlashes = inputURL.path.substring(1);
if (pathNoSlashes.endsWith("/")) {
pathNoSlashes = pathNoSlashes.substring(0, pathNoSlashes.length() - 1);
}
String[] files;
try {
files = listAssets(pathNoSlashes);
} catch (IOException e) {
throw new FileNotFoundException();
}
LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
for (int i = 0; i < files.length; ++i) {
entries[i] = localUrlforFullPath(new File(inputURL.path, files[i]).getPath());
}
return entries;
}
@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String path, JSONObject options, boolean directory)
throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
if (options != null && options.optBoolean("create")) {
throw new UnsupportedOperationException("Assets are read-only");
}
// Check whether the supplied path is absolute or relative
if (directory && !path.endsWith("/")) {
path += "/";
}
LocalFilesystemURL requestedURL;
if (path.startsWith("/")) {
requestedURL = localUrlforFullPath(normalizePath(path));
} else {
requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
}
// Throws a FileNotFoundException if it doesn't exist.
getFileMetadataForLocalURL(requestedURL);
boolean isDir = isDirectory(requestedURL.path);
if (directory && !isDir) {
throw new TypeMismatchException("path doesn't exist or is file");
} else if (!directory && isDir) {
throw new TypeMismatchException("path doesn't exist or is directory");
}
// Return the directory
return makeEntryForURL(requestedURL);
}
@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
JSONObject metadata = new JSONObject();
long size = inputURL.isDirectory ? 0 : getAssetSize(inputURL.path);
try {
metadata.put("size", size);
metadata.put("type", inputURL.isDirectory ? "text/directory" : resourceApi.getMimeType(toNativeUri(inputURL)));
metadata.put("name", new File(inputURL.path).getName());
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", 0);
} catch (JSONException e) {
return null;
}
return metadata;
}
@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
return false;
}
@Override
long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset, boolean isBinary) throws NoModificationAllowedException, IOException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
String filesystemPathForURL(LocalFilesystemURL url) {
return null;
}
@Override
LocalFilesystemURL URLforFilesystemPath(String path) {
return null;
}
@Override
boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
@Override
boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException {
throw new NoModificationAllowedException("Assets are read-only");
}
}

View File

@@ -0,0 +1,215 @@
/*
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 java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
public class ContentFilesystem extends Filesystem {
private final Context context;
public ContentFilesystem(Context context, CordovaResourceApi resourceApi) {
super(Uri.parse("content://"), "content", resourceApi);
this.context = context;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
String authorityAndPath = inputURL.uri.getEncodedPath().substring(this.name.length() + 2);
if (authorityAndPath.length() < 2) {
return null;
}
String ret = "content://" + authorityAndPath;
String query = inputURL.uri.getEncodedQuery();
if (query != null) {
ret += '?' + query;
}
String frag = inputURL.uri.getEncodedFragment();
if (frag != null) {
ret += '#' + frag;
}
return Uri.parse(ret);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"content".equals(inputURL.getScheme())) {
return null;
}
String subPath = inputURL.getEncodedPath();
if (subPath.length() > 0) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name)
.appendPath(inputURL.getAuthority());
if (subPath.length() > 0) {
b.appendEncodedPath(subPath);
}
Uri localUri = b.encodedQuery(inputURL.getEncodedQuery())
.encodedFragment(inputURL.getEncodedFragment())
.build();
return LocalFilesystemURL.parse(localUri);
}
@Override
public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
String fileName, JSONObject options, boolean directory) throws IOException, TypeMismatchException, JSONException {
throw new UnsupportedOperationException("getFile() not supported for content:. Use resolveLocalFileSystemURL instead.");
}
@Override
public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL)
throws NoModificationAllowedException {
Uri contentUri = toNativeUri(inputURL);
try {
context.getContentResolver().delete(contentUri, null, null);
} 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.
throw new NoModificationAllowedException("Deleting not supported for content uri: " + contentUri);
}
return true;
}
@Override
public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL)
throws NoModificationAllowedException {
throw new NoModificationAllowedException("Cannot remove content url");
}
@Override
public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
throw new UnsupportedOperationException("readEntriesAtLocalURL() not supported for content:. Use resolveLocalFileSystemURL instead.");
}
@Override
public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
long size = -1;
long lastModified = 0;
Uri nativeUri = toNativeUri(inputURL);
String mimeType = resourceApi.getMimeType(nativeUri);
Cursor cursor = openCursorForURL(nativeUri);
try {
if (cursor != null && cursor.moveToFirst()) {
size = resourceSizeForCursor(cursor);
lastModified = lastModifiedDateForCursor(cursor);
} else {
// Some content providers don't support cursors at all!
CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(nativeUri);
size = offr.length;
}
} catch (IOException e) {
throw new FileNotFoundException();
} finally {
if (cursor != null)
cursor.close();
}
JSONObject metadata = new JSONObject();
try {
metadata.put("size", size);
metadata.put("type", mimeType);
metadata.put("name", name);
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", lastModified);
} catch (JSONException e) {
return null;
}
return metadata;
}
@Override
public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
int offset, boolean isBinary) throws NoModificationAllowedException {
throw new NoModificationAllowedException("Couldn't write to file given its content URI");
}
@Override
public long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
throws NoModificationAllowedException {
throw new NoModificationAllowedException("Couldn't truncate file given its content URI");
}
protected Cursor openCursorForURL(Uri nativeUri) {
ContentResolver contentResolver = context.getContentResolver();
try {
return contentResolver.query(nativeUri, null, null, null, null);
} catch (UnsupportedOperationException e) {
return null;
}
}
private Long resourceSizeForCursor(Cursor cursor) {
int columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
if (columnIndex != -1) {
String sizeStr = cursor.getString(columnIndex);
if (sizeStr != null) {
return Long.parseLong(sizeStr);
}
}
return null;
}
protected Long lastModifiedDateForCursor(Cursor cursor) {
final String[] LOCAL_FILE_PROJECTION = { MediaStore.MediaColumns.DATE_MODIFIED };
int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]);
if (columnIndex != -1) {
String dateStr = cursor.getString(columnIndex);
if (dateStr != null) {
return Long.parseLong(dateStr);
}
}
return null;
}
@Override
public String filesystemPathForURL(LocalFilesystemURL url) {
File f = resourceApi.mapUriToFile(toNativeUri(url));
return f == null ? null : f.getAbsolutePath();
}
@Override
public LocalFilesystemURL URLforFilesystemPath(String path) {
// Returns null as we don't support reverse mapping back to content:// URLs
return null;
}
@Override
public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
return true;
}
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,325 @@
/*
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.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.cordova.CordovaResourceApi;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class Filesystem {
protected final Uri rootUri;
protected final CordovaResourceApi resourceApi;
public final String name;
private JSONObject rootEntry;
public Filesystem(Uri rootUri, String name, CordovaResourceApi resourceApi) {
this.rootUri = rootUri;
this.name = name;
this.resourceApi = resourceApi;
}
public interface ReadFileCallback {
public void handleData(InputStream inputStream, String contentType) throws IOException;
}
public static JSONObject makeEntryForURL(LocalFilesystemURL inputURL, Uri nativeURL) {
try {
String path = inputURL.path;
int end = path.endsWith("/") ? 1 : 0;
String[] parts = path.substring(0, path.length() - end).split("/+");
String fileName = parts[parts.length - 1];
JSONObject entry = new JSONObject();
entry.put("isFile", !inputURL.isDirectory);
entry.put("isDirectory", inputURL.isDirectory);
entry.put("name", fileName);
entry.put("fullPath", path);
// The file system can't be specified, as it would lead to an infinite loop,
// but the filesystem name can be.
entry.put("filesystemName", inputURL.fsName);
// Backwards compatibility
entry.put("filesystem", "temporary".equals(inputURL.fsName) ? 0 : 1);
String nativeUrlStr = nativeURL.toString();
if (inputURL.isDirectory && !nativeUrlStr.endsWith("/")) {
nativeUrlStr += "/";
}
entry.put("nativeURL", nativeUrlStr);
return entry;
} catch (JSONException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public JSONObject makeEntryForURL(LocalFilesystemURL inputURL) {
Uri nativeUri = toNativeUri(inputURL);
return nativeUri == null ? null : makeEntryForURL(inputURL, nativeUri);
}
public JSONObject makeEntryForNativeUri(Uri nativeUri) {
LocalFilesystemURL inputUrl = toLocalUri(nativeUri);
return inputUrl == null ? null : makeEntryForURL(inputUrl, nativeUri);
}
public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException {
return makeEntryForURL(inputURL);
}
public JSONObject makeEntryForFile(File file) {
return makeEntryForNativeUri(Uri.fromFile(file));
}
abstract JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String path,
JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException;
abstract boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException, NoModificationAllowedException;
abstract boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException, NoModificationAllowedException;
abstract LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException;
public final JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
LocalFilesystemURL[] children = listChildren(inputURL);
JSONArray entries = new JSONArray();
if (children != null) {
for (LocalFilesystemURL url : children) {
entries.put(makeEntryForURL(url));
}
}
return entries;
}
abstract JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException;
public Uri getRootUri() {
return rootUri;
}
public boolean exists(LocalFilesystemURL inputURL) {
try {
getFileMetadataForLocalURL(inputURL);
} catch (FileNotFoundException e) {
return false;
}
return true;
}
public Uri nativeUriForFullPath(String fullPath) {
Uri ret = null;
if (fullPath != null) {
String encodedPath = Uri.fromFile(new File(fullPath)).getEncodedPath();
if (encodedPath.startsWith("/")) {
encodedPath = encodedPath.substring(1);
}
ret = rootUri.buildUpon().appendEncodedPath(encodedPath).build();
}
return ret;
}
public LocalFilesystemURL localUrlforFullPath(String fullPath) {
Uri nativeUri = nativeUriForFullPath(fullPath);
if (nativeUri != null) {
return toLocalUri(nativeUri);
}
return null;
}
/**
* Removes multiple repeated //s, and collapses processes ../s.
*/
protected static 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.replaceFirst("/+", "");
}
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);
}
}
public abstract Uri toNativeUri(LocalFilesystemURL inputURL);
public abstract LocalFilesystemURL toLocalUri(Uri inputURL);
public JSONObject getRootEntry() {
if (rootEntry == null) {
rootEntry = makeEntryForNativeUri(rootUri);
}
return rootEntry;
}
public JSONObject getParentForLocalURL(LocalFilesystemURL inputURL) throws IOException {
Uri parentUri = inputURL.uri;
String parentPath = new File(inputURL.uri.getPath()).getParent();
if (!"/".equals(parentPath)) {
parentUri = inputURL.uri.buildUpon().path(parentPath + '/').build();
}
return getEntryForLocalURL(LocalFilesystemURL.parse(parentUri));
}
protected LocalFilesystemURL makeDestinationURL(String newName, LocalFilesystemURL srcURL, LocalFilesystemURL destURL, boolean isDirectory) {
// I know this looks weird but it is to work around a JSON bug.
if ("null".equals(newName) || "".equals(newName)) {
newName = srcURL.uri.getLastPathSegment();;
}
String newDest = destURL.uri.toString();
if (newDest.endsWith("/")) {
newDest = newDest + newName;
} else {
newDest = newDest + "/" + newName;
}
if (isDirectory) {
newDest += '/';
}
return LocalFilesystemURL.parse(newDest);
}
/* Read a source URL (possibly from a different filesystem, srcFs,) and copy it to
* the destination URL on this filesystem, optionally with a new filename.
* If move is true, then this method should either perform an atomic move operation
* or remove the source file when finished.
*/
public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
// First, check to see that we can do it
if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
throw new NoModificationAllowedException("Cannot move file at source URL");
}
final LocalFilesystemURL destination = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
Uri srcNativeUri = srcFs.toNativeUri(srcURL);
CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(srcNativeUri);
OutputStream os = null;
try {
os = getOutputStreamForURL(destination);
} catch (IOException e) {
ofrr.inputStream.close();
throw e;
}
// Closes streams.
resourceApi.copyResource(ofrr, os);
if (move) {
srcFs.removeFileAtLocalURL(srcURL);
}
return getEntryForLocalURL(destination);
}
public OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws IOException {
return resourceApi.openOutputStream(toNativeUri(inputURL));
}
public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end,
ReadFileCallback readFileCallback) throws IOException {
CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(toNativeUri(inputURL));
if (end < 0) {
end = ofrr.length;
}
long numBytesToRead = end - start;
try {
if (start > 0) {
ofrr.inputStream.skip(start);
}
InputStream inputStream = ofrr.inputStream;
if (end < ofrr.length) {
inputStream = new LimitedInputStream(inputStream, numBytesToRead);
}
readFileCallback.handleData(inputStream, ofrr.mimeType);
} finally {
ofrr.inputStream.close();
}
}
abstract long writeToFileAtURL(LocalFilesystemURL inputURL, String data, int offset,
boolean isBinary) throws NoModificationAllowedException, IOException;
abstract long truncateFileAtURL(LocalFilesystemURL inputURL, long size)
throws IOException, NoModificationAllowedException;
// This method should return null if filesystem urls cannot be mapped to paths
abstract String filesystemPathForURL(LocalFilesystemURL url);
abstract LocalFilesystemURL URLforFilesystemPath(String path);
abstract boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL);
protected class LimitedInputStream extends FilterInputStream {
long numBytesToRead;
public LimitedInputStream(InputStream in, long numBytesToRead) {
super(in);
this.numBytesToRead = numBytesToRead;
}
@Override
public int read() throws IOException {
if (numBytesToRead <= 0) {
return -1;
}
numBytesToRead--;
return in.read();
}
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
if (numBytesToRead <= 0) {
return -1;
}
int bytesToRead = byteCount;
if (byteCount > numBytesToRead) {
bytesToRead = (int)numBytesToRead; // Cast okay; long is less than int here.
}
int numBytesRead = in.read(buffer, byteOffset, bytesToRead);
numBytesToRead -= numBytesRead;
return numBytesRead;
}
}
}

View File

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

View File

@@ -0,0 +1,505 @@
/*
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 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 org.apache.cordova.CordovaResourceApi;
import org.json.JSONException;
import org.json.JSONObject;
import android.os.Build;
import android.os.Environment;
import android.util.Base64;
import android.net.Uri;
import android.content.Context;
import android.content.Intent;
public class LocalFilesystem extends Filesystem {
private final Context context;
public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) {
super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi);
this.context = context;
}
public String filesystemPathForFullPath(String fullPath) {
return new File(rootUri.getPath(), fullPath).toString();
}
@Override
public String filesystemPathForURL(LocalFilesystemURL url) {
return filesystemPathForFullPath(url.path);
}
private String fullPathForFilesystemPath(String absolutePath) {
if (absolutePath != null && absolutePath.startsWith(rootUri.getPath())) {
return absolutePath.substring(rootUri.getPath().length() - 1);
}
return null;
}
@Override
public Uri toNativeUri(LocalFilesystemURL inputURL) {
return nativeUriForFullPath(inputURL.path);
}
@Override
public LocalFilesystemURL toLocalUri(Uri inputURL) {
if (!"file".equals(inputURL.getScheme())) {
return null;
}
File f = new File(inputURL.getPath());
// Removes and duplicate /s (e.g. file:///a//b/c)
Uri resolvedUri = Uri.fromFile(f);
String rootUriNoTrailingSlash = rootUri.getEncodedPath();
rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
return null;
}
String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
// Strip leading slash
if (!subPath.isEmpty()) {
subPath = subPath.substring(1);
}
Uri.Builder b = new Uri.Builder()
.scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
.authority("localhost")
.path(name);
if (!subPath.isEmpty()) {
b.appendEncodedPath(subPath);
}
if (f.isDirectory() || inputURL.getPath().endsWith("/")) {
// Add trailing / for directories.
b.appendEncodedPath("");
}
return LocalFilesystemURL.parse(b.build());
}
@Override
public LocalFilesystemURL URLforFilesystemPath(String path) {
return localUrlforFullPath(fullPathForFilesystemPath(path));
}
@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 (directory && !path.endsWith("/")) {
path += "/";
}
if (path.startsWith("/")) {
requestedURL = localUrlforFullPath(normalizePath(path));
} else {
requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + 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 makeEntryForURL(requestedURL);
}
@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 exists(LocalFilesystemURL inputURL) {
File fp = new File(filesystemPathForURL(inputURL));
return fp.exists();
}
@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 LocalFilesystemURL[] listChildren(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();
}
File[] files = fp.listFiles();
if (files == null) {
// inputURL is a directory
return null;
}
LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
for (int i = 0; i < files.length; i++) {
entries[i] = URLforFilesystemPath(files[i].getPath());
}
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.uri + " 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", resourceApi.getMimeType(Uri.fromFile(file)));
metadata.put("name", file.getName());
metadata.put("fullPath", inputURL.path);
metadata.put("lastModifiedDate", file.lastModified());
} catch (JSONException e) {
return null;
}
return metadata;
}
private void copyFile(Filesystem srcFs, LocalFilesystemURL srcURL, File destFile, boolean move) throws IOException, InvalidModificationException, NoModificationAllowedException {
if (move) {
String realSrcPath = srcFs.filesystemPathForURL(srcURL);
if (realSrcPath != null) {
File srcFile = new File(realSrcPath);
if (srcFile.renameTo(destFile)) {
return;
}
// Trying to rename the file failed. Possibly because we moved across file system on the device.
}
}
CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(srcFs.toNativeUri(srcURL));
copyResource(offr, new FileOutputStream(destFile));
if (move) {
srcFs.removeFileAtLocalURL(srcURL);
}
}
private void copyDirectory(Filesystem srcFs, LocalFilesystemURL srcURL, File dstDir, boolean move) throws IOException, NoModificationAllowedException, InvalidModificationException, FileExistsException {
if (move) {
String realSrcPath = srcFs.filesystemPathForURL(srcURL);
if (realSrcPath != null) {
File srcDir = new File(realSrcPath);
// If the destination directory already exists and is empty then delete it. This is according to spec.
if (dstDir.exists()) {
if (dstDir.list().length > 0) {
throw new InvalidModificationException("directory is not empty");
}
dstDir.delete();
}
// Try to rename the directory
if (srcDir.renameTo(dstDir)) {
return;
}
// Trying to rename the file failed. Possibly because we moved across file system on the device.
}
}
if (dstDir.exists()) {
if (dstDir.list().length > 0) {
throw new InvalidModificationException("directory is not empty");
}
} else {
if (!dstDir.mkdir()) {
// If we can't create the directory then fail
throw new NoModificationAllowedException("Couldn't create the destination directory");
}
}
LocalFilesystemURL[] children = srcFs.listChildren(srcURL);
for (LocalFilesystemURL childLocalUrl : children) {
File target = new File(dstDir, new File(childLocalUrl.path).getName());
if (childLocalUrl.isDirectory) {
copyDirectory(srcFs, childLocalUrl, target, false);
} else {
copyFile(srcFs, childLocalUrl, target, false);
}
}
if (move) {
srcFs.recursiveRemoveFileAtLocalURL(srcURL);
}
}
@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");
}
// Figure out where we should be copying to
final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
Uri dstNativeUri = toNativeUri(destinationURL);
Uri srcNativeUri = srcFs.toNativeUri(srcURL);
// Check to see if source and destination are the same file
if (dstNativeUri.equals(srcNativeUri)) {
throw new InvalidModificationException("Can't copy onto itself");
}
if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
throw new InvalidModificationException("Source URL is read-only (cannot move)");
}
File destFile = new File(dstNativeUri.getPath());
if (destFile.exists()) {
if (!srcURL.isDirectory && destFile.isDirectory()) {
throw new InvalidModificationException("Can't copy/move a file to an existing directory");
} else if (srcURL.isDirectory && destFile.isFile()) {
throw new InvalidModificationException("Can't copy/move a directory to an existing file");
}
}
if (srcURL.isDirectory) {
// E.g. Copy /sdcard/myDir to /sdcard/myDir/backup
if (dstNativeUri.toString().startsWith(srcNativeUri.toString() + '/')) {
throw new InvalidModificationException("Can't copy directory into itself");
}
copyDirectory(srcFs, srcURL, destFile, move);
} else {
copyFile(srcFs, srcURL, destFile, move);
}
return makeEntryForURL(destinationURL);
}
@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];
String absolutePath = filesystemPathForURL(inputURL);
FileOutputStream out = new FileOutputStream(absolutePath, append);
try {
in.read(buff, 0, buff.length);
out.write(buff, 0, rawData.length);
out.flush();
} finally {
// Always close the output
out.close();
}
if (isPublicDirectory(absolutePath)) {
broadcastNewFile(Uri.fromFile(new File(absolutePath)));
}
}
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;
}
private boolean isPublicDirectory(String absolutePath) {
// TODO: should expose a way to scan app's private files (maybe via a flag).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Lollipop has a bug where SD cards are null.
for (File f : context.getExternalMediaDirs()) {
if(f != null && absolutePath.startsWith(f.getAbsolutePath())) {
return true;
}
}
}
String extPath = Environment.getExternalStorageDirectory().getAbsolutePath();
return absolutePath.startsWith(extPath);
}
/**
* Send broadcast of new file so files appear over MTP
*/
private void broadcastNewFile(Uri nativeUri) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, nativeUri);
context.sendBroadcast(intent);
}
@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.uri + " 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();
}
// This is a copy & paste from CordovaResource API that is required since CordovaResourceApi
// has a bug pre-4.0.0.
// TODO: Once cordova-android@4.0.0 is released, delete this copy and make the plugin depend on
// 4.0.0 with an engine tag.
private static void copyResource(CordovaResourceApi.OpenForReadResult input, OutputStream outputStream) throws IOException {
try {
InputStream inputStream = input.inputStream;
if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) {
FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel();
FileChannel outChannel = ((FileOutputStream)outputStream).getChannel();
long offset = 0;
long length = input.length;
if (input.assetFd != null) {
offset = input.assetFd.getStartOffset();
}
// transferFrom()'s 2nd arg is a relative position. Need to set the absolute
// position first.
inChannel.position(offset);
outChannel.transferFrom(inChannel, 0, length);
} else {
final int BUFFER_SIZE = 8192;
byte[] buffer = new byte[BUFFER_SIZE];
for (;;) {
int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
if (bytesRead <= 0) {
break;
}
outputStream.write(buffer, 0, bytesRead);
}
}
} finally {
input.inputStream.close();
if (outputStream != null) {
outputStream.close();
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
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.net.Uri;
public class LocalFilesystemURL {
public static final String FILESYSTEM_PROTOCOL = "cdvfile";
public final Uri uri;
public final String fsName;
public final String path;
public final boolean isDirectory;
private LocalFilesystemURL(Uri uri, String fsName, String fsPath, boolean isDirectory) {
this.uri = uri;
this.fsName = fsName;
this.path = fsPath;
this.isDirectory = isDirectory;
}
public static LocalFilesystemURL parse(Uri uri) {
if (!FILESYSTEM_PROTOCOL.equals(uri.getScheme())) {
return null;
}
String path = uri.getPath();
if (path.length() < 1) {
return null;
}
int firstSlashIdx = path.indexOf('/', 1);
if (firstSlashIdx < 0) {
return null;
}
String fsName = path.substring(1, firstSlashIdx);
path = path.substring(firstSlashIdx);
boolean isDirectory = path.charAt(path.length() - 1) == '/';
return new LocalFilesystemURL(uri, fsName, path, isDirectory);
}
public static LocalFilesystemURL parse(String uri) {
return parse(Uri.parse(uri));
}
public String toString() {
return uri.toString();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
/*
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.
*/
ext.postBuildExtras = {
def inAssetsDir = file("assets")
def outAssetsDir = inAssetsDir
def outFile = new File(outAssetsDir, "cdvasset.manifest")
def newTask = task("cdvCreateAssetManifest") << {
def contents = new HashMap()
def sizes = new HashMap()
contents[""] = inAssetsDir.list()
def tree = fileTree(dir: inAssetsDir)
tree.visit { fileDetails ->
if (fileDetails.isDirectory()) {
contents[fileDetails.relativePath.toString()] = fileDetails.file.list()
} else {
sizes[fileDetails.relativePath.toString()] = fileDetails.file.length()
}
}
outAssetsDir.mkdirs()
outFile.withObjectOutputStream { oos ->
oos.writeObject(contents)
oos.writeObject(sizes)
}
}
newTask.inputs.dir inAssetsDir
newTask.outputs.file outFile
def preBuildTask = tasks["preBuild"]
preBuildTask.dependsOn(newTask)
}

View File

@@ -0,0 +1,44 @@
/*
*
* 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.
*
*/
module.exports = {
setSandbox : function (success, fail, args, env) {
require("lib/webview").setSandbox(JSON.parse(decodeURIComponent(args[0])));
new PluginResult(args, env).ok();
},
getHomePath: function (success, fail, args, env) {
var homeDir = window.qnx.webplatform.getApplication().getEnv("HOME");
new PluginResult(args, env).ok(homeDir);
},
requestAllPaths: function (success, fail, args, env) {
var homeDir = 'file://' + window.qnx.webplatform.getApplication().getEnv("HOME").replace('/data', ''),
paths = {
applicationDirectory: homeDir + '/app/native/',
applicationStorageDirectory: homeDir + '/',
dataDirectory: homeDir + '/data/webviews/webfs/persistent/local__0/',
cacheDirectory: homeDir + '/data/webviews/webfs/temporary/local__0/',
externalRootDirectory: 'file:///accounts/1000/removable/sdcard/',
sharedDirectory: homeDir + '/shared/'
};
success(paths);
}
};

View File

@@ -0,0 +1,964 @@
/*
*
* 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.
*
*/
/*global require, exports, module*/
/*global FILESYSTEM_PREFIX*/
/*global IDBKeyRange*/
/* Heavily based on https://github.com/ebidel/idb.filesystem.js */
// window.webkitRequestFileSystem and window.webkitResolveLocalFileSystemURL
// are available only in Chrome and possible a good flag to indicate
// that we're running in Chrome
var isChrome = window.webkitRequestFileSystem && window.webkitResolveLocalFileSystemURL;
// For chrome we don't need to implement proxy methods
// All functionality can be accessed natively.
if (isChrome) {
var pathsPrefix = {
// Read-only directory where the application is installed.
applicationDirectory: location.origin + "/",
// Where to put app-specific data files.
dataDirectory: 'filesystem:file:///persistent/',
// Cached files that should survive app restarts.
// Apps should not rely on the OS to delete files in here.
cacheDirectory: 'filesystem:file:///temporary/',
};
exports.requestAllPaths = function(successCallback) {
successCallback(pathsPrefix);
};
require("cordova/exec/proxy").add("File", module.exports);
return;
}
var LocalFileSystem = require('./LocalFileSystem'),
FileSystem = require('./FileSystem'),
FileEntry = require('./FileEntry'),
FileError = require('./FileError'),
DirectoryEntry = require('./DirectoryEntry'),
File = require('./File');
(function(exports, global) {
var indexedDB = global.indexedDB || global.mozIndexedDB;
if (!indexedDB) {
throw "Firefox OS File plugin: indexedDB not supported";
}
var fs_ = null;
var idb_ = {};
idb_.db = null;
var FILE_STORE_ = 'entries';
var DIR_SEPARATOR = '/';
var pathsPrefix = {
// Read-only directory where the application is installed.
applicationDirectory: location.origin + "/",
// Where to put app-specific data files.
dataDirectory: 'file:///persistent/',
// Cached files that should survive app restarts.
// Apps should not rely on the OS to delete files in here.
cacheDirectory: 'file:///temporary/',
};
var unicodeLastChar = 65535;
/*** Exported functionality ***/
exports.requestFileSystem = function(successCallback, errorCallback, args) {
var type = args[0];
// Size is ignored since IDB filesystem size depends
// on browser implementation and can't be set up by user
var size = args[1]; // jshint ignore: line
if (type !== LocalFileSystem.TEMPORARY && type !== LocalFileSystem.PERSISTENT) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var name = type === LocalFileSystem.TEMPORARY ? 'temporary' : 'persistent';
var storageName = (location.protocol + location.host).replace(/:/g, '_');
var root = new DirectoryEntry('', DIR_SEPARATOR);
fs_ = new FileSystem(name, root);
idb_.open(storageName, function() {
successCallback(fs_);
}, errorCallback);
};
// Overridden by Android, BlackBerry 10 and iOS to populate fsMap
require('./fileSystems').getFs = function(name, callback) {
callback(new FileSystem(name, fs_.root));
};
// list a directory's contents (files and folders).
exports.readEntries = function(successCallback, errorCallback, args) {
var fullPath = args[0];
if (typeof successCallback !== 'function') {
throw Error('Expected successCallback argument.');
}
var path = resolveToFullPath_(fullPath);
exports.getDirectory(function() {
idb_.getAllEntries(path.fullPath + DIR_SEPARATOR, path.storagePath, function(entries) {
successCallback(entries);
}, errorCallback);
}, function() {
if (errorCallback) {
errorCallback(FileError.NOT_FOUND_ERR);
}
}, [path.storagePath, path.fullPath, {create: false}]);
};
exports.getFile = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var path = args[1];
var options = args[2] || {};
// Create an absolute path if we were handed a relative one.
path = resolveToFullPath_(fullPath, path);
idb_.get(path.storagePath, function(fileEntry) {
if (options.create === true && options.exclusive === true && fileEntry) {
// If create and exclusive are both true, and the path already exists,
// getFile must fail.
if (errorCallback) {
errorCallback(FileError.PATH_EXISTS_ERR);
}
} else if (options.create === true && !fileEntry) {
// If create is true, the path doesn't exist, and no other error occurs,
// getFile must create it as a zero-length file and return a corresponding
// FileEntry.
var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
newFileEntry.file_ = new MyFile({
size: 0,
name: newFileEntry.name,
lastModifiedDate: new Date(),
storagePath: path.storagePath
});
idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
} else if (options.create === true && fileEntry) {
if (fileEntry.isFile) {
// Overwrite file, delete then create new.
idb_['delete'](path.storagePath, function() {
var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
newFileEntry.file_ = new MyFile({
size: 0,
name: newFileEntry.name,
lastModifiedDate: new Date(),
storagePath: path.storagePath
});
idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
}, errorCallback);
} else {
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
}
} else if ((!options.create || options.create === false) && !fileEntry) {
// If create is not true and the path doesn't exist, getFile must fail.
if (errorCallback) {
errorCallback(FileError.NOT_FOUND_ERR);
}
} else if ((!options.create || options.create === false) && fileEntry &&
fileEntry.isDirectory) {
// If create is not true and the path exists, but is a directory, getFile
// must fail.
if (errorCallback) {
errorCallback(FileError.TYPE_MISMATCH_ERR);
}
} else {
// Otherwise, if no other error occurs, getFile must return a FileEntry
// corresponding to path.
successCallback(fileEntryFromIdbEntry(fileEntry));
}
}, errorCallback);
};
exports.getFileMetadata = function(successCallback, errorCallback, args) {
var fullPath = args[0];
exports.getFile(function(fileEntry) {
successCallback(new File(fileEntry.file_.name, fileEntry.fullPath, '', fileEntry.file_.lastModifiedDate,
fileEntry.file_.size));
}, errorCallback, [fullPath, null]);
};
exports.getMetadata = function(successCallback, errorCallback, args) {
exports.getFile(function (fileEntry) {
successCallback(
{
modificationTime: fileEntry.file_.lastModifiedDate,
size: fileEntry.file_.lastModifiedDate
});
}, errorCallback, args);
};
exports.setMetadata = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var metadataObject = args[1];
exports.getFile(function (fileEntry) {
fileEntry.file_.lastModifiedDate = metadataObject.modificationTime;
idb_.put(fileEntry, fileEntry.file_.storagePath, successCallback, errorCallback);
}, errorCallback, [fullPath, null]);
};
exports.write = function(successCallback, errorCallback, args) {
var fileName = args[0],
data = args[1],
position = args[2],
isBinary = args[3]; // jshint ignore: line
if (!data) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
if (typeof data === 'string' || data instanceof String) {
data = new Blob([data]);
}
exports.getFile(function(fileEntry) {
var blob_ = fileEntry.file_.blob_;
if (!blob_) {
blob_ = new Blob([data], {type: data.type});
} else {
// Calc the head and tail fragments
var head = blob_.slice(0, position);
var tail = blob_.slice(position + (data.size || data.byteLength));
// Calc the padding
var padding = position - head.size;
if (padding < 0) {
padding = 0;
}
// Do the "write". In fact, a full overwrite of the Blob.
blob_ = new Blob([head, new Uint8Array(padding), data, tail],
{type: data.type});
}
// Set the blob we're writing on this file entry so we can recall it later.
fileEntry.file_.blob_ = blob_;
fileEntry.file_.lastModifiedDate = new Date() || null;
fileEntry.file_.size = blob_.size;
fileEntry.file_.name = blob_.name;
fileEntry.file_.type = blob_.type;
idb_.put(fileEntry, fileEntry.file_.storagePath, function() {
successCallback(data.size || data.byteLength);
}, errorCallback);
}, errorCallback, [fileName, null]);
};
exports.readAsText = function(successCallback, errorCallback, args) {
var fileName = args[0],
enc = args[1],
startPos = args[2],
endPos = args[3];
readAs('text', fileName, enc, startPos, endPos, successCallback, errorCallback);
};
exports.readAsDataURL = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('dataURL', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.readAsBinaryString = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('binaryString', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.readAsArrayBuffer = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('arrayBuffer', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.removeRecursively = exports.remove = function(successCallback, errorCallback, args) {
if (typeof successCallback !== 'function') {
throw Error('Expected successCallback argument.');
}
var fullPath = resolveToFullPath_(args[0]).storagePath;
if (fullPath === pathsPrefix.cacheDirectory || fullPath === pathsPrefix.dataDirectory) {
errorCallback(FileError.NO_MODIFICATION_ALLOWED_ERR);
return;
}
function deleteEntry(isDirectory) {
// TODO: This doesn't protect against directories that have content in it.
// Should throw an error instead if the dirEntry is not empty.
idb_['delete'](fullPath, function() {
successCallback();
}, function() {
if (errorCallback) { errorCallback(); }
}, isDirectory);
}
// We need to to understand what we are deleting:
exports.getDirectory(function(entry) {
deleteEntry(entry.isDirectory);
}, function(){
//DirectoryEntry was already deleted or entry is FileEntry
deleteEntry(false);
}, [fullPath, null, {create: false}]);
};
exports.getDirectory = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var path = args[1];
var options = args[2];
// Create an absolute path if we were handed a relative one.
path = resolveToFullPath_(fullPath, path);
idb_.get(path.storagePath, function(folderEntry) {
if (!options) {
options = {};
}
if (options.create === true && options.exclusive === true && folderEntry) {
// If create and exclusive are both true, and the path already exists,
// getDirectory must fail.
if (errorCallback) {
errorCallback(FileError.PATH_EXISTS_ERR);
}
// There is a strange bug in mobilespec + FF, which results in coming to multiple else-if's
// so we are shielding from it with returns.
return;
}
if (options.create === true && !folderEntry) {
// If create is true, the path doesn't exist, and no other error occurs,
// getDirectory must create it as a zero-length file and return a corresponding
// MyDirectoryEntry.
var dirEntry = new DirectoryEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
idb_.put(dirEntry, path.storagePath, successCallback, errorCallback);
return;
}
if (options.create === true && folderEntry) {
if (folderEntry.isDirectory) {
// IDB won't save methods, so we need re-create the MyDirectoryEntry.
successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.filesystem));
} else {
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
}
return;
}
if ((!options.create || options.create === false) && !folderEntry) {
// Handle root special. It should always exist.
if (path.fullPath === DIR_SEPARATOR) {
successCallback(fs_.root);
return;
}
// If create is not true and the path doesn't exist, getDirectory must fail.
if (errorCallback) {
errorCallback(FileError.NOT_FOUND_ERR);
}
return;
}
if ((!options.create || options.create === false) && folderEntry && folderEntry.isFile) {
// If create is not true and the path exists, but is a file, getDirectory
// must fail.
if (errorCallback) {
errorCallback(FileError.TYPE_MISMATCH_ERR);
}
return;
}
// Otherwise, if no other error occurs, getDirectory must return a
// MyDirectoryEntry corresponding to path.
// IDB won't' save methods, so we need re-create MyDirectoryEntry.
successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.filesystem));
}, errorCallback);
};
exports.getParent = function(successCallback, errorCallback, args) {
if (typeof successCallback !== 'function') {
throw Error('Expected successCallback argument.');
}
var fullPath = args[0];
//fullPath is like this:
//file:///persistent/path/to/file or
//file:///persistent/path/to/directory/
if (fullPath === DIR_SEPARATOR || fullPath === pathsPrefix.cacheDirectory ||
fullPath === pathsPrefix.dataDirectory) {
successCallback(fs_.root);
return;
}
//To delete all slashes at the end
while (fullPath[fullPath.length - 1] === '/') {
fullPath = fullPath.substr(0, fullPath.length - 1);
}
var pathArr = fullPath.split(DIR_SEPARATOR);
pathArr.pop();
var parentName = pathArr.pop();
var path = pathArr.join(DIR_SEPARATOR) + DIR_SEPARATOR;
//To get parent of root files
var joined = path + parentName + DIR_SEPARATOR;//is like this: file:///persistent/
if (joined === pathsPrefix.cacheDirectory || joined === pathsPrefix.dataDirectory) {
exports.getDirectory(successCallback, errorCallback, [joined, DIR_SEPARATOR, {create: false}]);
return;
}
exports.getDirectory(successCallback, errorCallback, [path, parentName, {create: false}]);
};
exports.copyTo = function(successCallback, errorCallback, args) {
var srcPath = args[0];
var parentFullPath = args[1];
var name = args[2];
if (name.indexOf('/') !== -1 || srcPath === parentFullPath + name) {
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
return;
}
// Read src file
exports.getFile(function(srcFileEntry) {
var path = resolveToFullPath_(parentFullPath);
//Check directory
exports.getDirectory(function() {
// Create dest file
exports.getFile(function(dstFileEntry) {
exports.write(function() {
successCallback(dstFileEntry);
}, errorCallback, [dstFileEntry.file_.storagePath, srcFileEntry.file_.blob_, 0]);
}, errorCallback, [parentFullPath, name, {create: true}]);
}, function() { if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); }},
[path.storagePath, null, {create:false}]);
}, errorCallback, [srcPath, null]);
};
exports.moveTo = function(successCallback, errorCallback, args) {
var srcPath = args[0];
// parentFullPath and name parameters is ignored because
// args is being passed downstream to exports.copyTo method
var parentFullPath = args[1]; // jshint ignore: line
var name = args[2]; // jshint ignore: line
exports.copyTo(function (fileEntry) {
exports.remove(function () {
successCallback(fileEntry);
}, errorCallback, [srcPath]);
}, errorCallback, args);
};
exports.resolveLocalFileSystemURI = function(successCallback, errorCallback, args) {
var path = args[0];
// Ignore parameters
if (path.indexOf('?') !== -1) {
path = String(path).split("?")[0];
}
// support for encodeURI
if (/\%5/g.test(path) || /\%20/g.test(path)) {
path = decodeURI(path);
}
if (path.trim()[0] === '/') {
errorCallback && errorCallback(FileError.ENCODING_ERR);
return;
}
//support for cdvfile
if (path.trim().substr(0,7) === "cdvfile") {
if (path.indexOf("cdvfile://localhost") === -1) {
errorCallback && errorCallback(FileError.ENCODING_ERR);
return;
}
var indexPersistent = path.indexOf("persistent");
var indexTemporary = path.indexOf("temporary");
//cdvfile://localhost/persistent/path/to/file
if (indexPersistent !== -1) {
path = "file:///persistent" + path.substr(indexPersistent + 10);
} else if (indexTemporary !== -1) {
path = "file:///temporary" + path.substr(indexTemporary + 9);
} else {
errorCallback && errorCallback(FileError.ENCODING_ERR);
return;
}
}
// to avoid path form of '///path/to/file'
function handlePathSlashes(path) {
var cutIndex = 0;
for (var i = 0; i < path.length - 1; i++) {
if (path[i] === DIR_SEPARATOR && path[i + 1] === DIR_SEPARATOR) {
cutIndex = i + 1;
} else break;
}
return path.substr(cutIndex);
}
// Handle localhost containing paths (see specs )
if (path.indexOf('file://localhost/') === 0) {
path = path.replace('file://localhost/', 'file:///');
}
if (path.indexOf(pathsPrefix.dataDirectory) === 0) {
path = path.substring(pathsPrefix.dataDirectory.length - 1);
path = handlePathSlashes(path);
exports.requestFileSystem(function() {
exports.getFile(successCallback, function() {
exports.getDirectory(successCallback, errorCallback, [pathsPrefix.dataDirectory, path,
{create: false}]);
}, [pathsPrefix.dataDirectory, path, {create: false}]);
}, errorCallback, [LocalFileSystem.PERSISTENT]);
} else if (path.indexOf(pathsPrefix.cacheDirectory) === 0) {
path = path.substring(pathsPrefix.cacheDirectory.length - 1);
path = handlePathSlashes(path);
exports.requestFileSystem(function() {
exports.getFile(successCallback, function() {
exports.getDirectory(successCallback, errorCallback, [pathsPrefix.cacheDirectory, path,
{create: false}]);
}, [pathsPrefix.cacheDirectory, path, {create: false}]);
}, errorCallback, [LocalFileSystem.TEMPORARY]);
} else if (path.indexOf(pathsPrefix.applicationDirectory) === 0) {
path = path.substring(pathsPrefix.applicationDirectory.length);
//TODO: need to cut out redundant slashes?
var xhr = new XMLHttpRequest();
xhr.open("GET", path, true);
xhr.onreadystatechange = function () {
if (xhr.status === 200 && xhr.readyState === 4) {
exports.requestFileSystem(function(fs) {
fs.name = location.hostname;
//TODO: need to call exports.getFile(...) to handle errors correct
fs.root.getFile(path, {create: true}, writeFile, errorCallback);
}, errorCallback, [LocalFileSystem.PERSISTENT]);
}
};
xhr.onerror = function () {
errorCallback && errorCallback(FileError.NOT_READABLE_ERR);
};
xhr.send();
} else {
errorCallback && errorCallback(FileError.NOT_FOUND_ERR);
}
function writeFile(entry) {
entry.createWriter(function (fileWriter) {
fileWriter.onwriteend = function (evt) {
if (!evt.target.error) {
entry.filesystemName = location.hostname;
successCallback(entry);
}
};
fileWriter.onerror = function () {
errorCallback && errorCallback(FileError.NOT_READABLE_ERR);
};
fileWriter.write(new Blob([xhr.response]));
}, errorCallback);
}
};
exports.requestAllPaths = function(successCallback) {
successCallback(pathsPrefix);
};
/*** Helpers ***/
/**
* Interface to wrap the native File interface.
*
* This interface is necessary for creating zero-length (empty) files,
* something the Filesystem API allows you to do. Unfortunately, File's
* constructor cannot be called directly, making it impossible to instantiate
* an empty File in JS.
*
* @param {Object} opts Initial values.
* @constructor
*/
function MyFile(opts) {
var blob_ = new Blob();
this.size = opts.size || 0;
this.name = opts.name || '';
this.type = opts.type || '';
this.lastModifiedDate = opts.lastModifiedDate || null;
this.storagePath = opts.storagePath || '';
// Need some black magic to correct the object's size/name/type based on the
// blob that is saved.
Object.defineProperty(this, 'blob_', {
enumerable: true,
get: function() {
return blob_;
},
set: function(val) {
blob_ = val;
this.size = blob_.size;
this.name = blob_.name;
this.type = blob_.type;
this.lastModifiedDate = blob_.lastModifiedDate;
}.bind(this)
});
}
MyFile.prototype.constructor = MyFile;
// When saving an entry, the fullPath should always lead with a slash and never
// end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute
// one. This method ensures path is legit!
function resolveToFullPath_(cwdFullPath, path) {
path = path || '';
var fullPath = path;
var prefix = '';
cwdFullPath = cwdFullPath || DIR_SEPARATOR;
if (cwdFullPath.indexOf(FILESYSTEM_PREFIX) === 0) {
prefix = cwdFullPath.substring(0, cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
cwdFullPath = cwdFullPath.substring(cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
}
var relativePath = path[0] !== DIR_SEPARATOR;
if (relativePath) {
fullPath = cwdFullPath;
if (cwdFullPath !== DIR_SEPARATOR) {
fullPath += DIR_SEPARATOR + path;
} else {
fullPath += path;
}
}
// Remove doubled separator substrings
var re = new RegExp(DIR_SEPARATOR + DIR_SEPARATOR, 'g');
fullPath = fullPath.replace(re, DIR_SEPARATOR);
// Adjust '..'s by removing parent directories when '..' flows in path.
var parts = fullPath.split(DIR_SEPARATOR);
for (var i = 0; i < parts.length; ++i) {
var part = parts[i];
if (part === '..') {
parts[i - 1] = '';
parts[i] = '';
}
}
fullPath = parts.filter(function(el) {
return el;
}).join(DIR_SEPARATOR);
// Add back in leading slash.
if (fullPath[0] !== DIR_SEPARATOR) {
fullPath = DIR_SEPARATOR + fullPath;
}
// Replace './' by current dir. ('./one/./two' -> one/two)
fullPath = fullPath.replace(/\.\//g, DIR_SEPARATOR);
// Replace '//' with '/'.
fullPath = fullPath.replace(/\/\//g, DIR_SEPARATOR);
// Replace '/.' with '/'.
fullPath = fullPath.replace(/\/\./g, DIR_SEPARATOR);
// Remove '/' if it appears on the end.
if (fullPath[fullPath.length - 1] === DIR_SEPARATOR &&
fullPath !== DIR_SEPARATOR) {
fullPath = fullPath.substring(0, fullPath.length - 1);
}
var storagePath = prefix + fullPath;
storagePath = decodeURI(storagePath);
fullPath = decodeURI(fullPath);
return {
storagePath: storagePath,
fullPath: fullPath,
fileName: fullPath.split(DIR_SEPARATOR).pop(),
fsName: prefix.split(DIR_SEPARATOR).pop()
};
}
function fileEntryFromIdbEntry(fileEntry) {
// IDB won't save methods, so we need re-create the FileEntry.
var clonedFileEntry = new FileEntry(fileEntry.name, fileEntry.fullPath, fileEntry.filesystem);
clonedFileEntry.file_ = fileEntry.file_;
return clonedFileEntry;
}
function readAs(what, fullPath, encoding, startPos, endPos, successCallback, errorCallback) {
exports.getFile(function(fileEntry) {
var fileReader = new FileReader(),
blob = fileEntry.file_.blob_.slice(startPos, endPos);
fileReader.onload = function(e) {
successCallback(e.target.result);
};
fileReader.onerror = errorCallback;
switch (what) {
case 'text':
fileReader.readAsText(blob, encoding);
break;
case 'dataURL':
fileReader.readAsDataURL(blob);
break;
case 'arrayBuffer':
fileReader.readAsArrayBuffer(blob);
break;
case 'binaryString':
fileReader.readAsBinaryString(blob);
break;
}
}, errorCallback, [fullPath, null]);
}
/*** Core logic to handle IDB operations ***/
idb_.open = function(dbName, successCallback, errorCallback) {
var self = this;
// TODO: FF 12.0a1 isn't liking a db name with : in it.
var request = indexedDB.open(dbName.replace(':', '_')/*, 1 /*version*/);
request.onerror = errorCallback || onError;
request.onupgradeneeded = function(e) {
// First open was called or higher db version was used.
// console.log('onupgradeneeded: oldVersion:' + e.oldVersion,
// 'newVersion:' + e.newVersion);
self.db = e.target.result;
self.db.onerror = onError;
if (!self.db.objectStoreNames.contains(FILE_STORE_)) {
self.db.createObjectStore(FILE_STORE_/*,{keyPath: 'id', autoIncrement: true}*/);
}
};
request.onsuccess = function(e) {
self.db = e.target.result;
self.db.onerror = onError;
successCallback(e);
};
request.onblocked = errorCallback || onError;
};
idb_.close = function() {
this.db.close();
this.db = null;
};
idb_.get = function(fullPath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readonly');
var request = tx.objectStore(FILE_STORE_).get(fullPath);
tx.onabort = errorCallback || onError;
tx.oncomplete = function() {
successCallback(request.result);
};
};
idb_.getAllEntries = function(fullPath, storagePath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var results = [];
if (storagePath[storagePath.length - 1] === DIR_SEPARATOR) {
storagePath = storagePath.substring(0, storagePath.length - 1);
}
var range = IDBKeyRange.bound(storagePath + DIR_SEPARATOR + ' ',
storagePath + DIR_SEPARATOR + String.fromCharCode(unicodeLastChar));
var tx = this.db.transaction([FILE_STORE_], 'readonly');
tx.onabort = errorCallback || onError;
tx.oncomplete = function() {
results = results.filter(function(val) {
var pathWithoutSlash = val.fullPath;
if (val.fullPath[val.fullPath.length - 1] === DIR_SEPARATOR) {
pathWithoutSlash = pathWithoutSlash.substr(0, pathWithoutSlash.length - 1);
}
var valPartsLen = pathWithoutSlash.split(DIR_SEPARATOR).length;
var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length;
/* Input fullPath parameter equals '//' for root folder */
/* Entries in root folder has valPartsLen equals 2 (see below) */
if (fullPath[fullPath.length -1] === DIR_SEPARATOR && fullPath.trim().length === 2) {
fullPathPartsLen = 1;
} else if (fullPath[fullPath.length -1] === DIR_SEPARATOR) {
fullPathPartsLen = fullPath.substr(0, fullPath.length - 1).split(DIR_SEPARATOR).length;
} else {
fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length;
}
if (valPartsLen === fullPathPartsLen + 1) {
// If this a subfolder and entry is a direct child, include it in
// the results. Otherwise, it's not an entry of this folder.
return val;
} else return false;
});
successCallback(results);
};
var request = tx.objectStore(FILE_STORE_).openCursor(range);
request.onsuccess = function(e) {
var cursor = e.target.result;
if (cursor) {
var val = cursor.value;
results.push(val.isFile ? fileEntryFromIdbEntry(val) : new DirectoryEntry(val.name, val.fullPath, val.filesystem));
cursor['continue']();
}
};
};
idb_['delete'] = function(fullPath, successCallback, errorCallback, isDirectory) {
if (!idb_.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readwrite');
tx.oncomplete = successCallback;
tx.onabort = errorCallback || onError;
tx.oncomplete = function() {
if (isDirectory) {
//We delete nested files and folders after deleting parent folder
//We use ranges: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange
fullPath = fullPath + DIR_SEPARATOR;
//Range contains all entries in the form fullPath<symbol> where
//symbol in the range from ' ' to symbol which has code `unicodeLastChar`
var range = IDBKeyRange.bound(fullPath + ' ', fullPath + String.fromCharCode(unicodeLastChar));
var newTx = this.db.transaction([FILE_STORE_], 'readwrite');
newTx.oncomplete = successCallback;
newTx.onabort = errorCallback || onError;
newTx.objectStore(FILE_STORE_)['delete'](range);
} else {
successCallback();
}
};
tx.objectStore(FILE_STORE_)['delete'](fullPath);
};
idb_.put = function(entry, storagePath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readwrite');
tx.onabort = errorCallback || onError;
tx.oncomplete = function() {
// TODO: Error is thrown if we pass the request event back instead.
successCallback(entry);
};
tx.objectStore(FILE_STORE_).put(entry, storagePath);
};
// Global error handler. Errors bubble from request, to transaction, to db.
function onError(e) {
switch (e.target.errorCode) {
case 12:
console.log('Error - Attempt to open db with a lower version than the ' +
'current one.');
break;
default:
console.log('errorCode: ' + e.target.errorCode);
}
console.log(e, e.code, e.message);
}
})(module.exports, window);
require("cordova/exec/proxy").add("File", module.exports);

View File

@@ -0,0 +1,785 @@
/*
*
* 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 LocalFileSystem = require('./LocalFileSystem'),
FileSystem = require('./FileSystem'),
FileEntry = require('./FileEntry'),
FileError = require('./FileError'),
DirectoryEntry = require('./DirectoryEntry'),
File = require('./File');
/*
QUIRKS:
Does not fail when removing non-empty directories
Does not support metadata for directories
Does not support requestAllFileSystems
Does not support resolveLocalFileSystemURI
Methods copyTo and moveTo do not support directories
Heavily based on https://github.com/ebidel/idb.filesystem.js
*/
(function(exports, global) {
var indexedDB = global.indexedDB || global.mozIndexedDB;
if (!indexedDB) {
throw "Firefox OS File plugin: indexedDB not supported";
}
var fs_ = null;
var idb_ = {};
idb_.db = null;
var FILE_STORE_ = 'entries';
var DIR_SEPARATOR = '/';
var DIR_OPEN_BOUND = String.fromCharCode(DIR_SEPARATOR.charCodeAt(0) + 1);
var pathsPrefix = {
// Read-only directory where the application is installed.
applicationDirectory: location.origin + "/",
// Where to put app-specific data files.
dataDirectory: 'file:///persistent/',
// Cached files that should survive app restarts.
// Apps should not rely on the OS to delete files in here.
cacheDirectory: 'file:///temporary/',
};
/*** Exported functionality ***/
exports.requestFileSystem = function(successCallback, errorCallback, args) {
var type = args[0];
var size = args[1];
if (type !== LocalFileSystem.TEMPORARY && type !== LocalFileSystem.PERSISTENT) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var name = type === LocalFileSystem.TEMPORARY ? 'temporary' : 'persistent';
var storageName = (location.protocol + location.host).replace(/:/g, '_');
var root = new DirectoryEntry('', DIR_SEPARATOR);
fs_ = new FileSystem(name, root);
idb_.open(storageName, function() {
successCallback(fs_);
}, errorCallback);
};
require('./fileSystems').getFs = function(name, callback) {
callback(new FileSystem(name, fs_.root));
};
// list a directory's contents (files and folders).
exports.readEntries = function(successCallback, errorCallback, args) {
var fullPath = args[0];
if (!successCallback) {
throw Error('Expected successCallback argument.');
}
var path = resolveToFullPath_(fullPath);
idb_.getAllEntries(path.fullPath, path.storagePath, function(entries) {
successCallback(entries);
}, errorCallback);
};
exports.getFile = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var path = args[1];
var options = args[2] || {};
// Create an absolute path if we were handed a relative one.
path = resolveToFullPath_(fullPath, path);
idb_.get(path.storagePath, function(fileEntry) {
if (options.create === true && options.exclusive === true && fileEntry) {
// If create and exclusive are both true, and the path already exists,
// getFile must fail.
if (errorCallback) {
errorCallback(FileError.PATH_EXISTS_ERR);
}
} else if (options.create === true && !fileEntry) {
// If create is true, the path doesn't exist, and no other error occurs,
// getFile must create it as a zero-length file and return a corresponding
// FileEntry.
var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
newFileEntry.file_ = new MyFile({
size: 0,
name: newFileEntry.name,
lastModifiedDate: new Date(),
storagePath: path.storagePath
});
idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
} else if (options.create === true && fileEntry) {
if (fileEntry.isFile) {
// Overwrite file, delete then create new.
idb_['delete'](path.storagePath, function() {
var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
newFileEntry.file_ = new MyFile({
size: 0,
name: newFileEntry.name,
lastModifiedDate: new Date(),
storagePath: path.storagePath
});
idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
}, errorCallback);
} else {
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
}
} else if ((!options.create || options.create === false) && !fileEntry) {
// If create is not true and the path doesn't exist, getFile must fail.
if (errorCallback) {
errorCallback(FileError.NOT_FOUND_ERR);
}
} else if ((!options.create || options.create === false) && fileEntry &&
fileEntry.isDirectory) {
// If create is not true and the path exists, but is a directory, getFile
// must fail.
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
} else {
// Otherwise, if no other error occurs, getFile must return a FileEntry
// corresponding to path.
successCallback(fileEntryFromIdbEntry(fileEntry));
}
}, errorCallback);
};
exports.getFileMetadata = function(successCallback, errorCallback, args) {
var fullPath = args[0];
exports.getFile(function(fileEntry) {
successCallback(new File(fileEntry.file_.name, fileEntry.fullPath, '', fileEntry.file_.lastModifiedDate,
fileEntry.file_.size));
}, errorCallback, [fullPath, null]);
};
exports.getMetadata = function(successCallback, errorCallback, args) {
exports.getFile(function (fileEntry) {
successCallback(
{
modificationTime: fileEntry.file_.lastModifiedDate,
size: fileEntry.file_.lastModifiedDate
});
}, errorCallback, args);
};
exports.setMetadata = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var metadataObject = args[1];
exports.getFile(function (fileEntry) {
fileEntry.file_.lastModifiedDate = metadataObject.modificationTime;
}, errorCallback, [fullPath, null]);
};
exports.write = function(successCallback, errorCallback, args) {
var fileName = args[0],
data = args[1],
position = args[2],
isBinary = args[3];
if (!data) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
exports.getFile(function(fileEntry) {
var blob_ = fileEntry.file_.blob_;
if (!blob_) {
blob_ = new Blob([data], {type: data.type});
} else {
// Calc the head and tail fragments
var head = blob_.slice(0, position);
var tail = blob_.slice(position + data.byteLength);
// Calc the padding
var padding = position - head.size;
if (padding < 0) {
padding = 0;
}
// Do the "write". In fact, a full overwrite of the Blob.
blob_ = new Blob([head, new Uint8Array(padding), data, tail],
{type: data.type});
}
// Set the blob we're writing on this file entry so we can recall it later.
fileEntry.file_.blob_ = blob_;
fileEntry.file_.lastModifiedDate = data.lastModifiedDate || null;
fileEntry.file_.size = blob_.size;
fileEntry.file_.name = blob_.name;
fileEntry.file_.type = blob_.type;
idb_.put(fileEntry, fileEntry.file_.storagePath, function() {
successCallback(data.byteLength);
}, errorCallback);
}, errorCallback, [fileName, null]);
};
exports.readAsText = function(successCallback, errorCallback, args) {
var fileName = args[0],
enc = args[1],
startPos = args[2],
endPos = args[3];
readAs('text', fileName, enc, startPos, endPos, successCallback, errorCallback);
};
exports.readAsDataURL = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('dataURL', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.readAsBinaryString = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('binaryString', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.readAsArrayBuffer = function(successCallback, errorCallback, args) {
var fileName = args[0],
startPos = args[1],
endPos = args[2];
readAs('arrayBuffer', fileName, null, startPos, endPos, successCallback, errorCallback);
};
exports.removeRecursively = exports.remove = function(successCallback, errorCallback, args) {
var fullPath = args[0];
// TODO: This doesn't protect against directories that have content in it.
// Should throw an error instead if the dirEntry is not empty.
idb_['delete'](fullPath, function() {
successCallback();
}, errorCallback);
};
exports.getDirectory = function(successCallback, errorCallback, args) {
var fullPath = args[0];
var path = args[1];
var options = args[2];
// Create an absolute path if we were handed a relative one.
path = resolveToFullPath_(fullPath, path);
idb_.get(path.storagePath, function(folderEntry) {
if (!options) {
options = {};
}
if (options.create === true && options.exclusive === true && folderEntry) {
// If create and exclusive are both true, and the path already exists,
// getDirectory must fail.
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
} else if (options.create === true && !folderEntry) {
// If create is true, the path doesn't exist, and no other error occurs,
// getDirectory must create it as a zero-length file and return a corresponding
// MyDirectoryEntry.
var dirEntry = new DirectoryEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));
idb_.put(dirEntry, path.storagePath, successCallback, errorCallback);
} else if (options.create === true && folderEntry) {
if (folderEntry.isDirectory) {
// IDB won't save methods, so we need re-create the MyDirectoryEntry.
successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.fileSystem));
} else {
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
}
} else if ((!options.create || options.create === false) && !folderEntry) {
// Handle root special. It should always exist.
if (path.fullPath === DIR_SEPARATOR) {
successCallback(fs_.root);
return;
}
// If create is not true and the path doesn't exist, getDirectory must fail.
if (errorCallback) {
errorCallback(FileError.NOT_FOUND_ERR);
}
} else if ((!options.create || options.create === false) && folderEntry &&
folderEntry.isFile) {
// If create is not true and the path exists, but is a file, getDirectory
// must fail.
if (errorCallback) {
errorCallback(FileError.INVALID_MODIFICATION_ERR);
}
} else {
// Otherwise, if no other error occurs, getDirectory must return a
// MyDirectoryEntry corresponding to path.
// IDB won't' save methods, so we need re-create MyDirectoryEntry.
successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.fileSystem));
}
}, errorCallback);
};
exports.getParent = function(successCallback, errorCallback, args) {
var fullPath = args[0];
if (fullPath === DIR_SEPARATOR) {
successCallback(fs_.root);
return;
}
var pathArr = fullPath.split(DIR_SEPARATOR);
pathArr.pop();
var namesa = pathArr.pop();
var path = pathArr.join(DIR_SEPARATOR);
exports.getDirectory(successCallback, errorCallback, [path, namesa, {create: false}]);
};
exports.copyTo = function(successCallback, errorCallback, args) {
var srcPath = args[0];
var parentFullPath = args[1];
var name = args[2];
// Read src file
exports.getFile(function(srcFileEntry) {
// Create dest file
exports.getFile(function(dstFileEntry) {
exports.write(function() {
successCallback(dstFileEntry);
}, errorCallback, [dstFileEntry.file_.storagePath, srcFileEntry.file_.blob_, 0]);
}, errorCallback, [parentFullPath, name, {create: true}]);
}, errorCallback, [srcPath, null]);
};
exports.moveTo = function(successCallback, errorCallback, args) {
var srcPath = args[0];
var parentFullPath = args[1];
var name = args[2];
exports.copyTo(function (fileEntry) {
exports.remove(function () {
successCallback(fileEntry);
}, errorCallback, [srcPath]);
}, errorCallback, args);
};
exports.resolveLocalFileSystemURI = function(successCallback, errorCallback, args) {
var path = args[0];
// Ignore parameters
if (path.indexOf('?') !== -1) {
path = String(path).split("?")[0];
}
// support for encodeURI
if (/\%5/g.test(path)) {
path = decodeURI(path);
}
if (path.indexOf(pathsPrefix.dataDirectory) === 0) {
path = path.substring(pathsPrefix.dataDirectory.length - 1);
exports.requestFileSystem(function(fs) {
fs.root.getFile(path, {create: false}, successCallback, function() {
fs.root.getDirectory(path, {create: false}, successCallback, errorCallback);
});
}, errorCallback, [LocalFileSystem.PERSISTENT]);
} else if (path.indexOf(pathsPrefix.cacheDirectory) === 0) {
path = path.substring(pathsPrefix.cacheDirectory.length - 1);
exports.requestFileSystem(function(fs) {
fs.root.getFile(path, {create: false}, successCallback, function() {
fs.root.getDirectory(path, {create: false}, successCallback, errorCallback);
});
}, errorCallback, [LocalFileSystem.TEMPORARY]);
} else if (path.indexOf(pathsPrefix.applicationDirectory) === 0) {
path = path.substring(pathsPrefix.applicationDirectory.length);
var xhr = new XMLHttpRequest();
xhr.open("GET", path, true);
xhr.onreadystatechange = function () {
if (xhr.status === 200 && xhr.readyState === 4) {
exports.requestFileSystem(function(fs) {
fs.name = location.hostname;
fs.root.getFile(path, {create: true}, writeFile, errorCallback);
}, errorCallback, [LocalFileSystem.PERSISTENT]);
}
};
xhr.onerror = function () {
errorCallback && errorCallback(FileError.NOT_READABLE_ERR);
};
xhr.send();
} else {
errorCallback && errorCallback(FileError.NOT_FOUND_ERR);
}
function writeFile(entry) {
entry.createWriter(function (fileWriter) {
fileWriter.onwriteend = function (evt) {
if (!evt.target.error) {
entry.filesystemName = location.hostname;
successCallback(entry);
}
};
fileWriter.onerror = function () {
errorCallback && errorCallback(FileError.NOT_READABLE_ERR);
};
fileWriter.write(new Blob([xhr.response]));
}, errorCallback);
}
};
exports.requestAllPaths = function(successCallback) {
successCallback(pathsPrefix);
};
/*** Helpers ***/
/**
* Interface to wrap the native File interface.
*
* This interface is necessary for creating zero-length (empty) files,
* something the Filesystem API allows you to do. Unfortunately, File's
* constructor cannot be called directly, making it impossible to instantiate
* an empty File in JS.
*
* @param {Object} opts Initial values.
* @constructor
*/
function MyFile(opts) {
var blob_ = new Blob();
this.size = opts.size || 0;
this.name = opts.name || '';
this.type = opts.type || '';
this.lastModifiedDate = opts.lastModifiedDate || null;
this.storagePath = opts.storagePath || '';
// Need some black magic to correct the object's size/name/type based on the
// blob that is saved.
Object.defineProperty(this, 'blob_', {
enumerable: true,
get: function() {
return blob_;
},
set: function(val) {
blob_ = val;
this.size = blob_.size;
this.name = blob_.name;
this.type = blob_.type;
this.lastModifiedDate = blob_.lastModifiedDate;
}.bind(this)
});
}
MyFile.prototype.constructor = MyFile;
// When saving an entry, the fullPath should always lead with a slash and never
// end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute
// one. This method ensures path is legit!
function resolveToFullPath_(cwdFullPath, path) {
path = path || '';
var fullPath = path;
var prefix = '';
cwdFullPath = cwdFullPath || DIR_SEPARATOR;
if (cwdFullPath.indexOf(FILESYSTEM_PREFIX) === 0) {
prefix = cwdFullPath.substring(0, cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
cwdFullPath = cwdFullPath.substring(cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
}
var relativePath = path[0] !== DIR_SEPARATOR;
if (relativePath) {
fullPath = cwdFullPath;
if (cwdFullPath != DIR_SEPARATOR) {
fullPath += DIR_SEPARATOR + path;
} else {
fullPath += path;
}
}
// Adjust '..'s by removing parent directories when '..' flows in path.
var parts = fullPath.split(DIR_SEPARATOR);
for (var i = 0; i < parts.length; ++i) {
var part = parts[i];
if (part == '..') {
parts[i - 1] = '';
parts[i] = '';
}
}
fullPath = parts.filter(function(el) {
return el;
}).join(DIR_SEPARATOR);
// Add back in leading slash.
if (fullPath[0] !== DIR_SEPARATOR) {
fullPath = DIR_SEPARATOR + fullPath;
}
// Replace './' by current dir. ('./one/./two' -> one/two)
fullPath = fullPath.replace(/\.\//g, DIR_SEPARATOR);
// Replace '//' with '/'.
fullPath = fullPath.replace(/\/\//g, DIR_SEPARATOR);
// Replace '/.' with '/'.
fullPath = fullPath.replace(/\/\./g, DIR_SEPARATOR);
// Remove '/' if it appears on the end.
if (fullPath[fullPath.length - 1] == DIR_SEPARATOR &&
fullPath != DIR_SEPARATOR) {
fullPath = fullPath.substring(0, fullPath.length - 1);
}
return {
storagePath: prefix + fullPath,
fullPath: fullPath,
fileName: fullPath.split(DIR_SEPARATOR).pop(),
fsName: prefix.split(DIR_SEPARATOR).pop()
};
}
function fileEntryFromIdbEntry(fileEntry) {
// IDB won't save methods, so we need re-create the FileEntry.
var clonedFileEntry = new FileEntry(fileEntry.name, fileEntry.fullPath, fileEntry.fileSystem);
clonedFileEntry.file_ = fileEntry.file_;
return clonedFileEntry;
}
function readAs(what, fullPath, encoding, startPos, endPos, successCallback, errorCallback) {
exports.getFile(function(fileEntry) {
var fileReader = new FileReader(),
blob = fileEntry.file_.blob_.slice(startPos, endPos);
fileReader.onload = function(e) {
successCallback(e.target.result);
};
fileReader.onerror = errorCallback;
switch (what) {
case 'text':
fileReader.readAsText(blob, encoding);
break;
case 'dataURL':
fileReader.readAsDataURL(blob);
break;
case 'arrayBuffer':
fileReader.readAsArrayBuffer(blob);
break;
case 'binaryString':
fileReader.readAsBinaryString(blob);
break;
}
}, errorCallback, [fullPath, null]);
}
/*** Core logic to handle IDB operations ***/
idb_.open = function(dbName, successCallback, errorCallback) {
var self = this;
// TODO: FF 12.0a1 isn't liking a db name with : in it.
var request = indexedDB.open(dbName.replace(':', '_')/*, 1 /*version*/);
request.onerror = errorCallback || onError;
request.onupgradeneeded = function(e) {
// First open was called or higher db version was used.
// console.log('onupgradeneeded: oldVersion:' + e.oldVersion,
// 'newVersion:' + e.newVersion);
self.db = e.target.result;
self.db.onerror = onError;
if (!self.db.objectStoreNames.contains(FILE_STORE_)) {
var store = self.db.createObjectStore(FILE_STORE_/*,{keyPath: 'id', autoIncrement: true}*/);
}
};
request.onsuccess = function(e) {
self.db = e.target.result;
self.db.onerror = onError;
successCallback(e);
};
request.onblocked = errorCallback || onError;
};
idb_.close = function() {
this.db.close();
this.db = null;
};
idb_.get = function(fullPath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readonly');
//var request = tx.objectStore(FILE_STORE_).get(fullPath);
var range = IDBKeyRange.bound(fullPath, fullPath + DIR_OPEN_BOUND,
false, true);
var request = tx.objectStore(FILE_STORE_).get(range);
tx.onabort = errorCallback || onError;
tx.oncomplete = function(e) {
successCallback(request.result);
};
};
idb_.getAllEntries = function(fullPath, storagePath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var results = [];
if (storagePath[storagePath.length - 1] === DIR_SEPARATOR) {
storagePath = storagePath.substring(0, storagePath.length - 1);
}
range = IDBKeyRange.bound(
storagePath + DIR_SEPARATOR, storagePath + DIR_OPEN_BOUND, false, true);
var tx = this.db.transaction([FILE_STORE_], 'readonly');
tx.onabort = errorCallback || onError;
tx.oncomplete = function(e) {
results = results.filter(function(val) {
var valPartsLen = val.fullPath.split(DIR_SEPARATOR).length;
var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length;
if (fullPath === DIR_SEPARATOR && valPartsLen < fullPathPartsLen + 1) {
// Hack to filter out entries in the root folder. This is inefficient
// because reading the entires of fs.root (e.g. '/') returns ALL
// results in the database, then filters out the entries not in '/'.
return val;
} else if (fullPath !== DIR_SEPARATOR &&
valPartsLen === fullPathPartsLen + 1) {
// If this a subfolder and entry is a direct child, include it in
// the results. Otherwise, it's not an entry of this folder.
return val;
}
});
successCallback(results);
};
var request = tx.objectStore(FILE_STORE_).openCursor(range);
request.onsuccess = function(e) {
var cursor = e.target.result;
if (cursor) {
var val = cursor.value;
results.push(val.isFile ? fileEntryFromIdbEntry(val) : new DirectoryEntry(val.name, val.fullPath, val.fileSystem));
cursor['continue']();
}
};
};
idb_['delete'] = function(fullPath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readwrite');
tx.oncomplete = successCallback;
tx.onabort = errorCallback || onError;
//var request = tx.objectStore(FILE_STORE_).delete(fullPath);
var range = IDBKeyRange.bound(
fullPath, fullPath + DIR_OPEN_BOUND, false, true);
tx.objectStore(FILE_STORE_)['delete'](range);
};
idb_.put = function(entry, storagePath, successCallback, errorCallback) {
if (!this.db) {
errorCallback && errorCallback(FileError.INVALID_MODIFICATION_ERR);
return;
}
var tx = this.db.transaction([FILE_STORE_], 'readwrite');
tx.onabort = errorCallback || onError;
tx.oncomplete = function(e) {
// TODO: Error is thrown if we pass the request event back instead.
successCallback(entry);
};
tx.objectStore(FILE_STORE_).put(entry, storagePath);
};
// Global error handler. Errors bubble from request, to transaction, to db.
function onError(e) {
switch (e.target.errorCode) {
case 12:
console.log('Error - Attempt to open db with a lower version than the ' +
'current one.');
break;
default:
console.log('errorCode: ' + e.target.errorCode);
}
console.log(e, e.code, e.message);
}
// Clean up.
// TODO: Is there a place for this?
// global.addEventListener('beforeunload', function(e) {
// idb_.db && idb_.db.close();
// }, false);
})(module.exports, window);
require("cordova/exec/proxy").add("File", module.exports);

View File

@@ -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.
*/
#import "CDVFile.h"
extern NSString* const kCDVAssetsLibraryPrefix;
extern NSString* const kCDVAssetsLibraryScheme;
@interface CDVAssetLibraryFilesystem : NSObject<CDVFileSystem> {
}
- (id) initWithName:(NSString *)name;
@end

View File

@@ -0,0 +1,253 @@
/*
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 "CDVAssetLibraryFilesystem.h"
#import <Cordova/CDV.h>
#import <AssetsLibrary/ALAsset.h>
#import <AssetsLibrary/ALAssetRepresentation.h>
#import <AssetsLibrary/ALAssetsLibrary.h>
#import <MobileCoreServices/MobileCoreServices.h>
NSString* const kCDVAssetsLibraryPrefix = @"assets-library://";
NSString* const kCDVAssetsLibraryScheme = @"assets-library";
@implementation CDVAssetLibraryFilesystem
@synthesize name=_name, urlTransformer;
/*
The CDVAssetLibraryFilesystem works with resources which are identified
by iOS as
asset-library://<path>
and represents them internally as URLs of the form
cdvfile://localhost/assets-library/<path>
*/
- (NSURL *)assetLibraryURLForLocalURL:(CDVFilesystemURL *)url
{
if ([url.url.scheme isEqualToString:kCDVFilesystemURLPrefix]) {
NSString *path = [[url.url absoluteString] substringFromIndex:[@"cdvfile://localhost/assets-library" length]];
return [NSURL URLWithString:[NSString stringWithFormat:@"assets-library:/%@", path]];
}
return url.url;
}
- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url
{
NSDictionary* entry = [self makeEntryForLocalURL:url];
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:entry];
}
- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url {
return [self makeEntryForPath:url.fullPath isDirectory:NO];
}
- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir
{
NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:5];
NSString* lastPart = [fullPath lastPathComponent];
if (isDir && ![fullPath hasSuffix:@"/"]) {
fullPath = [fullPath stringByAppendingString:@"/"];
}
[dirEntry setObject:[NSNumber numberWithBool:!isDir] forKey:@"isFile"];
[dirEntry setObject:[NSNumber numberWithBool:isDir] forKey:@"isDirectory"];
[dirEntry setObject:fullPath forKey:@"fullPath"];
[dirEntry setObject:lastPart forKey:@"name"];
[dirEntry setObject:self.name forKey: @"filesystemName"];
NSURL* nativeURL = [NSURL URLWithString:[NSString stringWithFormat:@"assets-library:/%@",fullPath]];
if (self.urlTransformer) {
nativeURL = self.urlTransformer(nativeURL);
}
dirEntry[@"nativeURL"] = [nativeURL absoluteString];
return dirEntry;
}
/* 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";
} else if ([[fullPath pathExtension] rangeOfString:@"css"].location != NSNotFound) {
mimeType = @"text/css";
}
}
CFRelease(typeId);
}
}
return mimeType;
}
- (id)initWithName:(NSString *)name
{
if (self) {
_name = name;
}
return self;
}
- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options
{
// return unsupported result for assets-library URLs
return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"getFile not supported for assets-library URLs."];
}
- (CDVPluginResult*)getParentForURL:(CDVFilesystemURL *)localURI
{
// we don't (yet?) support getting the parent of an asset
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_READABLE_ERR];
}
- (CDVPluginResult*)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options
{
// setMetadata doesn't make sense for asset library files
return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR];
}
- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI
{
// return error for assets-library URLs
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
}
- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI
{
// return error for assets-library URLs
return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"removeRecursively not supported for assets-library URLs."];
}
- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI
{
// return unsupported result for assets-library URLs
return [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"readEntries not supported for assets-library URLs."];
}
- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos
{
// assets-library files can't be truncated
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
}
- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend
{
// text can't be written into assets-library files
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NO_MODIFICATION_ALLOWED_ERR];
}
- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback
{
// Copying to an assets library file is not doable, since we can't write it.
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
callback(result);
}
- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)url
{
NSString *path = nil;
if ([[url.url scheme] isEqualToString:kCDVAssetsLibraryScheme]) {
path = [url.url path];
} else {
path = url.fullPath;
}
if ([path hasSuffix:@"/"]) {
path = [path substringToIndex:([path length]-1)];
}
return path;
}
- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback
{
ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) {
if (asset) {
// We have the asset! Get the data and send it off.
ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation];
NSUInteger size = (end > start) ? (end - start) : [assetRepresentation size];
Byte* buffer = (Byte*)malloc(size);
NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:start length: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:[self assetLibraryURLForLocalURL:localURL] resultBlock:resultBlock failureBlock:failureBlock];
}
- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback
{
// 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:localURL.fullPath forKey:@"fullPath"];
NSString* filename = [assetRepresentation filename];
[fileInfo setObject:filename forKey:@"name"];
[fileInfo setObject:[CDVAssetLibraryFilesystem getMimeTypeFromPath:filename] forKey:@"type"];
NSDate* creationDate = [asset valueForProperty:ALAssetPropertyDate];
NSNumber* msDate = [NSNumber numberWithDouble:[creationDate timeIntervalSince1970] * 1000];
[fileInfo setObject:msDate forKey:@"lastModifiedDate"];
callback([CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo]);
} else {
// We couldn't find the asset. Send the appropriate error.
callback([CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]);
}
};
ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) {
// Retrieving the asset failed for some reason. Send the appropriate error.
callback([CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]);
};
ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init];
[assetsLibrary assetForURL:[self assetLibraryURLForLocalURL:localURL] resultBlock:resultBlock failureBlock:failureBlock];
return;
}
@end

View File

@@ -0,0 +1,157 @@
/*
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>
NSString* const kCDVAssetsLibraryPrefix;
NSString* const kCDVFilesystemURLPrefix;
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;
@interface CDVFilesystemURL : NSObject {
NSURL *_url;
NSString *_fileSystemName;
NSString *_fullPath;
}
- (id) initWithString:(NSString*)strURL;
- (id) initWithURL:(NSURL*)URL;
+ (CDVFilesystemURL *)fileSystemURLWithString:(NSString *)strURL;
+ (CDVFilesystemURL *)fileSystemURLWithURL:(NSURL *)URL;
- (NSString *)absoluteURL;
@property (atomic) NSURL *url;
@property (atomic) NSString *fileSystemName;
@property (atomic) NSString *fullPath;
@end
@interface CDVFilesystemURLProtocol : NSURLProtocol
@end
@protocol CDVFileSystem
- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url;
- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options;
- (CDVPluginResult *)getParentForURL:(CDVFilesystemURL *)localURI;
- (CDVPluginResult *)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options;
- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI;
- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI;
- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI;
- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos;
- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend;
- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback;
- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback;
- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback;
- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url;
- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir;
@property (nonatomic,strong) NSString *name;
@property (nonatomic, copy) NSURL*(^urlTransformer)(NSURL*);
@optional
- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)localURI;
- (CDVFilesystemURL *)URLforFilesystemPath:(NSString *)path;
@end
@interface CDVFile : CDVPlugin {
NSString* rootDocsPath;
NSString* appDocsPath;
NSString* appLibraryPath;
NSString* appTempPath;
NSMutableArray* fileSystems_;
BOOL userHasAllowed;
}
- (NSNumber*)checkFreeDiskSpace:(NSString*)appPath;
- (NSDictionary*)makeEntryForPath:(NSString*)fullPath fileSystemName:(NSString *)fsName isDirectory:(BOOL)isDir;
- (NSDictionary *)makeEntryForURL:(NSURL *)URL;
- (CDVFilesystemURL *)fileSystemURLforLocalPath:(NSString *)localPath;
- (NSObject<CDVFileSystem> *)filesystemForURL:(CDVFilesystemURL *)localURL;
/* Native Registration API */
- (void)registerFilesystem:(NSObject<CDVFileSystem> *)fs;
- (NSObject<CDVFileSystem> *)fileSystemByName:(NSString *)fsName;
/* Exec API */
- (void)requestFileSystem:(CDVInvokedUrlCommand*)command;
- (void)resolveLocalFileSystemURI:(CDVInvokedUrlCommand*)command;
- (void)getDirectory:(CDVInvokedUrlCommand*)command;
- (void)getFile:(CDVInvokedUrlCommand*)command;
- (void)getParent:(CDVInvokedUrlCommand*)command;
- (void)removeRecursively:(CDVInvokedUrlCommand*)command;
- (void)remove:(CDVInvokedUrlCommand*)command;
- (void)copyTo:(CDVInvokedUrlCommand*)command;
- (void)moveTo:(CDVInvokedUrlCommand*)command;
- (void)getFileMetadata:(CDVInvokedUrlCommand*)command;
- (void)readEntries:(CDVInvokedUrlCommand*)command;
- (void)readAsText:(CDVInvokedUrlCommand*)command;
- (void)readAsDataURL:(CDVInvokedUrlCommand*)command;
- (void)readAsArrayBuffer:(CDVInvokedUrlCommand*)command;
- (void)write:(CDVInvokedUrlCommand*)command;
- (void)testFileExists:(CDVInvokedUrlCommand*)command;
- (void)testDirectoryExists:(CDVInvokedUrlCommand*)command;
- (void)getFreeDiskSpace:(CDVInvokedUrlCommand*)command;
- (void)truncate:(CDVInvokedUrlCommand*)command;
- (void)doCopyMove:(CDVInvokedUrlCommand*)command isCopy:(BOOL)bCopy;
/* Compatibilty with older File API */
- (NSString*)getMimeTypeFromPath:(NSString*)fullPath;
- (NSDictionary *)getDirectoryEntry:(NSString *)target isDirectory:(BOOL)bDirRequest;
/* Conversion between filesystem paths and URLs */
- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)URL;
/* Internal methods for testing */
- (void)_getLocalFilesystemPath:(CDVInvokedUrlCommand*)command;
@property (nonatomic, strong) NSString* rootDocsPath;
@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 (nonatomic, strong) NSMutableArray* fileSystems;
@property BOOL userHasAllowed;
@end
#define kW3FileTemporary @"temporary"
#define kW3FilePersistent @"persistent"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
/*
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"
@interface CDVLocalFilesystem : NSObject<CDVFileSystem> {
NSString *_name;
NSString *_fsRoot;
}
- (id) initWithName:(NSString *)name root:(NSString *)fsRoot;
+ (NSString*)getMimeTypeFromPath:(NSString*)fullPath;
@property (nonatomic,strong) NSString *fsRoot;
@end

View File

@@ -0,0 +1,734 @@
/*
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 "CDVLocalFilesystem.h"
#import <Cordova/CDV.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <sys/xattr.h>
@implementation CDVLocalFilesystem
@synthesize name=_name, fsRoot=_fsRoot, urlTransformer;
- (id) initWithName:(NSString *)name root:(NSString *)fsRoot
{
if (self) {
_name = name;
_fsRoot = fsRoot;
}
return self;
}
/*
* IN
* NSString localURI
* OUT
* CDVPluginResult result containing a file or directoryEntry for the localURI, or an error if the
* URI represents a non-existent path, or is unrecognized or otherwise malformed.
*/
- (CDVPluginResult *)entryForLocalURI:(CDVFilesystemURL *)url
{
CDVPluginResult* result = nil;
NSDictionary* entry = [self makeEntryForLocalURL:url];
if (entry) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:entry];
} else {
// return NOT_FOUND_ERR
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
}
return result;
}
- (NSDictionary *)makeEntryForLocalURL:(CDVFilesystemURL *)url {
NSString *path = [self filesystemPathForURL:url];
NSFileManager* fileMgr = [[NSFileManager alloc] init];
BOOL isDir = NO;
// see if exists and is file or dir
BOOL bExists = [fileMgr fileExistsAtPath:path isDirectory:&isDir];
if (bExists) {
return [self makeEntryForPath:url.fullPath isDirectory:isDir];
} else {
return nil;
}
}
- (NSDictionary*)makeEntryForPath:(NSString*)fullPath isDirectory:(BOOL)isDir
{
NSMutableDictionary* dirEntry = [NSMutableDictionary dictionaryWithCapacity:5];
NSString* lastPart = [[self stripQueryParametersFromPath:fullPath] lastPathComponent];
if (isDir && ![fullPath hasSuffix:@"/"]) {
fullPath = [fullPath stringByAppendingString:@"/"];
}
[dirEntry setObject:[NSNumber numberWithBool:!isDir] forKey:@"isFile"];
[dirEntry setObject:[NSNumber numberWithBool:isDir] forKey:@"isDirectory"];
[dirEntry setObject:fullPath forKey:@"fullPath"];
[dirEntry setObject:lastPart forKey:@"name"];
[dirEntry setObject:self.name forKey: @"filesystemName"];
NSURL* nativeURL = [NSURL fileURLWithPath:[self filesystemPathForFullPath:fullPath]];
if (self.urlTransformer) {
nativeURL = self.urlTransformer(nativeURL);
}
dirEntry[@"nativeURL"] = [nativeURL absoluteString];
return dirEntry;
}
- (NSString *)stripQueryParametersFromPath:(NSString *)fullPath
{
NSRange questionMark = [fullPath rangeOfString:@"?"];
if (questionMark.location != NSNotFound) {
return [fullPath substringWithRange:NSMakeRange(0,questionMark.location)];
}
return fullPath;
}
- (NSString *)filesystemPathForFullPath:(NSString *)fullPath
{
NSString *path = nil;
NSString *strippedFullPath = [self stripQueryParametersFromPath:fullPath];
path = [NSString stringWithFormat:@"%@%@", self.fsRoot, strippedFullPath];
if ([path length] > 1 && [path hasSuffix:@"/"]) {
path = [path substringToIndex:([path length]-1)];
}
return path;
}
/*
* IN
* NSString localURI
* OUT
* NSString full local filesystem path for the represented file or directory, or nil if no such path is possible
* The file or directory does not necessarily have to exist. nil is returned if the filesystem type is not recognized,
* or if the URL is malformed.
* The incoming URI should be properly escaped (no raw spaces, etc. URI percent-encoding is expected).
*/
- (NSString *)filesystemPathForURL:(CDVFilesystemURL *)url
{
return [self filesystemPathForFullPath:url.fullPath];
}
- (CDVFilesystemURL *)URLforFullPath:(NSString *)fullPath
{
if (fullPath) {
NSString* escapedPath = [fullPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if ([fullPath hasPrefix:@"/"]) {
return [CDVFilesystemURL fileSystemURLWithString:[NSString stringWithFormat:@"%@://localhost/%@%@", kCDVFilesystemURLPrefix, self.name, escapedPath]];
}
return [CDVFilesystemURL fileSystemURLWithString:[NSString stringWithFormat:@"%@://localhost/%@/%@", kCDVFilesystemURLPrefix, self.name, escapedPath]];
}
return nil;
}
- (CDVFilesystemURL *)URLforFilesystemPath:(NSString *)path
{
return [self URLforFullPath:[self fullPathForFileSystemPath:path]];
}
- (NSString *)normalizePath:(NSString *)rawPath
{
// If this is an absolute path, the first path component will be '/'. Skip it if that's the case
BOOL isAbsolutePath = [rawPath hasPrefix:@"/"];
if (isAbsolutePath) {
rawPath = [rawPath substringFromIndex:1];
}
NSMutableArray *components = [NSMutableArray arrayWithArray:[rawPath pathComponents]];
for (int index = 0; index < [components count]; ++index) {
if ([[components objectAtIndex:index] isEqualToString:@".."]) {
[components removeObjectAtIndex:index];
if (index > 0) {
[components removeObjectAtIndex:index-1];
--index;
}
}
}
if (isAbsolutePath) {
return [NSString stringWithFormat:@"/%@", [components componentsJoinedByString:@"/"]];
} else {
return [components componentsJoinedByString:@"/"];
}
}
- (BOOL)valueForKeyIsNumber:(NSDictionary*)dict key:(NSString*)key
{
BOOL bNumber = NO;
NSObject* value = dict[key];
if (value) {
bNumber = [value isKindOfClass:[NSNumber class]];
}
return bNumber;
}
- (CDVPluginResult *)getFileForURL:(CDVFilesystemURL *)baseURI requestedPath:(NSString *)requestedPath options:(NSDictionary *)options
{
CDVPluginResult* result = nil;
BOOL bDirRequest = NO;
BOOL create = NO;
BOOL exclusive = NO;
int errorCode = 0; // !!! risky - no error code currently defined for 0
if ([self valueForKeyIsNumber:options key:@"create"]) {
create = [(NSNumber*)[options valueForKey:@"create"] boolValue];
}
if ([self valueForKeyIsNumber:options key:@"exclusive"]) {
exclusive = [(NSNumber*)[options valueForKey:@"exclusive"] boolValue];
}
if ([self valueForKeyIsNumber:options key:@"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 {
// Build new fullPath for the requested resource.
// We concatenate the two paths together, and then scan the resulting string to remove
// parent ("..") references. Any parent references at the beginning of the string are
// silently removed.
NSString *combinedPath = [baseURI.fullPath stringByAppendingPathComponent:requestedPath];
combinedPath = [self normalizePath:combinedPath];
CDVFilesystemURL* requestedURL = [self URLforFullPath:combinedPath];
NSFileManager* fileMgr = [[NSFileManager alloc] init];
BOOL bIsDir;
BOOL bExists = [fileMgr fileExistsAtPath:[self filesystemPathForURL:requestedURL] isDirectory:&bIsDir];
if (bExists && (create == NO) && (bIsDir == !bDirRequest)) {
// path exists and is not 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:[self filesystemPathForURL:requestedURL] withIntermediateDirectories:NO attributes:nil error:&pError];
} else {
// create the empty file
bSuccess = [fileMgr createFileAtPath:[self filesystemPathForURL:requestedURL] 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 makeEntryForPath:requestedURL.fullPath isDirectory:bDirRequest]];
}
} // are all possible conditions met?
}
if (errorCode > 0) {
// create error callback
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
}
return result;
}
- (CDVPluginResult*)getParentForURL:(CDVFilesystemURL *)localURI
{
CDVPluginResult* result = nil;
CDVFilesystemURL *newURI = nil;
if ([localURI.fullPath isEqualToString:@""]) {
// return self
newURI = localURI;
} else {
newURI = [CDVFilesystemURL fileSystemURLWithURL:[localURI.url URLByDeletingLastPathComponent]]; /* TODO: UGLY - FIX */
}
NSFileManager* fileMgr = [[NSFileManager alloc] init];
BOOL bIsDir;
BOOL bExists = [fileMgr fileExistsAtPath:[self filesystemPathForURL:newURI] isDirectory:&bIsDir];
if (bExists) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[self makeEntryForPath:newURI.fullPath isDirectory:bIsDir]];
} else {
// invalid path or file does not exist
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
}
return result;
}
- (CDVPluginResult*)setMetadataForURL:(CDVFilesystemURL *)localURI withObject:(NSDictionary *)options
{
BOOL ok = NO;
NSString* filePath = [self filesystemPathForURL:localURI];
// 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) {
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else {
return [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR];
}
}
/* 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 removing filesystem entry at %@: %@", fullPath, [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;
}
- (CDVPluginResult *)removeFileAtURL:(CDVFilesystemURL *)localURI
{
NSString *fileSystemPath = [self filesystemPathForURL:localURI];
NSFileManager* fileMgr = [[NSFileManager alloc] init];
BOOL bIsDir = NO;
BOOL bExists = [fileMgr fileExistsAtPath:fileSystemPath isDirectory:&bIsDir];
if (!bExists) {
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
}
if (bIsDir && ([[fileMgr contentsOfDirectoryAtPath:fileSystemPath error:nil] count] != 0)) {
// dir is not empty
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:INVALID_MODIFICATION_ERR];
}
return [self doRemove:fileSystemPath];
}
- (CDVPluginResult *)recursiveRemoveFileAtURL:(CDVFilesystemURL *)localURI
{
NSString *fileSystemPath = [self filesystemPathForURL:localURI];
return [self doRemove:fileSystemPath];
}
/*
* IN
* NSString localURI
* OUT
* NSString full local filesystem path for the represented file or directory, or nil if no such path is possible
* The file or directory does not necessarily have to exist. nil is returned if the filesystem type is not recognized,
* or if the URL is malformed.
* The incoming URI should be properly escaped (no raw spaces, etc. URI percent-encoding is expected).
*/
- (NSString *)fullPathForFileSystemPath:(NSString *)fsPath
{
if ([fsPath hasPrefix:self.fsRoot]) {
return [fsPath substringFromIndex:[self.fsRoot length]];
}
return nil;
}
- (CDVPluginResult *)readEntriesAtURL:(CDVFilesystemURL *)localURI
{
NSFileManager* fileMgr = [[NSFileManager alloc] init];
NSError* __autoreleasing error = nil;
NSString *fileSystemPath = [self filesystemPathForURL:localURI];
NSArray* contents = [fileMgr contentsOfDirectoryAtPath:fileSystemPath 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 = [fileSystemPath stringByAppendingPathComponent:name];
BOOL bIsDir = NO;
[fileMgr fileExistsAtPath:entryPath isDirectory:&bIsDir];
NSDictionary* entryDict = [self makeEntryForPath:[self fullPathForFileSystemPath:entryPath] isDirectory:bIsDir];
[entries addObject:entryDict];
}
}
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:entries];
} else {
// assume not found but could check error for more specific error conditions
return [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR];
}
}
- (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;
}
- (CDVPluginResult *)truncateFileAtURL:(CDVFilesystemURL *)localURI atPosition:(unsigned long long)pos
{
unsigned long long newPos = [self truncateFile:[self filesystemPathForURL:localURI] atPosition:pos];
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(int)newPos];
}
- (CDVPluginResult *)writeToFileAtURL:(CDVFilesystemURL *)localURL withData:(NSData*)encData append:(BOOL)shouldAppend
{
NSString *filePath = [self filesystemPathForURL:localURL];
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];
if (len == 0) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:(double)len];
} else {
[fileStream open];
bytesWritten = (int)[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];
}
return result;
}
/**
* 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;
}
- (void)copyFileToURL:(CDVFilesystemURL *)destURL withName:(NSString *)newName fromFileSystem:(NSObject<CDVFileSystem> *)srcFs atURL:(CDVFilesystemURL *)srcURL copy:(BOOL)bCopy callback:(void (^)(CDVPluginResult *))callback
{
NSFileManager *fileMgr = [[NSFileManager alloc] init];
NSString *destRootPath = [self filesystemPathForURL:destURL];
BOOL bDestIsDir = NO;
BOOL bDestExists = [fileMgr fileExistsAtPath:destRootPath isDirectory:&bDestIsDir];
NSString *newFileSystemPath = [destRootPath stringByAppendingPathComponent:newName];
NSString *newFullPath = [self fullPathForFileSystemPath:newFileSystemPath];
BOOL bNewIsDir = NO;
BOOL bNewExists = [fileMgr fileExistsAtPath:newFileSystemPath isDirectory:&bNewIsDir];
CDVPluginResult *result = nil;
int errCode = 0;
if (!bDestExists) {
// the destination root does not exist
errCode = NOT_FOUND_ERR;
}
else if ([srcFs isKindOfClass:[CDVLocalFilesystem class]]) {
/* Same FS, we can shortcut with NSFileManager operations */
NSString *srcFullPath = [srcFs filesystemPathForURL:srcURL];
BOOL bSrcIsDir = NO;
BOOL bSrcExists = [fileMgr fileExistsAtPath:srcFullPath isDirectory:&bSrcIsDir];
if (!bSrcExists) {
// the source does not exist
errCode = NOT_FOUND_ERR;
} else if ([newFileSystemPath isEqualToString:srcFullPath]) {
// source and destination can not be the same
errCode = INVALID_MODIFICATION_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:newFileSystemPath]) {
// 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:newFileSystemPath 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:newFileSystemPath]) {
// can't move a dir into itself
errCode = INVALID_MODIFICATION_ERR;
} else if (bNewExists) {
if (bNewIsDir && ([[fileMgr contentsOfDirectoryAtPath:newFileSystemPath error:NULL] count] != 0)) {
// can't move dir to a dir that is not empty
errCode = INVALID_MODIFICATION_ERR;
newFileSystemPath = nil; // so we won't try to move
} else {
// remove destination so can perform the moveItemAtPath
bSuccess = [fileMgr removeItemAtPath:newFileSystemPath error:NULL];
if (!bSuccess) {
errCode = INVALID_MODIFICATION_ERR; // is this the correct error?
newFileSystemPath = nil;
}
}
} else if (bNewIsDir && [newFileSystemPath hasPrefix:srcFullPath]) {
// can't move a directory inside itself or to any child at any depth;
errCode = INVALID_MODIFICATION_ERR;
newFileSystemPath = nil;
}
if (newFileSystemPath != nil) {
bSuccess = [fileMgr moveItemAtPath:srcFullPath toPath:newFileSystemPath error:&error];
}
}
if (bSuccess) {
// should verify it is there and of the correct type???
NSDictionary* newEntry = [self makeEntryForPath:newFullPath isDirectory:bSrcIsDir];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry];
} else {
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;
}
}
}
}
} else {
// Need to copy the hard way
[srcFs readFileAtURL:srcURL start:0 end:-1 callback:^(NSData* data, NSString* mimeType, CDVFileError errorCode) {
CDVPluginResult* result = nil;
if (data != nil) {
BOOL bSuccess = [data writeToFile:newFileSystemPath atomically:YES];
if (bSuccess) {
// should verify it is there and of the correct type???
NSDictionary* newEntry = [self makeEntryForPath:newFullPath isDirectory:NO];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newEntry];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:ABORT_ERR];
}
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errorCode];
}
callback(result);
}];
return; // Async IO; return without callback.
}
if (result == nil) {
if (!errCode) {
errCode = INVALID_MODIFICATION_ERR; // Catch-all default
}
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:errCode];
}
callback(result);
}
/* 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";
} else if ([[fullPath pathExtension] rangeOfString:@"css"].location != NSNotFound) {
mimeType = @"text/css";
}
}
CFRelease(typeId);
}
}
return mimeType;
}
- (void)readFileAtURL:(CDVFilesystemURL *)localURL start:(NSInteger)start end:(NSInteger)end callback:(void (^)(NSData*, NSString* mimeType, CDVFileError))callback
{
NSString *path = [self filesystemPathForURL:localURL];
NSString* mimeType = [CDVLocalFilesystem 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);
}
- (void)getFileMetadataForURL:(CDVFilesystemURL *)localURL callback:(void (^)(CDVPluginResult *))callback
{
NSString *path = [self filesystemPathForURL:localURL];
CDVPluginResult *result;
NSFileManager* fileMgr = [[NSFileManager alloc] init];
NSError* __autoreleasing error = nil;
NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:path error:&error];
if (fileAttrs) {
// create dictionary of file info
NSMutableDictionary* fileInfo = [NSMutableDictionary dictionaryWithCapacity:5];
[fileInfo setObject:localURL.fullPath forKey:@"fullPath"];
[fileInfo setObject:@"" forKey:@"type"]; // can't easily get the mimetype unless create URL, send request and read response so skipping
[fileInfo setObject:[path lastPathComponent] forKey:@"name"];
// Ensure that directories (and other non-regular files) report size of 0
unsigned long long size = ([fileAttrs fileType] == NSFileTypeRegular ? [fileAttrs fileSize] : 0);
[fileInfo setObject:[NSNumber numberWithUnsignedLongLong:size] forKey:@"size"];
NSDate* modDate = [fileAttrs fileModificationDate];
if (modDate) {
[fileInfo setObject:[NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000] forKey:@"lastModifiedDate"];
}
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:fileInfo];
} else {
// didn't get fileAttribs
CDVFileError errorCode = ABORT_ERR;
NSLog(@"error getting metadata: %@", [error localizedDescription]);
if ([error code] == NSFileNoSuchFileError || [error code] == NSFileReadNoSuchFileError) {
errorCode = NOT_FOUND_ERR;
}
// log [NSNumber numberWithDouble: theMessage] objCtype to see what it returns
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode];
}
callback(result);
}
@end

View File

@@ -0,0 +1,912 @@
/*
* 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.
*/
#include "file.h"
#include <QApplication>
namespace {
class FileError {
public:
static const QString kEncodingErr;
static const QString kTypeMismatchErr;
static const QString kNotFoundErr;
static const QString kSecurityErr;
static const QString kAbortErr;
static const QString kNotReadableErr;
static const QString kNoModificationAllowedErr;
static const QString kInvalidStateErr;
static const QString kSyntaxErr;
static const QString kInvalidModificationErr;
static const QString kQuotaExceededErr;
static const QString kPathExistsErr;
};
bool checkFileName(const QString &name) {
if (name.contains(":")){
return false;
}
return true;
}
};
const QString FileError::kEncodingErr("FileError.ENCODING_ERR");
const QString FileError::kTypeMismatchErr("FileError.TYPE_MISMATCH_ERR");
const QString FileError::kNotFoundErr("FileError.NOT_FOUND_ERR");
const QString FileError::kSecurityErr("FileError.SECURITY_ERR");
const QString FileError::kAbortErr("FileError.ABORT_ERR");
const QString FileError::kNotReadableErr("FileError.NOT_READABLE_ERR");
const QString FileError::kNoModificationAllowedErr("FileError.NO_MODIFICATION_ALLOWED_ERR");
const QString FileError::kInvalidStateErr("FileError.INVALID_STATE_ERR");
const QString FileError::kSyntaxErr("FileError.SYNTAX_ERR");
const QString FileError::kInvalidModificationErr("FileError.INVALID_MODIFICATION_ERR");
const QString FileError::kQuotaExceededErr("FileError.QUOTA_EXCEEDED_ERR");
const QString FileError::kPathExistsErr("FileError.PATH_EXISTS_ERR");
File::File(Cordova *cordova) :
CPlugin(cordova),
_persistentDir(QString("%1/.local/share/%2/persistent").arg(QDir::homePath()).arg(QCoreApplication::applicationName())) {
QDir::root().mkpath(_persistentDir.absolutePath());
}
QVariantMap File::file2map(const QFileInfo &fileInfo) {
QVariantMap res;
res.insert("name", fileInfo.fileName());
QPair<QString, QString> r = GetRelativePath(fileInfo);
res.insert("fullPath", QString("/") + r.second);
res.insert("filesystemName", r.first);
res.insert("nativeURL", QString("file://localhost") + fileInfo.absoluteFilePath());
res.insert("isDirectory", (int)fileInfo.isDir());
res.insert("isFile", (int)fileInfo.isFile());
return res;
}
QVariantMap File::dir2map(const QDir &dir) {
return file2map(QFileInfo(dir.absolutePath()));
}
QPair<QString, QString> File::GetRelativePath(const QFileInfo &fileInfo) {
QString fullPath = fileInfo.isDir() ? QDir::cleanPath(fileInfo.absoluteFilePath()) : fileInfo.absoluteFilePath();
QString relativePath1 = _persistentDir.relativeFilePath(fullPath);
QString relativePath2 = QDir::temp().relativeFilePath(fullPath);
if (!(relativePath1[0] != '.' || relativePath2[0] != '.')) {
if (relativePath1.size() > relativePath2.size()) {
return QPair<QString, QString>("temporary", relativePath2);
} else {
return QPair<QString, QString>("persistent", relativePath1);
}
}
if (relativePath1[0] != '.')
return QPair<QString, QString>("persistent", relativePath1);
return QPair<QString, QString>("temporary", relativePath2);
}
void File::requestFileSystem(int scId, int ecId, unsigned short type, unsigned long long size) {
QDir dir;
if (size >= 1000485760){
this->callback(ecId, FileError::kQuotaExceededErr);
return;
}
if (type == 0)
dir = QDir::temp();
else
dir = _persistentDir;
if (type > 1) {
this->callback(ecId, FileError::kSyntaxErr);
return;
} else {
QVariantMap res;
res.insert("root", dir2map(dir));
if (type == 0)
res.insert("name", "temporary");
else
res.insert("name", "persistent");
this->cb(scId, res);
}
}
QPair<bool, QFileInfo> File::resolveURI(int ecId, const QString &uri) {
QPair<bool, QFileInfo> result;
result.first = false;
QUrl url = QUrl::fromUserInput(uri);
if (url.scheme() == "file" && url.isValid()) {
result.first = true;
result.second = QFileInfo(url.path());
return result;
}
if (url.scheme() != "cdvfile") {
if (ecId)
this->callback(ecId, FileError::kTypeMismatchErr);
return result;
}
QString path = url.path().replace("//", "/");
//NOTE: colon is not safe in url, it is not a valid path in Win and Mac, simple disable it here.
if (path.contains(":") || !url.isValid()){
if (ecId)
this->callback(ecId, FileError::kEncodingErr);
return result;
}
if (!path.startsWith("/persistent/") && !path.startsWith("/temporary/")) {
if (ecId)
this->callback(ecId, FileError::kEncodingErr);
return result;
}
result.first = true;
if (path.startsWith("/persistent/")) {
QString relativePath = path.mid(QString("/persistent/").size());
result.second = QFileInfo(_persistentDir.filePath(relativePath));
} else {
QString relativePath = path.mid(QString("/temporary/").size());
result.second = QFileInfo(QDir::temp().filePath(relativePath));
}
return result;
}
QPair<bool, QFileInfo> File::resolveURI(const QString &uri) {
return resolveURI(0, uri);
}
void File::_getLocalFilesystemPath(int scId, int ecId, const QString& uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
this->cb(scId, f1.second.absoluteFilePath());
}
void File::resolveLocalFileSystemURI(int scId, int ecId, const QString &uri) {
if (uri[0] == '/' || uri[0] == '.') {
this->callback(ecId, FileError::kEncodingErr);
return;
}
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFileInfo fileInfo = f1.second;
if (!fileInfo.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
this->cb(scId, file2map(fileInfo));
}
void File::getFile(int scId, int ecId, const QString &parentPath, const QString &rpath, const QVariantMap &options) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, parentPath + "/" + rpath);
if (!f1.first)
return;
bool create = options.value("create").toBool();
bool exclusive = options.value("exclusive").toBool();
QFile file(f1.second.absoluteFilePath());
// if create is false and the path represents a directory, return error
QFileInfo fileInfo = f1.second;
if ((!create) && fileInfo.isDir()) {
this->callback(ecId, FileError::kTypeMismatchErr);
return;
}
// if file does exist, and create is true and exclusive is true, return error
if (file.exists()) {
if (create && exclusive) {
this->callback(ecId, FileError::kPathExistsErr);
return;
}
}
else {
// if file does not exist and create is false, return error
if (!create) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
file.open(QIODevice::WriteOnly);
file.close();
// Check if creation was successfull
if (!file.exists()) {
this->callback(ecId, FileError::kNoModificationAllowedErr);
return;
}
}
this->cb(scId, file2map(QFileInfo(file)));
}
void File::getDirectory(int scId, int ecId, const QString &parentPath, const QString &rpath, const QVariantMap &options) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, parentPath + "/" + rpath);
if (!f1.first)
return;
bool create = options.value("create").toBool();
bool exclusive = options.value("exclusive").toBool();
QDir dir(f1.second.absoluteFilePath());
QFileInfo &fileInfo = f1.second;
if ((!create) && fileInfo.isFile()) {
this->callback(ecId, FileError::kTypeMismatchErr);
return;
}
if (dir.exists()) {
if (create && exclusive) {
this->callback(ecId, FileError::kPathExistsErr);
return;
}
}
else {
if (!create) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
QString folderName = dir.dirName();
dir.cdUp();
dir.mkdir(folderName);
dir.cd(folderName);
if (!dir.exists()) {
this->callback(ecId, FileError::kNoModificationAllowedErr);
return;
}
}
this->cb(scId, dir2map(dir));
}
void File::removeRecursively(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QDir dir(f1.second.absoluteFilePath());
if (File::rmDir(dir))
this->cb(scId);
else
this->callback(ecId, FileError::kNoModificationAllowedErr);
}
void File::write(int scId, int ecId, const QString &uri, const QString &_data, unsigned long long position, bool binary) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
file.open(QIODevice::WriteOnly);
file.close();
if (!file.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
QFileInfo fileInfo(file);
if (!file.open(QIODevice::ReadWrite)) {
this->callback(ecId, FileError::kNoModificationAllowedErr);
return;
}
if (!binary) {
QTextStream textStream(&file);
textStream.setCodec("UTF-8");
textStream.setAutoDetectUnicode(true);
if (!textStream.seek(position)) {
file.close();
fileInfo.refresh();
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
textStream << _data;
textStream.flush();
} else {
QByteArray data(_data.toUtf8());
if (!file.seek(position)) {
file.close();
fileInfo.refresh();
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
file.write(data.data(), data.length());
}
file.flush();
file.close();
fileInfo.refresh();
this->cb(scId, fileInfo.size() - position);
}
void File::truncate(int scId, int ecId, const QString &uri, unsigned long long size) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
if (!file.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (!file.resize(size)) {
this->callback(ecId, FileError::kNoModificationAllowedErr);
return;
}
this->cb(scId, size);
}
void File::getParent(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QDir dir(f1.second.absoluteFilePath());
//can't cdup more than app's root
// Try to change into upper directory
if (dir != _persistentDir && dir != QDir::temp()){
if (!dir.cdUp()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
}
this->cb(scId, dir2map(dir));
}
void File::remove(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFileInfo &fileInfo = f1.second;
//TODO: fix
if (!fileInfo.exists() || (fileInfo.absoluteFilePath() == _persistentDir.absolutePath()) || (QDir::temp() == fileInfo.absoluteFilePath())) {
this->callback(ecId, FileError::kNoModificationAllowedErr);
return;
}
if (fileInfo.isDir()) {
QDir dir(fileInfo.absoluteFilePath());
if (dir.rmdir(dir.absolutePath())) {
this->cb(scId);
return;
}
} else {
QFile file(fileInfo.absoluteFilePath());
if (file.remove()) {
this->cb(scId);
return;
}
}
this->callback(ecId, FileError::kInvalidModificationErr);
}
void File::getFileMetadata(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFileInfo &fileInfo = f1.second;
if (!fileInfo.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
} else {
QMimeType mime = _db.mimeTypeForFile(fileInfo.fileName());
QString args = QString("{name: %1, fullPath: %2, type: %3, lastModifiedDate: new Date(%4), size: %5}")
.arg(CordovaInternal::format(fileInfo.fileName())).arg(CordovaInternal::format(fileInfo.absoluteFilePath()))
.arg(CordovaInternal::format(mime.name())).arg(fileInfo.lastModified().toMSecsSinceEpoch())
.arg(fileInfo.size());
this->callback(scId, args);
}
}
void File::getMetadata(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFileInfo &fileInfo = f1.second;
if (!fileInfo.exists())
this->callback(ecId, FileError::kNotFoundErr);
else {
QVariantMap obj;
obj.insert("modificationTime", fileInfo.lastModified().toMSecsSinceEpoch());
obj.insert("size", fileInfo.isDir() ? 0 : fileInfo.size());
this->cb(scId, obj);
}
}
void File::readEntries(int scId, int ecId, const QString &uri) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QDir dir(f1.second.absoluteFilePath());
QString entriesList;
if (!dir.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
for (const QFileInfo &fileInfo: dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) {
entriesList += CordovaInternal::format(file2map(fileInfo)) + ",";
}
// Remove trailing comma
if (entriesList.size() > 0)
entriesList.remove(entriesList.size() - 1, 1);
entriesList = "new Array(" + entriesList + ")";
this->callback(scId, entriesList);
}
void File::readAsText(int scId, int ecId, const QString &uri, const QString &/*encoding*/, int sliceStart, int sliceEnd) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
if (!file.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (!file.open(QIODevice::ReadOnly)) {
this->callback(ecId, FileError::kNotReadableErr);
return;
}
QByteArray content = file.readAll();
if (sliceEnd == -1)
sliceEnd = content.size();
if (sliceEnd < 0) {
sliceEnd++;
sliceEnd = std::max(0, content.size() + sliceEnd);
}
if (sliceEnd > content.size())
sliceEnd = content.size();
if (sliceStart < 0)
sliceStart = std::max(0, content.size() + sliceStart);
if (sliceStart > content.size())
sliceStart = content.size();
if (sliceStart > sliceEnd)
sliceEnd = sliceStart;
//FIXME: encoding
content = content.mid(sliceStart, sliceEnd - sliceStart);
this->cb(scId, content);
}
void File::readAsArrayBuffer(int scId, int ecId, const QString &uri, int sliceStart, int sliceEnd) {
const QString str2array("\
(function strToArray(str) { \
var res = new Uint8Array(str.length); \
for (var i = 0; i < str.length; i++) { \
res[i] = str.charCodeAt(i); \
} \
return res; \
})(\"%1\")");
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
if (!file.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (!file.open(QIODevice::ReadOnly)) {
this->callback(ecId, FileError::kNotReadableErr);
return;
}
QString res;
QByteArray content = file.readAll();
if (sliceEnd == -1)
sliceEnd = content.size();
if (sliceEnd < 0) {
sliceEnd++;
sliceEnd = std::max(0, content.size() + sliceEnd);
}
if (sliceEnd > content.size())
sliceEnd = content.size();
if (sliceStart < 0)
sliceStart = std::max(0, content.size() + sliceStart);
if (sliceStart > content.size())
sliceStart = content.size();
if (sliceStart > sliceEnd)
sliceEnd = sliceStart;
content = content.mid(sliceStart, sliceEnd - sliceStart);
res.reserve(content.length() * 6);
for (uchar c: content) {
res += "\\x";
res += QString::number(c, 16).rightJustified(2, '0').toUpper();
}
this->callback(scId, str2array.arg(res));
}
void File::readAsBinaryString(int scId, int ecId, const QString &uri, int sliceStart, int sliceEnd) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
if (!file.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (!file.open(QIODevice::ReadOnly)) {
this->callback(ecId, FileError::kNotReadableErr);
return;
}
QString res;
QByteArray content = file.readAll();
if (sliceEnd == -1)
sliceEnd = content.size();
if (sliceEnd < 0) {
sliceEnd++;
sliceEnd = std::max(0, content.size() + sliceEnd);
}
if (sliceEnd > content.size())
sliceEnd = content.size();
if (sliceStart < 0)
sliceStart = std::max(0, content.size() + sliceStart);
if (sliceStart > content.size())
sliceStart = content.size();
if (sliceStart > sliceEnd)
sliceEnd = sliceStart;
content = content.mid(sliceStart, sliceEnd - sliceStart);
res.reserve(content.length() * 6);
for (uchar c: content) {
res += "\\x";
res += QString::number(c, 16).rightJustified(2, '0').toUpper();
}
this->callback(scId, "\"" + res + "\"");
}
void File::readAsDataURL(int scId, int ecId, const QString &uri, int sliceStart, int sliceEnd) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, uri);
if (!f1.first)
return;
QFile file(f1.second.absoluteFilePath());
QFileInfo &fileInfo = f1.second;
if (!file.exists()) {
this->callback(ecId, FileError::kNotReadableErr);
return;
}
if (!file.open(QIODevice::ReadOnly)) {
this->callback(ecId, FileError::kNotReadableErr);
return;
}
QByteArray content = file.readAll();
QString contentType(_db.mimeTypeForFile(fileInfo.fileName()).name());
if (sliceEnd == -1)
sliceEnd = content.size();
if (sliceEnd < 0) {
sliceEnd++;
sliceEnd = std::max(0, content.size() + sliceEnd);
}
if (sliceEnd > content.size())
sliceEnd = content.size();
if (sliceStart < 0)
sliceStart = std::max(0, content.size() + sliceStart);
if (sliceStart > content.size())
sliceStart = content.size();
if (sliceStart > sliceEnd)
sliceEnd = sliceStart;
content = content.mid(sliceStart, sliceEnd - sliceStart);
this->cb(scId, QString("data:%1;base64,").arg(contentType) + content.toBase64());
}
bool File::rmDir(const QDir &dir) {
if (dir == _persistentDir || dir == QDir::temp()) {//can't remove root dir
return false;
}
bool result = true;
if (dir.exists()) {
// Iterate over entries and remove them
Q_FOREACH(const QFileInfo &fileInfo, dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) {
if (fileInfo.isDir()) {
result = rmDir(fileInfo.absoluteFilePath());
}
else {
result = QFile::remove(fileInfo.absoluteFilePath());
}
if (!result) {
return result;
}
}
// Finally remove the current dir
return dir.rmdir(dir.absolutePath());
}
return result;
}
bool File::copyFile(int scId, int ecId,const QString& sourceUri, const QString& destinationUri, const QString& newName) {
QPair<bool, QFileInfo> destDir = resolveURI(ecId, destinationUri);
QPair<bool, QFileInfo> sourceFile = resolveURI(ecId, sourceUri);
if (!destDir.first || !sourceFile.first)
return false;
if (!checkFileName(newName)) {
this->callback(ecId, FileError::kEncodingErr);
return false;
}
if (destDir.second.isFile()) {
this->callback(ecId, FileError::kInvalidModificationErr);
return false;
}
if (!destDir.second.isDir()) {
this->callback(ecId, FileError::kNotFoundErr);
return false;
}
QFileInfo &fileInfo = sourceFile.second;
QString fileName((newName.isEmpty()) ? fileInfo.fileName() : newName);
QString destinationFile(QDir(destDir.second.absoluteFilePath()).filePath(fileName));
if (QFile::copy(fileInfo.absoluteFilePath(), destinationFile)){
this->cb(scId, file2map(QFileInfo(destinationFile)));
return true;
}
this->callback(ecId, FileError::kInvalidModificationErr);
return false;
}
void File::copyDir(int scId, int ecId,const QString& sourceUri, const QString& destinationUri, const QString& newName) {
QPair<bool, QFileInfo> destDir = resolveURI(ecId, destinationUri);
QPair<bool, QFileInfo> sourceDir = resolveURI(ecId, sourceUri);
if (!destDir.first || !sourceDir.first)
return;
if (!checkFileName(newName)) {
this->callback(ecId, FileError::kEncodingErr);
return;
}
QString targetName = ((newName.isEmpty()) ? sourceDir.second.fileName() : newName);
QString target(QDir(destDir.second.absoluteFilePath()).filePath(targetName));
if (QFileInfo(target).isFile()){
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
// check: copy directory into itself
if (QDir(sourceDir.second.absoluteFilePath()).relativeFilePath(target)[0] != '.'){
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (!QDir(target).exists()){
QDir(destDir.second.absoluteFilePath()).mkdir(target);;
} else{
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (copyFolder(sourceDir.second.absoluteFilePath(), target)){
this->cb(scId, dir2map(QDir(target)));
return;
}
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
void File::copyTo(int scId, int ecId, const QString& source, const QString& destinationDir, const QString& newName) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, source);
if (!f1.first)
return;
if (f1.second.isDir())
copyDir(scId, ecId, source, destinationDir, newName);
else
copyFile(scId, ecId, source, destinationDir, newName);
}
void File::moveFile(int scId, int ecId,const QString& sourceUri, const QString& destinationUri, const QString& newName) {
QPair<bool, QFileInfo> sourceFile = resolveURI(ecId, sourceUri);
QPair<bool, QFileInfo> destDir = resolveURI(ecId, destinationUri);
if (!destDir.first || !sourceFile.first)
return;
if (!checkFileName(newName)) {
this->callback(ecId, FileError::kEncodingErr);
return;
}
QString fileName = ((newName.isEmpty()) ? sourceFile.second.fileName() : newName);
QString target = QDir(destDir.second.absoluteFilePath()).filePath(fileName);
if (sourceFile.second == QFileInfo(target)) {
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (!destDir.second.exists()) {
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (!destDir.second.isDir()){
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (QFileInfo(target).exists()) {
if (!QFile::remove(target)) {
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
}
QFile::rename(sourceFile.second.absoluteFilePath(), target);
this->cb(scId, file2map(QFileInfo(target)));
}
void File::moveDir(int scId, int ecId,const QString& sourceUri, const QString& destinationUri, const QString& newName){
QPair<bool, QFileInfo> sourceDir = resolveURI(ecId, sourceUri);
QPair<bool, QFileInfo> destDir = resolveURI(ecId, destinationUri);
if (!destDir.first || !sourceDir.first)
return;
if (!checkFileName(newName)) {
this->callback(ecId, FileError::kEncodingErr);
return;
}
QString fileName = ((newName.isEmpty()) ? sourceDir.second.fileName() : newName);
QString target = QDir(destDir.second.absoluteFilePath()).filePath(fileName);
if (!destDir.second.exists()){
this->callback(ecId, FileError::kNotFoundErr);
return;
}
if (destDir.second.isFile()){
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
// check: copy directory into itself
if (QDir(sourceDir.second.absoluteFilePath()).relativeFilePath(target)[0] != '.'){
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (QFileInfo(target).exists() && !QDir(destDir.second.absoluteFilePath()).rmdir(fileName)) {
this->callback(ecId, FileError::kInvalidModificationErr);
return;
}
if (copyFolder(sourceDir.second.absoluteFilePath(), target)) {
rmDir(sourceDir.second.absoluteFilePath());
this->cb(scId, file2map(QFileInfo(target)));
} else {
this->callback(ecId, FileError::kNoModificationAllowedErr);
}
}
void File::moveTo(int scId, int ecId, const QString& source, const QString& destinationDir, const QString& newName) {
QPair<bool, QFileInfo> f1 = resolveURI(ecId, source);
if (!f1.first)
return;
if (f1.second.isDir())
moveDir(scId, ecId, source, destinationDir, newName);
else
moveFile(scId, ecId, source, destinationDir, newName);
}
bool File::copyFolder(const QString& sourceFolder, const QString& destFolder) {
QDir sourceDir(sourceFolder);
if (!sourceDir.exists())
return false;
QDir destDir(destFolder);
if (!destDir.exists()){
destDir.mkdir(destFolder);
}
QStringList files = sourceDir.entryList(QDir::Files);
for (int i = 0; i< files.count(); i++)
{
QString srcName = sourceFolder + "/" + files[i];
QString destName = destFolder + "/" + files[i];
QFile::copy(srcName, destName);
}
files.clear();
files = sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
for (int i = 0; i< files.count(); i++)
{
QString srcName = sourceFolder + "/" + files[i];
QString destName = destFolder + "/" + files[i];
copyFolder(srcName, destName);
}
return true;
}

View File

@@ -0,0 +1,81 @@
/*
* 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.
*/
#ifndef FILEAPI_H_SDASDASDAS
#define FILEAPI_H_SDASDASDAS
#include <QNetworkReply>
#include <QtCore>
#include <cplugin.h>
#include <cordova.h>
class File: public CPlugin {
Q_OBJECT
public:
explicit File(Cordova *cordova);
virtual const QString fullName() override {
return File::fullID();
}
virtual const QString shortName() override {
return "File";
}
static const QString fullID() {
return "File";
}
QPair<bool, QFileInfo> resolveURI(const QString &uri);
QPair<bool, QFileInfo> resolveURI(int ecId, const QString &uri);
QVariantMap file2map(const QFileInfo &dir);
public slots:
void requestFileSystem(int scId, int ecId, unsigned short type, unsigned long long size);
void resolveLocalFileSystemURI(int scId, int ecId, const QString&);
void getDirectory(int scId, int ecId, const QString&, const QString&, const QVariantMap&);
void getFile(int scId, int ecId, const QString &parentPath, const QString &rpath, const QVariantMap &options);
void readEntries(int scId, int ecId, const QString &uri);
void getParent(int scId, int ecId, const QString &uri);
void copyTo(int scId, int ecId, const QString& source, const QString& destinationDir, const QString& newName);
void moveTo(int scId, int ecId, const QString& source, const QString& destinationDir, const QString& newName);
void getFileMetadata(int scId, int ecId, const QString &);
void getMetadata(int scId, int ecId, const QString &);
void remove(int scId, int ecId, const QString &);
void removeRecursively(int scId, int ecId, const QString&);
void write(int scId, int ecId, const QString&, const QString&, unsigned long long position, bool binary);
void readAsText(int scId, int ecId, const QString&, const QString &encoding, int sliceStart, int sliceEnd);
void readAsDataURL(int scId, int ecId, const QString&, int sliceStart, int sliceEnd);
void readAsArrayBuffer(int scId, int ecId, const QString&, int sliceStart, int sliceEnd);
void readAsBinaryString(int scId, int ecId, const QString&, int sliceStart, int sliceEnd);
void truncate(int scId, int ecId, const QString&, unsigned long long size);
void _getLocalFilesystemPath(int scId, int ecId, const QString&);
private:
void moveFile(int scId, int ecId,const QString&, const QString&, const QString&);
void moveDir(int scId, int ecId,const QString&, const QString&, const QString&);
bool copyFile(int scId, int ecId, const QString&, const QString&, const QString&);
void copyDir(int scId, int ecId, const QString&, const QString&, const QString&);
bool rmDir(const QDir &dir);
bool copyFolder(const QString&, const QString&);
QPair<QString, QString> GetRelativePath(const QFileInfo &fileInfo);
QVariantMap dir2map(const QDir &dir);
QMimeDatabase _db;
const QDir _persistentDir;
QNetworkAccessManager _manager;
};
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff