123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- /*
- 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;
-
- import java.nio.charset.Charset;
-
- 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()) {
- // 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 long getFreeSpaceInBytes() {
- return DirectoryManager.getFreeSpaceInBytes(rootUri.getPath());
- }
-
- @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(Charset.defaultCharset());
- }
- 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());
- realException.initCause(e);
- 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();
- }
- }
- }
- }
|