private void IterateOverFiles(List <FolderFileItem> paths, string backupDirectory) { try { HadError = false; IsRunning = true; if (Directory.Exists(backupDirectory) || _isCalculatingFileSize) { var backupName = "backup-" + DateTime.Now.ToString("yyyy-MM-dd-H-mm-ss"); backupDirectory = Path.Combine(backupDirectory, "easy-backup", backupName); if (!Directory.Exists(backupDirectory) && !_isCalculatingFileSize) { Directory.CreateDirectory(backupDirectory); } else if (!_isCalculatingFileSize) { // ok, somehow they started two backups within the same second >_> wait 1 second and start again Task.Delay(1000); backupName = "backup-" + DateTime.Now.ToString("yyyy-MM-dd-H-mm-ss"); backupDirectory = Path.Combine(backupDirectory, "easy-backup", backupName); if (!Directory.Exists(backupDirectory)) { Directory.CreateDirectory(backupDirectory); } else { throw new Exception("Couldn't create backup directory (directory already exists)"); } } // ok, start copying the files if not using compressed file. if (!UsesCompressedFile || _isCalculatingFileSize) { foreach (FolderFileItem item in paths) { if (HasBeenCanceled) { break; } var directoryName = Path.GetDirectoryName(item.Path); var pathRoot = Path.GetPathRoot(item.Path); directoryName = directoryName.Replace(pathRoot, ""); directoryName = Path.Combine(pathRoot.Replace(":\\", ""), directoryName); var outputDirectoryPath = Path.Combine(backupDirectory, directoryName); if (!Directory.Exists(outputDirectoryPath) && !_isCalculatingFileSize) { Directory.CreateDirectory(outputDirectoryPath); } if (!_isCalculatingFileSize) { StartedCopyingItem?.Invoke(item); } if (item.IsDirectory && Directory.Exists(item.Path)) { if (item.OnlyCopiesLatestFile && item.CanEnableOnlyCopiesLatestFile) { // scan directory and copy only the latest file out of it var directoryInfo = new DirectoryInfo(item.Path); var latestFile = directoryInfo.GetFiles().OrderByDescending(x => x.LastWriteTimeUtc).FirstOrDefault(); if (latestFile != null) { if (_isCalculatingFileSize) { CalculatedBytesOfItem?.Invoke(item, (ulong)new FileInfo(latestFile.FullName).Length); } else { var outputBackupDirectory = Path.Combine(outputDirectoryPath, Path.GetFileName(item.Path)); // create directory if needed in backup path if (!Directory.Exists(outputBackupDirectory)) { Directory.CreateDirectory(outputBackupDirectory); } if (HasBeenCanceled) { break; } var outputPath = Path.Combine(outputBackupDirectory, Path.GetFileName(latestFile.FullName)); CopySingleFile(item, latestFile.FullName, outputPath); } } } else { if (HasBeenCanceled) { break; } _currentDirectorySize = 0; var outputPath = Path.Combine(outputDirectoryPath, Path.GetFileName(item.Path)); CopyDirectory(item, item.Path, outputPath, item.IsRecursive, item.ExcludedPaths); if (_isCalculatingFileSize) { CalculatedBytesOfItem?.Invoke(item, _currentDirectorySize); } } } else { if (_isCalculatingFileSize) { CalculatedBytesOfItem?.Invoke(item, (ulong)new FileInfo(item.Path).Length); } else { var outputPath = Path.Combine(outputDirectoryPath, Path.GetFileName(item.Path)); CopySingleFile(item, item.Path, outputPath); } } if (!HasBeenCanceled && !_isCalculatingFileSize) { FinishedCopyingItem?.Invoke(item); } } } else { // first, figure out each file that needs to be copied into the 7z file. this way we can optimize // the copy to 1 single Process start. var filePaths = new List <string>(); var pathsToFolderFileItem = new Dictionary <string, FolderFileItem>(); var pathToFileSize = new Dictionary <string, ulong>(); foreach (FolderFileItem item in paths) { if (HasBeenCanceled) { break; } if (item.IsDirectory && Directory.Exists(item.Path)) { if (item.OnlyCopiesLatestFile && item.CanEnableOnlyCopiesLatestFile) { // scan directory and copy only the latest file out of it var directoryInfo = new DirectoryInfo(item.Path); var latestFile = directoryInfo.GetFiles().OrderByDescending(x => x.LastWriteTimeUtc).FirstOrDefault(); if (latestFile != null) { if (HasBeenCanceled) { break; } pathsToFolderFileItem.Add(latestFile.FullName, item); filePaths.Add(latestFile.FullName); pathToFileSize.Add(item.Path, (ulong)new FileInfo(latestFile.FullName).Length); } } else { if (HasBeenCanceled) { break; } var filesWithSizesInDirectory = GetFilePathsAndSizesInDirectory(item.Path, item.IsRecursive, item.ExcludedPaths); foreach (KeyValuePair <string, ulong> entry in filesWithSizesInDirectory) { pathsToFolderFileItem.Add(entry.Key, item); pathToFileSize.Add(entry.Key, entry.Value); filePaths.Add(entry.Key); } } } else { pathsToFolderFileItem.Add(item.Path, item); pathToFileSize.Add(item.Path, (ulong)new FileInfo(item.Path).Length); filePaths.Add(item.Path); } } _directoryPathsSeen.Clear(); if (!HasBeenCanceled) { // ok, we can do le copy now BackupToCompressedFile(Path.Combine(backupDirectory, backupName + ".7z"), filePaths, pathsToFolderFileItem, pathToFileSize); if (HasBeenCanceled) { try { // not a huge deal if this fails Directory.Delete(backupDirectory); } catch (Exception) { } } } } } else { throw new Exception("Backup directory doesn't exist"); } IsRunning = false; } catch (Exception e) { HadError = true; BackupFailed?.Invoke(e); } finally { IsRunning = false; } }
private void BackupToCompressedFile(string destination, List <string> filePaths, Dictionary <string, FolderFileItem> pathsToFolderFileItem, Dictionary <string, ulong> pathsToFileSize) { var quotedFilePaths = new List <string>(); foreach (string filePath in filePaths) { quotedFilePaths.Add("\"" + filePath + "\""); } var is64BitOS = Utilities.Is64BitOS(); Process process = new Process(); var currentDir = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; var exePath = is64BitOS ? currentDir + "/tools/x64/7za.exe" : currentDir + "/tools/x86/7za.exe"; process.StartInfo.FileName = exePath; process.StartInfo.WorkingDirectory = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; // https://stackoverflow.com/a/6522928/3938401 process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.StandardOutputEncoding = Encoding.UTF8; //process.StartInfo.RedirectStandardError = true; process.EnableRaisingEvents = true; var didError = false; var sizeInBytes = 0UL; var remainingBytes = 0UL; var didStartCompressingFile = false; var lastPercent = 0.0; string currentFilePath = ""; FolderFileItem currentItem = null; var bytesCopiedForItem = new Dictionary <FolderFileItem, ulong>(); var didFinishCancel = false; var nextMessageIsError = false; string errorMessage = ""; process.OutputDataReceived += new DataReceivedEventHandler(delegate(object sender, DataReceivedEventArgs e) { if (string.IsNullOrWhiteSpace(e.Data) || didFinishCancel) // in case more events come through { return; } if (HasBeenCanceled || didError) { // ONLY WORKS IF YOU AREN'T ALREADY SHOWING A CONSOLE! // https://stackoverflow.com/a/29274238/3938401 if (AttachConsole((uint)process.Id)) { SetConsoleCtrlHandler(null, true); try { GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, process.SessionId); // ends the process process.Kill(); // process is canned, so OK to kill } catch { } finally { FreeConsole(); SetConsoleCtrlHandler(null, false); didFinishCancel = true; } return; } } if (e.Data.StartsWith("+ ") && e.Data.Trim() != "+") { didStartCompressingFile = true; currentFilePath = e.Data.Substring(2); if (currentItem != null && remainingBytes > 0) { CopiedBytesOfItem(currentItem, remainingBytes); bytesCopiedForItem[currentItem] += remainingBytes; if (!currentItem.IsDirectory) { FinishedCopyingItem?.Invoke(currentItem); } else { if (bytesCopiedForItem[currentItem] == currentItem.ByteSize) { FinishedCopyingItem?.Invoke(currentItem); } } } if (pathsToFolderFileItem.ContainsKey(currentFilePath)) { currentItem = pathsToFolderFileItem[currentFilePath]; } else { currentItem = null; } if (currentItem != null && !bytesCopiedForItem.ContainsKey(currentItem)) { bytesCopiedForItem[currentItem] = 0; } if (pathsToFileSize.ContainsKey(currentFilePath)) { sizeInBytes = remainingBytes = pathsToFileSize[currentFilePath]; } else { sizeInBytes = remainingBytes = 0; } } else if (e.Data.Contains("%") && didStartCompressingFile) { var percent = double.Parse(e.Data.Trim().Split('%')[0]); var actualPercent = percent - lastPercent; lastPercent = percent; var copiedBytes = Math.Floor((actualPercent / 100.0) * sizeInBytes); // floor -- would rather underestimate than overestimate if (currentItem != null) { CopiedBytesOfItem(currentItem, (ulong)copiedBytes); bytesCopiedForItem[currentItem] += (ulong)copiedBytes; } remainingBytes -= (ulong)copiedBytes; } else if (e.Data.Contains("Error:")) { nextMessageIsError = true; } else if (nextMessageIsError) { errorMessage = e.Data; didError = true; nextMessageIsError = false; } }); /** * Command line params: * -y (yes to prompts) * -ssw (Compresses files open for writing by another applications) * -bsp1 (output for progress to stdout) * -bse1 (output for errors to stdout) * -bb1 (log level 1) * -spf (Use fully qualified file paths) * -mx1 (compression level to fastest) * -v2g (split into 2 gb volumes -- https://superuser.com/a/184601) * -sccUTF-8 (set console output to UTF-8) * -p (set password for file) * */ var args = "-y -ssw -bsp1 -bse1 -bb1 -spf -mx1 -v2g -sccUTF-8"; if (UsesPasswordForCompressedFile) { var pass = Utilities.SecureStringToString(CompressedFilePassword); if (!string.IsNullOrWhiteSpace(pass)) { args = "-p" + pass + " " + args; // add password flag } } string inputPaths = string.Join("\n", quotedFilePaths); // to circumvent issue where inputPaths is too long for command line, need to write them to a file // and then load the file into 7z via command line params (@fileName as last param -- https://superuser.com/a/940894) var tmpFileName = Path.GetTempFileName(); using (StreamWriter sw = new StreamWriter(tmpFileName)) { sw.Write(inputPaths); } args = "a " + args + " \"" + destination + "\" @\"" + tmpFileName + "\""; // a = add file process.StartInfo.Arguments = args; process.Start(); process.BeginOutputReadLine(); //process.BeginErrorReadLine(); process.WaitForExit(); // make sure last item is handled properly if (!HasBeenCanceled && currentItem != null && remainingBytes > 0) { CopiedBytesOfItem(currentItem, remainingBytes); FinishedCopyingItem?.Invoke(currentItem); } if (HasBeenCanceled) { File.Delete(destination); } if (didError) { if (string.IsNullOrWhiteSpace(errorMessage)) { errorMessage = "Compression operation failed"; } throw new Exception(errorMessage); } }