diff --git a/src/main_window.cpp b/src/main_window.cpp index a0c62bf..ac91cdc 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -46,13 +46,32 @@ MainWindow::MainWindow(Config& config) /* Update backup progress window */ if (self->backup_win_) { - if (p.bytes_total > 0) { - double frac = (double)p.bytes_transferred / p.bytes_total; + if (p.estimated_total_size > 0) { + double frac = (p.estimated_total_size > 0) ? (double)p.bytes_transferred / p.estimated_total_size : 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%%", frac * 100); gtk_label_set_text(GTK_LABEL(self->backup_status_label_), pct); + + char size_info[128]; + snprintf(size_info, sizeof(size_info), "%s / %s", + format_size(p.bytes_transferred).c_str(), + format_size(p.estimated_total_size).c_str()); + gtk_label_set_text(GTK_LABEL(self->backup_size_label_), size_info); } + + if (p.estimated_remaining > 0 && p.estimated_remaining < 86400 * 365) { + std::string time_str = self->rsync_.format_time_remaining(p.estimated_remaining); + gtk_label_set_text(GTK_LABEL(self->backup_time_label_), time_str.c_str()); + } else if (p.elapsed_seconds > 0) { + char elapsed_buf[64]; + int mins = (int)(p.elapsed_seconds / 60); + int secs = (int)(p.elapsed_seconds) % 60; + snprintf(elapsed_buf, sizeof(elapsed_buf), "Elapsed: %dm %02ds", mins, secs); + gtk_label_set_text(GTK_LABEL(self->backup_time_label_), elapsed_buf); + } + if (!p.log_line.empty()) { const char* tag = nullptr; if (p.log_line.find("[error]") != std::string::npos) tag = "error"; @@ -65,6 +84,7 @@ MainWindow::MainWindow(Config& config) gtk_label_set_text(GTK_LABEL(self->backup_cmd_label_), cmd_display.c_str()); } else if (p.log_line.find("[restore") != std::string::npos) tag = "info"; + else if (p.log_line.find("[info]") != std::string::npos) tag = "info"; self->append_backup_line(p.log_line, tag); } if (p.finished) { @@ -682,6 +702,8 @@ void MainWindow::open_backup_window(const std::string& description) { gtk_text_buffer_set_text(buf, "", -1); gtk_label_set_text(GTK_LABEL(backup_cmd_label_), ""); gtk_label_set_text(GTK_LABEL(backup_status_label_), "Running..."); + gtk_label_set_text(GTK_LABEL(backup_time_label_), ""); + gtk_label_set_text(GTK_LABEL(backup_size_label_), ""); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(backup_progress_), 0.0); gtk_button_set_label(GTK_BUTTON(backup_cancel_btn_), "Cancel"); gtk_widget_show_all(backup_win_); @@ -768,6 +790,22 @@ void MainWindow::open_backup_window(const std::string& description) { gtk_box_pack_start(GTK_BOX(prog_box), backup_status_label_, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), prog_box, FALSE, FALSE, 0); + /* Time and size info */ + GtkWidget* info_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 16); + backup_time_label_ = gtk_label_new(""); + gtk_widget_set_halign(backup_time_label_, GTK_ALIGN_START); + PangoAttrList* info_attrs = pango_attr_list_new(); + pango_attr_list_insert(info_attrs, pango_attr_family_new("monospace")); + gtk_label_set_attributes(GTK_LABEL(backup_time_label_), info_attrs); + gtk_box_pack_start(GTK_BOX(info_box), backup_time_label_, TRUE, TRUE, 0); + + backup_size_label_ = gtk_label_new(""); + gtk_widget_set_halign(backup_size_label_, GTK_ALIGN_END); + gtk_label_set_attributes(GTK_LABEL(backup_size_label_), info_attrs); + pango_attr_list_unref(info_attrs); + gtk_box_pack_start(GTK_BOX(info_box), backup_size_label_, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), info_box, FALSE, FALSE, 0); + /* Buttons */ GtkWidget* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(btn_box, GTK_ALIGN_END); @@ -809,6 +847,8 @@ void MainWindow::close_backup_window() { backup_cmd_label_ = nullptr; backup_progress_ = nullptr; backup_status_label_ = nullptr; + backup_time_label_ = nullptr; + backup_size_label_ = nullptr; backup_cancel_btn_ = nullptr; } } diff --git a/src/main_window.h b/src/main_window.h index 8ce2928..a306c39 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -90,6 +90,8 @@ private: GtkWidget* backup_output_view_ = nullptr; GtkWidget* backup_progress_ = nullptr; GtkWidget* backup_status_label_ = nullptr; + GtkWidget* backup_time_label_ = nullptr; + GtkWidget* backup_size_label_ = nullptr; GtkWidget* backup_cancel_btn_ = nullptr; GtkWidget* backup_close_btn_ = nullptr; diff --git a/src/rsync_manager.cpp b/src/rsync_manager.cpp index 03e6956..4c0e6ab 100644 --- a/src/rsync_manager.cpp +++ b/src/rsync_manager.cpp @@ -6,9 +6,43 @@ #include #include #include +#include RsyncManager::RsyncManager(Config& config) : config_(config) {} +int64_t RsyncManager::estimate_backup_size(const std::string& source) { + std::string cmd = "du -sb \"" + source + "\" 2>/dev/null | awk '{print $1}'"; + + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) return 0; + + char buf[64]; + int64_t size = 0; + if (fgets(buf, sizeof(buf), pipe)) { + size = std::stoll(std::string(buf)); + } + pclose(pipe); + return size; +} + +std::string RsyncManager::format_time_remaining(double seconds) { + if (seconds < 0 || seconds > 86400 * 365) return "calculating..."; + + int hrs = (int)(seconds / 3600); + int mins = (int)((seconds - hrs * 3600) / 60); + int secs = (int)(seconds - hrs * 3600 - mins * 60); + + char buf[32]; + if (hrs > 0) { + snprintf(buf, sizeof(buf), "%dh %02dm %02ds remaining", hrs, mins, secs); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), "%dm %02ds remaining", mins, secs); + } else { + snprintf(buf, sizeof(buf), "%ds remaining", secs); + } + return buf; +} + std::vector RsyncManager::build_exclude_args() { std::vector args; for (const auto& e : config_.settings().exclude_patterns) { @@ -24,12 +58,21 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, if (running_) return false; cancel_requested_ = false; running_ = true; + start_time_ = std::chrono::steady_clock::now(); { std::lock_guard lock(log_mutex_); log_buffer_.clear(); } + if (on_progress) { + RsyncProgress p; + p.log_line = "[info] Estimating backup size..."; + on_progress(p); + } + + int64_t estimated_size = estimate_backup_size(source); + std::string target = dest + "/" + snapshot_name; std::string cmd = "rsync -aAX --info=progress2"; @@ -53,6 +96,7 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name, if (on_progress) { RsyncProgress p; p.log_line = "[cmd] " + cmd; + p.estimated_total_size = estimated_size; on_progress(p); } @@ -286,14 +330,36 @@ 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.]+)[kmgt]?/s)"); - std::regex re_file(R"(\s(\S+)$)"); + std::regex re_speed(R"(([\d.]+)\s*[KMGT]?B/s)"); + std::regex re_xfr(R"(\d+\s+files?)"); std::smatch m; if (std::regex_search(line, m, re_pct)) { - prog.bytes_transferred = std::stoll(m[1].str()); + int pct = std::stoi(m[1].str()); + if (prog.estimated_total_size > 0) { + prog.bytes_transferred = (int64_t)(prog.estimated_total_size * pct / 100); + } } + if (std::regex_search(line, m, re_speed)) { - prog.speed = (int)std::stod(m[1].str()); + std::string speed_str = m[1].str(); + double speed_val = std::stod(speed_str); + + if (line.find("GB/s") != std::string::npos || line.find("GiB/s") != std::string::npos) + prog.speed = (int)(speed_val * 1024 * 1024 * 1024); + else if (line.find("MB/s") != std::string::npos || line.find("MiB/s") != std::string::npos) + prog.speed = (int)(speed_val * 1024 * 1024); + else if (line.find("KB/s") != std::string::npos || line.find("KiB/s") != std::string::npos) + prog.speed = (int)(speed_val * 1024); + else + prog.speed = (int)speed_val; + } + + 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; } } diff --git a/src/rsync_manager.h b/src/rsync_manager.h index 1b58030..11355d6 100644 --- a/src/rsync_manager.h +++ b/src/rsync_manager.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "config.h" struct RsyncProgress { @@ -13,6 +14,9 @@ struct RsyncProgress { int64_t bytes_transferred = 0; int64_t bytes_total = 0; int speed = 0; + int64_t estimated_total_size = 0; + double elapsed_seconds = 0; + double estimated_remaining = 0; std::string current_file; std::string log_line; bool finished = false; @@ -43,6 +47,9 @@ public: std::function on_progress; + int64_t estimate_backup_size(const std::string& source); + std::string format_time_remaining(double seconds); + private: Config& config_; std::atomic running_{false}; @@ -50,6 +57,7 @@ private: std::thread worker_; std::mutex log_mutex_; std::vector log_buffer_; + std::chrono::steady_clock::time_point start_time_; std::vector build_exclude_args(); bool run_rsync(const std::vector& args);