#!/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 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 mygtk2 qw(gtknew); #- do not import gtkadd which conflicts with ugtk2 version use ugtk2 qw(:create :dialogs :helpers :wrappers); use Gtk2::SimpleList; use interactive; require_root_capability(); ugtk2::add_icon_path("/usr/share/draksnapshot/pixmaps/"); ######### read config my @ordered_intervals = qw(hourly daily weekly monthly); my ($backup_list, $exclude_list) = map { my $key = $_; my $list = Gtk2::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 %default_intervals = ( map { if (my ($type, $interval) = /^interval\s*(\S*)\s*(\S*)/) { $type => $interval; } } grep { /^interval\s/ } cat_($config_file) ); # initialize commented out fields: $default_intervals{$_} ||= undef foreach @ordered_intervals; ######### GUI $ugtk2::wm_icon = "draksnapshot-big"; my $my_win = ugtk2->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 ? create_factory_menu($my_win->{rwindow}, @menu_items) : undef; ######### menus end my %interval_titles = ( 'hourly' => N("Hourly interval"), 'daily' => N("Daily interval"), 'weekly' => N("Weekly interval"), 'monthly' => N("Monthly interval"), ); my (%entries, $where); gtkadd($my_win->{window}, gtknew('VBox', children => [ if_(!$::isEmbedded, 0, Gtk2::Banner->new('draksnapshot-big', N("Backup snapshots configuration"))), 0, gtknew('Title1', label => N("Settings")), 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('FileSelection', title => N("Path selection"), ok_button => { clicked => sub { $where->set_text($file_dlg->get_filename); $file_dlg->destroy; }, }, cancel_button => { clicked => sub { $file_dlg->destroy }, }, ); $file_dlg->set_transient_for($my_win->{real_window}); $file_dlg->set_modal(1); $file_dlg->set_filename($where->get_text); $file_dlg->show; }, ), ]), 0, gtknew('Title2', label => N("Intervals")), 0, gtknew('Table', col_spacings => 10, row_spacings => 5, homogeneous => 1, children => [ map { [ gtknew('Label_Left', text => $interval_titles{$_}), $entries{$_} = gtknew('Entry', text => $default_intervals{$_}) ]; } @ordered_intervals ]), 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', border_width => 5, spacing => 5, children_loose => [ gtknew('Button', text => N("Apply"), clicked => \&save), gtknew('Button', text => $::isEmbedded ? N("Cancel") : N("Close"), clicked => sub { save(); quit() }) ]) ]) ); $my_win->{window}->show_all; $my_win->main; ######### callbacks & helpers sub format_list { my ($list, $o_check) = @_; gtknew('HBox', children => [ 0, gtkset_size_request(Gtk2::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() { ugtk2->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 = Gtk2::FileSelection->new(N("Path selection")); $file_dlg->set_modal(1); $file_dlg->set_transient_for($dlg); $file_dlg->show; $file_dlg->set_filename($file->get_text); $file_dlg->cancel_button->signal_connect(clicked => sub { $file_dlg->destroy }); $file_dlg->ok_button->signal_connect(clicked => sub { $file->set_text($file_dlg->get_filename); $file_dlg->destroy; }); }); gtkpack_($dlg->vbox, 0, gtknew('Title2', label => N("Path")), 0, gtknew('HBox', border_width => 18, children => [ 1, $file, 0, $browse ]), ); #$dlg->set_has_separator(0); gtkadd($dlg->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('interval', map { my $val = $entries{$_}->get_text; if_($val, join("\t", 'interval', $_, $val)); } @ordered_intervals); save_keyword('backup', map { join("\t", 'backup', @$_[0], 'localhost/') } @{$backup_list->{data}}); save_keyword('exclude', map { join("\t", 'exclude', @$_[0]) } @{$exclude_list->{data}}); save_keyword('snapshot_root', join("\t", 'snapshot_root', $where->get_text)); generate_cron_entry(); } sub save_keyword { my ($keyword, @values) = @_; my ($removed, $done); my $new_val = join('', map { "$_\n" } @values); substInFile { if (/^$keyword/) { undef $_; $removed++; } if ($removed == 1 && !$done) { $done++; $_ .= $new_val; } } $config_file; # be safe: append_to_file($config_file, $new_val) if !$done; } sub generate_cron_entry() { my %minutes = ( 'hourly' => 0, 'daily' => 50, 'weekly' => 40, 'monthly' => 30, ); my $cron_file = "$::prefix/etc/cron.d/rsnapshot"; output($cron_file, qq(# WARNING: This file is autogenerated from /etc/rsnapshot.conf. # WARNING: Please alter /etc/rsnapshot.conf instead of $cron_file # # $cron_file: crontab fragment for rsnapshot ), (map { if (my ($type, $interval) = /^interval\s*(\S*)\s*(\S*)/) { # crontab: minute hour day_month month day_of_week user command join("\t", $minutes{$type}, ($type eq 'hourly' ? "*/" . 24/$interval : 4), ($type eq 'monthly' ? 1 : '*'), '*' , ($type eq 'weekly' ? 6 : '*'), 'root', "/usr/bin/rsnapshot $type" ), "\n"; } } grep { /^interval\s/ } cat_($config_file)), ); }