diff --git a/FileSorter.csproj b/FileSorter.csproj index 4960df1..1b273c7 100644 --- a/FileSorter.csproj +++ b/FileSorter.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 FileSorter.Program false 0.0.1 @@ -20,7 +20,7 @@ - + diff --git a/Program.cs b/Program.cs index fcc6f71..76d9b15 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,6 @@ -using System; +namespace FileSorter; + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,180 +10,179 @@ using PowerArgs; -namespace FileSorter +class Program { - class Program + [TabCompletion, MyArgHook] + class ProgramArgs { - [TabCompletion, MyArgHook] - class ProgramArgs - { - [HelpHook, ArgShortcut("?"), ArgShortcut("h"), ArgShortcut("--?"), ArgShortcut("--h"), ArgDescription("Shows help")] - public bool Help { get; set; } + [HelpHook, ArgShortcut("?"), ArgShortcut("h"), ArgShortcut("--?"), ArgShortcut("--h"), ArgDescription("Shows help")] + public bool Help { get; set; } + + [ArgShortcut("i"), ArgShortcut("--i"), ArgShortcut("in"), ArgShortcut("--in"), ArgDescription(@"The directory containing the files to process"), ArgDefaultValue(@"."), ArgExistingDirectory] + public string InputDirectory { get; set; } + + [ArgShortcut("o"), ArgShortcut("--o"), ArgShortcut("out"), ArgShortcut("--out"), ArgDescription(@"The directory in which to create folders containing the sorted files by year & month"), ArgDefaultValue(@".\out")] + public string OutputDirectory { get; set; } - [ArgShortcut("i"), ArgShortcut("--i"), ArgShortcut("in"), ArgShortcut("--in"), ArgDescription(@"The directory containing the files to process"), ArgDefaultValue(@"."), ArgExistingDirectory] - public string InputDirectory { get; set; } + [ArgShortcut("p"), ArgShortcut("--p"), ArgDescription(@"To denote the folder being processed contains images whose EXIF date should be used, if possible"), ArgDefaultValue(false)] + public bool IsPictures { get; set; } - [ArgShortcut("o"), ArgShortcut("--o"), ArgShortcut("out"), ArgShortcut("--out"), ArgDescription(@"The directory in which to create folders containing the sorted files by year & month"), ArgDefaultValue(@".\out")] - public string OutputDirectory { get; set; } + [ArgShortcut("whatif"), ArgShortcut("--whatif"), ArgDescription(@"Don't actually move files, just show what would happen if we were to move them"), ArgDefaultValue(false)] + public bool NoOp { get; set; } - [ArgShortcut("p"), ArgShortcut("--p"), ArgDescription(@"To denote the folder being processed contains images whose EXIF date should be used, if possible"), ArgDefaultValue(false)] - public bool IsPictures { get; set; } + [ArgShortcut("f"), ArgShortcut("--f"), ArgDescription(@"Automatically overwrite files in destination, if they exist"), ArgDefaultValue(false)] + public bool Force { get; set; } - [ArgShortcut("whatif"), ArgShortcut("--whatif"), ArgDescription(@"Don't actually move files, just show what would happen if we were to move them"), ArgDefaultValue(false)] - public bool NoOp { get; set; } + [ArgShortcut("u"), ArgShortcut("--u"), ArgDescription(@"True to update the creation & write time to match EXIF time, false otherwise"), ArgDefaultValue(false)] + public bool UpdateTimestamp { get; set; } - [ArgShortcut("f"), ArgShortcut("--f"), ArgDescription(@"Automatically overwrite files in destination, if they exist"), ArgDefaultValue(false)] - public bool Force { get; set; } + [ArgShortcut("n"), ArgShortcut("--n"), ArgDescription(@"Don't move any files (useful with -u to update times only)"), ArgDefaultValue(false)] + public bool NoMove { get; set; } - [ArgShortcut("u"), ArgShortcut("--u"), ArgDescription(@"True to update the creation & write time to match EXIF time, false otherwise"), ArgDefaultValue(false)] - public bool UpdateTimestamp { get; set; } + [ArgShortcut("r"), ArgShortcut("--r"), ArgDescription(@"True to process all files in all subdirectories of Input Directory. Compatible only with -NoMove"), ArgDefaultValue(false)] + public bool Recurse { get; set; } - [ArgShortcut("n"), ArgShortcut("--n"), ArgDescription(@"Don't move any files (useful with -u to update times only)"), ArgDefaultValue(false)] - public bool NoMove { get; set; } + [ArgShortcut("y"), ArgShortcut("--y"), ArgShortcut("--confirm"), ArgDescription(@"Do not prompt to commence operation"), ArgDefaultValue(false)] + public bool Confirm { get; set; } + } - [ArgShortcut("r"), ArgShortcut("--r"), ArgDescription(@"True to process all files in all subdirectories of Input Directory. Compatible only with -NoMove"), ArgDefaultValue(false)] - public bool Recurse { get; set; } + class MyArgHook : ArgHook + { + public override void BeforeValidateDefinition(HookContext context) + { + context.Definition.IsNonInteractive = true; - [ArgShortcut("y"), ArgShortcut("--y"), ArgShortcut("--confirm"), ArgDescription(@"Do not prompt to commence operation"), ArgDefaultValue(false)] - public bool Confirm { get; set; } + base.BeforeValidateDefinition(context); } - class MyArgHook : ArgHook + public override void AfterPopulateProperties(HookContext context) { - public override void BeforeValidateDefinition(HookContext context) - { - context.Definition.IsNonInteractive = true; + var input = context.Args as ProgramArgs; - base.BeforeValidateDefinition(context); + var errMsgs = new List(); + if (input.Recurse && !input.NoMove) + { + errMsgs.Add("ERROR: Recurse is only available if NoMove is also true"); } - public override void AfterPopulateProperties(HookContext context) + if (input.NoOp && input.Force) { - var input = context.Args as ProgramArgs; - - var errMsgs = new List(); - if (input.Recurse && !input.NoMove) - { - errMsgs.Add("ERROR: Recurse is only available if NoMove is also true"); - } - - if (input.NoOp && input.Force) - { - errMsgs.Add("ERROR: NoOp and Force cannot both be true"); - } + errMsgs.Add("ERROR: NoOp and Force cannot both be true"); + } - if (errMsgs.Any()) - { - Console.WriteLine(string.Concat(string.Join(Environment.NewLine, errMsgs), Environment.NewLine)); - PrintUsage(); - context.CancelAllProcessing(); - } + if (errMsgs.Any()) + { + Console.WriteLine(string.Concat(string.Join(Environment.NewLine, errMsgs), Environment.NewLine)); + PrintUsage(); + context.CancelAllProcessing(); } } + } - static void Main(string[] args) + static void Main(string[] args) + { + ProgramArgs input; + try { - ProgramArgs input; - try - { - input = PowerArgs.Args.Parse(args); + input = PowerArgs.Args.Parse(args); - if (input == null || input.Help) - { - // means client ran with '-h' to output help. - return; - } - } - catch (PowerArgs.ArgException ex) + if (input == null || input.Help) { - Console.WriteLine(ex.Message); + // means client ran with '-h' to output help. return; } + } + catch (PowerArgs.ArgException ex) + { + Console.WriteLine(ex.Message); + return; + } - var inputDirectory = string.IsNullOrEmpty(input.InputDirectory) ? Environment.CurrentDirectory : input.InputDirectory; - var outputDirectory = string.IsNullOrEmpty(input.OutputDirectory) ? inputDirectory : input.OutputDirectory; + var inputDirectory = string.IsNullOrEmpty(input.InputDirectory) ? Environment.CurrentDirectory : input.InputDirectory; + var outputDirectory = string.IsNullOrEmpty(input.OutputDirectory) ? inputDirectory : input.OutputDirectory; - var inputDirectoryInfo = new DirectoryInfo(inputDirectory); - var filesToProcess = inputDirectoryInfo.EnumerateFiles(@"*", new EnumerationOptions - { - RecurseSubdirectories = input.Recurse, - MatchCasing = MatchCasing.CaseInsensitive, - MatchType = MatchType.Simple, - ReturnSpecialDirectories = false - }); + var inputDirectoryInfo = new DirectoryInfo(inputDirectory); + var filesToProcess = inputDirectoryInfo.EnumerateFiles(@"*", new EnumerationOptions + { + RecurseSubdirectories = input.Recurse, + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + ReturnSpecialDirectories = false + }); - var outputDirectoryInfo = new DirectoryInfo(outputDirectory); + var outputDirectoryInfo = new DirectoryInfo(outputDirectory); - if (!input.Confirm) + if (!input.Confirm) + { + Console.WriteLine($@"About to process {filesToProcess.LongCount()} files(s) from {inputDirectoryInfo.FullName} into {outputDirectoryInfo.FullName} ..."); + Console.WriteLine(@"This operation cannot be undone! Press any key to continue or Esc to cancel"); + if (Console.ReadKey().Key == ConsoleKey.Escape) { - Console.WriteLine($@"About to process {filesToProcess.LongCount()} files(s) from {inputDirectoryInfo.FullName} into {outputDirectoryInfo.FullName} ..."); - Console.WriteLine(@"This operation cannot be undone! Press any key to continue or Esc to cancel"); - if (Console.ReadKey().Key == ConsoleKey.Escape) - { - return; - } + return; } + } - Console.WriteLine(@"Processing files..."); + Console.WriteLine(@"Processing files..."); - Parallel.ForEach(filesToProcess, fi => - { - var timeToUse = new[] { fi.CreationTime, fi.LastWriteTime, fi.LastAccessTime }.OrderBy(i => i).First(); + Parallel.ForEach(filesToProcess, fi => + { + var timeToUse = new[] { fi.CreationTime, fi.LastWriteTime, fi.LastAccessTime }.OrderBy(i => i).First(); - if (input.IsPictures) + if (input.IsPictures) + { + try { - try + using var exif = new ExifReader(fi.FullName); + exif.GetTagValue(ExifTags.DateTime, out DateTime time); + if (time != DateTime.MinValue) { - using var exif = new ExifReader(fi.FullName); - exif.GetTagValue(ExifTags.DateTime, out DateTime time); - if (time != DateTime.MinValue) + if (input.UpdateTimestamp && time != fi.CreationTime) { - if (input.UpdateTimestamp && time != fi.CreationTime) + Console.Write($@"Updating time on {fi.Name} from {timeToUse} -> {time} ..."); + if (!input.NoOp) { - Console.Write($@"Updating time on {fi.Name} from {timeToUse} -> {time} ..."); - if (!input.NoOp) - { - fi.CreationTime = fi.LastWriteTime = time; - } - Console.WriteLine(); + fi.CreationTime = fi.LastWriteTime = time; } - timeToUse = time; + Console.WriteLine(); } + + timeToUse = time; } - catch { } } + catch { } + } - if (!input.NoMove && !input.Recurse) + if (!input.NoMove && !input.Recurse) + { + var dirName = Path.Combine(timeToUse.ToString(@"yyyy"), timeToUse.ToString(@"MM MMMM")); + var targetFolder = Path.Combine(outputDirectory, dirName); + + if (!input.NoOp) { - var dirName = Path.Combine(timeToUse.ToString(@"yyyy"), timeToUse.ToString(@"MM MMMM")); - var targetFolder = Path.Combine(outputDirectory, dirName); + Directory.CreateDirectory(targetFolder); + } - if (!input.NoOp) + Console.Write($@"{fi.Name} -> {dirName} ..."); + if (!input.NoOp) + { + try { - Directory.CreateDirectory(targetFolder); + File.Move(fi.FullName, Path.Combine(targetFolder, fi.Name), input.Force); } - - Console.Write($@"{fi.Name} -> {dirName} ..."); - if (!input.NoOp) + catch (IOException ex) { - try - { - File.Move(fi.FullName, Path.Combine(targetFolder, fi.Name), input.Force); - } - catch (IOException ex) - { - Console.Error.WriteLine($@"Error: {ex.Message}"); - } + Console.Error.WriteLine($@"Error: {ex.Message}"); } - Console.WriteLine(); } - }); - } - private static void PrintUsage() - { - Console.Write(PowerArgs.ArgUsage.GenerateUsageFromTemplate()); - } + Console.WriteLine(); + } + }); + } + + private static void PrintUsage() + { + Console.Write(PowerArgs.ArgUsage.GenerateUsageFromTemplate()); } }