4
* Copyright 2016 Tony George <teejeetech@gmail.com>
6
* This program is free software; you can redistribute it and/or modify
7
* it under the terms of the GNU General Public License as published by
8
* the Free Software Foundation; either version 2 of the License, or
9
* (at your option) any later version.
11
* This program is distributed in the hope that it will be useful,
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
* GNU General Public License for more details.
16
* You should have received a copy of the GNU General Public License
17
* along with this program; if not, write to the Free Software
18
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
30
using TeeJee.FileSystem;
31
using TeeJee.JsonHelper;
32
using TeeJee.ProcessHelper;
33
using TeeJee.GtkHelper;
38
public const string AppName = "Timeshift RSYNC";
39
public const string AppShortName = "timeshift";
40
public const string AppVersion = "16.10.6";
41
public const string AppAuthor = "Tony George";
42
public const string AppAuthorEmail = "teejeetech@gmail.com";
44
const string GETTEXT_PACKAGE = "";
45
const string LOCALE_DIR = "/usr/share/locale";
47
extern void exit(int exit_code);
49
public class Main : GLib.Object{
50
public string app_path = "";
51
public string share_folder = "";
52
public string rsnapshot_conf_path = "";
53
public string app_conf_path = "";
54
public bool first_run = false;
56
public string backup_uuid = "";
57
public string backup_parent_uuid = "";
59
public Gee.ArrayList<Device> partitions;
61
public Gee.ArrayList<string> exclude_list_user;
62
public Gee.ArrayList<string> exclude_list_default;
63
public Gee.ArrayList<string> exclude_list_default_extra;
64
public Gee.ArrayList<string> exclude_list_home;
65
public Gee.ArrayList<string> exclude_list_restore;
66
public Gee.ArrayList<AppExcludeEntry> exclude_list_apps;
67
public Gee.ArrayList<MountEntry> mount_list;
68
public Gee.ArrayList<string> exclude_app_names;
70
public SnapshotRepo repo;
73
//private Gee.ArrayList<Device> grub_device_list;
75
public Device sys_root;
76
public Device sys_boot;
77
public Device sys_efi;
78
public Device sys_home;
80
public string mount_point_restore = "";
81
public string mount_point_app = "/mnt/timeshift";
83
public LinuxDistro current_distro;
84
public bool mirror_system = false;
86
public bool schedule_monthly = false;
87
public bool schedule_weekly = false;
88
public bool schedule_daily = true;
89
public bool schedule_hourly = false;
90
public bool schedule_boot = true;
91
public int count_monthly = 2;
92
public int count_weekly = 3;
93
public int count_daily = 5;
94
public int count_hourly = 6;
95
public int count_boot = 5;
97
public string app_mode = "";
99
//global vars for controlling threads
100
public bool thr_success = false;
102
public bool thread_estimate_running = false;
103
public bool thread_estimate_success = false;
105
public bool thread_restore_running = false;
106
public bool thread_restore_success = false;
108
public bool thread_delete_running = false;
109
public bool thread_delete_success = false;
111
public int thr_retval = -1;
112
public string thr_arg1 = "";
113
public bool thr_timeout_active = false;
114
public string thr_timeout_cmd = "";
116
public int startup_delay_interval_mins = 10;
117
public int retain_snapshots_max_days = 200;
119
public int64 snapshot_location_free_space = 0;
121
public const int SHIELD_ICON_SIZE = 64;
122
public const int64 MIN_FREE_SPACE = 1 * GB;
123
public static int64 first_snapshot_size = 0;
124
public static int64 first_snapshot_count = 0;
126
public string log_dir = "";
127
public string log_file = "";
128
public AppLock app_lock;
130
public Gee.ArrayList<Snapshot> delete_list;
132
public Snapshot snapshot_to_delete;
133
public Snapshot snapshot_to_restore;
134
public Device restore_target;
135
public bool reinstall_grub2 = false;
136
public string grub_device = "";
138
public bool cmd_skip_grub = false;
139
public string cmd_grub_device = "";
140
public string cmd_target_device = "";
141
public string cmd_backup_device = "";
142
public string cmd_snapshot = "";
143
public bool cmd_confirm = false;
144
public bool cmd_verbose = true;
146
public string progress_text = "";
147
public int snapshot_list_start_index = 0;
149
public RsyncTask task;
150
public DeleteFileTask delete_file_task;
154
public static int main (string[] args) {
157
LOG_TIMESTAMP = false;
160
if (args.length > 1) {
161
switch (args[1].down()) {
164
stdout.printf (Main.help_message ());
171
init_tmp(AppShortName);
176
* init_tmp() will fail if timeshift is run as normal user
177
* logging will be disabled temporarily so that the error is not displayed to user
181
var map = Device.get_mounted_filesystems_using_mtab();
182
foreach(Device pi in map.values){
183
log_msg(pi.description_full());
188
App = new Main(args);
190
bool success = App.start_application(args);
193
return (success) ? 0 : 1;
196
private static void set_locale(){
197
log_debug("setting locale...");
198
Intl.setlocale(GLib.LocaleCategory.MESSAGES, "timeshift");
199
Intl.textdomain(GETTEXT_PACKAGE);
200
Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8");
201
Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR);
204
public Main(string[] args){
210
//parse arguments (initial) ------------
212
parse_arguments(args);
214
//check for admin access before logging is initialized
215
//since writing to log directory requires admin access
217
if (!user_is_admin()){
218
msg = _("TimeShift needs admin access to backup and restore system files.") + "\n";
219
msg += _("Please run the application as admin (using 'sudo' or 'su')");
224
string title = _("Admin Access Required");
225
gtk_messagebox(title, msg, null, true);
231
//init log ------------------
234
string suffix = (app_mode.length == 0) ? "_gui" : "_" + app_mode;
236
DateTime now = new DateTime.now_local();
237
log_dir = "/var/log/timeshift";
238
log_file = path_combine(log_dir,
239
"%s_%s.log".printf(now.format("%Y-%m-%d_%H-%M-%S"), suffix));
241
var file = File.new_for_path (log_dir);
242
if (!file.query_exists ()) {
243
file.make_directory_with_parents();
246
file = File.new_for_path (log_file);
247
if (file.query_exists ()) {
251
dos_log = new DataOutputStream (file.create(FileCreateFlags.REPLACE_DESTINATION));
252
if ((app_mode == "")||(LOG_DEBUG)){
253
log_msg(_("Session log file") + ": %s".printf(log_file));
257
log_error (e.message);
261
log_msg(_("Running") + " %s v%s".printf(AppName, AppVersion));
263
//get Linux distribution info -----------------------
265
this.current_distro = LinuxDistro.get_dist_info("/");
266
log_msg(_("Distribution") + ": " + current_distro.full_name());
267
log_msg("DIST_ID" + ": " + current_distro.dist_id);
269
//check dependencies ---------------------
272
if (!check_dependencies(out message)){
274
string title = _("Missing Dependencies");
275
gtk_messagebox(title, message, null, true);
280
//check and create lock ------------------
282
app_lock = new AppLock();
284
if (!app_lock.create("timeshift", app_mode)){
286
if (app_lock.lock_message == "backup"){
287
msg = _("Another instance of Timeshift is creating a snapshot.") + "\n";
288
msg += _("Please wait a few minutes and try again.");
291
msg = _("Another instance of timeshift is currently running!") + "\n";
292
msg += _("Please check if you have multiple windows open.") + "\n";
295
string title = _("Scheduled snapshot in progress...");
296
gtk_messagebox(title, msg, null, true);
299
//already logged - do nothing
304
//initialize variables ------------------
306
this.app_path = (File.new_for_path (args[0])).get_parent().get_path ();
307
this.share_folder = "/usr/share";
308
this.app_conf_path = "/etc/timeshift.json";
309
//sys_root and sys_home will be initalized by update_partition_list()
311
//check if running locally -------------
313
string local_exec = args[0];
314
string local_conf = app_path + "/timeshift.json";
315
string local_share = app_path + "/share";
317
var f_local_exec = File.new_for_path(local_exec);
318
if (f_local_exec.query_exists()){
320
var f_local_conf = File.new_for_path(local_conf);
321
if (f_local_conf.query_exists()){
322
this.app_conf_path = local_conf;
325
var f_local_share = File.new_for_path(local_share);
326
if (f_local_share.query_exists()){
327
this.share_folder = local_share;
331
//timeshift is running from system directory - update app_path
332
this.app_path = get_cmd_path("timeshift");
335
//initialize lists -------------------------
337
repo = new SnapshotRepo();
339
mount_list = new Gee.ArrayList<MountEntry>();
340
delete_list = new Gee.ArrayList<Snapshot>();
342
exclude_app_names = new Gee.ArrayList<string>();
343
add_default_exclude_entries();
344
//add_app_exclude_entries();
346
//initialize app --------------------
349
detect_system_devices();
351
//finish initialization --------------
355
task = new RsyncTask();
356
delete_file_task = new DeleteFileTask();
358
log_debug("Main(): ok");
361
public bool start_application(string[] args){
362
bool is_success = true;
364
log_debug("start_application()");
370
log_error(_("Snapshots cannot be created in Live CD mode"));
381
case "list-snapshots":
382
//set backup device from commandline argument if available or prompt user if device is null
384
get_backup_device_from_cmd(false, null);
391
is_success = take_snapshot(false, "", null);
395
is_success = restore_snapshot(null);
403
is_success = delete_all_snapshots();
407
is_success = take_snapshot(true,"",null);
410
case "list-snapshots":
412
if (App.repo.has_snapshots()){
413
log_msg(_("Snapshots on device %s").printf(
414
repo.device.full_name_with_alias) + ":\n");
415
list_snapshots(false);
420
log_msg(_("No snapshots found on device") + " '%s'".printf(repo.device.device));
426
log_msg(_("Devices with Linux file systems") + ":\n");
432
log_debug("Creating MainWindow");
433
//Initialize main window
434
var window = new MainWindow ();
435
window.destroy.connect(Gtk.main_quit);
445
public bool check_dependencies(out string msg){
448
log_debug("check_dependencies()");
450
string[] dependencies = { "rsync","/sbin/blkid","df","mount","umount","fuser","crontab","cp","rm","touch","ln","sync"}; //"shutdown","chroot",
453
foreach(string cmd_tool in dependencies){
454
path = get_cmd_path (cmd_tool);
455
if ((path == null) || (path.length == 0)){
456
msg += " * " + cmd_tool + "\n";
461
msg = _("Commands listed below are not available on this system") + ":\n\n" + msg + "\n";
462
msg += _("Please install required packages and try running TimeShift again");
471
public bool check_btrfs_layout_system(Gtk.Window? win = null){
473
log_debug("check_btrfs_layout_system()");
475
bool supported = check_btrfs_layout(sys_root, sys_home);
478
string msg = _("The system partition has an unsupported subvolume layout.") + " ";
479
msg += _("Only ubuntu-type layouts with @ and @home subvolumes are currently supported.") + "\n\n";
480
msg += _("Application will exit.") + "\n\n";
481
string title = _("Not Supported");
484
gtk_set_busy(false, win);
485
gtk_messagebox(title, msg, win, true);
495
public bool check_btrfs_layout(Device? dev_root, Device? dev_home){
497
bool supported = true; // keep true for non-btrfs systems
499
if ((dev_root != null) && (dev_root.fstype == "btrfs")){
501
if ((dev_home != null) && (dev_home.fstype == "btrfs")){
502
supported = supported && check_btrfs_volume(dev_root, "@");
503
supported = supported && check_btrfs_volume(dev_home, "@home");
506
supported = supported && check_btrfs_volume(dev_root, "@,@home");
515
public void add_default_exclude_entries(){
517
exclude_list_user = new Gee.ArrayList<string>();
518
exclude_list_default = new Gee.ArrayList<string>();
519
exclude_list_default_extra = new Gee.ArrayList<string>();
520
exclude_list_home = new Gee.ArrayList<string>();
521
exclude_list_restore = new Gee.ArrayList<string>();
522
exclude_list_apps = new Gee.ArrayList<AppExcludeEntry>();
524
partitions = new Gee.ArrayList<Device>();
526
//default exclude entries -------------------
528
exclude_list_default.add("/dev/*");
529
exclude_list_default.add("/proc/*");
530
exclude_list_default.add("/sys/*");
531
exclude_list_default.add("/media/*");
532
exclude_list_default.add("/mnt/*");
533
exclude_list_default.add("/tmp/*");
534
exclude_list_default.add("/run/*");
535
exclude_list_default.add("/var/run/*");
536
exclude_list_default.add("/var/lock/*");
537
exclude_list_default.add("/lost+found");
538
exclude_list_default.add("/timeshift/*");
539
exclude_list_default.add("/data/*");
540
exclude_list_default.add("/cdrom/*");
542
exclude_list_default.add("/root/.thumbnails");
543
exclude_list_default.add("/root/.cache");
544
exclude_list_default.add("/root/.gvfs");
545
exclude_list_default.add("/root/.local/share/Trash");
547
exclude_list_default.add("/home/*/.thumbnails");
548
exclude_list_default.add("/home/*/.cache");
549
exclude_list_default.add("/home/*/.gvfs");
550
exclude_list_default.add("/home/*/.local/share/Trash");
552
//default extra ------------------
554
exclude_list_default_extra.add("/root/.mozilla/firefox/*.default/Cache");
555
exclude_list_default_extra.add("/root/.mozilla/firefox/*.default/OfflineCache");
556
exclude_list_default_extra.add("/root/.opera/cache");
557
exclude_list_default_extra.add("/root/.kde/share/apps/kio_http/cache");
558
exclude_list_default_extra.add("/root/.kde/share/cache/http");
560
exclude_list_default_extra.add("/home/*/.mozilla/firefox/*.default/Cache");
561
exclude_list_default_extra.add("/home/*/.mozilla/firefox/*.default/OfflineCache");
562
exclude_list_default_extra.add("/home/*/.opera/cache");
563
exclude_list_default_extra.add("/home/*/.kde/share/apps/kio_http/cache");
564
exclude_list_default_extra.add("/home/*/.kde/share/cache/http");
566
//default home ----------------
568
exclude_list_home.add("+ /root/.**");
569
exclude_list_home.add("/root/**");
570
exclude_list_home.add("+ /home/*/.**");
571
exclude_list_home.add("/home/*/**");
574
Most web browsers store their cache under ~/.cache and /tmp
575
These files will be excluded by the entries for ~/.cache and /tmp
576
There is no need to add special entries.
578
~/.cache/google-chrome -- Google Chrome
579
~/.cache/chromium -- Chromium
580
~/.cache/epiphany-browser -- Epiphany
581
~/.cache/midori/web -- Midori
582
/var/tmp/kdecache-$USER/http -- Rekonq
587
public void add_app_exclude_entries(){
588
AppExcludeEntry.clear();
593
string cmd = "echo ${SUDO_USER:-$(whoami)}";
597
ret_val = exec_script_sync(cmd, out std_out, out std_err);
599
if ((std_out == null) || (std_out.length == 0)){
604
user_name = std_out.strip();
605
home = "/home/%s".printf(user_name);
608
if ((sys_root == null)
609
|| ((restore_target.device != sys_root.device)
610
&& (restore_target.uuid != sys_root.uuid))){
612
home = mount_point_restore + home;
615
if ((sys_root == null)
616
|| ((restore_target.device != sys_root.device)
617
&& (restore_target.uuid != sys_root.uuid))){
619
home = mount_point_restore + home;
622
AppExcludeEntry.add_app_exclude_entries_from_path(home);
624
exclude_list_apps = AppExcludeEntry.get_apps_list(exclude_app_names);
627
public Gee.ArrayList<string> create_exclude_list_for_backup(){
628
var list = new Gee.ArrayList<string>();
630
//add default entries
631
foreach(string path in exclude_list_default){
632
if (!list.contains(path)){
637
//add default extra entries
638
foreach(string path in exclude_list_default_extra){
639
if (!list.contains(path)){
644
//add user entries from current settings
645
foreach(string path in exclude_list_user){
646
if (!list.contains(path)){
652
foreach(string path in exclude_list_home){
653
if (!list.contains(path)){
658
string timeshift_path = "/timeshift/*";
659
if (!list.contains(timeshift_path)){
660
list.add(timeshift_path);
666
public Gee.ArrayList<string> create_exclude_list_for_restore(){
668
exclude_list_restore.clear();
670
//add default entries
671
foreach(string path in exclude_list_default){
672
if (!exclude_list_restore.contains(path)){
673
exclude_list_restore.add(path);
678
//add default_extra entries
679
foreach(string path in exclude_list_default_extra){
680
if (!exclude_list_restore.contains(path)){
681
exclude_list_restore.add(path);
687
foreach(var entry in exclude_list_apps){
689
foreach(var pattern in entry.patterns){
690
if (!exclude_list_restore.contains(pattern)){
691
exclude_list_restore.add(pattern);
697
//add user entries from current settings
698
foreach(string path in exclude_list_user){
699
if (!exclude_list_restore.contains(path) && !exclude_list_home.contains(path)){
700
exclude_list_restore.add(path);
704
//add user entries from snapshot exclude list
705
if (snapshot_to_restore != null){
706
string list_file = path_combine(snapshot_to_restore.path, "exclude.list");
707
if (file_exists(list_file)){
708
foreach(string path in file_read(list_file).split("\n")){
709
if (!exclude_list_restore.contains(path) && !exclude_list_home.contains(path)){
710
exclude_list_restore.add(path);
717
foreach(string path in exclude_list_home){
718
if (!exclude_list_restore.contains(path)){
719
exclude_list_restore.add(path);
723
string timeshift_path = "/timeshift/*";
724
if (!exclude_list_restore.contains(timeshift_path)){
725
exclude_list_restore.add(timeshift_path);
728
return exclude_list_restore;
731
public bool save_exclude_list_for_backup(string output_path){
733
var list = create_exclude_list_for_backup();
736
foreach(var pattern in list){
737
if (pattern.strip().length > 0){
738
txt += "%s\n".printf(pattern);
742
string list_file = path_combine(output_path, "exclude.list");
743
return file_write(list_file, txt);
746
public bool save_exclude_list_for_restore(string output_path){
748
var list = create_exclude_list_for_restore();
750
log_debug("Exclude list -------------");
753
foreach(var pattern in list){
754
if (pattern.strip().length > 0){
755
txt += "%s\n".printf(pattern);
760
string list_file = path_combine(output_path, "exclude-restore.list");
761
return file_write(list_file, txt);
764
public void save_exclude_list_selections(){
766
// add new selected items
767
foreach(var entry in App.exclude_list_apps){
768
if (entry.enabled && !App.exclude_app_names.contains(entry.name)){
769
App.exclude_app_names.add(entry.name);
770
log_debug("add app name: %s".printf(entry.name));
774
// remove item only if present in current list and un-selected
775
foreach(var entry in App.exclude_list_apps){
776
if (!entry.enabled && App.exclude_app_names.contains(entry.name)){
777
App.exclude_app_names.remove(entry.name);
778
log_debug("remove app name: %s".printf(entry.name));
782
App.exclude_app_names.sort((a,b) => {
783
return Posix.strcmp(a,b);
789
public static string help_message (){
790
string msg = "\n" + AppName + " v" + AppVersion + " by Tony George (teejeetech@gmail.com)" + "\n";
794
msg += " timeshift --list-{snapshots|devices} [OPTIONS]\n";
795
msg += " timeshift --backup[-now] [OPTIONS]\n";
796
msg += " timeshift --restore [OPTIONS]\n";
797
msg += " timeshift --delete-[all] [OPTIONS]\n";
799
msg += _("Options") + ":\n";
801
msg += _("List") + ":\n";
802
msg += " --list[-snapshots] " + _("List snapshots") + "\n";
803
msg += " --list-devices " + _("List devices") + "\n";
805
msg += _("Backup") + ":\n";
806
msg += " --backup " + _("Take scheduled backup") + "\n";
807
msg += " --backup-now " + _("Take on-demand backup") + "\n";
809
msg += _("Restore") + ":\n";
810
msg += " --restore " + _("Restore snapshot") + "\n";
811
msg += " --clone " + _("Clone current system") + "\n";
812
msg += " --snapshot <name> " + _("Specify snapshot to restore") + "\n";
813
msg += " --target[-device] <device> " + _("Specify target device") + "\n";
814
msg += " --grub[-device] <device> " + _("Specify device for installing GRUB2 bootloader") + "\n";
815
msg += " --skip-grub " + _("Skip GRUB2 reinstall") + "\n";
817
msg += _("Delete") + ":\n";
818
msg += " --delete " + _("Delete snapshot") + "\n";
819
msg += " --delete-all " + _("Delete all snapshots") + "\n";
821
msg += _("Global") + ":\n";
822
msg += " --backup-device <device> " + _("Specify backup device") + "\n";
823
msg += " --yes " + _("Answer YES to all confirmation prompts") + "\n";
824
msg += " --debug " + _("Show additional debug messages") + "\n";
825
msg += " --verbose " + _("Show rsync output (default)") + "\n";
826
msg += " --quiet " + _("Hide rsync output") + "\n";
827
msg += " --help " + _("Show all options") + "\n";
830
msg += _("Examples") + ":\n";
832
msg += "timeshift --list\n";
833
msg += "timeshift --list --backup-device /dev/sda1\n";
834
msg += "timeshift --backup-now \n";
835
msg += "timeshift --restore \n";
836
msg += "timeshift --restore --snapshot '2014-10-12_16-29-08' --target /dev/sda1 --skip-grub\n";
837
msg += "timeshift --delete --snapshot '2014-10-12_16-29-08'\n";
838
msg += "timeshift --delete-all \n";
841
msg += _("Notes") + ":\n";
843
msg += " 1) --backup will take a snapshot only if a scheduled snapshot is due\n";
844
msg += " 2) --backup-now will take an immediate (forced) snapshot\n";
845
msg += " 3) --backup will not take snapshots till first snapshot is taken with --backup-now\n";
846
msg += " 4) Use --restore without other options to select options interactively\n";
847
msg += " 5) UUID can be specified instead of device name\n";
852
private void parse_arguments(string[] args){
854
log_debug("parse_arguments()");
856
for (int k = 1; k < args.length; k++) // Oth arg is app path
858
switch (args[k].down()){
860
LOG_TIMESTAMP = false;
866
LOG_TIMESTAMP = false;
872
LOG_TIMESTAMP = false;
874
app_mode = "delete-all";
878
LOG_TIMESTAMP = false;
880
mirror_system = false;
881
app_mode = "restore";
885
LOG_TIMESTAMP = false;
887
mirror_system = true;
888
app_mode = "restore";
892
LOG_TIMESTAMP = false;
894
app_mode = "ondemand";
898
cmd_skip_grub = true;
914
case "--grub-device":
915
reinstall_grub2 = true;
916
cmd_grub_device = args[++k];
920
case "--target-device":
921
cmd_target_device = args[++k];
924
case "--backup-device":
925
cmd_backup_device = args[++k];
929
case "--snapshot-name":
930
cmd_snapshot = args[++k];
939
case "--list-snapshots":
940
app_mode = "list-snapshots";
941
LOG_TIMESTAMP = false;
945
case "--list-devices":
946
app_mode = "list-devices";
947
LOG_TIMESTAMP = false;
952
LOG_TIMESTAMP = false;
953
log_error(_("Invalid command line arguments") + ": %s".printf(args[k]), true);
954
log_msg(Main.help_message());
960
/* LOG_ENABLE = false; disables all console output
961
* LOG_TIMESTAMP = false; disables the timestamp prepended to every line in terminal output
962
* LOG_DEBUG = false; disables additional console messages
963
* LOG_COMMANDS = true; enables printing of all commands on terminal
968
LOG_TIMESTAMP = true;
975
private void list_snapshots(bool paginate){
977
for(int index = 0; index < repo.snapshots.size; index++){
978
if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + 10))){
983
string[,] grid = new string[count+1,5];
984
bool[] right_align = { false, false, false, false, false};
988
grid[row, ++col] = _("Num");
989
grid[row, ++col] = "";
990
grid[row, ++col] = _("Name");
991
grid[row, ++col] = _("Tags");
992
grid[row, ++col] = _("Description");
995
for(int index = 0; index < repo.snapshots.size; index++){
996
Snapshot bak = repo.snapshots[index];
997
if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + 10))){
999
grid[row, ++col] = "%d".printf(index);
1000
grid[row, ++col] = ">";
1001
grid[row, ++col] = "%s".printf(bak.name);
1002
grid[row, ++col] = "%s".printf(bak.taglist_short);
1003
grid[row, ++col] = "%s".printf(bak.description);
1008
print_grid(grid, right_align);
1011
private void list_devices(Gee.ArrayList<Device> device_list){
1012
string[,] grid = new string[device_list.size+1,6];
1013
bool[] right_align = { false, false, false, true, true, false};
1017
grid[row, ++col] = _("Num");
1018
grid[row, ++col] = "";
1019
grid[row, ++col] = _("Device");
1020
//grid[row, ++col] = _("UUID");
1021
grid[row, ++col] = _("Size");
1022
grid[row, ++col] = _("Type");
1023
grid[row, ++col] = _("Label");
1026
foreach(var pi in device_list) {
1028
grid[row, ++col] = "%d".printf(row - 1);
1029
grid[row, ++col] = ">";
1030
grid[row, ++col] = "%s".printf(pi.full_name_with_alias);
1031
//grid[row, ++col] = "%s".printf(pi.uuid);
1032
grid[row, ++col] = "%s".printf((pi.size_bytes > 0) ? "%s".printf(pi.size) : "?? GB");
1033
grid[row, ++col] = "%s".printf(pi.fstype);
1034
grid[row, ++col] = "%s".printf(pi.label);
1038
print_grid(grid, right_align);
1041
private Gee.ArrayList<Device> list_all_devices(){
1044
var device_list = new Gee.ArrayList<Device>();
1045
foreach(var dev in Device.get_block_devices_using_lsblk()) {
1046
if (dev.has_linux_filesystem()){
1047
device_list.add(dev);
1051
string[,] grid = new string[device_list.size+1,6];
1052
bool[] right_align = { false, false, false, true, true, false};
1056
grid[row, ++col] = _("Num");
1057
grid[row, ++col] = "";
1058
grid[row, ++col] = _("Device");
1059
//grid[row, ++col] = _("UUID");
1060
grid[row, ++col] = _("Size");
1061
grid[row, ++col] = _("Type");
1062
grid[row, ++col] = _("Label");
1065
foreach(var pi in device_list) {
1067
grid[row, ++col] = "%d".printf(row - 1);
1068
grid[row, ++col] = ">";
1069
grid[row, ++col] = "%s".printf(pi.full_name_with_alias);
1070
//grid[row, ++col] = "%s".printf(pi.uuid);
1071
grid[row, ++col] = "%s".printf((pi.size_bytes > 0) ? "%s GB".printf(pi.size) : "?? GB");
1072
grid[row, ++col] = "%s".printf(pi.fstype);
1073
grid[row, ++col] = "%s".printf(pi.label);
1077
print_grid(grid, right_align);
1082
private Gee.ArrayList<Device> list_grub_devices(bool print_to_console = true){
1084
var grub_device_list = new Gee.ArrayList<Device>();
1085
foreach(var dev in Device.get_block_devices_using_lsblk()) {
1086
if (dev.type == "disk"){
1087
grub_device_list.add(dev);
1089
else if (dev.type == "part"){
1090
if (dev.has_linux_filesystem()){
1091
grub_device_list.add(dev);
1097
/*Note: Lists are already sorted. No need to sort again */
1099
string[,] grid = new string[grub_device_list.size+1,4];
1100
bool[] right_align = { false, false, false, false };
1104
grid[row, ++col] = _("Num");
1105
grid[row, ++col] = "";
1106
grid[row, ++col] = _("Device");
1107
grid[row, ++col] = _("Description");
1111
foreach(Device pi in grub_device_list) {
1113
grid[row, ++col] = "%d".printf(row - 1);
1114
grid[row, ++col] = ">";
1115
grid[row, ++col] = "%s".printf(pi.short_name_with_alias);
1117
if (pi.type == "disk"){
1118
desc = "%s".printf(((pi.vendor.length > 0)||(pi.model.length > 0)) ? (pi.vendor + " " + pi.model + " [MBR]") : "");
1121
desc = "%5s, ".printf(pi.fstype);
1122
desc += "%10s".printf((pi.size_bytes > 0) ? "%s GB".printf(pi.size) : "?? GB");
1123
desc += "%s".printf((pi.label.length > 0) ? ", " + pi.label : "");
1125
grid[row, ++col] = "%s".printf(desc);
1129
print_grid(grid, right_align);
1131
return grub_device_list;
1134
private void print_grid(string[,] grid, bool[] right_align, bool has_header = true){
1135
int[] col_width = new int[grid.length[1]];
1137
for(int col=0; col<grid.length[1]; col++){
1138
for(int row=0; row<grid.length[0]; row++){
1139
if (grid[row,col].length > col_width[col]){
1140
col_width[col] = grid[row,col].length;
1145
for(int row=0; row<grid.length[0]; row++){
1146
for(int col=0; col<grid.length[1]; col++){
1147
string fmt = "%" + (right_align[col] ? "+" : "-") + col_width[col].to_string() + "s ";
1148
stdout.printf(fmt.printf(grid[row,col]));
1150
stdout.printf("\n");
1152
if (has_header && (row == 0)){
1153
stdout.printf(string.nfill(78, '-'));
1154
stdout.printf("\n");
1162
public void get_backup_device_from_cmd(bool prompt_if_empty, Gtk.Window? parent_win){
1164
var list = new Gee.ArrayList<Device>();
1165
foreach(var pi in partitions){
1166
if (pi.has_linux_filesystem()){
1171
if (cmd_backup_device.length > 0){
1172
//set backup device from command line argument
1173
var cmd_dev = Device.get_device_by_name(cmd_backup_device);
1174
if (cmd_dev != null){
1175
repo = new SnapshotRepo.from_device(cmd_dev, null);
1176
if (!repo.available()){
1182
log_error(_("Could not find device") + ": '%s'".printf(cmd_backup_device));
1188
if ((repo.device == null) || (prompt_if_empty && (repo.snapshots.size == 0))){
1189
//prompt user for backup device
1192
log_msg(_("Select backup device") + ":\n");
1198
while (dev == null){
1200
if (attempts > 3) { break; }
1202
_("Enter device name or number (a=Abort)") + ": ");
1205
dev = read_stdin_device(list);
1211
log_error(_("Failed to get input from user in 3 attempts"));
1212
log_msg(_("Aborted."));
1217
repo = new SnapshotRepo.from_device(dev, null);
1218
if (!repo.available()){
1226
private Device? read_stdin_device(Gee.ArrayList<Device> device_list){
1227
var counter = new TimeoutCounter();
1228
counter.exit_on_timeout();
1229
string? line = stdin.read_line();
1232
line = (line != null) ? line.strip() : "";
1234
Device selected_device = null;
1236
if (line.down() == "a"){
1237
log_msg(_("Aborted."));
1241
else if ((line == null)||(line.length == 0)){
1242
log_error("Invalid input");
1244
else if (line.contains("/")){
1245
selected_device = Device.get_device_by_name(line);
1246
if (selected_device == null){
1247
log_error("Invalid input");
1251
selected_device = get_device_from_index(device_list, line);
1252
if (selected_device == null){
1253
log_error("Invalid input");
1257
return selected_device;
1260
private Device? read_stdin_device_mounts(Gee.ArrayList<Device> device_list, MountEntry mnt){
1261
var counter = new TimeoutCounter();
1262
counter.exit_on_timeout();
1263
string? line = stdin.read_line();
1266
line = (line != null) ? line.strip() : "";
1268
Device selected_device = null;
1270
if ((line == null)||(line.length == 0)||(line.down() == "c")||(line.down() == "d")){
1273
return restore_target; //root device
1276
return mnt.device; //keep current
1279
else if (line.down() == "a"){
1280
log_msg("Aborted.");
1284
else if ((line.down() == "n")||(line.down() == "r")){
1285
return restore_target; //root device
1287
else if (line.contains("/")){
1288
selected_device = Device.get_device_by_name(line);
1289
if (selected_device == null){
1290
log_error("Invalid input");
1294
selected_device = get_device_from_index(device_list, line);
1295
if (selected_device == null){
1296
log_error("Invalid input");
1300
return selected_device;
1303
private Device? get_device_from_index(Gee.ArrayList<Device> device_list, string index_string){
1305
if (int64.try_parse(index_string, out index)){
1307
foreach(Device pi in device_list) {
1317
private Snapshot read_stdin_snapshot(){
1318
var counter = new TimeoutCounter();
1319
counter.exit_on_timeout();
1320
string? line = stdin.read_line();
1323
line = (line != null) ? line.strip() : "";
1325
Snapshot selected_snapshot = null;
1327
if (line.down() == "a"){
1328
log_msg("Aborted.");
1332
else if (line.down() == "p"){
1333
snapshot_list_start_index -= 10;
1334
if (snapshot_list_start_index < 0){
1335
snapshot_list_start_index = 0;
1338
list_snapshots(true);
1341
else if (line.down() == "n"){
1342
if ((snapshot_list_start_index + 10) < repo.snapshots.size){
1343
snapshot_list_start_index += 10;
1346
list_snapshots(true);
1349
else if (line.contains("_")||line.contains("-")){
1351
log_error("Invalid input");
1353
else if ((line == null)||(line.length == 0)){
1354
log_error("Invalid input");
1358
if (int64.try_parse(line, out index)){
1359
if (index < repo.snapshots.size){
1360
selected_snapshot = repo.snapshots[(int) index];
1363
log_error("Invalid input");
1367
log_error("Invalid input");
1371
return selected_snapshot;
1374
private bool read_stdin_grub_install(){
1375
var counter = new TimeoutCounter();
1376
counter.exit_on_timeout();
1377
string? line = stdin.read_line();
1380
line = (line != null) ? line.strip() : line;
1382
if ((line == null)||(line.length == 0)){
1383
log_error("Invalid input");
1386
else if (line.down() == "a"){
1387
log_msg("Aborted.");
1392
else if (line.down() == "y"){
1393
cmd_skip_grub = false;
1394
reinstall_grub2 = true;
1397
else if (line.down() == "n"){
1398
cmd_skip_grub = true;
1399
reinstall_grub2 = false;
1402
else if ((line == null)||(line.length == 0)){
1403
log_error("Invalid input");
1407
log_error("Invalid input");
1412
private bool read_stdin_restore_confirm(){
1413
var counter = new TimeoutCounter();
1414
counter.exit_on_timeout();
1416
string? line = stdin.read_line();
1419
line = (line != null) ? line.strip() : "";
1421
if ((line.down() == "a")||(line.down() == "n")){
1422
log_msg("Aborted.");
1427
else if ((line == null)||(line.length == 0)){
1428
log_error("Invalid input");
1431
else if (line.down() == "y"){
1435
else if ((line == null)||(line.length == 0)){
1436
log_error("Invalid input");
1440
log_error("Invalid input");
1447
public bool scheduled{
1449
return !live_system()
1450
&& (schedule_boot || schedule_hourly || schedule_daily ||
1451
schedule_weekly || schedule_monthly);
1455
public bool live_system(){
1457
return (sys_root == null);
1462
public bool take_snapshot (
1463
bool is_ondemand, string snapshot_comments, Gtk.Window? parent_win){
1466
bool update_symlinks = false;
1468
string sys_uuid = (sys_root == null) ? "" : sys_root.uuid;
1472
log_debug("checking btrfs volumes on root device...");
1474
if (App.check_btrfs_layout_system() == false){
1478
// create a timestamp
1479
DateTime now = new DateTime.now_local();
1481
log_debug("checking if snapshot device is mounted...");
1483
log_debug("checking snapshot device...");
1486
if (!repo.has_space()){
1488
log_error(repo.status_message);
1489
log_error(repo.status_details + "\n");
1491
// remove invalid snapshots
1492
if (app_mode.length != 0){
1496
// check again ------------
1498
if (!repo.has_space()){
1499
log_error(repo.status_message);
1500
log_error(repo.status_details + "\n");
1505
string snapshot_dir = path_combine(repo.snapshot_location, "timeshift/snapshots");
1507
// create snapshot root if missing
1508
var f = File.new_for_path(snapshot_dir);
1509
if (!f.query_exists()){
1510
log_debug("mkdir: %s".printf(snapshot_dir));
1511
f.make_directory_with_parents();
1516
bool ok = backup_and_rotate ("ondemand",now);
1521
update_symlinks = true;
1524
else if (scheduled){
1525
Snapshot last_snapshot_boot = repo.get_latest_snapshot("boot", sys_uuid);
1526
Snapshot last_snapshot_hourly = repo.get_latest_snapshot("hourly", sys_uuid);
1527
Snapshot last_snapshot_daily = repo.get_latest_snapshot("daily", sys_uuid);
1528
Snapshot last_snapshot_weekly = repo.get_latest_snapshot("weekly", sys_uuid);
1529
Snapshot last_snapshot_monthly = repo.get_latest_snapshot("monthly", sys_uuid);
1531
DateTime dt_sys_boot = now.add_seconds((-1) * get_system_uptime_seconds());
1532
bool take_new = false;
1536
log_msg(_("Boot snapshots are enabled"));
1538
if (last_snapshot_boot == null){
1539
log_msg(_("Last boot snapshot not found"));
1542
else if (last_snapshot_boot.date.compare(dt_sys_boot) < 0){
1543
log_msg(_("Last boot snapshot is older than system start time"));
1547
int hours = (int) ((float) now.difference(last_snapshot_boot.date) / TimeSpan.HOUR);
1548
log_msg(_("Last boot snapshot is %d hours old").printf(hours));
1553
status = backup_and_rotate ("boot",now);
1555
log_error(_("Boot snapshot failed!"));
1559
update_symlinks = true;
1564
if (schedule_hourly){
1566
log_msg(_("Hourly snapshots are enabled"));
1568
if (last_snapshot_hourly == null){
1569
log_msg(_("Last hourly snapshot not found"));
1572
else if (last_snapshot_hourly.date.compare(now.add_hours(-1).add_minutes(1)) < 0){
1573
log_msg(_("Last hourly snapshot is more than 1 hour old"));
1577
int mins = (int) ((float) now.difference(last_snapshot_hourly.date) / TimeSpan.MINUTE);
1578
log_msg(_("Last hourly snapshot is %d minutes old").printf(mins));
1583
status = backup_and_rotate ("hourly",now);
1585
log_error(_("Hourly snapshot failed!"));
1589
update_symlinks = true;
1594
if (schedule_daily){
1596
log_msg(_("Daily snapshots are enabled"));
1598
if (last_snapshot_daily == null){
1599
log_msg(_("Last daily snapshot not found"));
1602
else if (last_snapshot_daily.date.compare(now.add_days(-1).add_minutes(1)) < 0){
1603
log_msg(_("Last daily snapshot is more than 1 day old"));
1607
int hours = (int) ((float) now.difference(last_snapshot_daily.date) / TimeSpan.HOUR);
1608
log_msg(_("Last daily snapshot is %d hours old").printf(hours));
1613
status = backup_and_rotate ("daily",now);
1615
log_error(_("Daily snapshot failed!"));
1619
update_symlinks = true;
1624
if (schedule_weekly){
1626
log_msg(_("Weekly snapshots are enabled"));
1628
if (last_snapshot_weekly == null){
1629
log_msg(_("Last weekly snapshot not found"));
1632
else if (last_snapshot_weekly.date.compare(now.add_weeks(-1).add_minutes(1)) < 0){
1633
log_msg(_("Last weekly snapshot is more than 1 week old"));
1637
int days = (int) ((float) now.difference(last_snapshot_weekly.date) / TimeSpan.DAY);
1638
log_msg(_("Last weekly snapshot is %d days old").printf(days));
1643
status = backup_and_rotate ("weekly",now);
1645
log_error(_("Weekly snapshot failed!"));
1649
update_symlinks = true;
1654
if (schedule_monthly){
1656
log_msg(_("Monthly snapshot are enabled"));
1658
if (last_snapshot_monthly == null){
1659
log_msg(_("Last monthly snapshot not found"));
1662
else if (last_snapshot_monthly.date.compare(now.add_months(-1).add_minutes(1)) < 0){
1663
log_msg(_("Last monthly snapshot is more than 1 month old"));
1667
int days = (int) ((float) now.difference(last_snapshot_monthly.date) / TimeSpan.DAY);
1668
log_msg(_("Last monthly snapshot is %d days old").printf(days));
1673
status = backup_and_rotate ("monthly",now);
1675
log_error(_("Monthly snapshot failed!"));
1679
update_symlinks = true;
1685
log_msg(_("Scheduled snapshots are disabled") + " - " + _("Nothing to do!"));
1689
if (app_mode.length != 0){
1693
if (update_symlinks){
1694
repo.load_snapshots();
1695
repo.create_symlinks();
1701
log_error (e.message);
1708
public bool backup_and_rotate(string tag, DateTime dt_created){
1712
bool backup_taken = false;
1715
var dt_begin = new DateTime.now_local();
1717
string sys_uuid = (sys_root == null) ? "" : sys_root.uuid;
1720
// get system boot time
1721
DateTime now = new DateTime.now_local();
1722
DateTime dt_sys_boot = now.add_seconds((-1) * get_system_uptime_seconds());
1724
// check if we can rotate an existing backup -------------
1726
DateTime dt_filter = null;
1728
if (tag != "ondemand"){
1731
dt_filter = dt_sys_boot;
1737
dt_filter = now.add_hours(-1);
1740
log_error(_("Unknown snapshot type") + ": %s".printf(tag));
1744
// find a recent backup that can be used
1745
Snapshot backup_to_rotate = null;
1746
foreach(var bak in repo.snapshots){
1747
if (bak.date.compare(dt_filter) > 0){
1748
backup_to_rotate = bak;
1753
if (backup_to_rotate != null){
1756
backup_to_rotate.add_tag(tag);
1758
backup_taken = true;
1759
var message = "%s '%s' %s '%s'".printf(
1760
_("Snapshot"), backup_to_rotate.name, _("tagged"), tag);
1767
log_msg("Creating new backup...");
1769
// take new backup ---------------------------------
1771
if (repo.snapshot_location.length == 0){
1772
log_error("Backup location not mounted");
1776
string time_stamp = dt_created.format("%Y-%m-%d_%H-%M-%S");
1777
string snapshot_dir = path_combine(repo.snapshot_location, "timeshift/snapshots");
1778
string snapshot_name = time_stamp;
1779
string snapshot_path = path_combine(snapshot_dir, snapshot_name);
1781
Snapshot snapshot_to_link = null;
1783
dir_create(path_combine(snapshot_path, "/localhost"));
1785
// check if a snapshot was restored recently and use it for linking ---------
1787
string ctl_path = path_combine(snapshot_dir, ".sync-restore");
1788
f = File.new_for_path(ctl_path);
1790
if (f.query_exists()){
1792
// read snapshot name from file
1793
string snap_path = file_read(ctl_path);
1794
string snap_name = file_basename(snap_path);
1796
// find the snapshot that was restored
1797
foreach(var bak in repo.snapshots){
1798
if ((bak.name == snap_name) && (bak.sys_uuid == sys_uuid)){
1800
snapshot_to_link = bak;
1801
// delete the restore-control-file
1808
// get latest snapshot to link if not set -------
1810
if (snapshot_to_link == null){
1811
snapshot_to_link = repo.get_latest_snapshot("", sys_uuid);
1814
string link_from_path = "";
1815
if (snapshot_to_link != null){
1816
log_msg("%s: %s".printf(_("Linking from snapshot"), snapshot_to_link.name));
1817
link_from_path = "%s/localhost/".printf(snapshot_to_link.path);
1820
// save exclude list ----------------
1822
bool ok = save_exclude_list_for_backup(snapshot_path);
1824
string exclude_from_file = path_combine(snapshot_path, "exclude.list");
1827
log_error(_("Failed to save exclude list"));
1831
// rsync file system -------------------
1833
progress_text = _("Synching files with rsync...");
1834
log_msg(progress_text);
1836
var log_file = snapshot_path + "/rsync-log";
1837
file_delete(log_file);
1839
task = new RsyncTask();
1841
task.source_path = "";
1842
task.dest_path = snapshot_path + "/localhost/";
1843
task.link_from_path = link_from_path;
1844
task.exclude_from_file = exclude_from_file;
1845
task.rsync_log_file = log_file;
1846
task.prg_count_total = Main.first_snapshot_count;
1848
task.relative = true;
1849
task.verbose = true;
1850
task.delete_extra = true;
1851
task.delete_excluded = true;
1852
task.delete_after = false;
1854
if (app_mode.length > 0){
1856
task.io_nice = true;
1861
while (task.status == AppStatus.RUNNING){
1866
if (task.total_size == 0){
1867
log_error(_("rsync returned an error"));
1868
log_error(_("Failed to create new snapshot"));
1872
// write control file
1873
write_snapshot_control_file(snapshot_path, dt_created, tag);
1876
progress_text = _("Parsing log file...");
1877
log_msg(progress_text);
1878
var task = new RsyncTask();
1879
task.parse_log(log_file);
1881
// finish ------------------------------
1883
var dt_end = new DateTime.now_local();
1884
TimeSpan elapsed = dt_end.difference(dt_begin);
1885
long seconds = (long)(elapsed * 1.0 / TimeSpan.SECOND);
1887
var message = "%s (%lds)".printf(_("Snapshot saved successfully"), seconds);
1890
OSDNotify.notify_send("TimeShift", message, 10000, "low");
1892
message = "%s '%s' %s '%s'".printf(
1893
_("Snapshot"), snapshot_name, _("tagged"), tag);
1896
repo.load_snapshots();
1900
log_error (e.message);
1907
public Snapshot write_snapshot_control_file(
1908
string snapshot_path, DateTime dt_created, string tag){
1910
var ctl_path = snapshot_path + "/info.json";
1911
var config = new Json.Object();
1913
config.set_string_member("created", dt_created.to_utc().to_unix().to_string());
1914
config.set_string_member("sys-uuid", sys_root.uuid);
1915
config.set_string_member("sys-distro", current_distro.full_name());
1916
config.set_string_member("app-version", AppVersion);
1917
config.set_string_member("tags", tag);
1918
config.set_string_member("comments", "");
1920
var json = new Json.Generator();
1923
var node = new Json.Node(NodeType.OBJECT);
1924
node.set_object(config);
1925
json.set_root(node);
1928
var f = File.new_for_path(ctl_path);
1929
if (f.query_exists()){
1933
json.to_file(ctl_path);
1935
log_error (e.message);
1938
return (new Snapshot(snapshot_path));
1941
// delete from terminal
1943
public void delete_snapshot(Snapshot? snapshot = null){
1947
// set snapshot -----------------------------------------------
1949
if (app_mode != ""){ //command-line mode
1951
if (cmd_snapshot.length > 0){
1953
//check command line arguments
1955
foreach(var bak in repo.snapshots) {
1956
if (bak.name == cmd_snapshot){
1957
snapshot_to_delete = bak;
1965
log_error(_("Could not find snapshot") + ": '%s'".printf(cmd_snapshot));
1970
//prompt user for snapshot
1971
if (snapshot_to_delete == null){
1973
if (repo.snapshots.size == 0){
1974
log_msg(_("No snapshots found on device") +
1975
" '%s'".printf(repo.device.device));
1980
log_msg(_("Select snapshot to delete") + ":\n");
1981
list_snapshots(true);
1985
while (snapshot_to_delete == null){
1987
if (attempts > 3) { break; }
1988
stdout.printf(_("Enter snapshot number (a=Abort, p=Previous, n=Next)") + ": ");
1990
snapshot_to_delete = read_stdin_snapshot();
1994
if (snapshot_to_delete == null){
1995
log_error(_("Failed to get input from user in 3 attempts"));
1996
log_msg(_("Aborted."));
2003
if (snapshot_to_delete == null){
2005
log_error(_("Snapshot to delete not specified!"));
2009
snapshot_to_delete.remove(true);
2012
public bool delete_all_snapshots(){
2013
return repo.remove_all();
2018
public void delete_begin(){
2020
log_debug("delete_begin()");
2023
thread_delete_running = true;
2024
thread_delete_success = false;
2025
Thread.create<void> (delete_thread, true);
2027
//new Thread<bool> ("", delete_thread);
2029
log_debug("delete_begin(): thread created");
2032
thread_delete_running = false;
2033
thread_delete_success = false;
2034
log_error (e.message);
2038
public void delete_thread(){
2040
log_debug("delete_thread()");
2042
foreach(var bak in delete_list){
2043
bak.mark_for_deletion();
2046
while (delete_list.size > 0){
2048
var bak = delete_list[0];
2049
bak.mark_for_deletion(); // mark for deletion again since initial list may have changed
2051
App.delete_file_task = bak.delete_file_task;
2052
App.delete_file_task.prg_count_total = Main.first_snapshot_count;
2054
bak.remove(true); // wait till complete
2056
if (App.delete_file_task.status != AppStatus.CANCELLED){
2058
var message = "%s '%s' (%s)".printf(
2059
_("Removed"), bak.name, App.delete_file_task.stat_time_elapsed);
2063
OSDNotify.notify_send("TimeShift", message, 10000, "low");
2065
delete_list.remove(bak);
2069
thread_delete_running = false;
2070
thread_delete_success = false;
2072
//return thread_delete_success;
2077
public void init_mount_list(){
2079
log_debug("Main: init_mount_list()");
2083
Gee.ArrayList<FsTabEntry> fstab_list = null;
2084
Gee.ArrayList<CryptTabEntry> crypttab_list = null;
2087
string fstab_path = "/etc/fstab";
2088
fstab_list = FsTabEntry.read_file(fstab_path);
2089
string cryttab_path = "/etc/crypttab";
2090
crypttab_list = CryptTabEntry.read_file(cryttab_path);
2093
fstab_list = snapshot_to_restore.fstab_list;
2094
crypttab_list = snapshot_to_restore.cryttab_list;
2097
bool root_found = false;
2098
bool boot_found = false;
2099
bool home_found = false;
2100
restore_target = null;
2102
foreach(var mnt in fstab_list){
2104
// skip mounting for non-system devices
2106
if (!mnt.is_for_system_directory()){
2110
// find device by name or uuid
2112
Device mnt_dev = null;
2113
if (mnt.device_uuid.length > 0){
2114
mnt_dev = Device.get_device_by_uuid(mnt.device_uuid);
2117
mnt_dev = Device.get_device_by_name(mnt.device);
2120
// replace mapped name with parent device
2122
if (mnt_dev == null){
2125
Note: This is required since the mapped name may be different on running system.
2126
Since we don't have the same mapped name, we cannot resolve the device without
2127
identifying the parent partition
2130
if (mnt.device.has_prefix("/dev/mapper/")){
2131
string mapped_name = mnt.device.replace("/dev/mapper/","");
2132
foreach(var entry in crypttab_list){
2133
if (entry.mapped_name == mapped_name){
2134
mnt.device = entry.device;
2140
// try again - find device by name or uuid
2142
if (mnt.device_uuid.length > 0){
2143
mnt_dev = Device.get_device_by_uuid(mnt.device_uuid);
2146
mnt_dev = Device.get_device_by_name(mnt.device);
2150
if (mnt_dev != null){
2152
log_debug("added: dev: %s, path: %s, options: %s".printf(
2153
mnt_dev.device, mnt.mount_point, mnt.options));
2155
mount_list.add(new MountEntry(mnt_dev, mnt.mount_point, mnt.options));
2157
if (mnt.mount_point == "/"){
2158
restore_target = mnt_dev;
2162
log_debug("missing: dev: %s, path: %s, options: %s".printf(
2163
mnt.device, mnt.mount_point, mnt.options));
2165
mount_list.add(new MountEntry(null, mnt.mount_point, mnt.options));
2168
if (mnt.mount_point == "/"){
2171
if (mnt.mount_point == "/boot"){
2174
if (mnt.mount_point == "/home"){
2180
mount_list.add(new MountEntry(null, "/", "")); // add root entry
2184
mount_list.add(new MountEntry(null, "/boot", "")); // add boot entry
2188
mount_list.add(new MountEntry(null, "/home", "")); // add home entry
2192
While cloning the system, /boot is the only mount point that
2193
we will leave unchanged (to avoid encrypted systems from breaking).
2194
All other mounts like /home will be defaulted to target device
2195
(to prevent the "cloned" system from using the original device)
2198
if (App.mirror_system){
2199
restore_target = null;
2200
foreach (var entry in mount_list){
2201
// user should select another device
2202
entry.device = null;
2206
foreach(var mnt in mount_list){
2207
if (mnt.device != null){
2208
log_debug("Entry: %s -> %s".printf(mnt.device.device, mnt.mount_point));
2211
log_debug("Entry: null -> %s".printf(mnt.mount_point));
2215
// sort - parent mountpoints will be placed above children
2216
mount_list.sort((a,b) => {
2217
return strcmp(a.mount_point, b.mount_point);
2220
log_debug("Main: init_mount_list(): exit");
2223
public bool restore_snapshot(Gtk.Window? parent_win){
2226
// set snapshot device --------------------------------
2228
if (!mirror_system){
2230
if (repo.device != null){
2231
//print snapshot_device name
2232
log_msg(string.nfill(78, '*'));
2233
log_msg(_("Backup Device") + ": %s".printf(repo.device.device));
2234
log_msg(string.nfill(78, '*'));
2238
log_error(_("Backup device not specified!"));
2243
// set snapshot ----------------------------------------
2245
if (!mirror_system){
2247
if (app_mode != ""){ //command-line mode
2249
if (cmd_snapshot.length > 0){
2251
//check command line arguments
2253
foreach(var bak in repo.snapshots) {
2254
if (bak.name == cmd_snapshot){
2255
snapshot_to_restore = bak;
2263
log_error(_("Could not find snapshot") + ": '%s'".printf(cmd_snapshot));
2268
//prompt user for snapshot
2269
if (snapshot_to_restore == null){
2271
if (!repo.has_snapshots()){
2272
log_error(_("No snapshots found on device") + ": '%s'".printf(repo.device.device));
2277
log_msg(_("Select snapshot to restore") + ":\n");
2278
list_snapshots(true);
2282
while (snapshot_to_restore == null){
2284
if (attempts > 3) { break; }
2285
stdout.printf(_("Enter snapshot number (a=Abort, p=Previous, n=Next)") + ": ");
2287
snapshot_to_restore = read_stdin_snapshot();
2291
if (snapshot_to_restore == null){
2292
log_error(_("Failed to get input from user in 3 attempts"));
2293
log_msg(_("Aborted."));
2300
if ((snapshot_to_restore != null) && (snapshot_to_restore.marked_for_deletion)){
2301
log_error(_("Invalid Snapshot"));
2302
log_error(_("Selected snapshot is marked for deletion"));
2306
if (snapshot_to_restore != null){
2307
//print snapshot name
2308
log_msg(string.nfill(78, '*'));
2309
log_msg(_("Snapshot") + ": %s ~ %s".printf(
2310
snapshot_to_restore.name, snapshot_to_restore.description));
2311
log_msg(string.nfill(78, '*'));
2315
log_error(_("Snapshot to restore not specified!"));
2320
// init mounts ---------------
2322
if (app_mode != ""){
2326
// remove mount points which will remain on root fs
2327
for(int i = App.mount_list.size-1; i >= 0; i--){
2329
var entry = App.mount_list[i];
2331
if (entry.device == null){
2332
App.mount_list.remove(entry);
2335
if (entry.mount_point == "/"){
2336
App.restore_target = entry.device;
2341
if (app_mode != ""){ //command line mode
2343
// set target device from cmd argument
2344
if (cmd_target_device.length > 0){
2346
//check command line arguments
2348
foreach(Device pi in partitions) {
2349
if (!pi.has_linux_filesystem()) { continue; }
2350
if ((pi.device == cmd_target_device)||((pi.uuid == cmd_target_device))){
2351
restore_target = pi;
2356
foreach(string symlink in pi.symlinks){
2357
if (symlink == cmd_target_device){
2358
restore_target = pi;
2363
if (found){ break; }
2369
log_error(_("Could not find device") + ": '%s'".printf(cmd_target_device));
2377
// select devices in mount_list --------------------
2379
log_debug("Selecting devices for mount points");
2381
if (app_mode != ""){ //command line mode
2383
for(int i = 0; i < mount_list.size; i++){
2384
MountEntry mnt = mount_list[i];
2386
string default_device = "";
2388
log_debug("selecting: %s".printf(mnt.mount_point));
2390
// no need to ask user to map remaining devices if restoring same system
2391
if ((restore_target != null) && (sys_root != null)
2392
&& (restore_target.uuid == sys_root.uuid)){
2398
default_device = (restore_target != null) ? restore_target.device : "";
2401
if (mnt.device != null){
2402
default_device = mnt.device.device;
2405
default_device = (restore_target != null) ? restore_target.device : "";
2409
//prompt user for device
2412
log_msg(_("Select '%s' device (default = %s)").printf(
2413
mnt.mount_point, default_device) + ":\n");
2414
var device_list = list_all_devices();
2418
while (dev == null){
2420
if (attempts > 3) { break; }
2423
_("[a = Abort, d = Default (%s), r = Root device]").printf(default_device) + "\n\n");
2426
_("Enter device name or number")
2430
dev = read_stdin_device_mounts(device_list, mnt);
2435
log_error(_("Failed to get input from user in 3 attempts"));
2436
log_msg(_("Aborted."));
2444
log_debug("selected: %s".printf(dev.uuid));
2448
if (mnt.mount_point == "/"){
2449
restore_target = dev;
2452
log_msg(string.nfill(78, '*'));
2454
if ((mnt.mount_point != "/")
2455
&& (restore_target != null)
2456
&& (dev.device == restore_target.device)){
2458
log_msg(_("'%s' will be on root device").printf(mnt.mount_point), true);
2461
log_msg(_("'%s' will be on '%s'").printf(
2462
mnt.mount_point, mnt.device.short_name_with_alias), true);
2464
//log_debug("UUID=%s".printf(restore_target.uuid));
2466
log_msg(string.nfill(78, '*'));
2472
// mount selected devices ---------------------------------------
2474
log_debug("Mounting selected devices");
2476
if (restore_target != null){
2477
if (app_mode != ""){ //commandline mode
2478
if ((sys_root == null) || (restore_target.uuid != sys_root.uuid)){
2479
//mount target device and other devices
2480
bool status = mount_target_device(null);
2481
if (status == false){
2487
//mounting is already done
2492
log_error(_("Target device not specified!"));
2496
// set grub device -----------------------------------------------
2498
log_debug("Setting grub device");
2500
if (app_mode != ""){ //command line mode
2502
if (cmd_grub_device.length > 0){
2504
log_debug("Grub device is specified as command argument");
2506
//check command line arguments
2508
var device_list = list_grub_devices(false);
2510
foreach(Device dev in device_list) {
2512
if ((dev.device == cmd_grub_device)
2513
||((dev.uuid.length > 0) && (dev.uuid == cmd_grub_device))){
2515
grub_device = dev.device;
2520
if (dev.type == "part"){
2521
foreach(string symlink in dev.symlinks){
2522
if (symlink == cmd_grub_device){
2523
grub_device = dev.device;
2528
if (found){ break; }
2535
log_error(_("Could not find device") + ": '%s'".printf(cmd_grub_device));
2543
reinstall_grub2 = true;
2546
if ((cmd_skip_grub == false) && (reinstall_grub2 == false)){
2550
while ((cmd_skip_grub == false) && (reinstall_grub2 == false)){
2552
if (attempts > 3) { break; }
2553
stdout.printf(_("Re-install GRUB2 bootloader? (y/n)") + ": ");
2555
read_stdin_grub_install();
2558
if ((cmd_skip_grub == false) && (reinstall_grub2 == false)){
2559
log_error(_("Failed to get input from user in 3 attempts"));
2560
log_msg(_("Aborted."));
2567
if ((reinstall_grub2) && (grub_device.length == 0)){
2570
log_msg(_("Select GRUB device") + ":\n");
2571
var device_list = list_grub_devices();
2575
while (grub_device.length == 0){
2578
if (attempts > 3) { break; }
2580
stdout.printf(_("Enter device name or number (a=Abort)") + ": ");
2583
// TODO: provide option for default boot device
2585
var list = new Gee.ArrayList<Device>();
2586
foreach(var pi in partitions){
2587
if (pi.has_linux_filesystem()){
2592
Device dev = read_stdin_device(device_list);
2593
if (dev != null) { grub_device = dev.device; }
2598
if (grub_device.length == 0){
2600
log_error(_("Failed to get input from user in 3 attempts"));
2601
log_msg(_("Aborted."));
2607
if ((reinstall_grub2) && (grub_device.length > 0)){
2609
log_msg(string.nfill(78, '*'));
2610
log_msg(_("GRUB Device") + ": %s".printf(grub_device));
2611
log_msg(string.nfill(78, '*'));
2614
log_msg(string.nfill(78, '*'));
2615
log_msg(_("GRUB will NOT be reinstalled"));
2616
log_msg(string.nfill(78, '*'));
2620
if ((app_mode != "")&&(cmd_confirm == false)){
2622
string msg_devices = "";
2623
string msg_reboot = "";
2624
string msg_disclaimer = "";
2626
App.disclaimer_pre_restore(
2627
false, out msg_devices, out msg_reboot,
2628
out msg_disclaimer);
2631
while (cmd_confirm == false){
2633
if (attempts > 3) { break; }
2634
stdout.printf(_("Continue with restore? (y/n): "));
2636
read_stdin_restore_confirm();
2639
if (cmd_confirm == false){
2640
log_error(_("Failed to get input from user in 3 attempts"));
2641
log_msg(_("Aborted."));
2648
thread_restore_running = true;
2649
thr_success = false;
2650
Thread.create<void> (restore_snapshot_thread, true);
2652
catch (ThreadError e) {
2653
thread_restore_running = false;
2654
thr_success = false;
2655
log_error (e.message);
2658
while (thread_restore_running){
2660
Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
2663
snapshot_to_restore = null;
2668
public void disclaimer_pre_restore(bool formatted,
2669
out string msg_devices, out string msg_reboot,
2670
out string msg_disclaimer){
2674
log_debug("Main: disclaimer_pre_restore()");
2676
// msg_devices -----------------------------------------
2679
msg += "\n%s\n%s\n%s\n".printf(
2680
string.nfill(70,'='),
2682
string.nfill(70,'=')
2686
msg += _("Data will be modified on following devices:") + "\n\n";
2688
int max_mount = _("Mount").length;
2689
int max_dev = _("Device").length;
2690
int max_vol = _("Subvol").length;
2692
foreach(var entry in mount_list){
2693
if (entry.device == null){ continue; }
2695
string dev_name = entry.device.short_name_with_alias;
2697
if (dev_name.length > max_dev){
2698
max_dev = dev_name.length;
2700
if (entry.mount_point.length > max_mount){
2701
max_mount = entry.mount_point.length;
2703
if (entry.subvolume_name().length > max_vol){
2704
max_vol = entry.subvolume_name().length;
2708
bool show_subvolume = false;
2709
foreach(var entry in App.mount_list){
2710
if (entry.device == null){ continue; }
2712
if ((entry.device != null)
2713
&& (entry.device.fstype == "btrfs")
2714
&& (entry.subvolume_name().length > 0)){
2716
// subvolumes are used - show subvolume column
2717
show_subvolume = true;
2722
var txt = ("%%-%ds %%-%ds".printf(max_dev, max_mount))
2723
.printf(_("Device"),_("Mount"));
2724
if (show_subvolume){
2725
txt += " %s".printf(_("Subvol"));
2729
txt += string.nfill(max_dev, '-') + " " + string.nfill(max_mount, '-');
2730
if (show_subvolume){
2731
txt += " " + string.nfill(max_vol, '-');
2735
foreach(var entry in App.mount_list){
2736
if (entry.device == null){ continue; }
2738
txt += ("%%-%ds %%-%ds".printf(max_dev, max_mount)).printf(
2739
entry.device.device_name_with_parent, entry.mount_point);
2741
if (show_subvolume){
2742
txt += " %s".printf(entry.subvolume_name());
2749
msg += "<span size=\"medium\"><tt>%s</tt></span>".printf(txt);
2752
msg += "%s\n".printf(txt);
2757
//msg += _("Files will be overwritten on the target device!") + "\n";
2758
//msg += _("If restore fails and you are unable to boot the system, then boot from the Ubuntu Live CD, install Timeshift, and try to restore again.") + "\n";
2760
// msg_reboot -----------------------
2763
if ((sys_root != null) && (restore_target != null)
2764
&& (restore_target.device == sys_root.device)){
2766
msg += _("Please save your work and close all applications.") + "\n";
2767
msg += _("System will reboot after files are restored.");
2772
// msg_disclaimer --------------------------------------
2776
msg += "\n%s\n%s\n%s\n".printf(
2777
string.nfill(70,'='),
2778
_("Disclaimer").up(),
2779
string.nfill(70,'=')
2783
msg += _("This software comes without absolutely NO warranty and the author takes no responsibility for any damage arising from the use of this program.");
2784
msg += " " + _("If these terms are not acceptable to you, please do not proceed beyond this point!");
2790
msg_disclaimer = msg;
2792
// display messages in console mode
2794
if (app_mode.length > 0){
2795
log_msg(msg_devices);
2796
log_msg(msg_reboot);
2797
log_msg(msg_disclaimer);
2800
log_debug("Main: disclaimer_pre_restore(): exit");
2803
public void restore_snapshot_thread(){
2807
string sh_grub = "";
2808
string sh_reboot = "";
2812
string source_path = "";
2814
if (snapshot_to_restore != null){
2815
source_path = snapshot_to_restore.path;
2818
source_path = "/tmp/timeshift";
2819
if (!dir_exists(source_path)){
2820
dir_create(source_path);
2824
log_debug("source_path=%s".printf(source_path));
2826
//set target path ----------------
2828
bool restore_current_system;
2831
if ((sys_root != null)
2832
&& ((restore_target.device == sys_root.device)
2833
|| (restore_target.uuid == sys_root.uuid))){
2835
restore_current_system = true;
2839
restore_current_system = false;
2840
target_path = mount_point_restore + "/";
2842
if (mount_point_restore.strip().length == 0){
2843
log_error(_("Target device is not mounted"));
2844
thr_success = false;
2845
thread_restore_running = false;
2850
log_debug("target_path=%s".printf(target_path));
2852
//save exclude list for restore --------------
2854
save_exclude_list_for_restore(source_path);
2856
//create script -------------
2860
if (restore_current_system){
2861
log_debug("restoring current system");
2863
sh += "echo '" + _("Please do not interrupt the restore process!") + "'\n";
2864
sh += "echo '" + _("System will reboot after files are restored") + "'\n";
2870
var log_path = source_path + "/rsync-log-restore";
2871
var f = File.new_for_path(log_path);
2872
if (f.query_exists()){
2876
//run rsync ----------
2878
sh += "rsync -avir --force --delete-after";
2879
sh += " --log-file=\"%s\"".printf(log_path);
2880
sh += " --exclude-from=\"%s\"".printf(source_path + "/exclude-restore.list");
2883
sh += " \"%s\" \"%s\" \n".printf("/", target_path);
2886
sh += " \"%s\" \"%s\" \n".printf(source_path + "/localhost/", target_path);
2892
log_debug("rsync script:");
2895
//chroot and re-install grub2 --------
2897
log_debug("reinstall_grub2=%s".printf(
2898
reinstall_grub2.to_string()));
2900
log_debug("grub_device=%s".printf(
2901
(grub_device == null) ? "null" : grub_device));
2903
var target_distro = LinuxDistro.get_dist_info(target_path);
2907
if (reinstall_grub2 && (grub_device != null) && (grub_device.length > 0)){
2909
sh_grub += "sync \n";
2910
sh_grub += "echo '' \n";
2911
sh_grub += "echo '" + _("Re-installing GRUB2 bootloader...") + "' \n";
2914
if (!restore_current_system){
2915
if (target_distro.dist_type == "arch"){
2916
chroot += "arch-chroot \"%s\"".printf(target_path);
2919
chroot += "chroot \"%s\"".printf(target_path);
2923
// bind system directories for chrooted system
2924
sh_grub += "for i in /dev /proc /run /sys; do mount --bind \"$i\" \"%s$i\"; done \n".printf(target_path);
2926
// search for other operating systems
2927
//sh_grub += "chroot \"%s\" os-prober \n".printf(target_path);
2929
// re-install grub ---------------
2931
if (target_distro.dist_type == "redhat"){
2933
// this will run only in clone mode
2935
sh_grub += "%s grub2-install --recheck %s \n".printf(chroot, grub_device);
2938
* grub2-install should NOT be run on Fedora EFI systems
2939
* https://fedoraproject.org/wiki/GRUB_2
2940
* Instead following packages should be reinstalled:
2941
* dnf reinstall grub2-efi grub2-efi-modules shim
2943
* Bootloader installation will be skipped while restoring in GUI mode.
2944
* Fedora seems to boot correctly even after installing new
2945
* kernels and restoring a snapshot with an older kernel.
2949
sh_grub += "%s grub-install --recheck %s \n".printf(chroot, grub_device);
2952
// create new grub menu
2953
//sh_grub += "chroot \"%s\" grub-mkconfig -o /boot/grub/grub.cfg \n".printf(target_path);
2955
// update initramfs --------------
2957
if (target_distro.dist_type == "redhat"){
2958
sh_grub += "%s dracut -f -v \n".printf(chroot);
2960
else if (target_distro.dist_type == "arch"){
2961
sh_grub += "%s mkinitcpio -p /etc/mkinitcpio.d/*.preset\n".printf(chroot);
2964
sh_grub += "%s update-initramfs -u -k all \n".printf(chroot);
2967
// update grub menu --------------
2969
if ((target_distro.dist_type == "redhat") || (target_distro.dist_type == "arch")){
2970
sh_grub += "%s grub-mkconfig -o /boot/grub2/grub.cfg \n".printf(chroot);
2973
sh_grub += "%s update-grub \n".printf(chroot);
2976
sh_grub += "echo '' \n";
2978
// sync file systems
2979
sh_grub += "echo '" + _("Synching file systems...") + "' \n";
2980
sh_grub += "sync \n";
2981
sh_grub += "echo '' \n";
2983
// unmount chrooted system
2984
sh_grub += "echo '" + _("Cleaning up...") + "' \n";
2985
sh_grub += "for i in /dev /proc /run /sys; do umount -f \"%s$i\"; done \n".printf(target_path);
2986
sh_grub += "sync \n";
2988
log_debug("GRUB2 install script:");
2994
log_debug("skipping sh_grub: reinstall_grub2=%s, grub_device=%s".printf(
2995
reinstall_grub2.to_string(), (grub_device == null) ? "null" : grub_device));
2998
//reboot if required --------
3000
if (restore_current_system){
3001
sh_reboot += "echo '' \n";
3002
sh_reboot += "echo '" + _("Rebooting system...") + "' \n";
3003
sh += "reboot -f \n";
3004
//sh_reboot += "shutdown -r now \n";
3007
//check if current system is being restored and do some housekeeping ---------
3009
if (restore_current_system){
3011
//invalidate the .sync snapshot -------
3013
string sync_name = ".sync";
3014
string snapshot_dir = path_combine(repo.snapshot_location, "timeshift/snapshots");
3015
string sync_path = snapshot_dir + "/" + sync_name;
3016
string control_file_path = sync_path + "/info.json";
3018
f = File.new_for_path(control_file_path);
3019
if(f.query_exists()){
3020
f.delete(); //delete the control file
3023
//save a control file for updating the .sync snapshot -----
3025
control_file_path = snapshot_dir + "/.sync-restore";
3027
f = File.new_for_path(control_file_path);
3028
if(f.query_exists()){
3029
f.delete(); //delete existing file
3032
file_write(control_file_path, snapshot_to_restore.path); //save snapshot name
3035
//run the script --------------------
3037
if (snapshot_to_restore != null){
3038
log_msg(_("Restoring snapshot..."));
3041
log_msg(_("Cloning system..."));
3044
progress_text = _("Synching files with rsync...");
3045
log_msg(progress_text);
3047
if (app_mode == ""){
3049
// gui mode --------------
3051
if (restore_current_system){
3052
//current system, gui, fullscreen
3053
temp_script = save_bash_script_temp(sh + sh_grub + sh_reboot);
3054
// Note: sh_grub will be empty if reinstall_grub2 = false
3057
var dlg = new TerminalWindow.with_parent(null);
3058
dlg.execute_script(temp_script, true);
3061
// other system, gui ------------------------
3063
//App.progress_text = "Sync";
3065
progress_text = _("Building file list...");
3066
//log_msg(progress_text); // gui-only message
3068
task = new RsyncTask();
3069
task.relative = false;
3070
task.verbose = true;
3071
task.delete_extra = true;
3072
task.delete_excluded = false;
3073
task.delete_after = true;
3076
task.source_path = "/";
3080
path_combine(source_path, "localhost");
3083
task.dest_path = target_path;
3085
task.exclude_from_file =
3086
path_combine(source_path, "exclude-restore.list");
3088
task.rsync_log_file = log_path;
3089
task.prg_count_total = Main.first_snapshot_count;
3092
while (task.status == AppStatus.RUNNING){
3095
if (App.task.status_line.length > 0){
3096
progress_text = _("Synching files with rsync...");
3102
if (!restore_current_system){
3103
App.progress_text = "Updating /etc/fstab and /etc/crypttab on target system...";
3104
log_msg(App.progress_text);
3106
fix_fstab_file(target_path);
3107
fix_crypttab_file(target_path);
3110
// re-install grub ------------
3112
if (reinstall_grub2){
3114
App.progress_text = "Re-installing GRUB2 bootloader...";
3115
log_msg(App.progress_text);
3119
//string std_out, std_err;
3120
ret_val = exec_script_sync(sh_grub, null, null);
3121
//log_to_file(std_out);
3122
//log_to_file(std_err);
3125
log_debug("GRUB2 install completed");
3128
ret_val = task.exit_code;
3133
// console mode ----------
3135
if (restore_current_system){
3136
script += sh_grub + sh_reboot;
3139
log_debug("verbose=%s".printf(cmd_verbose.to_string()));
3142
//current/other system, console, verbose
3143
ret_val = exec_script_sync(script, null, null, false, false, false, true);
3147
//current/other system, console, quiet
3148
string std_out, std_err;
3149
ret_val = exec_script_sync(script, out std_out, out std_err);
3150
log_to_file(std_out);
3151
log_to_file(std_err);
3154
if (!restore_current_system){
3156
// fix fstab and crypttab files ------
3158
fix_fstab_file(target_path);
3159
fix_crypttab_file(target_path);
3161
// re-install grub ------------
3163
if (reinstall_grub2){
3165
App.progress_text = "Re-installing GRUB2 bootloader...";
3166
log_msg(App.progress_text);
3169
//current/other system, console, verbose
3170
ret_val = exec_script_sync(sh_grub, null, null, false, false, false, true);
3174
//current/other system, console, quiet
3175
string std_out, std_err;
3176
ret_val = exec_script_sync(sh_grub, out std_out, out std_err);
3177
log_to_file(std_out);
3178
log_to_file(std_err);
3184
// check for errors ----------------------
3187
log_error(_("Restore failed with exit code") + ": %d".printf(ret_val));
3188
thr_success = false;
3189
thread_restore_running = false;
3192
log_msg(_("Restore completed without errors"));
3193
//thr_success = true;
3194
//thread_restore_running = false;
3197
// unmount ----------
3199
unmount_target_device(false);
3202
log_error (e.message);
3203
thr_success = false;
3204
thread_restore_running = false;
3207
thread_restore_running = false;
3210
public void fix_fstab_file(string target_path){
3212
string fstab_path = target_path + "etc/fstab";
3213
var fstab_list = FsTabEntry.read_file(fstab_path);
3215
foreach(var mnt in mount_list){
3217
var entry = FsTabEntry.find_entry_by_mount_point(fstab_list, mnt.mount_point);
3221
entry = new FsTabEntry();
3222
entry.mount_point = mnt.mount_point;
3223
fstab_list.add(entry);
3226
//update fstab entry
3227
entry.device = "UUID=%s".printf(mnt.device.uuid);
3228
entry.type = mnt.device.fstype;
3230
// fix mount options for non-btrfs device
3231
if (mnt.device.fstype != "btrfs"){
3232
// remove subvol option
3233
entry.remove_option("subvol=%s".printf(entry.subvolume_name()));
3238
* Remove fstab entries for any system directories that
3239
* the user has not explicitly mapped before restore/clone
3240
* This ensures that the cloned/restored system does not mount
3241
* any devices to system paths that the user has not explicitly specified
3244
for(int i = fstab_list.size - 1; i >= 0; i--){
3245
var entry = fstab_list[i];
3247
if (!entry.is_for_system_directory()){ continue; }
3249
var mnt = MountEntry.find_entry_by_mount_point(mount_list, entry.mount_point);
3251
fstab_list.remove(entry);
3255
// write the updated file
3257
FsTabEntry.write_file(fstab_list, fstab_path, false);
3259
log_msg(_("Updated /etc/fstab on target device") + ": %s".printf(fstab_path));
3261
// create directories on disk for mount points in /etc/fstab
3263
foreach(var entry in fstab_list){
3264
if (entry.mount_point.length == 0){ continue; }
3265
if (!entry.mount_point.has_prefix("/")){ continue; }
3267
string mount_path = path_combine(
3268
target_path, entry.mount_point);
3270
if (entry.is_comment
3271
|| entry.is_empty_line
3272
|| (mount_path.length == 0)){
3277
if (!dir_exists(mount_path)){
3279
log_msg("Created mount point on target device: %s".printf(
3280
entry.mount_point));
3282
dir_create(mount_path);
3287
public void fix_crypttab_file(string target_path){
3288
string crypttab_path = target_path + "etc/crypttab";
3289
var crypttab_list = CryptTabEntry.read_file(crypttab_path);
3291
// add option "nofail" to existing entries
3293
foreach(var entry in crypttab_list){
3294
entry.append_option("nofail");
3297
// check and add entries for mapped devices which are encrypted
3299
foreach(var mnt in mount_list){
3300
if ((mnt.device != null) && (mnt.device.is_on_encrypted_partition())){
3303
var entry = CryptTabEntry.find_entry_by_uuid(
3304
crypttab_list, mnt.device.parent.uuid);
3308
entry = new CryptTabEntry();
3309
crypttab_list.add(entry);
3312
// set custom values
3313
entry.device_uuid = mnt.device.parent.uuid;
3314
entry.mapped_name = "luks-%s".printf(mnt.device.parent.uuid);
3315
entry.keyfile = "none";
3316
entry.options = "luks,nofail";
3320
CryptTabEntry.write_file(crypttab_list, crypttab_path, false);
3322
log_msg(_("Updated /etc/crypttab on target device") + ": %s".printf(crypttab_path));
3325
public Device? dst_root{
3327
foreach(var mnt in mount_list){
3328
if (mnt.mount_point == "/"){
3336
public Device? dst_boot{
3338
foreach(var mnt in mount_list){
3339
if (mnt.mount_point == "/boot"){
3347
public Device? dst_efi{
3349
foreach(var mnt in mount_list){
3350
if (mnt.mount_point == "/boot/efi"){
3358
public Device? dst_home{
3360
foreach(var mnt in mount_list){
3361
if (mnt.mount_point == "/home"){
3371
public void save_app_config(){
3373
log_debug("load_app_config()");
3375
var config = new Json.Object();
3377
if ((repo != null) && repo.available()){
3378
// save backup device uuid
3379
config.set_string_member("backup_device_uuid",
3380
(repo.device == null) ? "" : repo.device.uuid);
3382
// save parent uuid if backup device has parent
3383
config.set_string_member("parent_device_uuid",
3384
(repo.device.has_parent()) ? repo.device.parent.uuid : "");
3387
// retain values for next run
3388
config.set_string_member("backup_device_uuid", backup_uuid);
3389
config.set_string_member("parent_device_uuid", backup_parent_uuid);
3392
config.set_string_member("use_snapshot_path_user",
3393
repo.use_snapshot_path_custom.to_string());
3395
config.set_string_member("snapshot_path_user",
3396
repo.snapshot_path_user.to_string());
3398
config.set_string_member("schedule_monthly", schedule_monthly.to_string());
3399
config.set_string_member("schedule_weekly", schedule_weekly.to_string());
3400
config.set_string_member("schedule_daily", schedule_daily.to_string());
3401
config.set_string_member("schedule_hourly", schedule_hourly.to_string());
3402
config.set_string_member("schedule_boot", schedule_boot.to_string());
3404
config.set_string_member("count_monthly", count_monthly.to_string());
3405
config.set_string_member("count_weekly", count_weekly.to_string());
3406
config.set_string_member("count_daily", count_daily.to_string());
3407
config.set_string_member("count_hourly", count_hourly.to_string());
3408
config.set_string_member("count_boot", count_boot.to_string());
3410
config.set_string_member("snapshot_size", first_snapshot_size.to_string());
3411
config.set_string_member("snapshot_count", first_snapshot_count.to_string());
3413
Json.Array arr = new Json.Array();
3414
foreach(string path in exclude_list_user){
3415
arr.add_string_element(path);
3417
config.set_array_member("exclude",arr);
3419
arr = new Json.Array();
3420
foreach(var name in exclude_app_names){
3421
arr.add_string_element(name);
3423
config.set_array_member("exclude-apps",arr);
3425
var json = new Json.Generator();
3428
var node = new Json.Node(NodeType.OBJECT);
3429
node.set_object(config);
3430
json.set_root(node);
3433
json.to_file(this.app_conf_path);
3435
log_error (e.message);
3438
if ((app_mode == "")||(LOG_DEBUG)){
3439
log_msg(_("App config saved") + ": '%s'".printf(this.app_conf_path));
3443
public void load_app_config(){
3445
log_debug("load_app_config()");
3447
var f = File.new_for_path(this.app_conf_path);
3448
if (!f.query_exists()) {
3450
log_debug("first run mode: config file not found");
3455
var parser = new Json.Parser();
3457
parser.load_from_file(this.app_conf_path);
3459
log_error (e.message);
3461
var node = parser.get_root();
3462
var config = node.get_object();
3464
// initialize repo using config file values
3466
backup_uuid = json_get_string(config,"backup_device_uuid", backup_uuid);
3467
backup_parent_uuid = json_get_string(config,"parent_device_uuid", backup_parent_uuid);
3471
this.schedule_monthly = json_get_bool(config,"schedule_monthly",schedule_monthly);
3472
this.schedule_weekly = json_get_bool(config,"schedule_weekly",schedule_weekly);
3473
this.schedule_daily = json_get_bool(config,"schedule_daily",schedule_daily);
3474
this.schedule_hourly = json_get_bool(config,"schedule_hourly",schedule_hourly);
3475
this.schedule_boot = json_get_bool(config,"schedule_boot",schedule_boot);
3477
this.count_monthly = json_get_int(config,"count_monthly",count_monthly);
3478
this.count_weekly = json_get_int(config,"count_weekly",count_weekly);
3479
this.count_daily = json_get_int(config,"count_daily",count_daily);
3480
this.count_hourly = json_get_int(config,"count_hourly",count_hourly);
3481
this.count_boot = json_get_int(config,"count_boot",count_boot);
3483
Main.first_snapshot_size = json_get_int64(config,"snapshot_size",
3484
Main.first_snapshot_size);
3486
Main.first_snapshot_count = json_get_int64(config,"snapshot_count",
3487
Main.first_snapshot_count);
3489
exclude_list_user.clear();
3491
if (config.has_member ("exclude")){
3492
foreach (Json.Node jnode in config.get_array_member ("exclude").get_elements()) {
3494
string path = jnode.get_string();
3496
if (!exclude_list_user.contains(path)
3497
&& !exclude_list_default.contains(path)
3498
&& !exclude_list_home.contains(path)){
3500
exclude_list_user.add(path);
3505
exclude_app_names.clear();
3507
if (config.has_member ("exclude-apps")){
3508
var apps = config.get_array_member("exclude-apps");
3509
foreach (Json.Node jnode in apps.get_elements()) {
3511
string name = jnode.get_string();
3513
if (!exclude_app_names.contains(name)){
3514
exclude_app_names.add(name);
3519
if ((app_mode == "")||(LOG_DEBUG)){
3520
log_msg(_("App config loaded") + ": '%s'".printf(this.app_conf_path));
3524
public void initialize_repo(){
3526
log_debug("backup_uuid=%s".printf(backup_uuid));
3527
log_debug("backup_parent_uuid=%s".printf(backup_parent_uuid));
3529
if (backup_uuid.length > 0){
3530
log_debug("repo: creating from uuid");
3531
repo = new SnapshotRepo.from_uuid(backup_uuid, null);
3533
if ((repo == null) || !repo.available()){
3534
if (backup_parent_uuid.length > 0){
3535
log_debug("repo: creating from parent uuid");
3536
repo = new SnapshotRepo.from_uuid(backup_parent_uuid, null);
3541
if (sys_root != null){
3542
log_debug("repo: uuid is empty, creating from root device");
3543
repo = new SnapshotRepo.from_device(sys_root, null);
3546
log_debug("repo: root device is null");
3547
repo = new SnapshotRepo.from_null(null);
3551
// initialize repo using command line parameter
3553
if (cmd_backup_device.length > 0){
3554
var cmd_dev = Device.get_device_by_name(cmd_backup_device);
3555
if (cmd_dev != null){
3556
log_debug("repo: creating from command argument: %s".printf(cmd_backup_device));
3557
repo = new SnapshotRepo.from_device(cmd_dev, null);
3559
// TODO: move this code to main window
3562
log_error(_("Could not find device") + ": '%s'".printf(cmd_backup_device));
3568
/* Note: In command-line mode, user will be prompted for backup device */
3570
/* The backup device specified in config file will be mounted at this point if:
3571
* 1) app is running in GUI mode, OR
3572
* 2) app is running command mode without backup device argument
3578
public void update_partitions(){
3580
log_debug("update_partitions()");
3584
partitions = Device.get_filesystems();
3586
foreach(var pi in partitions){
3588
// sys_root and sys_home will be detected by detect_system_devices()
3589
if ((repo != null) && (repo.device != null) && (pi.uuid == repo.device.uuid)){
3594
pi.dist_info = LinuxDistro.get_dist_info(pi.mount_points[0].mount_point).full_name();
3598
if (partitions.size == 0){
3599
log_error("ts: " + _("Failed to get partition list."));
3602
log_debug("partition list updated");
3605
public void detect_system_devices(){
3607
log_debug("detect_system_devices()");
3614
foreach(Device pi in partitions){
3615
foreach(var mp in pi.mount_points){
3616
if (mp.mount_point == "/"){
3618
if ((app_mode == "")||(LOG_DEBUG)){
3619
log_msg(_("/ is mapped to device") + ": %s, UUID=%s".printf(pi.device,pi.uuid));
3623
if (mp.mount_point == "/home"){
3625
if ((app_mode == "")||(LOG_DEBUG)){
3626
log_msg(_("/home is mapped to device") + ": %s, UUID=%s".printf(pi.device,pi.uuid));
3630
if (mp.mount_point == "/boot"){
3632
if ((app_mode == "")||(LOG_DEBUG)){
3633
log_msg(_("/boot is mapped to device") + ": %s, UUID=%s".printf(pi.device,pi.uuid));
3637
if (mp.mount_point == "/boot/efi"){
3639
if ((app_mode == "")||(LOG_DEBUG)){
3640
log_msg(_("/boot/efi is mapped to device") + ": %s, UUID=%s".printf(pi.device,pi.uuid));
3647
public bool mount_target_device(Gtk.Window? parent_win){
3649
* Target device will be mounted explicitly to /mnt/timeshift/restore
3650
* Existing mount points are not used since we need to mount other devices in sub-directories
3653
log_debug("mount_target_device()");
3655
if (restore_target == null){
3659
//check and create restore mount point for restore
3660
mount_point_restore = mount_point_app + "/restore";
3661
dir_create(mount_point_restore);
3663
/*var already_mounted = false;
3664
var dev_mounted = Device.get_device_by_path(mount_point_restore);
3665
if ((dev_mounted != null)
3666
&& (dev_mounted.uuid == restore_target.uuid)){
3668
foreach(var mp in dev_mounted.mount_points){
3669
if ((mp.mount_point == mount_point_restore)
3670
&& (mp.mount_options == "subvol=@")){
3673
return; //already_mounted
3679
unmount_target_device();
3681
// unlock encrypted device
3682
if (restore_target.is_encrypted_partition()){
3684
string msg_out, msg_err;
3686
restore_target = Device.luks_unlock(
3687
restore_target, "", "", parent_win, out msg_out, out msg_err);
3690
if (restore_target == null){
3691
log_error(_("Target device not specified!"));
3695
//update mount entry
3696
foreach (MountEntry mnt in mount_list) {
3697
if (mnt.mount_point == "/"){
3698
mnt.device = restore_target;
3704
// mount root device
3705
if (restore_target.fstype == "btrfs"){
3707
//check subvolume layout
3709
bool supported = check_btrfs_layout(dst_root, dst_home);
3711
if (!supported && snapshot_to_restore.has_subvolumes()){
3712
string msg = _("The target partition has an unsupported subvolume layout.") + "\n";
3713
msg += _("Only ubuntu-type layouts with @ and @home subvolumes are currently supported.");
3715
if (app_mode == ""){
3716
string title = _("Unsupported Subvolume Layout");
3717
gtk_messagebox(title, msg, null, true);
3720
log_error("\n" + msg);
3727
// mount all devices
3728
foreach (var mnt in mount_list) {
3730
// unlock encrypted device
3731
if (mnt.device.is_encrypted_partition()){
3733
string msg_out, msg_err;
3735
mnt.device = Device.luks_unlock(
3736
mnt.device, "", "", parent_win, out msg_out, out msg_err);
3739
if (mnt.device == null){
3744
string mount_options = "";
3745
if (mnt.device.fstype == "btrfs"){
3746
if (mnt.mount_point == "/"){
3747
mount_options = "subvol=@";
3749
else if (mnt.mount_point == "/home"){
3750
mount_options = "subvol=@home";
3754
if (!Device.mount(mnt.device.uuid, mount_point_restore + mnt.mount_point, mount_options)){
3762
public void unmount_target_device(bool exit_on_error = true){
3763
if (mount_point_restore == null) { return; }
3765
log_debug("unmount_target_device()");
3767
//unmount the target device only if it was mounted by application
3768
if (mount_point_restore.has_prefix(mount_point_app)){ //always true
3769
unmount_device(mount_point_restore,exit_on_error);
3776
public bool unmount_device(string mount_point, bool exit_on_error = true){
3777
bool is_unmounted = Device.unmount(mount_point);
3780
if (app_mode == ""){
3781
string title = _("Critical Error");
3782
string msg = _("Failed to unmount device!") + "\n" + _("Application will exit");
3783
gtk_messagebox(title, msg, null, true);
3789
return is_unmounted;
3792
public SnapshotLocationStatus check_backup_location(out string message, out string details){
3793
repo.check_status();
3794
message = repo.status_message;
3795
details = repo.status_details;
3796
return repo.status_code;
3799
public bool check_btrfs_volume(Device dev, string subvol_names){
3801
log_debug("check_btrfs_volume():%s".printf(subvol_names));
3803
string mnt_btrfs = mount_point_app + "/btrfs";
3804
dir_create(mnt_btrfs);
3806
Device.unmount(mnt_btrfs);
3807
Device.mount(dev.uuid, mnt_btrfs);
3809
bool supported = true;
3811
foreach(string subvol_name in subvol_names.split(",")){
3812
supported = supported && dir_exists(path_combine(mnt_btrfs,subvol_name));
3815
if (Device.unmount(mnt_btrfs)){
3816
if (dir_exists(mnt_btrfs) && (dir_count(mnt_btrfs) == 0)){
3817
dir_delete(mnt_btrfs);
3818
log_debug(_("Removed mount directory: '%s'").printf(mnt_btrfs));
3825
public bool backup_device_online(){
3826
/*if (snapshot_device != null){
3827
//mount_backup_device(null);
3828
if (Device.get_device_mount_points(snapshot_device.uuid).size > 0){
3833
string message, details;
3834
var status = App.check_backup_location(out message, out details);
3837
case SnapshotLocationStatus.NO_SNAPSHOTS_HAS_SPACE:
3838
case SnapshotLocationStatus.NO_SNAPSHOTS_NO_SPACE:
3839
case SnapshotLocationStatus.HAS_SNAPSHOTS_HAS_SPACE:
3840
case SnapshotLocationStatus.HAS_SNAPSHOTS_NO_SPACE:
3843
//gtk_messagebox(message,details, this, true);
3849
public int64 estimate_system_size(){
3851
log_debug("estimate_system_size()");
3853
if (Main.first_snapshot_size > 0){
3854
return Main.first_snapshot_size;
3856
else if (live_system()){
3861
thread_estimate_running = true;
3862
thr_success = false;
3863
Thread.create<void> (estimate_system_size_thread, true);
3864
} catch (ThreadError e) {
3865
thread_estimate_running = false;
3866
thr_success = false;
3867
log_error (e.message);
3870
while (thread_estimate_running){
3872
Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
3877
log_debug("estimate_system_size(): ok");
3879
return Main.first_snapshot_size;
3882
public void estimate_system_size_thread(){
3883
thread_estimate_running = true;
3889
int64 required_space = 0;
3890
int64 file_count = 0;
3894
log_msg("Using temp dir '%s'".printf(TEMP_DIR));
3896
string file_exclude_list = path_combine(TEMP_DIR, "exclude.list");
3897
var f = File.new_for_path(file_exclude_list);
3898
if (f.query_exists()){
3902
string file_log = path_combine(TEMP_DIR, "rsync.log");
3903
f = File.new_for_path(file_log);
3904
if (f.query_exists()){
3908
string dir_empty = path_combine(TEMP_DIR, "empty");
3909
f = File.new_for_path(dir_empty);
3910
if (!f.query_exists()){
3911
dir_create(dir_empty);
3914
save_exclude_list_for_backup(TEMP_DIR);
3916
cmd = "LC_ALL=C ; rsync -ai --delete --numeric-ids --relative --stats --dry-run --delete-excluded --exclude-from='%s' /. '%s' &> '%s'".printf(file_exclude_list, dir_empty, file_log);
3919
ret_val = exec_script_sync(cmd, out std_out, out std_err);
3921
if (file_exists(file_log)){
3922
cmd = "cat '%s' | awk '/Total file size/ {print $4}'".printf(file_log);
3923
ret_val = exec_script_sync(cmd, out std_out, out std_err);
3925
required_space = long.parse(std_out.replace(",","").strip());
3927
cmd = "wc -l '%s'".printf(escape_single_quote(file_log));
3928
ret_val = exec_script_sync(cmd, out std_out, out std_err);
3930
file_count = long.parse(std_out.split(" ")[0].strip());
3936
log_error (_("Failed to estimate system size"));
3937
log_error (std_err);
3938
thr_success = false;
3942
log_error (_("Failed to estimate system size"));
3943
log_error (std_err);
3944
log_error (std_out);
3945
thr_success = false;
3949
log_error (e.message);
3950
thr_success = false;
3953
if ((required_space == 0) && (sys_root != null)){
3954
required_space = sys_root.used_bytes;
3957
Main.first_snapshot_size = required_space;
3958
Main.first_snapshot_count = file_count;
3960
log_debug("First snapshot size: %s".printf(format_file_size(required_space)));
3961
log_debug("File count: %lld".printf(first_snapshot_count));
3963
thread_estimate_running = false;
3968
public void cron_job_update(){
3970
if (live_system()) { return; }
3972
// check and remove crontab entries created by previous versions of timeshift
3974
string entry = "*/30 * * * * timeshift --backup";
3975
CronTab.remove_job(entry);
3977
foreach(string interval in new string[] {"@monthly","@weekly","@daily"}){
3978
entry = "%s timeshift --backup".printf(interval);
3979
CronTab.remove_job(entry);
3982
//entry = "^@(daily|weekly|monthly|hourly) timeshift --backup$";
3983
//CronTab.remove_job(entry, true);
3985
//entry = "^@reboot sleep [0-9]*m && timeshift --backup$";
3986
//CronTab.remove_job(entry, true);
3988
// update crontab entries
3990
string entry_boot = "@reboot sleep %dm && timeshift --backup".printf(startup_delay_interval_mins);
3991
//entry_boot += " #timeshift-16.10-hourly";
3993
string entry_hourly = "@hourly timeshift --backup";
3994
//entry_hourly += " #timeshift-16.10-boot";
3997
CronTab.add_job(entry_boot);
3998
CronTab.add_job(entry_hourly);
4001
CronTab.remove_job(entry_boot);
4002
CronTab.remove_job(entry_hourly);
4005
/*string cmd = "timeshift --backup";
4008
CronTab.add_script_hourly("timeshift-backup", cmd);
4011
CronTab.remove_script_hourly("timeshift-backup");
4017
public void clean_logs(){
4019
log_debug("clean_logs()");
4021
Gee.ArrayList<string> list = new Gee.ArrayList<string>();
4024
var dir = File.new_for_path (log_dir);
4025
var enumerator = dir.enumerate_children ("*", 0);
4027
var info = enumerator.next_file ();
4030
while (info != null) {
4031
if (info.get_file_type() == FileType.REGULAR) {
4032
path = log_dir + "/" + info.get_name();
4033
if (path != log_file) {
4037
info = enumerator.next_file ();
4040
CompareDataFunc<string> compare_func = (a, b) => {
4044
list.sort((owned) compare_func);
4046
if (list.size > 500){
4047
for(int k=0; k<100; k++){
4048
var file = File.new_for_path (list[k]);
4049
if (file.query_exists()){
4053
log_msg(_("Older log files removed"));
4057
log_error (e.message);
4061
public void exit_app (){
4063
log_debug("exit_app()");
4065
if (app_mode == ""){
4066
//update app config only in GUI mode
4072
unmount_target_device(false);