Added: Support for reading and writing serialized objects to files and deleting files older than x days in FileUtils

This commit is contained in:
agnostic-apollo 2021-08-20 06:36:01 +05:00
parent 7de0613617
commit 35ea19dd75
4 changed files with 278 additions and 23 deletions

View File

@ -4,6 +4,10 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DataUtils {
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
@ -172,4 +176,21 @@ public class DataUtils {
return string == null || string.isEmpty();
}
/** Get size of a serializable object. */
public static long getSerializedSize(Serializable object) {
if (object == null) return 0;
try {
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
objectOutputStream.close();
return byteOutputStream.toByteArray().length;
} catch (Exception e) {
return -1;
}
}
}

View File

@ -14,6 +14,9 @@ import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.models.errors.FunctionErrno;
import org.apache.commons.io.filefilter.AgeFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
@ -22,10 +25,15 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.LinkOption;
import java.nio.file.StandardCopyOption;
import java.util.Calendar;
import java.util.Iterator;
import java.util.regex.Pattern;
public class FileUtils {
@ -100,6 +108,29 @@ public class FileUtils {
return path;
}
/**
* Convert special characters `\/:*?"<>|` to underscore.
*
* @param fileName The name to sanitize.
* @param sanitizeWhitespaces If set to {@code true}, then white space characters ` \t\n` will be
* converted.
* @param sanitizeWhitespaces If set to {@code true}, then file name will be converted to lowe case.
* @return Returns the {@code sanitized name}.
*/
public static String sanitizeFileName(String fileName, boolean sanitizeWhitespaces, boolean toLower) {
if (fileName == null) return null;
if (sanitizeWhitespaces)
fileName = fileName.replaceAll("[\\\\/:*?\"<>| \t\n]", "_");
else
fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
if (toLower)
return fileName.toLowerCase();
else
return fileName;
}
/**
* Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
* only normalized.
@ -996,7 +1027,7 @@ public class FileUtils {
/**
* Delete regular file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@ -1005,13 +1036,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.REGULAR.getValue());
}
/**
* Delete directory file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@ -1020,13 +1051,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.DIRECTORY.getValue());
}
/**
* Delete symlink file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@ -1035,13 +1066,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
}
/**
* Delete regular, directory or symlink file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@ -1050,7 +1081,7 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS);
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileTypes.FILE_TYPE_NORMAL_FLAGS);
}
/**
@ -1065,6 +1096,8 @@ public class FileUtils {
* @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 ignoreWrongFileType The {@code boolean} that decides if it should be considered an
* error if file type is not one from {@code allowedFileTypeFlags}.
* @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
@ -1072,29 +1105,39 @@ public class FileUtils {
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, final boolean ignoreWrongFileType, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(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);
Logger.logVerbose(LOG_TAG, "Processing delete of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\"");
// 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 with error
else
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(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)
// If the file type of the file does not exist in the allowedFileTypeFlags
if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
// If wrong file type is to be ignored
if (ignoreWrongFileType) {
Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
return null;
}
// Else return with error
return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
}
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/*
@ -1209,6 +1252,77 @@ public class FileUtils {
return null;
}
/**
* Delete files under a directory older than x days.
*
* 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 label The optional label for directory to clear. This can optionally be {@code null}.
* @param filePath The {@code path} for directory to clear.
* @param dirFilter The optional filter to apply when finding subdirectories.
* If this parameter is {@code null}, subdirectories will not be included in the
* search. Use TrueFileFilter.INSTANCE to match all directories.
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
* @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 error} if deleting was not successful, otherwise {@code null}.
*/
public static Error deleteFilesOlderThanXDays(String label, final String filePath, final IOFileFilter dirFilter, int days, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFilesOlderThanXDays");
if (days < 0) return FunctionErrno.ERRNO_INVALID_PARAMETER.getError(label + "days", "deleteFilesOlderThanXDays", " It must be >= 0.");
Error error;
try {
Logger.logVerbose(LOG_TAG, "Deleting files under " + label + "directory at path \"" + filePath + "\" older than " + days + " days");
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 FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory");
}
// 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 FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "directory under which files had to be deleted", filePath);
}
// If directory exists, delete its contents
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -(days));
// AgeFileFilter seems to apply to symlink destination timestamp instead of symlink file itself
Iterator<File> filesToDelete =
org.apache.commons.io.FileUtils.iterateFiles(file, new AgeFileFilter(calendar.getTime()), dirFilter);
while (filesToDelete.hasNext()) {
File subFile = filesToDelete.next();
error = deleteFile(label + " directory sub", subFile.getAbsolutePath(), true, true, allowedFileTypeFlags);
if (error != null)
return error;
}
} catch (Exception e) {
return FileUtilsErrno.ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, days, e.getMessage());
}
return null;
}
/**
@ -1281,6 +1395,72 @@ public class FileUtils {
return null;
}
public static class ReadSerializableObjectResult {
public Error error;
public Serializable serializableObject;
ReadSerializableObjectResult(Error error, Serializable serializableObject) {
this.error = error;
this.serializableObject = serializableObject;
}
}
/**
* Read a {@link Serializable} object from file at path.
*
* @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 readObjectType The {@link Class} of the object.
* @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 error} if reading was not successful, otherwise {@code null}.
*/
@NonNull
public static <T extends Serializable> ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class<T> readObjectType, final boolean ignoreNonExistentFile) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return new ReadSerializableObjectResult(FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readSerializableObjectFromFile"), null);
Logger.logVerbose(LOG_TAG, "Reading serializable object from " + label + "file at path \"" + filePath + "\"");
T serializableObject;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"), null);
}
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If reading is to be ignored if file does not exist
if (ignoreNonExistentFile)
return new ReadSerializableObjectResult(null, null);
// Else return with error
else
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be read", filePath), null);
}
FileInputStream fileInputStream = null;
ObjectInputStream objectInputStream = null;
try {
// Read string from file
fileInputStream = new FileInputStream(filePath);
objectInputStream = new ObjectInputStream(fileInputStream);
//serializableObject = (T) objectInputStream.readObject();
serializableObject = readObjectType.cast(objectInputStream.readObject());
//Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
} catch (Exception e) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()), null);
} finally {
closeCloseable(fileInputStream);
closeCloseable(objectInputStream);
}
return new ReadSerializableObjectResult(null, serializableObject);
}
/**
* Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path.
*
@ -1288,6 +1468,7 @@ public class FileUtils {
* @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 dataString The data to write to file.
* @param append The {@code boolean} that decides if file should be appended to or not.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/
@ -1299,15 +1480,7 @@ public class FileUtils {
Error error;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
error = preWriteToFile(label, filePath);
if (error != null)
return error;
@ -1337,6 +1510,63 @@ public class FileUtils {
return null;
}
/**
* Write the {@link Serializable} {@code serializableObject} to file at path.
*
* @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 serializableObject The object to write to file.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/
public static <T extends Serializable> Error writeSerializableObjectToFile(String label, final String filePath, final T serializableObject) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeSerializableObjectToFile");
Logger.logVerbose(LOG_TAG, "Writing serializable object to " + label + "file at path \"" + filePath + "\"");
Error error;
error = preWriteToFile(label, filePath);
if (error != null)
return error;
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// Write object to file
fileOutputStream = new FileOutputStream(filePath);
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serializableObject);
objectOutputStream.flush();
} catch (Exception e) {
return FileUtilsErrno.ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
} finally {
closeCloseable(fileOutputStream);
closeCloseable(objectOutputStream);
}
return null;
}
private static Error preWriteToFile(String label, String filePath) {
Error error;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
if (error != null)
return error;
return null;
}
/**

View File

@ -56,6 +56,7 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
public static final Errno ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 304, "Deleting %1$s under directory at path \"%2$s\" old than %3$s days failed.\nException: %4$s");
@ -64,6 +65,8 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
public static final Errno ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 354, "Reading serializable object from %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 355, "Writing serializable object to %1$s at path \"%2$s\" failed.\nException: %3$s");

View File

@ -11,6 +11,7 @@ public class FunctionErrno extends Errno {
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
public static final Errno ERRNO_INVALID_PARAMETER = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is invalid.\"%3$s\"");
FunctionErrno(final String type, final int code, final String message) {