} // end BackupProcessor constructor // doBackup(): /// <summary>Run the actual backup job.</summary> public void doBackup() { // Empty the list of warnings, and create a new object to track the total expected size of the backup job. backupProcessWarnings.Clear(); BackupSizeInfo totalExpectedSizeInfo = new BackupSizeInfo() { fileCount_All = 0, fileCount_Unique = 0, byteCount_All = 0, byteCount_Unique = 0 }; // Scan all the source paths to figure out the expected size of the backup job. userInterface.report("Scanning source directory trees:", ConsoleOutput.Verbosity.NormalEvents); foreach (SourcePathInfo sourcePath in sourcePaths) { // Get the expected size for this source path. userInterface.report(1, $"Scanning {sourcePath.BaseItemFullPath}...", ConsoleOutput.Verbosity.NormalEvents); BackupSizeInfo currentTreeSizeInfo = sourcePath.calculateSize(); userInterface.report(2, $"Files found: {currentTreeSizeInfo.fileCount_All:n0}; Bytes: {currentTreeSizeInfo.byteCount_All:n0}", ConsoleOutput.Verbosity.NormalEvents); // Add the results to the total expected size of the entire backup job. totalExpectedSizeInfo += currentTreeSizeInfo; } userInterface.report(1, $"Total files found: {totalExpectedSizeInfo.fileCount_All:n0}; Total Bytes: {totalExpectedSizeInfo.byteCount_All:n0}", ConsoleOutput.Verbosity.NormalEvents); // Set up variables used for tracking the backup process. BackupSizeInfo completedBackupSizeInfo = new BackupSizeInfo { fileCount_All = 0, fileCount_Unique = 0, byteCount_All = 0, byteCount_Unique = 0 }; DateTime copyStartTime = DateTime.Now; try { // Get the backups root directory and ensure it exists. DirectoryInfo backupsRootDirectory = new DirectoryInfo(backupsDestinationRootPath); backupsRootDirectory.Create(); // Open or create the backups database for this backups destination. using (BackupsDatabase database = new BackupsDatabase(backupsDestinationRootPath, userInterface)) { // Create subdirectory for this new backup job, based on the current date and time. DirectoryInfo destinationBaseDirectory = createBackupTimeSubdirectory(backupsRootDirectory); userInterface.report($"Backing up to {destinationBaseDirectory.FullName}", ConsoleOutput.Verbosity.NormalEvents); // Copy all the files from each of the source paths. foreach (SourcePathInfo currentSourcePath in sourcePaths) { BackupSizeInfo currentSourceBackupSizeInfo; string driveName; // Call makeFolderTreeBackup() to do the copying work. // Usually we make a copy of the base source path item within the timestamp directory // (e.g., if the source path is "/foo/bar/", we create a "bar/" directory within the timestamped destination), and everything goes inside that. // But if if the source path is a root directory, that doesn't work, because the source directory doesn't have an actual name. // In that case, we get the drive name (e.g. "C" on Windows), or an empty string if there is no drive name (e.g., on Linux), append "_root", // and use that as the destination directory name. if (pathIsRootDirectory(currentSourcePath.BaseItemFullPath, out driveName)) // CHANGE CODE HERE: handle exceptions { currentSourceBackupSizeInfo = makeFolderTreeBackup( currentSourcePath, Path.Combine(destinationBaseDirectory.FullName, driveName + "_root"), database, totalExpectedSizeInfo, completedBackupSizeInfo); } else { currentSourceBackupSizeInfo = makeFolderTreeBackup( currentSourcePath, destinationBaseDirectory.FullName, database, totalExpectedSizeInfo, completedBackupSizeInfo); } // Update the total completed backup size info with the size of this now-completed individual source path. completedBackupSizeInfo += currentSourceBackupSizeInfo; } } // end using(database) } catch (System.IO.PathTooLongException ex) { userInterface.report($"Error: Path too long: {ex.Message}", ConsoleOutput.Verbosity.ErrorsAndWarnings); userInterface.report("You may need to enable long path support for your operating system, use a file system which supports longer paths (e.g., NTFS, ext3, or ext4 rather than FAT), or create a symbolic link to the destination directory to shorten the path string.", ConsoleOutput.Verbosity.ErrorsAndWarnings); } // Grab the end time of the copy process. DateTime copyEndTime = DateTime.Now; // Calculate how long, in seconds, the copy process took. int totalTime = (int)Math.Round(copyEndTime.Subtract(copyStartTime).TotalSeconds); // Figure out how many files and bytes we copied in total, as physical copies, and as hard links. long totalFiles = completedBackupSizeInfo.fileCount_All, physicallyCopiedFiles = completedBackupSizeInfo.fileCount_Unique, skippedFiles = completedBackupSizeInfo.fileCount_Skip, linkedFiles = totalFiles - physicallyCopiedFiles - skippedFiles, allCopiedFiles = totalFiles - skippedFiles; long totalBytes = completedBackupSizeInfo.byteCount_All, physicallyCopiedBytes = completedBackupSizeInfo.byteCount_Unique, skippedBytes = completedBackupSizeInfo.byteCount_Skip, linkedBytes = totalBytes - physicallyCopiedBytes - skippedBytes, allCopiedBytes = totalBytes - skippedBytes; userInterface.report($"Backup complete.", ConsoleOutput.Verbosity.NormalEvents); // If there were any warnings generated (e.g., directories skipped due to access permissions), report those. if (backupProcessWarnings.Count > 0) { userInterface.report("", ConsoleOutput.Verbosity.NormalEvents); foreach (string warning in backupProcessWarnings) { userInterface.report($"Warning: {warning}", ConsoleOutput.Verbosity.ErrorsAndWarnings); } userInterface.report("", ConsoleOutput.Verbosity.NormalEvents); } // Report the final totals from the backup process. userInterface.report(1, $"Copy process duration: {totalTime:n0} seconds.", ConsoleOutput.Verbosity.NormalEvents); userInterface.report(1, $"Total files copied: {allCopiedFiles:n0} ({physicallyCopiedFiles:n0} new physical copies needed, {linkedFiles:n0} hardlinks utilized)", ConsoleOutput.Verbosity.NormalEvents); userInterface.report(1, $"Total bytes copied: {allCopiedBytes:n0} ({physicallyCopiedBytes:n0} physically copied, {linkedBytes:n0} hardlinked)", ConsoleOutput.Verbosity.NormalEvents); if (skippedFiles > 0) { userInterface.report(1, $"Skipped: {skippedFiles:n} files ({skippedBytes:n} bytes)", ConsoleOutput.Verbosity.NormalEvents); } } // end doBackup()
} // end completionPercentage() // makeFolderTreeBackup(): /// <summary>Does the backup copying from a specified source path to a specified destination, storing info in the specified database.</summary> /// <returns>A <c>BackupSizeInfo</c> object containing the total size of the copy job that was completed.</returns> /// <param name="sourceInfo">The source to copy from.</param> /// <param name="destinationBasePath">The base destination path.</param> /// <param name="database">The database to use for looking up and storing copy and hard link match info.</param> /// <param name="totalExpectedBackupSize">The total expected size of the backup job that is in progress.</param> /// <param name="previouslyCompleteSizeInfo">The total size of the backup job completed up to this point.</param> private BackupSizeInfo makeFolderTreeBackup( SourcePathInfo sourceInfo, string destinationBasePath, BackupsDatabase database, BackupSizeInfo totalExpectedBackupSize, BackupSizeInfo previouslyCompleteSizeInfo) { // Set up variable to track the size of copying done within this directory tree. BackupSizeInfo thisTreeCompletedSizeInfo = new BackupSizeInfo() { fileCount_All = 0, fileCount_Unique = 0, byteCount_All = 0, byteCount_Unique = 0 }; // Set up variables for tracking ongoing changes to the completion percentage, for the purpose of reporting updates to the user. int previousPercentComplete, percentComplete = getCompletionPercentage(totalExpectedBackupSize.byteCount_All, previouslyCompleteSizeInfo.byteCount_All); // Iterate through each item in the source, copying it to the destination. foreach (BackupItemInfo item in sourceInfo.getAllItems()) { // Get the full path string for the object as it will exist in the destination. string fullItemDestinationPath = Path.Combine(destinationBasePath, item.RelativePath); if (item.Type == BackupItemInfo.ItemType.Directory) { (new DirectoryInfo(fullItemDestinationPath)).Create(); // Item is a directory, so simply create it at the destination location. } else if (item.Type == BackupItemInfo.ItemType.UnreadableDirectory) { backupProcessWarnings.Add($"Directory skipped due to unauthorized access error: {item.RelativePath}"); // Item is a directory but can't be read, so skip it and add a warning to show the user at the end. } else // Item is a file. { // Calculate the source file hash. FileInfo currentSourceFile = new FileInfo(item.FullPath); string fileHash; try { fileHash = getHash(currentSourceFile); } // CHANGE CODE HERE: Handle all the possible exceptions catch (Exception ex) when(ex is UnauthorizedAccessException || ex is System.IO.IOException) { if (ex is UnauthorizedAccessException) { backupProcessWarnings.Add($"File skipped due to unauthorized access error: {item.RelativePath}"); } else { backupProcessWarnings.Add($"File skipped, unable to read. Another process may be using the file: {item.RelativePath}"); } thisTreeCompletedSizeInfo.fileCount_Skip++; thisTreeCompletedSizeInfo.fileCount_All++; thisTreeCompletedSizeInfo.byteCount_Skip += currentSourceFile.Length; thisTreeCompletedSizeInfo.byteCount_All += currentSourceFile.Length; previousPercentComplete = percentComplete; percentComplete = getCompletionPercentage(totalExpectedBackupSize.byteCount_All, previouslyCompleteSizeInfo.byteCount_All + thisTreeCompletedSizeInfo.byteCount_All); userInterface.reportProgress(percentComplete, previousPercentComplete, ConsoleOutput.Verbosity.NormalEvents); continue; // Skip to the next item in the foreach loop. } // Look in the database and find an existing, previously backed up file to create a hard link to, // if any exists within the current run's rules for using links. HardLinkMatch hardLinkMatch = database.getAvailableHardLinkMatch( fileHash, currentSourceFile.Length, currentSourceFile.LastWriteTimeUtc, maxHardLinksPerFile, maxDaysBeforeNewFullFileCopy); // Ensure the destination directory for this file exists. (new FileInfo(fullItemDestinationPath)).Directory.Create(); // Make a full copy of the file if needed, but otherwise create a hard link from a previous backup if (hardLinkMatch == null) { // No hard link match found in the database, so do a copy operation. userInterface.report(1, $"Backing up file {item.FullPath} to {fullItemDestinationPath} [copying]", ConsoleOutput.Verbosity.LowImportanceEvents); currentSourceFile.CopyTo(fullItemDestinationPath); thisTreeCompletedSizeInfo.fileCount_Unique++; thisTreeCompletedSizeInfo.byteCount_Unique += currentSourceFile.Length; } else { // A usable hard link match was found in the database, so create a hard link instead of making a new full copy. string linkFilePath = hardLinkMatch.MatchingFilePath; userInterface.report(1, $"Backing up file {item.FullPath} to {fullItemDestinationPath} [identical existing file found; creating hardlink to {linkFilePath}]", ConsoleOutput.Verbosity.LowImportanceEvents); hardLinker.createHardLink(fullItemDestinationPath, linkFilePath); // CHANGE CODE HERE: Handle exceptions. } // Add this file to the total copy amounts being tracked. thisTreeCompletedSizeInfo.fileCount_All++; thisTreeCompletedSizeInfo.byteCount_All += currentSourceFile.Length; // Record in the backups database the new copy or link that was made. FileInfo newFile = new FileInfo(fullItemDestinationPath); database.addFileBackupRecord(fullItemDestinationPath, newFile.Length, fileHash, newFile.LastWriteTimeUtc, (hardLinkMatch == null ? null : hardLinkMatch.ID)); } // end if/else on (item.Type == BackupItemInfo.ItemType.Directory) // Figure out the new completion percentage, and update the user on the progress. previousPercentComplete = percentComplete; percentComplete = getCompletionPercentage(totalExpectedBackupSize.byteCount_All, previouslyCompleteSizeInfo.byteCount_All + thisTreeCompletedSizeInfo.byteCount_All); userInterface.reportProgress(percentComplete, previousPercentComplete, ConsoleOutput.Verbosity.NormalEvents); } // end foreach (BackupItemInfo item in sourcePath.Items) return(thisTreeCompletedSizeInfo); } // end makeFolderTreeBackup()