#!/usr/bin/perl ################################################################################ # Backup Configuration Tool # # # # Copyright (C) 2008 Mandriva # # # # Thierry Vignaud # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License Version 2 as # # published by the Free Software Foundation. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the Free Software # # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ################################################################################ use warnings; use strict; use lib qw(/usr/lib/libDrakX); use standalone; #- warning, standalone must be loaded very first, for 'explanations' use common; use interactive; use MDV::Snapshot::Common; use MDV::Snapshot::Hal; # i18n: IMPORTANT: to get correct namespace (drakconf instead of libDrakX) BEGIN { unshift @::textdomains, 'draksnapshot' } use mygtk3 qw(gtknew); #- do not import gtkadd which conflicts with ugtk2 version use ugtk3 qw(:create :dialogs :helpers :wrappers); use Gtk3::SimpleList; use interactive; ugtk3::add_icon_path("/usr/share/draksnapshot/pixmaps/"); ######### read config my @ordered_intervals = qw(hourly daily weekly monthly); my %default_values = ( "hourly" => 6, "daily" => 7, "weekly" => 4, "monthly" => "", ); my ($backup_list, $exclude_list) = map { my $key = $_; my $list = Gtk3::SimpleList->new('' => 'text'); # properly size when not embedded: $list->set_size_request($::isEmbedded ? -1 : 500, 100); $list->set_headers_visible(0); @{$list->{data}} = map { [ (split(/\s/, $_, 3))[1] ] } grep { /^$key\s/ } cat_($config_file); $list; } qw(backup exclude); my $interval_exists; my %default_intervals = ( map { if (my ($type, $retain) = /^retain\s*(\S*)\s*(\S*)/) { $interval_exists = 1; $type => $retain; } } grep { /^retain\s/ } cat_($config_file) ); if ($interval_exists) { # If there is some interval in the config file, keep them as is $default_intervals{$_} ||= 0 foreach @ordered_intervals; } else { # Else we had broken it (#58534) so restore defaults %default_intervals = %default_values; } ######### GUI $ugtk3::wm_icon = "draksnapshot-big"; my $my_win = ugtk3->new(N("Backup snapshots configuration")); unless ($::isEmbedded) { $my_win->{window}->set_border_width(5); $my_win->{window}->set_default_size(540,460); } $my_win->{window}->signal_connect(delete_event => \&quit); ### menus definition # the menus are not shown # but they provides shiny shortcut like C-q my @menu_items = ( { path => N("/_File"), item_type => '' }, { path => N("/File/_Quit"), accelerator => N("Q"), callback => \&quit }, { path => N("/_Help"), item_type => '' }, { path => N("/Help/_About...") } ); my $_menubar = $::isEmbedded ? Gtk3::MenuBar->new($my_win->{rwindow}, @menu_items) : undef; #my $_menubar = create_factory_menu($my_win->{rwindow}, @menu_items); ######### menus end my $where; # if not configured, just default where will be mounted the discs by HAL: if (!$backup_directory || $::testing) { my $dbus = get_system_bus(); if ($dbus) {#decting by dbus is broken # my @discs = map { $_ } eval { find_removable_volumes($dbus) }; # $backup_directory = $discs[0]; } if (!$backup_directory) #not defined either by common.pm, rsnapshot.conf, or hal/udisks2 (current situation); Set a dummy path to be changed by user { $backup_directory="~/not_existing_path"; } } my $dialog = ugtk3::new(N("Backup snapshots configuration")); my $d_window = $dialog->{window}; gtkadd($dialog->{rwindow}, gtknew('VBox', children => [ 0, gtknew('Title2', label => N("Backup list")), 1, format_list($backup_list, 1), 0, gtknew('Title2', label => N("Exclude list")), 1, format_list($exclude_list), 0, gtknew('HButtonBox', layout => 'end', children_tight => [ gtknew('Button', text => N("Close"), clicked => sub { $d_window->hide }), ]), ])); my $is_enabled = to_bool(glob("/etc/cron.*/rsnapshot")); my ($box, $button); gtkadd($my_win->{window}, gtknew('VBox', children => [ if_(!$::isEmbedded, 0, Gtk3::Banner->new('draksnapshot-big', N("Backup snapshots configuration"))), 0, gtknew('Title1', label => N("Settings")), 0, gtknew('CheckButton', text => N("Enable Backups"), active_ref => \$is_enabled, toggled => sub { my ($w) = @_; return if !$w || !$box; $box->set_sensitive($w->get_active); }), 0, $box = gtknew('VBox', sensitive => $is_enabled, children => [ 0, gtknew('CheckButton', text => N("Backup the whole system"), toggled => sub { my ($w) = @_; $button->set_sensitive(!$w->get_active); }), 0, gtknew('HBox', spacing => 5, children => [ 0, gtknew('Label_Left', text => N("Where to backup")), 1, $where = gtknew('Entry', text => $backup_directory), 0, gtknew('Button', text => N("Browse"), clicked => sub { my $file_dlg; $file_dlg = gtknew('FileChooserDialog', title => N("Path selection"), action => 'select_folder', transient_for => $my_win->{real_window}, modal => 1); $file_dlg->set_filename($where->get_text); $file_dlg->show; my $answer = $file_dlg->run; if ($answer eq 'ok') { $where->set_text($file_dlg->get_filename); } $file_dlg->destroy; }, ), ]), 0, gtknew('HButtonBox', layout => 'start', border_width => 5, spacing => 5, children_loose => [ # does not exist before 2009.0, if you need it, you can revert to Button type if needed ( CS4 ) $button = gtknew('Install_Button', text => N("Advanced"), clicked => sub { $d_window->show }), ]), ]), 0, gtknew('HButtonBox', layout => 'end', border_width => 5, spacing => 5, children_loose => [ gtknew('Button', text => N("Restore"), clicked => \&restore), gtknew('Button', text => N("Apply"), clicked => \&save), gtknew('Button', text => $::isEmbedded ? N("Cancel") : N("Close"), clicked => sub { quit() }) ]) ]) ); $my_win->{window}->set_size_request(550, -1); $my_win->{window}->show_all; $my_win->main; ######### callbacks & helpers sub format_list { my ($list, $o_check) = @_; gtknew('HBox', children => [ 0, gtkset_size_request(Gtk3::Alignment->new(0, 0, 0, 0), 35, 1), 1, gtknew('ScrolledWindow', child => $list), 0, gtknew('VBox', border_width => 5, spacing => 5, children_tight => [ # FIXME: add "up" & "down" buttons? "edit" button? gtknew('Button', text => N("Add"), clicked => sub { add($list, $o_check); }), gtknew('Button', text => N("Remove"), clicked => sub { my ($tree, $iter) = $list->get_selection->get_selected; return if !$iter; #my $removed_idx = $tree->get($iter, 5); $tree->remove($iter); #sensitive_buttons(0); #$modified++; }), ]), ], ); } sub quit() { ugtk3->exit(0) } sub add { my ($list, $check, $o_iter) = @_; my $model = $list->get_model; my $dlg = gtknew('Dialog', transient_for => $my_win->{real_window}, title => N("Add")); my $browse = gtknew('Button', text => N("browse")); my $file = gtknew('Entry', $o_iter ? (text => $model->get($o_iter, 1)) : ()); my $alrd_exsts = defined $o_iter; $browse->signal_connect(clicked => sub { my $file_dlg = Gtk3::FileChooserDialog->new(N("Path selection"), $dlg, 'select_folder', N("Cancel") => 'cancel', N("OK") => 'ok'); $file_dlg->set_modal(1); $file_dlg->set_transient_for($dlg); $file_dlg->show; $file_dlg->set_filename($file->get_text); my $answer_add=$file_dlg->run; if ($answer_add eq 'cancel') { $file_dlg->destroy; } else { if ($answer_add eq 'ok') { $file->set_text($file_dlg->get_filename); $file_dlg->destroy; } } }); gtkpack_($dlg->get_content_area, 0, gtknew('Title2', label => N("Path")), 0, gtknew('HBox', border_width => 18, children => [ 1, $file, 0, $browse ]), ); #$dlg->set_has_separator(0); gtkadd($dlg->get_action_area, create_okcancel(my $w = { cancel_clicked => sub { $dlg->destroy }, ok_clicked => sub { my $path = $file->get_text; if ($check && $path !~ m!^/!) { err_dialog(N("Warning"), N("The first character of the path must be a slash (\"/\"):\n\"%s\"", $path)); return 1; } # create new item if needed (that is when adding a new one) at end of list if (!$o_iter) { push @{$list->{data}}, $path; } $dlg->destroy; } }, ), ); $w->{ok}->set_sensitive(!$model->get($o_iter, 0)) if $alrd_exsts; $dlg->show_all; } sub save() { save_keyword('retain', map { my $val = $default_intervals{$_}; if_($val, join("\t", 'retain', $_, $val)); } @ordered_intervals); if (!$button->get('sensitive')) { # Mandriva defaults @{$backup_list->{data}} = '/'; #qw(/bin /boot /etc /home /lib /lib64 /opt); @{$exclude_list->{data}} = qw(/media /mnt /proc /sys /tmp /var/run /var/tmp *~); } my $where2snapshot = $where->get_text; save_keyword('backup', map { join("\t", 'backup', @$_[0], 'localhost/') } @{$backup_list->{data}}); save_keyword('no_create_root', "no_create_root\t1"); save_keyword('exclude', map { join("\t", 'exclude', $_) } uniq($where2snapshot, map { @$_[0] } @{$exclude_list->{data}})); save_keyword('snapshot_root', join("\t", 'snapshot_root', $where2snapshot)); generate_cron_entry(); } sub fork_exec { my $pid = run_program::raw({ detach => 1 }, @_); return $pid; } sub restore() { fork_exec('/usr/sbin/draksnapshot-restore'); } sub save_keyword { my ($keyword, @values) = @_; my ($removed, $done); my $new_val = join('', map { "$_\n" } @values); substInFile { if (/^$keyword/ || /^#$keyword/ && !$removed) { undef $_; $removed++; } if ($removed && !$done) { $done++; $_ .= $new_val; } } $config_file; # be safe: append_to_file($config_file, $new_val) if !$done; } sub generate_cron_entry() { # handle upgrading from old scheme to new scheme: unlink("$::prefix/etc/cron.d/rsnapshot"); my $cron_file = "$::prefix/etc/cron.\%s/rsnapshot"; if (!$is_enabled) { # Mandriva defaults unlink(sprintf($cron_file, $_)) foreach qw(hourly daily weekly monthly); return; } foreach my $type (qw(hourly daily weekly monthly)) { my $file = sprintf($cron_file, $type); output_with_perm($file, 0755, qq(#!/bin/sh # WARNING: This file is autogenerated from /etc/rsnapshot.conf. # WARNING: Please alter /etc/rsnapshot.conf instead of $cron_file # Then rerun draksnapshot-config # # $file: crontab fragment for rsnapshot /usr/bin/rsnapshot $type > /dev/null )); } }