| /* |
| 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(); |
| } |
| } |
| } |
| } |