mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-02 21:24:18 -04:00
* Nothing changed, this is just to retrigger a stable build. (#1967) * v0.7.3 - The Quality of Life Update (#2036) * Version bump * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Merged develop in --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3 - The Quality of Life Update (#2041) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Fixed bug in CI pipeline for main --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Reverted a scaling issue for fit to width * Fixed an issue where creating a new library wouldn't persist advanced options due to a conflict with default value. When deleting a library, give the library name in the prompt. * Fixed kbd tags in epubs with paper theme having a style conflict. * Fixed an edge case where the incorrect first cover could be chosen in some strange grouping situations. * Manually sort directories as some OSes don't return them in a natural sort order. * Fixed an issue where autocompleting when adding a directory could throw an error when you're typing. --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
995 lines
38 KiB
C#
995 lines
38 KiB
C#
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.Extensions;
|
|
using Kavita.Common.Helpers;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace API.Services;
|
|
|
|
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; }
|
|
/// <summary>
|
|
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
|
|
/// </summary>
|
|
string BookmarkDirectory { get; }
|
|
/// <summary>
|
|
/// Lists out top-level folders for a given directory. Filters out System and Hidden folders.
|
|
/// </summary>
|
|
/// <param name="rootPath">Absolute path of directory to scan.</param>
|
|
/// <returns>List of folder names</returns>
|
|
IEnumerable<DirectoryDto> ListDirectory(string rootPath);
|
|
Task<byte[]> ReadFileAsync(string path);
|
|
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
|
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> newFilenames);
|
|
bool Exists(string directory);
|
|
void CopyFileToDirectory(string fullFilePath, string targetDirectory);
|
|
int TraverseTreeParallelForEach(string root, Action<string> action, string searchPattern, ILogger logger);
|
|
bool IsDriveMounted(string path);
|
|
bool IsDirectoryEmpty(string path);
|
|
long GetTotalSize(IEnumerable<string> paths);
|
|
void ClearDirectory(string directoryPath);
|
|
void ClearAndDeleteDirectory(string directoryPath);
|
|
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
|
bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "");
|
|
Dictionary<string, string> FindHighestDirectoriesFromFiles(IEnumerable<string> libraryFolders,
|
|
IList<string> filePaths);
|
|
IEnumerable<string> GetFoldersTillRoot(string rootPath, string fullPath);
|
|
IEnumerable<string> GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
|
bool ExistOrCreate(string directoryPath);
|
|
void DeleteFiles(IEnumerable<string> files);
|
|
void RemoveNonImages(string directoryName);
|
|
void Flatten(string directoryName);
|
|
Task<bool> CheckWriteAccess(string directoryName);
|
|
IEnumerable<string> GetFilesWithCertainExtensions(string path,
|
|
string searchPatternExpression = "",
|
|
SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
|
IEnumerable<string> GetDirectories(string folderPath);
|
|
IEnumerable<string> GetDirectories(string folderPath, GlobMatcher? matcher);
|
|
string GetParentDirectoryName(string fileOrFolder);
|
|
IList<string> ScanFiles(string folderPath, 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; }
|
|
private readonly ILogger<DirectoryService> _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",
|
|
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<DirectoryService> logger, IFileSystem fileSystem)
|
|
{
|
|
_logger = logger;
|
|
FileSystem = fileSystem;
|
|
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
|
|
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
|
|
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
|
|
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
|
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
|
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
|
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
|
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
|
|
|
ExistOrCreate(SiteThemeDirectory);
|
|
ExistOrCreate(CoverImageDirectory);
|
|
ExistOrCreate(CacheDirectory);
|
|
ExistOrCreate(LogDirectory);
|
|
ExistOrCreate(TempDirectory);
|
|
ExistOrCreate(BookmarkDirectory);
|
|
ExistOrCreate(FaviconDirectory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a set of regex search criteria, get files in the given path.
|
|
/// </summary>
|
|
/// <remarks>This will always exclude <see cref="Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith"/> patterns</remarks>
|
|
/// <param name="path">Directory to search</param>
|
|
/// <param name="searchPatternExpression">Regex version of search pattern (ie \.mp3|\.mp4). Defaults to * meaning all files.</param>
|
|
/// <param name="searchOption">SearchOption to use, defaults to TopDirectoryOnly</param>
|
|
/// <returns>List of file paths</returns>
|
|
public IEnumerable<string> GetFilesWithCertainExtensions(string path,
|
|
string searchPatternExpression = "",
|
|
SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
|
{
|
|
if (!FileSystem.Directory.Exists(path)) return ImmutableList<string>.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));
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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]
|
|
/// </summary>
|
|
/// <param name="rootPath"></param>
|
|
/// <param name="fullPath"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<string> 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<string>();
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does Directory Exist
|
|
/// </summary>
|
|
/// <param name="directory"></param>
|
|
/// <returns></returns>
|
|
public bool Exists(string directory)
|
|
{
|
|
var di = FileSystem.DirectoryInfo.New(directory);
|
|
return di.Exists;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get files given a path.
|
|
/// </summary>
|
|
/// <remarks>This will automatically filter out restricted files, like MacOsMetadata files</remarks>
|
|
/// <param name="path"></param>
|
|
/// <param name="fileNameRegex">An optional regex string to search against. Will use file path to match against.</param>
|
|
/// <param name="searchOption">Defaults to top level directory only, can be given all to provide recursive searching</param>
|
|
/// <returns></returns>
|
|
public IEnumerable<string> GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
|
{
|
|
if (!FileSystem.Directory.Exists(path)) return ImmutableList<string>.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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="fullFilePath"></param>
|
|
/// <param name="targetDirectory"></param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies all files and subdirectories within a directory to a target location
|
|
/// </summary>
|
|
/// <param name="sourceDirName">Directory to copy from. Does not copy the parent folder</param>
|
|
/// <param name="destDirName">Destination to copy to. Will be created if doesn't exist</param>
|
|
/// <param name="searchPattern">Defaults to all files</param>
|
|
/// <returns>If was successful</returns>
|
|
/// <exception cref="DirectoryNotFoundException">Thrown when source directory does not exist</exception>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the root path of a path exists or not.
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
public bool IsDriveMounted(string path)
|
|
{
|
|
return FileSystem.DirectoryInfo.New(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Checks if the root path of a path is empty or not.
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
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<string>() : FileSystem.Directory.GetFiles(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the total number of bytes for a given set of full file paths
|
|
/// </summary>
|
|
/// <param name="paths"></param>
|
|
/// <returns>Total bytes</returns>
|
|
public long GetTotalSize(IEnumerable<string> paths)
|
|
{
|
|
return paths.Sum(path => FileSystem.FileInfo.New(path).Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="directoryPath"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all files within the directory, then the directory itself.
|
|
/// </summary>
|
|
/// <param name="directoryPath"></param>
|
|
public void ClearAndDeleteDirectory(string directoryPath)
|
|
{
|
|
if (!FileSystem.Directory.Exists(directoryPath)) return;
|
|
|
|
var di = FileSystem.DirectoryInfo.New(directoryPath);
|
|
|
|
ClearDirectory(directoryPath);
|
|
|
|
di.Delete(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all files and folders within the directory path
|
|
/// </summary>
|
|
/// <param name="directoryPath"></param>
|
|
/// <returns></returns>
|
|
public void ClearDirectory(string directoryPath)
|
|
{
|
|
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);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies files to a destination directory. If the destination directory doesn't exist, this will create it.
|
|
/// </summary>
|
|
/// <remarks>If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported.</remarks>
|
|
/// <param name="filePaths"></param>
|
|
/// <param name="directoryPath"></param>
|
|
/// <param name="prepend">An optional string to prepend to the target file's name</param>
|
|
/// <returns></returns>
|
|
public bool CopyFilesToDirectory(IEnumerable<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies files to a destination directory. If the destination directory doesn't exist, this will create it.
|
|
/// </summary>
|
|
/// <remarks>If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported.</remarks>
|
|
/// <param name="filePaths"></param>
|
|
/// <param name="directoryPath"></param>
|
|
/// <param name="newFilenames">A list that matches one to one with filePaths. Each filepath will be renamed to newFilenames</param>
|
|
/// <returns></returns>
|
|
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="fileToCopy"></param>
|
|
/// <param name="directoryPath"></param>
|
|
/// <param name="prepend"></param>
|
|
/// <returns></returns>
|
|
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 (FileCopyAppendRegex().IsMatch(noExtension))
|
|
if (FileCopyAppend.IsMatch(noExtension))
|
|
{
|
|
//var match = FileCopyAppendRegex().Match(noExtension).Value;
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lists all directories in a root path. Will exclude Hidden or System directories.
|
|
/// </summary>
|
|
/// <param name="rootPath"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<DirectoryDto> ListDirectory(string rootPath)
|
|
{
|
|
if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList<DirectoryDto>.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a file's into byte[]. Returns empty array if file doesn't exist.
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <returns></returns>
|
|
public async Task<byte[]> ReadFileAsync(string path)
|
|
{
|
|
if (!FileSystem.File.Exists(path)) return Array.Empty<byte>();
|
|
return await FileSystem.File.ReadAllBytesAsync(path);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Finds the highest directories from a set of file paths. Does not return the root path, will always select the highest non-root path.
|
|
/// </summary>
|
|
/// <remarks>If the file paths do not contain anything from libraryFolders, this returns an empty dictionary back</remarks>
|
|
/// <param name="libraryFolders">List of top level folders which files belong to</param>
|
|
/// <param name="filePaths">List of file paths that belong to libraryFolders</param>
|
|
/// <returns></returns>
|
|
public Dictionary<string, string> FindHighestDirectoriesFromFiles(IEnumerable<string> libraryFolders, IList<string> filePaths)
|
|
{
|
|
var stopLookingForDirectories = false;
|
|
var dirs = new Dictionary<string, string>();
|
|
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.Last()));
|
|
if (!dirs.ContainsKey(fullPath))
|
|
{
|
|
dirs.Add(fullPath, string.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
return dirs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope.
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <returns>List of directory paths, empty if path doesn't exist</returns>
|
|
public IEnumerable<string> GetDirectories(string folderPath)
|
|
{
|
|
if (!FileSystem.Directory.Exists(folderPath)) return ImmutableArray<string>.Empty;
|
|
return FileSystem.Directory.GetDirectories(folderPath)
|
|
.Where(path => ExcludeDirectories.Matches(path).Count == 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope.
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <param name="matcher">A set of glob rules that will filter directories out</param>
|
|
/// <returns>List of directory paths, empty if path doesn't exist</returns>
|
|
public IEnumerable<string> 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}"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all directories, including subdirectories. Automatically excludes directories that shouldn't be in scope.
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<string> GetAllDirectories(string folderPath)
|
|
{
|
|
if (!FileSystem.Directory.Exists(folderPath)) return ImmutableArray<string>.Empty;
|
|
var directories = new List<string>();
|
|
|
|
var foundDirs = GetDirectories(folderPath);
|
|
foreach (var foundDir in foundDirs)
|
|
{
|
|
directories.Add(foundDir);
|
|
directories.AddRange(GetAllDirectories(foundDir));
|
|
}
|
|
|
|
return directories;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the parent directories name for a file or folder. Empty string is path is not valid.
|
|
/// </summary>
|
|
/// <param name="fileOrFolder"></param>
|
|
/// <returns></returns>
|
|
public string GetParentDirectoryName(string fileOrFolder)
|
|
{
|
|
try
|
|
{
|
|
return Tasks.Scanner.Parser.Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans a directory by utilizing a recursive folder search. If a .kavitaignore file is found, will ignore matching patterns
|
|
/// </summary>
|
|
/// <param name="folderPath"></param>
|
|
/// <param name="matcher"></param>
|
|
/// <returns></returns>
|
|
public IList<string> ScanFiles(string folderPath, GlobMatcher? matcher = null)
|
|
{
|
|
_logger.LogDebug("[ScanFiles] called on {Path}", folderPath);
|
|
var files = new List<string>();
|
|
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, matcher));
|
|
}
|
|
|
|
|
|
// Get the matcher from either ignore or global (default setup)
|
|
if (matcher == null)
|
|
{
|
|
files.AddRange(GetFilesWithCertainExtensions(folderPath, Tasks.Scanner.Parser.Parser.SupportedExtensions));
|
|
}
|
|
else
|
|
{
|
|
var foundFiles = GetFilesWithCertainExtensions(folderPath,
|
|
Tasks.Scanner.Parser.Parser.SupportedExtensions)
|
|
.Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name));
|
|
files.AddRange(foundFiles);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively scans a folder and returns the max last write time on any folders and files
|
|
/// </summary>
|
|
/// <remarks>If the folder is empty, this will return MaxValue for a DateTime</remarks>
|
|
/// <param name="folderPath"></param>
|
|
/// <returns>Max Last Write Time</returns>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a GlobMatcher from a .kavitaignore file found at path. Returns null otherwise.
|
|
/// </summary>
|
|
/// <param name="filePath"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="root">Directory to scan</param>
|
|
/// <param name="action">Action to apply on file path</param>
|
|
/// <param name="searchPattern">Regex pattern to search against</param>
|
|
/// <param name="logger"></param>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public int TraverseTreeParallelForEach(string root, Action<string> 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<string>();
|
|
|
|
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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to delete the files passed to it. Swallows exceptions.
|
|
/// </summary>
|
|
/// <param name="files">Full path of files to delete</param>
|
|
public void DeleteFiles(IEnumerable<string> files)
|
|
{
|
|
foreach (var file in files)
|
|
{
|
|
try
|
|
{
|
|
FileSystem.FileInfo.New(file).Delete();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
/* Swallow exception */
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the human-readable file size for an arbitrary, 64-bit file size
|
|
/// <remarks>The default format is "0.## XB", e.g. "4.2 KB" or "1.43 GB"</remarks>
|
|
/// </summary>
|
|
/// https://www.somacon.com/p576.php
|
|
/// <param name="bytes"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all files except images from the directory. Includes sub directories.
|
|
/// </summary>
|
|
/// <param name="directoryName">Fully qualified directory</param>
|
|
public void RemoveNonImages(string directoryName)
|
|
{
|
|
DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Tasks.Scanner.Parser.Parser.IsImage(file)));
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Flattens all files in subfolders to the passed directory recursively.
|
|
///
|
|
///
|
|
/// foo<para />
|
|
/// ├── 1.txt<para />
|
|
/// ├── 2.txt<para />
|
|
/// ├── 3.txt<para />
|
|
/// ├── 4.txt<para />
|
|
/// └── bar<para />
|
|
/// ├── 1.txt<para />
|
|
/// ├── 2.txt<para />
|
|
/// └── 5.txt<para />
|
|
///
|
|
/// becomes:<para />
|
|
/// foo<para />
|
|
/// ├── 1.txt<para />
|
|
/// ├── 2.txt<para />
|
|
/// ├── 3.txt<para />
|
|
/// ├── 4.txt<para />
|
|
/// ├── bar_1.txt<para />
|
|
/// ├── bar_2.txt<para />
|
|
/// └── bar_5.txt<para />
|
|
/// </summary>
|
|
/// <param name="directoryName">Fully qualified Directory name</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a directory has write permissions
|
|
/// </summary>
|
|
/// <param name="directoryName">Fully qualified path</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> 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 + "");
|
|
// 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 + "")}{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);
|
|
}
|
|
}
|
|
}
|