using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Globalization; using System.Threading; using System.Windows.Media.Imaging; using Microsoft.Graph; using Directory = System.IO.Directory; using File = System.IO.File; using FileSystemInfo = System.IO.FileSystemInfo; namespace Image_Sorter { class Program { static void Main(string[] args) { Worker(args); Console.ReadLine(); } public const string MsaClientId = ""; //public const string MsaReturnUrl = "urn:ietf:wg:oauth:2.0:oob"; private static GraphServiceClient graphClient { get; set; } // Threading synchronisation static volatile SemaphoreSlim folderCreateMarshall = new(1); // Store created folders in a lookup static volatile Dictionary CreatedFolderItems = new(); static readonly List DNGTypes = new List { "raw", "cr2", "dng" }; static readonly List ImagePrefixes = new List { "", "Screenshot_", "VID" }; static async void Worker(string[] args) { Console.WriteLine("Image Sorter {0} (C) 2021 Brychan Dempsey"); Console.WriteLine("Moves images from the source folder and arranges them by date (from metadata) in the destination folder"); string SourceDir = ""; string DestinationDir = ""; string OneDriveSourceDir = ""; string OneDriveDestinationDir = ""; DriveItem OneDriveSourceItem = null; DriveItem OneDriveDestItem = null; bool copyFiles = false; if (args.Length > 0 && args[0].ToLower() == "\\s") { int sourceDirSpecified = Array.IndexOf(args, "\\d"); if (sourceDirSpecified != -1) { if (Directory.Exists(args[sourceDirSpecified + 1])) { SourceDir = args[sourceDirSpecified + 1]; } } int targetDirSpecified = Array.IndexOf(args, "\\t"); if (targetDirSpecified != -1) { if (Directory.Exists(args[targetDirSpecified + 1])) { DestinationDir = args[targetDirSpecified + 1]; } } else { DestinationDir = SourceDir; } if (args.Contains("\\c")) { copyFiles = true; } // Redirect all console writes Console.SetOut(new StringWriter()); } else if (args.Length > 0 && (args[0] == "\\?" || args[0].ToLower() == "\\h")) { Console.WriteLine("Image Sorter"); Console.WriteLine("Can be run in silent mode using the argument: \\s"); Console.WriteLine("Additional flags: \\d - Source Directory (Usage: \\s \\d "); Console.WriteLine("\t\t\\t - Target Directory (Usage: \\s \\t "); Console.WriteLine("\t\t\\c - Copy files if this flag is specified"); Console.WriteLine("Usage Example:"); Console.WriteLine("\\s \\d \"C:\\Camera Files\\Images\" \\t \"C:\\Camera Files\\Sorted\\\""); Console.WriteLine("\nIf the destination directory is not specified, it will default to the source directory"); Console.ReadLine(); Environment.Exit(0); } else { Console.WriteLine("Enter the source directory:"); SourceDir = Console.ReadLine(); Console.WriteLine("Enter the destionation directory:"); DestinationDir = Console.ReadLine(); Console.WriteLine("Enter \'y\' to perform a copy instead of move"); string tRead = Console.ReadLine(); if (tRead.Trim().ToLower().Equals("y")) { copyFiles = true; } } Console.WriteLine("Scanning the source directory..."); List SourceFiles = GetFiles(SourceDir); Console.WriteLine("Found {0} files", SourceFiles.Count); string OneDriveLoc = ""; //Drive contextDrive = null; if (SourceDir.ToLower().Contains("onedrive")) { OneDriveLoc = SourceDir[0..(SourceDir.ToLower().IndexOf("onedrive\\") + "onedrive\\".Length)]; } bool OneDriveLogin = false; if ((File.GetAttributes(SourceDir).HasFlag(FileAttributes.Offline) || SourceDir.Contains("OneDrive") || SourceFiles.Any((x) => File.GetAttributes(SourceDir + x).HasFlag(FileAttributes.Offline))) && DestinationDir.Contains(OneDriveLoc)) { Console.WriteLine("Source and target folders seem to be inside a OneDrive folder.\nWould you like to sign-in to OneDrive and manage files online?"); string tRead = Console.ReadLine(); if (tRead.Trim().ToLower().Equals("y")) { // Do a OneDrive login if (SourceDir.ToLower().Split("onedrive").Length > 2) { throw new ArgumentException("Cannot identify OneDrive source folder"); } try { graphClient = AuthenticationHelper.GetAuthenticatedClient(); // The user isn't yet logged in - formulate a request: User me = await graphClient.Me.Request().GetAsync(); //contextDrive = await graphClient.Drives.Request().GetAsync(); Console.WriteLine("Authentication of {0} successful. Welcome {1}.", AuthenticationHelper.UserAccount.Username, me.DisplayName); OneDriveLogin = true; OneDriveSourceDir = SourceDir.Replace(OneDriveLoc, "").Replace('\\', '/'); OneDriveDestinationDir = DestinationDir.Replace(OneDriveLoc, "").Replace('\\', '/'); if (!OneDriveSourceDir.StartsWith('/')) { OneDriveSourceDir = '/' + OneDriveSourceDir; } if (!OneDriveDestinationDir.StartsWith('/')) { OneDriveDestinationDir = '/' + OneDriveDestinationDir; } // Finally, evaluate the base source and destination folders OneDriveDestItem = await graphClient.Drive.Root.ItemWithPath(OneDriveDestinationDir).Request().GetAsync(); OneDriveSourceItem = await graphClient.Drive.Root.ItemWithPath(OneDriveSourceDir).Request().GetAsync(); } catch (ServiceException exception) { Console.WriteLine(exception); } } } // Check if the folder is a OneDrive folder // Do Login if not already complete int processedCount = 0; object countLock = new(); object folderLock = new(); Task updateFilesTask = new(() => { _ = Parallel.ForEach(SourceFiles, async (filepath) => { uint fab = (uint)File.GetAttributes(SourceDir + filepath); // It seems that FileAttributes.Offline is no longer used to store on-demand OneDrive file status. // One undefined flag - 1048576 - is set // As well as defined - 4194304 - FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS // We will check initially for the presense of both bool fileState = ((uint)fab & 1048576) == 1048576 && (fab & 4194304) == 4194304; if (OneDriveLogin && fileState) { // File is online-only, process via OneDrive API DriveItem file = null; DriveItem newFolder = null; DriveItem subFolder = null; try { file = await graphClient.Drive.Root.ItemWithPath(OneDriveSourceDir + filepath.Replace('\\', '/')).Request().GetAsync(); } catch (ServiceException ex) { Console.WriteLine(ex); } // Folder creation will cause a race condition (which modifies the base object), // Need to intelligently handle this; threads that are simultaneously trying to create the same folder // must wait; if the folder exists, we don't need to create it. // Therefore, a semaphore must be used to determine the current state of access // To handle this smartly, we create a boolean that suggests if the folder is being created (Marshalled by a semaphore) // The first thread to reach it will set it to true, then check if it exists asynchronously DateTime fileTime = DateTime.MinValue; if (file.Photo.TakenDateTime != null) { DateTimeOffset dateTimeOffset = (DateTimeOffset)file.Photo.TakenDateTime; fileTime = dateTimeOffset.LocalDateTime; } else { fileTime = file.FileSystemInfo.CreatedDateTime < file.FileSystemInfo.LastModifiedDateTime ? file.FileSystemInfo.CreatedDateTime.Value.LocalDateTime : file.FileSystemInfo.LastModifiedDateTime.Value.LocalDateTime; } if (fileTime != DateTime.MinValue) { // Get the destination folder. If if (folderCreateMarshall.CurrentCount > 0 && CreatedFolderItems.ContainsKey(fileTime.Year.ToString())) { newFolder = CreatedFolderItems[fileTime.Year.ToString()]; } else { // Create a folder object and wait DriveItem tempNewFolder = new() { Name = fileTime.Year.ToString(), Folder = new Folder(), AdditionalData = new Dictionary { { "@microsoft.graph.conflictBehavior", "fail" } } }; // Wait for our turn to create the object await folderCreateMarshall.WaitAsync(); try { // Check the folder wasn't surprise created if (CreatedFolderItems.ContainsKey(fileTime.Year.ToString())) { newFolder = CreatedFolderItems[fileTime.Year.ToString()]; } else { Console.WriteLine("Creating Folder"); // Folder doesn't exist or wasn't added; create and add newFolder = await graphClient.Drive.Root.ItemWithPath(OneDriveDestinationDir) .Children.Request().AddAsync(tempNewFolder); CreatedFolderItems.Add(fileTime.Year.ToString(), newFolder); } } catch (ServiceException ex) { // TODO: Handle the existing item if (ex.StatusCode == System.Net.HttpStatusCode.Conflict) { try { newFolder = await graphClient.Drive.Root.ItemWithPath(OneDriveDestinationDir + "/" + fileTime.Year.ToString()).Request().GetAsync(); CreatedFolderItems.Add(fileTime.Year.ToString(), newFolder); } catch (ServiceException exNF) { Console.WriteLine(exNF); } } else { Console.WriteLine(ex); } } catch (Exception ex) { Console.WriteLine(ex); } finally { folderCreateMarshall.Release(); } } if (folderCreateMarshall.CurrentCount > 0 && CreatedFolderItems.ContainsKey(fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month))) { subFolder = CreatedFolderItems[fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month)]; } else { // Create a folder and wait DriveItem tempNewFolder = new() { Name = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month), Folder = new Folder(), AdditionalData = new Dictionary { { "@microsoft.graph.conflictBehavior", "fail" } } }; await folderCreateMarshall.WaitAsync(); try { // Check the folder wasn't surprise created if (CreatedFolderItems.ContainsKey(fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month))) { subFolder = CreatedFolderItems[fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month)]; } else { Console.WriteLine("Creating Subfolder"); // Folder doesn't exist or wasn't added; create and add subFolder = await graphClient.Drive.Root.ItemWithPath(OneDriveDestinationDir + "/" + fileTime.Year.ToString()) .Children.Request().AddAsync(tempNewFolder); CreatedFolderItems.Add(fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month), subFolder); } } catch (ServiceException ex) { if (ex.StatusCode == System.Net.HttpStatusCode.Conflict) { try { subFolder = await graphClient.Drive.Root.ItemWithPath(OneDriveDestinationDir + "/" + fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month)).Request().GetAsync(); CreatedFolderItems.Add(fileTime.Year.ToString() + "/" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(fileTime.Month), subFolder); } catch (ServiceException exNF) { Console.WriteLine(exNF); } } else { Console.WriteLine(ex); } } finally { folderCreateMarshall.Release(); } } // Finally, get the resulting files ItemReference parentReference = new() { Id = subFolder.Id }; try { // Fail-fast, copy item first if (await graphClient.Me.Drive.Items[file.Id].Copy(null, parentReference).Request().PostAsync() != file && !copyFiles) { // Only if the returned file is not a duplicate, and we aren't copying, shall we remove the old file await graphClient.Drive.Items[file.Id].Request().DeleteAsync(); } } catch (ServiceException ex) { Console.WriteLine(ex); } } } else { string fileDestination = ""; DateTime photoDate = new(); bool hasMetadata = true; using (FileStream fs = new(SourceDir + filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) { // If the file is online-only, allow a large amount of time to download if (fileState) { fs.ReadTimeout = (int)TimeSpan.FromMinutes(60).TotalMilliseconds; // Allow about 60 minutes for a download; should be enough for even large concurrent downloads } bool ruleMatch = false; Dictionary imgMetadata = GetMetaData(fs); if (imgMetadata.ContainsKey("date")) { photoDate = DateTime.Parse(imgMetadata["date"]); ruleMatch = true; } // If the file is a negative, it may have a conjugate image file else if (DNGTypes.Contains(Path.GetExtension(filepath).TrimStart('.'))) { List matches = SourceFiles.FindAll((x) => Path.GetFileNameWithoutExtension(x).Equals(Path.GetFileNameWithoutExtension(SourceDir + filepath)) && !Path.GetExtension(x).Equals(Path.GetExtension(SourceDir + filepath))); if (matches.Count == 1) { using (FileStream tfs = new(matches[0], FileMode.Open, FileAccess.Read, FileShare.Read)) { imgMetadata = GetMetaData(tfs); } if (imgMetadata.ContainsKey("date")) { photoDate = DateTime.Parse(imgMetadata["date"]); Console.WriteLine(" Conjugate file found - " + Path.GetFileName(SourceDir + filepath)); ruleMatch = true; } } } // Try parsing a date by the rules set in ImagePrefixes, using the first if found else if (ImagePrefixes.Exists((x) => TryParseDate(Path.GetFileNameWithoutExtension(SourceDir + filepath).TrimStart(x.ToCharArray()), out photoDate))) { Console.WriteLine(" Date successfully parsed from file: {0} - Parsed Date: {1} ", Path.GetFileName(SourceDir + filepath), photoDate.ToString("dd-MM-yyyy")); ruleMatch = true; } // Failsafe to avoid implausable dates (Greater than system year + 10, < 1990) if (ruleMatch && (photoDate.Year > DateTime.Now.Year + 10 || photoDate.Year < 1990)) { ruleMatch = false; } // Finally, resort to the least of the file creation time && file modified time if (!ruleMatch) { FileSystemInfo fileInfo = new FileInfo(SourceDir + filepath); photoDate = fileInfo.CreationTime < fileInfo.LastWriteTime ? fileInfo.CreationTime : fileInfo.LastWriteTime; } } fileDestination = "\\" + photoDate.Year; fileDestination += "\\" + CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(photoDate.Month); Directory.CreateDirectory(DestinationDir + fileDestination + "\\"); fileDestination += "\\" + filepath[filepath.LastIndexOf('\\')..]; if (copyFiles) { try { File.Copy(SourceDir + filepath, DestinationDir + fileDestination); filepath = DestinationDir + fileDestination; } catch { Console.WriteLine("Failed to copy {0}", Path.GetFileName(SourceDir + filepath)); } } else { try { File.Move(SourceDir + filepath, DestinationDir + fileDestination); filepath = DestinationDir + fileDestination; } catch { Console.WriteLine("Failed to move {0}", Path.GetFileName(SourceDir + filepath)); } } } lock (countLock) { processedCount++; } }); }); int lastProcessed = 0; updateFilesTask.Start(); bool eval = false; while (!eval) { lock (countLock) { if (processedCount != lastProcessed) { Console.WriteLine("{0}/{1} processed.", processedCount, SourceFiles.Count); lastProcessed = processedCount; } if (processedCount == SourceFiles.Count) eval = true; } Thread.Sleep(10); } Console.ReadLine(); } static Dictionary GetMetaData(FileStream fileStream) { Dictionary Metadata = new Dictionary(); try { BitmapSource srcimg = BitmapFrame.Create(fileStream); BitmapMetadata md = (BitmapMetadata)srcimg.Metadata; string date = md.DateTaken; if (!string.IsNullOrEmpty(date)) { Metadata.Add("date", date); } } catch { Console.WriteLine("Couldn't get metadata - {0}", fileStream.Name); } return Metadata; } static List GetFiles(string SourceDirectory) { // Get the files, remove the source // This gives us a source list with relative pathing. This allows OneDrive files to be easily incorporated List foundFiles = Directory.GetFiles(SourceDirectory, "*", SearchOption.TopDirectoryOnly).Select((s) => s.Replace(SourceDirectory, "")).ToList(); foreach (var subDirectory in Directory.GetDirectories(SourceDirectory)) { foundFiles.AddRange(GetFiles(subDirectory)); } return foundFiles; } static bool TryParseDate(string input, out DateTime dt) { // Attempt a base conversion first // As datetime's built-in converter does not seem to be able to handle more complex strings, // convert the to a usable format before attempting to convert a datetime //int pos = ImagePrefixes.FindIndex(0, (x) => input.StartsWith(x)); //input = input.Replace(ImagePrefixes[pos], ""); // Remove the prefix if (DateTime.TryParse(input, out dt)) return true; StringBuilder converted = new StringBuilder(); string modified = input; DateTime result = new DateTime(); // if we are in the format "ddddsddsdd" we can try to parse that date if (isMatch(input)) { int year = Convert.ToInt32(input.Substring(0, 4)); int month = Convert.ToInt32(input.Substring(5, 2)); int day = Convert.ToInt32(input.Substring(8, 2)); try { result = new DateTime(year, month, day); } catch { return false; } } // Else try the straight digit parsing, so ensure we have 8 contiguous digits to parse. else if (isMatch(input, "dddddddd")) { int year = Convert.ToInt32(input.Substring(0, 4)); int month = Convert.ToInt32(input.Substring(4, 2)); int day = Convert.ToInt32(input.Substring(6, 2)); try { result = new DateTime(year, month, day); } catch { return false; } } if (result.Year <= DateTime.Now.Year + 10 && result.Year >= 1980) { // Falls within an acceptable date range, so try use it as a date dt = result; return true; } return false; } private static bool isNumeric(char c1) { if (c1 >= 48 && c1 < 58) { return true; } else return false; } private static bool isSymbol(char c1) { if ((c1 >= 32 && c1 < 48) || (c1 >= 58 && c1 < 65) || (c1 >= 91 && c1 < 97) || (c1 >= 123 && c1 < 127)) { return true; } else return false; } /// /// Checks if the provided string matches the format specified /// d = digit /// a = alphanumeric /// s = symbol /// * = any /// /// /// /// private static bool isMatch(string source, string format = "ddddsddsdd") { if (source.Length < format.Length) return false; bool state = true; for (int i = 0; i < format.Length; i++) { if (format[i] == 'd') { if (!isNumeric(source[i])) { return false; } } else if (format[i] == 's') { if (!isSymbol(source[i])) { return false; } } } return state; } } }