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:
+42
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+70
-4
@@ -6,9 +6,43 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <poll.h>
|
||||
#include <chrono>
|
||||
|
||||
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> 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<std::mutex> 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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#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<void(const RsyncProgress&)> on_progress;
|
||||
|
||||
int64_t estimate_backup_size(const std::string& source);
|
||||
std::string format_time_remaining(double seconds);
|
||||
|
||||
private:
|
||||
Config& config_;
|
||||
std::atomic<bool> running_{false};
|
||||
@@ -50,6 +57,7 @@ private:
|
||||
std::thread worker_;
|
||||
std::mutex log_mutex_;
|
||||
std::vector<std::string> log_buffer_;
|
||||
std::chrono::steady_clock::time_point start_time_;
|
||||
|
||||
std::vector<std::string> build_exclude_args();
|
||||
bool run_rsync(const std::vector<std::string>& args);
|
||||
|
||||
Reference in New Issue
Block a user