/// <inheritdoc cref="IArchiver.ListFiles"/>
        public IEnumerable <IFileInArchive> ListFiles(string archivePath)
        {
            if (!File.Exists(archivePath))
            {
                return(Enumerable.Empty <IFileInArchive>());
            }

            var prolibExe = new ProcessIo(_prolibPath);

            if (!prolibExe.TryExecute(new ProcessArgs().Append(archivePath, "-list")))   // -date mdy
            {
                throw new Exception("Error while listing files from a .pl.", new Exception(prolibExe.BatchOutput.ToString()));
            }

            var outputList = new List <IFileInArchive>();
            var regex      = new Regex(@"^(.+)\s+(\d+)\s+(\w+)\s+(\d+)\s+(\d{2}\/\d{2}\/\d{2}\s\d{2}\:\d{2}\:\d{2})\s(\d{2}\/\d{2}\/\d{2}\s\d{2}\:\d{2}\:\d{2})");

            foreach (var output in prolibExe.StandardOutputArray)
            {
                var match = regex.Match(output);
                if (match.Success)
                {
                    // Third match is the file type. PROLIB recognizes two file types: R (r-code file type) and O (any other file type).
                    // Fourth is the offset, the distance, in bytes, of the start of the file from the beginning of the library.
                    var type    = match.Groups[3].Value;
                    var newFile = new FileInProlib {
                        PathInArchive = match.Groups[1].Value.TrimEnd(),
                        SizeInBytes   = ulong.Parse(match.Groups[2].Value),
                        IsRcode       = !string.IsNullOrEmpty(type) && type[0] == 'R'
                    };
                    if (DateTime.TryParseExact(match.Groups[5].Value, @"MM/dd/yy HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
                    {
                        newFile.DateAdded = date;
                    }

                    if (DateTime.TryParseExact(match.Groups[6].Value, @"MM/dd/yy HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
                    {
                        newFile.LastWriteTime = date;
                    }

                    outputList.Add(newFile);
                }
            }

            return(outputList);
        }
        /// <inheritdoc cref="IArchiverBasic.ArchiveFileSet"/>
        public int ArchiveFileSet(IEnumerable <IFileToArchive> filesToArchive)
        {
            var filesToPack = filesToArchive.ToList();

            filesToPack.ForEach(f => f.Processed = false);
            int totalFiles     = filesToPack.Count;
            int totalFilesDone = 0;

            foreach (var plGroupedFiles in filesToPack.GroupBy(f => f.ArchivePath))
            {
                string uniqueTempFolder = null;
                try {
                    var archiveFolder = CreateArchiveFolder(plGroupedFiles.Key);

                    // create a unique temp folder for this .pl
                    uniqueTempFolder = Path.Combine(archiveFolder, $"{Path.GetFileName(plGroupedFiles.Key)}~{Path.GetRandomFileName()}");
                    var dirInfo = Directory.CreateDirectory(uniqueTempFolder);
                    dirInfo.Attributes |= FileAttributes.Hidden;

                    var subFolders = new Dictionary <string, List <FilesToMove> >();
                    foreach (var file in plGroupedFiles)
                    {
                        var subFolderPath = Path.GetDirectoryName(Path.Combine(uniqueTempFolder, file.PathInArchive));
                        if (!string.IsNullOrEmpty(subFolderPath))
                        {
                            if (!subFolders.ContainsKey(subFolderPath))
                            {
                                subFolders.Add(subFolderPath, new List <FilesToMove>());
                                if (!Directory.Exists(subFolderPath))
                                {
                                    Directory.CreateDirectory(subFolderPath);
                                }
                            }

                            if (File.Exists(file.SourcePath))
                            {
                                subFolders[subFolderPath].Add(new FilesToMove(file.SourcePath, Path.Combine(uniqueTempFolder, file.PathInArchive), file.PathInArchive));
                            }
                        }
                    }

                    var prolibExe = new ProcessIo(_prolibPath)
                    {
                        WorkingDirectory = uniqueTempFolder
                    };

                    foreach (var subFolder in subFolders)
                    {
                        _cancelToken?.ThrowIfCancellationRequested();

                        // move files to the temp subfolder
                        Parallel.ForEach(subFolder.Value, file => {
                            if (file.Move)
                            {
                                File.Move(file.Origin, file.Temp);
                            }
                            else
                            {
                                File.Copy(file.Origin, file.Temp);
                            }
                        });

                        // for files containing a space, we don't have a choice, call extract for each...
                        foreach (var file in subFolder.Value.Where(f => f.RelativePath.Contains(" ")))
                        {
                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-create", "-nowarn", "-add", file.RelativePath)))
                            {
                                throw new ArchiverException($"Failed to pack {file.Origin.PrettyQuote()} into {plGroupedFiles.Key.PrettyQuote()} and relative archive path {file.RelativePath}.", new ArchiverException(prolibExe.BatchOutput.ToString()));
                            }
                        }

                        var remainingFiles = subFolder.Value.Where(f => !f.RelativePath.Contains(" ")).ToList();
                        if (remainingFiles.Count > 0)
                        {
                            // for the other files, we can use the -pf parameter
                            var pfContent = new StringBuilder();
                            pfContent.AppendLine("-create -nowarn -add");
                            foreach (var file in remainingFiles)
                            {
                                pfContent.AppendLine(file.RelativePath);
                            }

                            var pfPath = Path.Combine(uniqueTempFolder, $"{Path.GetFileName(plGroupedFiles.Key)}~{Path.GetRandomFileName()}.pf");

                            File.WriteAllText(pfPath, pfContent.ToString(), _encoding);

                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-pf", pfPath)))
                            {
                                throw new ArchiverException($"Failed to pack to {plGroupedFiles.Key.PrettyQuote()}.", new Exception(prolibExe.BatchOutput.ToString()));
                            }

                            if (File.Exists(pfPath))
                            {
                                File.Delete(pfPath);
                            }
                        }

                        // move files from the temp subfolder
                        foreach (var file in subFolder.Value)
                        {
                            try {
                                if (file.Move)
                                {
                                    File.Move(file.Temp, file.Origin);
                                }
                                else if (!File.Exists(file.Temp))
                                {
                                    throw new ArchiverException($"Failed to move back the temporary file {file.Origin} from {file.Temp}.");
                                }
                            } catch (Exception e) {
                                throw new ArchiverException($"Failed to move back the temporary file {file.Origin} from {file.Temp}.", e);
                            }

                            totalFilesDone++;
                            OnProgress?.Invoke(this, ArchiverEventArgs.NewProgress(plGroupedFiles.Key, file.RelativePath, Math.Round(totalFilesDone / (double)totalFiles * 100, 2)));
                        }
                    }

                    // compress .pl
                    prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-compress", "-nowarn"));

                    foreach (var file in plGroupedFiles)
                    {
                        file.Processed = true;
                    }
                } catch (OperationCanceledException) {
                    throw;
                } catch (Exception e) {
                    throw new ArchiverException($"Failed to pack to {plGroupedFiles.Key.PrettyQuote()}.", e);
                } finally {
                    // delete temp folder
                    if (Directory.Exists(uniqueTempFolder))
                    {
                        Directory.Delete(uniqueTempFolder, true);
                    }
                }
            }

            return(totalFilesDone);
        }
        /// <inheritdoc cref="IArchiver.ExtractFileSet"/>
        public int ExtractFileSet(IEnumerable <IFileInArchiveToExtract> filesToExtractIn)
        {
            var filesToExtract = filesToExtractIn.ToList();

            filesToExtract.ForEach(f => f.Processed = false);
            int totalFiles     = filesToExtract.Count;
            int totalFilesDone = 0;
            var prolibExe      = new ProcessIo(_prolibPath);

            foreach (var plGroupedFiles in filesToExtract.GroupBy(f => f.ArchivePath))
            {
                if (!File.Exists(plGroupedFiles.Key))
                {
                    continue;
                }

                // process only files that actually exist
                var archiveFileList        = ListFiles(plGroupedFiles.Key).Select(f => f.PathInArchive).ToHashSet();
                var plGroupedFilesFiltered = plGroupedFiles.Where(f => archiveFileList.Contains(f.PathInArchive)).ToList();

                try {
                    foreach (var extractDirGroupedFiles in plGroupedFilesFiltered.GroupBy(f => Path.GetDirectoryName(f.ExtractionPath)))
                    {
                        prolibExe.WorkingDirectory = extractDirGroupedFiles.Key;
                        Directory.CreateDirectory(extractDirGroupedFiles.Key);

                        // for files containing a space, we don't have a choice, call extract for each...
                        foreach (var file in extractDirGroupedFiles.Where(deploy => deploy.PathInArchive.Contains(" ")))
                        {
                            _cancelToken?.ThrowIfCancellationRequested();
                            if (File.Exists(file.ExtractionPath))
                            {
                                File.Delete(file.ExtractionPath);
                            }
                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-nowarn", "-yank", file.PathInArchive)))
                            {
                                throw new ArchiverException($"Failed to extract {file.PathInArchive.PrettyQuote()} from {plGroupedFiles.Key.PrettyQuote()}.", new Exception(prolibExe.BatchOutput.ToString()));
                            }
                            totalFilesDone++;
                            OnProgress?.Invoke(this, ArchiverEventArgs.NewProgress(plGroupedFiles.Key, file.PathInArchive, Math.Round(totalFilesDone / (double)totalFiles * 100, 2)));
                        }

                        _cancelToken?.ThrowIfCancellationRequested();
                        var remainingFiles = extractDirGroupedFiles.Where(deploy => !deploy.PathInArchive.Contains(" ")).ToList();
                        if (remainingFiles.Count > 0)
                        {
                            // for the other files, we can use the -pf parameter
                            var pfContent = new StringBuilder();
                            pfContent.AppendLine("-nowarn");
                            pfContent.AppendLine("-yank");
                            foreach (var file in remainingFiles)
                            {
                                pfContent.AppendLine(file.PathInArchive);
                                if (File.Exists(file.ExtractionPath))
                                {
                                    File.Delete(file.ExtractionPath);
                                }
                            }

                            var pfPath = Path.Combine(extractDirGroupedFiles.Key, $"{Path.GetFileName(plGroupedFiles.Key)}~{Path.GetRandomFileName()}.pf");

                            File.WriteAllText(pfPath, pfContent.ToString(), _encoding);

                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-pf", pfPath)))
                            {
                                throw new ArchiverException($"Failed to extract from {plGroupedFiles.Key.PrettyQuote()}.", new Exception(prolibExe.BatchOutput.ToString()));
                            }

                            foreach (var file in remainingFiles)
                            {
                                totalFilesDone++;
                                OnProgress?.Invoke(this, ArchiverEventArgs.NewProgress(plGroupedFiles.Key, file.PathInArchive, Math.Round(totalFilesDone / (double)totalFiles * 100, 2)));
                            }

                            if (File.Exists(pfPath))
                            {
                                File.Delete(pfPath);
                            }
                        }
                    }

                    foreach (var file in plGroupedFiles)
                    {
                        file.Processed = true;
                    }
                } catch (OperationCanceledException) {
                    throw;
                } catch (Exception e) {
                    throw new ArchiverException($"Failed to process {plGroupedFiles.Key.PrettyQuote()}.", e);
                }
            }

            return(totalFilesDone);
        }
        /// <inheritdoc cref="IArchiver.MoveFileSet"/>
        public int MoveFileSet(IEnumerable <IFileInArchiveToMove> filesToMoveIn)
        {
            var filesToMove = filesToMoveIn.ToList();

            filesToMove.ForEach(f => f.Processed = false);
            int totalFiles     = filesToMove.Count;
            int totalFilesDone = 0;

            foreach (var plGroupedFiles in filesToMove.GroupBy(f => f.ArchivePath))
            {
                string uniqueTempFolder = null;
                try {
                    // process only files that actually exist
                    var archiveFileList        = ListFiles(plGroupedFiles.Key).Select(f => f.PathInArchive).ToHashSet();
                    var plGroupedFilesFiltered = plGroupedFiles.Where(f => archiveFileList.Contains(f.PathInArchive)).ToList();

                    if (!plGroupedFilesFiltered.Any())
                    {
                        continue;
                    }

                    var archiveFolder = CreateArchiveFolder(plGroupedFiles.Key);

                    // create a unique temp folder for this .pl
                    uniqueTempFolder = Path.Combine(archiveFolder, $"{Path.GetFileName(plGroupedFiles.Key)}~{Path.GetRandomFileName()}");
                    var dirInfo = Directory.CreateDirectory(uniqueTempFolder);
                    dirInfo.Attributes |= FileAttributes.Hidden;

                    var subFolders = new Dictionary <string, List <FilesToMove> >();
                    foreach (var file in plGroupedFilesFiltered)
                    {
                        var subFolderPath = Path.GetDirectoryName(Path.Combine(uniqueTempFolder, file.NewRelativePathInArchive));
                        if (!string.IsNullOrEmpty(subFolderPath))
                        {
                            if (!subFolders.ContainsKey(subFolderPath))
                            {
                                subFolders.Add(subFolderPath, new List <FilesToMove>());
                                if (!Directory.Exists(subFolderPath))
                                {
                                    Directory.CreateDirectory(subFolderPath);
                                }
                            }
                            subFolders[subFolderPath].Add(new FilesToMove(file.PathInArchive, Path.Combine(uniqueTempFolder, file.NewRelativePathInArchive), file.NewRelativePathInArchive));
                        }
                    }

                    var prolibExe = new ProcessIo(_prolibPath);

                    foreach (var subFolder in subFolders)
                    {
                        _cancelToken?.ThrowIfCancellationRequested();

                        foreach (var file in subFolder.Value)
                        {
                            prolibExe.WorkingDirectory = Path.GetDirectoryName(file.Temp);
                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-nowarn", "yank", file.Origin)))
                            {
                                throw new ArchiverException($"Failed to extract {file.Origin.PrettyQuote()} from {plGroupedFiles.Key.PrettyQuote()}.", new Exception(prolibExe.BatchOutput.ToString()));
                            }

                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-nowarn", "-delete", file.Origin)))
                            {
                                throw new ArchiverException($"Failed to delete {file.Origin.PrettyQuote()} in {plGroupedFiles.Key.PrettyQuote()}.", new ArchiverException(prolibExe.BatchOutput.ToString()));
                            }

                            File.Move(Path.Combine(prolibExe.WorkingDirectory, Path.GetFileName(file.Origin)), file.Temp);

                            prolibExe.WorkingDirectory = uniqueTempFolder;
                            prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-nowarn", "-delete", file.RelativePath));
                            if (!prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-create", "-nowarn", "-add", file.RelativePath)))
                            {
                                throw new ArchiverException($"Failed to pack {file.Origin.PrettyQuote()} into {plGroupedFiles.Key.PrettyQuote()} and relative archive path {file.RelativePath}.", new ArchiverException(prolibExe.BatchOutput.ToString()));
                            }

                            totalFilesDone++;
                            OnProgress?.Invoke(this, ArchiverEventArgs.NewProgress(plGroupedFiles.Key, file.RelativePath, Math.Round(totalFilesDone / (double)totalFiles * 100, 2)));
                        }
                    }

                    // compress .pl
                    prolibExe.TryExecute(new ProcessArgs().Append(plGroupedFiles.Key, "-compress", "-nowarn"));

                    // delete temp folder
                    Directory.Delete(uniqueTempFolder, true);

                    foreach (var file in plGroupedFiles)
                    {
                        file.Processed = true;
                    }
                } catch (OperationCanceledException) {
                    throw;
                } catch (Exception e) {
                    throw new ArchiverException($"Failed to pack to {plGroupedFiles.Key.PrettyQuote()}.", e);
                } finally {
                    // delete temp folder
                    if (Directory.Exists(uniqueTempFolder))
                    {
                        Directory.Delete(uniqueTempFolder, true);
                    }
                }
            }

            return(totalFilesDone);
        }