diff --git a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java index 91969103..7c101c73 100644 --- a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java @@ -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; + } + } + } diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java index 07179f27..dd057e4c 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java @@ -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 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 ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class 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 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; + } + /** diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java index 2935d7a4..48b4b096 100644 --- a/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/FileUtilsErrno.java @@ -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"); diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java index 8d099b34..bade0e1d 100644 --- a/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/FunctionErrno.java @@ -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) {