mirror of https://github.com/termux/termux-app
1628 lines
83 KiB
Java
1628 lines
83 KiB
Java
package com.termux.shared.file;
|
|
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.system.Os;
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import com.google.common.io.RecursiveDeleteOption;
|
|
import com.termux.shared.R;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.shared.file.filesystem.FileType;
|
|
import com.termux.shared.file.filesystem.FileTypes;
|
|
import com.termux.shared.data.DataUtils;
|
|
import com.termux.shared.logger.Logger;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.BufferedWriter;
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStreamWriter;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.file.LinkOption;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.util.regex.Pattern;
|
|
|
|
public class FileUtils {
|
|
|
|
private static final String LOG_TAG = "FileUtils";
|
|
|
|
/**
|
|
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
|
*
|
|
* @param path The {@code path} to expand.
|
|
* @return Returns the {@code expand path}.
|
|
*/
|
|
public static String getExpandedTermuxPath(String path) {
|
|
if (path != null && !path.isEmpty()) {
|
|
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
|
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
|
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
|
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
|
*
|
|
* @param path The {@code path} to unexpand.
|
|
* @return Returns the {@code unexpand path}.
|
|
*/
|
|
public static String getUnExpandedTermuxPath(String path) {
|
|
if (path != null && !path.isEmpty()) {
|
|
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
|
|
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* If {@code expandPath} is enabled, then input path is first attempted to be expanded by calling
|
|
* {@link #getExpandedTermuxPath(String)}.
|
|
*
|
|
* Then if path is already an absolute path, then it is used as is to get canonical path.
|
|
* If path is not an absolute path and {code prefixForNonAbsolutePath} is not {@code null}, then
|
|
* {code prefixForNonAbsolutePath} + "/" is prefixed before path before getting canonical path.
|
|
* If path is not an absolute path and {code prefixForNonAbsolutePath} is {@code null}, then
|
|
* "/" is prefixed before path before getting canonical path.
|
|
*
|
|
* If an exception is raised to get the canonical path, then absolute path is returned.
|
|
*
|
|
* @param path The {@code path} to convert.
|
|
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
|
|
* can be set to {@code null} if non-absolute paths should
|
|
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
|
|
* will automatically do this anyways.
|
|
* @return Returns the {@code canonical path}.
|
|
*/
|
|
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
|
|
if (path == null) path = "";
|
|
|
|
if (expandPath)
|
|
path = getExpandedTermuxPath(path);
|
|
|
|
String absolutePath;
|
|
|
|
// If path is already an absolute path
|
|
if (path.startsWith("/")) {
|
|
absolutePath = path;
|
|
} else {
|
|
if (prefixForNonAbsolutePath != null)
|
|
absolutePath = prefixForNonAbsolutePath + "/" + path;
|
|
else
|
|
absolutePath = "/" + path;
|
|
}
|
|
|
|
try {
|
|
return new File(absolutePath).getCanonicalPath();
|
|
} catch(Exception e) {
|
|
}
|
|
|
|
return absolutePath;
|
|
}
|
|
|
|
/**
|
|
* Removes one or more forward slashes "//" with single slash "/"
|
|
* Removes "./"
|
|
* Removes trailing forward slash "/"
|
|
*
|
|
* @param path The {@code path} to convert.
|
|
* @return Returns the {@code normalized path}.
|
|
*/
|
|
public static String normalizePath(String path) {
|
|
if (path == null) return null;
|
|
|
|
path = path.replaceAll("/+", "/");
|
|
path = path.replaceAll("\\./", "");
|
|
|
|
if (path.endsWith("/")) {
|
|
path = path.substring(0, path.length() - 1);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
|
|
* only normalized.
|
|
*
|
|
* @param path The {@code path} to check.
|
|
* @param dirPath The {@code directory path} to check in.
|
|
* @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
|
|
* under the directory and does not equal it.
|
|
* @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
|
|
*/
|
|
public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) {
|
|
if (path == null || dirPath == null) return false;
|
|
|
|
try {
|
|
path = new File(path).getCanonicalPath();
|
|
} catch(Exception e) {
|
|
return false;
|
|
}
|
|
|
|
String normalizedDirPath = normalizePath(dirPath);
|
|
|
|
if (ensureUnder)
|
|
return !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
|
|
else
|
|
return path.startsWith(normalizedDirPath + "/");
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checks whether a regular file exists at {@code filePath}.
|
|
*
|
|
* @param filePath The {@code path} for regular file to check.
|
|
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
|
|
* finding if file exists. Check {@link #getFileType(String, boolean)}
|
|
* for details.
|
|
* @return Returns {@code true} if regular file exists, otherwise {@code false}.
|
|
*/
|
|
public static boolean regularFileExists(final String filePath, final boolean followLinks) {
|
|
return getFileType(filePath, followLinks) == FileType.REGULAR;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a directory file exists at {@code filePath}.
|
|
*
|
|
* @param filePath The {@code path} for directory file to check.
|
|
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
|
|
* finding if file exists. Check {@link #getFileType(String, boolean)}
|
|
* for details.
|
|
* @return Returns {@code true} if directory file exists, otherwise {@code false}.
|
|
*/
|
|
public static boolean directoryFileExists(final String filePath, final boolean followLinks) {
|
|
return getFileType(filePath, followLinks) == FileType.DIRECTORY;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a symlink file exists at {@code filePath}.
|
|
*
|
|
* @param filePath The {@code path} for symlink file to check.
|
|
* @return Returns {@code true} if symlink file exists, otherwise {@code false}.
|
|
*/
|
|
public static boolean symlinkFileExists(final String filePath) {
|
|
return getFileType(filePath, false) == FileType.SYMLINK;
|
|
}
|
|
|
|
/**
|
|
* Checks whether any file exists at {@code filePath}.
|
|
*
|
|
* @param filePath The {@code path} for file to check.
|
|
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
|
|
* finding if file exists. Check {@link #getFileType(String, boolean)}
|
|
* for details.
|
|
* @return Returns {@code true} if file exists, otherwise {@code false}.
|
|
*/
|
|
public static boolean fileExists(final String filePath, final boolean followLinks) {
|
|
return getFileType(filePath, followLinks) != FileType.NO_EXIST;
|
|
}
|
|
|
|
/**
|
|
* Checks the type of file that exists at {@code filePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link FileTypes#getFileType(String, boolean)}
|
|
*
|
|
* @param filePath The {@code path} for file to check.
|
|
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
|
|
* finding type. If set to {@code true}, then type of symlink target will
|
|
* be returned if file at {@code filePath} is a symlink. If set to
|
|
* {@code false}, then type of file at {@code filePath} itself will be
|
|
* returned.
|
|
* @return Returns the {@link FileType} of file.
|
|
*/
|
|
public static FileType getFileType(final String filePath, final boolean followLinks) {
|
|
return FileTypes.getFileType(filePath, followLinks);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Validate the existence and permissions of regular file at path.
|
|
*
|
|
* If the {@code parentDirPath} is not {@code null}, then setting of missing permissions will
|
|
* only be done if {@code path} is under {@code parentDirPath}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the regular file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to validate. Symlinks will not be followed.
|
|
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
|
|
* This can optionally be {@code null}. It is not canonicalized and only normalized.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
|
* automatically set defined by {@code permissionsToCheck}.
|
|
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
|
* are to be set or if they should be overridden.
|
|
* @param ignoreErrorsIfPathIsUnderParentDirPath The {@code boolean} that decides if permission
|
|
* errors are to be ignored if path is under
|
|
* {@code parentDirPath}.
|
|
* @return Returns the {@code errmsg} if path is not a regular file, or validating permissions
|
|
* failed, otherwise {@code null}.
|
|
*/
|
|
public static String validateRegularFileExistenceAndPermissions(@NonNull final Context context, String label, final String filePath, final String parentDirPath,
|
|
final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
|
|
final boolean ignoreErrorsIfPathIsUnderParentDirPath) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "regular file path", "validateRegularFileExistenceAndPermissions");
|
|
|
|
try {
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a regular file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
|
|
return context.getString(R.string.error_non_regular_file_found, label + "file");
|
|
}
|
|
|
|
boolean isPathUnderParentDirPath = false;
|
|
if (parentDirPath != null) {
|
|
// The path can only be under parent directory path
|
|
isPathUnderParentDirPath = isPathInDirPath(filePath, parentDirPath, true);
|
|
}
|
|
|
|
// If setPermissions is enabled and path is a regular file
|
|
if (setPermissions && permissionsToCheck != null && fileType == FileType.REGULAR) {
|
|
// If there is not parentDirPath restriction or path is under parentDirPath
|
|
if (parentDirPath == null || (isPathUnderParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
|
|
if (setMissingPermissionsOnly)
|
|
setMissingFilePermissions(label + "file", filePath, permissionsToCheck);
|
|
else
|
|
setFilePermissions(label + "file", filePath, permissionsToCheck);
|
|
}
|
|
}
|
|
|
|
// If path is not a regular file
|
|
// Regular files cannot be automatically created so we do not ignore if missing
|
|
if (fileType != FileType.REGULAR) {
|
|
return context.getString(R.string.error_no_regular_file_found, label + "file");
|
|
}
|
|
|
|
// If there is not parentDirPath restriction or path is not under parentDirPath or
|
|
// if permission errors must not be ignored for paths under parentDirPath
|
|
if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) {
|
|
if (permissionsToCheck != null) {
|
|
// Check if permissions are missing
|
|
return checkMissingFilePermissions(context, label + "regular", filePath, permissionsToCheck, false);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_validate_file_existence_and_permissions_failed_with_exception, label + "file", filePath, e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* Validate the existence and permissions of directory file at path.
|
|
*
|
|
* If the {@code parentDirPath} is not {@code null}, then creation of missing directory and
|
|
* setting of missing permissions will only be done if {@code path} is under
|
|
* {@code parentDirPath} or equals {@code parentDirPath}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
|
|
* @param parentDirPath The optional {@code parent directory path} to restrict operations to.
|
|
* This can optionally be {@code null}. It is not canonicalized and only normalized.
|
|
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
|
* should be created if its missing.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
|
* automatically set defined by {@code permissionsToCheck}.
|
|
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
|
* are to be set or if they should be overridden.
|
|
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
|
|
* and permission errors are to be ignored if path is
|
|
* in {@code parentDirPath}.
|
|
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
|
* error is to be ignored. This allows making an attempt to set
|
|
* executable permissions, but ignoring if it fails.
|
|
* @return Returns the {@code errmsg} if path is not a directory file, failed to create it,
|
|
* or validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String validateDirectoryFileExistenceAndPermissions(@NonNull final Context context, String label, final String filePath, final String parentDirPath, final boolean createDirectoryIfMissing,
|
|
final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
|
|
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "directory file path", "validateDirectoryExistenceAndPermissions");
|
|
|
|
try {
|
|
File file = new File(filePath);
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a directory file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
|
|
return context.getString(R.string.error_non_directory_file_found, label + "directory");
|
|
}
|
|
|
|
boolean isPathInParentDirPath = false;
|
|
if (parentDirPath != null) {
|
|
// The path can be equal to parent directory path or under it
|
|
isPathInParentDirPath = isPathInDirPath(filePath, parentDirPath, false);
|
|
}
|
|
|
|
if (createDirectoryIfMissing || setPermissions) {
|
|
// If there is not parentDirPath restriction or path is in parentDirPath
|
|
if (parentDirPath == null || (isPathInParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
|
|
// If createDirectoryIfMissing is enabled and no file exists at path, then create directory
|
|
if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) {
|
|
Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\"");
|
|
// Create directory and update fileType if successful, otherwise return with error
|
|
if (file.mkdirs())
|
|
fileType = getFileType(filePath, false);
|
|
else
|
|
return context.getString(R.string.error_creating_file_failed, label + "directory file", filePath);
|
|
}
|
|
|
|
// If setPermissions is enabled and path is a directory
|
|
if (setPermissions && permissionsToCheck != null && fileType == FileType.DIRECTORY) {
|
|
if (setMissingPermissionsOnly)
|
|
setMissingFilePermissions(label + "directory", filePath, permissionsToCheck);
|
|
else
|
|
setFilePermissions(label + "directory", filePath, permissionsToCheck);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there is not parentDirPath restriction or path is not in parentDirPath or
|
|
// if existence or permission errors must not be ignored for paths in parentDirPath
|
|
if (parentDirPath == null || !isPathInParentDirPath || !ignoreErrorsIfPathIsInParentDirPath) {
|
|
// If path is not a directory
|
|
// Directories can be automatically created so we can ignore if missing with above check
|
|
if (fileType != FileType.DIRECTORY) {
|
|
return context.getString(R.string.error_file_not_found_at_path, label + "directory", filePath);
|
|
}
|
|
|
|
if (permissionsToCheck != null) {
|
|
// Check if permissions are missing
|
|
return checkMissingFilePermissions(context, label + "directory", filePath, permissionsToCheck, ignoreIfNotExecutable);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_validate_directory_existence_and_permissions_failed_with_exception, label + "directory file", filePath, e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Create a regular file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param filePath The {@code path} for regular file to create.
|
|
* @return Returns the {@code errmsg} if path is not a regular file or failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createRegularFile(@NonNull final Context context, final String filePath) {
|
|
return createRegularFile(context, null, filePath);
|
|
}
|
|
|
|
/**
|
|
* Create a regular file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the regular file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for regular file to create.
|
|
* @return Returns the {@code errmsg} if path is not a regular file or failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createRegularFile(@NonNull final Context context, final String label, final String filePath) {
|
|
return createRegularFile(context, label, filePath,
|
|
null, false, false);
|
|
}
|
|
|
|
/**
|
|
* Create a regular file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateRegularFileExistenceAndPermissions(Context, String, String, String, String, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the regular file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for regular file to create.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
|
* automatically set defined by {@code permissionsToCheck}.
|
|
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
|
* are to be set or if they should be overridden.
|
|
* @return Returns the {@code errmsg} if path is not a regular file, failed to create it,
|
|
* or validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String createRegularFile(@NonNull final Context context, String label, final String filePath,
|
|
final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "createRegularFile");
|
|
|
|
String errmsg;
|
|
|
|
File file = new File(filePath);
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a regular file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
|
|
return context.getString(R.string.error_non_regular_file_found, label + "file");
|
|
}
|
|
|
|
// If regular file already exists
|
|
if (fileType == FileType.REGULAR) {
|
|
return null;
|
|
}
|
|
|
|
// Create the file parent directory
|
|
errmsg = createParentDirectoryFile(context, label + "regular file parent", filePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
|
|
try {
|
|
Logger.logVerbose(LOG_TAG, "Creating " + label + "regular file at path \"" + filePath + "\"");
|
|
|
|
if (!file.createNewFile())
|
|
return context.getString(R.string.error_creating_file_failed, label + "regular file", filePath);
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_creating_file_failed_with_exception, label + "regular file", filePath, e.getMessage());
|
|
}
|
|
|
|
return validateRegularFileExistenceAndPermissions(context, label, filePath,
|
|
null,
|
|
permissionsToCheck, setPermissions, setMissingPermissionsOnly,
|
|
false);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Create parent directory of file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the parent directory file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file whose parent needs to be created.
|
|
* @return Returns the {@code errmsg} if parent path is not a directory file or failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createParentDirectoryFile(@NonNull final Context context, final String label, final String filePath) {
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "createParentDirectoryFile");
|
|
|
|
File file = new File(filePath);
|
|
String fileParentPath = file.getParent();
|
|
|
|
if (fileParentPath != null)
|
|
return createDirectoryFile(context, label, fileParentPath,
|
|
null, false, false);
|
|
else
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a directory file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param filePath The {@code path} for directory file to create.
|
|
* @return Returns the {@code errmsg} if path is not a directory file or failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createDirectoryFile(@NonNull final Context context, final String filePath) {
|
|
return createDirectoryFile(context, null, filePath);
|
|
}
|
|
|
|
/**
|
|
* Create a directory file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for directory file to create.
|
|
* @return Returns the {@code errmsg} if path is not a directory file or failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createDirectoryFile(@NonNull final Context context, final String label, final String filePath) {
|
|
return createDirectoryFile(context, label, filePath,
|
|
null, false, false);
|
|
}
|
|
|
|
/**
|
|
* Create a directory file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #validateDirectoryFileExistenceAndPermissions(Context, String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for directory file to create.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
|
* automatically set defined by {@code permissionsToCheck}.
|
|
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
|
* are to be set or if they should be overridden.
|
|
* @return Returns the {@code errmsg} if path is not a directory file, failed to create it,
|
|
* or validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String createDirectoryFile(@NonNull final Context context, final String label, final String filePath,
|
|
final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
|
|
return validateDirectoryFileExistenceAndPermissions(context, label, filePath,
|
|
null, true,
|
|
permissionsToCheck, setPermissions, setMissingPermissionsOnly,
|
|
false, false);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Create a symlink file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #createSymlinkFile(Context, String, String, String, boolean, boolean, boolean)}.
|
|
*
|
|
* Dangling symlinks will be allowed.
|
|
* Symlink destination will be overwritten if it already exists but only if its a symlink.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param targetFilePath The {@code path} TO which the symlink file will be created.
|
|
* @param destFilePath The {@code path} AT which the symlink file will be created.
|
|
* @return Returns the {@code errmsg} if path is not a symlink file, failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createSymlinkFile(@NonNull final Context context, final String targetFilePath, final String destFilePath) {
|
|
return createSymlinkFile(context, null, targetFilePath, destFilePath,
|
|
true, true, true);
|
|
}
|
|
|
|
/**
|
|
* Create a symlink file at path.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #createSymlinkFile(Context, String, String, String, boolean, boolean, boolean)}.
|
|
*
|
|
* Dangling symlinks will be allowed.
|
|
* Symlink destination will be overwritten if it already exists but only if its a symlink.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the symlink file. This can optionally be {@code null}.
|
|
* @param targetFilePath The {@code path} TO which the symlink file will be created.
|
|
* @param destFilePath The {@code path} AT which the symlink file will be created.
|
|
* @return Returns the {@code errmsg} if path is not a symlink file, failed to create it,
|
|
* otherwise {@code null}.
|
|
*/
|
|
public static String createSymlinkFile(@NonNull final Context context, String label, final String targetFilePath, final String destFilePath) {
|
|
return createSymlinkFile(context, label, targetFilePath, destFilePath,
|
|
true, true, true);
|
|
}
|
|
|
|
/**
|
|
* Create a symlink file at path.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the symlink file. This can optionally be {@code null}.
|
|
* @param targetFilePath The {@code path} TO which the symlink file will be created.
|
|
* @param destFilePath The {@code path} AT which the symlink file will be created.
|
|
* @param allowDangling The {@code boolean} that decides if it should be considered an
|
|
* error if source file doesn't exist.
|
|
* @param overwrite The {@code boolean} that decides if destination file should be overwritten if
|
|
* it already exists. If set to {@code true}, then destination file will be
|
|
* deleted before symlink is created.
|
|
* @param overwriteOnlyIfDestIsASymlink The {@code boolean} that decides if overwrite should
|
|
* only be done if destination file is also a symlink.
|
|
* @return Returns the {@code errmsg} if path is not a symlink file, failed to create it,
|
|
* or validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String createSymlinkFile(@NonNull final Context context, String label, final String targetFilePath, final String destFilePath,
|
|
final boolean allowDangling, final boolean overwrite, final boolean overwriteOnlyIfDestIsASymlink) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (targetFilePath == null || targetFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "target file path", "createSymlinkFile");
|
|
if (destFilePath == null || destFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "destination file path", "createSymlinkFile");
|
|
|
|
String errmsg;
|
|
|
|
try {
|
|
File destFile = new File(destFilePath);
|
|
|
|
String targetFileAbsolutePath = targetFilePath;
|
|
// If target path is relative instead of absolute
|
|
if (!targetFilePath.startsWith("/")) {
|
|
String destFileParentPath = destFile.getParent();
|
|
if (destFileParentPath != null)
|
|
targetFileAbsolutePath = destFileParentPath + "/" + targetFilePath;
|
|
}
|
|
|
|
FileType targetFileType = getFileType(targetFileAbsolutePath, false);
|
|
FileType destFileType = getFileType(destFilePath, false);
|
|
|
|
// If target file does not exist
|
|
if (targetFileType == FileType.NO_EXIST) {
|
|
// If dangling symlink should not be allowed, then return with error
|
|
if (!allowDangling)
|
|
return context.getString(R.string.error_file_not_found_at_path, label + "symlink target file", targetFileAbsolutePath);
|
|
}
|
|
|
|
// If destination exists
|
|
if (destFileType != FileType.NO_EXIST) {
|
|
// If destination must not be overwritten
|
|
if (!overwrite) {
|
|
return null;
|
|
}
|
|
|
|
// If overwriteOnlyIfDestIsASymlink is enabled but destination file is not a symlink
|
|
if (overwriteOnlyIfDestIsASymlink && destFileType != FileType.SYMLINK)
|
|
return context.getString(R.string.error_cannot_overwrite_a_non_symlink_file_type, label + " file", destFilePath, targetFilePath, destFileType.getName());
|
|
|
|
// Delete the destination file
|
|
errmsg = deleteFile(context, label + "symlink destination", destFilePath, true);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
} else {
|
|
// Create the destination file parent directory
|
|
errmsg = createParentDirectoryFile(context, label + "symlink destination file parent", destFilePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
}
|
|
|
|
// create a symlink at destFilePath to targetFilePath
|
|
Logger.logVerbose(LOG_TAG, "Creating " + label + "symlink file at path \"" + destFilePath + "\" to \"" + targetFilePath + "\"");
|
|
Os.symlink(targetFilePath, destFilePath);
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_creating_symlink_file_failed_with_exception, label + "symlink file", destFilePath, targetFilePath, e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Copy a regular file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a regular
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to copy. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to copy.
|
|
* @param destFilePath The {@code destination path} for file to copy.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to copied doesn't exist.
|
|
* @return Returns the {@code errmsg} if copy was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String copyRegularFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
false, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Move a regular file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a regular
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to move. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to move.
|
|
* @param destFilePath The {@code destination path} for file to move.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to moved doesn't exist.
|
|
* @return Returns the {@code errmsg} if move was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String moveRegularFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
true, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Copy a directory file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a directory
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to copy. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to copy.
|
|
* @param destFilePath The {@code destination path} for file to copy.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to copied doesn't exist.
|
|
* @return Returns the {@code errmsg} if copy was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String copyDirectoryFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
false, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Move a directory file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a directory
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to move. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to move.
|
|
* @param destFilePath The {@code destination path} for file to move.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to moved doesn't exist.
|
|
* @return Returns the {@code errmsg} if move was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String moveDirectoryFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
true, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Copy a symlink file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a symlink
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to copy. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to copy.
|
|
* @param destFilePath The {@code destination path} for file to copy.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to copied doesn't exist.
|
|
* @return Returns the {@code errmsg} if copy was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String copySymlinkFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
false, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Move a symlink file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its a symlink
|
|
* file, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to move. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to move.
|
|
* @param destFilePath The {@code destination path} for file to move.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to moved doesn't exist.
|
|
* @return Returns the {@code errmsg} if move was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String moveSymlinkFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
true, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Copy a file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its the same file
|
|
* type as the source, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to copy. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to copy.
|
|
* @param destFilePath The {@code destination path} for file to copy.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to copied doesn't exist.
|
|
* @return Returns the {@code errmsg} if copy was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String copyFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
false, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Move a file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #copyOrMoveFile(Context, String, String, String, boolean, boolean, int, boolean, boolean)}.
|
|
*
|
|
* If destination file already exists, then it will be overwritten, but only if its the same file
|
|
* type as the source, otherwise an error will be returned.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to move. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to move.
|
|
* @param destFilePath The {@code destination path} for file to move.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to moved doesn't exist.
|
|
* @return Returns the {@code errmsg} if move was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String moveFile(@NonNull final Context context, final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
|
|
return copyOrMoveFile(context, label, srcFilePath, destFilePath,
|
|
true, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
|
|
true, true);
|
|
}
|
|
|
|
/**
|
|
* Copy or move a file from {@code sourceFilePath} to {@code destFilePath}.
|
|
*
|
|
* The {@code sourceFilePath} and {@code destFilePath} must be the canonical path to the source
|
|
* and destination since symlinks will not be followed.
|
|
*
|
|
* If the {@code sourceFilePath} or {@code destFilePath} is a canonical path to a directory,
|
|
* then any symlink files found under the directory will be deleted, but not their targets when
|
|
* deleting source after move and deleting destination before copy/move.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to copy or move. This can optionally be {@code null}.
|
|
* @param srcFilePath The {@code source path} for file to copy or move.
|
|
* @param destFilePath The {@code destination path} for file to copy or move.
|
|
* @param moveFile The {@code boolean} that decides if source file needs to be copied or moved.
|
|
* If set to {@code true}, then source file will be moved, otherwise it will be
|
|
* copied.
|
|
* @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
|
|
* error if source file to copied or moved doesn't exist.
|
|
* @param allowedFileTypeFlags The flags that are matched against the source file's {@link FileType}
|
|
* to see if it should be copied/moved or not. This is a safety measure
|
|
* to prevent accidental copy/move/delete of the wrong type of file,
|
|
* like a directory instead of a regular file. You can pass
|
|
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow copy/move of any file type.
|
|
* @param overwrite The {@code boolean} that decides if destination file should be overwritten if
|
|
* it already exists. If set to {@code true}, then destination file will be
|
|
* deleted before source is copied or moved.
|
|
* @param overwriteOnlyIfDestSameFileTypeAsSrc The {@code boolean} that decides if overwrite should
|
|
* only be done if destination file is also the same file
|
|
* type as the source file.
|
|
* @return Returns the {@code errmsg} if copy or move was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String copyOrMoveFile(@NonNull final Context context, String label, final String srcFilePath, final String destFilePath,
|
|
final boolean moveFile, final boolean ignoreNonExistentSrcFile, int allowedFileTypeFlags,
|
|
final boolean overwrite, final boolean overwriteOnlyIfDestSameFileTypeAsSrc) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (srcFilePath == null || srcFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "source file path", "copyOrMoveFile");
|
|
if (destFilePath == null || destFilePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "destination file path", "copyOrMoveFile");
|
|
|
|
String mode = (moveFile ? "Moving" : "Copying");
|
|
String modePast = (moveFile ? "moved" : "copied");
|
|
|
|
String errmsg;
|
|
|
|
InputStream inputStream = null;
|
|
OutputStream outputStream = null;
|
|
|
|
try {
|
|
Logger.logVerbose(LOG_TAG, mode + " " + label + "source file from \"" + srcFilePath + "\" to destination \"" + destFilePath + "\"");
|
|
|
|
File srcFile = new File(srcFilePath);
|
|
File destFile = new File(destFilePath);
|
|
|
|
FileType srcFileType = getFileType(srcFilePath, false);
|
|
FileType destFileType = getFileType(destFilePath, false);
|
|
|
|
String srcFileCanonicalPath = srcFile.getCanonicalPath();
|
|
String destFileCanonicalPath = destFile.getCanonicalPath();
|
|
|
|
// If source file does not exist
|
|
if (srcFileType == FileType.NO_EXIST) {
|
|
// If copy or move is to be ignored if source file is not found
|
|
if (ignoreNonExistentSrcFile)
|
|
return null;
|
|
// Else return with error
|
|
else
|
|
return context.getString(R.string.error_file_not_found_at_path, label + "source file", srcFilePath);
|
|
}
|
|
|
|
// If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error
|
|
if ((allowedFileTypeFlags & srcFileType.getValue()) <= 0)
|
|
return context.getString(R.string.error_file_not_an_allowed_file_type, label + "source file meant to be " + modePast, srcFilePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
|
|
|
|
// If source and destination file path are the same
|
|
if (srcFileCanonicalPath.equals(destFileCanonicalPath))
|
|
return context.getString(R.string.error_copying_or_moving_file_to_same_path, mode + " " + label + "source file", srcFilePath, destFilePath);
|
|
|
|
// If destination exists
|
|
if (destFileType != FileType.NO_EXIST) {
|
|
// If destination must not be overwritten
|
|
if (!overwrite) {
|
|
return null;
|
|
}
|
|
|
|
// If overwriteOnlyIfDestSameFileTypeAsSrc is enabled but destination file does not match source file type
|
|
if (overwriteOnlyIfDestSameFileTypeAsSrc && destFileType != srcFileType)
|
|
return context.getString(R.string.error_cannot_overwrite_a_different_file_type, label + "source file", mode.toLowerCase(), srcFilePath, destFilePath, destFileType.getName(), srcFileType.getName());
|
|
|
|
// Delete the destination file
|
|
errmsg = deleteFile(context, label + "destination file", destFilePath, true);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
}
|
|
|
|
|
|
// Copy or move source file to dest
|
|
boolean copyFile = !moveFile;
|
|
|
|
// If moveFile is true
|
|
if (moveFile) {
|
|
// We first try to rename source file to destination file to save a copy operation in case both source and destination are on the same filesystem
|
|
Logger.logVerbose(LOG_TAG, "Attempting to rename source to destination.");
|
|
|
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=358
|
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/android/system/Os.java;l=512
|
|
// Uses File.getPath() to get the path of source and destination and not the canonical path
|
|
if (!srcFile.renameTo(destFile)) {
|
|
// If destination directory is a subdirectory of the source directory
|
|
// Copying is still allowed by copyDirectory() by excluding destination directory files
|
|
if (srcFileType == FileType.DIRECTORY && destFileCanonicalPath.startsWith(srcFileCanonicalPath + File.separator))
|
|
return context.getString(R.string.error_cannot_move_directory_to_sub_directory_of_itself, label + "source directory", srcFilePath, destFilePath);
|
|
|
|
// If rename failed, then we copy
|
|
Logger.logVerbose(LOG_TAG, "Renaming " + label + "source file to destination failed, attempting to copy.");
|
|
copyFile = true;
|
|
}
|
|
}
|
|
|
|
// If moveFile is false or renameTo failed while moving
|
|
if (copyFile) {
|
|
Logger.logVerbose(LOG_TAG, "Attempting to copy source to destination.");
|
|
|
|
// Create the dest file parent directory
|
|
errmsg = createParentDirectoryFile(context, label + "dest file parent", destFilePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
|
|
if (srcFileType == FileType.DIRECTORY) {
|
|
// Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
|
|
org.apache.commons.io.FileUtils.copyDirectory(srcFile, destFile, true);
|
|
} else if (srcFileType == FileType.SYMLINK) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
|
|
} else {
|
|
// read the target for the source file and create a symlink at dest
|
|
// source file metadata will be lost
|
|
errmsg = createSymlinkFile(context, label + "dest file", Os.readlink(srcFilePath), destFilePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
}
|
|
} else {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
|
|
} else {
|
|
// Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
|
|
org.apache.commons.io.FileUtils.copyFile(srcFile, destFile, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If source file had to be moved
|
|
if (moveFile) {
|
|
// Delete the source file since copying would have succeeded
|
|
errmsg = deleteFile(context, label + "source file", srcFilePath, true);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
}
|
|
|
|
Logger.logVerbose(LOG_TAG, mode + " successful.");
|
|
}
|
|
catch (Exception e) {
|
|
return context.getString(R.string.error_copying_or_moving_file_failed_with_exception, mode + " " + label + "file", srcFilePath, destFilePath, e.getMessage());
|
|
} finally {
|
|
closeCloseable(inputStream);
|
|
closeCloseable(outputStream);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Delete regular file at path.
|
|
*
|
|
* This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to delete. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to delete.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to deleted doesn't exist.
|
|
* @return Returns the {@code errmsg} if deletion was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String deleteRegularFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) {
|
|
return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue());
|
|
}
|
|
|
|
/**
|
|
* Delete directory file at path.
|
|
*
|
|
* This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to delete. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to delete.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to deleted doesn't exist.
|
|
* @return Returns the {@code errmsg} if deletion was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String deleteDirectoryFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) {
|
|
return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue());
|
|
}
|
|
|
|
/**
|
|
* Delete symlink file at path.
|
|
*
|
|
* This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to delete. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to delete.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to deleted doesn't exist.
|
|
* @return Returns the {@code errmsg} if deletion was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String deleteSymlinkFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) {
|
|
return deleteFile(context, label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue());
|
|
}
|
|
|
|
/**
|
|
* Delete regular, directory or symlink file at path.
|
|
*
|
|
* This function is a wrapper for {@link #deleteFile(Context, String, String, boolean, int)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to delete. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to delete.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to deleted doesn't exist.
|
|
* @return Returns the {@code errmsg} if deletion was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String deleteFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile) {
|
|
return deleteFile(context, label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS);
|
|
}
|
|
|
|
/**
|
|
* Delete file at path.
|
|
*
|
|
* The {@code filePath} must be the canonical path to the file to be deleted since symlinks will
|
|
* not be followed.
|
|
* If the {@code filePath} is a canonical path to a directory, then any symlink files found under
|
|
* the directory will be deleted, but not their targets.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to delete. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to delete.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to deleted doesn't exist.
|
|
* @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
|
|
* see if it should be deleted or not. This is a safety measure to
|
|
* prevent accidental deletion of the wrong type of file, like a
|
|
* directory instead of a regular file. You can pass
|
|
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
|
|
* @return Returns the {@code errmsg} if deletion was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String deleteFile(@NonNull final Context context, String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "deleteFile");
|
|
|
|
try {
|
|
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
|
|
|
|
File file = new File(filePath);
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file does not exist
|
|
if (fileType == FileType.NO_EXIST) {
|
|
// If delete is to be ignored if file does not exist
|
|
if (ignoreNonExistentFile)
|
|
return null;
|
|
// Else return with error
|
|
else
|
|
return context.getString(R.string.error_file_not_found_at_path, label + "file meant to be deleted", filePath);
|
|
}
|
|
|
|
// If the file type of the file does not exist in the allowedFileTypeFlags, then return with error
|
|
if ((allowedFileTypeFlags & fileType.getValue()) <= 0)
|
|
return context.getString(R.string.error_file_not_an_allowed_file_type, label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
/* Try to use {@link SecureDirectoryStream} if available for safer directory
|
|
deletion, it should be available for android >= 8.0
|
|
* https://guava.dev/releases/24.1-jre/api/docs/com/google/common/io/MoreFiles.html#deleteRecursively-java.nio.file.Path-com.google.common.io.RecursiveDeleteOption...-
|
|
* https://github.com/google/guava/issues/365
|
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixSecureDirectoryStream.java
|
|
*
|
|
* MoreUtils is marked with the @Beta annotation so the API may be removed in
|
|
* future but has been there for a few years now
|
|
*/
|
|
//noinspection UnstableApiUsage
|
|
com.google.common.io.MoreFiles.deleteRecursively(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
|
|
} else {
|
|
if (fileType == FileType.DIRECTORY) {
|
|
// deleteDirectory() instead of forceDelete() gets the files list first instead of walking directory tree, so seems safer
|
|
// Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
|
|
org.apache.commons.io.FileUtils.deleteDirectory(file);
|
|
} else {
|
|
// Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
|
|
org.apache.commons.io.FileUtils.forceDelete(file);
|
|
}
|
|
}
|
|
|
|
// If file still exists after deleting it
|
|
fileType = getFileType(filePath, false);
|
|
if (fileType != FileType.NO_EXIST)
|
|
return context.getString(R.string.error_file_still_exists_after_deleting, label + "file meant to be deleted", filePath);
|
|
}
|
|
catch (Exception e) {
|
|
return context.getString(R.string.error_deleting_file_failed_with_exception, label + "file", filePath, e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Clear contents of directory at path without deleting the directory. If directory does not exist
|
|
* it will be created automatically.
|
|
*
|
|
* This function is a wrapper for
|
|
* {@link #clearDirectory(Context, String, String)}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param filePath The {@code path} for directory to clear.
|
|
* @return Returns the {@code errmsg} if clearing was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String clearDirectory(Context context, String filePath) {
|
|
return clearDirectory(context, null, filePath);
|
|
}
|
|
|
|
/**
|
|
* Clear contents of directory at path without deleting the directory. If directory does not exist
|
|
* it will be created automatically.
|
|
*
|
|
* The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
|
|
* Any symlink files found under the directory will be deleted, but not their targets.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for directory to clear. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for directory to clear.
|
|
* @return Returns the {@code errmsg} if clearing was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String clearDirectory(@NonNull final Context context, String label, final String filePath) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "clearDirectory");
|
|
|
|
String errmsg;
|
|
|
|
try {
|
|
Logger.logVerbose(LOG_TAG, "Clearing " + label + "directory at path \"" + filePath + "\"");
|
|
|
|
File file = new File(filePath);
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a directory file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
|
|
return context.getString(R.string.error_non_directory_file_found, label + "directory");
|
|
}
|
|
|
|
// If directory exists, clear its contents
|
|
if (fileType == FileType.DIRECTORY) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
//noinspection UnstableApiUsage
|
|
com.google.common.io.MoreFiles.deleteDirectoryContents(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
|
|
} else {
|
|
// Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
|
|
org.apache.commons.io.FileUtils.cleanDirectory(new File(filePath));
|
|
}
|
|
}
|
|
// Else create it
|
|
else {
|
|
errmsg = createDirectoryFile(context, label, filePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
}
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_clearing_directory_failed_with_exception, label + "directory", filePath, e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Read a {@link String} from file at path with a specific {@link Charset} into {@code dataString}.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to read. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to read.
|
|
* @param charset The {@link Charset} of the file. If this is {@code null},
|
|
* * then default {@link Charset} will be used.
|
|
* @param dataStringBuilder The {@code StringBuilder} to read data into.
|
|
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
|
|
* error if file to read doesn't exist.
|
|
* @return Returns the {@code errmsg} if reading was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String readStringFromFile(@NonNull final Context context, String label, final String filePath, Charset charset, @NonNull final StringBuilder dataStringBuilder, final boolean ignoreNonExistentFile) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "readStringFromFile");
|
|
|
|
Logger.logVerbose(LOG_TAG, "Reading string from " + label + "file at path \"" + filePath + "\"");
|
|
|
|
String errmsg;
|
|
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a regular file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
|
|
return context.getString(R.string.error_non_regular_file_found, label + "file");
|
|
}
|
|
|
|
// If file does not exist
|
|
if (fileType == FileType.NO_EXIST) {
|
|
// If reading is to be ignored if file does not exist
|
|
if (ignoreNonExistentFile)
|
|
return null;
|
|
// Else return with error
|
|
else
|
|
return context.getString(R.string.error_file_not_found_at_path, label + "file meant to be read", filePath);
|
|
}
|
|
|
|
if (charset == null) charset = Charset.defaultCharset();
|
|
|
|
// Check if charset is supported
|
|
errmsg = isCharsetSupported(context, charset);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
|
|
FileInputStream fileInputStream = null;
|
|
BufferedReader bufferedReader = null;
|
|
try {
|
|
// Read string from file
|
|
fileInputStream = new FileInputStream(filePath);
|
|
bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, charset));
|
|
|
|
String receiveString;
|
|
|
|
boolean firstLine = true;
|
|
while ((receiveString = bufferedReader.readLine()) != null ) {
|
|
if (!firstLine) dataStringBuilder.append("\n"); else firstLine = false;
|
|
dataStringBuilder.append(receiveString);
|
|
}
|
|
|
|
Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES, true, false, true), "-"));
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_reading_string_to_file_failed_with_exception, label + "file", filePath, e.getMessage());
|
|
} finally {
|
|
closeCloseable(fileInputStream);
|
|
closeCloseable(bufferedReader);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for file to write. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to write.
|
|
* @param charset The {@link Charset} of the {@code dataString}. If this is {@code null},
|
|
* then default {@link Charset} will be used.
|
|
* @param append The {@code boolean} that decides if file should be appended to or not.
|
|
* @return Returns the {@code errmsg} if writing was not successful, otherwise {@code null}.
|
|
*/
|
|
public static String writeStringToFile(@NonNull final Context context, String label, final String filePath, Charset charset, final String dataString, final boolean append) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "writeStringToFile");
|
|
|
|
Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("Writing string to " + label + "file at path \"" + filePath + "\"", DataUtils.getTruncatedCommandOutput(dataString, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES, true, false, true), "-"));
|
|
|
|
String errmsg;
|
|
|
|
FileType fileType = getFileType(filePath, false);
|
|
|
|
// If file exists but not a regular file
|
|
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
|
|
return context.getString(R.string.error_non_regular_file_found, label + "file");
|
|
}
|
|
|
|
// Create the file parent directory
|
|
errmsg = createParentDirectoryFile(context, label + "file parent", filePath);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
|
|
if (charset == null) charset = Charset.defaultCharset();
|
|
|
|
// Check if charset is supported
|
|
errmsg = isCharsetSupported(context, charset);
|
|
if (errmsg != null)
|
|
return errmsg;
|
|
|
|
FileOutputStream fileOutputStream = null;
|
|
BufferedWriter bufferedWriter = null;
|
|
try {
|
|
// Write string to file
|
|
fileOutputStream = new FileOutputStream(filePath, append);
|
|
bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream, charset));
|
|
|
|
bufferedWriter.write(dataString);
|
|
bufferedWriter.flush();
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_writing_string_to_file_failed_with_exception, label + "file", filePath, e.getMessage());
|
|
} finally {
|
|
closeCloseable(fileOutputStream);
|
|
closeCloseable(bufferedWriter);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check if a specific {@link Charset} is supported.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param charset The {@link Charset} to check.
|
|
* @return Returns the {@code errmsg} if charset is not supported or failed to check it, otherwise {@code null}.
|
|
*/
|
|
public static String isCharsetSupported(@NonNull final Context context, final Charset charset) {
|
|
if (charset == null) return context.getString(R.string.error_null_or_empty_parameter, "charset", "isCharsetSupported");
|
|
|
|
try {
|
|
if (!Charset.isSupported(charset.name())) {
|
|
return context.getString(R.string.error_unsupported_charset, charset.name());
|
|
}
|
|
} catch (Exception e) {
|
|
return context.getString(R.string.error_checking_if_charset_supported_failed, charset.name(), e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Close a {@link Closeable} object if not {@code null} and ignore any exceptions raised.
|
|
*
|
|
* @param closeable The {@link Closeable} object to close.
|
|
*/
|
|
public static void closeCloseable(final Closeable closeable) {
|
|
if (closeable != null) {
|
|
try {
|
|
closeable.close();
|
|
}
|
|
catch (IOException e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
|
|
* will be removed.
|
|
*
|
|
* @param filePath The {@code path} for file to set permissions to.
|
|
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
*/
|
|
public static void setFilePermissions(final String filePath, final String permissionsToSet) {
|
|
setFilePermissions(null, filePath, permissionsToSet);
|
|
}
|
|
|
|
/**
|
|
* Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
|
|
* will be removed.
|
|
*
|
|
* @param label The optional label for the file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to set permissions to.
|
|
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
*/
|
|
public static void setFilePermissions(String label, final String filePath, final String permissionsToSet) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return;
|
|
|
|
if (!isValidPermissionString(permissionsToSet)) {
|
|
Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setFilePermissions: \"" + permissionsToSet + "\"");
|
|
return;
|
|
}
|
|
|
|
File file = new File(filePath);
|
|
|
|
if (permissionsToSet.contains("r")) {
|
|
if (!file.canRead()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting read permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setReadable(true);
|
|
}
|
|
} else {
|
|
if (file.canRead()) {
|
|
Logger.logVerbose(LOG_TAG, "Removing read permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setReadable(false);
|
|
}
|
|
}
|
|
|
|
|
|
if (permissionsToSet.contains("w")) {
|
|
if (!file.canWrite()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting write permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setWritable(true);
|
|
}
|
|
} else {
|
|
if (file.canWrite()) {
|
|
Logger.logVerbose(LOG_TAG, "Removing write permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setWritable(false);
|
|
}
|
|
}
|
|
|
|
|
|
if (permissionsToSet.contains("x")) {
|
|
if (!file.canExecute()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting execute permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setExecutable(true);
|
|
}
|
|
} else {
|
|
if (file.canExecute()) {
|
|
Logger.logVerbose(LOG_TAG, "Removing execute permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setExecutable(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
|
|
* will not be removed.
|
|
*
|
|
* @param filePath The {@code path} for file to set permissions to.
|
|
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
*/
|
|
public static void setMissingFilePermissions(final String filePath, final String permissionsToSet) {
|
|
setMissingFilePermissions(null, filePath, permissionsToSet);
|
|
}
|
|
|
|
/**
|
|
* Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
|
|
* will not be removed.
|
|
*
|
|
* @param label The optional label for the file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to set permissions to.
|
|
* @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
*/
|
|
public static void setMissingFilePermissions(String label, final String filePath, final String permissionsToSet) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return;
|
|
|
|
if (!isValidPermissionString(permissionsToSet)) {
|
|
Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setMissingFilePermissions: \"" + permissionsToSet + "\"");
|
|
return;
|
|
}
|
|
|
|
File file = new File(filePath);
|
|
|
|
if (permissionsToSet.contains("r") && !file.canRead()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting missing read permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setReadable(true);
|
|
}
|
|
|
|
if (permissionsToSet.contains("w") && !file.canWrite()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting missing write permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setWritable(true);
|
|
}
|
|
|
|
if (permissionsToSet.contains("x") && !file.canExecute()) {
|
|
Logger.logVerbose(LOG_TAG, "Setting missing execute permissions for " + label + "file at path \"" + filePath + "\"");
|
|
file.setExecutable(true);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checking missing permissions for file at path.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param filePath The {@code path} for file to check permissions for.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
|
* error is to be ignored.
|
|
* @return Returns the {@code errmsg} if validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String checkMissingFilePermissions(@NonNull final Context context, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
|
|
return checkMissingFilePermissions(context, null, filePath, permissionsToCheck, ignoreIfNotExecutable);
|
|
}
|
|
|
|
/**
|
|
* Checking missing permissions for file at path.
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @param label The optional label for the file. This can optionally be {@code null}.
|
|
* @param filePath The {@code path} for file to check permissions for.
|
|
* @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
|
|
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
|
* error is to be ignored.
|
|
* @return Returns the {@code errmsg} if validating permissions failed, otherwise {@code null}.
|
|
*/
|
|
public static String checkMissingFilePermissions(@NonNull final Context context, String label, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
|
|
label = (label == null ? "" : label + " ");
|
|
if (filePath == null || filePath.isEmpty()) return context.getString(R.string.error_null_or_empty_parameter, label + "file path", "checkMissingFilePermissions");
|
|
|
|
if (!isValidPermissionString(permissionsToCheck)) {
|
|
Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
|
|
return context.getString(R.string.error_invalid_file_permissions_string_to_check);
|
|
}
|
|
|
|
File file = new File(filePath);
|
|
|
|
// If file is not readable
|
|
if (permissionsToCheck.contains("r") && !file.canRead()) {
|
|
return context.getString(R.string.error_file_not_readable, label + "file");
|
|
}
|
|
|
|
// If file is not writable
|
|
if (permissionsToCheck.contains("w") && !file.canWrite()) {
|
|
return context.getString(R.string.error_file_not_writable, label + "file");
|
|
}
|
|
// If file is not executable
|
|
// This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
|
|
else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
|
|
return context.getString(R.string.error_file_not_executable, label + "file");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checks whether string exactly matches the 3 character permission string that
|
|
* contains the "r", "w", "x" or "-" in-order.
|
|
*
|
|
* @param string The {@link String} to check.
|
|
* @return Returns {@code true} if string exactly matches a permission string, otherwise {@code false}.
|
|
*/
|
|
public static boolean isValidPermissionString(final String string) {
|
|
if (string == null || string.isEmpty()) return false;
|
|
return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
|
|
}
|
|
|
|
}
|