/// <summary>
        /// Goes through all files in the <paramref name="baseDirectory"/> and its subdirectories to create a <see cref="Catalog"/>.
        /// </summary>
        private Catalog createCatalogForDirectory(IDirectoryInfo baseDirectory, IFileInfo[] allFiles, out List <string> errors)
        {
            errors = new List <string>();
            List <FileInstance> fileInstances = new List <FileInstance>();
            int fileCount = 0;

            foreach (IFileInfo file in allFiles)
            {
                try
                {
                    FileInstance fileInstance = this.createFileInstance(baseDirectory, file);
                    fileInstances.Add(fileInstance);
                }
                catch (System.IO.IOException ioException)
                {
                    errors.Add($"Couldn't read ({ioException.Message}): {file}");
                }
                fileCount++;
                if ((fileCount % 20) == 0)
                {
                    this.outputWriter.WriteLine($"{(double)fileCount / allFiles.Length:P}% ({fileCount} / {allFiles.Length})");
                }
            }
            return(new Catalog(baseDirectory.FullName, DateTime.Now, fileInstances));
        }
        private void commandCatalogUpdate(Dictionary <string, object> arguments)
        {
            bool isDryRun = arguments.ContainsKey("--dryrun");

            IFileInfo catalogFile = (IFileInfo)arguments["CatalogFile"];

            if (!catalogFile.Exists)
            {
                throw new CommandLineArgumentException("<CatalogFile>", "Catalog file does not exist.");
            }

            Catalog        originalCatalog    = Catalog.Read(catalogFile);
            IDirectoryInfo catalogedDirectory = catalogFile.Directory;

            if (!catalogedDirectory.FullName.Equals(this.fileSystem.DirectoryInfo.FromDirectoryName(originalCatalog.BaseDirectoryPath).FullName, StringComparison.OrdinalIgnoreCase))
            {
                throw new CommandLineArgumentException("<CatalogFile>", "Catalog file was moved and does not represent a catalog of the directory it is currently in. Cannot update.");
            }

            this.outputWriter.Write("Getting files in cataloged directory... ");
            Dictionary <string, IFileInfo> foundFiles = catalogedDirectory.GetFiles("*", System.IO.SearchOption.AllDirectories)
                                                        .Where((file) => file.Name != ".kcatalog")
                                                        .ToDictionary((file) => file.GetRelativePath(catalogedDirectory), (file) => file, StringComparer.OrdinalIgnoreCase);

            this.outputWriter.WriteLine($"Found {foundFiles.Count} files.");

            bool hasChanges = false;
            Dictionary <Hash256, FileInstance> removedFileHashes = new Dictionary <Hash256, FileInstance>();
            HashSet <FileInstance>             newFileInstances  = new HashSet <FileInstance>(originalCatalog.FileInstances);

            foreach (FileInstance fileInstance in originalCatalog.FileInstances)
            {
                if (!foundFiles.Remove(fileInstance.RelativePath))
                {
                    newFileInstances.Remove(fileInstance);
                    if (!removedFileHashes.ContainsKey(fileInstance.FileContentsHash))
                    {
                        // Only add the first removed file instance which is probably the oldest one
                        removedFileHashes.Add(fileInstance.FileContentsHash, fileInstance);
                    }
                    hasChanges = true;
                }
            }

            foreach (KeyValuePair <string, IFileInfo> leftOverFile in foundFiles.OrderBy((kvp) => kvp.Key))
            {
                FileInstance fileInstance = this.createFileInstance(catalogedDirectory, leftOverFile.Value);
                if (removedFileHashes.TryGetValue(fileInstance.FileContentsHash, out FileInstance removedFileInstance))
                {
                    this.log($"File moved       : {leftOverFile.Key} (from {removedFileInstance.RelativePath})");
                    removedFileHashes.Remove(removedFileInstance.FileContentsHash);
                }
                else
                {
                    this.log($"New file added   : {leftOverFile.Key}");
                }
                newFileInstances.Add(fileInstance);
                hasChanges = true;
            }

            if (hasChanges)
            {
                Catalog updatedCatalog = new Catalog(originalCatalog.BaseDirectoryPath, originalCatalog.CatalogedOn, DateTime.Now, newFileInstances);

                foreach (FileInstance removedFileInstance in removedFileHashes.Values)
                {
                    // These are all the files left over that haven't been detected as moved
                    if (updatedCatalog.FileInstancesByHash.TryGetValue(removedFileInstance.FileContentsHash, out IReadOnlyList <FileInstance> otherFileInstances))
                    {
                        // In this case there are files that still exist with the same hash, so all we've done is remove a duplicate
                        this.log($"Duplicate removed: {removedFileInstance.RelativePath} (from {otherFileInstances.First().RelativePath})");
                    }
                    else
                    {
                        // No duplicates, no newly files to be a move, it is truly removed
                        this.log($"File deleted     : {removedFileInstance.RelativePath}");
                    }
                }

                if (!isDryRun)
                {
                    updatedCatalog.Write(catalogFile);
                }
            }
        }
        private void commandPhotoArchive(Dictionary <string, object> arguments)
        {
            IDirectoryInfo sourceDirectory = (IDirectoryInfo)arguments["SourceDirectory"];
            IFileInfo      catalogFile     = (IFileInfo)arguments["CatalogFile"];

            if (!catalogFile.Exists)
            {
                throw new CommandLineArgumentException("<CatalogFile>", "Catalog file does not exist.");
            }

            Catalog        catalog          = Catalog.Read(catalogFile);
            IDirectoryInfo archiveDirectory = catalogFile.Directory;

            if (!archiveDirectory.FullName.Equals(this.fileSystem.DirectoryInfo.FromDirectoryName(catalog.BaseDirectoryPath).FullName, StringComparison.OrdinalIgnoreCase))
            {
                throw new CommandLineArgumentException("<CatalogFile>", "Catalog file was moved and does not represent a catalog of the directory it is currently in. Cannot archive to it.");
            }

            this.outputWriter.Write("Getting files in source directory... ");
            IFileInfo[] sourceFiles = sourceDirectory.GetFiles("*", SearchOption.AllDirectories);
            this.outputWriter.WriteLine($"Found {sourceFiles.Length} files.");

            foreach (IFileInfo sourceFile in sourceFiles)
            {
                if (CommandRunner.photoFileNameRegex.IsMatch(sourceFile.Name))
                {
                    Match    match    = CommandRunner.photoFileNameRegex.Match(sourceFile.Name);
                    string   prefix   = match.Groups["prefix"].Value;
                    int      month    = int.Parse(match.Groups["month"].Value);
                    int      day      = int.Parse(match.Groups["day"].Value);
                    int      year     = int.Parse(match.Groups["year"].Value);
                    DateTime dateTime = new DateTime(year, month, day);

                    string dayFolderPath = this.fileSystem.Path.Combine(archiveDirectory.FullName, this.getYearFormatted(dateTime), this.getMonthFormatted(dateTime), this.getDayFormatted(dateTime));

                    // We strip the prefix so that photos and videos are all side-by-side, sorted by timestamp
                    string archiveFileName = sourceFile.Name.Substring(prefix.Length);
                    string archiveFilePath = this.fileSystem.Path.Combine(dayFolderPath, archiveFileName);

                    if (this.fileSystem.File.Exists(archiveFilePath))
                    {
                        if (this.fileSystem.File.ReadAllBytes(archiveFilePath).SequenceEqual(this.fileSystem.File.ReadAllBytes(sourceFile.FullName)))
                        {
                            // Files are the same so just delete the source file since it already exists in the archive directory
                            sourceFile.Delete();
                        }
                        else
                        {
                            this.log($"Cannot archive file, identical file name already exists with different file contents: {sourceFile} to {archiveFilePath}");
                        }
                    }
                    else
                    {
                        FileInstance sourceFileInstance = this.createFileInstance(sourceDirectory, sourceFile);
                        if (!catalog.FileInstancesByHash.ContainsKey(sourceFileInstance.FileContentsHash))
                        {
                            // Only archive this file if it doesn't already exist in the catalog elsewhere
                            this.fileSystem.Directory.CreateDirectory(this.fileSystem.Path.GetDirectoryName(archiveFilePath));
                            sourceFile.MoveTo(archiveFilePath);
                        }
                        else
                        {
                            this.log($"Will not archive file, it is already in the catalog elsewhere: {sourceFile}");
                        }
                    }
                }
                else
                {
                    this.log($"Cannot archive file, unknown date: {sourceFile}");
                }
            }
        }