GoogleDriveZipBackupTool
(License: This article, along with any associated source code, is licensed under The LGPL 2.1)
In Part 1 , we explored the design and implementation of GoogleDriveBackup.Core, a robust class library containing the essential logic for backing up, restoring, and repairing Google Drive data using C# and .NET. We discussed its architecture, including managers for different tasks, the crucial role of the manifest file, the flat archive structure, and the implementation of features like incremental backups, parallel processing, and resumable restores.
This second part focuses on the GoogleDriveZipBackupTool console application – the user-facing layer that leverages the power of the Core library. We'll examine how this application handles user interaction, parses command-line arguments, manages application flow, displays progress and results, and ultimately acts as an orchestrator, directing the Core library to perform the requested actions. This demonstrates the practical benefits of the UI/Core separation discussed in Part 1.
The solution consists of two main projects:
GoogleDriveBackup.Core: The class library developed in Part 1. Contains no UI code.
GoogleDriveZipBackupTool: A .NET Console Application project that references GoogleDriveBackup.Core. This is our UI layer.
The Main method in Program.cs serves as the application's entry point and sets up the essential groundwork:
Logging: Serilog is configured to provide detailed logging to both the console (INFO level and above) and rolling daily log files (DEBUG level and above). This is invaluable for diagnostics.
Cancellation Handling: A CancellationTokenSource (_cts) is initialized. The Console.CancelKeyPress event is hooked to call _cts.Cancel() when the user presses Ctrl+C. This token is then passed down through the application layers, allowing long-running operations within the Core library (like downloads or uploads) to be gracefully interrupted.
Mode Detection: It checks args.Length. If arguments are present, it enters Command-Line Interface (CLI) mode (HandleCommandLineAsync). Otherwise, it proceeds to Interactive Mode (RunAppLogicAsync).
Global Exception Handling: A top-level try-catch block ensures that any unhandled exceptions (including OperationCanceledException) are logged, a user-friendly message is displayed, and an appropriate exit code is returned.
Log Flushing: Log.CloseAndFlushAsync() is called in a finally block to ensure all buffered log messages are written before the application exits.
// Simplified Main structure
public static class Program
{
private static CancellationTokenSource? _cts;
// ... other static fields ...
static async Task<int> Main(string[] args)
{
// 1. Configure Serilog (File and Console sinks)
Log.Logger = new LoggerConfiguration() /* ... */ .CreateLogger();
_cts = new CancellationTokenSource();
// 2. Setup Ctrl+C Cancellation
Console.CancelKeyPress += (sender, eventArgs) => {
Log.Warning("Cancellation requested via Ctrl+C.");
_cts?.Cancel();
eventArgs.Cancel = true; // Prevent immediate termination
};
int exitCode = 0;
try
{
// 3. Mode Detection
if (args.Length > 0)
{
Log.Information("Starting (Command Line Mode)");
exitCode = await HandleCommandLineAsync(args);
}
else
{
Log.Information("Starting (Interactive Mode)");
await RunAppLogicAsync(); // Interactive loop doesn't typically return error code
}
}
// 4. Global Exception Handling
catch (OperationCanceledException) { /* ... log and set exitCode ... */ }
catch (Exception ex) { /* ... log fatal, set exitCode ... */ }
finally
{
_cts?.Dispose();
// 5. Flush Logs
await Log.CloseAndFlushAsync();
// Optional: Keep console open in interactive mode
}
return exitCode;
}
// ... other methods ...
}
The CLI mode enables automation and scripting. Key aspects include:
Argument Parsing (ParseArguments): A simple helper function splits arguments based on = into a Dictionary<string, string>. It handles quoted values and basic validation. Keys are treated case-insensitively.
Settings Loading & Overrides:
It checks for a settings=path/to/profile.settings.json argument. If present, it uses SettingsManager.LoadSettingsFromPath to load the specified profile. Otherwise, it loads the default app_settings.json using SettingsManager.LoadSettings.
The ApplySettingOverrides method iterates through the parsed arguments. For keys matching properties in AppSettings (e.g., localbackuppath, cycle, paralleltasks), it updates the loaded AppSettings object in memory for this specific run. This allows temporary changes without modifying the underlying settings file.
Conditional Execution (runIfDue=true):
If this flag is present, the application checks the LastSuccessfulBackupUtc property (loaded from the settings file) against the configured BackupCycleHours.
It calculates the time elapsed since the last successful backup.
If the elapsed time is less than the cycle duration, it logs a message and exits gracefully (exit code 0), skipping the backup. Otherwise, it proceeds. This enables scheduled tasks to run the backup only when necessary.
Authentication: Calls GoogleDriveService.AuthenticateAsync() to ensure valid credentials before proceeding with actions requiring API access.
Core Manager Instantiation: Creates instances of BackupManager, RestoreManager, and RepairManager, passing the authenticated DriveService and the effective AppSettings object.
Action Dispatching: A switch statement based on the mandatory action= argument determines the operation (backup, restore, resume-restore, repair).
Invoking Core Methods: The appropriate method on the corresponding Core manager is called asynchronously, passing necessary parameters (like input paths, previous backup paths) and the CancellationToken.
// Example: Calling BackupManager from CLI handler
case "backup":
// ... checks, get previous path ...
Log.Information("Starting Backup (CLI)...");
BackupResult backupResult = await backupManager.StartBackupAsync(
_appSettings.GoogleDriveFolderId!,
prevBackupPath,
progress, // IProgress<BackupProgressReport> instance
_cts!.Token); // Pass the cancellation token
DisplayBackupResult(backupResult); // Show summary
success = backupResult.Success && !backupResult.Cancelled;
if (success) {
// Update LastSuccessfulBackupUtc in _appSettings
// Save _appSettings back using SettingsManager.SaveSettingsToPath
// Update legacy status file if needed
}
break;
Result Handling & State Updates: After the Core method returns a result object (BackupResult, RestoreResult, RepairResult), the DisplayXResult helper formats and prints a summary. For successful backups, the LastSuccessfulBackupUtc timestamp in the AppSettings object is updated, and SettingsManager saves the modified settings back to the file used for the run (either the default or the specified profile). This ensures the runIfDue logic works correctly next time.
Exit Codes: Returns 0 for success (or successful skip via runIfDue), 1 for general errors, and 2 for user cancellation.
This mode provides a menu-driven experience:
Initialization: Loads default settings (SettingsManager.LoadSettings) and status (StatusManager.LoadBackupStatus). Performs an initial CheckBackupCycle and authenticates using GoogleDriveService.AuthenticateAsync. Instantiates the Core managers.
Main Loop: A while(true) loop continuously displays the menu (PrintMenu) and prompts for user input.
Menu Options & Core Interaction: A switch statement handles the user's choice:
1. Backup: Prompts for an optional previous backup path (PromptForPreviousBackup), creates a progress handler (CreateProgressHandler), calls backupManager.StartBackupAsync, displays the result (DisplayBackupResult), and if successful, updates _appSettings.LastSuccessfulBackupUtc and saves both the main settings file (SettingsManager.SaveSettings) and the legacy status file (StatusManager.SaveBackupStatus).
2. Restore: Prompts for the backup ZIP path (PromptForZipPath), creates a progress handler, calls restoreManager.StartRestoreAsync (with null for the resume path), and displays the result (DisplayRestoreResult).
3. Resume Restore: Prompts for the temporary folder path containing the _restore_state.json file, creates a progress handler, calls restoreManager.StartRestoreAsync (with null for the archive path and the provided folder path for resume), and displays the result.
4. Configure Settings: Enters a sub-loop (ConfigureSettingsAndUpdateGlobals) that allows the user to modify properties of the AppSettings object (Drive IDs, paths, cycle, verbosity, parallel tasks) interactively using helper prompts (PromptForSettingChange, etc.). If changes are saved, SettingsManager.SaveSettings persists them to app_settings.json, and the Core managers are re-instantiated with the updated settings.
5. Repair: Prompts for the damaged ZIP path (PromptForZipPath), creates a progress handler, calls repairManager.RepairBackupAsync, and displays the result (DisplayRepairResult).
6. Manage Exclusions: Enters a sub-loop (ManageExclusions) to add/remove/clear exclusion paths stored in AppSettings.ExcludedRelativePaths. Saves changes via SettingsManager.SaveSettings.
7/8. Save/Load Profiles: Prompts for a file path and uses SettingsManager.SaveSettingsToPath or SettingsManager.LoadSettingsFromPath to persist or load the entire AppSettings object. Loading a profile prompts the user to confirm applying and saving it as the new default configuration.
9. Check Status: Calls CheckBackupCycle to display the time since the last backup relative to the configured cycle.
10. Exit: Breaks the loop.
Re-instantiation: After operations that modify settings (Configure, Load Profile, Manage Exclusions), the Core manager instances (backupManager, restoreManager, repairManager) are re-created using the newly loaded/modified AppSettings to ensure they operate with the current configuration.
The console application handles several cross-cutting concerns:
Progress Reporting (CreateProgressHandler): This method creates an IProgress<BackupProgressReport> instance. The lambda expression within it decides how to display progress based on the ShowVerboseProgress setting:
Verbose: Prints detailed messages for almost every step, including file paths.
Summary: Uses \r (carriage return) to update a single line showing the current action and progress count (e.g., [Core] Downloading Files: 123 of 456...), throttling updates to avoid excessive console flickering. It prints "Done." when an action completes.
Result Display (DisplayBackupResult, etc.): Helper methods take the result objects returned by the Core managers and format them into a user-friendly summary table printed to the console, highlighting success/failure and key statistics.
User Prompts: Consistent helper functions (PromptForZipPath, PromptForSettingChange, etc.) are used to get input from the user in interactive mode, often showing the current or default value.
Error/Warning Feedback: Uses Console.ForegroundColor to highlight errors (Red) and warnings (Yellow) distinctly from normal output.
CLI Examples:
GoogleDriveZipBackupTool.exe action=backup runIfDue=true settings=C:\Backups\MyWorkProfile.settings.json parallelTasks=4
(Runs backup using a specific profile, only if due, with 4 parallel downloads)
GoogleDriveZipBackupTool.exe action=restore input="C:\Backups\Archive 2023-10-26.zip"
(Restores the specified archive using default settings)
GoogleDriveZipBackupTool.exe action=repair input=MyBackup.zip
(Attempts to repair MyBackup.zip using default settings)
GoogleDriveZipBackupTool.exe action=resume-restore resume="C:\Temp\restore_extract_abc123"
(Resumes an interrupted restore from the specified temp folder)
Interactive Mode:
Simply run GoogleDriveZipBackupTool.exe without arguments and follow the on-screen menu prompts.
The GoogleDriveZipBackupTool console application effectively demonstrates how to build a functional and flexible user interface on top of a well-defined core library like GoogleDriveBackup.Core. By handling argument parsing, user interaction, progress display, and orchestrating calls to the Core managers, it provides both scriptable automation via its CLI and an easy-to-use menu system for interactive users. The clear separation of concerns, established in Part 1, makes the console application relatively straightforward, allowing it to focus on UI tasks while relying on the Core library for the complex underlying logic. This architecture not only results in a cleaner, more maintainable application but also opens the door for easily creating alternative interfaces, such as a GUI, in the future by reusing the same powerful core engine.