termux-app

Форк
0
2044 строки · 102.1 Кб
1
package com.termux.shared.file;
2

3
import android.os.Build;
4
import android.system.Os;
5

6
import androidx.annotation.NonNull;
7
import androidx.annotation.Nullable;
8

9
import com.google.common.io.RecursiveDeleteOption;
10
import com.termux.shared.file.filesystem.FileType;
11
import com.termux.shared.file.filesystem.FileTypes;
12
import com.termux.shared.data.DataUtils;
13
import com.termux.shared.logger.Logger;
14
import com.termux.shared.errors.Errno;
15
import com.termux.shared.errors.Error;
16
import com.termux.shared.errors.FunctionErrno;
17

18
import org.apache.commons.io.filefilter.AgeFileFilter;
19
import org.apache.commons.io.filefilter.IOFileFilter;
20

21
import java.io.BufferedReader;
22
import java.io.BufferedWriter;
23
import java.io.Closeable;
24
import java.io.File;
25
import java.io.FileInputStream;
26
import java.io.FileOutputStream;
27
import java.io.IOException;
28
import java.io.InputStreamReader;
29
import java.io.ObjectInputStream;
30
import java.io.ObjectOutputStream;
31
import java.io.OutputStreamWriter;
32
import java.io.Serializable;
33
import java.nio.charset.Charset;
34
import java.nio.file.LinkOption;
35
import java.nio.file.StandardCopyOption;
36
import java.util.Arrays;
37
import java.util.Calendar;
38
import java.util.Collections;
39
import java.util.Iterator;
40
import java.util.List;
41
import java.util.regex.Pattern;
42

43
public class FileUtils {
44

45
    /** Required file permissions for the executable file for app usage. Executable file must have read and execute permissions */
46
    public static final String APP_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
47
    /** Required file permissions for the working directory for app usage. Working directory must have read and write permissions.
48
     * Execute permissions should be attempted to be set, but ignored if they are missing */
49
    public static final String APP_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
50

51
    private static final String LOG_TAG = "FileUtils";
52

53
    /**
54
     * Get canonical path.
55
     *
56
     * If path is already an absolute path, then it is used as is to get canonical path.
57
     * If path is not an absolute path and {code prefixForNonAbsolutePath} is not {@code null}, then
58
     * {code prefixForNonAbsolutePath} + "/" is prefixed before path before getting canonical path.
59
     * If path is not an absolute path and {code prefixForNonAbsolutePath} is {@code null}, then
60
     * "/" is prefixed before path before getting canonical path.
61
     *
62
     * If an exception is raised to get the canonical path, then absolute path is returned.
63
     *
64
     * @param path The {@code path} to convert.
65
     * @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
66
     *                                 can be set to {@code null} if non-absolute paths should
67
     *                                 be prefixed with "/". The call to {@link File#getCanonicalPath()}
68
     *                                 will automatically do this anyways.
69
     * @return Returns the {@code canonical path}.
70
     */
71
    public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath) {
72
        if (path == null) path = "";
73

74
        String absolutePath;
75

76
        // If path is already an absolute path
77
        if (path.startsWith("/")) {
78
            absolutePath = path;
79
        } else {
80
            if (prefixForNonAbsolutePath != null)
81
                absolutePath = prefixForNonAbsolutePath + "/" + path;
82
            else
83
                absolutePath = "/" + path;
84
        }
85

86
        try {
87
            return new File(absolutePath).getCanonicalPath();
88
        } catch(Exception e) {
89
        }
90

91
        return absolutePath;
92
    }
93

94
    /**
95
     * Removes one or more forward slashes "//" with single slash "/"
96
     * Removes "./"
97
     * Removes trailing forward slash "/"
98
     *
99
     * @param path The {@code path} to convert.
100
     * @return Returns the {@code normalized path}.
101
     */
102
    @Nullable
103
    public static String normalizePath(String path) {
104
        if (path == null) return null;
105

106
        path = path.replaceAll("/+", "/");
107
        path = path.replaceAll("\\./", "");
108

109
        if (path.endsWith("/")) {
110
            path = path.replaceAll("/+$", "");
111
        }
112

113
        return path;
114
    }
115

116
    /**
117
     * Convert special characters `\/:*?"<>|` to underscore.
118
     *
119
     * @param fileName The name to sanitize.
120
     * @param sanitizeWhitespaces If set to {@code true}, then white space characters ` \t\n` will be
121
     *                            converted.
122
     * @param toLower If set to {@code true}, then file name will be converted to lower case.
123
     * @return Returns the {@code sanitized name}.
124
     */
125
    public static String sanitizeFileName(String fileName, boolean sanitizeWhitespaces, boolean toLower) {
126
        if (fileName == null) return null;
127

128
        if (sanitizeWhitespaces)
129
            fileName = fileName.replaceAll("[\\\\/:*?\"<>| \t\n]", "_");
130
        else
131
            fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
132

133
        if (toLower)
134
            return fileName.toLowerCase();
135
        else
136
            return fileName;
137
    }
138

139
    /**
140
     * Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
141
     * only normalized.
142
     *
143
     * @param path The {@code path} to check.
144
     * @param dirPath The {@code directory path} to check in.
145
     * @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
146
     *                    under the directory and does not equal it.
147
     * @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
148
     */
149
    public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) {
150
        return isPathInDirPaths(path, Collections.singletonList(dirPath), ensureUnder);
151
    }
152

153
    /**
154
     * Determines whether path is in one of the {@code dirPaths}. The {@code dirPaths} are not
155
     * canonicalized and only normalized.
156
     *
157
     * @param path The {@code path} to check.
158
     * @param dirPaths The {@code directory paths} to check in.
159
     * @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
160
     *                    under the directories and does not equal it.
161
     * @return Returns {@code true} if path in {@code dirPaths}, otherwise returns {@code false}.
162
     */
163
    public static boolean isPathInDirPaths(String path, final List<String> dirPaths, final boolean ensureUnder) {
164
        if (path == null || path.isEmpty() || dirPaths == null || dirPaths.size() < 1) return false;
165

166
        try {
167
            path = new File(path).getCanonicalPath();
168
        } catch(Exception e) {
169
            return false;
170
        }
171

172
        boolean isPathInDirPaths;
173

174
        for (String dirPath : dirPaths) {
175
            String normalizedDirPath = normalizePath(dirPath);
176

177
            if (ensureUnder)
178
                isPathInDirPaths = !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
179
            else
180
                isPathInDirPaths = path.startsWith(normalizedDirPath + "/");
181

182
            if (isPathInDirPaths) return true;
183
        }
184

185
        return false;
186
    }
187

188

189

190
    /**
191
     * Validate that directory is empty or contains only files in {@code ignoredSubFilePaths}.
192
     *
193
     * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
194
     * is not considered empty.
195
     *
196
     * @param label The optional label for directory to check. This can optionally be {@code null}.
197
     * @param filePath The {@code path} for directory to check.
198
     * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
199
     *                            Validation is done for the paths.
200
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
201
     *                              error if file to be checked doesn't exist.
202
     * @return Returns {@code null} if directory is empty or contains only files in {@code ignoredSubFilePaths}.
203
     * Returns {@code FileUtilsErrno#ERRNO_NON_EMPTY_DIRECTORY_FILE} if a file was found that did not
204
     * exist in the {@code ignoredSubFilePaths}, otherwise returns an appropriate {@code error} if
205
     * checking was not successful.
206
     */
207
    public static Error validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String label, String filePath,
208
                                                                              final List<String> ignoredSubFilePaths,
209
                                                                              final boolean ignoreNonExistentFile) {
210
        label = (label == null || label.isEmpty() ? "" : label + " ");
211
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "isDirectoryFileEmptyOrOnlyContainsSpecificFiles");
212

213
        try {
214
            File file = new File(filePath);
215
            FileType fileType = getFileType(filePath, false);
216

217
            // If file exists but not a directory file
218
            if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
219
                return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
220
            }
221

222
            // If file does not exist
223
            if (fileType == FileType.NO_EXIST) {
224
                // If checking is to be ignored if file does not exist
225
                if (ignoreNonExistentFile)
226
                    return null;
227
                else {
228
                    label += "directory to check if is empty or only contains specific files";
229
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
230
                }
231
            }
232

233
            File[] subFiles = file.listFiles();
234
            if (subFiles == null || subFiles.length == 0)
235
                return null;
236

237
            // If sub files exists but no file should be ignored
238
            if (ignoredSubFilePaths == null || ignoredSubFilePaths.size() == 0)
239
                return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
240

241
            // If a sub file does not exist in ignored file path
242
            if (nonIgnoredSubFileExists(subFiles, ignoredSubFilePaths)) {
243
                return FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE.getError(label, filePath);
244
            }
245

246
        } catch (Exception e) {
247
            return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage());
248
        }
249

250
        return null;
251
    }
252

253
    /**
254
     * Check if {@code subFiles} contains contains a file not in {@code ignoredSubFilePaths}.
255
     *
256
     * If parent path of an ignored file exists, but ignored file itself does not exist, then directory
257
     * is not considered empty.
258
     *
259
     * This function should ideally not be called by itself but through
260
     * {@link #validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(String, String, List, boolean)}.
261
     *
262
     * @param subFiles The list of files of a directory to check.
263
     * @param ignoredSubFilePaths The list of absolute file paths under {@code filePath} dir.
264
     *                            Validation is done for the paths.
265
     * @return Returns {@code true} if a file was found that did not exist in the {@code ignoredSubFilePaths},
266
     * otherwise  {@code false}.
267
     */
268
    public static boolean nonIgnoredSubFileExists(File[] subFiles, @NonNull List<String> ignoredSubFilePaths) {
269
        if (subFiles == null || subFiles.length == 0) return false;
270

271
        String subFilePath;
272
        for (File subFile : subFiles) {
273
            subFilePath = subFile.getAbsolutePath();
274
            // If sub file does not exist in ignored sub file paths
275
            if (!ignoredSubFilePaths.contains(subFilePath)) {
276
                boolean isParentPath = false;
277
                for (String ignoredSubFilePath : ignoredSubFilePaths) {
278
                    if (ignoredSubFilePath.startsWith(subFilePath + "/") && fileExists(ignoredSubFilePath, false)) {
279
                        isParentPath = true;
280
                        break;
281
                    }
282
                }
283
                // If sub file is not a parent of any existing ignored sub file paths
284
                if (!isParentPath) {
285
                    return true;
286
                }
287
            }
288
                
289
            if (getFileType(subFilePath, false) == FileType.DIRECTORY) {
290
                // If non ignored sub file found, then early exit, otherwise continue looking
291
                if (nonIgnoredSubFileExists(subFile.listFiles(), ignoredSubFilePaths))
292
                     return true;
293
            }
294
        }
295

296
        return false;
297
    }
298

299

300

301
    /**
302
     * Checks whether a regular file exists at {@code filePath}.
303
     *
304
     * @param filePath The {@code path} for regular file to check.
305
     * @param followLinks The {@code boolean} that decides if symlinks will be followed while
306
     *                       finding if file exists. Check {@link #getFileType(String, boolean)}
307
     *                       for details.
308
     * @return Returns {@code true} if regular file exists, otherwise {@code false}.
309
     */
310
    public static boolean regularFileExists(final String filePath, final boolean followLinks) {
311
        return getFileType(filePath, followLinks) == FileType.REGULAR;
312
    }
313

314
    /**
315
     * Checks whether a directory file exists at {@code filePath}.
316
     *
317
     * @param filePath The {@code path} for directory file to check.
318
     * @param followLinks The {@code boolean} that decides if symlinks will be followed while
319
     *                       finding if file exists. Check {@link #getFileType(String, boolean)}
320
     *                       for details.
321
     * @return Returns {@code true} if directory file exists, otherwise {@code false}.
322
     */
323
    public static boolean directoryFileExists(final String filePath, final boolean followLinks) {
324
        return getFileType(filePath, followLinks) == FileType.DIRECTORY;
325
    }
326

327
    /**
328
     * Checks whether a symlink file exists at {@code filePath}.
329
     *
330
     * @param filePath The {@code path} for symlink file to check.
331
     * @return Returns {@code true} if symlink file exists, otherwise {@code false}.
332
     */
333
    public static boolean symlinkFileExists(final String filePath) {
334
        return getFileType(filePath, false) == FileType.SYMLINK;
335
    }
336

337
    /**
338
     * Checks whether a regular or directory file exists at {@code filePath}.
339
     *
340
     * @param filePath The {@code path} for regular file to check.
341
     * @param followLinks The {@code boolean} that decides if symlinks will be followed while
342
     *                       finding if file exists. Check {@link #getFileType(String, boolean)}
343
     *                       for details.
344
     * @return Returns {@code true} if regular or directory file exists, otherwise {@code false}.
345
     */
346
    public static boolean regularOrDirectoryFileExists(final String filePath, final boolean followLinks) {
347
        FileType fileType = getFileType(filePath, followLinks);
348
        return fileType == FileType.REGULAR || fileType == FileType.DIRECTORY;
349
    }
350

351
    /**
352
     * Checks whether any file exists at {@code filePath}.
353
     *
354
     * @param filePath The {@code path} for file to check.
355
     * @param followLinks The {@code boolean} that decides if symlinks will be followed while
356
     *                       finding if file exists. Check {@link #getFileType(String, boolean)}
357
     *                       for details.
358
     * @return Returns {@code true} if file exists, otherwise {@code false}.
359
     */
360
    public static boolean fileExists(final String filePath, final boolean followLinks) {
361
        return getFileType(filePath, followLinks) != FileType.NO_EXIST;
362
    }
363

364
    /**
365
     * Get the type of file that exists at {@code filePath}.
366
     *
367
     * This function is a wrapper for
368
     * {@link FileTypes#getFileType(String, boolean)}
369
     *
370
     * @param filePath The {@code path} for file to check.
371
     * @param followLinks The {@code boolean} that decides if symlinks will be followed while
372
     *                       finding type. If set to {@code true}, then type of symlink target will
373
     *                       be returned if file at {@code filePath} is a symlink. If set to
374
     *                       {@code false}, then type of file at {@code filePath} itself will be
375
     *                       returned.
376
     * @return Returns the {@link FileType} of file.
377
     */
378
    @NonNull
379
    public static FileType getFileType(final String filePath, final boolean followLinks) {
380
        return FileTypes.getFileType(filePath, followLinks);
381
    }
382

383

384

385
    /**
386
     * Validate the existence and permissions of regular file at path.
387
     *
388
     * If the {@code parentDirPath} is not {@code null}, then setting of missing permissions will
389
     * only be done if {@code path} is under {@code parentDirPath}.
390
     *
391
     * @param label The optional label for the regular file. This can optionally be {@code null}.
392
     * @param filePath The {@code path} for file to validate. Symlinks will not be followed.
393
     * @param parentDirPath The optional {@code parent directory path} to restrict operations to.
394
     *                      This can optionally be {@code null}. It is not canonicalized and only normalized.
395
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
396
     * @param setPermissions The {@code boolean} that decides if permissions are to be
397
     *                              automatically set defined by {@code permissionsToCheck}.
398
     * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
399
     *                                  are to be set or if they should be overridden.
400
     * @param ignoreErrorsIfPathIsUnderParentDirPath The {@code boolean} that decides if permission
401
     *                                               errors are to be ignored if path is under
402
     *                                               {@code parentDirPath}.
403
     * @return Returns the {@code error} if path is not a regular file, or validating permissions
404
     * failed, otherwise {@code null}.
405
     */
406
    public static Error validateRegularFileExistenceAndPermissions(String label, final String filePath, final String parentDirPath,
407
                                                                   final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
408
                                                                   final boolean ignoreErrorsIfPathIsUnderParentDirPath) {
409
        label = (label == null || label.isEmpty() ? "" : label + " ");
410
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "regular file path", "validateRegularFileExistenceAndPermissions");
411

412
        try {
413
            FileType fileType = getFileType(filePath, false);
414

415
            // If file exists but not a regular file
416
            if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
417
                return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
418
            }
419

420
            boolean isPathUnderParentDirPath = false;
421
            if (parentDirPath != null) {
422
                // The path can only be under parent directory path
423
                isPathUnderParentDirPath = isPathInDirPath(filePath, parentDirPath, true);
424
            }
425

426
            // If setPermissions is enabled and path is a regular file
427
            if (setPermissions && permissionsToCheck != null && fileType == FileType.REGULAR) {
428
                // If there is not parentDirPath restriction or path is under parentDirPath
429
                if (parentDirPath == null || (isPathUnderParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
430
                    if (setMissingPermissionsOnly)
431
                        setMissingFilePermissions(label + "file", filePath, permissionsToCheck);
432
                    else
433
                        setFilePermissions(label + "file", filePath, permissionsToCheck);
434
                }
435
            }
436

437
            // If path is not a regular file
438
            // Regular files cannot be automatically created so we do not ignore if missing
439
            if (fileType != FileType.REGULAR) {
440
                label += "regular file";
441
                return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
442
            }
443

444
            // If there is not parentDirPath restriction or path is not under parentDirPath or
445
            // if permission errors must not be ignored for paths under parentDirPath
446
            if (parentDirPath == null || !isPathUnderParentDirPath || !ignoreErrorsIfPathIsUnderParentDirPath) {
447
                if (permissionsToCheck != null) {
448
                    // Check if permissions are missing
449
                    return checkMissingFilePermissions(label + "regular", filePath, permissionsToCheck, false);
450
                }
451
            }
452
        } catch (Exception e) {
453
            return FileUtilsErrno.ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
454
        }
455

456
        return null;
457

458
    }
459

460
    /**
461
     * Validate the existence and permissions of directory file at path.
462
     *
463
     * If the {@code parentDirPath} is not {@code null}, then creation of missing directory and
464
     * setting of missing permissions will only be done if {@code path} is under
465
     * {@code parentDirPath} or equals {@code parentDirPath}.
466
     *
467
     * @param label The optional label for the directory file. This can optionally be {@code null}.
468
     * @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
469
     * @param parentDirPath The optional {@code parent directory path} to restrict operations to.
470
     *                      This can optionally be {@code null}. It is not canonicalized and only normalized.
471
     * @param createDirectoryIfMissing The {@code boolean} that decides if directory file
472
     *                                 should be created if its missing.
473
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
474
     * @param setPermissions The {@code boolean} that decides if permissions are to be
475
     *                              automatically set defined by {@code permissionsToCheck}.
476
     * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
477
     *                                  are to be set or if they should be overridden.
478
     * @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
479
     *                                  and permission errors are to be ignored if path is
480
     *                                  in {@code parentDirPath}.
481
     * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
482
     *                              error is to be ignored. This allows making an attempt to set
483
     *                              executable permissions, but ignoring if it fails.
484
     * @return Returns the {@code error} if path is not a directory file, failed to create it,
485
     * or validating permissions failed, otherwise {@code null}.
486
     */
487
    public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final String parentDirPath, final boolean createDirectoryIfMissing,
488
                                                                     final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly,
489
                                                                     final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
490
        label = (label == null || label.isEmpty() ? "" : label + " ");
491
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "directory file path", "validateDirectoryExistenceAndPermissions");
492

493
        try {
494
            File file = new File(filePath);
495
            FileType fileType = getFileType(filePath, false);
496

497
            // If file exists but not a directory file
498
            if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
499
                return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
500
            }
501

502
            boolean isPathInParentDirPath = false;
503
            if (parentDirPath != null) {
504
                // The path can be equal to parent directory path or under it
505
                isPathInParentDirPath = isPathInDirPath(filePath, parentDirPath, false);
506
            }
507

508
            if (createDirectoryIfMissing || setPermissions) {
509
                // If there is not parentDirPath restriction or path is in parentDirPath
510
                if (parentDirPath == null || (isPathInParentDirPath && getFileType(parentDirPath, false) == FileType.DIRECTORY)) {
511
                    // If createDirectoryIfMissing is enabled and no file exists at path, then create directory
512
                    if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) {
513
                        Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\"");
514
                        // Create directory and update fileType if successful, otherwise return with error
515
                        // It "might" be possible that mkdirs returns false even though directory was created
516
                        boolean result = file.mkdirs();
517
                        fileType = getFileType(filePath, false);
518
                        if (!result && fileType != FileType.DIRECTORY)
519
                            return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath);
520
                    }
521

522
                    // If setPermissions is enabled and path is a directory
523
                    if (setPermissions && permissionsToCheck != null && fileType == FileType.DIRECTORY) {
524
                        if (setMissingPermissionsOnly)
525
                            setMissingFilePermissions(label + "directory", filePath, permissionsToCheck);
526
                        else
527
                            setFilePermissions(label + "directory", filePath, permissionsToCheck);
528
                    }
529
                }
530
            }
531

532
            // If there is not parentDirPath restriction or path is not in parentDirPath or
533
            // if existence or permission errors must not be ignored for paths in parentDirPath
534
            if (parentDirPath == null || !isPathInParentDirPath || !ignoreErrorsIfPathIsInParentDirPath) {
535
                // If path is not a directory
536
                // Directories can be automatically created so we can ignore if missing with above check
537
                if (fileType != FileType.DIRECTORY) {
538
                    label += "directory";
539
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
540
                }
541

542
                if (permissionsToCheck != null) {
543
                    // Check if permissions are missing
544
                    return checkMissingFilePermissions(label + "directory", filePath, permissionsToCheck, ignoreIfNotExecutable);
545
                }
546
            }
547
        } catch (Exception e) {
548
            return FileUtilsErrno.ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION.getError(e, label + "directory file", filePath, e.getMessage());
549
        }
550

551
        return null;
552
    }
553

554

555

556
    /**
557
     * Create a regular file at path.
558
     *
559
     * This function is a wrapper for
560
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
561
     *
562
     * @param filePath The {@code path} for regular file to create.
563
     * @return Returns the {@code error} if path is not a regular file or failed to create it,
564
     * otherwise {@code null}.
565
     */
566
    public static Error createRegularFile(final String filePath) {
567
        return createRegularFile(null, filePath);
568
    }
569

570
    /**
571
     * Create a regular file at path.
572
     *
573
     * This function is a wrapper for
574
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
575
     *
576
     * @param label The optional label for the regular file. This can optionally be {@code null}.
577
     * @param filePath The {@code path} for regular file to create.
578
     * @return Returns the {@code error} if path is not a regular file or failed to create it,
579
     * otherwise {@code null}.
580
     */
581
    public static Error createRegularFile(final String label, final String filePath) {
582
        return createRegularFile(label, filePath,
583
            null, false, false);
584
    }
585

586
    /**
587
     * Create a regular file at path.
588
     *
589
     * This function is a wrapper for
590
     * {@link #validateRegularFileExistenceAndPermissions(String, String, String, String, boolean, boolean, boolean)}.
591
     *
592
     * @param label The optional label for the regular file. This can optionally be {@code null}.
593
     * @param filePath The {@code path} for regular file to create.
594
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
595
     * @param setPermissions The {@code boolean} that decides if permissions are to be
596
     *                              automatically set defined by {@code permissionsToCheck}.
597
     * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
598
     *                                  are to be set or if they should be overridden.
599
     * @return Returns the {@code error} if path is not a regular file, failed to create it,
600
     * or validating permissions failed, otherwise {@code null}.
601
     */
602
    public static Error createRegularFile(String label, final String filePath,
603
                                          final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
604
        label = (label == null || label.isEmpty() ? "" : label + " ");
605
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "createRegularFile");
606

607
        Error error;
608

609
        File file = new File(filePath);
610
        FileType fileType = getFileType(filePath, false);
611

612
        // If file exists but not a regular file
613
        if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
614
            return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
615
        }
616

617
        // If regular file already exists
618
        if (fileType == FileType.REGULAR) {
619
            return null;
620
        }
621

622
        // Create the file parent directory
623
        error = createParentDirectoryFile(label + "regular file parent", filePath);
624
        if (error != null)
625
            return error;
626

627
        try {
628
            Logger.logVerbose(LOG_TAG, "Creating " + label + "regular file at path \"" + filePath + "\"");
629

630
            if (!file.createNewFile())
631
                return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "regular file", filePath);
632
        } catch (Exception e) {
633
            return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "regular file", filePath, e.getMessage());
634
        }
635

636
        return validateRegularFileExistenceAndPermissions(label, filePath,
637
            null,
638
            permissionsToCheck, setPermissions, setMissingPermissionsOnly,
639
            false);
640
    }
641

642

643

644
    /**
645
     * Create parent directory of file at path.
646
     *
647
     * This function is a wrapper for
648
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
649
     *
650
     * @param label The optional label for the parent directory file. This can optionally be {@code null}.
651
     * @param filePath The {@code path} for file whose parent needs to be created.
652
     * @return Returns the {@code error} if parent path is not a directory file or failed to create it,
653
     * otherwise {@code null}.
654
     */
655
    public static Error createParentDirectoryFile(final String label, final String filePath) {
656
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "createParentDirectoryFile");
657

658
        File file = new File(filePath);
659
        String fileParentPath = file.getParent();
660

661
        if (fileParentPath != null)
662
            return createDirectoryFile(label, fileParentPath,
663
                null, false, false);
664
        else
665
            return null;
666
    }
667

668
    /**
669
     * Create a directory file at path.
670
     *
671
     * This function is a wrapper for
672
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
673
     *
674
     * @param filePath The {@code path} for directory file to create.
675
     * @return Returns the {@code error} if path is not a directory file or failed to create it,
676
     * otherwise {@code null}.
677
     */
678
    public static Error createDirectoryFile(final String filePath) {
679
        return createDirectoryFile(null, filePath);
680
    }
681

682
    /**
683
     * Create a directory file at path.
684
     *
685
     * This function is a wrapper for
686
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
687
     *
688
     * @param label The optional label for the directory file. This can optionally be {@code null}.
689
     * @param filePath The {@code path} for directory file to create.
690
     * @return Returns the {@code error} if path is not a directory file or failed to create it,
691
     * otherwise {@code null}.
692
     */
693
    public static Error createDirectoryFile(final String label, final String filePath) {
694
        return createDirectoryFile(label, filePath,
695
            null, false, false);
696
    }
697

698
    /**
699
     * Create a directory file at path.
700
     *
701
     * This function is a wrapper for
702
     * {@link #validateDirectoryFileExistenceAndPermissions(String, String, String, boolean, String, boolean, boolean, boolean, boolean)}.
703
     *
704
     * @param label The optional label for the directory file. This can optionally be {@code null}.
705
     * @param filePath The {@code path} for directory file to create.
706
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
707
     * @param setPermissions The {@code boolean} that decides if permissions are to be
708
     *                              automatically set defined by {@code permissionsToCheck}.
709
     * @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
710
     *                                  are to be set or if they should be overridden.
711
     * @return Returns the {@code error} if path is not a directory file, failed to create it,
712
     * or validating permissions failed, otherwise {@code null}.
713
     */
714
    public static Error createDirectoryFile(final String label, final String filePath,
715
                                            final String permissionsToCheck, final boolean setPermissions, final boolean setMissingPermissionsOnly) {
716
        return validateDirectoryFileExistenceAndPermissions(label, filePath,
717
            null, true,
718
            permissionsToCheck, setPermissions, setMissingPermissionsOnly,
719
            false, false);
720
    }
721

722

723

724
    /**
725
     * Create a symlink file at path.
726
     *
727
     * This function is a wrapper for
728
     * {@link #createSymlinkFile(String, String, String, boolean, boolean, boolean)}.
729
     *
730
     * Dangling symlinks will be allowed.
731
     * Symlink destination will be overwritten if it already exists but only if its a symlink.
732
     *
733
     * @param targetFilePath The {@code path} TO which the symlink file will be created.
734
     * @param destFilePath The {@code path} AT which the symlink file will be created.
735
     * @return Returns the {@code error} if path is not a symlink file, failed to create it,
736
     * otherwise {@code null}.
737
     */
738
    public static Error createSymlinkFile(final String targetFilePath, final String destFilePath) {
739
        return createSymlinkFile(null, targetFilePath, destFilePath,
740
            true, true, true);
741
    }
742

743
    /**
744
     * Create a symlink file at path.
745
     *
746
     * This function is a wrapper for
747
     * {@link #createSymlinkFile(String, String, String, boolean, boolean, boolean)}.
748
     *
749
     * Dangling symlinks will be allowed.
750
     * Symlink destination will be overwritten if it already exists but only if its a symlink.
751
     *
752
     * @param label The optional label for the symlink file. This can optionally be {@code null}.
753
     * @param targetFilePath The {@code path} TO which the symlink file will be created.
754
     * @param destFilePath The {@code path} AT which the symlink file will be created.
755
     * @return Returns the {@code error} if path is not a symlink file, failed to create it,
756
     * otherwise {@code null}.
757
     */
758
    public static Error createSymlinkFile(String label, final String targetFilePath, final String destFilePath) {
759
        return createSymlinkFile(label, targetFilePath, destFilePath,
760
            true, true, true);
761
    }
762

763
    /**
764
     * Create a symlink file at path.
765
     *
766
     * @param label The optional label for the symlink file. This can optionally be {@code null}.
767
     * @param targetFilePath The {@code path} TO which the symlink file will be created.
768
     * @param destFilePath The {@code path} AT which the symlink file will be created.
769
     * @param allowDangling The {@code boolean} that decides if it should be considered an
770
     *                              error if source file doesn't exist.
771
     * @param overwrite The {@code boolean} that decides if destination file should be overwritten if
772
     *                  it already exists. If set to {@code true}, then destination file will be
773
     *                  deleted before symlink is created.
774
     * @param overwriteOnlyIfDestIsASymlink The {@code boolean} that decides if overwrite should
775
     *                                         only be done if destination file is also a symlink.
776
     * @return Returns the {@code error} if path is not a symlink file, failed to create it,
777
     * or validating permissions failed, otherwise {@code null}.
778
     */
779
    public static Error createSymlinkFile(String label, final String targetFilePath, final String destFilePath,
780
                                          final boolean allowDangling, final boolean overwrite, final boolean overwriteOnlyIfDestIsASymlink) {
781
        label = (label == null || label.isEmpty() ? "" : label + " ");
782
        if (targetFilePath == null || targetFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "target file path", "createSymlinkFile");
783
        if (destFilePath == null || destFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "destination file path", "createSymlinkFile");
784

785
        Error error;
786

787
        try {
788
            File destFile = new File(destFilePath);
789

790
            String targetFileAbsolutePath = targetFilePath;
791
            // If target path is relative instead of absolute
792
            if (!targetFilePath.startsWith("/")) {
793
                String destFileParentPath = destFile.getParent();
794
                if (destFileParentPath != null)
795
                    targetFileAbsolutePath = destFileParentPath + "/" +  targetFilePath;
796
            }
797

798
            FileType targetFileType = getFileType(targetFileAbsolutePath, false);
799
            FileType destFileType = getFileType(destFilePath, false);
800

801
            // If target file does not exist
802
            if (targetFileType == FileType.NO_EXIST) {
803
                // If dangling symlink should not be allowed, then return with error
804
                if (!allowDangling) {
805
                    label += "symlink target file";
806
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, targetFileAbsolutePath).setLabel(label);
807
                }
808
            }
809

810
            // If destination exists
811
            if (destFileType != FileType.NO_EXIST) {
812
                // If destination must not be overwritten
813
                if (!overwrite) {
814
                    return null;
815
                }
816

817
                // If overwriteOnlyIfDestIsASymlink is enabled but destination file is not a symlink
818
                if (overwriteOnlyIfDestIsASymlink && destFileType != FileType.SYMLINK)
819
                    return FileUtilsErrno.ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE.getError(label + " file", destFilePath, targetFilePath, destFileType.getName());
820

821
                // Delete the destination file
822
                error = deleteFile(label + "symlink destination", destFilePath, true);
823
                if (error != null)
824
                    return error;
825
            } else {
826
                // Create the destination file parent directory
827
                error = createParentDirectoryFile(label + "symlink destination file parent", destFilePath);
828
                if (error != null)
829
                    return error;
830
            }
831

832
            // create a symlink at destFilePath to targetFilePath
833
            Logger.logVerbose(LOG_TAG, "Creating " + label + "symlink file at path \"" + destFilePath + "\" to \"" + targetFilePath + "\"");
834
            Os.symlink(targetFilePath, destFilePath);
835
        } catch (Exception e) {
836
            return FileUtilsErrno.ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "symlink file", destFilePath, targetFilePath, e.getMessage());
837
        }
838

839
        return null;
840
    }
841

842

843

844
    /**
845
     * Copy a regular file from {@code sourceFilePath} to {@code destFilePath}.
846
     *
847
     * This function is a wrapper for
848
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
849
     *
850
     * If destination file already exists, then it will be overwritten, but only if its a regular
851
     * file, otherwise an error will be returned.
852
     *
853
     * @param label The optional label for file to copy. This can optionally be {@code null}.
854
     * @param srcFilePath The {@code source path} for file to copy.
855
     * @param destFilePath The {@code destination path} for file to copy.
856
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
857
     *                              error if source file to copied doesn't exist.
858
     * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
859
     */
860
    public static Error copyRegularFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
861
        return copyOrMoveFile(label, srcFilePath, destFilePath,
862
            false, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
863
            true, true);
864
    }
865

866
    /**
867
     * Move a regular file from {@code sourceFilePath} to {@code destFilePath}.
868
     *
869
     * This function is a wrapper for
870
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
871
     *
872
     * If destination file already exists, then it will be overwritten, but only if its a regular
873
     * file, otherwise an error will be returned.
874
     *
875
     * @param label The optional label for file to move. This can optionally be {@code null}.
876
     * @param srcFilePath The {@code source path} for file to move.
877
     * @param destFilePath The {@code destination path} for file to move.
878
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
879
     *                              error if source file to moved doesn't exist.
880
     * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
881
     */
882
    public static Error moveRegularFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
883
        return copyOrMoveFile(label, srcFilePath, destFilePath,
884
            true, ignoreNonExistentSrcFile, FileType.REGULAR.getValue(),
885
            true, true);
886
    }
887

888
    /**
889
     * Copy a directory file from {@code sourceFilePath} to {@code destFilePath}.
890
     *
891
     * This function is a wrapper for
892
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
893
     *
894
     * If destination file already exists, then it will be overwritten, but only if its a directory
895
     * file, otherwise an error will be returned.
896
     *
897
     * @param label The optional label for file to copy. This can optionally be {@code null}.
898
     * @param srcFilePath The {@code source path} for file to copy.
899
     * @param destFilePath The {@code destination path} for file to copy.
900
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
901
     *                              error if source file to copied doesn't exist.
902
     * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
903
     */
904
    public static Error copyDirectoryFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
905
        return copyOrMoveFile(label, srcFilePath, destFilePath,
906
            false, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
907
            true, true);
908
    }
909

910
    /**
911
     * Move a directory file from {@code sourceFilePath} to {@code destFilePath}.
912
     *
913
     * This function is a wrapper for
914
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
915
     *
916
     * If destination file already exists, then it will be overwritten, but only if its a directory
917
     * file, otherwise an error will be returned.
918
     *
919
     * @param label The optional label for file to move. This can optionally be {@code null}.
920
     * @param srcFilePath The {@code source path} for file to move.
921
     * @param destFilePath The {@code destination path} for file to move.
922
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
923
     *                              error if source file to moved doesn't exist.
924
     * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
925
     */
926
    public static Error moveDirectoryFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
927
        return copyOrMoveFile(label, srcFilePath, destFilePath,
928
            true, ignoreNonExistentSrcFile, FileType.DIRECTORY.getValue(),
929
            true, true);
930
    }
931

932
    /**
933
     * Copy a symlink file from {@code sourceFilePath} to {@code destFilePath}.
934
     *
935
     * This function is a wrapper for
936
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
937
     *
938
     * If destination file already exists, then it will be overwritten, but only if its a symlink
939
     * file, otherwise an error will be returned.
940
     *
941
     * @param label The optional label for file to copy. This can optionally be {@code null}.
942
     * @param srcFilePath The {@code source path} for file to copy.
943
     * @param destFilePath The {@code destination path} for file to copy.
944
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
945
     *                              error if source file to copied doesn't exist.
946
     * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
947
     */
948
    public static Error copySymlinkFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
949
        return copyOrMoveFile(label, srcFilePath, destFilePath,
950
            false, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
951
            true, true);
952
    }
953

954
    /**
955
     * Move a symlink file from {@code sourceFilePath} to {@code destFilePath}.
956
     *
957
     * This function is a wrapper for
958
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
959
     *
960
     * If destination file already exists, then it will be overwritten, but only if its a symlink
961
     * file, otherwise an error will be returned.
962
     *
963
     * @param label The optional label for file to move. This can optionally be {@code null}.
964
     * @param srcFilePath The {@code source path} for file to move.
965
     * @param destFilePath The {@code destination path} for file to move.
966
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
967
     *                              error if source file to moved doesn't exist.
968
     * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
969
     */
970
    public static Error moveSymlinkFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
971
        return copyOrMoveFile(label, srcFilePath, destFilePath,
972
            true, ignoreNonExistentSrcFile, FileType.SYMLINK.getValue(),
973
            true, true);
974
    }
975

976
    /**
977
     * Copy a file from {@code sourceFilePath} to {@code destFilePath}.
978
     *
979
     * This function is a wrapper for
980
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
981
     *
982
     * If destination file already exists, then it will be overwritten, but only if its the same file
983
     * type as the source, otherwise an error will be returned.
984
     *
985
     * @param label The optional label for file to copy. This can optionally be {@code null}.
986
     * @param srcFilePath The {@code source path} for file to copy.
987
     * @param destFilePath The {@code destination path} for file to copy.
988
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
989
     *                              error if source file to copied doesn't exist.
990
     * @return Returns the {@code error} if copy was not successful, otherwise {@code null}.
991
     */
992
    public static Error copyFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
993
        return copyOrMoveFile(label, srcFilePath, destFilePath,
994
            false, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
995
            true, true);
996
    }
997

998
    /**
999
     * Move a file from {@code sourceFilePath} to {@code destFilePath}.
1000
     *
1001
     * This function is a wrapper for
1002
     * {@link #copyOrMoveFile(String, String, String, boolean, boolean, int, boolean, boolean)}.
1003
     *
1004
     * If destination file already exists, then it will be overwritten, but only if its the same file
1005
     * type as the source, otherwise an error will be returned.
1006
     *
1007
     * @param label The optional label for file to move. This can optionally be {@code null}.
1008
     * @param srcFilePath The {@code source path} for file to move.
1009
     * @param destFilePath The {@code destination path} for file to move.
1010
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
1011
     *                              error if source file to moved doesn't exist.
1012
     * @return Returns the {@code error} if move was not successful, otherwise {@code null}.
1013
     */
1014
    public static Error moveFile(final String label, final String srcFilePath, final String destFilePath, final boolean ignoreNonExistentSrcFile) {
1015
        return copyOrMoveFile(label, srcFilePath, destFilePath,
1016
            true, ignoreNonExistentSrcFile, FileTypes.FILE_TYPE_NORMAL_FLAGS,
1017
            true, true);
1018
    }
1019

1020
    /**
1021
     * Copy or move a file from {@code sourceFilePath} to {@code destFilePath}.
1022
     *
1023
     * The {@code sourceFilePath} and {@code destFilePath} must be the canonical path to the source
1024
     * and destination since symlinks will not be followed.
1025
     *
1026
     * If the {@code sourceFilePath} or {@code destFilePath} is a canonical path to a directory,
1027
     * then any symlink files found under the directory will be deleted, but not their targets when
1028
     * deleting source after move and deleting destination before copy/move.
1029
     *
1030
     * @param label The optional label for file to copy or move. This can optionally be {@code null}.
1031
     * @param srcFilePath The {@code source path} for file to copy or move.
1032
     * @param destFilePath The {@code destination path} for file to copy or move.
1033
     * @param moveFile The {@code boolean} that decides if source file needs to be copied or moved.
1034
     *                 If set to {@code true}, then source file will be moved, otherwise it will be
1035
     *                 copied.
1036
     * @param ignoreNonExistentSrcFile The {@code boolean} that decides if it should be considered an
1037
     *                              error if source file to copied or moved doesn't exist.
1038
     * @param allowedFileTypeFlags The flags that are matched against the source file's {@link FileType}
1039
     *                             to see if it should be copied/moved or not. This is a safety measure
1040
     *                             to prevent accidental copy/move/delete of the wrong type of file,
1041
     *                             like a directory instead of a regular file. You can pass
1042
     *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow copy/move of any file type.
1043
     * @param overwrite The {@code boolean} that decides if destination file should be overwritten if
1044
     *                  it already exists. If set to {@code true}, then destination file will be
1045
     *                  deleted before source is copied or moved.
1046
     * @param overwriteOnlyIfDestSameFileTypeAsSrc The {@code boolean} that decides if overwrite should
1047
     *                                         only be done if destination file is also the same file
1048
     *                                          type as the source file.
1049
     * @return Returns the {@code error} if copy or move was not successful, otherwise {@code null}.
1050
     */
1051
    public static Error copyOrMoveFile(String label, final String srcFilePath, final String destFilePath,
1052
                                       final boolean moveFile, final boolean ignoreNonExistentSrcFile, int allowedFileTypeFlags,
1053
                                       final boolean overwrite, final boolean overwriteOnlyIfDestSameFileTypeAsSrc) {
1054
        label = (label == null || label.isEmpty() ? "" : label + " ");
1055
        if (srcFilePath == null || srcFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "source file path", "copyOrMoveFile");
1056
        if (destFilePath == null || destFilePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "destination file path", "copyOrMoveFile");
1057

1058
        String mode = (moveFile ? "Moving" : "Copying");
1059
        String modePast = (moveFile ? "moved" : "copied");
1060

1061
        Error error;
1062

1063
        try {
1064
            Logger.logVerbose(LOG_TAG, mode + " " + label + "source file from \"" + srcFilePath + "\" to destination \"" + destFilePath + "\"");
1065

1066
            File srcFile = new File(srcFilePath);
1067
            File destFile = new File(destFilePath);
1068

1069
            FileType srcFileType = getFileType(srcFilePath, false);
1070
            FileType destFileType = getFileType(destFilePath, false);
1071

1072
            String srcFileCanonicalPath = srcFile.getCanonicalPath();
1073
            String destFileCanonicalPath = destFile.getCanonicalPath();
1074

1075
            // If source file does not exist
1076
            if (srcFileType == FileType.NO_EXIST) {
1077
                // If copy or move is to be ignored if source file is not found
1078
                if (ignoreNonExistentSrcFile)
1079
                    return null;
1080
                    // Else return with error
1081
                else {
1082
                    label += "source file";
1083
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, srcFilePath).setLabel(label);
1084
                }
1085
            }
1086

1087
            // If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error
1088
            if ((allowedFileTypeFlags & srcFileType.getValue()) <= 0)
1089
                return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "source file meant to be " + modePast, srcFilePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
1090

1091
            // If source and destination file path are the same
1092
            if (srcFileCanonicalPath.equals(destFileCanonicalPath))
1093
                return FileUtilsErrno.ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH.getError(mode + " " + label + "source file", srcFilePath, destFilePath);
1094

1095
            // If destination exists
1096
            if (destFileType != FileType.NO_EXIST) {
1097
                // If destination must not be overwritten
1098
                if (!overwrite) {
1099
                    return null;
1100
                }
1101

1102
                // If overwriteOnlyIfDestSameFileTypeAsSrc is enabled but destination file does not match source file type
1103
                if (overwriteOnlyIfDestSameFileTypeAsSrc && destFileType != srcFileType)
1104
                    return FileUtilsErrno.ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE.getError(label + "source file", mode.toLowerCase(), srcFilePath, destFilePath, destFileType.getName(), srcFileType.getName());
1105

1106
                // Delete the destination file
1107
                error = deleteFile(label + "destination", destFilePath, true);
1108
                if (error != null)
1109
                    return error;
1110
            }
1111

1112

1113
            // Copy or move source file to dest
1114
            boolean copyFile = !moveFile;
1115

1116
            // If moveFile is true
1117
            if (moveFile) {
1118
                // 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
1119
                Logger.logVerbose(LOG_TAG, "Attempting to rename source to destination.");
1120

1121
                // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=358
1122
                // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/android/system/Os.java;l=512
1123
                // Uses File.getPath() to get the path of source and destination and not the canonical path
1124
                if (!srcFile.renameTo(destFile)) {
1125
                    // If destination directory is a subdirectory of the source directory
1126
                    // Copying is still allowed by copyDirectory() by excluding destination directory files
1127
                    if (srcFileType == FileType.DIRECTORY && destFileCanonicalPath.startsWith(srcFileCanonicalPath + File.separator))
1128
                        return FileUtilsErrno.ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF.getError(label + "source directory", srcFilePath, destFilePath);
1129

1130
                    // If rename failed, then we copy
1131
                    Logger.logVerbose(LOG_TAG, "Renaming " + label + "source file to destination failed, attempting to copy.");
1132
                    copyFile = true;
1133
                }
1134
            }
1135

1136
            // If moveFile is false or renameTo failed while moving
1137
            if (copyFile) {
1138
                Logger.logVerbose(LOG_TAG, "Attempting to copy source to destination.");
1139

1140
                // Create the dest file parent directory
1141
                error = createParentDirectoryFile(label + "dest file parent", destFilePath);
1142
                if (error != null)
1143
                    return error;
1144

1145
                if (srcFileType == FileType.DIRECTORY) {
1146
                    // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
1147
                    org.apache.commons.io.FileUtils.copyDirectory(srcFile, destFile, true);
1148
                } else if (srcFileType == FileType.SYMLINK) {
1149
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1150
                        java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
1151
                    } else {
1152
                        // read the target for the source file and create a symlink at dest
1153
                        // source file metadata will be lost
1154
                        error = createSymlinkFile(label + "dest", Os.readlink(srcFilePath), destFilePath);
1155
                        if (error != null)
1156
                            return error;
1157
                    }
1158
                } else {
1159
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1160
                        java.nio.file.Files.copy(srcFile.toPath(), destFile.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
1161
                    } else {
1162
                        // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
1163
                        org.apache.commons.io.FileUtils.copyFile(srcFile, destFile, true);
1164
                    }
1165
                }
1166
            }
1167

1168
            // If source file had to be moved
1169
            if (moveFile) {
1170
                // Delete the source file since copying would have succeeded
1171
                error = deleteFile(label + "source", srcFilePath, true);
1172
                if (error != null)
1173
                    return error;
1174
            }
1175

1176
            Logger.logVerbose(LOG_TAG, mode + " successful.");
1177
        }
1178
        catch (Exception e) {
1179
            return FileUtilsErrno.ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION.getError(e, mode + " " + label + "file", srcFilePath, destFilePath, e.getMessage());
1180
        }
1181

1182
        return null;
1183
    }
1184

1185

1186

1187
    /**
1188
     * Delete regular file at path.
1189
     *
1190
     * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
1191
     *
1192
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1193
     * @param filePath The {@code path} for file to delete.
1194
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1195
     *                              error if file to deleted doesn't exist.
1196
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1197
     */
1198
    public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
1199
        return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.REGULAR.getValue());
1200
    }
1201

1202
    /**
1203
     * Delete directory file at path.
1204
     *
1205
     * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
1206
     *
1207
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1208
     * @param filePath The {@code path} for file to delete.
1209
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1210
     *                              error if file to deleted doesn't exist.
1211
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1212
     */
1213
    public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
1214
        return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.DIRECTORY.getValue());
1215
    }
1216

1217
    /**
1218
     * Delete symlink file at path.
1219
     *
1220
     * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
1221
     *
1222
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1223
     * @param filePath The {@code path} for file to delete.
1224
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1225
     *                              error if file to deleted doesn't exist.
1226
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1227
     */
1228
    public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
1229
        return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
1230
    }
1231

1232
    /**
1233
     * Delete socket file at path.
1234
     *
1235
     * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
1236
     *
1237
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1238
     * @param filePath The {@code path} for file to delete.
1239
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1240
     *                              error if file to deleted doesn't exist.
1241
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1242
     */
1243
    public static Error deleteSocketFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
1244
        return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SOCKET.getValue());
1245
    }
1246

1247
    /**
1248
     * Delete regular, directory or symlink file at path.
1249
     *
1250
     * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
1251
     *
1252
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1253
     * @param filePath The {@code path} for file to delete.
1254
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1255
     *                              error if file to deleted doesn't exist.
1256
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1257
     */
1258
    public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
1259
        return deleteFile(label, filePath, ignoreNonExistentFile, false, FileTypes.FILE_TYPE_NORMAL_FLAGS);
1260
    }
1261

1262
    /**
1263
     * Delete file at path.
1264
     *
1265
     * The {@code filePath} must be the canonical path to the file to be deleted since symlinks will
1266
     * not be followed.
1267
     * If the {@code filePath} is a canonical path to a directory, then any symlink files found under
1268
     * the directory will be deleted, but not their targets.
1269
     *
1270
     * @param label The optional label for file to delete. This can optionally be {@code null}.
1271
     * @param filePath The {@code path} for file to delete.
1272
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1273
     *                              error if file to deleted doesn't exist.
1274
     * @param ignoreWrongFileType The {@code boolean} that decides if it should be considered an
1275
     *                              error if file type is not one from {@code allowedFileTypeFlags}.
1276
     * @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
1277
     *                             see if it should be deleted or not. This is a safety measure to
1278
     *                             prevent accidental deletion of the wrong type of file, like a
1279
     *                             directory instead of a regular file. You can pass
1280
     *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
1281
     * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
1282
     */
1283
    public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, final boolean ignoreWrongFileType, int allowedFileTypeFlags) {
1284
        label = (label == null || label.isEmpty() ? "" : label + " ");
1285
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile");
1286

1287
        try {
1288
            File file = new File(filePath);
1289
            FileType fileType = getFileType(filePath, false);
1290

1291
            Logger.logVerbose(LOG_TAG, "Processing delete of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\"");
1292

1293
            // If file does not exist
1294
            if (fileType == FileType.NO_EXIST) {
1295
                // If delete is to be ignored if file does not exist
1296
                if (ignoreNonExistentFile)
1297
                    return null;
1298
                    // Else return with error
1299
                else {
1300
                    label += "file meant to be deleted";
1301
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
1302
                }
1303
            }
1304

1305
            // If the file type of the file does not exist in the allowedFileTypeFlags
1306
            if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
1307
                // If wrong file type is to be ignored
1308
                if (ignoreWrongFileType) {
1309
                    Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
1310
                    return null;
1311
                }
1312

1313
                // Else return with error
1314
                return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, fileType.getName(), FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
1315
            }
1316

1317
            Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
1318

1319
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1320
                /*
1321
                 * Try to use {@link SecureDirectoryStream} if available for safer directory
1322
                 * deletion, it should be available for android >= 8.0
1323
                 * 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...-
1324
                 * https://github.com/google/guava/issues/365
1325
                 * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixSecureDirectoryStream.java
1326
                 *
1327
                 * MoreUtils is marked with the @Beta annotation so the API may be removed in
1328
                 * future but has been there for a few years now.
1329
                 *
1330
                 * If an exception is thrown, the exception message might not contain the full errors.
1331
                 * Individual failures get added to suppressed throwables which can be extracted
1332
                 * from the exception object by calling `Throwable[] getSuppressed()`. So just logging
1333
                 * the exception message and stacktrace may not be enough, the suppressed throwables
1334
                 * need to be logged as well, which the Logger class does if they are found in the
1335
                 * exception added to the Error that's returned by this function.
1336
                 * https://github.com/google/guava/blob/v30.1.1/guava/src/com/google/common/io/MoreFiles.java#L775
1337
                 */
1338
                //noinspection UnstableApiUsage
1339
                com.google.common.io.MoreFiles.deleteRecursively(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
1340
            } else {
1341
                if (fileType == FileType.DIRECTORY) {
1342
                    // deleteDirectory() instead of forceDelete() gets the files list first instead of walking directory tree, so seems safer
1343
                    // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
1344
                    org.apache.commons.io.FileUtils.deleteDirectory(file);
1345
                } else {
1346
                    // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
1347
                    org.apache.commons.io.FileUtils.forceDelete(file);
1348
                }
1349
            }
1350

1351
            // If file still exists after deleting it
1352
            fileType = getFileType(filePath, false);
1353
            if (fileType != FileType.NO_EXIST)
1354
                return FileUtilsErrno.ERRNO_FILE_STILL_EXISTS_AFTER_DELETING.getError(label + "file meant to be deleted", filePath);
1355
        }
1356
        catch (Exception e) {
1357
            return FileUtilsErrno.ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
1358
        }
1359

1360
        return null;
1361
    }
1362

1363

1364

1365
    /**
1366
     * Clear contents of directory at path without deleting the directory. If directory does not exist
1367
     * it will be created automatically.
1368
     *
1369
     * This function is a wrapper for
1370
     * {@link #clearDirectory(String, String)}.
1371
     *
1372
     * @param filePath The {@code path} for directory to clear.
1373
     * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}.
1374
     */
1375
    public static Error clearDirectory(String filePath) {
1376
        return clearDirectory(null, filePath);
1377
    }
1378

1379
    /**
1380
     * Clear contents of directory at path without deleting the directory. If directory does not exist
1381
     * it will be created automatically.
1382
     *
1383
     * The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
1384
     * Any symlink files found under the directory will be deleted, but not their targets.
1385
     *
1386
     * @param label The optional label for directory to clear. This can optionally be {@code null}.
1387
     * @param filePath The {@code path} for directory to clear.
1388
     * @return Returns the {@code error} if clearing was not successful, otherwise {@code null}.
1389
     */
1390
    public static Error clearDirectory(String label, final String filePath) {
1391
        label = (label == null || label.isEmpty() ? "" : label + " ");
1392
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "clearDirectory");
1393

1394
        Error error;
1395

1396
        try {
1397
            Logger.logVerbose(LOG_TAG, "Clearing " + label + "directory at path \"" + filePath + "\"");
1398

1399
            File file = new File(filePath);
1400
            FileType fileType = getFileType(filePath, false);
1401

1402
            // If file exists but not a directory file
1403
            if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
1404
                return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
1405
            }
1406

1407
            // If directory exists, clear its contents
1408
            if (fileType == FileType.DIRECTORY) {
1409
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1410
                    /* If an exception is thrown, the exception message might not contain the full errors.
1411
                     * Individual failures get added to suppressed throwables. */
1412
                    //noinspection UnstableApiUsage
1413
                    com.google.common.io.MoreFiles.deleteDirectoryContents(file.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
1414
                } else {
1415
                    // Will give runtime exceptions on android < 8 due to missing classes like java.nio.file.Path if org.apache.commons.io version > 2.5
1416
                    org.apache.commons.io.FileUtils.cleanDirectory(new File(filePath));
1417
                }
1418
            }
1419
            // Else create it
1420
            else {
1421
                error = createDirectoryFile(label, filePath);
1422
                if (error != null)
1423
                    return error;
1424
            }
1425
        } catch (Exception e) {
1426
            return FileUtilsErrno.ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, e.getMessage());
1427
        }
1428

1429
        return null;
1430
    }
1431

1432
    /**
1433
     * Delete files under a directory older than x days.
1434
     *
1435
     * The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
1436
     * Any symlink files found under the directory will be deleted, but not their targets.
1437
     *
1438
     * @param label The optional label for directory to clear. This can optionally be {@code null}.
1439
     * @param filePath The {@code path} for directory to clear.
1440
     * @param dirFilter  The optional filter to apply when finding subdirectories.
1441
     *                   If this parameter is {@code null}, subdirectories will not be included in the
1442
     *                   search. Use TrueFileFilter.INSTANCE to match all directories.
1443
     * @param days The x amount of days before which files should be deleted. This must be `>=0`.
1444
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1445
     *                              error if file to deleted doesn't exist.
1446
     * @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
1447
     *                             see if it should be deleted or not. This is a safety measure to
1448
     *                             prevent accidental deletion of the wrong type of file, like a
1449
     *                             directory instead of a regular file. You can pass
1450
     *                             {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
1451
     * @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
1452
     */
1453
    public static Error deleteFilesOlderThanXDays(String label, final String filePath, final IOFileFilter dirFilter, int days, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
1454
        label = (label == null || label.isEmpty() ? "" : label + " ");
1455
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFilesOlderThanXDays");
1456
        if (days < 0) return FunctionErrno.ERRNO_INVALID_PARAMETER.getError(label + "days", "deleteFilesOlderThanXDays", " It must be >= 0.");
1457

1458
        Error error;
1459

1460
        try {
1461
            Logger.logVerbose(LOG_TAG, "Deleting files under " + label + "directory at path \"" + filePath + "\" older than " + days + " days");
1462

1463
            File file = new File(filePath);
1464
            FileType fileType = getFileType(filePath, false);
1465

1466
            // If file exists but not a directory file
1467
            if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
1468
                return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
1469
            }
1470

1471
            // If file does not exist
1472
            if (fileType == FileType.NO_EXIST) {
1473
                // If delete is to be ignored if file does not exist
1474
                if (ignoreNonExistentFile)
1475
                    return null;
1476
                    // Else return with error
1477
                else {
1478
                    label += "directory under which files had to be deleted";
1479
                    return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
1480
                }
1481
            }
1482

1483
            // TODO: Use FileAttributes with support for atime (default), mtime, ctime. Add regex for ignoring file and dir absolute paths.
1484
            // FIXME: iterateFiles() does not return subdirectories even with TrueFileFilter for file and dir.
1485
            // FIXME: Empty directories remain
1486

1487
            // If directory exists, delete its contents
1488
            Calendar calendar = Calendar.getInstance();
1489
            calendar.add(Calendar.DATE, -(days));
1490
            // AgeFileFilter seems to apply to symlink destination timestamp instead of symlink file itself
1491
            Iterator<File> filesToDelete =
1492
                org.apache.commons.io.FileUtils.iterateFiles(file, new AgeFileFilter(calendar.getTime()), dirFilter);
1493
            while (filesToDelete.hasNext()) {
1494
                File subFile = filesToDelete.next();
1495
                error = deleteFile(label + " directory sub", subFile.getAbsolutePath(), true, true, allowedFileTypeFlags);
1496
                if (error != null)
1497
                    return error;
1498
            }
1499
        } catch (Exception e) {
1500
            return FileUtilsErrno.ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, days, e.getMessage());
1501
        }
1502

1503
        return null;
1504

1505
    }
1506

1507

1508

1509

1510

1511
    /**
1512
     * Read a text {@link String} from file at path with a specific {@link Charset} into {@code dataString}.
1513
     *
1514
     * @param label The optional label for file to read. This can optionally be {@code null}.
1515
     * @param filePath The {@code path} for file to read.
1516
     * @param charset The {@link Charset} of the file. If this is {@code null},
1517
     *                then default {@link Charset} will be used.
1518
     * @param dataStringBuilder The {@code StringBuilder} to read data into.
1519
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1520
     *                              error if file to read doesn't exist.
1521
     * @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
1522
     */
1523
    public static Error readTextFromFile(String label, final String filePath, Charset charset, @NonNull final StringBuilder dataStringBuilder, final boolean ignoreNonExistentFile) {
1524
        label = (label == null || label.isEmpty() ? "" : label + " ");
1525
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readStringFromFile");
1526

1527
        Logger.logVerbose(LOG_TAG, "Reading text from " + label + "file at path \"" + filePath + "\"");
1528

1529
        Error error;
1530

1531
        FileType fileType = getFileType(filePath, false);
1532

1533
        // If file exists but not a regular file
1534
        if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
1535
            return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
1536
        }
1537

1538
        // If file does not exist
1539
        if (fileType == FileType.NO_EXIST) {
1540
            // If reading is to be ignored if file does not exist
1541
            if (ignoreNonExistentFile)
1542
                return null;
1543
                // Else return with error
1544
            else {
1545
                label += "file meant to be read";
1546
                return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
1547
            }
1548
        }
1549

1550
        if (charset == null) charset = Charset.defaultCharset();
1551

1552
        // Check if charset is supported
1553
        error = isCharsetSupported(charset);
1554
        if (error != null)
1555
            return error;
1556

1557
        FileInputStream fileInputStream = null;
1558
        BufferedReader bufferedReader = null;
1559
        try {
1560
            // Read text from file
1561
            fileInputStream = new FileInputStream(filePath);
1562
            bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, charset));
1563

1564
            String receiveString;
1565

1566
            boolean firstLine = true;
1567
            while ((receiveString = bufferedReader.readLine()) != null ) {
1568
                if (!firstLine) dataStringBuilder.append("\n"); else firstLine = false;
1569
                dataStringBuilder.append(receiveString);
1570
            }
1571

1572
            Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
1573
        } catch (Exception e) {
1574
            return FileUtilsErrno.ERRNO_READING_TEXT_FROM_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
1575
        } finally {
1576
            closeCloseable(fileInputStream);
1577
            closeCloseable(bufferedReader);
1578
        }
1579

1580
        return null;
1581
    }
1582

1583
    public static class ReadSerializableObjectResult {
1584
        public final Error error;
1585
        public final Serializable serializableObject;
1586

1587
        ReadSerializableObjectResult(Error error, Serializable serializableObject) {
1588
            this.error = error;
1589
            this.serializableObject = serializableObject;
1590
        }
1591
    }
1592

1593
    /**
1594
     * Read a {@link Serializable} object from file at path.
1595
     *
1596
     * @param label The optional label for file to read. This can optionally be {@code null}.
1597
     * @param filePath The {@code path} for file to read.
1598
     * @param readObjectType The {@link Class} of the object.
1599
     * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
1600
     *                              error if file to read doesn't exist.
1601
     * @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
1602
     */
1603
    @NonNull
1604
    public static <T extends Serializable> ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class<T> readObjectType, final boolean ignoreNonExistentFile) {
1605
        label = (label == null || label.isEmpty() ? "" : label + " ");
1606
        if (filePath == null || filePath.isEmpty()) return new ReadSerializableObjectResult(FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readSerializableObjectFromFile"), null);
1607

1608
        Logger.logVerbose(LOG_TAG, "Reading serializable object from " + label + "file at path \"" + filePath + "\"");
1609

1610
        T serializableObject;
1611

1612
        FileType fileType = getFileType(filePath, false);
1613

1614
        // If file exists but not a regular file
1615
        if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
1616
            return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file"), null);
1617
        }
1618

1619
        // If file does not exist
1620
        if (fileType == FileType.NO_EXIST) {
1621
            // If reading is to be ignored if file does not exist
1622
            if (ignoreNonExistentFile)
1623
                return new ReadSerializableObjectResult(null, null);
1624
                // Else return with error
1625
            else {
1626
                label += "file meant to be read";
1627
                return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label), null);
1628
            }
1629
        }
1630

1631
        FileInputStream fileInputStream = null;
1632
        ObjectInputStream objectInputStream = null;
1633
        try {
1634
            // Read serializable object from file
1635
            fileInputStream = new FileInputStream(filePath);
1636
            objectInputStream = new ObjectInputStream(fileInputStream);
1637
            //serializableObject = (T) objectInputStream.readObject();
1638
            serializableObject = readObjectType.cast(objectInputStream.readObject());
1639

1640
            //Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
1641
        } catch (Exception e) {
1642
            return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()), null);
1643
        } finally {
1644
            closeCloseable(fileInputStream);
1645
            closeCloseable(objectInputStream);
1646
        }
1647

1648
        return new ReadSerializableObjectResult(null, serializableObject);
1649
    }
1650

1651
    /**
1652
     * Write text {@code dataString} with a specific {@link Charset} to file at path.
1653
     *
1654
     * @param label The optional label for file to write. This can optionally be {@code null}.
1655
     * @param filePath The {@code path} for file to write.
1656
     * @param charset The {@link Charset} of the {@code dataString}. If this is {@code null},
1657
     *                then default {@link Charset} will be used.
1658
     * @param dataString The data to write to file.
1659
     * @param append The {@code boolean} that decides if file should be appended to or not.
1660
     * @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
1661
     */
1662
    public static Error writeTextToFile(String label, final String filePath, Charset charset, final String dataString, final boolean append) {
1663
        label = (label == null || label.isEmpty() ? "" : label + " ");
1664
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeStringToFile");
1665

1666
        Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("Writing text to " + label + "file at path \"" + filePath + "\"", DataUtils.getTruncatedCommandOutput(dataString, Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
1667

1668
        Error error;
1669

1670
        error = preWriteToFile(label, filePath);
1671
        if (error != null)
1672
            return error;
1673

1674
        if (charset == null) charset = Charset.defaultCharset();
1675

1676
        // Check if charset is supported
1677
        error = isCharsetSupported(charset);
1678
        if (error != null)
1679
            return error;
1680

1681
        FileOutputStream fileOutputStream = null;
1682
        BufferedWriter bufferedWriter = null;
1683
        try {
1684
            // Write text to file
1685
            fileOutputStream = new FileOutputStream(filePath, append);
1686
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream, charset));
1687

1688
            bufferedWriter.write(dataString);
1689
            bufferedWriter.flush();
1690
        } catch (Exception e) {
1691
            return FileUtilsErrno.ERRNO_WRITING_TEXT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
1692
        } finally {
1693
            closeCloseable(fileOutputStream);
1694
            closeCloseable(bufferedWriter);
1695
        }
1696

1697
        return null;
1698
    }
1699

1700
    /**
1701
     * Write the {@link Serializable} {@code serializableObject} to file at path.
1702
     *
1703
     * @param label The optional label for file to write. This can optionally be {@code null}.
1704
     * @param filePath The {@code path} for file to write.
1705
     * @param serializableObject The object to write to file.
1706
     * @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
1707
     */
1708
    public static <T extends Serializable> Error writeSerializableObjectToFile(String label, final String filePath, final T serializableObject) {
1709
        label = (label == null || label.isEmpty() ? "" : label + " ");
1710
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeSerializableObjectToFile");
1711

1712
        Logger.logVerbose(LOG_TAG, "Writing serializable object to " + label + "file at path \"" + filePath + "\"");
1713

1714
        Error error;
1715

1716
        error = preWriteToFile(label, filePath);
1717
        if (error != null)
1718
            return error;
1719

1720
        FileOutputStream fileOutputStream = null;
1721
        ObjectOutputStream objectOutputStream = null;
1722
        try {
1723
            // Write serializable object to file
1724
            fileOutputStream = new FileOutputStream(filePath);
1725
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
1726

1727
            objectOutputStream.writeObject(serializableObject);
1728
            objectOutputStream.flush();
1729
        } catch (Exception e) {
1730
            return FileUtilsErrno.ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
1731
        } finally {
1732
            closeCloseable(fileOutputStream);
1733
            closeCloseable(objectOutputStream);
1734
        }
1735

1736
        return null;
1737
    }
1738

1739
    private static Error preWriteToFile(String label, String filePath) {
1740
        Error error;
1741

1742
        FileType fileType = getFileType(filePath, false);
1743

1744
        // If file exists but not a regular file
1745
        if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
1746
            return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
1747
        }
1748

1749
        // Create the file parent directory
1750
        error = createParentDirectoryFile(label + "file parent", filePath);
1751
        if (error != null)
1752
            return error;
1753

1754
        return null;
1755
    }
1756

1757

1758

1759
    /**
1760
     * Check if a specific {@link Charset} is supported.
1761
     *
1762
     * @param charset The {@link Charset} to check.
1763
     * @return Returns the {@code error} if charset is not supported or failed to check it, otherwise {@code null}.
1764
     */
1765
    public static Error isCharsetSupported(final Charset charset) {
1766
        if (charset == null) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("charset", "isCharsetSupported");
1767

1768
        try {
1769
            if (!Charset.isSupported(charset.name())) {
1770
                return FileUtilsErrno.ERRNO_UNSUPPORTED_CHARSET.getError(charset.name());
1771
            }
1772
        } catch (Exception e) {
1773
            return FileUtilsErrno.ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED.getError(e, charset.name(), e.getMessage());
1774
        }
1775

1776
        return null;
1777
    }
1778

1779

1780

1781
    /**
1782
     * Close a {@link Closeable} object if not {@code null} and ignore any exceptions raised.
1783
     *
1784
     * @param closeable The {@link Closeable} object to close.
1785
     */
1786
    public static void closeCloseable(final Closeable closeable) {
1787
        if (closeable != null) {
1788
            try {
1789
                closeable.close();
1790
            }
1791
            catch (IOException e) {
1792
                // ignore
1793
            }
1794
        }
1795
    }
1796

1797

1798

1799
    /**
1800
     * Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
1801
     * will be removed.
1802
     *
1803
     * @param filePath The {@code path} for file to set permissions to.
1804
     * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
1805
     */
1806
    public static void setFilePermissions(final String filePath, final String permissionsToSet) {
1807
        setFilePermissions(null, filePath, permissionsToSet);
1808
    }
1809

1810
    /**
1811
     * Set permissions for file at path. Existing permission outside the {@code permissionsToSet}
1812
     * will be removed.
1813
     *
1814
     * @param label The optional label for the file. This can optionally be {@code null}.
1815
     * @param filePath The {@code path} for file to set permissions to.
1816
     * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
1817
     */
1818
    public static void setFilePermissions(String label, final String filePath, final String permissionsToSet) {
1819
        label = (label == null || label.isEmpty() ? "" : label + " ");
1820
        if (filePath == null || filePath.isEmpty()) return;
1821

1822
        if (!isValidPermissionString(permissionsToSet)) {
1823
            Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setFilePermissions: \"" + permissionsToSet + "\"");
1824
            return;
1825
        }
1826

1827
        File file = new File(filePath);
1828

1829
        if (permissionsToSet.contains("r")) {
1830
            if (!file.canRead()) {
1831
                Logger.logVerbose(LOG_TAG, "Setting read permissions for " + label + "file at path \"" + filePath + "\"");
1832
                file.setReadable(true);
1833
            }
1834
        } else {
1835
            if (file.canRead()) {
1836
                Logger.logVerbose(LOG_TAG, "Removing read permissions for " + label + "file at path \"" + filePath + "\"");
1837
                file.setReadable(false);
1838
            }
1839
        }
1840

1841

1842
        if (permissionsToSet.contains("w")) {
1843
            if (!file.canWrite()) {
1844
                Logger.logVerbose(LOG_TAG, "Setting write permissions for " + label + "file at path \"" + filePath + "\"");
1845
                file.setWritable(true);
1846
            }
1847
        } else {
1848
            if (file.canWrite()) {
1849
                Logger.logVerbose(LOG_TAG, "Removing write permissions for " + label + "file at path \"" + filePath + "\"");
1850
                file.setWritable(false);
1851
            }
1852
        }
1853

1854

1855
        if (permissionsToSet.contains("x")) {
1856
            if (!file.canExecute()) {
1857
                Logger.logVerbose(LOG_TAG, "Setting execute permissions for " + label + "file at path \"" + filePath + "\"");
1858
                file.setExecutable(true);
1859
            }
1860
        } else {
1861
            if (file.canExecute()) {
1862
                Logger.logVerbose(LOG_TAG, "Removing execute permissions for " + label + "file at path \"" + filePath + "\"");
1863
                file.setExecutable(false);
1864
            }
1865
        }
1866
    }
1867

1868

1869

1870
    /**
1871
     * Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
1872
     * will not be removed.
1873
     *
1874
     * @param filePath The {@code path} for file to set permissions to.
1875
     * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
1876
     */
1877
    public static void setMissingFilePermissions(final String filePath, final String permissionsToSet) {
1878
        setMissingFilePermissions(null, filePath, permissionsToSet);
1879
    }
1880

1881
    /**
1882
     * Set missing permissions for file at path. Existing permission outside the {@code permissionsToSet}
1883
     * will not be removed.
1884
     *
1885
     * @param label The optional label for the file. This can optionally be {@code null}.
1886
     * @param filePath The {@code path} for file to set permissions to.
1887
     * @param permissionsToSet The 3 character string that contains the "r", "w", "x" or "-" in-order.
1888
     */
1889
    public static void setMissingFilePermissions(String label, final String filePath, final String permissionsToSet) {
1890
        label = (label == null || label.isEmpty() ? "" : label + " ");
1891
        if (filePath == null || filePath.isEmpty()) return;
1892

1893
        if (!isValidPermissionString(permissionsToSet)) {
1894
            Logger.logError(LOG_TAG, "Invalid permissionsToSet passed to setMissingFilePermissions: \"" + permissionsToSet + "\"");
1895
            return;
1896
        }
1897

1898
        File file = new File(filePath);
1899

1900
        if (permissionsToSet.contains("r") && !file.canRead()) {
1901
            Logger.logVerbose(LOG_TAG, "Setting missing read permissions for " + label + "file at path \"" + filePath + "\"");
1902
            file.setReadable(true);
1903
        }
1904

1905
        if (permissionsToSet.contains("w") && !file.canWrite()) {
1906
            Logger.logVerbose(LOG_TAG, "Setting missing write permissions for " + label + "file at path \"" + filePath + "\"");
1907
            file.setWritable(true);
1908
        }
1909

1910
        if (permissionsToSet.contains("x") && !file.canExecute()) {
1911
            Logger.logVerbose(LOG_TAG, "Setting missing execute permissions for " + label + "file at path \"" + filePath + "\"");
1912
            file.setExecutable(true);
1913
        }
1914
    }
1915

1916

1917

1918
    /**
1919
     * Checking missing permissions for file at path.
1920
     *
1921
     * @param filePath The {@code path} for file to check permissions for.
1922
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
1923
     * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
1924
     *                              error is to be ignored.
1925
     * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}.
1926
     */
1927
    public static Error checkMissingFilePermissions(final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
1928
        return checkMissingFilePermissions(null, filePath, permissionsToCheck, ignoreIfNotExecutable);
1929
    }
1930

1931
    /**
1932
     * Checking missing permissions for file at path.
1933
     *
1934
     * @param label The optional label for the file. This can optionally be {@code null}.
1935
     * @param filePath The {@code path} for file to check permissions for.
1936
     * @param permissionsToCheck The 3 character string that contains the "r", "w", "x" or "-" in-order.
1937
     * @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
1938
     *                              error is to be ignored.
1939
     * @return Returns the {@code error} if validating permissions failed, otherwise {@code null}.
1940
     */
1941
    public static Error checkMissingFilePermissions(String label, final String filePath, final String permissionsToCheck, final boolean ignoreIfNotExecutable) {
1942
        label = (label == null || label.isEmpty() ? "" : label + " ");
1943
        if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "checkMissingFilePermissions");
1944

1945
        if (!isValidPermissionString(permissionsToCheck)) {
1946
            Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\"");
1947
            return FileUtilsErrno.ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK.getError();
1948
        }
1949

1950
        File file = new File(filePath);
1951

1952
        // If file is not readable
1953
        if (permissionsToCheck.contains("r") && !file.canRead()) {
1954
            return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file", filePath).setLabel(label + "file");
1955
        }
1956

1957
        // If file is not writable
1958
        if (permissionsToCheck.contains("w") && !file.canWrite()) {
1959
            return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file", filePath).setLabel(label + "file");
1960
        }
1961
        // If file is not executable
1962
        // This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
1963
        else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
1964
            return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file", filePath).setLabel(label + "file");
1965
        }
1966

1967
        return null;
1968
    }
1969

1970

1971

1972
    /**
1973
     * Checks whether string exactly matches the 3 character permission string that
1974
     * contains the "r", "w", "x" or "-" in-order.
1975
     *
1976
     * @param string The {@link String} to check.
1977
     * @return Returns {@code true} if string exactly matches a permission string, otherwise {@code false}.
1978
     */
1979
    public static boolean isValidPermissionString(final String string) {
1980
        if (string == null || string.isEmpty()) return false;
1981
        return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
1982
    }
1983

1984

1985

1986
    /**
1987
     * Get a {@link Error} that contains a shorter version of {@link Errno} message.
1988
     *
1989
     * @param error The original {@link Error} returned by one of the {@link FileUtils} functions.
1990
     * @return Returns the shorter {@link Error} if one exists, otherwise original {@code error}.
1991
     */
1992
    public static Error getShortFileUtilsError(final Error error) {
1993
        String type = error.getType();
1994
        if (!FileUtilsErrno.TYPE.equals(type)) return error;
1995

1996
        Errno shortErrno = FileUtilsErrno.ERRNO_SHORT_MAPPING.get(Errno.valueOf(type, error.getCode()));
1997
        if (shortErrno == null) return error;
1998

1999
        List<Throwable> throwables = error.getThrowablesList();
2000
        if (throwables.isEmpty())
2001
            return shortErrno.getError(DataUtils.getDefaultIfNull(error.getLabel(), "file"));
2002
        else
2003
            return shortErrno.getError(throwables, error.getLabel(), "file");
2004
    }
2005

2006

2007
    /**
2008
     * Get file dirname for file at {@code filePath}.
2009
     *
2010
     * @param filePath The {@code path} for file.
2011
     * @return Returns the file dirname if not {@code null}.
2012
     */
2013
    public static String getFileDirname(String filePath) {
2014
        if (DataUtils.isNullOrEmpty(filePath)) return null;
2015
        int lastSlash = filePath.lastIndexOf('/');
2016
        return (lastSlash == -1) ? null : filePath.substring(0, lastSlash);
2017
    }
2018

2019
    /**
2020
     * Get file basename for file at {@code filePath}.
2021
     *
2022
     * @param filePath The {@code path} for file.
2023
     * @return Returns the file basename if not {@code null}.
2024
     */
2025
    public static String getFileBasename(String filePath) {
2026
        if (DataUtils.isNullOrEmpty(filePath)) return null;
2027
        int lastSlash = filePath.lastIndexOf('/');
2028
        return (lastSlash == -1) ? filePath : filePath.substring(lastSlash + 1);
2029
    }
2030

2031
    /**
2032
     * Get file basename for file at {@code filePath} without extension.
2033
     *
2034
     * @param filePath The {@code path} for file.
2035
     * @return Returns the file basename without extension if not {@code null}.
2036
     */
2037
    public static String getFileBasenameWithoutExtension(String filePath) {
2038
        String fileBasename = getFileBasename(filePath);
2039
        if (DataUtils.isNullOrEmpty(fileBasename)) return null;
2040
        int lastDot = fileBasename.lastIndexOf('.');
2041
        return (lastDot == -1) ? fileBasename : fileBasename.substring(0, lastDot);
2042
    }
2043

2044
}
2045

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.