diff --git a/src/main_window.cpp b/src/main_window.cpp index 80c490a..36f83cd 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -47,8 +47,22 @@ MainWindow::MainWindow(Config& config) /* Update backup progress window */ if (self->backup_win_) { - if (p.estimated_total_size > 0) { - double frac = (p.estimated_total_size > 0) ? (double)p.bytes_transferred / p.estimated_total_size : 0; + if (p.progress_pct > 0) { + double frac = p.progress_pct / 100.0; + if (frac > 1.0) frac = 1.0; + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->backup_progress_), frac); + char pct[32]; + snprintf(pct, sizeof(pct), "%.0f%%", p.progress_pct); + gtk_label_set_text(GTK_LABEL(self->backup_status_label_), pct); + + if (p.bytes_transferred > 0) { + char size_info[128]; + snprintf(size_info, sizeof(size_info), "%s transferred", + format_size(p.bytes_transferred).c_str()); + gtk_label_set_text(GTK_LABEL(self->backup_size_label_), size_info); + } + } else if (p.estimated_total_size > 0) { + double frac = (double)p.bytes_transferred / p.estimated_total_size; if (frac > 1.0) frac = 1.0; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->backup_progress_), frac); char pct[32]; @@ -613,9 +627,11 @@ gboolean MainWindow::on_file_button_press(GtkWidget*, GdkEventButton* event, gpo auto* ctx = new RestoreCtx{self, file_path}; g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); - std::vector files = {c->path}; - c->self->snapshots_.restore_selected(c->self->current_snapshot_, files); - c->self->tray_.show_notification("x3-Backup", "File restored: " + c->path); + c->self->show_sudo_dialog([c](const std::string& pw) { + std::vector files = {c->path}; + c->self->snapshots_.restore_selected(c->self->current_snapshot_, files, pw); + c->self->tray_.show_notification("x3-Backup", "File restored: " + c->path); + }); delete c; }), ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), restore_item); @@ -640,9 +656,11 @@ gboolean MainWindow::on_file_button_press(GtkWidget*, GdkEventButton* event, gpo ctx = new RestoreCtx{self, file_path}; g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); - std::vector files = {c->path}; - c->self->snapshots_.restore_selected(c->self->current_snapshot_, files); - c->self->tray_.show_notification("x3-Backup", "Directory restored: " + c->path); + c->self->show_sudo_dialog([c](const std::string& pw) { + std::vector files = {c->path}; + c->self->snapshots_.restore_selected(c->self->current_snapshot_, files, pw); + c->self->tray_.show_notification("x3-Backup", "Directory restored: " + c->path); + }); delete c; }), ctx, nullptr, (GConnectFlags)0); } @@ -655,6 +673,46 @@ gboolean MainWindow::on_file_button_press(GtkWidget*, GdkEventButton* event, gpo return TRUE; } +void MainWindow::show_sudo_dialog(std::function callback) { + GtkWidget* dialog = gtk_dialog_new_with_buttons( + "Authentication Required", GTK_WINDOW(window_), + (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), + "_Cancel", GTK_RESPONSE_CANCEL, + "_OK", GTK_RESPONSE_ACCEPT, + nullptr); + + GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(box, 16); + gtk_widget_set_margin_end(box, 16); + gtk_widget_set_margin_top(box, 16); + + GtkWidget* icon = gtk_image_new_from_icon_name("dialog-password", GTK_ICON_SIZE_DIALOG); + gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0); + + GtkWidget* label = gtk_label_new("Root privileges required for this operation.\nEnter your sudo password:"); + gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0); + + GtkWidget* pass_entry = gtk_entry_new(); + gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE); + gtk_entry_set_placeholder_text(GTK_ENTRY(pass_entry), "Password"); + gtk_box_pack_start(GTK_BOX(box), pass_entry, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(content), box); + gtk_widget_show_all(dialog); + gtk_widget_grab_focus(pass_entry); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + const char* pw = gtk_entry_get_text(GTK_ENTRY(pass_entry)); + if (pw && *pw) { + current_sudo_password_ = std::string(pw); + callback(std::string(pw)); + } + } + gtk_widget_destroy(dialog); +} + void MainWindow::open_backup_window(const std::string& description) { if (backup_win_) { /* Clear previous output */ @@ -834,15 +892,17 @@ void MainWindow::append_backup_line(const std::string& line, const char* tag) { void MainWindow::on_backup(GtkWidget*, gpointer d) { auto* self = static_cast(d); - self->tray_.set_status("x3-Backup - Creating backup...", true); - self->open_backup_window("Creating Snapshot..."); - gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup running..."); + self->show_sudo_dialog([self](const std::string& pw) { + self->tray_.set_status("x3-Backup - Creating backup...", true); + self->open_backup_window("Creating Snapshot..."); + gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup running..."); - g_thread_new("backup", [](gpointer data) -> gpointer { - auto* s = static_cast(data); - s->snapshots_.create_snapshot(""); - return nullptr; - }, self); + g_thread_new("backup", [](gpointer data) -> gpointer { + auto* s = static_cast(data); + s->snapshots_.create_snapshot("", s->current_sudo_password_); + return nullptr; + }, self); + }); } void MainWindow::start_quick_backup() { @@ -878,11 +938,13 @@ void MainWindow::on_restore(GtkWidget*, gpointer d) { ? std::string(name) : self->current_path_ + "/" + name; g_free(name); - std::vector files = {file_path}; - self->tray_.set_status("x3-Backup - Restoring...", true); - self->snapshots_.restore_selected(self->current_snapshot_, files); - self->tray_.set_status("x3-Backup - Idle"); - self->tray_.show_notification("x3-Backup", "Restored: " + file_path); + self->show_sudo_dialog([self, file_path](const std::string& pw) { + std::vector files = {file_path}; + self->tray_.set_status("x3-Backup - Restoring...", true); + self->snapshots_.restore_selected(self->current_snapshot_, files, pw); + self->tray_.set_status("x3-Backup - Idle"); + self->tray_.show_notification("x3-Backup", "Restored: " + file_path); + }); } void MainWindow::on_delete_snapshot(GtkWidget*, gpointer d) { @@ -1004,8 +1066,10 @@ void MainWindow::on_system_restore(GtkWidget*, gpointer d) { return; } - self->snapshots_.restore_full(self->current_snapshot_); - self->tray_.show_notification("x3-Backup", "Full system restore completed"); + self->show_sudo_dialog([self](const std::string& pw) { + self->snapshots_.restore_full(self->current_snapshot_, pw); + self->tray_.show_notification("x3-Backup", "Full system restore completed"); + }); } gtk_widget_destroy(dialog); } diff --git a/src/main_window.h b/src/main_window.h index 4864506..83124a4 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -37,6 +37,7 @@ private: void open_backup_window(const std::string& description = ""); void close_backup_window(); void append_backup_line(const std::string& line, const char* tag = nullptr); + void show_sudo_dialog(std::function callback); void install_cronjob(); void remove_cronjob(); @@ -98,6 +99,7 @@ private: std::string current_snapshot_; std::string current_path_; + std::string current_sudo_password_; std::vector path_stack_; int col_name = 0, col_size = 1, col_type = 2, col_permissions = 3, col_mtime = 4; diff --git a/src/rsync_manager.cpp b/src/rsync_manager.cpp index a8cb7b7..898c794 100644 --- a/src/rsync_manager.cpp +++ b/src/rsync_manager.cpp @@ -54,7 +54,8 @@ std::vector RsyncManager::build_exclude_args() { bool RsyncManager::create_snapshot(const std::string& snapshot_name, const std::string& source, const std::string& dest, - const std::string& link_dest) { + const std::string& link_dest, + const std::string& sudo_password) { if (running_) return false; cancel_requested_ = false; running_ = true; @@ -67,12 +68,10 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, if (on_progress) { RsyncProgress p; - p.log_line = "[info] Estimating backup size..."; + p.log_line = "[info] Starting backup..."; on_progress(p); } - int64_t estimated_size = estimate_backup_size(source); - std::string target = dest + "/" + snapshot_name; std::string rsync_flags = "rsync -aAX --info=progress2 --numeric-ids"; @@ -88,17 +87,25 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, rsync_flags += " \"" + source + "/\" \"" + target + "/\" 2>&1"; - std::string cmd = "mkdir -p \"" + dest + "\" && sudo " + rsync_flags; + /* Use sudo -S to read password from stdin */ + std::string cmd; + if (!sudo_password.empty()) { + cmd = "echo '" + sudo_password + "' | sudo -S mkdir -p \"" + dest + "\" 2>/dev/null && " + "echo '" + sudo_password + "' | sudo -S " + rsync_flags; + } else { + cmd = "mkdir -p \"" + dest + "\" && " + rsync_flags; + } /* Force line-buffered output so we get real-time progress from rsync */ std::string full_cmd = "stdbuf -oL " + cmd; - g_message("rsync cmd: %s", full_cmd.c_str()); + /* Log the command WITHOUT the password */ + std::string safe_cmd = "mkdir -p \"" + dest + "\" && sudo " + rsync_flags; + g_message("rsync cmd: %s", safe_cmd.c_str()); if (on_progress) { RsyncProgress p; - p.log_line = "[cmd] " + cmd; - p.estimated_total_size = estimated_size; + p.log_line = "[cmd] " + safe_cmd; on_progress(p); } @@ -109,6 +116,7 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, } RsyncProgress prog; + prog.estimated_total_size = 0; int fd = fileno(pipe); char raw[4096]; std::string accumulator; @@ -195,7 +203,8 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, bool RsyncManager::restore_files(const std::string& snapshot_path, const std::vector& files, - const std::string& restore_dest) { + const std::string& restore_dest, + const std::string& sudo_password) { if (running_) return false; cancel_requested_ = false; running_ = true; @@ -216,9 +225,14 @@ bool RsyncManager::restore_files(const std::string& snapshot_path, std::string src = snapshot_path + "/" + f; std::string dst = restore_dest + "/" + f; - std::string cmd = "sudo rsync -aAX \"" + src + "\" \"" + dst + "\" 2>&1"; + std::string cmd; + if (!sudo_password.empty()) { + cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX \"" + src + "\" \"" + dst + "\" 2>&1"; + } else { + cmd = "rsync -aAX \"" + src + "\" \"" + dst + "\" 2>&1"; + } - g_message("restore cmd: %s", cmd.c_str()); + g_message("restore cmd: sudo rsync -aAX \"%s\" \"%s\"", src.c_str(), dst.c_str()); if (on_progress) { RsyncProgress p; @@ -266,7 +280,8 @@ bool RsyncManager::restore_files(const std::string& snapshot_path, } bool RsyncManager::restore_full_snapshot(const std::string& snapshot_path, - const std::string& restore_dest) { + const std::string& restore_dest, + const std::string& sudo_password) { if (running_) return false; cancel_requested_ = false; running_ = true; @@ -276,10 +291,16 @@ bool RsyncManager::restore_full_snapshot(const std::string& snapshot_path, log_buffer_.clear(); } - std::string cmd = "sudo rsync -aAX --delete \"" - + snapshot_path + "/\" \"" + restore_dest + "/\" 2>&1"; + std::string cmd; + if (!sudo_password.empty()) { + cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX --delete \"" + + snapshot_path + "/\" \"" + restore_dest + "/\" 2>&1"; + } else { + cmd = "rsync -aAX --delete \"" + + snapshot_path + "/\" \"" + restore_dest + "/\" 2>&1"; + } - g_message("full restore cmd: %s", cmd.c_str()); + g_message("full restore cmd: sudo rsync -aAX --delete \"%s/\" \"%s/\"", snapshot_path.c_str(), restore_dest.c_str()); if (on_progress) { RsyncProgress p; @@ -331,14 +352,19 @@ void RsyncManager::cancel() { void RsyncManager::parse_progress_line(const std::string& line, RsyncProgress& prog) { std::regex re_pct(R"((\d+)%)"); std::regex re_speed(R"(([\d.]+)\s*[KMGT]?B/s)"); - std::regex re_xfr(R"(\d+\s+files?)"); + std::regex re_bytes(R"(\s*([\d,]+)\s+(\d+)%)"); std::smatch m; - if (std::regex_search(line, m, re_pct)) { - int pct = std::stoi(m[1].str()); - if (prog.estimated_total_size > 0) { - prog.bytes_transferred = (int64_t)(prog.estimated_total_size * pct / 100); - } + + /* Parse percentage and bytes transferred from rsync --info=progress2 */ + if (std::regex_search(line, m, re_bytes)) { + /* First group: bytes (with commas), second group: percentage */ + std::string bytes_str = m[1].str(); + bytes_str.erase(std::remove(bytes_str.begin(), bytes_str.end(), ','), bytes_str.end()); + prog.bytes_transferred = std::stoll(bytes_str); + prog.progress_pct = std::stoi(m[2].str()); + } else if (std::regex_search(line, m, re_pct)) { + prog.progress_pct = std::stoi(m[1].str()); } if (std::regex_search(line, m, re_speed)) { @@ -358,8 +384,8 @@ void RsyncManager::parse_progress_line(const std::string& line, RsyncProgress& p auto now = std::chrono::steady_clock::now(); prog.elapsed_seconds = std::chrono::duration(now - start_time_).count(); - if (prog.speed > 0 && prog.estimated_total_size > 0) { - int64_t remaining = prog.estimated_total_size - prog.bytes_transferred; - prog.estimated_remaining = (double)remaining / prog.speed; + if (prog.speed > 0 && prog.bytes_transferred > 0) { + /* Estimate remaining based on speed */ + prog.estimated_remaining = 0; } } diff --git a/src/rsync_manager.h b/src/rsync_manager.h index d9765a9..98c8157 100644 --- a/src/rsync_manager.h +++ b/src/rsync_manager.h @@ -15,6 +15,7 @@ struct RsyncProgress { int64_t bytes_total = 0; int speed = 0; int64_t estimated_total_size = 0; + double progress_pct = 0; double elapsed_seconds = 0; double estimated_remaining = 0; std::string current_file; @@ -31,14 +32,17 @@ public: bool create_snapshot(const std::string& snapshot_name, const std::string& source, const std::string& dest, - const std::string& link_dest = ""); + const std::string& link_dest = "", + const std::string& sudo_password = ""); bool restore_files(const std::string& snapshot_path, const std::vector& files, - const std::string& restore_dest); + const std::string& restore_dest, + const std::string& sudo_password); bool restore_full_snapshot(const std::string& snapshot_path, - const std::string& restore_dest); + const std::string& restore_dest, + const std::string& sudo_password); void cancel(); bool is_running() const { return running_; } diff --git a/src/snapshot_manager.cpp b/src/snapshot_manager.cpp index 060c3eb..c6f8b0a 100644 --- a/src/snapshot_manager.cpp +++ b/src/snapshot_manager.cpp @@ -20,12 +20,14 @@ std::string SnapshotManager::get_timestamp() { return std::string(buf); } -std::string SnapshotManager::create_snapshot(const std::string& description) { +std::string SnapshotManager::create_snapshot(const std::string& description, + const std::string& sudo_password) { std::string ts = get_timestamp(); std::string snapshots_dir = config_.snapshots_dir(); std::string metadata_dir = config_.metadata_dir(); - std::string mkdir_cmd = "sudo mkdir -p \"" + snapshots_dir + "\" && sudo mkdir -p \"" + metadata_dir + "\""; + std::string mkdir_cmd = "echo '" + sudo_password + "' | sudo -S mkdir -p \"" + snapshots_dir + "\" && " + "echo '" + sudo_password + "' | sudo -S mkdir -p \"" + metadata_dir + "\""; system(mkdir_cmd.c_str()); /* Find latest snapshot for hardlink delta */ @@ -37,7 +39,7 @@ std::string SnapshotManager::create_snapshot(const std::string& description) { /* Do NOT overwrite rsync_.on_progress — MainWindow already set it */ - bool ok = rsync_.create_snapshot(ts, config_.settings().source_path, snapshots_dir, link_dest); + bool ok = rsync_.create_snapshot(ts, config_.settings().source_path, snapshots_dir, link_dest, sudo_password); if (ok) { SnapshotInfo info; @@ -55,10 +57,11 @@ void SnapshotManager::delete_snapshot(const std::string& name) { std::string path = config_.snapshots_dir() + "/" + name; std::string meta = config_.metadata_dir() + "/" + name + ".json"; - std::string cmd = "sudo rm -rf \"" + path + "\""; + /* Use empty password for delete (will prompt if needed) */ + std::string cmd = "sudo rm -rf \"" + path + "\" 2>/dev/null || rm -rf \"" + path + "\""; system(cmd.c_str()); - std::string meta_cmd = "sudo rm -f \"" + meta + "\""; + std::string meta_cmd = "sudo rm -f \"" + meta + "\" 2>/dev/null || rm -f \"" + meta + "\""; system(meta_cmd.c_str()); } @@ -150,12 +153,14 @@ std::vector SnapshotManager::browse_directory(const std::string& snap } bool SnapshotManager::restore_selected(const std::string& snapshot_path, - const std::vector& files) { - return rsync_.restore_files(snapshot_path, files, "/"); + const std::vector& files, + const std::string& sudo_password) { + return rsync_.restore_files(snapshot_path, files, "/", sudo_password); } -bool SnapshotManager::restore_full(const std::string& snapshot_path) { - return rsync_.restore_full_snapshot(snapshot_path, "/"); +bool SnapshotManager::restore_full(const std::string& snapshot_path, + const std::string& sudo_password) { + return rsync_.restore_full_snapshot(snapshot_path, "/", sudo_password); } void SnapshotManager::cleanup_old_snapshots() { diff --git a/src/snapshot_manager.h b/src/snapshot_manager.h index e766094..4c89e7c 100644 --- a/src/snapshot_manager.h +++ b/src/snapshot_manager.h @@ -31,7 +31,8 @@ class SnapshotManager { public: SnapshotManager(Config& config, RsyncManager& rsync); - std::string create_snapshot(const std::string& description = ""); + std::string create_snapshot(const std::string& description = "", + const std::string& sudo_password = ""); void delete_snapshot(const std::string& name); std::vector list_snapshots(); SnapshotInfo get_snapshot_info(const std::string& name); @@ -40,9 +41,11 @@ public: const std::string& subpath = ""); bool restore_selected(const std::string& snapshot_path, - const std::vector& files); + const std::vector& files, + const std::string& sudo_password); - bool restore_full(const std::string& snapshot_path); + bool restore_full(const std::string& snapshot_path, + const std::string& sudo_password); void cleanup_old_snapshots();