feat: add backup progress estimation with time remaining

- Add estimate_backup_size() using du -sb before rsync starts
- Parse transfer speed from rsync output (KB/s, MB/s, GB/s)
- Calculate remaining time based on speed and remaining bytes
- Show estimated time remaining and size progress in UI
- Add format_time_remaining() for human-readable time display
- New UI elements: backup_time_label_ and backup_size_label_
This commit is contained in:
x3
2026-09-07 19:29:29 +00:00
parent 33044731c8
commit f95a634655
4 changed files with 122 additions and 6 deletions
+42 -2
View File
@@ -46,13 +46,32 @@ MainWindow::MainWindow(Config& config)
/* Update backup progress window */ /* Update backup progress window */
if (self->backup_win_) { if (self->backup_win_) {
if (p.bytes_total > 0) { if (p.estimated_total_size > 0) {
double frac = (double)p.bytes_transferred / p.bytes_total; 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); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->backup_progress_), frac);
char pct[32]; char pct[32];
snprintf(pct, sizeof(pct), "%.0f%%", frac * 100); snprintf(pct, sizeof(pct), "%.0f%%", frac * 100);
gtk_label_set_text(GTK_LABEL(self->backup_status_label_), pct); 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()) { if (!p.log_line.empty()) {
const char* tag = nullptr; const char* tag = nullptr;
if (p.log_line.find("[error]") != std::string::npos) tag = "error"; 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()); 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("[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); self->append_backup_line(p.log_line, tag);
} }
if (p.finished) { if (p.finished) {
@@ -682,6 +702,8 @@ void MainWindow::open_backup_window(const std::string& description) {
gtk_text_buffer_set_text(buf, "", -1); gtk_text_buffer_set_text(buf, "", -1);
gtk_label_set_text(GTK_LABEL(backup_cmd_label_), ""); 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_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_progress_bar_set_fraction(GTK_PROGRESS_BAR(backup_progress_), 0.0);
gtk_button_set_label(GTK_BUTTON(backup_cancel_btn_), "Cancel"); gtk_button_set_label(GTK_BUTTON(backup_cancel_btn_), "Cancel");
gtk_widget_show_all(backup_win_); 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(prog_box), backup_status_label_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), prog_box, 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 */ /* Buttons */
GtkWidget* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); GtkWidget* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(btn_box, GTK_ALIGN_END); gtk_widget_set_halign(btn_box, GTK_ALIGN_END);
@@ -809,6 +847,8 @@ void MainWindow::close_backup_window() {
backup_cmd_label_ = nullptr; backup_cmd_label_ = nullptr;
backup_progress_ = nullptr; backup_progress_ = nullptr;
backup_status_label_ = nullptr; backup_status_label_ = nullptr;
backup_time_label_ = nullptr;
backup_size_label_ = nullptr;
backup_cancel_btn_ = nullptr; backup_cancel_btn_ = nullptr;
} }
} }
+2
View File
@@ -90,6 +90,8 @@ private:
GtkWidget* backup_output_view_ = nullptr; GtkWidget* backup_output_view_ = nullptr;
GtkWidget* backup_progress_ = nullptr; GtkWidget* backup_progress_ = nullptr;
GtkWidget* backup_status_label_ = nullptr; GtkWidget* backup_status_label_ = nullptr;
GtkWidget* backup_time_label_ = nullptr;
GtkWidget* backup_size_label_ = nullptr;
GtkWidget* backup_cancel_btn_ = nullptr; GtkWidget* backup_cancel_btn_ = nullptr;
GtkWidget* backup_close_btn_ = nullptr; GtkWidget* backup_close_btn_ = nullptr;
+70 -4
View File
@@ -6,9 +6,43 @@
#include <unistd.h> #include <unistd.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <poll.h> #include <poll.h>
#include <chrono>
RsyncManager::RsyncManager(Config& config) : config_(config) {} 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<std::string> RsyncManager::build_exclude_args() { std::vector<std::string> RsyncManager::build_exclude_args() {
std::vector<std::string> args; std::vector<std::string> args;
for (const auto& e : config_.settings().exclude_patterns) { 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; if (running_) return false;
cancel_requested_ = false; cancel_requested_ = false;
running_ = true; running_ = true;
start_time_ = std::chrono::steady_clock::now();
{ {
std::lock_guard<std::mutex> lock(log_mutex_); std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.clear(); 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 target = dest + "/" + snapshot_name;
std::string cmd = "rsync -aAX --info=progress2"; std::string cmd = "rsync -aAX --info=progress2";
@@ -53,6 +96,7 @@ bool RsyncManager::create_snapshot(const std::string& snapshot_name,
if (on_progress) { if (on_progress) {
RsyncProgress p; RsyncProgress p;
p.log_line = "[cmd] " + cmd; p.log_line = "[cmd] " + cmd;
p.estimated_total_size = estimated_size;
on_progress(p); on_progress(p);
} }
@@ -286,14 +330,36 @@ void RsyncManager::cancel() {
void RsyncManager::parse_progress_line(const std::string& line, RsyncProgress& prog) { void RsyncManager::parse_progress_line(const std::string& line, RsyncProgress& prog) {
std::regex re_pct(R"((\d+)%)"); std::regex re_pct(R"((\d+)%)");
std::regex re_speed(R"(([\d.]+)[kmgt]?/s)"); std::regex re_speed(R"(([\d.]+)\s*[KMGT]?B/s)");
std::regex re_file(R"(\s(\S+)$)"); std::regex re_xfr(R"(\d+\s+files?)");
std::smatch m; std::smatch m;
if (std::regex_search(line, m, re_pct)) { 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)) { 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<double>(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;
} }
} }
+8
View File
@@ -5,6 +5,7 @@
#include <atomic> #include <atomic>
#include <thread> #include <thread>
#include <mutex> #include <mutex>
#include <chrono>
#include "config.h" #include "config.h"
struct RsyncProgress { struct RsyncProgress {
@@ -13,6 +14,9 @@ struct RsyncProgress {
int64_t bytes_transferred = 0; int64_t bytes_transferred = 0;
int64_t bytes_total = 0; int64_t bytes_total = 0;
int speed = 0; int speed = 0;
int64_t estimated_total_size = 0;
double elapsed_seconds = 0;
double estimated_remaining = 0;
std::string current_file; std::string current_file;
std::string log_line; std::string log_line;
bool finished = false; bool finished = false;
@@ -43,6 +47,9 @@ public:
std::function<void(const RsyncProgress&)> on_progress; std::function<void(const RsyncProgress&)> on_progress;
int64_t estimate_backup_size(const std::string& source);
std::string format_time_remaining(double seconds);
private: private:
Config& config_; Config& config_;
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
@@ -50,6 +57,7 @@ private:
std::thread worker_; std::thread worker_;
std::mutex log_mutex_; std::mutex log_mutex_;
std::vector<std::string> log_buffer_; std::vector<std::string> log_buffer_;
std::chrono::steady_clock::time_point start_time_;
std::vector<std::string> build_exclude_args(); std::vector<std::string> build_exclude_args();
bool run_rsync(const std::vector<std::string>& args); bool run_rsync(const std::vector<std::string>& args);