openEventsPlatformApp/plugins/cordova-plugin-file/src/android/LocalFilesystem.java
plugins/cordova-plugin-file/src/android/LocalFilesystem.javaRaw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18 */
19package org.apache.cordova.file;
20
21import java.io.ByteArrayInputStream;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.io.RandomAccessFile;
30import java.nio.channels.FileChannel;
31import org.apache.cordova.CordovaResourceApi;
32import org.json.JSONException;
33import org.json.JSONObject;
34
35import android.os.Build;
36import android.os.Environment;
37import android.util.Base64;
38import android.net.Uri;
39import android.content.Context;
40import android.content.Intent;
41
42import java.nio.charset.Charset;
43
44public class LocalFilesystem extends Filesystem {
45 private final Context context;
46
47 public LocalFilesystem(String name, Context context, CordovaResourceApi resourceApi, File fsRoot) {
48 super(Uri.fromFile(fsRoot).buildUpon().appendEncodedPath("").build(), name, resourceApi);
49 this.context = context;
50 }
51
52 public String filesystemPathForFullPath(String fullPath) {
53 return new File(rootUri.getPath(), fullPath).toString();
54 }
55
56 @Override
57 public String filesystemPathForURL(LocalFilesystemURL url) {
58 return filesystemPathForFullPath(url.path);
59 }
60
61 private String fullPathForFilesystemPath(String absolutePath) {
62 if (absolutePath != null && absolutePath.startsWith(rootUri.getPath())) {
63 return absolutePath.substring(rootUri.getPath().length() - 1);
64 }
65 return null;
66 }
67
68 @Override
69 public Uri toNativeUri(LocalFilesystemURL inputURL) {
70 return nativeUriForFullPath(inputURL.path);
71 }
72
73 @Override
74 public LocalFilesystemURL toLocalUri(Uri inputURL) {
75 if (!"file".equals(inputURL.getScheme())) {
76 return null;
77 }
78 File f = new File(inputURL.getPath());
79 // Removes and duplicate /s (e.g. file:///a//b/c)
80 Uri resolvedUri = Uri.fromFile(f);
81 String rootUriNoTrailingSlash = rootUri.getEncodedPath();
82 rootUriNoTrailingSlash = rootUriNoTrailingSlash.substring(0, rootUriNoTrailingSlash.length() - 1);
83 if (!resolvedUri.getEncodedPath().startsWith(rootUriNoTrailingSlash)) {
84 return null;
85 }
86 String subPath = resolvedUri.getEncodedPath().substring(rootUriNoTrailingSlash.length());
87 // Strip leading slash
88 if (!subPath.isEmpty()) {
89 subPath = subPath.substring(1);
90 }
91 Uri.Builder b = new Uri.Builder()
92 .scheme(LocalFilesystemURL.FILESYSTEM_PROTOCOL)
93 .authority("localhost")
94 .path(name);
95 if (!subPath.isEmpty()) {
96 b.appendEncodedPath(subPath);
97 }
98 if (f.isDirectory()) {
99 // Add trailing / for directories.
100 b.appendEncodedPath("");
101 }
102 return LocalFilesystemURL.parse(b.build());
103 }
104
105 @Override
106 public LocalFilesystemURL URLforFilesystemPath(String path) {
107 return localUrlforFullPath(fullPathForFilesystemPath(path));
108 }
109
110 @Override
111 public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
112 String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
113 boolean create = false;
114 boolean exclusive = false;
115
116 if (options != null) {
117 create = options.optBoolean("create");
118 if (create) {
119 exclusive = options.optBoolean("exclusive");
120 }
121 }
122
123 // Check for a ":" character in the file to line up with BB and iOS
124 if (path.contains(":")) {
125 throw new EncodingException("This path has an invalid \":\" in it.");
126 }
127
128 LocalFilesystemURL requestedURL;
129
130 // Check whether the supplied path is absolute or relative
131 if (directory && !path.endsWith("/")) {
132 path += "/";
133 }
134 if (path.startsWith("/")) {
135 requestedURL = localUrlforFullPath(normalizePath(path));
136 } else {
137 requestedURL = localUrlforFullPath(normalizePath(inputURL.path + "/" + path));
138 }
139
140 File fp = new File(this.filesystemPathForURL(requestedURL));
141
142 if (create) {
143 if (exclusive && fp.exists()) {
144 throw new FileExistsException("create/exclusive fails");
145 }
146 if (directory) {
147 fp.mkdir();
148 } else {
149 fp.createNewFile();
150 }
151 if (!fp.exists()) {
152 throw new FileExistsException("create fails");
153 }
154 }
155 else {
156 if (!fp.exists()) {
157 throw new FileNotFoundException("path does not exist");
158 }
159 if (directory) {
160 if (fp.isFile()) {
161 throw new TypeMismatchException("path doesn't exist or is file");
162 }
163 } else {
164 if (fp.isDirectory()) {
165 throw new TypeMismatchException("path doesn't exist or is directory");
166 }
167 }
168 }
169
170 // Return the directory
171 return makeEntryForURL(requestedURL);
172 }
173
174 @Override
175 public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException {
176
177 File fp = new File(filesystemPathForURL(inputURL));
178
179 // You can't delete a directory that is not empty
180 if (fp.isDirectory() && fp.list().length > 0) {
181 throw new InvalidModificationException("You can't delete a directory that is not empty.");
182 }
183
184 return fp.delete();
185 }
186
187 @Override
188 public boolean exists(LocalFilesystemURL inputURL) {
189 File fp = new File(filesystemPathForURL(inputURL));
190 return fp.exists();
191 }
192
193 @Override
194 public long getFreeSpaceInBytes() {
195 return DirectoryManager.getFreeSpaceInBytes(rootUri.getPath());
196 }
197
198 @Override
199 public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException {
200 File directory = new File(filesystemPathForURL(inputURL));
201 return removeDirRecursively(directory);
202 }
203
204 protected boolean removeDirRecursively(File directory) throws FileExistsException {
205 if (directory.isDirectory()) {
206 for (File file : directory.listFiles()) {
207 removeDirRecursively(file);
208 }
209 }
210
211 if (!directory.delete()) {
212 throw new FileExistsException("could not delete: " + directory.getName());
213 } else {
214 return true;
215 }
216 }
217
218 @Override
219 public LocalFilesystemURL[] listChildren(LocalFilesystemURL inputURL) throws FileNotFoundException {
220 File fp = new File(filesystemPathForURL(inputURL));
221
222 if (!fp.exists()) {
223 // The directory we are listing doesn't exist so we should fail.
224 throw new FileNotFoundException();
225 }
226
227 File[] files = fp.listFiles();
228 if (files == null) {
229 // inputURL is a directory
230 return null;
231 }
232 LocalFilesystemURL[] entries = new LocalFilesystemURL[files.length];
233 for (int i = 0; i < files.length; i++) {
234 entries[i] = URLforFilesystemPath(files[i].getPath());
235 }
236
237 return entries;
238 }
239
240 @Override
241 public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
242 File file = new File(filesystemPathForURL(inputURL));
243
244 if (!file.exists()) {
245 throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
246 }
247
248 JSONObject metadata = new JSONObject();
249 try {
250 // Ensure that directories report a size of 0
251 metadata.put("size", file.isDirectory() ? 0 : file.length());
252 metadata.put("type", resourceApi.getMimeType(Uri.fromFile(file)));
253 metadata.put("name", file.getName());
254 metadata.put("fullPath", inputURL.path);
255 metadata.put("lastModifiedDate", file.lastModified());
256 } catch (JSONException e) {
257 return null;
258 }
259 return metadata;
260 }
261
262 private void copyFile(Filesystem srcFs, LocalFilesystemURL srcURL, File destFile, boolean move) throws IOException, InvalidModificationException, NoModificationAllowedException {
263 if (move) {
264 String realSrcPath = srcFs.filesystemPathForURL(srcURL);
265 if (realSrcPath != null) {
266 File srcFile = new File(realSrcPath);
267 if (srcFile.renameTo(destFile)) {
268 return;
269 }
270 // Trying to rename the file failed. Possibly because we moved across file system on the device.
271 }
272 }
273
274 CordovaResourceApi.OpenForReadResult offr = resourceApi.openForRead(srcFs.toNativeUri(srcURL));
275 copyResource(offr, new FileOutputStream(destFile));
276
277 if (move) {
278 srcFs.removeFileAtLocalURL(srcURL);
279 }
280 }
281
282 private void copyDirectory(Filesystem srcFs, LocalFilesystemURL srcURL, File dstDir, boolean move) throws IOException, NoModificationAllowedException, InvalidModificationException, FileExistsException {
283 if (move) {
284 String realSrcPath = srcFs.filesystemPathForURL(srcURL);
285 if (realSrcPath != null) {
286 File srcDir = new File(realSrcPath);
287 // If the destination directory already exists and is empty then delete it. This is according to spec.
288 if (dstDir.exists()) {
289 if (dstDir.list().length > 0) {
290 throw new InvalidModificationException("directory is not empty");
291 }
292 dstDir.delete();
293 }
294 // Try to rename the directory
295 if (srcDir.renameTo(dstDir)) {
296 return;
297 }
298 // Trying to rename the file failed. Possibly because we moved across file system on the device.
299 }
300 }
301
302 if (dstDir.exists()) {
303 if (dstDir.list().length > 0) {
304 throw new InvalidModificationException("directory is not empty");
305 }
306 } else {
307 if (!dstDir.mkdir()) {
308 // If we can't create the directory then fail
309 throw new NoModificationAllowedException("Couldn't create the destination directory");
310 }
311 }
312
313 LocalFilesystemURL[] children = srcFs.listChildren(srcURL);
314 for (LocalFilesystemURL childLocalUrl : children) {
315 File target = new File(dstDir, new File(childLocalUrl.path).getName());
316 if (childLocalUrl.isDirectory) {
317 copyDirectory(srcFs, childLocalUrl, target, false);
318 } else {
319 copyFile(srcFs, childLocalUrl, target, false);
320 }
321 }
322
323 if (move) {
324 srcFs.recursiveRemoveFileAtLocalURL(srcURL);
325 }
326 }
327
328 @Override
329 public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
330 Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {
331
332 // Check to see if the destination directory exists
333 String newParent = this.filesystemPathForURL(destURL);
334 File destinationDir = new File(newParent);
335 if (!destinationDir.exists()) {
336 // The destination does not exist so we should fail.
337 throw new FileNotFoundException("The source does not exist");
338 }
339
340 // Figure out where we should be copying to
341 final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL, srcURL.isDirectory);
342
343 Uri dstNativeUri = toNativeUri(destinationURL);
344 Uri srcNativeUri = srcFs.toNativeUri(srcURL);
345 // Check to see if source and destination are the same file
346 if (dstNativeUri.equals(srcNativeUri)) {
347 throw new InvalidModificationException("Can't copy onto itself");
348 }
349
350 if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) {
351 throw new InvalidModificationException("Source URL is read-only (cannot move)");
352 }
353
354 File destFile = new File(dstNativeUri.getPath());
355 if (destFile.exists()) {
356 if (!srcURL.isDirectory && destFile.isDirectory()) {
357 throw new InvalidModificationException("Can't copy/move a file to an existing directory");
358 } else if (srcURL.isDirectory && destFile.isFile()) {
359 throw new InvalidModificationException("Can't copy/move a directory to an existing file");
360 }
361 }
362
363 if (srcURL.isDirectory) {
364 // E.g. Copy /sdcard/myDir to /sdcard/myDir/backup
365 if (dstNativeUri.toString().startsWith(srcNativeUri.toString() + '/')) {
366 throw new InvalidModificationException("Can't copy directory into itself");
367 }
368 copyDirectory(srcFs, srcURL, destFile, move);
369 } else {
370 copyFile(srcFs, srcURL, destFile, move);
371 }
372 return makeEntryForURL(destinationURL);
373 }
374
375 @Override
376 public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
377 int offset, boolean isBinary) throws IOException, NoModificationAllowedException {
378
379 boolean append = false;
380 if (offset > 0) {
381 this.truncateFileAtURL(inputURL, offset);
382 append = true;
383 }
384
385 byte[] rawData;
386 if (isBinary) {
387 rawData = Base64.decode(data, Base64.DEFAULT);
388 } else {
389 rawData = data.getBytes(Charset.defaultCharset());
390 }
391 ByteArrayInputStream in = new ByteArrayInputStream(rawData);
392 try
393 {
394 byte buff[] = new byte[rawData.length];
395 String absolutePath = filesystemPathForURL(inputURL);
396 FileOutputStream out = new FileOutputStream(absolutePath, append);
397 try {
398 in.read(buff, 0, buff.length);
399 out.write(buff, 0, rawData.length);
400 out.flush();
401 } finally {
402 // Always close the output
403 out.close();
404 }
405 if (isPublicDirectory(absolutePath)) {
406 broadcastNewFile(Uri.fromFile(new File(absolutePath)));
407 }
408 }
409 catch (NullPointerException e)
410 {
411 // This is a bug in the Android implementation of the Java Stack
412 NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString());
413 realException.initCause(e);
414 throw realException;
415 }
416
417 return rawData.length;
418 }
419
420 private boolean isPublicDirectory(String absolutePath) {
421 // TODO: should expose a way to scan app's private files (maybe via a flag).
422 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
423 // Lollipop has a bug where SD cards are null.
424 for (File f : context.getExternalMediaDirs()) {
425 if(f != null && absolutePath.startsWith(f.getAbsolutePath())) {
426 return true;
427 }
428 }
429 }
430
431 String extPath = Environment.getExternalStorageDirectory().getAbsolutePath();
432 return absolutePath.startsWith(extPath);
433 }
434
435 /**
436 * Send broadcast of new file so files appear over MTP
437 */
438 private void broadcastNewFile(Uri nativeUri) {
439 Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, nativeUri);
440 context.sendBroadcast(intent);
441 }
442
443 @Override
444 public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException {
445 File file = new File(filesystemPathForURL(inputURL));
446
447 if (!file.exists()) {
448 throw new FileNotFoundException("File at " + inputURL.uri + " does not exist.");
449 }
450
451 RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw");
452 try {
453 if (raf.length() >= size) {
454 FileChannel channel = raf.getChannel();
455 channel.truncate(size);
456 return size;
457 }
458
459 return raf.length();
460 } finally {
461 raf.close();
462 }
463
464
465 }
466
467 @Override
468 public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
469 String path = filesystemPathForURL(inputURL);
470 File file = new File(path);
471 return file.exists();
472 }
473
474 // This is a copy & paste from CordovaResource API that is required since CordovaResourceApi
475 // has a bug pre-4.0.0.
476 // TODO: Once cordova-android@4.0.0 is released, delete this copy and make the plugin depend on
477 // 4.0.0 with an engine tag.
478 private static void copyResource(CordovaResourceApi.OpenForReadResult input, OutputStream outputStream) throws IOException {
479 try {
480 InputStream inputStream = input.inputStream;
481 if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) {
482 FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel();
483 FileChannel outChannel = ((FileOutputStream)outputStream).getChannel();
484 long offset = 0;
485 long length = input.length;
486 if (input.assetFd != null) {
487 offset = input.assetFd.getStartOffset();
488 }
489 // transferFrom()'s 2nd arg is a relative position. Need to set the absolute
490 // position first.
491 inChannel.position(offset);
492 outChannel.transferFrom(inChannel, 0, length);
493 } else {
494 final int BUFFER_SIZE = 8192;
495 byte[] buffer = new byte[BUFFER_SIZE];
496
497 for (;;) {
498 int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE);
499
500 if (bytesRead <= 0) {
501 break;
502 }
503 outputStream.write(buffer, 0, bytesRead);
504 }
505 }
506 } finally {
507 input.inputStream.close();
508 if (outputStream != null) {
509 outputStream.close();
510 }
511 }
512 }
513}