diff --git a/Image Sorter/AuthenticationHelper.cs b/Image Sorter/AuthenticationHelper.cs new file mode 100644 index 0000000..2767914 --- /dev/null +++ b/Image Sorter/AuthenticationHelper.cs @@ -0,0 +1,102 @@ +using Microsoft.Graph; +using Microsoft.Identity.Client; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace Image_Sorter +{ + public class AuthenticationHelper + { + // The Client ID is used by the application to uniquely identify itself to the v2.0 authentication endpoint. + static string clientId = Program.MsaClientId; + public static string[] Scopes = { "Files.Read.All" }; + public static IPublicClientApplication app = PublicClientApplicationBuilder.Create(clientId).WithRedirectUri("http://localhost:8192/oauth2callback/").Build(); + //public static PublicClientApplicationBuilder IdentityClientApp = PublicClientApplicationBuilder.Create(clientId);// new PublicClientApplication(clientId); + + public static string AccessToken = null; + public static IAccount UserAccount = null; + + public static DateTimeOffset Expiration; + + private static GraphServiceClient graphClient = null; + + // Get an access token for the given context and resourceId. An attempt is first made to + // acquire the token silently. If that fails, then we try to acquire the token by prompting the user. + public static GraphServiceClient GetAuthenticatedClient() + { + if (graphClient == null) + { + // Create Microsoft Graph client. + try + { + graphClient = new GraphServiceClient( + "https://graph.microsoft.com/v1.0", + new DelegateAuthenticationProvider( + async (requestMessage) => + { + var token = await GetTokenForUserAsync(); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); + // This header has been added to identify our sample in the Microsoft Graph service. If extracting this code for your project please remove. + //requestMessage.Headers.Add("SampleID", "uwp-csharp-apibrowser-sample"); + + })); + return graphClient; + } + + catch (Exception ex) + { + Console.WriteLine("Could not create a graph client: " + ex.Message); + } + } + + return graphClient; + } + + + /// + /// Get Token for User. + /// + /// Token for user. + public static async Task GetTokenForUserAsync() + { + AuthenticationResult authResult; + /*try + { + authResult = await app.AcquireTokenInteractive(null).WithPrompt(Microsoft.Identity.Client.Prompt.SelectAccount).ExecuteAsync(); + AccessToken = authResult.AccessToken; + UserAccount = authResult.Account; + } + + catch (Exception e) + { + //Console.WriteLine(e); + }*/ + // Attempt to aquire an existing token. If we're already authed, the existing account will be valid + try + { + authResult = await app.AcquireTokenSilent(Scopes, UserAccount).ExecuteAsync(); + AccessToken = authResult.AccessToken; + UserAccount = authResult.Account; + } + + catch (Exception) + { + if (AccessToken == null || Expiration <= DateTimeOffset.UtcNow.AddMinutes(5)) + { + authResult = await app.AcquireTokenInteractive(null).WithPrompt(Microsoft.Identity.Client.Prompt.SelectAccount).ExecuteAsync(); + + AccessToken = authResult.AccessToken; + UserAccount = authResult.Account; + Expiration = authResult.ExpiresOn; + } + } + + return AccessToken; + } + } +} diff --git a/Image Sorter/Image Sorter.csproj b/Image Sorter/Image Sorter.csproj index bfebfbb..36dceb2 100644 --- a/Image Sorter/Image Sorter.csproj +++ b/Image Sorter/Image Sorter.csproj @@ -1,82 +1,20 @@ - - - + + - Debug - AnyCPU - {6739EA9D-6361-4B5B-B687-07C30FB82B3B} Exe - Image_Sorter - Image Sorter - v4.7.2 - 512 - true - true - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + net5.0-windows + true + Image_Sorter.Program + true + - - - - - - - - - - + + - - + + + - - - - - - False - Microsoft .NET Framework 4.7.2 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - \ No newline at end of file + diff --git a/Image Sorter/Program.cs b/Image Sorter/Program.cs index 03ff7f3..d4c70e4 100644 --- a/Image Sorter/Program.cs +++ b/Image Sorter/Program.cs @@ -4,24 +4,50 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Windows.Media.Imaging; 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 readonly List DNGTypes = new List {"raw", "cr2", "dng"}; - static readonly List ImagePrefixes = new List { "", "Screenshot_", "VID"}; static void Main(string[] args) { - Console.WriteLine("Image Sorter {0} (C)2019 Brychan Dempsey"); + 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") + if (args.Length > 0 && args[0].ToLower() == "\\s") { int sourceDirSpecified = Array.IndexOf(args, "\\d"); if (sourceDirSpecified != -1) @@ -66,113 +92,363 @@ namespace Image_Sorter } 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()); + 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(); - Task updateFilesTask = new Task(() => + object countLock = new(); + + object folderLock = new(); + + + Task updateFilesTask = new(() => { - Parallel.ForEach(SourceFiles, (filepath) => - { - string fileDestination = ""; - DateTime photoDate = new DateTime(); - bool hasMetadata = true; - using (FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - 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(filepath)) && !Path.GetExtension(x).Equals(Path.GetExtension(filepath))); - if (matches.Count == 1) - { - using (FileStream tfs = new FileStream(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(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(filepath).TrimStart(x.ToCharArray()), out photoDate))) - { - Console.WriteLine(" Date successfully parsed from file: {0} - Parsed Date: {1} ", Path.GetFileName(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(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.Remove(0, filepath.LastIndexOf("\\")); - if (copyFiles) - { - try - { - File.Copy(filepath, DestinationDir + fileDestination); - filepath = DestinationDir + fileDestination; - } - catch - { - Console.WriteLine("Failed to copy {0}", Path.GetFileName(filepath)); - } + _ = 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) + { - } - else - { - try - { - File.Move(filepath, DestinationDir + fileDestination); - filepath = DestinationDir + fileDestination; - } - catch - { - Console.WriteLine("Failed to move {0}", Path.GetFileName(filepath)); - } + // 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); + } - } - lock (countLock) - { - processedCount++; - } - }); + // 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(); @@ -192,12 +468,11 @@ namespace Image_Sorter Thread.Sleep(10); } Console.ReadLine(); - } - static Dictionary GetMetaData(FileStream fileStream) + static Dictionary GetMetaData(FileStream fileStream) { - + Dictionary Metadata = new Dictionary(); try { @@ -216,9 +491,12 @@ namespace Image_Sorter return Metadata; } + static List GetFiles(string SourceDirectory) { - List foundFiles = Directory.GetFiles(SourceDirectory, "*", SearchOption.TopDirectoryOnly).ToList(); + // 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)); @@ -269,7 +547,7 @@ namespace Image_Sorter return false; } } - if (result.Year <= DateTime.Now.Year+10 && result.Year >= 1980) + 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; diff --git a/Image Sorter/Properties/AssemblyInfo.cs b/Image Sorter/Properties/AssemblyInfo.cs deleted file mode 100644 index e61ad3d..0000000 --- a/Image Sorter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Image Sorter")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Image Sorter")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6739ea9d-6361-4b5b-b687-07c30fb82b3b")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")]