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 */
|
/* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user