2
using TeeJee.FileSystem;
4
using TeeJee.JsonHelper;
5
using TeeJee.ProcessHelper;
6
using TeeJee.GtkHelper;
10
public class SnapshotRepo : GLib.Object{
11
public Device device = null;
12
public string snapshot_path_user = "";
13
public string snapshot_path_mount = "";
14
public bool use_snapshot_path_custom = false;
16
public Gee.ArrayList<Snapshot?> snapshots;
18
public string status_message = "";
19
public string status_details = "";
20
public SnapshotLocationStatus status_code;
23
private Gtk.Window? parent_window = null;
24
private bool thr_success = false;
25
private bool thr_running = false;
26
//private int thr_retval = -1;
27
private string thr_args1 = "";
29
public SnapshotRepo.from_path(string path, Gtk.Window? parent_win){
30
this.snapshot_path_user = path;
31
this.use_snapshot_path_custom = true;
32
this.parent_window = parent_win;
34
snapshots = new Gee.ArrayList<Snapshot>();
36
log_msg(_("Selected snapshot path") + ": %s".printf(path));
38
var list = Device.get_disk_space_using_df(path);
42
log_msg(_("Device") + ": %s".printf(device.device));
43
log_msg(_("Free space") + ": %s".printf(format_file_size(device.free_bytes)));
47
public SnapshotRepo.from_device(Device dev, Gtk.Window? parent_win){
49
this.use_snapshot_path_custom = false;
50
this.parent_window = parent_win;
52
snapshots = new Gee.ArrayList<Snapshot>();
54
if ((dev != null) && (dev.uuid.length > 0)){
56
unlock_and_mount_device();
58
log_msg(_("Selected snapshot device") + ": %s".printf(device.device));
59
log_msg(_("Free space") + ": %s".printf(format_file_size(device.free_bytes)));;
63
public string snapshot_location {
65
if (use_snapshot_path_custom && dir_exists(snapshot_path_user)){
66
return snapshot_path_user;
69
return snapshot_path_mount;
76
public bool unlock_and_mount_device(){
78
// unlock encrypted device
79
if (device.is_encrypted()){
81
device = unlock_encrypted_device(device);
88
if (device.fstype == "btrfs"){
90
snapshot_path_mount = "/mnt/timeshift/backup";
92
Device.unmount(snapshot_path_mount);
96
bool ok = Device.mount(device.uuid, snapshot_path_mount, "");
98
snapshot_path_mount = "";
102
var mps = Device.get_device_mount_points(device.uuid);
105
snapshot_path_mount = mps[0].mount_point;
108
Device.automount_udisks(device.device);
110
mps = Device.get_device_mount_points(device.uuid);
112
snapshot_path_mount = mps[0].mount_point;
115
snapshot_path_mount = "";
123
public Device unlock_encrypted_device(Device luks_device){
124
Device luks_unlocked = null;
126
string mapped_name = "%s_unlocked".printf(luks_device.name);
128
var partitions = Device.get_block_devices_using_lsblk();
130
// check if already unlocked
131
foreach(var part in partitions){
132
if (part.pkname == luks_device.kname){
133
log_msg(_("Unlocked device is mapped to '%s'").printf(part.device));
139
if (parent_window == null){
141
var counter = new TimeoutCounter();
142
counter.kill_process_on_timeout("cryptsetup", 20, true);
144
// prompt user to unlock
145
string cmd = "cryptsetup luksOpen '%s' '%s'".printf(luks_device.device, mapped_name);
150
partitions = Device.get_block_devices_using_lsblk();
153
foreach(var part in partitions){
154
if (part.pkname == luks_device.kname){
155
log_msg(_("Unlocked device is mapped to '%s'").printf(part.name));
162
// prompt user for password
163
string passphrase = gtk_inputbox(
164
_("Encrypted Device"),
165
_("Enter passphrase to unlock '%s'").printf(luks_device.name),
166
parent_window, true);
168
string message, details;
169
luks_unlocked = Device.luks_unlock(luks_device, mapped_name, passphrase,
170
out message, out details);
172
bool is_error = (luks_unlocked == null);
174
gtk_messagebox(message, details, null, is_error);
177
return luks_unlocked;
180
public bool load_snapshots(){
184
string path = snapshot_location + "/timeshift/snapshots";
186
if (!dir_exists(path)){
187
log_error("Path not found: %s".printf(path));
192
var dir = File.new_for_path (path);
193
var enumerator = dir.enumerate_children ("*", 0);
195
var info = enumerator.next_file ();
196
while (info != null) {
197
if (info.get_file_type() == FileType.DIRECTORY) {
198
if (info.get_name() != ".sync") {
199
Snapshot bak = new Snapshot(path + "/" + info.get_name());
205
info = enumerator.next_file ();
209
log_error (e.message);
213
snapshots.sort((a,b) => {
214
Snapshot t1 = (Snapshot) a;
215
Snapshot t2 = (Snapshot) b;
216
return t1.date.compare(t2.date);
222
// get tagged snapshots
224
public Gee.ArrayList<Snapshot?> get_snapshots_by_tag(string tag = ""){
225
var list = new Gee.ArrayList<Snapshot?>();
227
foreach(Snapshot bak in snapshots){
228
if (tag == "" || bak.has_tag(tag)){
233
Snapshot t1 = (Snapshot) a;
234
Snapshot t2 = (Snapshot) b;
235
return (t1.date.compare(t2.date));
241
public Snapshot? get_latest_snapshot(string tag = ""){
242
var list = get_snapshots_by_tag(tag);
245
return list[list.size - 1];
250
public Snapshot? get_oldest_snapshot(string tag = ""){
251
var list = get_snapshots_by_tag(tag);
261
public void check_status(){
263
status_code = SnapshotLocationStatus.HAS_SNAPSHOTS_HAS_SPACE;
268
log_msg("Config: Free space limit is %s".printf(
269
format_file_size(App.minimum_free_disk_space)));
276
if (use_snapshot_path_custom){
277
log_msg("Custom path is selected for snapshot location");
280
log_msg(_("Snapshot device") + ": '%s'".printf(
281
(device == null) ? " UNKNOWN" : device.device));
283
log_msg(_("Snapshot location") + ": '%s'".printf(snapshot_location));
285
log_msg(status_message);
286
log_msg(status_details);
288
log_msg("Status: %s".printf(
289
status_code.to_string().replace("SNAPSHOT_LOCATION_STATUS_","")));
292
public bool is_available(){
293
if (use_snapshot_path_custom){
294
if (snapshot_path_user.strip().length == 0){
295
status_message = _("Snapshot location not selected");
296
status_details = _("Select the location for saving snapshots");
297
status_code = SnapshotLocationStatus.NOT_SELECTED;
301
if (!dir_exists(snapshot_path_user)){
302
status_message = _("Snapshot location not available!");
303
status_details = _("Path not found") + ": '%s'".printf(snapshot_path_user);
304
status_code = SnapshotLocationStatus.NOT_AVAILABLE;
309
bool hardlink_supported =
310
filesystem_supports_hardlinks(snapshot_path_user, out is_readonly);
313
status_message = _("File system is read-only!");
314
status_details = _("Select another location for saving snapshots");
315
status_code = SnapshotLocationStatus.READ_ONLY_FS;
318
else if (!hardlink_supported){
319
status_message = _("File system does not support hard-links!");
320
status_details = _("Select another location for saving snapshots");
321
status_code = SnapshotLocationStatus.HARDLINKS_NOT_SUPPORTED;
333
status_message = _("Snapshot location not selected");
334
status_details = _("Select the location for saving snapshots");
335
status_code = SnapshotLocationStatus.NOT_SELECTED;
338
else if (device.device.length == 0){
339
status_message = _("Snapshot location not available!");
340
status_details = _("Device not found") + ": UUID='%s'".printf(device.uuid);
341
status_code = SnapshotLocationStatus.NOT_AVAILABLE;
351
public bool has_snapshots(){
353
return (snapshots.size > 0);
356
public bool has_space(){
359
device.query_disk_space();
362
if (snapshots.size > 0){
363
// has snapshots, check minimum space
365
var min_free = App.minimum_free_disk_space;
367
if (device.free_bytes < min_free){
368
status_message = _("Not enough disk space");
369
status_message += " (< %s)".printf(format_file_size(min_free));
371
status_details = _("Select another device or free up some space");
373
status_code = SnapshotLocationStatus.HAS_SNAPSHOTS_NO_SPACE;
378
status_message = "ok";
380
status_details = _("%d snapshots, %s free").printf(
381
snapshots.size, format_file_size(device.free_bytes));
383
status_code = SnapshotLocationStatus.HAS_SNAPSHOTS_HAS_SPACE;
389
// no snapshots, check estimated space
391
var required_space = App.first_snapshot_size;
393
if (device.free_bytes < required_space){
394
status_message = _("Not enough disk space");
395
status_message += " (< %s)".printf(format_file_size(required_space));
397
status_details = _("Select another device or free up some space");
399
status_code = SnapshotLocationStatus.NO_SNAPSHOTS_NO_SPACE;
403
status_message = _("No snapshots on this device");
405
status_details = _("First snapshot requires:");
406
status_details += " %s".printf(format_file_size(required_space));
408
status_code = SnapshotLocationStatus.NO_SNAPSHOTS_HAS_SPACE;
416
public void auto_remove(){
417
DateTime now = new DateTime.now_local();
419
bool show_msg = false;
422
// delete older backups - boot ---------------
424
var list = get_snapshots_by_tag("boot");
426
if (list.size > App.count_boot){
427
log_msg(_("Maximum backups exceeded for backup level") + " '%s'".printf("boot"));
428
while (list.size > App.count_boot){
429
list[0].remove_tag("boot");
430
log_msg(_("Snapshot") + " '%s' ".printf(list[0].name) + _("un-tagged") + " '%s'".printf("boot"));
431
list = get_snapshots_by_tag("boot");
435
// delete older backups - hourly, daily, weekly, monthly ---------
437
string[] levels = { "hourly","daily","weekly","monthly" };
439
foreach(string level in levels){
440
list = get_snapshots_by_tag(level);
442
if (list.size == 0) { continue; }
446
dt_limit = now.add_hours(-1 * App.count_hourly);
449
dt_limit = now.add_days(-1 * App.count_daily);
452
dt_limit = now.add_weeks(-1 * App.count_weekly);
455
dt_limit = now.add_months(-1 * App.count_monthly);
458
dt_limit = now.add_years(-1 * 10);
462
if (list[0].date.compare(dt_limit) < 0){
464
log_msg(_("Maximum backups exceeded for backup level") + " '%s'".printf(level));
466
while (list[0].date.compare(dt_limit) < 0){
467
list[0].remove_tag(level);
468
log_msg(_("Snapshot") + " '%s' ".printf(list[0].name) + _("un-tagged") + " '%s'".printf(level));
469
list = get_snapshots_by_tag(level);
474
// delete older backups - max days -------
478
foreach(var bak in snapshots){
479
if (bak.date.compare(now.add_days(-1 * App.retain_snapshots_max_days)) < 0){
480
if (!bak.has_tag("ondemand")){
483
log_msg(_("Removing backups older than") + " %d ".printf(
484
App.retain_snapshots_max_days) + _("days..."));
488
log_msg(_("Snapshot") + " '%s' ".printf(bak.name) + _("un-tagged"));
497
// delete older backups - minimum space -------
499
device.query_disk_space();
503
while ((device.size_bytes - device.used_bytes) < App.minimum_free_disk_space){
507
if (snapshots.size > 0){
508
if (!snapshots[0].has_tag("ondemand")){
511
log_msg(_("Free space is less than") + " %lld GB".printf(
512
App.minimum_free_disk_space / GB));
513
log_msg(_("Removing older backups to free disk space"));
517
snapshots[0].remove();
521
device.query_disk_space();
525
public void remove_untagged(){
526
bool show_msg = true;
528
foreach(Snapshot bak in snapshots){
529
if (bak.tags.size == 0){
532
log_msg(_("Removing un-tagged snapshots..."));
541
public bool remove_all(){
542
string timeshift_dir = snapshot_location + "/timeshift";
543
string sync_dir = snapshot_location + "/timeshift/snapshots/.sync";
545
if (dir_exists(timeshift_dir)){
547
foreach(var bak in snapshots){
554
if (dir_exists(sync_dir)){
555
if (!delete_directory(sync_dir)){
561
return delete_directory(timeshift_dir);
564
log_msg(_("No snapshots found") + " '%s'".printf(snapshot_location));
569
public bool remove_sync_dir(){
570
string sync_dir = snapshot_location + "/timeshift/snapshots/.sync";
572
if (dir_exists(sync_dir)){
573
if (!delete_directory(sync_dir)){
583
private bool delete_directory(string dir_path){
584
thr_args1 = dir_path;
589
Thread.create<void> (delete_directory_thread, true);
590
} catch (ThreadError e) {
593
log_error (e.message);
598
Thread.usleep((ulong) GLib.TimeSpan.MILLISECOND * 100);
606
private void delete_directory_thread(){
613
var f = File.new_for_path(thr_args1);
614
if(f.query_exists()){
615
cmd = "rm -rf \"%s\"".printf(thr_args1);
617
if (LOG_COMMANDS) { log_debug(cmd); }
619
Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
622
log_error(_("Failed to remove") + ": '%s'".printf(thr_args1));
628
log_msg(_("Removed") + ": '%s'".printf(thr_args1));
635
log_error(_("Directory not found") + ": '%s'".printf(thr_args1));
641
log_error (e.message);
650
public void create_symlinks(){
656
cleanup_symlink_dir("boot");
657
cleanup_symlink_dir("hourly");
658
cleanup_symlink_dir("daily");
659
cleanup_symlink_dir("weekly");
660
cleanup_symlink_dir("monthly");
661
cleanup_symlink_dir("ondemand");
665
foreach(var bak in snapshots){
666
foreach(string tag in bak.tags){
668
path = snapshot_location + "/timeshift/snapshots-%s".printf(tag);
669
cmd = "ln --symbolic \"../snapshots/%s\" -t \"%s\"".printf(bak.name, path);
671
if (LOG_COMMANDS) { log_debug(cmd); }
673
ret_val = exec_sync(cmd, out std_out, out std_err);
676
log_error(_("Failed to create symlinks") + ": snapshots-%s".printf(tag));
682
log_debug (_("Symlinks updated"));
685
public void cleanup_symlink_dir(string tag){
692
string path = snapshot_location + "/timeshift/snapshots-%s".printf(tag);
693
var f = File.new_for_path(path);
694
if (f.query_exists()){
695
cmd = "rm -rf \"%s\"".printf(path + "/");
697
if (LOG_COMMANDS) { log_debug(cmd); }
699
Process.spawn_command_line_sync(cmd, out std_out, out std_err, out ret_val);
702
log_error(_("Failed to delete symlinks") + ": 'snapshots-%s'".printf(tag));
707
f.make_directory_with_parents();
710
log_error (e.message);
716
public enum SnapshotLocationStatus{
718
-1 - device un-available, path does not exist
719
0 - first snapshot taken, disk space sufficient
720
1 - first snapshot taken, disk space not sufficient
721
2 - first snapshot not taken, disk space not sufficient
722
3 - first snapshot not taken, disk space sufficient
724
5 - hardlinks not supported
728
HAS_SNAPSHOTS_HAS_SPACE = 0,
729
HAS_SNAPSHOTS_NO_SPACE = 1,
730
NO_SNAPSHOTS_NO_SPACE = 2,
731
NO_SNAPSHOTS_HAS_SPACE = 3,
733
HARDLINKS_NOT_SUPPORTED = 5