feat: x3-backup v1.0.0 initial release
GTK3 backup/snapshot tool using rsync backend. Features: - rsync-based snapshots with hardlink delta for space efficiency - Snapshot sidebar with date-sorted list - Full file browser with permissions display (name, size, perms, mtime) - Right-click context menu: Restore file/dir with sudo prompt - System tray icon: backup status, context menu (show/backup/settings/quit) - Menu system: File, Backup, System, Help - Settings dialog: backup destination, source path, retention, exclusions - Cronjob management: install/remove scheduled backups - Live Log tab: real-time rsync output during backup - History Log tab: full log file viewer with color-coded entries - System Backup mode: full root filesystem backup - System Restore mode: restore entire system from snapshot - CLI mode: --backup, --system-backup for cron/headless use - /mnt always excluded from backups - Preserves permissions, ACLs, xattrs (-aAX flags) - Log file: ~/.config/x3-backup/x3-backup.log - Config: ~/.config/x3-backup/settings.json - uninstall.sh for clean removal
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Autotools generated
|
||||||
|
/configure
|
||||||
|
/Makefile
|
||||||
|
/Makefile.in
|
||||||
|
/src/Makefile
|
||||||
|
/src/Makefile.in
|
||||||
|
/build-aux/
|
||||||
|
autom4te.cache/
|
||||||
|
config.log
|
||||||
|
config.status
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
|
x3-backup
|
||||||
|
.deps/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
build/
|
||||||
|
obj/
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
SUBDIRS = src
|
||||||
|
|
||||||
|
EXTRA_DIST = desktop/x3-backup.desktop icons
|
||||||
|
|
||||||
|
install-data-local:
|
||||||
|
$(MKDIR_P) $(DESTDIR)$(datadir)/applications
|
||||||
|
$(INSTALL_DATA) desktop/x3-backup.desktop $(DESTDIR)$(datadir)/applications/x3-backup.desktop
|
||||||
|
$(MKDIR_P) $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps
|
||||||
|
$(INSTALL_DATA) icons/x3-backup.png $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/x3-backup.png
|
||||||
|
|
||||||
|
uninstall-local:
|
||||||
|
rm -f $(DESTDIR)$(datadir)/applications/x3-backup.desktop
|
||||||
|
rm -f $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/x3-backup.png
|
||||||
|
|
||||||
|
CLEANFILES = *~
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
AC_INIT([x3-backup], [1.0.0], [x3@sysdaemon.xyz])
|
||||||
|
AC_CONFIG_SRCDIR([src/main.cpp])
|
||||||
|
AC_CONFIG_AUX_DIR([build-aux])
|
||||||
|
AM_INIT_AUTOMAKE([foreign -Wall -Werror subdir-objects])
|
||||||
|
|
||||||
|
AC_PROG_CXX
|
||||||
|
AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
|
||||||
|
|
||||||
|
PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.24])
|
||||||
|
PKG_CHECK_MODULES([JSON_GLIB], [json-glib-1.0])
|
||||||
|
|
||||||
|
AC_CHECK_PROG([RSYNC], [rsync], [yes], [no])
|
||||||
|
AS_IF([test "x$RSYNC" = "xno"], [AC_MSG_ERROR([rsync is required])])
|
||||||
|
|
||||||
|
AC_CHECK_PROG([CRONTAB], [crontab], [yes], [no])
|
||||||
|
AC_CHECK_PROG([PKEXEC], [pkexec], [yes], [no])
|
||||||
|
|
||||||
|
AC_CONFIG_FILES([Makefile src/Makefile])
|
||||||
|
AC_OUTPUT
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=x3-Backup
|
||||||
|
Comment=GTK3 Backup/Snapshot Tool using rsync
|
||||||
|
Exec=x3-backup
|
||||||
|
Icon=x3-backup
|
||||||
|
Terminal=false
|
||||||
|
Categories=System;Utility;
|
||||||
|
Keywords=backup;snapshot;rsync;restore;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
bin_PROGRAMS = x3-backup
|
||||||
|
|
||||||
|
x3_backup_SOURCES = \
|
||||||
|
main.cpp \
|
||||||
|
config.cpp \
|
||||||
|
rsync_manager.cpp \
|
||||||
|
snapshot_manager.cpp \
|
||||||
|
main_window.cpp \
|
||||||
|
settings_dialog.cpp \
|
||||||
|
system_tray.cpp
|
||||||
|
|
||||||
|
x3_backup_CXXFLAGS = \
|
||||||
|
-std=c++17 \
|
||||||
|
$(GTK_CFLAGS) \
|
||||||
|
$(JSON_GLIB_CFLAGS) \
|
||||||
|
-DLOCALEDIR=\"$(localedir)\" \
|
||||||
|
-DBINDIR=\"$(bindir)\"
|
||||||
|
|
||||||
|
x3_backup_LDADD = \
|
||||||
|
$(GTK_LIBS) \
|
||||||
|
$(JSON_GLIB_LIBS) \
|
||||||
|
-lstdc++fs
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
#include "config.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <json-glib/json-glib.h>
|
||||||
|
|
||||||
|
Config::Config() {
|
||||||
|
const char* home = getenv("HOME");
|
||||||
|
if (!home) home = ".";
|
||||||
|
config_dir_ = std::string(home) + "/.config/x3-backup";
|
||||||
|
ensure_dirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::ensure_dirs() {
|
||||||
|
std::string cmd = "mkdir -p \"" + config_dir_ + "\"";
|
||||||
|
system(cmd.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::load() {
|
||||||
|
std::string path = config_file();
|
||||||
|
FILE* f = fopen(path.c_str(), "r");
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long len = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
char* buf = new char[len + 1];
|
||||||
|
fread(buf, 1, len, f);
|
||||||
|
buf[len] = 0;
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
JsonParser* parser = json_parser_new();
|
||||||
|
GError* err = nullptr;
|
||||||
|
if (!json_parser_load_from_data(parser, buf, len, &err)) {
|
||||||
|
g_warning("Config parse error: %s", err->message);
|
||||||
|
g_error_free(err);
|
||||||
|
g_object_unref(parser);
|
||||||
|
delete[] buf;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete[] buf;
|
||||||
|
|
||||||
|
JsonObject* root = json_node_get_object(json_parser_get_root(parser));
|
||||||
|
|
||||||
|
if (json_object_has_member(root, "backup_dest"))
|
||||||
|
settings_.backup_dest = json_object_get_string_member(root, "backup_dest");
|
||||||
|
if (json_object_has_member(root, "source_path"))
|
||||||
|
settings_.source_path = json_object_get_string_member(root, "source_path");
|
||||||
|
if (json_object_has_member(root, "preserve_permissions"))
|
||||||
|
settings_.preserve_permissions = json_object_get_boolean_member(root, "preserve_permissions");
|
||||||
|
if (json_object_has_member(root, "preserve_acls"))
|
||||||
|
settings_.preserve_acls = json_object_get_boolean_member(root, "preserve_acls");
|
||||||
|
if (json_object_has_member(root, "preserve_xattrs"))
|
||||||
|
settings_.preserve_xattrs = json_object_get_boolean_member(root, "preserve_xattrs");
|
||||||
|
if (json_object_has_member(root, "delete_removed"))
|
||||||
|
settings_.delete_removed = json_object_get_boolean_member(root, "delete_removed");
|
||||||
|
if (json_object_has_member(root, "retention_days"))
|
||||||
|
settings_.retention_days = (int)json_object_get_int_member(root, "retention_days");
|
||||||
|
if (json_object_has_member(root, "retention_count"))
|
||||||
|
settings_.retention_count = (int)json_object_get_int_member(root, "retention_count");
|
||||||
|
if (json_object_has_member(root, "cron_enabled"))
|
||||||
|
settings_.cron_enabled = json_object_get_boolean_member(root, "cron_enabled");
|
||||||
|
if (json_object_has_member(root, "cron_schedule"))
|
||||||
|
settings_.cron_schedule = json_object_get_string_member(root, "cron_schedule");
|
||||||
|
if (json_object_has_member(root, "cron_label"))
|
||||||
|
settings_.cron_label = json_object_get_string_member(root, "cron_label");
|
||||||
|
|
||||||
|
if (json_object_has_member(root, "exclude_patterns")) {
|
||||||
|
JsonArray* arr = json_object_get_array_member(root, "exclude_patterns");
|
||||||
|
settings_.exclude_patterns.clear();
|
||||||
|
for (guint i = 0; i < json_array_get_length(arr); i++) {
|
||||||
|
settings_.exclude_patterns.push_back(json_array_get_string_element(arr, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_object_unref(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::save() {
|
||||||
|
JsonBuilder* builder = json_builder_new();
|
||||||
|
json_builder_begin_object(builder);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "backup_dest");
|
||||||
|
json_builder_add_string_value(builder, settings_.backup_dest.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "source_path");
|
||||||
|
json_builder_add_string_value(builder, settings_.source_path.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "preserve_permissions");
|
||||||
|
json_builder_add_boolean_value(builder, settings_.preserve_permissions);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "preserve_acls");
|
||||||
|
json_builder_add_boolean_value(builder, settings_.preserve_acls);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "preserve_xattrs");
|
||||||
|
json_builder_add_boolean_value(builder, settings_.preserve_xattrs);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "delete_removed");
|
||||||
|
json_builder_add_boolean_value(builder, settings_.delete_removed);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "retention_days");
|
||||||
|
json_builder_add_int_value(builder, settings_.retention_days);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "retention_count");
|
||||||
|
json_builder_add_int_value(builder, settings_.retention_count);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "cron_enabled");
|
||||||
|
json_builder_add_boolean_value(builder, settings_.cron_enabled);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "cron_schedule");
|
||||||
|
json_builder_add_string_value(builder, settings_.cron_schedule.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "cron_label");
|
||||||
|
json_builder_add_string_value(builder, settings_.cron_label.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "exclude_patterns");
|
||||||
|
json_builder_begin_array(builder);
|
||||||
|
for (const auto& e : settings_.exclude_patterns) {
|
||||||
|
json_builder_add_string_value(builder, e.c_str());
|
||||||
|
}
|
||||||
|
json_builder_end_array(builder);
|
||||||
|
|
||||||
|
json_builder_end_object(builder);
|
||||||
|
|
||||||
|
JsonGenerator* gen = json_generator_new();
|
||||||
|
json_generator_set_root(gen, json_builder_get_root(builder));
|
||||||
|
json_generator_set_pretty(gen, TRUE);
|
||||||
|
json_generator_set_indent(gen, 2);
|
||||||
|
|
||||||
|
std::string path = config_file();
|
||||||
|
json_generator_to_file(gen, path.c_str(), nullptr);
|
||||||
|
|
||||||
|
g_object_unref(gen);
|
||||||
|
g_object_unref(builder);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
struct BackupSettings {
|
||||||
|
std::string backup_dest = "/mnt/backups";
|
||||||
|
std::string source_path = "/";
|
||||||
|
std::vector<std::string> exclude_patterns = {"/mnt", "/proc", "/sys", "/dev", "/run", "/tmp", "/lost+found"};
|
||||||
|
bool preserve_permissions = true;
|
||||||
|
bool preserve_acls = true;
|
||||||
|
bool preserve_xattrs = true;
|
||||||
|
bool delete_removed = true;
|
||||||
|
int retention_days = 30;
|
||||||
|
int retention_count = 10;
|
||||||
|
bool cron_enabled = false;
|
||||||
|
std::string cron_schedule = "0 2 * * *";
|
||||||
|
std::string cron_label = "x3-backup-auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
public:
|
||||||
|
Config();
|
||||||
|
void load();
|
||||||
|
void save();
|
||||||
|
|
||||||
|
BackupSettings& settings() { return settings_; }
|
||||||
|
const BackupSettings& settings() const { return settings_; }
|
||||||
|
|
||||||
|
std::string config_dir() const { return config_dir_; }
|
||||||
|
std::string config_file() const { return config_dir_ + "/settings.json"; }
|
||||||
|
std::string snapshots_dir() const { return settings_.backup_dest + "/snapshots"; }
|
||||||
|
std::string metadata_dir() const { return settings_.backup_dest + "/metadata"; }
|
||||||
|
|
||||||
|
bool is_system_backup_mode() const { return system_backup_mode_; }
|
||||||
|
void set_system_backup_mode(bool v) { system_backup_mode_ = v; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
BackupSettings settings_;
|
||||||
|
std::string config_dir_;
|
||||||
|
bool system_backup_mode_ = false;
|
||||||
|
|
||||||
|
void ensure_dirs();
|
||||||
|
};
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <glib.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include "config.h"
|
||||||
|
#include "rsync_manager.h"
|
||||||
|
#include "snapshot_manager.h"
|
||||||
|
#include "main_window.h"
|
||||||
|
|
||||||
|
static FILE* log_file = nullptr;
|
||||||
|
static bool log_to_stdout = false;
|
||||||
|
|
||||||
|
static std::string get_log_path() {
|
||||||
|
const char* home = getenv("HOME");
|
||||||
|
if (!home) home = ".";
|
||||||
|
std::string dir = std::string(home) + "/.config/x3-backup";
|
||||||
|
std::string cmd = "mkdir -p \"" + dir + "\"";
|
||||||
|
(void)system(cmd.c_str());
|
||||||
|
return dir + "/x3-backup.log";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_handler(const gchar* domain, GLogLevelFlags level, const gchar* message, gpointer) {
|
||||||
|
const char* level_str = "INFO";
|
||||||
|
if (level & G_LOG_LEVEL_WARNING) level_str = "WARN";
|
||||||
|
if (level & G_LOG_LEVEL_CRITICAL) level_str = "CRIT";
|
||||||
|
if (level & G_LOG_LEVEL_ERROR) level_str = "ERROR";
|
||||||
|
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
char timebuf[32];
|
||||||
|
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", t);
|
||||||
|
|
||||||
|
char line[2048];
|
||||||
|
snprintf(line, sizeof(line), "[%s] [%s] %s: %s\n", timebuf, level_str, domain ? domain : "app", message);
|
||||||
|
|
||||||
|
if (log_to_stdout) {
|
||||||
|
fputs(line, stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
if (log_file) {
|
||||||
|
fputs(line, log_file);
|
||||||
|
fflush(log_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_logging(bool cli_mode) {
|
||||||
|
const char* env_log = g_getenv("X3_BACKUP_LOG");
|
||||||
|
if (env_log && strcmp(env_log, "1") == 0) {
|
||||||
|
log_to_stdout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cli_mode) {
|
||||||
|
log_to_stdout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string path = get_log_path();
|
||||||
|
log_file = fopen(path.c_str(), "a");
|
||||||
|
if (!log_file) {
|
||||||
|
g_warning("Cannot open log file: %s", path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
g_log_set_handler("x3-backup", (GLogLevelFlags)(G_LOG_LEVEL_MASK), log_handler, nullptr);
|
||||||
|
g_log_set_handler(nullptr, (GLogLevelFlags)(G_LOG_LEVEL_MASK), log_handler, nullptr);
|
||||||
|
|
||||||
|
g_message("=== x3-backup started ===");
|
||||||
|
g_message("Log file: %s", path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
gtk_init(&argc, &argv);
|
||||||
|
|
||||||
|
bool system_mode = false;
|
||||||
|
bool backup_now = false;
|
||||||
|
bool show_help = false;
|
||||||
|
bool cli_mode = false;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--system-backup") == 0) {
|
||||||
|
system_mode = true;
|
||||||
|
backup_now = true;
|
||||||
|
cli_mode = true;
|
||||||
|
} else if (strcmp(argv[i], "--backup") == 0) {
|
||||||
|
backup_now = true;
|
||||||
|
cli_mode = true;
|
||||||
|
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||||
|
show_help = true;
|
||||||
|
} else if (strcmp(argv[i], "--log") == 0) {
|
||||||
|
log_to_stdout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_logging(cli_mode);
|
||||||
|
|
||||||
|
if (show_help) {
|
||||||
|
g_print("x3-Backup - GTK3 Backup/Snapshot Tool\n\n");
|
||||||
|
g_print("Usage: x3-backup [OPTIONS]\n\n");
|
||||||
|
g_print("Options:\n");
|
||||||
|
g_print(" --backup Create a backup immediately and exit\n");
|
||||||
|
g_print(" --system-backup Create a full system backup and exit\n");
|
||||||
|
g_print(" --log Print log to stdout\n");
|
||||||
|
g_print(" --help, -h Show this help\n\n");
|
||||||
|
g_print("Without options, x3-backup starts in GUI mode with system tray.\n\n");
|
||||||
|
g_print("Log file: ~/.config/x3-backup/x3-backup.log\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Config config;
|
||||||
|
config.load();
|
||||||
|
|
||||||
|
if (backup_now) {
|
||||||
|
if (system_mode) {
|
||||||
|
config.set_system_backup_mode(true);
|
||||||
|
config.settings().source_path = "/";
|
||||||
|
g_message("System backup mode: backing up entire system");
|
||||||
|
}
|
||||||
|
|
||||||
|
RsyncManager rsync(config);
|
||||||
|
SnapshotManager snapshots(config, rsync);
|
||||||
|
|
||||||
|
g_message("Starting backup...");
|
||||||
|
snapshots.cleanup_old_snapshots();
|
||||||
|
std::string ts = snapshots.create_snapshot(system_mode ? "System backup" : "Manual backup");
|
||||||
|
if (!ts.empty()) {
|
||||||
|
g_message("Backup completed: %s", ts.c_str());
|
||||||
|
} else {
|
||||||
|
g_warning("Backup failed!");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow window(config);
|
||||||
|
window.run();
|
||||||
|
|
||||||
|
g_message("=== x3-backup exiting ===");
|
||||||
|
if (log_file) fclose(log_file);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,994 @@
|
|||||||
|
#include "main_window.h"
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <grp.h>
|
||||||
|
|
||||||
|
static std::string format_size(int64_t bytes) {
|
||||||
|
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
||||||
|
int u = 0;
|
||||||
|
double size = bytes;
|
||||||
|
while (size >= 1024 && u < 4) { size /= 1024; u++; }
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "%.1f %s", size, units[u]);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string mode_string(mode_t mode) {
|
||||||
|
char buf[11];
|
||||||
|
buf[0] = S_ISDIR(mode) ? 'd' : '-';
|
||||||
|
buf[1] = (mode & S_IRUSR) ? 'r' : '-';
|
||||||
|
buf[2] = (mode & S_IWUSR) ? 'w' : '-';
|
||||||
|
buf[3] = (mode & S_IXUSR) ? 'x' : '-';
|
||||||
|
buf[4] = (mode & S_IRGRP) ? 'r' : '-';
|
||||||
|
buf[5] = (mode & S_IWGRP) ? 'w' : '-';
|
||||||
|
buf[6] = (mode & S_IXGRP) ? 'x' : '-';
|
||||||
|
buf[7] = (mode & S_IROTH) ? 'r' : '-';
|
||||||
|
buf[8] = (mode & S_IWOTH) ? 'w' : '-';
|
||||||
|
buf[9] = (mode & S_IXOTH) ? 'x' : '-';
|
||||||
|
buf[10] = 0;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow::MainWindow(Config& config)
|
||||||
|
: config_(config), rsync_(config), snapshots_(config, rsync_) {
|
||||||
|
|
||||||
|
rsync_.on_progress = [this](const RsyncProgress& p) {
|
||||||
|
struct Ctx { MainWindow* self; RsyncProgress prog; };
|
||||||
|
Ctx* ctx = new Ctx{this, p};
|
||||||
|
g_idle_add([](gpointer data) -> gboolean {
|
||||||
|
auto* ctx = static_cast<Ctx*>(data);
|
||||||
|
auto* self = ctx->self;
|
||||||
|
const RsyncProgress& p = ctx->prog;
|
||||||
|
if (self->progress_bar_) {
|
||||||
|
if (p.bytes_total > 0) {
|
||||||
|
double frac = (double)p.bytes_transferred / p.bytes_total;
|
||||||
|
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->progress_bar_), frac);
|
||||||
|
}
|
||||||
|
if (!p.current_file.empty()) {
|
||||||
|
std::string label = "Syncing: " + p.current_file;
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->progress_label_), label.c_str());
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->live_log_label_), label.c_str());
|
||||||
|
}
|
||||||
|
if (!p.log_line.empty()) {
|
||||||
|
const char* tag = nullptr;
|
||||||
|
if (p.log_line.find("[error]") != std::string::npos) tag = "error";
|
||||||
|
else if (p.log_line.find("[done]") != std::string::npos) tag = "success";
|
||||||
|
else if (p.log_line.find("[cmd]") != std::string::npos) tag = "info";
|
||||||
|
else if (p.log_line.find("[restore") != std::string::npos) tag = "info";
|
||||||
|
self->append_live_log(p.log_line, tag);
|
||||||
|
}
|
||||||
|
if (p.finished) {
|
||||||
|
if (self->progress_window_) {
|
||||||
|
gtk_widget_destroy(self->progress_window_);
|
||||||
|
self->progress_window_ = nullptr;
|
||||||
|
}
|
||||||
|
self->refresh_snapshot_list();
|
||||||
|
if (p.error) {
|
||||||
|
self->append_live_log("[ERROR] " + p.error_message, "error");
|
||||||
|
self->tray_.show_notification("x3-Backup", p.error_message, true);
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup failed");
|
||||||
|
} else {
|
||||||
|
self->append_live_log("[SUCCESS] Backup completed", "success");
|
||||||
|
self->tray_.show_notification("x3-Backup", "Backup completed successfully");
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup completed");
|
||||||
|
}
|
||||||
|
self->tray_.set_status("x3-Backup - Idle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete ctx;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
tray_.on_show = [this]() { show(); };
|
||||||
|
tray_.on_quit = [this]() { gtk_main_quit(); };
|
||||||
|
tray_.on_quick_backup = [this]() { start_quick_backup(); };
|
||||||
|
tray_.on_settings = [this]() {
|
||||||
|
SettingsDialog dlg(GTK_WINDOW(window_), config_);
|
||||||
|
dlg.run();
|
||||||
|
};
|
||||||
|
|
||||||
|
build_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow::~MainWindow() {}
|
||||||
|
|
||||||
|
void MainWindow::build_ui() {
|
||||||
|
window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||||
|
gtk_window_set_title(GTK_WINDOW(window_), "x3-Backup");
|
||||||
|
gtk_window_set_default_size(GTK_WINDOW(window_), 1100, 700);
|
||||||
|
g_signal_connect(window_, "delete-event", G_CALLBACK(on_tray_close), this);
|
||||||
|
|
||||||
|
const char* icon_paths[] = {
|
||||||
|
"icons/x3-backup.png",
|
||||||
|
"/usr/share/icons/hicolor/256x256/apps/x3-backup.png",
|
||||||
|
nullptr
|
||||||
|
};
|
||||||
|
for (int i = 0; icon_paths[i]; ++i) {
|
||||||
|
if (g_file_test(icon_paths[i], G_FILE_TEST_EXISTS)) {
|
||||||
|
gtk_window_set_icon_from_file(GTK_WINDOW(window_), icon_paths[i], nullptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_container_add(GTK_CONTAINER(window_), vbox);
|
||||||
|
|
||||||
|
build_menu();
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), toolbar_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
build_toolbar();
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), toolbar_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), paned, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
/* Left: snapshot list */
|
||||||
|
GtkWidget* left_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_widget_set_size_request(left_box, 300, -1);
|
||||||
|
gtk_paned_pack1(GTK_PANED(paned), left_box, FALSE, FALSE);
|
||||||
|
|
||||||
|
GtkWidget* left_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
gtk_widget_set_margin_start(left_header, 8);
|
||||||
|
gtk_widget_set_margin_top(left_header, 6);
|
||||||
|
GtkWidget* left_title = gtk_label_new(NULL);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(left_title), "<b>Snapshots</b>");
|
||||||
|
gtk_widget_set_halign(left_title, GTK_ALIGN_START);
|
||||||
|
gtk_box_pack_start(GTK_BOX(left_header), left_title, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(left_box), left_header, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(left_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* snap_scroll = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(snap_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
snapshot_listbox_ = gtk_list_box_new();
|
||||||
|
gtk_list_box_set_selection_mode(GTK_LIST_BOX(snapshot_listbox_), GTK_SELECTION_SINGLE);
|
||||||
|
g_signal_connect(snapshot_listbox_, "row-selected", G_CALLBACK(on_snapshot_selected), this);
|
||||||
|
gtk_container_add(GTK_CONTAINER(snap_scroll), snapshot_listbox_);
|
||||||
|
gtk_box_pack_start(GTK_BOX(left_box), snap_scroll, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
refresh_snapshot_list();
|
||||||
|
|
||||||
|
/* Right: notebook with file browser + log tabs */
|
||||||
|
GtkWidget* right_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_paned_pack2(GTK_PANED(paned), right_box, TRUE, TRUE);
|
||||||
|
|
||||||
|
notebook_ = gtk_notebook_new();
|
||||||
|
gtk_box_pack_start(GTK_BOX(right_box), notebook_, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
/* ── Tab 1: File Browser ── */
|
||||||
|
GtkWidget* browser_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
|
GtkWidget* nav_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
gtk_widget_set_margin_start(nav_box, 4);
|
||||||
|
gtk_widget_set_margin_top(nav_box, 4);
|
||||||
|
gtk_widget_set_margin_bottom(nav_box, 4);
|
||||||
|
GtkWidget* up_btn = gtk_button_new_from_icon_name("go-up", GTK_ICON_SIZE_MENU);
|
||||||
|
g_signal_connect(up_btn, "clicked", G_CALLBACK(on_navigate_up), this);
|
||||||
|
gtk_box_pack_start(GTK_BOX(nav_box), up_btn, FALSE, FALSE, 0);
|
||||||
|
path_entry_ = gtk_entry_new();
|
||||||
|
gtk_editable_set_editable(GTK_EDITABLE(path_entry_), FALSE);
|
||||||
|
gtk_widget_set_hexpand(path_entry_, TRUE);
|
||||||
|
gtk_box_pack_start(GTK_BOX(nav_box), path_entry_, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(browser_box), nav_box, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
nav_label_ = gtk_label_new("Select a snapshot to browse");
|
||||||
|
gtk_widget_set_halign(nav_label_, GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_margin_start(nav_label_, 8);
|
||||||
|
gtk_box_pack_start(GTK_BOX(browser_box), nav_label_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
/* File tree */
|
||||||
|
file_store_ = gtk_list_store_new(5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
||||||
|
file_tree_ = gtk_tree_view_new_with_model(GTK_TREE_MODEL(file_store_));
|
||||||
|
g_object_unref(file_store_);
|
||||||
|
|
||||||
|
GtkCellRenderer* name_renderer = gtk_cell_renderer_text_new();
|
||||||
|
GtkTreeViewColumn* name_col = gtk_tree_view_column_new_with_attributes("Name", name_renderer, "text", col_name, nullptr);
|
||||||
|
gtk_tree_view_column_set_sort_column_id(name_col, col_name);
|
||||||
|
gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), name_col);
|
||||||
|
|
||||||
|
GtkCellRenderer* size_renderer = gtk_cell_renderer_text_new();
|
||||||
|
GtkTreeViewColumn* size_col = gtk_tree_view_column_new_with_attributes("Size", size_renderer, "text", col_size, nullptr);
|
||||||
|
gtk_tree_view_column_set_sort_column_id(size_col, col_size);
|
||||||
|
gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), size_col);
|
||||||
|
|
||||||
|
GtkCellRenderer* perm_renderer = gtk_cell_renderer_text_new();
|
||||||
|
GtkTreeViewColumn* perm_col = gtk_tree_view_column_new_with_attributes("Permissions", perm_renderer, "text", col_permissions, nullptr);
|
||||||
|
gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), perm_col);
|
||||||
|
|
||||||
|
GtkCellRenderer* mtime_renderer = gtk_cell_renderer_text_new();
|
||||||
|
GtkTreeViewColumn* mtime_col = gtk_tree_view_column_new_with_attributes("Modified", mtime_renderer, "text", col_mtime, nullptr);
|
||||||
|
gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), mtime_col);
|
||||||
|
|
||||||
|
g_signal_connect(file_tree_, "row-activated", G_CALLBACK(on_file_row_activated), this);
|
||||||
|
g_signal_connect(file_tree_, "button-press-event", G_CALLBACK(on_file_button_press), this);
|
||||||
|
|
||||||
|
GtkWidget* tree_scroll = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(tree_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_container_add(GTK_CONTAINER(tree_scroll), file_tree_);
|
||||||
|
gtk_box_pack_start(GTK_BOX(browser_box), tree_scroll, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), browser_box, gtk_label_new("Files"));
|
||||||
|
|
||||||
|
/* ── Tab 2: Live Log ── */
|
||||||
|
GtkWidget* live_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
gtk_widget_set_margin_start(live_box, 4);
|
||||||
|
gtk_widget_set_margin_top(live_box, 4);
|
||||||
|
|
||||||
|
live_log_label_ = gtk_label_new("No backup running");
|
||||||
|
gtk_widget_set_halign(live_log_label_, GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_margin_start(live_log_label_, 4);
|
||||||
|
PangoAttrList* mono_attrs = pango_attr_list_new();
|
||||||
|
pango_attr_list_insert(mono_attrs, pango_attr_family_new("monospace"));
|
||||||
|
gtk_label_set_attributes(GTK_LABEL(live_log_label_), mono_attrs);
|
||||||
|
pango_attr_list_unref(mono_attrs);
|
||||||
|
gtk_box_pack_start(GTK_BOX(live_box), live_log_label_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* live_scroll = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(live_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
live_log_textview_ = gtk_text_view_new();
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(live_log_textview_), FALSE);
|
||||||
|
gtk_text_view_set_monospace(GTK_TEXT_VIEW(live_log_textview_), TRUE);
|
||||||
|
GtkTextBuffer* live_buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_));
|
||||||
|
gtk_text_buffer_create_tag(live_buf, "normal", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(live_buf, "error", "foreground", "#e74c3c", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(live_buf, "success", "foreground", "#2ecc71", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(live_buf, "info", "foreground", "#3498db", nullptr);
|
||||||
|
gtk_container_add(GTK_CONTAINER(live_scroll), live_log_textview_);
|
||||||
|
gtk_box_pack_start(GTK_BOX(live_box), live_scroll, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
GtkWidget* live_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
GtkWidget* clear_live_btn = gtk_button_new_with_label("Clear");
|
||||||
|
g_signal_connect(clear_live_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->live_log_textview_));
|
||||||
|
gtk_text_buffer_set_text(buf, "", -1);
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "No backup running");
|
||||||
|
}), this);
|
||||||
|
gtk_box_pack_start(GTK_BOX(live_toolbar), clear_live_btn, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(live_box), live_toolbar, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), live_box, gtk_label_new("Live Log"));
|
||||||
|
|
||||||
|
/* ── Tab 3: History Log ── */
|
||||||
|
GtkWidget* hist_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
gtk_widget_set_margin_start(hist_box, 4);
|
||||||
|
gtk_widget_set_margin_top(hist_box, 4);
|
||||||
|
|
||||||
|
GtkWidget* hist_label = gtk_label_new("x3-Backup Log History");
|
||||||
|
gtk_widget_set_halign(hist_label, GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_margin_start(hist_label, 4);
|
||||||
|
PangoAttrList* mono2 = pango_attr_list_new();
|
||||||
|
pango_attr_list_insert(mono2, pango_attr_family_new("monospace"));
|
||||||
|
gtk_label_set_attributes(GTK_LABEL(hist_label), mono2);
|
||||||
|
pango_attr_list_unref(mono2);
|
||||||
|
gtk_box_pack_start(GTK_BOX(hist_box), hist_label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
backlog_scrolled_ = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(backlog_scrolled_), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
backlog_textview_ = gtk_text_view_new();
|
||||||
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(backlog_textview_), FALSE);
|
||||||
|
gtk_text_view_set_monospace(GTK_TEXT_VIEW(backlog_textview_), TRUE);
|
||||||
|
gtk_container_add(GTK_CONTAINER(backlog_scrolled_), backlog_textview_);
|
||||||
|
gtk_box_pack_start(GTK_BOX(hist_box), backlog_scrolled_, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
GtkWidget* hist_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
GtkWidget* refresh_hist_btn = gtk_button_new_with_label("Refresh");
|
||||||
|
GtkWidget* clear_hist_btn = gtk_button_new_with_label("Clear");
|
||||||
|
g_signal_connect(refresh_hist_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
self->load_log_history();
|
||||||
|
}), this);
|
||||||
|
g_signal_connect(clear_hist_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->backlog_textview_));
|
||||||
|
gtk_text_buffer_set_text(buf, "", -1);
|
||||||
|
}), this);
|
||||||
|
gtk_box_pack_start(GTK_BOX(hist_toolbar), refresh_hist_btn, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(hist_toolbar), clear_hist_btn, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(hist_box), hist_toolbar, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), hist_box, gtk_label_new("History Log"));
|
||||||
|
|
||||||
|
log_path_ = std::string(g_get_home_dir()) + "/.config/x3-backup/x3-backup.log";
|
||||||
|
|
||||||
|
build_statusbar();
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), statusbar_, FALSE, FALSE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::build_menu() {
|
||||||
|
toolbar_ = gtk_menu_bar_new();
|
||||||
|
|
||||||
|
/* File menu */
|
||||||
|
GtkWidget* file_menu = gtk_menu_new();
|
||||||
|
GtkWidget* file_item = gtk_menu_item_new_with_label("File");
|
||||||
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_item), file_menu);
|
||||||
|
|
||||||
|
GtkWidget* new_backup = gtk_menu_item_new_with_label("New Backup");
|
||||||
|
g_signal_connect(new_backup, "activate", G_CALLBACK(on_new_backup), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), new_backup);
|
||||||
|
|
||||||
|
GtkWidget* sys_backup = gtk_menu_item_new_with_label("System Backup (full)");
|
||||||
|
g_signal_connect(sys_backup, "activate", G_CALLBACK(on_system_backup), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), sys_backup);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
|
||||||
|
|
||||||
|
GtkWidget* settings = gtk_menu_item_new_with_label("Settings");
|
||||||
|
g_signal_connect(settings, "activate", G_CALLBACK(on_settings), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), settings);
|
||||||
|
|
||||||
|
GtkWidget* show_log = gtk_menu_item_new_with_label("Show Log");
|
||||||
|
g_signal_connect(show_log, "activate", G_CALLBACK(on_show_log), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), show_log);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
|
||||||
|
|
||||||
|
GtkWidget* quit = gtk_menu_item_new_with_label("Quit");
|
||||||
|
g_signal_connect(quit, "activate", G_CALLBACK(on_quit), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), file_item);
|
||||||
|
|
||||||
|
/* Backup menu */
|
||||||
|
GtkWidget* backup_menu = gtk_menu_new();
|
||||||
|
GtkWidget* backup_item = gtk_menu_item_new_with_label("Backup");
|
||||||
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(backup_item), backup_menu);
|
||||||
|
|
||||||
|
GtkWidget* run_backup = gtk_menu_item_new_with_label("Run Backup Now");
|
||||||
|
g_signal_connect(run_backup, "activate", G_CALLBACK(on_backup), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), run_backup);
|
||||||
|
|
||||||
|
GtkWidget* restore = gtk_menu_item_new_with_label("Restore Selected");
|
||||||
|
g_signal_connect(restore, "activate", G_CALLBACK(on_restore), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), restore);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), gtk_separator_menu_item_new());
|
||||||
|
|
||||||
|
GtkWidget* del_snap = gtk_menu_item_new_with_label("Delete Snapshot");
|
||||||
|
g_signal_connect(del_snap, "activate", G_CALLBACK(on_delete_snapshot), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), del_snap);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), backup_item);
|
||||||
|
|
||||||
|
/* System menu */
|
||||||
|
GtkWidget* sys_menu = gtk_menu_new();
|
||||||
|
GtkWidget* sys_item = gtk_menu_item_new_with_label("System");
|
||||||
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(sys_item), sys_menu);
|
||||||
|
|
||||||
|
GtkWidget* sys_restore = gtk_menu_item_new_with_label("Restore Full System");
|
||||||
|
g_signal_connect(sys_restore, "activate", G_CALLBACK(on_system_restore), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), sys_restore);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), gtk_separator_menu_item_new());
|
||||||
|
|
||||||
|
GtkWidget* install_cron = gtk_menu_item_new_with_label("Install Cronjob");
|
||||||
|
g_signal_connect(install_cron, "activate", G_CALLBACK(on_install_cron), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), install_cron);
|
||||||
|
|
||||||
|
GtkWidget* remove_cron = gtk_menu_item_new_with_label("Remove Cronjob");
|
||||||
|
g_signal_connect(remove_cron, "activate", G_CALLBACK(on_remove_cron), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), remove_cron);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), sys_item);
|
||||||
|
|
||||||
|
/* Help menu */
|
||||||
|
GtkWidget* help_menu = gtk_menu_new();
|
||||||
|
GtkWidget* help_item = gtk_menu_item_new_with_label("Help");
|
||||||
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_item), help_menu);
|
||||||
|
|
||||||
|
GtkWidget* about = gtk_menu_item_new_with_label("About");
|
||||||
|
g_signal_connect(about, "activate", G_CALLBACK(on_about), this);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), about);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), help_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::build_toolbar() {
|
||||||
|
GtkWidget* tbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
gtk_widget_set_margin_start(tbar, 6);
|
||||||
|
gtk_widget_set_margin_end(tbar, 6);
|
||||||
|
gtk_widget_set_margin_top(tbar, 4);
|
||||||
|
gtk_widget_set_margin_bottom(tbar, 4);
|
||||||
|
|
||||||
|
auto mkbtn = [](const char* label, GCallback cb, gpointer data) -> GtkWidget* {
|
||||||
|
GtkWidget* b = gtk_button_new_with_label(label);
|
||||||
|
g_signal_connect(b, "clicked", cb, data);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
gtk_box_pack_start(GTK_BOX(tbar), mkbtn("New Backup", G_CALLBACK(on_new_backup), this), FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Restore", G_CALLBACK(on_restore), this), FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Delete", G_CALLBACK(on_delete_snapshot), this), FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(tbar), gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Settings", G_CALLBACK(on_settings), this), FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
progress_bar_ = gtk_progress_bar_new();
|
||||||
|
gtk_widget_set_size_request(progress_bar_, 200, -1);
|
||||||
|
gtk_box_pack_end(GTK_BOX(tbar), progress_bar_, FALSE, FALSE, 0);
|
||||||
|
progress_label_ = gtk_label_new("");
|
||||||
|
gtk_box_pack_end(GTK_BOX(tbar), progress_label_, FALSE, FALSE, 4);
|
||||||
|
|
||||||
|
gtk_box_pack_start(GTK_BOX(toolbar_), tbar, FALSE, FALSE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::build_statusbar() {
|
||||||
|
statusbar_ = gtk_statusbar_new();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::refresh_snapshot_list() {
|
||||||
|
GList* children = gtk_container_get_children(GTK_CONTAINER(snapshot_listbox_));
|
||||||
|
for (GList* l = children; l; l = l->next)
|
||||||
|
gtk_widget_destroy(GTK_WIDGET(l->data));
|
||||||
|
g_list_free(children);
|
||||||
|
|
||||||
|
auto snapshots = snapshots_.list_snapshots();
|
||||||
|
for (const auto& s : snapshots) {
|
||||||
|
GtkWidget* row_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||||
|
gtk_widget_set_margin_start(row_box, 8);
|
||||||
|
gtk_widget_set_margin_end(row_box, 8);
|
||||||
|
gtk_widget_set_margin_top(row_box, 6);
|
||||||
|
gtk_widget_set_margin_bottom(row_box, 6);
|
||||||
|
|
||||||
|
GtkWidget* name_label = gtk_label_new(s.display_name.c_str());
|
||||||
|
gtk_widget_set_halign(name_label, GTK_ALIGN_START);
|
||||||
|
PangoAttrList* attrs = pango_attr_list_new();
|
||||||
|
pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
|
||||||
|
gtk_label_set_attributes(GTK_LABEL(name_label), attrs);
|
||||||
|
pango_attr_list_unref(attrs);
|
||||||
|
gtk_box_pack_start(GTK_BOX(row_box), name_label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
std::string detail;
|
||||||
|
if (!s.description.empty()) detail = s.description;
|
||||||
|
else detail = "Size: " + format_size(s.size_bytes);
|
||||||
|
GtkWidget* detail_label = gtk_label_new(detail.c_str());
|
||||||
|
gtk_widget_set_halign(detail_label, GTK_ALIGN_START);
|
||||||
|
gtk_label_set_ellipsize(GTK_LABEL(detail_label), PANGO_ELLIPSIZE_END);
|
||||||
|
gtk_box_pack_start(GTK_BOX(row_box), detail_label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_list_box_insert(GTK_LIST_BOX(snapshot_listbox_), row_box, -1);
|
||||||
|
}
|
||||||
|
gtk_widget_show_all(snapshot_listbox_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_snapshot_selected(GtkListBox*, GtkListBoxRow* row, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
if (!row) return;
|
||||||
|
int idx = gtk_list_box_row_get_index(row);
|
||||||
|
auto snapshots = self->snapshots_.list_snapshots();
|
||||||
|
if (idx < 0 || idx >= (int)snapshots.size()) return;
|
||||||
|
|
||||||
|
self->current_snapshot_ = snapshots[idx].path;
|
||||||
|
self->current_path_ = "";
|
||||||
|
self->path_stack_.clear();
|
||||||
|
self->browse_path(self->current_snapshot_, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::browse_path(const std::string& snapshot, const std::string& path) {
|
||||||
|
current_snapshot_ = snapshot;
|
||||||
|
current_path_ = path;
|
||||||
|
|
||||||
|
std::string display = path.empty() ? "/" : path;
|
||||||
|
gtk_entry_set_text(GTK_ENTRY(path_entry_), display.c_str());
|
||||||
|
|
||||||
|
std::string info = "Browsing: " + snapshot;
|
||||||
|
if (!path.empty()) info += "/" + path;
|
||||||
|
gtk_label_set_text(GTK_LABEL(nav_label_), info.c_str());
|
||||||
|
|
||||||
|
gtk_list_store_clear(file_store_);
|
||||||
|
|
||||||
|
auto entries = snapshots_.browse_directory(snapshot, path);
|
||||||
|
for (const auto& e : entries) {
|
||||||
|
GtkTreeIter iter;
|
||||||
|
gtk_list_store_append(file_store_, &iter);
|
||||||
|
gtk_list_store_set(file_store_, &iter,
|
||||||
|
col_name, e.name.c_str(),
|
||||||
|
col_size, (e.is_dir ? "-" : format_size(e.size).c_str()),
|
||||||
|
col_type, e.is_dir ? "dir" : "file",
|
||||||
|
col_permissions, mode_string(e.mode).c_str(),
|
||||||
|
col_mtime, "",
|
||||||
|
-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::navigate_up() {
|
||||||
|
if (current_path_.empty()) return;
|
||||||
|
|
||||||
|
auto pos = current_path_.rfind('/');
|
||||||
|
if (pos == std::string::npos || pos == 0) {
|
||||||
|
path_stack_.pop_back();
|
||||||
|
if (path_stack_.empty()) {
|
||||||
|
current_path_ = "";
|
||||||
|
} else {
|
||||||
|
current_path_ = path_stack_.back();
|
||||||
|
path_stack_.pop_back();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path_stack_.push_back(current_path_);
|
||||||
|
current_path_ = current_path_.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
browse_path(current_snapshot_, current_path_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_file_row_activated(GtkTreeView* tree, GtkTreePath* path, GtkTreeViewColumn*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
if (self->current_snapshot_.empty()) return;
|
||||||
|
|
||||||
|
GtkTreeModel* model = gtk_tree_view_get_model(tree);
|
||||||
|
GtkTreeIter iter;
|
||||||
|
if (!gtk_tree_model_get_iter(model, &iter, path)) return;
|
||||||
|
|
||||||
|
gchar* name = nullptr;
|
||||||
|
gchar* type = nullptr;
|
||||||
|
gtk_tree_model_get(model, &iter, 0, &name, 2, &type, -1);
|
||||||
|
|
||||||
|
if (type && strcmp(type, "dir") == 0) {
|
||||||
|
std::string new_path = self->current_path_.empty()
|
||||||
|
? std::string(name)
|
||||||
|
: self->current_path_ + "/" + name;
|
||||||
|
self->path_stack_.push_back(self->current_path_);
|
||||||
|
self->browse_path(self->current_snapshot_, new_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_free(name);
|
||||||
|
g_free(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean MainWindow::on_file_button_press(GtkWidget*, GdkEventButton* event, gpointer d) {
|
||||||
|
if (event->type != GDK_BUTTON_PRESS || event->button != 3) return FALSE;
|
||||||
|
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
if (self->current_snapshot_.empty()) return FALSE;
|
||||||
|
|
||||||
|
GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->file_tree_));
|
||||||
|
GtkTreeModel* model = nullptr;
|
||||||
|
GtkTreeIter iter;
|
||||||
|
if (!gtk_tree_selection_get_selected(sel, &model, &iter)) return FALSE;
|
||||||
|
|
||||||
|
gchar* name = nullptr;
|
||||||
|
gchar* type = nullptr;
|
||||||
|
gtk_tree_model_get(model, &iter, 0, &name, 2, &type, -1);
|
||||||
|
|
||||||
|
std::string file_path = self->current_path_.empty()
|
||||||
|
? std::string(name)
|
||||||
|
: self->current_path_ + "/" + name;
|
||||||
|
|
||||||
|
GtkWidget* menu = gtk_menu_new();
|
||||||
|
|
||||||
|
/* Restore item */
|
||||||
|
GtkWidget* restore_item = gtk_menu_item_new_with_label("Restore");
|
||||||
|
struct RestoreCtx { MainWindow* self; std::string path; };
|
||||||
|
auto* ctx = new RestoreCtx{self, file_path};
|
||||||
|
g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* c = static_cast<RestoreCtx*>(d);
|
||||||
|
c->self->show_sudo_dialog([c](const std::string& pw) {
|
||||||
|
std::vector<std::string> 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);
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
if (type && strcmp(type, "file") == 0) {
|
||||||
|
GtkWidget* info_item = gtk_menu_item_new_with_label("Properties");
|
||||||
|
g_signal_connect_data(info_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* c = static_cast<RestoreCtx*>(d);
|
||||||
|
GtkWidget* dlg = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
||||||
|
"File: %s", c->path.c_str());
|
||||||
|
gtk_dialog_run(GTK_DIALOG(dlg));
|
||||||
|
gtk_widget_destroy(dlg);
|
||||||
|
delete c;
|
||||||
|
}), ctx, nullptr, (GConnectFlags)0);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(menu), info_item);
|
||||||
|
} else {
|
||||||
|
g_free(name);
|
||||||
|
g_free(type);
|
||||||
|
/* For dirs, free old ctx and create restore for dir */
|
||||||
|
delete ctx;
|
||||||
|
ctx = new RestoreCtx{self, file_path};
|
||||||
|
g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* c = static_cast<RestoreCtx*>(d);
|
||||||
|
c->self->show_sudo_dialog([c](const std::string& pw) {
|
||||||
|
std::vector<std::string> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_widget_show_all(menu);
|
||||||
|
gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
|
||||||
|
|
||||||
|
g_free(name);
|
||||||
|
g_free(type);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::show_sudo_dialog(std::function<void(const std::string&)> 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 access is required to restore files.\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) {
|
||||||
|
callback(std::string(pw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::show_progress_dialog(const std::string& title) {
|
||||||
|
progress_window_ = gtk_window_new(GTK_WINDOW_POPUP);
|
||||||
|
gtk_window_set_title(GTK_WINDOW(progress_window_), title.c_str());
|
||||||
|
gtk_window_set_default_size(GTK_WINDOW(progress_window_), 400, 120);
|
||||||
|
gtk_window_set_position(GTK_WINDOW(progress_window_), GTK_WIN_POS_CENTER);
|
||||||
|
gtk_window_set_keep_above(GTK_WINDOW(progress_window_), TRUE);
|
||||||
|
|
||||||
|
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_margin_start(vbox, 16);
|
||||||
|
gtk_widget_set_margin_top(vbox, 16);
|
||||||
|
gtk_container_add(GTK_CONTAINER(progress_window_), vbox);
|
||||||
|
|
||||||
|
GtkWidget* label = gtk_label_new(title.c_str());
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
progress_bar_ = gtk_progress_bar_new();
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), progress_bar_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
progress_label_ = gtk_label_new("Starting...");
|
||||||
|
gtk_box_pack_start(GTK_BOX(vbox), progress_label_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_widget_show_all(progress_window_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_backup(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
self->tray_.set_status("x3-Backup - Creating backup...", true);
|
||||||
|
self->show_progress_dialog("Creating Snapshot...");
|
||||||
|
|
||||||
|
g_thread_new("backup", [](gpointer data) -> gpointer {
|
||||||
|
auto* s = static_cast<MainWindow*>(data);
|
||||||
|
s->snapshots_.create_snapshot();
|
||||||
|
return nullptr;
|
||||||
|
}, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::start_quick_backup() {
|
||||||
|
on_backup(nullptr, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_restore(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
if (self->current_snapshot_.empty()) {
|
||||||
|
GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
|
||||||
|
"Please select a snapshot first.");
|
||||||
|
gtk_dialog_run(GTK_DIALOG(msg));
|
||||||
|
gtk_widget_destroy(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->file_tree_));
|
||||||
|
GtkTreeModel* model = nullptr;
|
||||||
|
GtkTreeIter iter;
|
||||||
|
if (!gtk_tree_selection_get_selected(sel, &model, &iter)) {
|
||||||
|
GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
|
||||||
|
"Please select a file or directory to restore.");
|
||||||
|
gtk_dialog_run(GTK_DIALOG(msg));
|
||||||
|
gtk_widget_destroy(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gchar* name = nullptr;
|
||||||
|
gtk_tree_model_get(model, &iter, 0, &name, -1);
|
||||||
|
std::string file_path = self->current_path_.empty()
|
||||||
|
? std::string(name) : self->current_path_ + "/" + name;
|
||||||
|
g_free(name);
|
||||||
|
|
||||||
|
self->show_sudo_dialog([self, file_path](const std::string& pw) {
|
||||||
|
std::vector<std::string> 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) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
if (self->current_snapshot_.empty()) return;
|
||||||
|
|
||||||
|
GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO,
|
||||||
|
"Delete this snapshot? This cannot be undone.");
|
||||||
|
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) {
|
||||||
|
std::string name = self->current_snapshot_;
|
||||||
|
self->snapshots_.delete_snapshot(name);
|
||||||
|
self->current_snapshot_ = "";
|
||||||
|
self->refresh_snapshot_list();
|
||||||
|
self->tray_.show_notification("x3-Backup", "Snapshot deleted");
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_new_backup(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
|
||||||
|
GtkWidget* dialog = gtk_dialog_new_with_buttons(
|
||||||
|
"New Backup", GTK_WINDOW(self->window_),
|
||||||
|
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
|
||||||
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||||
|
"_Create", 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, 12);
|
||||||
|
gtk_widget_set_margin_end(box, 12);
|
||||||
|
gtk_widget_set_margin_top(box, 12);
|
||||||
|
|
||||||
|
GtkWidget* label = gtk_label_new("Description (optional):");
|
||||||
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||||
|
gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* desc_entry = gtk_entry_new();
|
||||||
|
gtk_entry_set_placeholder_text(GTK_ENTRY(desc_entry), "My backup");
|
||||||
|
gtk_box_pack_start(GTK_BOX(box), desc_entry, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_container_add(GTK_CONTAINER(content), box);
|
||||||
|
gtk_widget_show_all(dialog);
|
||||||
|
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||||
|
const char* desc = gtk_entry_get_text(GTK_ENTRY(desc_entry));
|
||||||
|
std::string description = (desc && *desc) ? desc : "";
|
||||||
|
self->on_backup(nullptr, self);
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_settings(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
SettingsDialog dlg(GTK_WINDOW(self->window_), self->config_);
|
||||||
|
if (dlg.run()) {
|
||||||
|
self->refresh_snapshot_list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_quit(GtkWidget*, gpointer) {
|
||||||
|
gtk_main_quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_about(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
gtk_show_about_dialog(GTK_WINDOW(self->window_),
|
||||||
|
"program-name", "x3-Backup",
|
||||||
|
"version", "1.0.0",
|
||||||
|
"comments", "GTK3 backup/snapshot tool using rsync",
|
||||||
|
"copyright", "2026 x3",
|
||||||
|
"website", "https://git.sysdaemon.xyz/x3/x3-backup",
|
||||||
|
"license-type", GTK_LICENSE_GPL_3_0,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_navigate_up(GtkWidget*, gpointer d) {
|
||||||
|
static_cast<MainWindow*>(d)->navigate_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_show_log(GtkWidget*, gpointer d) {
|
||||||
|
static_cast<MainWindow*>(d)->show_log_window();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_system_backup(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
|
||||||
|
GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO,
|
||||||
|
"This will create a full system backup.\n"
|
||||||
|
"Files in /mnt and other exclude paths will be skipped.\n\nContinue?");
|
||||||
|
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) {
|
||||||
|
self->start_quick_backup();
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_system_restore(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
|
||||||
|
GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO,
|
||||||
|
"This will restore the ENTIRE system from a snapshot.\n"
|
||||||
|
"All current files will be overwritten!\n\n"
|
||||||
|
"Are you sure you want to continue?");
|
||||||
|
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) {
|
||||||
|
if (self->current_snapshot_.empty()) {
|
||||||
|
GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
|
||||||
|
"Please select a snapshot first.");
|
||||||
|
gtk_dialog_run(GTK_DIALOG(msg));
|
||||||
|
gtk_widget_destroy(msg);
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::install_cronjob() {
|
||||||
|
if (config_.settings().cron_schedule.empty()) return;
|
||||||
|
|
||||||
|
std::string label = config_.settings().cron_label;
|
||||||
|
std::string schedule = config_.settings().cron_schedule;
|
||||||
|
std::string cmd = "$(which x3-backup) --backup &";
|
||||||
|
|
||||||
|
std::string crontab_cmd = "(crontab -l 2>/dev/null | grep -v '" + label + "'; echo '" + schedule + " " + cmd + " # " + label + "') | crontab -";
|
||||||
|
system(crontab_cmd.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::remove_cronjob() {
|
||||||
|
std::string label = config_.settings().cron_label;
|
||||||
|
std::string cmd = "crontab -l 2>/dev/null | grep -v '" + label + "' | crontab -";
|
||||||
|
system(cmd.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_install_cron(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
self->install_cronjob();
|
||||||
|
GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
||||||
|
"Cronjob installed: %s\nSchedule: %s",
|
||||||
|
self->config_.settings().cron_label.c_str(),
|
||||||
|
self->config_.settings().cron_schedule.c_str());
|
||||||
|
gtk_dialog_run(GTK_DIALOG(msg));
|
||||||
|
gtk_widget_destroy(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_remove_cron(GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
self->remove_cronjob();
|
||||||
|
GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_),
|
||||||
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
||||||
|
"Cronjob removed.");
|
||||||
|
gtk_dialog_run(GTK_DIALOG(msg));
|
||||||
|
gtk_widget_destroy(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean MainWindow::on_tray_close(GtkWidget*, GdkEvent*, gpointer d) {
|
||||||
|
auto* self = static_cast<MainWindow*>(d);
|
||||||
|
self->hide();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::run() {
|
||||||
|
gtk_widget_show_all(window_);
|
||||||
|
tray_.set_status("x3-Backup - Idle");
|
||||||
|
gtk_main();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::show() {
|
||||||
|
gtk_widget_show_all(window_);
|
||||||
|
gtk_window_present(GTK_WINDOW(window_));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::hide() {
|
||||||
|
gtk_widget_hide(window_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MainWindow::is_visible() const {
|
||||||
|
return gtk_widget_get_visible(window_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::append_live_log(const std::string& line, const char* tag) {
|
||||||
|
if (!live_log_textview_) return;
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_));
|
||||||
|
GtkTextIter end;
|
||||||
|
gtk_text_buffer_get_end_iter(buf, &end);
|
||||||
|
if (tag) {
|
||||||
|
gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag);
|
||||||
|
} else {
|
||||||
|
gtk_text_buffer_insert(buf, &end, line.c_str(), -1);
|
||||||
|
}
|
||||||
|
gtk_text_buffer_insert(buf, &end, "\n", -1);
|
||||||
|
|
||||||
|
/* Auto-scroll to bottom */
|
||||||
|
GtkTextView* tv = GTK_TEXT_VIEW(live_log_textview_);
|
||||||
|
GtkTextMark* mark = gtk_text_buffer_create_mark(buf, nullptr, &end, FALSE);
|
||||||
|
gtk_text_view_scroll_mark_onscreen(tv, mark);
|
||||||
|
gtk_text_buffer_delete_mark(buf, mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::load_log_history() {
|
||||||
|
if (!backlog_textview_ || log_path_.empty()) return;
|
||||||
|
|
||||||
|
std::ifstream f(log_path_);
|
||||||
|
if (!f.is_open()) return;
|
||||||
|
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(backlog_textview_));
|
||||||
|
gtk_text_buffer_set_text(buf, "", -1);
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
GtkTextIter end;
|
||||||
|
gtk_text_buffer_get_end_iter(buf, &end);
|
||||||
|
|
||||||
|
const char* tag = nullptr;
|
||||||
|
if (line.find("[ERROR]") != std::string::npos || line.find("[CRIT]") != std::string::npos)
|
||||||
|
tag = "error";
|
||||||
|
else if (line.find("[done]") != std::string::npos || line.find("completed") != std::string::npos)
|
||||||
|
tag = "success";
|
||||||
|
else if (line.find("[info]") != std::string::npos || line.find("===") != std::string::npos)
|
||||||
|
tag = "info";
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag);
|
||||||
|
} else {
|
||||||
|
gtk_text_buffer_insert(buf, &end, line.c_str(), -1);
|
||||||
|
}
|
||||||
|
gtk_text_buffer_insert(buf, &end, "\n", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create color tags if not yet */
|
||||||
|
GtkTextBuffer* lbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_));
|
||||||
|
if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(lbuf), "error")) {
|
||||||
|
gtk_text_buffer_create_tag(lbuf, "error", "foreground", "#e74c3c", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(lbuf, "success", "foreground", "#2ecc71", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(lbuf, "info", "foreground", "#3498db", nullptr);
|
||||||
|
}
|
||||||
|
if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buf), "error")) {
|
||||||
|
gtk_text_buffer_create_tag(buf, "error", "foreground", "#e74c3c", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(buf, "success", "foreground", "#2ecc71", nullptr);
|
||||||
|
gtk_text_buffer_create_tag(buf, "info", "foreground", "#3498db", nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::show_log_window() {
|
||||||
|
load_log_history();
|
||||||
|
/* Switch to history log tab */
|
||||||
|
if (notebook_) {
|
||||||
|
int n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook_));
|
||||||
|
if (n >= 3) {
|
||||||
|
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <fstream>
|
||||||
|
#include "config.h"
|
||||||
|
#include "rsync_manager.h"
|
||||||
|
#include "snapshot_manager.h"
|
||||||
|
#include "system_tray.h"
|
||||||
|
#include "settings_dialog.h"
|
||||||
|
|
||||||
|
class MainWindow {
|
||||||
|
public:
|
||||||
|
MainWindow(Config& config);
|
||||||
|
~MainWindow();
|
||||||
|
|
||||||
|
GtkWidget* widget() const { return window_; }
|
||||||
|
void run();
|
||||||
|
void show();
|
||||||
|
void hide();
|
||||||
|
bool is_visible() const;
|
||||||
|
|
||||||
|
void start_quick_backup();
|
||||||
|
|
||||||
|
SystemTray& tray() { return tray_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void build_ui();
|
||||||
|
void build_menu();
|
||||||
|
void build_toolbar();
|
||||||
|
void build_sidebar();
|
||||||
|
void build_file_browser();
|
||||||
|
void build_statusbar();
|
||||||
|
|
||||||
|
void refresh_snapshot_list();
|
||||||
|
void load_selected_snapshot();
|
||||||
|
void browse_path(const std::string& snapshot, const std::string& path);
|
||||||
|
void navigate_up();
|
||||||
|
|
||||||
|
void on_backup_clicked();
|
||||||
|
void on_restore_clicked();
|
||||||
|
void on_delete_snapshot_clicked();
|
||||||
|
void on_new_backup_clicked();
|
||||||
|
|
||||||
|
void show_sudo_dialog(std::function<void(const std::string&)> callback);
|
||||||
|
void show_progress_dialog(const std::string& title);
|
||||||
|
|
||||||
|
void install_cronjob();
|
||||||
|
void remove_cronjob();
|
||||||
|
void show_log_window();
|
||||||
|
void append_live_log(const std::string& line, const char* tag = nullptr);
|
||||||
|
void load_log_history();
|
||||||
|
|
||||||
|
static void on_backup(GtkWidget* w, gpointer d);
|
||||||
|
static void on_restore(GtkWidget* w, gpointer d);
|
||||||
|
static void on_delete_snapshot(GtkWidget* w, gpointer d);
|
||||||
|
static void on_new_backup(GtkWidget* w, gpointer d);
|
||||||
|
static void on_settings(GtkWidget* w, gpointer d);
|
||||||
|
static void on_quit(GtkWidget* w, gpointer d);
|
||||||
|
static void on_about(GtkWidget* w, gpointer d);
|
||||||
|
static void on_show_log(GtkWidget* w, gpointer d);
|
||||||
|
static void on_snapshot_selected(GtkListBox* box, GtkListBoxRow* row, gpointer d);
|
||||||
|
static void on_file_row_activated(GtkTreeView* tree, GtkTreePath* path, GtkTreeViewColumn* col, gpointer d);
|
||||||
|
static gboolean on_file_button_press(GtkWidget* widget, GdkEventButton* event, gpointer d);
|
||||||
|
static void on_navigate_up(GtkWidget* w, gpointer d);
|
||||||
|
static void on_system_backup(GtkWidget* w, gpointer d);
|
||||||
|
static void on_system_restore(GtkWidget* w, gpointer d);
|
||||||
|
static void on_install_cron(GtkWidget* w, gpointer d);
|
||||||
|
static void on_remove_cron(GtkWidget* w, gpointer d);
|
||||||
|
static gboolean on_tray_close(GtkWidget* widget, GdkEvent* event, gpointer d);
|
||||||
|
|
||||||
|
Config& config_;
|
||||||
|
RsyncManager rsync_;
|
||||||
|
SnapshotManager snapshots_;
|
||||||
|
SystemTray tray_;
|
||||||
|
|
||||||
|
GtkWidget* window_ = nullptr;
|
||||||
|
GtkWidget* notebook_ = nullptr;
|
||||||
|
GtkWidget* snapshot_listbox_ = nullptr;
|
||||||
|
GtkWidget* file_tree_ = nullptr;
|
||||||
|
GtkWidget* path_entry_ = nullptr;
|
||||||
|
GtkWidget* statusbar_ = nullptr;
|
||||||
|
GtkWidget* progress_bar_ = nullptr;
|
||||||
|
GtkWidget* progress_label_ = nullptr;
|
||||||
|
GtkWidget* progress_window_ = nullptr;
|
||||||
|
GtkWidget* nav_label_ = nullptr;
|
||||||
|
|
||||||
|
GtkListStore* file_store_ = nullptr;
|
||||||
|
GtkWidget* toolbar_ = nullptr;
|
||||||
|
GtkWidget* log_window_ = nullptr;
|
||||||
|
GtkWidget* log_textview_ = nullptr;
|
||||||
|
guint log_timer_ = 0;
|
||||||
|
|
||||||
|
GtkWidget* live_log_textview_ = nullptr;
|
||||||
|
GtkWidget* live_log_label_ = nullptr;
|
||||||
|
GtkWidget* backlog_textview_ = nullptr;
|
||||||
|
GtkWidget* backlog_scrolled_ = nullptr;
|
||||||
|
|
||||||
|
std::string log_path_;
|
||||||
|
|
||||||
|
std::string current_snapshot_;
|
||||||
|
std::string current_path_;
|
||||||
|
std::vector<std::string> path_stack_;
|
||||||
|
|
||||||
|
int col_name = 0, col_size = 1, col_type = 2, col_permissions = 3, col_mtime = 4;
|
||||||
|
};
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#include "rsync_manager.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
#include <regex>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
RsyncManager::RsyncManager(Config& config) : config_(config) {}
|
||||||
|
|
||||||
|
std::vector<std::string> RsyncManager::build_exclude_args() {
|
||||||
|
std::vector<std::string> args;
|
||||||
|
for (const auto& e : config_.settings().exclude_patterns) {
|
||||||
|
args.push_back("--exclude=" + e);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RsyncManager::create_snapshot(const std::string& snapshot_name,
|
||||||
|
const std::string& source,
|
||||||
|
const std::string& dest,
|
||||||
|
const std::string& link_dest) {
|
||||||
|
if (running_) return false;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
running_ = true;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string target = dest + "/" + snapshot_name;
|
||||||
|
std::string cmd = "rsync -aAX --info=progress2";
|
||||||
|
|
||||||
|
if (config_.settings().delete_removed)
|
||||||
|
cmd += " --delete";
|
||||||
|
|
||||||
|
if (!link_dest.empty())
|
||||||
|
cmd += " --link-dest=\"" + link_dest + "\"";
|
||||||
|
|
||||||
|
for (const auto& e : config_.settings().exclude_patterns) {
|
||||||
|
cmd += " --exclude=\"" + e + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd += " \"" + source + "/\" \"" + target + "/\" 2>&1";
|
||||||
|
|
||||||
|
g_message("rsync cmd: %s", cmd.c_str());
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.log_line = "[cmd] " + cmd;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* pipe = popen(cmd.c_str(), "r");
|
||||||
|
if (!pipe) {
|
||||||
|
running_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RsyncProgress prog;
|
||||||
|
char buf[4096];
|
||||||
|
while (fgets(buf, sizeof(buf), pipe)) {
|
||||||
|
if (cancel_requested_) break;
|
||||||
|
std::string line(buf);
|
||||||
|
|
||||||
|
/* Remove trailing newline */
|
||||||
|
if (!line.empty() && line.back() == '\n') line.pop_back();
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.push_back(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_progress_line(line, prog);
|
||||||
|
prog.log_line = line;
|
||||||
|
if (on_progress) on_progress(prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = pclose(pipe);
|
||||||
|
running_ = false;
|
||||||
|
|
||||||
|
if (cancel_requested_) {
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.error = true;
|
||||||
|
p.error_message = "Cancelled by user";
|
||||||
|
p.finished = true;
|
||||||
|
p.log_line = "[cancelled] Operation cancelled by user";
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WEXITSTATUS(status) != 0) {
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.error = true;
|
||||||
|
p.error_message = "rsync exited with status " + std::to_string(WEXITSTATUS(status));
|
||||||
|
p.finished = true;
|
||||||
|
p.log_line = "[error] rsync exited with status " + std::to_string(WEXITSTATUS(status));
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
prog.finished = true;
|
||||||
|
prog.log_line = "[done] Snapshot created: " + snapshot_name;
|
||||||
|
on_progress(prog);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RsyncManager::restore_files(const std::string& snapshot_path,
|
||||||
|
const std::vector<std::string>& files,
|
||||||
|
const std::string& restore_dest,
|
||||||
|
const std::string& sudo_password) {
|
||||||
|
if (running_) return false;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
running_ = true;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
int total = files.size();
|
||||||
|
int current = 0;
|
||||||
|
|
||||||
|
for (const auto& f : files) {
|
||||||
|
if (cancel_requested_) break;
|
||||||
|
current++;
|
||||||
|
|
||||||
|
std::string src = snapshot_path + "/" + f;
|
||||||
|
std::string dst = restore_dest + "/" + f;
|
||||||
|
|
||||||
|
std::string cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX \"" + src + "\" \"" + dst + "\" 2>&1";
|
||||||
|
|
||||||
|
g_message("restore cmd: %s", cmd.c_str());
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.log_line = "[restore] (" + std::to_string(current) + "/" + std::to_string(total) + ") " + f;
|
||||||
|
p.files_transferred = current;
|
||||||
|
p.files_total = total;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* pipe = popen(cmd.c_str(), "r");
|
||||||
|
if (pipe) {
|
||||||
|
char rbuf[512];
|
||||||
|
while (fgets(rbuf, sizeof(rbuf), pipe)) {
|
||||||
|
std::string line(rbuf);
|
||||||
|
if (!line.empty() && line.back() == '\n') line.pop_back();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.push_back(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int status = pclose(pipe);
|
||||||
|
if (WEXITSTATUS(status) != 0) {
|
||||||
|
success = false;
|
||||||
|
g_warning("Failed to restore: %s", f.c_str());
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.log_line = "[error] Failed to restore: " + f;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
running_ = false;
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.finished = true;
|
||||||
|
p.log_line = success ? "[done] Restore completed" : "[done] Restore completed with errors";
|
||||||
|
p.error = !success;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RsyncManager::restore_full_snapshot(const std::string& snapshot_path,
|
||||||
|
const std::string& restore_dest,
|
||||||
|
const std::string& sudo_password) {
|
||||||
|
if (running_) return false;
|
||||||
|
cancel_requested_ = false;
|
||||||
|
running_ = true;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX --delete \""
|
||||||
|
+ snapshot_path + "/\" \"" + restore_dest + "/\" 2>&1";
|
||||||
|
|
||||||
|
g_message("full restore cmd: %s", cmd.c_str());
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.log_line = "[restore-full] Restoring entire system from " + snapshot_path;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* pipe = popen(cmd.c_str(), "r");
|
||||||
|
if (!pipe) {
|
||||||
|
running_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buf[4096];
|
||||||
|
while (fgets(buf, sizeof(buf), pipe)) {
|
||||||
|
if (cancel_requested_) break;
|
||||||
|
std::string line(buf);
|
||||||
|
if (!line.empty() && line.back() == '\n') line.pop_back();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex_);
|
||||||
|
log_buffer_.push_back(line);
|
||||||
|
}
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.log_line = line;
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = pclose(pipe);
|
||||||
|
running_ = false;
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
RsyncProgress p;
|
||||||
|
p.finished = true;
|
||||||
|
p.error = (WEXITSTATUS(status) != 0);
|
||||||
|
if (p.error) p.error_message = "Restore failed with status " + std::to_string(WEXITSTATUS(status));
|
||||||
|
p.log_line = p.error ? "[error] Full restore failed" : "[done] Full system restore completed";
|
||||||
|
on_progress(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WEXITSTATUS(status) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RsyncManager::cancel() {
|
||||||
|
cancel_requested_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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::smatch m;
|
||||||
|
if (std::regex_search(line, m, re_pct)) {
|
||||||
|
prog.bytes_transferred = std::stoll(m[1].str());
|
||||||
|
}
|
||||||
|
if (std::regex_search(line, m, re_speed)) {
|
||||||
|
prog.speed = (int)std::stod(m[1].str());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <functional>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
struct RsyncProgress {
|
||||||
|
int64_t files_transferred = 0;
|
||||||
|
int64_t files_total = 0;
|
||||||
|
int64_t bytes_transferred = 0;
|
||||||
|
int64_t bytes_total = 0;
|
||||||
|
int speed = 0;
|
||||||
|
std::string current_file;
|
||||||
|
std::string log_line;
|
||||||
|
bool finished = false;
|
||||||
|
bool error = false;
|
||||||
|
std::string error_message;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RsyncManager {
|
||||||
|
public:
|
||||||
|
RsyncManager(Config& config);
|
||||||
|
|
||||||
|
bool create_snapshot(const std::string& snapshot_name,
|
||||||
|
const std::string& source,
|
||||||
|
const std::string& dest,
|
||||||
|
const std::string& link_dest = "");
|
||||||
|
|
||||||
|
bool restore_files(const std::string& snapshot_path,
|
||||||
|
const std::vector<std::string>& files,
|
||||||
|
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& sudo_password);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
bool is_running() const { return running_; }
|
||||||
|
|
||||||
|
std::function<void(const RsyncProgress&)> on_progress;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Config& config_;
|
||||||
|
std::atomic<bool> running_{false};
|
||||||
|
std::atomic<bool> cancel_requested_{false};
|
||||||
|
std::thread worker_;
|
||||||
|
std::mutex log_mutex_;
|
||||||
|
std::vector<std::string> log_buffer_;
|
||||||
|
|
||||||
|
std::vector<std::string> build_exclude_args();
|
||||||
|
bool run_rsync(const std::vector<std::string>& args);
|
||||||
|
void parse_progress_line(const std::string& line, RsyncProgress& prog);
|
||||||
|
};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#include "settings_dialog.h"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
SettingsDialog::SettingsDialog(GtkWindow* parent, Config& config)
|
||||||
|
: parent_(parent), config_(config) {}
|
||||||
|
|
||||||
|
void SettingsDialog::populate_excludes() {
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(exclude_textview_));
|
||||||
|
std::string text;
|
||||||
|
for (const auto& e : config_.settings().exclude_patterns) {
|
||||||
|
if (!text.empty()) text += "\n";
|
||||||
|
text += e;
|
||||||
|
}
|
||||||
|
gtk_text_buffer_set_text(buf, text.c_str(), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsDialog::save_excludes() {
|
||||||
|
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(exclude_textview_));
|
||||||
|
GtkTextIter start, end;
|
||||||
|
gtk_text_buffer_get_bounds(buf, &start, &end);
|
||||||
|
char* text = gtk_text_buffer_get_text(buf, &start, &end, FALSE);
|
||||||
|
|
||||||
|
config_.settings().exclude_patterns.clear();
|
||||||
|
std::istringstream iss(text);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(iss, line)) {
|
||||||
|
if (!line.empty()) {
|
||||||
|
config_.settings().exclude_patterns.push_back(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_free(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsDialog::run() {
|
||||||
|
GtkWidget* dialog = gtk_dialog_new_with_buttons(
|
||||||
|
"Settings", parent_,
|
||||||
|
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
|
||||||
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||||
|
"_Save", GTK_RESPONSE_ACCEPT,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||||
|
GtkWidget* notebook = gtk_notebook_new();
|
||||||
|
gtk_widget_set_margin_start(notebook, 8);
|
||||||
|
gtk_widget_set_margin_end(notebook, 8);
|
||||||
|
gtk_widget_set_margin_top(notebook, 8);
|
||||||
|
|
||||||
|
/* ── General Tab ── */
|
||||||
|
GtkWidget* gen_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_margin_start(gen_box, 8);
|
||||||
|
gtk_widget_set_margin_end(gen_box, 8);
|
||||||
|
gtk_widget_set_margin_top(gen_box, 8);
|
||||||
|
|
||||||
|
GtkWidget* dest_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
GtkWidget* dest_label = gtk_label_new("Backup Destination:");
|
||||||
|
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_label, FALSE, FALSE, 0);
|
||||||
|
dest_entry_ = gtk_entry_new();
|
||||||
|
gtk_entry_set_text(GTK_ENTRY(dest_entry_), config_.settings().backup_dest.c_str());
|
||||||
|
gtk_widget_set_hexpand(dest_entry_, TRUE);
|
||||||
|
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_entry_, TRUE, TRUE, 0);
|
||||||
|
GtkWidget* dest_btn = gtk_button_new_with_label("Browse...");
|
||||||
|
g_signal_connect(dest_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* self = static_cast<SettingsDialog*>(d);
|
||||||
|
GtkWidget* fc = gtk_file_chooser_dialog_new("Select Backup Destination",
|
||||||
|
self->parent_, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
||||||
|
"_Cancel", GTK_RESPONSE_CANCEL, "_Select", GTK_RESPONSE_ACCEPT, nullptr);
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(fc)) == GTK_RESPONSE_ACCEPT) {
|
||||||
|
char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(fc));
|
||||||
|
if (path) {
|
||||||
|
gtk_entry_set_text(GTK_ENTRY(self->dest_entry_), path);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(fc);
|
||||||
|
}), this);
|
||||||
|
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_btn, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(gen_box), dest_hbox, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* src_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
GtkWidget* src_label = gtk_label_new("Source Path:");
|
||||||
|
gtk_box_pack_start(GTK_BOX(src_hbox), src_label, FALSE, FALSE, 0);
|
||||||
|
source_entry_ = gtk_entry_new();
|
||||||
|
gtk_entry_set_text(GTK_ENTRY(source_entry_), config_.settings().source_path.c_str());
|
||||||
|
gtk_widget_set_hexpand(source_entry_, TRUE);
|
||||||
|
gtk_box_pack_start(GTK_BOX(src_hbox), source_entry_, TRUE, TRUE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(gen_box), src_hbox, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* ret_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
GtkWidget* ret_days_label = gtk_label_new("Keep snapshots (days):");
|
||||||
|
gtk_box_pack_start(GTK_BOX(ret_box), ret_days_label, FALSE, FALSE, 0);
|
||||||
|
retention_days_spin_ = gtk_spin_button_new_with_range(0, 365, 1);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(retention_days_spin_), config_.settings().retention_days);
|
||||||
|
gtk_box_pack_start(GTK_BOX(ret_box), retention_days_spin_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* ret_count_label = gtk_label_new("Max snapshots:");
|
||||||
|
gtk_box_pack_start(GTK_BOX(ret_box), ret_count_label, FALSE, FALSE, 0);
|
||||||
|
retention_count_spin_ = gtk_spin_button_new_with_range(0, 100, 1);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(retention_count_spin_), config_.settings().retention_count);
|
||||||
|
gtk_box_pack_start(GTK_BOX(ret_box), retention_count_spin_, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(gen_box), ret_box, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
delete_check_ = gtk_check_button_new_with_label("Delete files from backup that are no longer in source");
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(delete_check_), config_.settings().delete_removed);
|
||||||
|
gtk_box_pack_start(GTK_BOX(gen_box), delete_check_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gen_box, gtk_label_new("General"));
|
||||||
|
|
||||||
|
/* ── Permissions Tab ── */
|
||||||
|
GtkWidget* perm_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_margin_start(perm_box, 8);
|
||||||
|
gtk_widget_set_margin_top(perm_box, 8);
|
||||||
|
|
||||||
|
acl_check_ = gtk_check_button_new_with_label("Preserve ACLs (Access Control Lists)");
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(acl_check_), config_.settings().preserve_acls);
|
||||||
|
gtk_box_pack_start(GTK_BOX(perm_box), acl_check_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
xattr_check_ = gtk_check_button_new_with_label("Preserve extended attributes (xattrs)");
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(xattr_check_), config_.settings().preserve_xattrs);
|
||||||
|
gtk_box_pack_start(GTK_BOX(perm_box), xattr_check_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), perm_box, gtk_label_new("Permissions"));
|
||||||
|
|
||||||
|
/* ── Excludes Tab ── */
|
||||||
|
GtkWidget* excl_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_margin_start(excl_box, 8);
|
||||||
|
gtk_widget_set_margin_top(excl_box, 8);
|
||||||
|
|
||||||
|
GtkWidget* excl_label = gtk_label_new("Exclude patterns (one per line):");
|
||||||
|
gtk_box_pack_start(GTK_BOX(excl_box), excl_label, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* scroll = gtk_scrolled_window_new(nullptr, nullptr);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_widget_set_size_request(scroll, 400, 200);
|
||||||
|
exclude_textview_ = gtk_text_view_new();
|
||||||
|
gtk_text_view_set_monospace(GTK_TEXT_VIEW(exclude_textview_), TRUE);
|
||||||
|
gtk_container_add(GTK_CONTAINER(scroll), exclude_textview_);
|
||||||
|
gtk_box_pack_start(GTK_BOX(excl_box), scroll, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
populate_excludes();
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), excl_box, gtk_label_new("Excludes"));
|
||||||
|
|
||||||
|
/* ── Cronjob Tab ── */
|
||||||
|
GtkWidget* cron_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
|
gtk_widget_set_margin_start(cron_box, 8);
|
||||||
|
gtk_widget_set_margin_top(cron_box, 8);
|
||||||
|
|
||||||
|
cron_enabled_check_ = gtk_check_button_new_with_label("Enable scheduled backups (cronjob)");
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cron_enabled_check_), config_.settings().cron_enabled);
|
||||||
|
gtk_box_pack_start(GTK_BOX(cron_box), cron_enabled_check_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* sched_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
GtkWidget* sched_label = gtk_label_new("Schedule (crontab format):");
|
||||||
|
gtk_box_pack_start(GTK_BOX(sched_hbox), sched_label, FALSE, FALSE, 0);
|
||||||
|
cron_schedule_entry_ = gtk_entry_new();
|
||||||
|
gtk_entry_set_text(GTK_ENTRY(cron_schedule_entry_), config_.settings().cron_schedule.c_str());
|
||||||
|
gtk_entry_set_placeholder_text(GTK_ENTRY(cron_schedule_entry_), "0 2 * * *");
|
||||||
|
gtk_widget_set_size_request(cron_schedule_entry_, 200, -1);
|
||||||
|
gtk_box_pack_start(GTK_BOX(sched_hbox), cron_schedule_entry_, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
GtkWidget* cron_help = gtk_label_new(NULL);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(cron_help),
|
||||||
|
"<small>Examples: <i>0 2 * * *</i> = daily at 2am, "
|
||||||
|
"<i>0 */6 * * *</i> = every 6h, "
|
||||||
|
"<i>0 2 * * 0</i> = Sundays at 2am</small>");
|
||||||
|
gtk_widget_set_halign(cron_help, GTK_ALIGN_START);
|
||||||
|
gtk_box_pack_start(GTK_BOX(cron_box), sched_hbox, FALSE, FALSE, 0);
|
||||||
|
gtk_box_pack_start(GTK_BOX(cron_box), cron_help, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), cron_box, gtk_label_new("Schedule"));
|
||||||
|
|
||||||
|
gtk_box_pack_start(GTK_BOX(content), notebook, TRUE, TRUE, 0);
|
||||||
|
gtk_widget_show_all(dialog);
|
||||||
|
|
||||||
|
bool saved = false;
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||||
|
config_.settings().backup_dest = gtk_entry_get_text(GTK_ENTRY(dest_entry_));
|
||||||
|
config_.settings().source_path = gtk_entry_get_text(GTK_ENTRY(source_entry_));
|
||||||
|
config_.settings().retention_days = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(retention_days_spin_));
|
||||||
|
config_.settings().retention_count = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(retention_count_spin_));
|
||||||
|
config_.settings().delete_removed = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(delete_check_));
|
||||||
|
config_.settings().preserve_acls = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(acl_check_));
|
||||||
|
config_.settings().preserve_xattrs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(xattr_check_));
|
||||||
|
config_.settings().cron_enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cron_enabled_check_));
|
||||||
|
config_.settings().cron_schedule = gtk_entry_get_text(GTK_ENTRY(cron_schedule_entry_));
|
||||||
|
save_excludes();
|
||||||
|
config_.save();
|
||||||
|
saved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class SettingsDialog {
|
||||||
|
public:
|
||||||
|
SettingsDialog(GtkWindow* parent, Config& config);
|
||||||
|
|
||||||
|
bool run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
GtkWindow* parent_;
|
||||||
|
Config& config_;
|
||||||
|
GtkWidget* dest_entry_;
|
||||||
|
GtkWidget* source_entry_;
|
||||||
|
GtkWidget* retention_days_spin_;
|
||||||
|
GtkWidget* retention_count_spin_;
|
||||||
|
GtkWidget* delete_check_;
|
||||||
|
GtkWidget* acl_check_;
|
||||||
|
GtkWidget* xattr_check_;
|
||||||
|
GtkWidget* cron_enabled_check_;
|
||||||
|
GtkWidget* cron_schedule_entry_;
|
||||||
|
GtkWidget* exclude_textview_;
|
||||||
|
|
||||||
|
void populate_excludes();
|
||||||
|
void save_excludes();
|
||||||
|
};
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
#include "snapshot_manager.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <json-glib/json-glib.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
SnapshotManager::SnapshotManager(Config& config, RsyncManager& rsync)
|
||||||
|
: config_(config), rsync_(rsync) {}
|
||||||
|
|
||||||
|
std::string SnapshotManager::get_timestamp() {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
char buf[64];
|
||||||
|
strftime(buf, sizeof(buf), "%Y-%m-%d_%H-%M-%S", t);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SnapshotManager::create_snapshot(const std::string& description) {
|
||||||
|
std::string ts = get_timestamp();
|
||||||
|
std::string snapshots_dir = config_.snapshots_dir();
|
||||||
|
std::string metadata_dir = config_.metadata_dir();
|
||||||
|
|
||||||
|
std::string cmd1 = "mkdir -p \"" + snapshots_dir + "\"";
|
||||||
|
std::string cmd2 = "mkdir -p \"" + metadata_dir + "\"";
|
||||||
|
system(cmd1.c_str());
|
||||||
|
system(cmd2.c_str());
|
||||||
|
|
||||||
|
/* Find latest snapshot for hardlink delta */
|
||||||
|
std::string link_dest = "";
|
||||||
|
auto snapshots = list_snapshots();
|
||||||
|
if (!snapshots.empty()) {
|
||||||
|
link_dest = snapshots[0].path;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsync_.on_progress = on_progress;
|
||||||
|
bool ok = rsync_.create_snapshot(ts, config_.settings().source_path, snapshots_dir, link_dest);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
SnapshotInfo info;
|
||||||
|
info.name = ts;
|
||||||
|
info.timestamp = ts;
|
||||||
|
info.path = snapshots_dir + "/" + ts;
|
||||||
|
info.description = description;
|
||||||
|
save_metadata(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok ? ts : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapshotManager::delete_snapshot(const std::string& name) {
|
||||||
|
std::string path = config_.snapshots_dir() + "/" + name;
|
||||||
|
std::string cmd = "rm -rf \"" + path + "\"";
|
||||||
|
system(cmd.c_str());
|
||||||
|
|
||||||
|
std::string meta = config_.metadata_dir() + "/" + name + ".json";
|
||||||
|
std::string meta_cmd = "rm -f \"" + meta + "\"";
|
||||||
|
system(meta_cmd.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SnapshotInfo> SnapshotManager::list_snapshots() {
|
||||||
|
std::vector<SnapshotInfo> result;
|
||||||
|
std::string dir = config_.snapshots_dir();
|
||||||
|
|
||||||
|
DIR* d = opendir(dir.c_str());
|
||||||
|
if (!d) return result;
|
||||||
|
|
||||||
|
struct dirent* entry;
|
||||||
|
while ((entry = readdir(d)) != nullptr) {
|
||||||
|
if (entry->d_name[0] == '.') continue;
|
||||||
|
if (entry->d_type != DT_DIR) continue;
|
||||||
|
|
||||||
|
SnapshotInfo info;
|
||||||
|
info.name = entry->d_name;
|
||||||
|
info.timestamp = entry->d_name;
|
||||||
|
info.path = dir + "/" + entry->d_name;
|
||||||
|
|
||||||
|
/* Format display name */
|
||||||
|
std::string ts = info.timestamp;
|
||||||
|
if (ts.size() >= 19) {
|
||||||
|
info.display_name = ts.substr(0, 10) + " " + ts.substr(11, 8);
|
||||||
|
} else {
|
||||||
|
info.display_name = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_metadata(info);
|
||||||
|
result.push_back(info);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
|
||||||
|
std::sort(result.begin(), result.end(), [](const SnapshotInfo& a, const SnapshotInfo& b) {
|
||||||
|
return a.timestamp > b.timestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapshotInfo SnapshotManager::get_snapshot_info(const std::string& name) {
|
||||||
|
SnapshotInfo info;
|
||||||
|
info.name = name;
|
||||||
|
info.path = config_.snapshots_dir() + "/" + name;
|
||||||
|
info.timestamp = name;
|
||||||
|
load_metadata(info);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FileEntry> SnapshotManager::browse_directory(const std::string& snapshot_path,
|
||||||
|
const std::string& subpath) {
|
||||||
|
std::vector<FileEntry> result;
|
||||||
|
std::string full = snapshot_path;
|
||||||
|
if (!subpath.empty()) full += "/" + subpath;
|
||||||
|
|
||||||
|
DIR* d = opendir(full.c_str());
|
||||||
|
if (!d) return result;
|
||||||
|
|
||||||
|
struct dirent* entry;
|
||||||
|
while ((entry = readdir(d)) != nullptr) {
|
||||||
|
if (entry->d_name[0] == '.' && (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
FileEntry fe;
|
||||||
|
fe.name = entry->d_name;
|
||||||
|
fe.is_dir = (entry->d_type == DT_DIR);
|
||||||
|
fe.full_path = subpath.empty() ? fe.name : subpath + "/" + fe.name;
|
||||||
|
|
||||||
|
std::string file_path = full + "/" + entry->d_name;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(file_path.c_str(), &st) == 0) {
|
||||||
|
fe.size = st.st_size;
|
||||||
|
fe.mode = st.st_mode;
|
||||||
|
fe.uid = st.st_uid;
|
||||||
|
fe.gid = st.st_gid;
|
||||||
|
fe.mtime = st.st_mtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(fe);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
|
||||||
|
std::sort(result.begin(), result.end(), [](const FileEntry& a, const FileEntry& b) {
|
||||||
|
if (a.is_dir != b.is_dir) return a.is_dir > b.is_dir;
|
||||||
|
return a.name < b.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SnapshotManager::restore_selected(const std::string& snapshot_path,
|
||||||
|
const std::vector<std::string>& files,
|
||||||
|
const std::string& sudo_password) {
|
||||||
|
return rsync_.restore_files(snapshot_path, files, "/", sudo_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
auto snapshots = list_snapshots();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int i = snapshots.size() - 1; i >= 0; i--) {
|
||||||
|
bool should_delete = false;
|
||||||
|
|
||||||
|
if (config_.settings().retention_count > 0 &&
|
||||||
|
(int)snapshots.size() - count > config_.settings().retention_count) {
|
||||||
|
should_delete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_.settings().retention_days > 0) {
|
||||||
|
struct tm tm = {};
|
||||||
|
std::string ts = snapshots[i].timestamp;
|
||||||
|
if (ts.size() >= 19) {
|
||||||
|
sscanf(ts.c_str(), "%d-%d-%d_%d-%d-%d",
|
||||||
|
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
|
||||||
|
&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
|
||||||
|
tm.tm_year -= 1900;
|
||||||
|
tm.tm_mon -= 1;
|
||||||
|
time_t snap_time = mktime(&tm);
|
||||||
|
double diff = difftime(time(nullptr), snap_time);
|
||||||
|
if (diff > config_.settings().retention_days * 86400.0) {
|
||||||
|
should_delete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_delete) {
|
||||||
|
delete_snapshot(snapshots[i].name);
|
||||||
|
} else {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapshotManager::save_metadata(const SnapshotInfo& info) {
|
||||||
|
std::string path = config_.metadata_dir() + "/" + info.name + ".json";
|
||||||
|
|
||||||
|
JsonBuilder* builder = json_builder_new();
|
||||||
|
json_builder_begin_object(builder);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "name");
|
||||||
|
json_builder_add_string_value(builder, info.name.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "timestamp");
|
||||||
|
json_builder_add_string_value(builder, info.timestamp.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "description");
|
||||||
|
json_builder_add_string_value(builder, info.description.c_str());
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "size_bytes");
|
||||||
|
json_builder_add_int_value(builder, info.size_bytes);
|
||||||
|
|
||||||
|
json_builder_set_member_name(builder, "file_count");
|
||||||
|
json_builder_add_int_value(builder, info.file_count);
|
||||||
|
|
||||||
|
json_builder_end_object(builder);
|
||||||
|
|
||||||
|
JsonGenerator* gen = json_generator_new();
|
||||||
|
json_generator_set_root(gen, json_builder_get_root(builder));
|
||||||
|
json_generator_set_pretty(gen, TRUE);
|
||||||
|
json_generator_to_file(gen, path.c_str(), nullptr);
|
||||||
|
|
||||||
|
g_object_unref(gen);
|
||||||
|
g_object_unref(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapshotManager::load_metadata(SnapshotInfo& info) {
|
||||||
|
std::string path = config_.metadata_dir() + "/" + info.name + ".json";
|
||||||
|
FILE* f = fopen(path.c_str(), "r");
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long len = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
char* buf = new char[len + 1];
|
||||||
|
fread(buf, 1, len, f);
|
||||||
|
buf[len] = 0;
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
JsonParser* parser = json_parser_new();
|
||||||
|
GError* err = nullptr;
|
||||||
|
if (json_parser_load_from_data(parser, buf, len, &err)) {
|
||||||
|
JsonObject* root = json_node_get_object(json_parser_get_root(parser));
|
||||||
|
if (json_object_has_member(root, "description"))
|
||||||
|
info.description = json_object_get_string_member(root, "description");
|
||||||
|
if (json_object_has_member(root, "size_bytes"))
|
||||||
|
info.size_bytes = json_object_get_int_member(root, "size_bytes");
|
||||||
|
if (json_object_has_member(root, "file_count"))
|
||||||
|
info.file_count = (int)json_object_get_int_member(root, "file_count");
|
||||||
|
}
|
||||||
|
if (err) g_error_free(err);
|
||||||
|
g_object_unref(parser);
|
||||||
|
delete[] buf;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <ctime>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "rsync_manager.h"
|
||||||
|
|
||||||
|
struct SnapshotInfo {
|
||||||
|
std::string name;
|
||||||
|
std::string timestamp;
|
||||||
|
std::string display_name;
|
||||||
|
std::string description;
|
||||||
|
int64_t size_bytes = 0;
|
||||||
|
int file_count = 0;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileEntry {
|
||||||
|
std::string name;
|
||||||
|
std::string full_path;
|
||||||
|
bool is_dir = false;
|
||||||
|
int64_t size = 0;
|
||||||
|
mode_t mode = 0;
|
||||||
|
uid_t uid = 0;
|
||||||
|
gid_t gid = 0;
|
||||||
|
time_t mtime = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SnapshotManager {
|
||||||
|
public:
|
||||||
|
SnapshotManager(Config& config, RsyncManager& rsync);
|
||||||
|
|
||||||
|
std::string create_snapshot(const std::string& description = "");
|
||||||
|
void delete_snapshot(const std::string& name);
|
||||||
|
std::vector<SnapshotInfo> list_snapshots();
|
||||||
|
SnapshotInfo get_snapshot_info(const std::string& name);
|
||||||
|
|
||||||
|
std::vector<FileEntry> browse_directory(const std::string& snapshot_path,
|
||||||
|
const std::string& subpath = "");
|
||||||
|
|
||||||
|
bool restore_selected(const std::string& snapshot_path,
|
||||||
|
const std::vector<std::string>& files,
|
||||||
|
const std::string& sudo_password);
|
||||||
|
|
||||||
|
bool restore_full(const std::string& snapshot_path,
|
||||||
|
const std::string& sudo_password);
|
||||||
|
|
||||||
|
void cleanup_old_snapshots();
|
||||||
|
|
||||||
|
std::function<void(const RsyncProgress&)> on_progress;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Config& config_;
|
||||||
|
RsyncManager& rsync_;
|
||||||
|
|
||||||
|
std::string get_timestamp();
|
||||||
|
void save_metadata(const SnapshotInfo& info);
|
||||||
|
void load_metadata(SnapshotInfo& info);
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "system_tray.h"
|
||||||
|
|
||||||
|
SystemTray::SystemTray() : icon_(nullptr), menu_(nullptr) {
|
||||||
|
icon_ = gtk_status_icon_new();
|
||||||
|
gtk_status_icon_set_tooltip_text(icon_, "x3-Backup - Idle");
|
||||||
|
gtk_status_icon_set_visible(icon_, TRUE);
|
||||||
|
|
||||||
|
g_signal_connect(icon_, "activate", G_CALLBACK(on_status_icon_activate), this);
|
||||||
|
g_signal_connect(icon_, "button-press-event", G_CALLBACK(on_status_icon_button_press), this);
|
||||||
|
|
||||||
|
set_icon("drive-harddisk");
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemTray::~SystemTray() {
|
||||||
|
if (icon_) {
|
||||||
|
gtk_status_icon_set_visible(icon_, FALSE);
|
||||||
|
g_object_unref(icon_);
|
||||||
|
icon_ = nullptr;
|
||||||
|
}
|
||||||
|
if (menu_) {
|
||||||
|
gtk_widget_destroy(menu_);
|
||||||
|
menu_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemTray::set_status(const std::string& tooltip, bool backup_running) {
|
||||||
|
if (!icon_) return;
|
||||||
|
gtk_status_icon_set_tooltip_text(icon_, tooltip.c_str());
|
||||||
|
|
||||||
|
if (backup_running) {
|
||||||
|
set_icon("preferences-system");
|
||||||
|
} else {
|
||||||
|
set_icon("drive-harddisk");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemTray::set_icon(const char* icon_name) {
|
||||||
|
if (!icon_) return;
|
||||||
|
gtk_status_icon_set_from_icon_name(icon_, icon_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemTray::show_notification(const std::string& summary, const std::string& body, bool is_error) {
|
||||||
|
GNotification* notification = g_notification_new(summary.c_str());
|
||||||
|
g_notification_set_body(notification, body.c_str());
|
||||||
|
g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_NORMAL);
|
||||||
|
g_application_send_notification(g_application_get_default(), nullptr, notification);
|
||||||
|
g_object_unref(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemTray::on_status_icon_activate(GtkStatusIcon*, gpointer data) {
|
||||||
|
auto* self = static_cast<SystemTray*>(data);
|
||||||
|
if (self->on_show) self->on_show();
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean SystemTray::on_status_icon_button_press(GtkStatusIcon*, GdkEventButton* event, gpointer data) {
|
||||||
|
auto* self = static_cast<SystemTray*>(data);
|
||||||
|
|
||||||
|
if (event->button != 3) return FALSE;
|
||||||
|
|
||||||
|
if (self->menu_) {
|
||||||
|
gtk_widget_destroy(self->menu_);
|
||||||
|
self->menu_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->menu_ = gtk_menu_new();
|
||||||
|
|
||||||
|
GtkWidget* show_item = gtk_menu_item_new_with_label("Show x3-Backup");
|
||||||
|
g_signal_connect(show_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* s = static_cast<SystemTray*>(d);
|
||||||
|
if (s->on_show) s->on_show();
|
||||||
|
}), self);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), show_item);
|
||||||
|
|
||||||
|
GtkWidget* backup_item = gtk_menu_item_new_with_label("Quick Backup");
|
||||||
|
g_signal_connect(backup_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* s = static_cast<SystemTray*>(d);
|
||||||
|
if (s->on_quick_backup) s->on_quick_backup();
|
||||||
|
}), self);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), backup_item);
|
||||||
|
|
||||||
|
GtkWidget* settings_item = gtk_menu_item_new_with_label("Settings");
|
||||||
|
g_signal_connect(settings_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* s = static_cast<SystemTray*>(d);
|
||||||
|
if (s->on_settings) s->on_settings();
|
||||||
|
}), self);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), settings_item);
|
||||||
|
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), gtk_separator_menu_item_new());
|
||||||
|
|
||||||
|
GtkWidget* quit_item = gtk_menu_item_new_with_label("Quit");
|
||||||
|
g_signal_connect(quit_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||||
|
auto* s = static_cast<SystemTray*>(d);
|
||||||
|
if (s->on_quit) s->on_quit();
|
||||||
|
}), self);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), quit_item);
|
||||||
|
|
||||||
|
gtk_widget_show_all(self->menu_);
|
||||||
|
gtk_menu_popup_at_pointer(GTK_MENU(self->menu_), (GdkEvent*)event);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class SystemTray {
|
||||||
|
public:
|
||||||
|
SystemTray();
|
||||||
|
~SystemTray();
|
||||||
|
|
||||||
|
void set_status(const std::string& tooltip, bool backup_running = false);
|
||||||
|
void set_icon(const char* icon_name);
|
||||||
|
void show_notification(const std::string& summary, const std::string& body, bool is_error = false);
|
||||||
|
bool is_visible() const { return icon_ != nullptr; }
|
||||||
|
|
||||||
|
std::function<void()> on_show;
|
||||||
|
std::function<void()> on_quit;
|
||||||
|
std::function<void()> on_quick_backup;
|
||||||
|
std::function<void()> on_settings;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GtkStatusIcon* icon_;
|
||||||
|
GtkWidget* menu_;
|
||||||
|
|
||||||
|
static void on_status_icon_activate(GtkStatusIcon* icon, gpointer data);
|
||||||
|
static gboolean on_status_icon_button_press(GtkStatusIcon* icon, GdkEventButton* event, gpointer data);
|
||||||
|
};
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== x3-Backup Uninstall ==="
|
||||||
|
|
||||||
|
# Remove installed files
|
||||||
|
echo "Removing installed files..."
|
||||||
|
if [ -f /usr/local/bin/x3-backup ]; then
|
||||||
|
sudo rm -f /usr/local/bin/x3-backup
|
||||||
|
echo " Removed /usr/local/bin/x3-backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /usr/local/share/applications/x3-backup.desktop ]; then
|
||||||
|
sudo rm -f /usr/local/share/applications/x3-backup.desktop
|
||||||
|
echo " Removed desktop file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /usr/local/share/icons/hicolor/256x256/apps/x3-backup.png ]; then
|
||||||
|
sudo rm -f /usr/local/share/icons/hicolor/256x256/apps/x3-backup.png
|
||||||
|
echo " Removed icon"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove cronjob
|
||||||
|
echo "Removing cronjob..."
|
||||||
|
crontab -l 2>/dev/null | grep -v 'x3-backup-auto' | crontab - 2>/dev/null || true
|
||||||
|
echo " Cronjob removed"
|
||||||
|
|
||||||
|
# Ask about config and data
|
||||||
|
echo ""
|
||||||
|
echo "Config and backup data in ~/.config/x3-backup/ were NOT removed."
|
||||||
|
echo "To remove config: rm -rf ~/.config/x3-backup"
|
||||||
|
echo ""
|
||||||
|
echo "Backup snapshots were NOT removed."
|
||||||
|
echo "To remove backups: check your configured backup destination"
|
||||||
|
echo ""
|
||||||
|
echo "Done."
|
||||||
Reference in New Issue
Block a user