package cgeo.geocaching.utils;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.R;
import android.os.Handler;
import android.os.Message;
import android.os.StatFs;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.List;
import okhttp3.Response;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
/**
* Utility class for files
*
*/
public final class FileUtils {
public static final String HEADER_LAST_MODIFIED = "last-modified";
public static final String HEADER_ETAG = "etag";
private static final int MAX_DIRECTORY_SCAN_DEPTH = 30;
private static final String FILE_PROTOCOL = "file://";
private FileUtils() {
// utility class
}
public static void listDir(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler) {
listDirInternally(result, directory, chooser, feedBackHandler, 0);
}
private static void listDirInternally(final List<File> result, final File directory, final FileSelector chooser, final Handler feedBackHandler, final int depths) {
if (directory == null || !directory.isDirectory() || !directory.canRead()
|| result == null
|| chooser == null) {
return;
}
final File[] files = directory.listFiles();
if (ArrayUtils.isNotEmpty(files)) {
for (final File file : files) {
if (chooser.shouldEnd()) {
return;
}
if (!file.canRead()) {
continue;
}
String name = file.getName();
if (file.isFile()) {
if (chooser.isSelected(file)) {
result.add(file); // add file to list
}
} else if (file.isDirectory()) {
if (name.charAt(0) == '.') {
continue; // skip hidden directories
}
if (name.length() > 16) {
name = name.substring(0, 14) + CgeoApplication.getInstance().getString(R.string.ellipsis);
}
if (feedBackHandler != null) {
feedBackHandler.sendMessage(Message.obtain(feedBackHandler, 0, name));
}
if (depths < MAX_DIRECTORY_SCAN_DEPTH) {
listDirInternally(result, file, chooser, feedBackHandler, depths + 1); // go deeper
}
}
}
}
}
public static boolean deleteDirectory(@NonNull final File dir) {
final File[] files = dir.listFiles();
// Although we are called on an existing directory, it might have been removed concurrently
// in the meantime, for example by the user or by another cleanup task.
if (files != null) {
for (final File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
delete(file);
}
}
}
return delete(dir);
}
/**
* Moves a file/directory to a new name. Tries a rename first (faster) and falls back
* to a copy + delete. Target directories are created if needed.
*
* @param source
* source file or directory
* @param target
* target file or directory
* @return true, if successfully
*/
public static boolean move(final File source, final File target) {
if (!source.exists()) {
return false;
}
FileUtils.mkdirs(target.getParentFile());
boolean success = source.renameTo(target);
if (!success) {
// renameTo might fail across mount points, try copy/delete instead
success = FileUtils.copy(source, target);
if (success) {
FileUtils.deleteDirectory(source);
} else {
Log.w("Couldn't move " + source + " to " + target);
}
}
return success;
}
/**
* Moves a file/directory into the targetDirectory.
*
* @param source
* source file or directory
* @param targetDirectory
* target directory
* @return success true or false
*/
public static boolean moveTo(final File source, final File targetDirectory) {
return move(source, new File(targetDirectory, source.getName()));
}
/**
* Get the guessed file extension of an URL. A file extension can contain up-to 4 characters in addition to the dot.
*
* @param url
* the relative or absolute URL
* @return the file extension, including the leading dot, or the empty string if none could be determined
*/
@NonNull
public static String getExtension(@NonNull final String url) {
final String urlExt;
if (url.startsWith("data:")) {
// "data:image/png;base64,i53…" -> ".png"
urlExt = StringUtils.substringAfter(StringUtils.substringBefore(url, ";"), "/");
} else {
// "http://example.com/foo/bar.png" -> ".png"
urlExt = StringUtils.substringAfterLast(url, ".");
}
return urlExt.length() >= 1 && urlExt.length() <= 4 ? "." + urlExt : "";
}
/**
* Copy a file into another. The directory structure of target file will be created if needed.
*
* @param source
* the source file
* @param destination
* the target file
* @return true if the copy happened without error, false otherwise
*/
public static boolean copy(@NonNull final File source, @NonNull final File destination) {
try {
if (source.isDirectory()) {
org.apache.commons.io.FileUtils.copyDirectory(source, destination);
} else {
org.apache.commons.io.FileUtils.copyFile(source, destination);
}
return true;
} catch (final IOException e) {
Log.w("FileUtils.copy: could not copy file", e);
return false;
}
}
/**
* Deletes all files from a directory with the given prefix.
*
* @param directory
* The directory to remove the files from
* @param prefix
* The filename prefix
*/
public static void deleteFilesWithPrefix(@NonNull final File directory, @NonNull final String prefix) {
final FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(final File dir, @NonNull final String filename) {
return filename.startsWith(prefix);
}
};
final File[] filesToDelete = directory.listFiles(filter);
if (filesToDelete == null) {
return;
}
for (final File file : filesToDelete) {
try {
if (!delete(file)) {
Log.w("FileUtils.deleteFilesWithPrefix: Can't delete file " + file.getName());
}
} catch (final Exception e) {
Log.w("FileUtils.deleteFilesWithPrefix", e);
}
}
}
/**
* Save a stream to a file.
* <p/>
* If the response could not be saved to the file due, for example, to a network error, the file will not exist when
* this method returns.
*
* @param inputStream
* the stream whose content will be saved
* @param targetFile
* the target file, which will be created if necessary
* @return true if the operation was successful, false otherwise
*/
public static boolean saveToFile(@Nullable final InputStream inputStream, @NonNull final File targetFile) {
if (inputStream == null) {
return false;
}
File tempFile = null;
try {
tempFile = File.createTempFile("download", null, targetFile.getParentFile());
final FileOutputStream fos = new FileOutputStream(tempFile);
IOUtils.copy(inputStream, fos);
fos.close();
return tempFile.renameTo(targetFile);
} catch (final IOException e) {
Log.e("FileUtils.saveToFile", e);
deleteIgnoringFailure(tempFile);
deleteIgnoringFailure(targetFile);
} finally {
IOUtils.closeQuietly(inputStream);
}
return false;
}
@NonNull
public static File buildFile(final File base, @NonNull final String fileName, final boolean isUrl, final boolean createDirs) {
if (createDirs) {
mkdirs(base);
}
return new File(base, isUrl ? CryptUtils.md5(fileName) + getExtension(fileName) : fileName);
}
/**
* Save an HTTP response to a file.
*
* @param response
* the response whose entity content will be saved
* @param targetFile
* the target file, which will be created if necessary
* @return true if the operation was successful, false otherwise, in which case the file will not exist
*/
public static boolean saveEntityToFile(@NonNull final Response response, @NonNull final File targetFile) {
try {
final boolean saved = saveToFile(response.body().byteStream(), targetFile);
if (saved) {
saveHeader(HEADER_ETAG, response, targetFile);
saveHeader(HEADER_LAST_MODIFIED, response, targetFile);
}
return saved;
} catch (final Exception e) {
Log.e("FileUtils.saveEntityToFile", e);
}
return false;
}
private static void saveHeader(final String name, @NonNull final Response response, @NonNull final File baseFile) {
final String header = response.header(name);
final File file = filenameForHeader(baseFile, name);
if (header == null) {
deleteIgnoringFailure(file);
} else {
try {
saveToFile(new ByteArrayInputStream(header.getBytes("UTF-8")), file);
} catch (final UnsupportedEncodingException e) {
// Do not try to display the header in the log message, as our default encoding is
// likely to be UTF-8 and it will fail as well.
Log.e("FileUtils.saveHeader: unable to decode header", e);
}
}
}
@NonNull
private static File filenameForHeader(@NonNull final File baseFile, final String name) {
return new File(baseFile.getAbsolutePath() + "-" + name);
}
/**
* Get the saved header value for this file.
*
* @param baseFile
* the name of the cached resource
* @param name
* the name of the header ("etag" or "last-modified")
* @return the cached value, or <tt>null</tt> if none has been cached
*/
@Nullable
public static String getSavedHeader(@NonNull final File baseFile, final String name) {
try {
final File file = filenameForHeader(baseFile, name);
final Reader reader = new InputStreamReader(new FileInputStream(file), CharEncoding.UTF_8);
try {
// No header will be more than 256 bytes
final char[] value = new char[256];
final int count = reader.read(value);
return new String(value, 0, count);
} finally {
IOUtils.closeQuietly(reader);
}
} catch (final FileNotFoundException ignored) {
// Do nothing, the file does not exist
} catch (final Exception e) {
Log.w("could not read saved header " + name + " for " + baseFile, e);
}
return null;
}
public interface FileSelector {
boolean isSelected(File file);
boolean shouldEnd();
}
/**
* Create a unique non existing file named like the given file name. If a file with the given name already exists,
* add a number as suffix to the file name.<br>
* Example: For the file name "file.ext" this will return the first file of the list
* <ul>
* <li>file.ext</li>
* <li>file_2.ext</li>
* <li>file_3.ext</li>
* </ul>
* which does not yet exist.
*/
@NonNull
public static File getUniqueNamedFile(final File file) {
if (!file.exists()) {
return file;
}
final String baseNameAndPath = file.getPath();
final String prefix = StringUtils.substringBeforeLast(baseNameAndPath, ".") + "_";
final String extension = "." + StringUtils.substringAfterLast(baseNameAndPath, ".");
for (int i = 2; i < Integer.MAX_VALUE; i++) {
final File numbered = new File(prefix + i + extension);
if (!numbered.exists()) {
return numbered;
}
}
throw new IllegalStateException("Unable to generate a non-existing file name");
}
/**
* This usage of this method indicates that the return value of File.delete() can safely be ignored.
*/
public static void deleteIgnoringFailure(final File file) {
final boolean success = file.delete() || !file.exists();
if (!success) {
Log.i("Could not delete " + file.getAbsolutePath());
}
}
/**
* Deletes a file and logs deletion failures.
*
* @return {@code true} if this file was deleted, {@code false} otherwise.
*/
public static boolean delete(final File file) {
final boolean success = file.delete() || !file.exists();
if (!success) {
Log.e("Could not delete " + file.getAbsolutePath());
}
return success;
}
/**
* Creates the directory named by the given file, creating any missing parent directories in the process.
*
* @return {@code true} if the directory was created, {@code false} on failure or if the directory already
* existed.
*/
public static boolean mkdirs(final File file) {
final boolean success = file.mkdirs() || file.isDirectory(); // mkdirs returns false on existing directories
if (!success) {
Log.w("Could not make directories " + file.getAbsolutePath());
}
return success;
}
public static boolean writeFileUTF16(final File file, final String content) {
try {
org.apache.commons.io.FileUtils.write(file, content, CharEncoding.UTF_16);
} catch (final IOException e) {
Log.e("FileUtils.writeFileUTF16", e);
return false;
}
return true;
}
/**
* Check if the URL represents a file on the local file system.
*
* @return <tt>true</tt> if the URL scheme is <tt>file</tt>, <tt>false</tt> otherwise
*/
public static boolean isFileUrl(final String url) {
return StringUtils.startsWith(url, FILE_PROTOCOL);
}
/**
* Build an URL from a file name.
*
* @param file a local file name
* @return an URL with the <tt>file</tt> scheme
*/
@NonNull
public static String fileToUrl(final File file) {
return FILE_PROTOCOL + file.getAbsolutePath();
}
/**
* Local file name when {@link #isFileUrl(String)} is <tt>true</tt>.
*
* @return the local file
*/
@NonNull
public static File urlToFile(final String url) {
return new File(StringUtils.substring(url, FILE_PROTOCOL.length()));
}
/**
* Returns the size in bytes of a file or directory.
*/
public static long getSize(final File file) {
if (file == null || !file.exists()) {
return 0;
}
if (file.isDirectory()) {
long result = 0;
final File[] fileList = file.listFiles();
if (ArrayUtils.isNotEmpty(fileList)) {
for (final File aFileList : fileList) {
result += getSize(aFileList);
}
}
return result; // return the file size
}
return file.length();
}
/**
* Returns the available space in bytes on the mount point used by the given dir.
*/
@SuppressWarnings("deprecation")
public static long getFreeDiskSpace(final File dir) {
if (dir == null) {
return 0;
}
try {
final StatFs statFs = new StatFs(dir.getAbsolutePath());
return (long) statFs.getAvailableBlocks() * (long) statFs.getBlockSize();
} catch (final IllegalArgumentException ignored) {
// thrown if the directory isn't pointing to an external storage
}
return 0;
}
}