#!/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 MDV::Snapshot::Hal; # i18n: IMPORTANT: to get correct namespace (drakconf instead of libDrakX) BEGIN { unshift @::textdomains, 'draksnapshot' } 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 $product_id = common::parse_LDAP_namespace_structure(cat_("$root/etc/product.id")); my @ordered_intervals = qw(hourly daily weekly monthly); my %default_values = ( "hourly" => 6, "daily" => 7, "weekly" => 4, ); 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{$_} ||= $default_values{$_} 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 snapshots"), 'daily' => N("Daily snapshots"), 'weekly' => N("Weekly snapshots"), 'monthly' => N("Monthly snapshots"), ); my (%entries, $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) { my @discs = map { $_->GetProperty('volume.mount_point') } find_removable_volumes($dbus); $backup_directory = $discs[0]; } } my $dialog = ugtk2->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, Gtk2::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; }, ), ]), if_(0, 0, gtknew('Title2', label => N("Number of snapshots to keep stored")), 0, gtknew('Table', col_spacings => 10, row_spacings => 5, homogeneous => 1, children => [ map { [ gtknew('Label_Left', text => $interval_titles{$_}), $entries{$_} = gtknew('SpinButton', value => $default_intervals{$_}, lower => 0, upper => 100, tip => N("The number of snapshots for this type of interval (\"%s\") that will be stored", $_) ) ]; } @ordered_intervals ])), 0, gtknew('HButtonBox', layout => 'start', border_width => 5, spacing => 5, children_loose => [ $button = gtknew($product_id->{version} =~ /200[6-8]/ ? 'Button' : '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("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(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 = $default_intervals{$_}; if_($val, join("\t", 'interval', $_, $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 save_keyword { my ($keyword, @values) = @_; my ($removed, $done); my $new_val = join('', map { "$_\n" } @values); substInFile { if (/^$keyword/ || /^#$keyword/ && !$removed) { 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() { # 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 )); } }