mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -04:00
* Refactored the drawer into offcanvas component. Had to write some hacks to emulate how bootstrap's javascript implementation works as ngBootstrap doesn't have a component yet. * Cleaned up some of the code * Rewrote drawer to align it with the new design * First pass, refactored table of content into it's own component * Refactored all of the settings logic into a separate component. Everything is broken. * More settings on on reactive form * More code cleanup on settings * Misc fixes around the drawer code. Fixed a bug where range sliders were inheriting background color of normal text inputs * Fixed dark mode with book reader. We now clear the theme from the main app so book reader is self-contained. Styles for dark mode are injected into the reading-section. Styles that were previously in scss are now only for the actual menu system. * Cleaned up drawer styling on header * Removed an ngIf statement for click to paginate * Tweaked the accent style to have smaller font size and adjusted style on light mode. Cleaned up some clearTimeout code in a further effort to streamline codebase. * Refactored Dark mode into a basic theme. Currently styles are hardcoded. * Patched book theme in from themes branch * Patched in the backend for Book Theme (not tested yet) * Fixed a bug in seeding code for book themes. Started integration of themes into the reader settings * Everything except managing themes is working. Themes are a bit shakey, having second thoughts if we should have them or not. * Reverted the ability to do custom user book themes. Code is stable with system themes. * Stablize the Styles (#1128) * Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful. * When a series has a reading list, we now show the connection on Series detail. * Removed all baseurl code from UI and not-connected component since we no longer use it. * Fixed tag badges not showing a border. Added last read time to the series detail page * Fixed up error interceptor to remove no-connection code * Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI. * Cleaned up the selected tab and tab display logic * Fixed a bad where statement in reading lists for series * Fixed up tab logic again * Fixed a small margin on search backdrop * Made badge expander button smaller to align with badges * Fixed a few UIs due to .form-group and .form-row being removed * Updated Theme component page to help with style testing * Added more components to theme tester * Cleaned up some styling * Fixed opacity on search item hover * Bump versions by dotnet-bump-version. * Tweaked the accordion styles for light mode * Set dark book theme as default. Refactored resetSettings to be much cleaner * Started the refactor to allow book themes to affect global css variables * Fixed some issues with my css variable declarations * Fixed a close model state update * Lots of work, but dark mode on the book reader is basically done. We have to code the themes much like the site themes * Some black theme enhancements * Started working on column layout in book reader. * Cleaned up the CSS on Reader Settings * Hooked up reading direction * Got column and double column layout working * Implemented some basic virtual paging and hooked in book color theme and layout mode into user preferences. * Migration wrote, can edit page layout and color theme on book reader. Removed book dark mode since no longer needed. Fixed a bug on login/register forms where when input is focused, text is white and not black. * When loading book reader, apply column layout. * Lots of work around 2 column layout, working on images not splitting. Still not working, committing so i can merge develop in and validate code with new manga reader. * Fixed images being split into 2 BUT regression on each page boundary, total reading height is smaller and smaller * Fixed some rendering bugs where toggling column layouts would shrink images on screen constantly. Fixed a bug where bottom bar wouldn't render on column layout in some conditions (this might need to be reworked) * Started progress on progress work * Updated .NET to 6.0.4 * Fixed a bug where DataContextModelSnapshot was being removed on build thus new migrations were broken. * Tweaked the code around progress saving so that we don't loose track of last scroll element on page load * Trying to restore progress, but stuck * Extra merge stuff * Fixed a bug where volumes that are a range fail to generate series detail * No gutters on whole app. Book reader backend now applies the image class automatically at the backend. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Removed bottom padding * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Fixed the height of the action bar to simplify logic and keep the code cleaner. Refactored book service image scoping to be much more streamlined and efficient * Fixed the height of action bar to 62px and adjusted code to use the hardcoded px. (code commented) * Removed commented out code from fixed action bar height * Progress restoration seems to be working * Code cleanup * Ensure the bottom action bar is at the bottom of the viewport on small pages * Fixed book fonts not setting properly and added OpenDyslexic font. * Fixed up some font issues * Updated drawer so all sections are open by default * Switched some LINQ to use MinBy * When navigating between pages and column layout, adjust the shift for the user. * Removed some debug code * Blacklist .qpkg folders and don't scan Recently-Snapshot or recycle folders. * Renamed the scale width to be scoped to kavita to avoid conflicts. * Refactored ngx-sliders out to use normal range instead. Changed up the preferences to separate image and book settinngs into own accordion. * updated user preferences for new migration options (not committed yet) * Removed some debug code * Remove console.logs * Migration committed, let's release this to users. * A lot of crazy code just to ensure that when you close drawer the toggle reflectst that state.
768 lines
31 KiB
C#
768 lines
31 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.Extensions;
|
|
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; }
|
|
/// <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<string> ListDirectory(string rootPath);
|
|
Task<byte[]> ReadFileAsync(string path);
|
|
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
|
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);
|
|
}
|
|
public class DirectoryService : IDirectoryService
|
|
{
|
|
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; }
|
|
private readonly ILogger<DirectoryService> _logger;
|
|
|
|
private static readonly Regex ExcludeDirectories = new Regex(
|
|
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
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");
|
|
|
|
ExistOrCreate(SiteThemeDirectory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a set of regex search criteria, get files in the given path.
|
|
/// </summary>
|
|
/// <remarks>This will always exclude <see cref="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>
|
|
private 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);
|
|
|
|
return FileSystem.Directory.EnumerateFiles(path, "*", searchOption)
|
|
.Where(file =>
|
|
reSearchPattern.IsMatch(FileSystem.Path.GetExtension(file)) && !FileSystem.Path.GetFileName(file).StartsWith(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.FromDirectoryName(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.FromDirectoryName(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);
|
|
return FileSystem.Directory.EnumerateFiles(path, "*", searchOption)
|
|
.Where(file =>
|
|
{
|
|
var fileName = FileSystem.Path.GetFileName(file);
|
|
return reSearchPattern.IsMatch(fileName) &&
|
|
!fileName.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith);
|
|
});
|
|
}
|
|
|
|
return FileSystem.Directory.EnumerateFiles(path, "*", searchOption).Where(file =>
|
|
!FileSystem.Path.GetFileName(file).StartsWith(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.FromFileName(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.FromDirectoryName(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.FromFileName(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.FromDirectoryName(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.FromFileName(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.FromDirectoryName(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.FromDirectoryName(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.FromDirectoryName(directoryPath);
|
|
if (!di.Exists) return;
|
|
|
|
foreach (var file in di.EnumerateFiles())
|
|
{
|
|
file.Delete();
|
|
}
|
|
foreach (var dir in di.EnumerateDirectories())
|
|
{
|
|
dir.Delete(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="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.FromFileName(file);
|
|
var targetFile = FileSystem.FileInfo.FromFileName(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>
|
|
/// 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 = "")
|
|
{
|
|
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
|
|
var filename = prepend + fileInfo.Name;
|
|
|
|
var targetFile = FileSystem.FileInfo.FromFileName(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);
|
|
return RenameFileForCopy(FileSystem.Path.Join(directoryPath, newFilename), directoryPath, prepend);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lists all directories in a root path. Will exclude Hidden or System directories.
|
|
/// </summary>
|
|
/// <param name="rootPath"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<string> ListDirectory(string rootPath)
|
|
{
|
|
if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList<string>.Empty;
|
|
|
|
var di = FileSystem.DirectoryInfo.FromDirectoryName(rootPath);
|
|
var dirs = di.GetDirectories()
|
|
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
|
.Select(d => d.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)
|
|
{
|
|
if (stopLookingForDirectories) break;
|
|
foreach (var file in filePaths)
|
|
{
|
|
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 = Path.Join(folder, parts.Last());
|
|
if (!dirs.ContainsKey(fullPath))
|
|
{
|
|
dirs.Add(fullPath, string.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
return dirs;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Recursively scans files and applies an action on them. This uses as many cores the underlying PC has to speed
|
|
/// up processing.
|
|
/// </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 = FileSystem.Directory.GetDirectories(currentDir).Where(path => ExcludeDirectories.Matches(path).Count == 0);
|
|
}
|
|
// Thrown if we do not have discovery permission on the directory.
|
|
catch (UnauthorizedAccessException e) {
|
|
Console.WriteLine(e.Message);
|
|
logger.LogError(e, "Unauthorized access on {Directory}", currentDir);
|
|
continue;
|
|
}
|
|
// Thrown if another process has deleted the directory after we retrieved its name.
|
|
catch (DirectoryNotFoundException e) {
|
|
Console.WriteLine(e.Message);
|
|
logger.LogError(e, "Directory not found on {Directory}", currentDir);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
files = GetFilesWithCertainExtensions(currentDir, searchPattern)
|
|
.ToArray();
|
|
}
|
|
catch (UnauthorizedAccessException e) {
|
|
Console.WriteLine(e.Message);
|
|
continue;
|
|
}
|
|
catch (DirectoryNotFoundException e) {
|
|
Console.WriteLine(e.Message);
|
|
continue;
|
|
}
|
|
catch (IOException e) {
|
|
Console.WriteLine(e.Message);
|
|
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 UnauthorizedAccessException) {
|
|
// Here we just output a message and go on.
|
|
Console.WriteLine(ex.Message);
|
|
_logger.LogError(ex, "Unauthorized access on file");
|
|
return true;
|
|
}
|
|
// Handle other exceptions here if necessary...
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// 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.FromFileName(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 => !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.FromDirectoryName(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 ex)
|
|
{
|
|
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 = 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}_{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 (Parser.Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue;
|
|
|
|
FlattenDirectory(root, subDirectory, ref directoryIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|