arnaucube
arnaucube/openEventsPlatformAppPublic Self-hosted repository
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}