using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using API.DTOs.System; using API.Entities.Enums; using API.Extensions; using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services; #nullable enable public interface IDirectoryService { IFileSystem FileSystem { get; } string CacheDirectory { get; } string CoverImageDirectory { get; } string LogDirectory { get; } string TempDirectory { get; } string ConfigDirectory { get; } string SiteThemeDirectory { get; } string FaviconDirectory { get; } string LocalizationDirectory { get; } /// /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. /// string BookmarkDirectory { get; } /// /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. /// /// Absolute path of directory to scan. /// List of folder names IEnumerable ListDirectory(string rootPath); Task ReadFileAsync(string path); bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); bool Exists(string directory); void CopyFileToDirectory(string fullFilePath, string targetDirectory); int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); bool IsDriveMounted(string path); bool IsDirectoryEmpty(string path); long GetTotalSize(IEnumerable paths); void ClearDirectory(string directoryPath); void ClearAndDeleteDirectory(string directoryPath); string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, IList filePaths); IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); bool ExistOrCreate(string directoryPath); void DeleteFiles(IEnumerable files); void RemoveNonImages(string directoryName); void Flatten(string directoryName); Task CheckWriteAccess(string directoryName); IEnumerable GetFilesWithCertainExtensions(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); IEnumerable GetDirectories(string folderPath); IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); string GetParentDirectoryName(string fileOrFolder); IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null); DateTime GetLastWriteTime(string folderPath); GlobMatcher? CreateMatcherFromFile(string filePath); } public class DirectoryService : IDirectoryService { public const string KavitaIgnoreFile = ".kavitaignore"; public IFileSystem FileSystem { get; } public string CacheDirectory { get; } public string CoverImageDirectory { get; } public string LogDirectory { get; } public string TempDirectory { get; } public string ConfigDirectory { get; } public string BookmarkDirectory { get; } public string SiteThemeDirectory { get; } public string FaviconDirectory { get; } public string LocalizationDirectory { get; } private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; private static readonly Regex ExcludeDirectories = new Regex( @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb|\.caltrash|#recycle", MatchOptions, Tasks.Scanner.Parser.Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", MatchOptions, Tasks.Scanner.Parser.Parser.RegexTimeout); public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); public DirectoryService(ILogger logger, IFileSystem fileSystem) { _logger = logger; FileSystem = fileSystem; ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config"); ExistOrCreate(ConfigDirectory); CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers"); ExistOrCreate(CoverImageDirectory); CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache"); ExistOrCreate(CacheDirectory); LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs"); ExistOrCreate(LogDirectory); TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp"); ExistOrCreate(TempDirectory); BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks"); ExistOrCreate(BookmarkDirectory); SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes"); ExistOrCreate(SiteThemeDirectory); FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons"); ExistOrCreate(FaviconDirectory); LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N"); } /// /// Given a set of regex search criteria, get files in the given path. /// /// This will always exclude patterns /// Directory to search /// Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly /// List of file paths public IEnumerable GetFilesWithCertainExtensions(string path, string searchPatternExpression = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (!FileSystem.Directory.Exists(path)) return ImmutableList.Empty; var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); return FileSystem.Directory.EnumerateFiles(path, "*", searchOption) .Where(file => reSearchPattern.IsMatch(FileSystem.Path.GetExtension(file)) && !FileSystem.Path.GetFileName(file).StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)); } /// /// Returns a list of folders from end of fullPath to rootPath. If a file is passed at the end of the fullPath, it will be ignored. /// /// Example) (C:/Manga/, C:/Manga/Love Hina/Specials/Omake/) returns [Omake, Specials, Love Hina] /// /// /// /// public IEnumerable GetFoldersTillRoot(string rootPath, string fullPath) { var separator = FileSystem.Path.AltDirectorySeparatorChar; if (fullPath.Contains(FileSystem.Path.DirectorySeparatorChar)) { fullPath = fullPath.Replace(FileSystem.Path.DirectorySeparatorChar, FileSystem.Path.AltDirectorySeparatorChar); } if (rootPath.Contains(Path.DirectorySeparatorChar)) { rootPath = rootPath.Replace(FileSystem.Path.DirectorySeparatorChar, FileSystem.Path.AltDirectorySeparatorChar); } var path = fullPath.EndsWith(separator) ? fullPath.Substring(0, fullPath.Length - 1) : fullPath; var root = rootPath.EndsWith(separator) ? rootPath.Substring(0, rootPath.Length - 1) : rootPath; var paths = new List(); // If a file is at the end of the path, remove it before we start processing folders if (FileSystem.Path.GetExtension(path) != string.Empty) { path = path.Substring(0, path.LastIndexOf(separator)); } while (FileSystem.Path.GetDirectoryName(path) != Path.GetDirectoryName(root)) { var folder = FileSystem.DirectoryInfo.New(path).Name; paths.Add(folder); path = path.Substring(0, path.LastIndexOf(separator)); } return paths; } /// /// Does Directory Exist /// /// /// public bool Exists(string directory) { var di = FileSystem.DirectoryInfo.New(directory); return di.Exists; } /// /// Get files given a path. /// /// This will automatically filter out restricted files, like MacOsMetadata files /// /// An optional regex string to search against. Will use file path to match against. /// Defaults to top level directory only, can be given all to provide recursive searching /// public IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (!FileSystem.Directory.Exists(path)) return ImmutableList.Empty; if (fileNameRegex != string.Empty) { var reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); return FileSystem.Directory.EnumerateFiles(path, "*", searchOption) .Where(file => { var fileName = FileSystem.Path.GetFileName(file); return reSearchPattern.IsMatch(fileName) && !fileName.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith); }); } return FileSystem.Directory.EnumerateFiles(path, "*", searchOption).Where(file => !FileSystem.Path.GetFileName(file).StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)); } /// /// Copies a file into a directory. Does not maintain parent folder of file. /// Will create target directory if doesn't exist. Automatically overwrites what is there. /// /// /// public void CopyFileToDirectory(string fullFilePath, string targetDirectory) { try { var fileInfo = FileSystem.FileInfo.New(fullFilePath); if (!fileInfo.Exists) return; ExistOrCreate(targetDirectory); fileInfo.CopyTo(FileSystem.Path.Join(targetDirectory, fileInfo.Name), true); } catch (Exception ex) { _logger.LogError(ex, "There was a critical error when copying {File} to {Directory}", fullFilePath, targetDirectory); } } /// /// Copies all files and subdirectories within a directory to a target location /// /// Directory to copy from. Does not copy the parent folder /// Destination to copy to. Will be created if doesn't exist /// Defaults to all files /// If was successful /// Thrown when source directory does not exist public bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "") { if (string.IsNullOrEmpty(sourceDirName)) return false; // Get the subdirectories for the specified directory. var dir = FileSystem.DirectoryInfo.New(sourceDirName); if (!dir.Exists) { throw new DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } var dirs = dir.GetDirectories(); // If the destination directory doesn't exist, create it. ExistOrCreate(destDirName); // Get the files in the directory and copy them to the new location. var files = GetFilesWithExtension(dir.FullName, searchPattern).Select(n => FileSystem.FileInfo.New(n)); foreach (var file in files) { var tempPath = FileSystem.Path.Combine(destDirName, file.Name); file.CopyTo(tempPath, false); } // If copying subdirectories, copy them and their contents to new location. foreach (var subDir in dirs) { var tempPath = FileSystem.Path.Combine(destDirName, subDir.Name); CopyDirectoryToDirectory(subDir.FullName, tempPath); } return true; } /// /// Checks if the root path of a path exists or not. /// /// /// public bool IsDriveMounted(string path) { return FileSystem.DirectoryInfo.New(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists; } /// /// Checks if the root path of a path is empty or not. /// /// /// public bool IsDirectoryEmpty(string path) { return FileSystem.Directory.Exists(path) && !FileSystem.Directory.EnumerateFileSystemEntries(path).Any(); } public string[] GetFilesWithExtension(string path, string searchPatternExpression = "") { if (searchPatternExpression != string.Empty) { return GetFilesWithCertainExtensions(path, searchPatternExpression).ToArray(); } return !FileSystem.Directory.Exists(path) ? Array.Empty() : FileSystem.Directory.GetFiles(path); } /// /// Returns the total number of bytes for a given set of full file paths /// /// /// Total bytes public long GetTotalSize(IEnumerable paths) { return paths.Sum(path => FileSystem.FileInfo.New(path).Length); } /// /// Returns true if the path exists and is a directory. If path does not exist, this will create it. Returns false in all fail cases. /// /// /// public bool ExistOrCreate(string directoryPath) { var di = FileSystem.DirectoryInfo.New(directoryPath); if (di.Exists) return true; try { FileSystem.Directory.CreateDirectory(directoryPath); } catch (Exception) { return false; } return true; } /// /// Deletes all files within the directory, then the directory itself. /// /// public void ClearAndDeleteDirectory(string directoryPath) { if (!FileSystem.Directory.Exists(directoryPath)) return; var di = FileSystem.DirectoryInfo.New(directoryPath); ClearDirectory(directoryPath); di.Delete(true); } /// /// Deletes all files and folders within the directory path /// /// /// public void ClearDirectory(string directoryPath) { directoryPath = directoryPath.Replace(Environment.NewLine, string.Empty); var di = FileSystem.DirectoryInfo.New(directoryPath); if (!di.Exists) return; try { foreach (var file in di.EnumerateFiles()) { file.Delete(); } foreach (var dir in di.EnumerateDirectories()) { dir.Delete(true); } } catch (UnauthorizedAccessException ex) { _logger.LogError(ex, "[ClearDirectory] Could not delete {DirectoryPath} due to permission issue", directoryPath); } } /// /// Copies files to a destination directory. If the destination directory doesn't exist, this will create it. /// /// If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported. /// /// /// An optional string to prepend to the target file's name /// public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = "") { ExistOrCreate(directoryPath); string? currentFile = null; try { foreach (var file in filePaths) { currentFile = file; if (!FileSystem.File.Exists(file)) { _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); continue; } var fileInfo = FileSystem.FileInfo.New(file); var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(file, directoryPath, prepend)); fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name)); } } catch (Exception ex) { _logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath); return false; } return true; } /// /// Copies files to a destination directory. If the destination directory doesn't exist, this will create it. /// /// If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported. /// /// /// A list that matches one to one with filePaths. Each filepath will be renamed to newFilenames /// public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames) { ExistOrCreate(directoryPath); string? currentFile = null; var index = 0; try { foreach (var file in filePaths) { currentFile = file; if (!FileSystem.File.Exists(file)) { _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); continue; } var fileInfo = FileSystem.FileInfo.New(file); var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath)); fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name)); index++; } } catch (Exception ex) { _logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath); return false; } return true; } /// /// Generates the combined filepath given a prepend (optional), output directory path, and a full input file path. /// If the output file already exists, will append (1), (2), etc until it can be written out /// /// /// /// /// private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "") { while (true) { var fileInfo = FileSystem.FileInfo.New(fileToCopy); var filename = prepend + fileInfo.Name; var targetFile = FileSystem.FileInfo.New(FileSystem.Path.Join(directoryPath, filename)); if (!targetFile.Exists) { return targetFile.FullName; } var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name); if (FileCopyAppend.IsMatch(noExtension)) { var match = FileCopyAppend.Match(noExtension).Value; var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty); noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})"); } else { noExtension += " (1)"; } var newFilename = prepend + noExtension + FileSystem.Path.GetExtension(fileInfo.Name); fileToCopy = FileSystem.Path.Join(directoryPath, newFilename); } } /// /// Lists all directories in a root path. Will exclude Hidden or System directories. /// /// /// public IEnumerable ListDirectory(string rootPath) { if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList.Empty; var di = FileSystem.DirectoryInfo.New(rootPath); var dirs = di.GetDirectories() .Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System))) .Select(d => new DirectoryDto() { Name = d.Name, FullPath = d.FullName, }) .OrderBy(s => s.Name) .ToImmutableList(); return dirs; } /// /// Reads a file's into byte[]. Returns empty array if file doesn't exist. /// /// /// public async Task ReadFileAsync(string path) { if (!FileSystem.File.Exists(path)) return Array.Empty(); return await FileSystem.File.ReadAllBytesAsync(path); } /// /// Finds the highest directories from a set of file paths. Does not return the root path, will always select the highest non-root path. /// /// If the file paths do not contain anything from libraryFolders, this returns an empty dictionary back /// List of top level folders which files belong to /// List of file paths that belong to libraryFolders /// public Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, IList filePaths) { var stopLookingForDirectories = false; var dirs = new Dictionary(); foreach (var folder in libraryFolders.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) { if (stopLookingForDirectories) break; foreach (var file in filePaths.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) { if (!file.Contains(folder)) continue; var parts = GetFoldersTillRoot(folder, file).ToList(); if (parts.Count == 0) { // Break from all loops, we done, just scan folder.Path (library root) dirs.Add(folder, string.Empty); stopLookingForDirectories = true; break; } var fullPath = Tasks.Scanner.Parser.Parser.NormalizePath(Path.Join(folder, parts[parts.Count - 1])); dirs.TryAdd(fullPath, string.Empty); } } return dirs; } /// /// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope. /// /// /// List of directory paths, empty if path doesn't exist public IEnumerable GetDirectories(string folderPath) { if (!FileSystem.Directory.Exists(folderPath)) return ImmutableArray.Empty; return FileSystem.Directory.GetDirectories(folderPath) .Where(path => ExcludeDirectories.Matches(path).Count == 0); } /// /// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope. /// /// /// A set of glob rules that will filter directories out /// List of directory paths, empty if path doesn't exist public IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher) { if (matcher == null) return GetDirectories(folderPath); return GetDirectories(folderPath) .Where(folder => !matcher.ExcludeMatches( $"{FileSystem.DirectoryInfo.New(folder).Name}{FileSystem.Path.AltDirectorySeparatorChar}")); } /// /// Returns all directories, including subdirectories. Automatically excludes directories that shouldn't be in scope. /// /// /// public IEnumerable GetAllDirectories(string folderPath) { if (!FileSystem.Directory.Exists(folderPath)) return ImmutableArray.Empty; var directories = new List(); var foundDirs = GetDirectories(folderPath); foreach (var foundDir in foundDirs) { directories.Add(foundDir); directories.AddRange(GetAllDirectories(foundDir)); } return directories; } /// /// Returns the parent directories name for a file or folder. Empty string is path is not valid. /// /// /// public string GetParentDirectoryName(string fileOrFolder) { try { return Tasks.Scanner.Parser.Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName); } catch (Exception) { return string.Empty; } } /// /// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns /// /// /// /// /// public IList ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null) { _logger.LogDebug("[ScanFiles] called on {Path}", folderPath); var files = new List(); if (!Exists(folderPath)) return files; var potentialIgnoreFile = FileSystem.Path.Join(folderPath, KavitaIgnoreFile); if (matcher == null) { matcher = CreateMatcherFromFile(potentialIgnoreFile); } else { matcher.Merge(CreateMatcherFromFile(potentialIgnoreFile)); } var directories = GetDirectories(folderPath, matcher); foreach (var directory in directories) { files.AddRange(ScanFiles(directory, supportedExtensions, matcher)); } // Get the matcher from either ignore or global (default setup) if (matcher == null) { files.AddRange(GetFilesWithCertainExtensions(folderPath, supportedExtensions)); } else { var foundFiles = GetFilesWithCertainExtensions(folderPath, supportedExtensions) .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)); files.AddRange(foundFiles); } return files; } /// /// Recursively scans a folder and returns the max last write time on any folders and files /// /// If the folder is empty, this will return MaxValue for a DateTime /// /// Max Last Write Time public DateTime GetLastWriteTime(string folderPath) { if (!FileSystem.Directory.Exists(folderPath)) throw new IOException($"{folderPath} does not exist"); var fileEntries = FileSystem.Directory.GetFileSystemEntries(folderPath, "*.*", SearchOption.AllDirectories); if (fileEntries.Length == 0) return DateTime.MaxValue; return fileEntries.Max(path => FileSystem.File.GetLastWriteTime(path)); } /// /// Generates a GlobMatcher from a .kavitaignore file found at path. Returns null otherwise. /// /// /// public GlobMatcher? CreateMatcherFromFile(string filePath) { if (!FileSystem.File.Exists(filePath)) { return null; } // Read file in and add each line to Matcher var lines = FileSystem.File.ReadAllLines(filePath); if (lines.Length == 0) { return null; } GlobMatcher matcher = new(); foreach (var line in lines.Where(s => !string.IsNullOrEmpty(s))) { matcher.AddExclude(line); } return matcher; } /// /// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed /// up processing. /// NOTE: This is no longer parallel due to user's machines locking up /// /// Directory to scan /// Action to apply on file path /// Regex pattern to search against /// /// public int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger) { //Count of files traversed and timer for diagnostic output var fileCount = 0; // Data structure to hold names of subfolders to be examined for files. var dirs = new Stack(); if (!FileSystem.Directory.Exists(root)) { throw new ArgumentException("The directory doesn't exist"); } dirs.Push(root); while (dirs.Count > 0) { var currentDir = dirs.Pop(); IEnumerable subDirs; string[] files; try { subDirs = GetDirectories(currentDir); } // Thrown if we do not have discovery permission on the directory. catch (UnauthorizedAccessException e) { logger.LogCritical(e, "Unauthorized access on {Directory}", currentDir); continue; } // Thrown if another process has deleted the directory after we retrieved its name. catch (DirectoryNotFoundException e) { logger.LogCritical(e, "Directory not found on {Directory}", currentDir); continue; } try { files = GetFilesWithCertainExtensions(currentDir, searchPattern) .ToArray(); } catch (UnauthorizedAccessException e) { logger.LogCritical(e, "Unauthorized access on a file in {Directory}", currentDir); continue; } catch (DirectoryNotFoundException e) { logger.LogCritical(e, "Directory not found on a file in {Directory}", currentDir); continue; } catch (IOException e) { logger.LogCritical(e, "IO exception on a file in {Directory}", currentDir); continue; } // Execute in parallel if there are enough files in the directory. // Otherwise, execute sequentially. Files are opened and processed // synchronously but this could be modified to perform async I/O. try { foreach (var file in files) { action(file); fileCount++; } } catch (AggregateException ae) { ae.Handle((ex) => { if (ex is not UnauthorizedAccessException) return false; // Here we just output a message and go on. _logger.LogError(ex, "Unauthorized access on file"); return true; // Handle other exceptions here if necessary... }); } // Push the subdirectories onto the stack for traversal. // This could also be done before handing the files. foreach (var str in subDirs) dirs.Push(str); } return fileCount; } /// /// Attempts to delete the files passed to it. Swallows exceptions. /// /// Full path of files to delete public void DeleteFiles(IEnumerable files) { foreach (var file in files) { try { FileSystem.FileInfo.New(file).Delete(); } catch (Exception) { /* Swallow exception */ } } } /// /// Returns the human-readable file size for an arbitrary, 64-bit file size /// The default format is "0.## XB", e.g. "4.2 KB" or "1.43 GB" /// /// https://www.somacon.com/p576.php /// /// public static string GetHumanReadableBytes(long bytes) { // Get absolute value var absoluteBytes = (bytes < 0 ? -bytes : bytes); // Determine the suffix and readable value string suffix; double readable; switch (absoluteBytes) { // Exabyte case >= 0x1000000000000000: suffix = "EB"; readable = (bytes >> 50); break; // Petabyte case >= 0x4000000000000: suffix = "PB"; readable = (bytes >> 40); break; // Terabyte case >= 0x10000000000: suffix = "TB"; readable = (bytes >> 30); break; // Gigabyte case >= 0x40000000: suffix = "GB"; readable = (bytes >> 20); break; // Megabyte case >= 0x100000: suffix = "MB"; readable = (bytes >> 10); break; // Kilobyte case >= 0x400: suffix = "KB"; readable = bytes; break; default: return bytes.ToString("0 B"); // Byte } // Divide by 1024 to get fractional value readable = (readable / 1024); // Return formatted number with suffix return readable.ToString("0.## ") + suffix; } /// /// Removes all files except images from the directory. Includes sub directories. /// /// Fully qualified directory public void RemoveNonImages(string directoryName) { DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Tasks.Scanner.Parser.Parser.IsImage(file))); } /// /// Flattens all files in subfolders to the passed directory recursively. /// /// /// foo /// ├── 1.txt /// ├── 2.txt /// ├── 3.txt /// ├── 4.txt /// └── bar /// ├── 1.txt /// ├── 2.txt /// └── 5.txt /// /// becomes: /// foo /// ├── 1.txt /// ├── 2.txt /// ├── 3.txt /// ├── 4.txt /// ├── bar_1.txt /// ├── bar_2.txt /// └── bar_5.txt /// /// Fully qualified Directory name public void Flatten(string directoryName) { if (string.IsNullOrEmpty(directoryName) || !FileSystem.Directory.Exists(directoryName)) return; var directory = FileSystem.DirectoryInfo.New(directoryName); var index = 0; FlattenDirectory(directory, directory, ref index); } /// /// Checks whether a directory has write permissions /// /// Fully qualified path /// public async Task CheckWriteAccess(string directoryName) { try { ExistOrCreate(directoryName); await FileSystem.File.WriteAllTextAsync( FileSystem.Path.Join(directoryName, "test.txt"), string.Empty); } catch (Exception) { ClearAndDeleteDirectory(directoryName); return false; } ClearAndDeleteDirectory(directoryName); return true; } private static void FlattenDirectory(IFileSystemInfo root, IDirectoryInfo directory, ref int directoryIndex) { if (!root.FullName.Equals(directory.FullName)) { var fileIndex = 1; foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName)) { if (file.Directory == null) continue; var paddedIndex = Tasks.Scanner.Parser.Parser.PadZeros(directoryIndex + string.Empty); // We need to rename the files so that after flattening, they are in the order we found them var newName = $"{paddedIndex}_{Tasks.Scanner.Parser.Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; var newPath = Path.Join(root.FullName, newName); if (!File.Exists(newPath)) file.MoveTo(newPath); fileIndex++; } directoryIndex++; } foreach (var subDirectory in directory.EnumerateDirectories().OrderByNatural(d => d.FullName)) { // We need to check if the directory is not a blacklisted (ie __MACOSX) if (Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue; FlattenDirectory(root, subDirectory, ref directoryIndex); } } }