#!/usr/bin/perl -w
# ----------------------------------------------------------------------
#   Redo Backup
#   A simple GUI interface that allows bare-metal backup and restore.
# ----------------------------------------------------------------------
#   Copyright (C) 2012 RedoBackup.org
#   French translation / traduction française :
#   Philippe RONFLETTE & Christophe LOUVET
#   Original files with extension / fichiers originaux avec extension .orig
# ----------------------------------------------------------------------
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   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, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------

use strict;
use utf8;
use Encode;
use warnings;
use Glib qw{ TRUE FALSE };
use Gtk2 '-init';
use XML::Simple;
use Gtk2::SimpleList;
use IO::Handle;
use Data::Dumper;


##### Constants found in our program ###################################
use constant VERSION       => `cat /usr/share/redo/VERSION`;
use constant CD_LABEL      => `cat /usr/share/redo/TITLE`;
use constant MOUNT_POINT   => "/mnt/backup";
use constant DATA_DIR      => "/usr/share/redo/";
use constant APP_ICON      => DATA_DIR."redobackup-icon.png";
use constant GLADE_UI      => "redobackup.glade";
use constant MAX_WIDTH     => 96;
use constant FS_SUPPORT    => ( "ext2",
                                "ext3",
                                "ext4",
                                "fat",
                                "fat12",
                                "fat16",
                                "fat32",
                                "jfs",
                                "vfat",
                                "ntfs",
                                "hfsp",
                                "reiser4",
                                "reiserfs",
                                "ufs",
                                "vmfs",
                                "xfs" );
use constant BACKUP_STEPS  => ( "Etape 1: Choix de la source",
                                "Etape 2: Choix des partitions à sauvegarder",
                                "Etape 3: Sélectionnez la destination de la sauvegarde",
                                "Etape 4: Sélectionnez le répertoire de sauvegarde",
                                "Etape 5: Donnez un nom à votre sauvegarde",
                                "Création de la sauvegarde" );
use constant RESTORE_STEPS => ( "Etape 1 : Sélectionnez le périphérique source",
                                "Etape 2: Sélectionnez l'image à restaurer",
                                "Etape 3: Sélectionnez le disque à restaurer",
                                "Restauration à partir d'une sauvegarde" );
use constant USB_STEPS     => ( "Etape 1: Sélectionnez le périphérique de destination",
                                "Etape 2: Installation sur la clé USB" );


##### Test code ########################################################
#$ARGV[0] = 'restore';


##### Main code ########################################################
$| = 1;
main();
exit(0);


##### Subroutines and callbacks ########################################
sub main {
  # Start the main application
  our $builder;
  my $window;
  $builder = Gtk2::Builder->new();
  $builder->add_from_file(DATA_DIR.GLADE_UI);
  $window = $builder->get_object('main_app');
  $builder->connect_signals(undef);
  # Change the background of the header to white
  my $eventbox = $builder->get_object('main_image_eventbox');
  $eventbox->modify_bg('GTK_STATE_NORMAL', Gtk2::Gdk::Color->new(0xffff,0xffff,0xffff));
  $builder->get_object('main_tabs')->set_show_tabs(FALSE);
  $builder->get_object('backup_tabs')->set_show_tabs(FALSE);
  $builder->get_object('backup_tabs')->set_show_border(FALSE);
  $builder->get_object('restore_tabs')->set_show_tabs(FALSE);
  $builder->get_object('restore_tabs')->set_show_border(FALSE);
  $builder->get_object('usb_tabs')->set_show_tabs(FALSE);
  $builder->get_object('usb_tabs')->set_show_border(FALSE);
  my $ver = VERSION;
  chomp($ver);
  set_status("Version $ver");
  $window->maximize();
  $window->show();
  my $mode = '';
  if (defined($ARGV[0])) { $mode = $ARGV[0]; }
  if ($mode eq 'backup') { backup_mode(); }
  if ($mode eq 'restore') { restore_mode(); }
  `notify-send 'Bienvenue dans Redo' 'Beaucoup d'autres outils sont disponibles à partir du menu principal accessible dans l'angle en bas à gauche' -i info`;
  Gtk2->main;
}


##### Backup Logic #####################################################
sub backup_mode {
  # Enter Backup mode
  our %status;
  $status{'action'} = 'backup';
  backup_step(1);
}

sub backup_step {
  # Advance to the next Backup Step
  my $step = $_[0];
  if (!defined($step)) { $step = 1; }
  our $builder;
  our %status;
  set_main_title("Sauvegarde");
  $builder->get_object('main_tabs')->set_current_page(1);
  $step = $builder->get_object('backup_tabs')->get_current_page()+1;
  set_subtitle((BACKUP_STEPS)[$step-1]);
  if ($step == 1) {
    # Backup Step 1
    find_local_drives();
    print_drive_list();
    set_drive_dropdown('backup_source');
    $builder->get_object('backup_button_next')->set_sensitive(TRUE);
  } elsif ($step == 2) {
    # Backup Step 2
    $status{'backup_drive'} = $builder->get_object('backup_source')->get_active_text();
    set_partition_list('backup_partitions', $status{'backup_drive'});
    $builder->get_object('backup_button_next')->set_sensitive(TRUE);
  } elsif ($step == 3) {
    # Backup Step 3
    backup_dest_changed();
    set_partition_dropdown('backup_dest_local');
    set_share_dropdown('backup_dest_network');
    $builder->get_object('backup_button_next')->set_sensitive(TRUE);
  } elsif ($step == 4) {
    # Backup Step 4
    $builder->get_object('backup_button_next')->set_sensitive(FALSE);
    if ($builder->get_object('backup_dest_use_local')->get_active()) {
      my ($type, $source) = split(/\|/, $builder->get_object('backup_dest_local')->get_active_text());
      $status{'mount_type'} = $type;
      $status{'mount_source'} = $source;
    } else {
      my ($type, undef) = split(/\|/, $builder->get_object('backup_dest_network')->get_active_text());
      $status{'mount_type'} = $type;
      $status{'mount_source'} = $builder->get_object('backup_dest_server')->get_text();
      $status{'mount_user'} = $builder->get_object('backup_dest_user')->get_text();
      $status{'mount_pass'} = $builder->get_object('backup_dest_pass')->get_text();
      $status{'mount_domain'} = $builder->get_object('backup_dest_domain')->get_text();
    }
    my $mount_ok = mount_data();
    if ($mount_ok==0) {
      message_box("Impossible d'accéder au disque de destination. Si le périphérique est partagé sur un réseau, assurez-vous que le nom d'utilisateur et le mot de passe sont corrects et essayez à nouveau.");
      $step = $builder->get_object('backup_tabs')->set_current_page(2);
      set_subtitle((BACKUP_STEPS)[2]);
    } else  {
      $builder->get_object('backup_folder')->set_text('/');
      $builder->get_object('backup_folder_browse')->grab_focus();
      my $free_mb = get_free_space(1);
      if ($free_mb < 1000) { message_box("Attention! Ce disque a seulement ".$free_mb."MB d'espace libre. Assurez-vous que cela sera suffisant pour votre sauvegarde."); }
    }
    $builder->get_object('backup_button_next')->set_sensitive(TRUE);
  } elsif ($step == 5) {
    # Backup Step 5
    my (undef,undef,undef,$d,$m,$y,undef,undef,undef) = localtime(time);
    $y = sprintf("%04d", $y+1900);
    $m = sprintf("%02d", $m+1);
    $d = sprintf("%02d", $d);
    $builder->get_object('backup_name')->set_text("$y$m$d");
    $builder->get_object('backup_name')->grab_focus();
    $builder->get_object('backup_button_next')->set_sensitive(TRUE);
  } elsif ($step == 6) {
    # Backup Step 6
    $builder->get_object('backup_button_next')->hide();
    # Start the process
    refresh_window();
    sleep(0.25);
    do_backup();
  }
  refresh_window();
}


sub backup_scan_network {
  # Scan the network and add shares to list
  scan_network('backup_dest_network');
}

sub backup_dest_changed {
  # The destination was changed (local/network)
  our $builder;
  my $local = $builder->get_object('backup_dest_use_local')->get_active();
  if ($local!=1) {
    $builder->get_object('backup_frame_local')->hide();
    $builder->get_object('backup_frame_network')->show();
  } else {
    $builder->get_object('backup_frame_network')->hide();
    $builder->get_object('backup_frame_local')->show();
  }
  refresh_window();
}

sub backup_dest_network_changed {
  # The network dest was changed
  our $builder;
  my $share = $builder->get_object('backup_dest_network')->get_active_text();
  if (defined($share)) {
    my ($type, $server) = split(/\|/, $share);
    my $server_editable = TRUE;
    my $domain_editable = TRUE;
    if ($server eq '') {
      if ($type eq 'FTP') {
        # It's a manually-entered FTP share
        $server = get_subnet();
        $server =~ s/\d+$//g;
      } else {
        # It's a manually-entered SMB share
        $server = "//ADDRESS/FOLDER";
      }
    } else {
      # It's an automatically-found share
      $server =~ s/^smb:|^ftp:\/\///g;
      $server_editable = FALSE;
    }
    if ($type ne 'SMB') { $domain_editable = FALSE; }
    $builder->get_object('backup_dest_server')->set_text($server);
    $builder->get_object('backup_dest_server')->set_sensitive($server_editable);
    $builder->get_object('backup_dest_domain')->set_sensitive($domain_editable);
  }
}

sub do_backup {
  # Begin the backup process
  our %drives;
  our %status;
  our $builder;
  $builder->get_object('backup_progress')->set_sensitive(TRUE);
  # Get partition selections and output folder
  my $src = $status{'backup_drive'};
  my $dest = MOUNT_POINT.$builder->get_object('backup_folder')->get_text();
  print "*** Sauvegarde $src vers $dest ***\n\n";
  $dest =~ s/\/$//;
  $dest =~ s/ /\\ /g;
  my $file = $builder->get_object('backup_name')->get_text();
  my $src_drive = $src;
  $src_drive =~ s/\d//g;
  $src_drive = "/dev/".$src_drive;
  # Save MBR
  system("dd if=$src_drive of=$dest/$file.mbr bs=32768 count=1");
  system("sfdisk -dx $src_drive > $dest/$file.sfdisk");
  print "\t* MBR et table de partitions de $src_drive sauvés sur $dest\n";
  # Save size of source drive in blocks
  my $bd = `fdisk -l $src_drive | grep '$src_drive:'`;
  my (undef, $bytes) = split(", ", $bd);
  $bytes =~ s/\D//g;
  system("echo $bytes > $dest/$file.size");  
  print "\t* Taille de $src_drive ($bytes bytes) sauvé sur $dest/$file.size\n";
  # Save list of partitions we will be backing up
  my @partlist = get_selected_partitions('backup_partitions');
  my $partlist_str = join("\n", @partlist);
  save_file($partlist_str, "$dest/$file.backup");
  # Get the total bytes we're going to be saving
  #print Dumper(%drives);
  my $total_bytes = 0;
  foreach my $part (@partlist) {
    my ($dn, $pn) = get_drivenum_partnum($part);
    my $dev = $src_drive;
    $dev =~ s/^\/dev\///g;
    my $part_bytes = $drives{$dev}{'parts'}{$pn}{'bytes'};
    $total_bytes += $part_bytes;
  }
  $status{'start'} = time();
  $status{'part_list'} = \@partlist;
  $status{'last_part'} = $partlist[-1];
  $status{'total_parts'} = scalar(@partlist);
  $status{'current_part'} = 0;
  $status{'total_bytes'} = $total_bytes;
  $status{'done_bytes'} = 0;
  $status{'frame'} = 0;
  $status{'free'} = get_free_space();
  print "\t* Liste des partitions sauvées sur $dest/$file.backup\n";
  # Create backup by calling progress sub
  print "\t* Nombre de partitions à sauver : ".$status{'total_parts'}."\n";
  print "\t* Taille totale de la sauvegarde : ".$status{'total_bytes'}."\n";
  Glib::Timeout->add(50, \&update_backup_progress);
  refresh_window();
}

sub update_backup_progress {
  # Update the backup progress bar
  our $PROGRESS;
  our %drives;
  our %status;
  our $builder;
  my $progress_bar = $builder->get_object('backup_progress');
  my $data = undef;
  our $i;
  my $part = $status{'part_list'}[$status{'current_part'}];
  my ($dn, $pn) = get_drivenum_partnum($part);
  my $src = $status{'backup_drive'};
  my $dest = MOUNT_POINT.$builder->get_object('backup_folder')->get_text();
  $dest =~ s/\/$//;
  $dest =~ s/ /\\ /g;
  my $file = $builder->get_object('backup_name')->get_text();
  my $fs = lc($drives{$src}{'parts'}{$pn}{'fstype'});
  # See if the filehandle is open
  if (!defined($PROGRESS)) {
    # No command is being executed
    my $tool = which_backup_tool($fs);
    set_status("Préparation de la sauvegarde du disque $dn, Partition $pn...");
    print "*** Processus $part ($fs) en cours $tool...\n";
    system("umount /dev/$part 2>&1");
    print "*** Exécute : ( $tool -c -F -L /partclone.log -s /dev/$part | gzip -c --fast | split -d -a 3 -b 2048m - /$dest/$file"."_part$pn. ) 2>&1 |\n";
    open $PROGRESS, "( $tool -c -F -L /partclone.log -s /dev/$part | gzip -c --fast | split -d -a 3 -b 2048m - /$dest/$file"."_part$pn. ) 2>&1 |";
    sleep(0.5);
    return TRUE;
  } else {
    # Read available data and append it to $status{'command_output'} until a newline is reached
    my $char = '';
    do {
      $char = getc($PROGRESS);
      if (!defined($char)) {
        return 1;
      } else {
        if (($char eq "\n") || ($char eq "\r") || (ord($char)==27)) {
          undef($char);
        } else {
          $status{'command_output'} .= $char;
        }
      }
    } while (defined($char));
  }
  # Try to read command progress and append it to the data
  $data = $status{'command_output'};
  undef($status{'command_output'});
  if (defined($data)) {
    # Remove newlines and lines that are all spaces
    $data =~ s/\n|\r|\[A|^\s+$//g;
    refresh_window();
    $i++;
    # Split it up and show the results    
    if ($data =~ m/Starting to clone device/) {
      # Starting a filesystem
      my (undef, $current_part) = split(/\(\/dev\//, $data);
      $current_part =~ s/\).*$//g;
      $status{'current_part_device'} = $current_part;
      print ">>> Démarrage de la sauvegarde $status{'current_part_device'}\n";
      print ">>> Liste des partitions :\n";
      foreach my $item (@{$status{'part_list'}}) {
        my $msg = '';
        if ($item eq $current_part) { $msg = "(En cours)"; }
        print "\t* $item $msg\n";
      }
    } elsif ($data =~ m/Device size: /) {
      # Device size reported
      my (undef, $dev_size) = split(/: /, $data);
      $dev_size =~ s/^\s+|\s+$//g;
      print(">>> Taille du périphérique : $dev_size\n");
    } elsif ($data =~ m/Space in use: /) {
      # Device used space reported
      my (undef, $dev_used) = split(/: /, $data);
      $dev_used =~ s/^\s+|\s+$//g;
      print(">>> Espace utilisé : $dev_used\n");
    } elsif ($data =~ m/^.*\.c.*: /) {
      # A warning was encountered
      my (undef, $warning) = split(/: /, $data);
      print(">>> Attention: $warning\n");
      set_status("Attention : $warning");
    } elsif ($data =~ m/Reading Super Block/) {
      # Reading super block
      print(">>> Lecture des informations du disque\n");
      set_status("Lecture des infromations du disque $dn, Partition $pn...");
    } elsif ($data =~ m/Calculating bitmap/) {
      # Calculating bitmap
      print(">>> Calculating bitmap\n");
      set_status("Calcul de la taille de l'image pour le disque $dn, Partition $pn...");
    } elsif ($data =~ m/Elapsed.+\%$/) {
      # Calculating bitmap, percentage given
      #Elapsed: 00:00:03, Remaining: 00:00:00, Completed:  93.41%
      my $elapsed = substr($data,9,8);
      my $remaining = substr($data,30,8);
      my $pct = substr($data,51,6);
      $pct =~ s/\s+//g;
      $builder->get_object('backup_progress_status')->set_text('Lecture des informations pour la partition '.($status{'current_part'}+1)." de ".$status{'total_parts'});
      print ">>> Calcul du temps de lecture: $pct% Restant: $remaining\n";
      set_status("Lecture de l'image pour le disque $dn, partition $pn ($pct% done, $remaining remaining)");
    } elsif ($data =~ m/Elapsed.+,$/) {
      # Backup progress line
      $status{'frame'}++;
      if ($status{'frame'} >= 10) {
        $status{'frame'} = 0;
        $status{'free'} = get_free_space();
      }
      my $elapsed = substr($data,9,8);
      my $remaining = substr($data,30,8);
      my $pct = substr($data,51,6);
      my $rate = substr($data,60,12);
      $pct =~ s/\s+//g;
      $rate =~ s/\s+//g;
      print ">>> Passé: *$elapsed* | Restant: *$remaining* | Percent: *$pct* | Speed: *$rate*\n";
      $builder->get_object('backup_progress_status')->set_text("Partition ".($status{'current_part'}+1)." de ".$status{'total_parts'}." (".$pct."%)\n$elapsed Passé\n$remaining Restant");
      # Base overall percentage on number of total bytes
      my $current_bytes = int(($pct/100) * $drives{$src}{'parts'}{$pn}{'bytes'}) + $status{'done_bytes'};
      my $overall_pct = ($current_bytes / $status{'total_bytes'}) + (($pct / 100) / $status{'total_bytes'});
      if ($overall_pct > 1) { $overall_pct = 1; }
      set_status("Sauve la partition ".($status{'current_part'}+1)." (".$drives{$src}{'parts'}{$pn}{'size'}.") at $rate (".$status{'free'}." free space)");
      print ">>> Disque : $src | Partition: $pn | Current bytes: $current_bytes | Overall: $overall_pct\n";
      $progress_bar->set_fraction($overall_pct);
      $progress_bar->set_text(sprintf("%.2f", $overall_pct * 100)."% Complet");
    } elsif ($data =~ m/successfully cloned/) {
      set_status("Partition sauvée.");
      print ">>> Partition $status{'current_part_device'} complet.\n";
      print "\tCurrent part: ".($status{'current_part'}+1)."\n";
      print "\tNombre de partitions : $status{'total_parts'}\n";
      if ($status{'current_part'}==$status{'total_parts'}-1 ) {
        $builder->get_object('backup_progress_status')->set_text('');
        $progress_bar->set_fraction(1);
        set_status("Sauvegarde terminée.");
        my @lines = split(/\n/, $data);
        my $start = FALSE;
        $builder->get_object('backup_button_cancel')->set_label('Quitter');
        refresh_window();
        # Unmount stuff
        system("sync");
        sleep(0.5);
        system("umount ".MOUNT_POINT);
        beeper('done');
        my $elapsed = sprintf("%.1f", (time()-$status{'start'})/60);
        message_box("Sauvegarde faite en $elapsed minutes.");
        system("notify-send -i ".APP_ICON." 'Sauvegarde réalisée avec succès.'");
        print ">>> Operation complete.\n";
        return 0;
      } else {
        close $PROGRESS;
        undef($PROGRESS);
        # Need to add the size of the partition to the total bytes completed
        $status{'done_bytes'} += $drives{$src}{'parts'}{$pn}{'bytes'};
        print ">>> Backed up ".$status{'done_bytes'}." of ".$status{'total_bytes'}." total bytes.\n";
        $status{'current_part'}++;
        print ">>> Advancing to next partition.\n";
        return TRUE;
      }
    } else {
      if (length($data)>0) {
        print "$data\n";
        if ($data =~ m/error|warning/i) {
          error_message($data);
        }
      }
    }
  }
  return TRUE;   
}

sub backup_name_changed {
  # Make sure the backup name is valid
  our $builder;
  my $val = FALSE;
  my $backup_name = $builder->get_object('backup_name')->get_text();
  $backup_name =~ s/[^\w|-]|_+//g;
  $builder->get_object('backup_name')->set_text($backup_name);
  if ($backup_name ne '') {
    $val = TRUE;  
  } else {
    $val = FALSE;
  }
  $builder->get_object('backup_button_next')->set_sensitive($val);
}

sub select_backup_folder {
  # Open the folder selection dialog
  our $builder;
  my $folder_chooser = Gtk2::FileChooserDialog->new("Sélectionner un répertoire", $builder->get_object('main_app'), "select-folder", "Abandon" => "cancel", "Sauver ici" => "ok");
  $folder_chooser->set_modal(TRUE);
  $folder_chooser->set_current_folder(MOUNT_POINT."/".$builder->get_object('backup_folder')->get_text());
  my $response = $folder_chooser->run();
  if ($response eq 'ok') {
    my $current_folder = $folder_chooser->get_current_folder();
    if (substr($current_folder,0,length(MOUNT_POINT)) eq MOUNT_POINT) {
      $current_folder = substr($current_folder,length(MOUNT_POINT));
      if ($current_folder eq '') { $current_folder = '/'; }
    } else {
      error_message("Vous devez sélectionner un dossier dans ".MOUNT_POINT."'.".chr(13).chr(13)."Veuillez choisir un autre dossier.");
      $current_folder = '/';
    }
    $builder->get_object('backup_folder')->set_text($current_folder);
  }
  $folder_chooser->destroy();
}


##### Restore Logic ####################################################
sub restore_mode {
  # Enter Restore mode
  our %status;
  $status{'action'} = 'restore';
  restore_step(1);
}

sub restore_step {
  # Advance to the next Restore Step
  my $step = $_[0];
  if (!defined($step)) { $step = 1; }
  our $builder;
  our %status;
  set_main_title("Restore");
  $builder->get_object('main_tabs')->set_current_page(2);
  $step = $builder->get_object('restore_tabs')->get_current_page()+1;
  set_subtitle((RESTORE_STEPS)[$step-1]);
  if ($step == 1) {
    # Restore Step 1
    restore_source_changed();
    find_local_drives();
    set_partition_dropdown('restore_source_local');
    set_share_dropdown('restore_source_network');
    $builder->get_object('restore_button_next')->set_sensitive(TRUE);
  } elsif ($step == 2) {
    # Restore Step 2
    $builder->get_object('restore_button_next')->set_sensitive(FALSE);
    if ($builder->get_object('restore_source_use_local')->get_active()) {
      my ($type, $source) = split(/\|/, $builder->get_object('restore_source_local')->get_active_text());
      $status{'mount_type'} = $type;
      $status{'mount_source'} = $source;
    } else {
      my ($type, undef) = split(/\|/, $builder->get_object('restore_source_network')->get_active_text());
      $status{'mount_type'} = $type;
      $status{'mount_source'} = $builder->get_object('restore_source_server')->get_text();
      $status{'mount_user'} = $builder->get_object('restore_source_user')->get_text();
      $status{'mount_pass'} = $builder->get_object('restore_source_pass')->get_text();
      $status{'mount_domain'} = $builder->get_object('restore_source_domain')->get_text();
    }
    my $mount_ok = mount_data();
    if ($mount_ok==0) {
      message_box("Accès impossible au disque source. Si le périphérique est partagé sur un réseau, assurez-vous que le nom d'utilisateur et le mot de passe sont corrects et essayez à nouveau.");
      $step = $builder->get_object('restore_tabs')->set_current_page(0);
      set_subtitle((RESTORE_STEPS)[0]);
    } else  {
      $builder->get_object('restore_file')->set_current_folder(MOUNT_POINT);
      $builder->get_object('restore_filefilter')->set_name('Backup Images');
      $builder->get_object('restore_filefilter')->add_pattern('*.backup');
    }
    $builder->get_object('restore_button_next')->set_sensitive(TRUE);
  } elsif ($step == 3) {
    # Restore Step 4
    set_drive_dropdown('restore_dest');
    $builder->get_object('restore_button_next')->set_sensitive(TRUE);
  } elsif ($step == 4) {
    # Restore Step 5
    $builder->get_object('restore_button_next')->hide();
    $builder->get_object('restore_progress')->set_sensitive(FALSE);
    refresh_window();
    do_restore();
    refresh_window();
    $builder->get_object('restore_button_cancel')->set_label('Quitter');
  }
  refresh_window();
}

sub restore_scan_network {
  # Scan the network and add shares to list
  scan_network('restore_source_network');
}

sub restore_source_changed {
  # The source was changed (local/network)
  our $builder;
  my $local = $builder->get_object('restore_source_use_local')->get_active();
  if ($local!=1) {
    $builder->get_object('restore_frame_local')->hide();
    $builder->get_object('restore_frame_network')->show();
  } else {
    $builder->get_object('restore_frame_network')->hide();
    $builder->get_object('restore_frame_local')->show();
  }
  refresh_window();
}

sub restore_source_network_changed {
  # The network source was changed
  our $builder;
  my $share = $builder->get_object('restore_source_network')->get_active_text();
  if (defined($share)) {
    my ($type, $server) = split(/\|/, $share);
    my $server_editable = TRUE;
    my $domain_editable = TRUE;
    if ($server eq '') {
      if ($type eq 'FTP') {
        # It's a manually-entered FTP share
        $server = get_subnet();
        $server =~ s/\d+$//g;
      } else {
        # It's a manually-entered SMB share
        $server = "//HOST/FOLDER";
      }
    } else {
      # It's an automatically-found share
      $server =~ s/^smb:|^ftp:\/\///g;
      $server_editable = FALSE;
    }
    if ($type ne 'SMB') { $domain_editable = FALSE; }
    $builder->get_object('restore_source_server')->set_text($server);
    $builder->get_object('restore_source_server')->set_sensitive($server_editable);
    $builder->get_object('restore_source_domain')->set_sensitive($domain_editable);
  }
}

sub restore_file_changed {
  # Make sure the restore file is valid
  our $builder;
  my $val = FALSE;
  my $restore_file = $builder->get_object('restore_file')->get_filename();
  if (!defined($restore_file)) { $restore_file = ''; }
  if ($restore_file ne '') {
    $val = TRUE;  
  } else {
    $val = FALSE;
  }
  $builder->get_object('restore_button_next')->set_sensitive($val);
}

sub do_restore {
  # Restore from the backup
  our $builder;
  our %status;
  our %drives;
  our $PROGRESS;
  set_status("Préparation du disque de destination...");
  $builder->get_object('restore_progress')->set_sensitive(TRUE);
  my $src = $builder->get_object('restore_file')->get_filename();
  my $dest_drive = $builder->get_object('restore_dest')->get_active_text();
  $src =~ s/\.backup$//g;
  $src =~ s/ /\\ /g;
  my $src_mbr = $src.'.mbr';
  my $src_parts = $src.'.backup';
  $src_parts =~ s/\\//g;
  my $src_size = $src.'.size';
  my $src_sfdisk = $src.'.sfdisk';
  # Warn if we are restoring to the source drive
  my $src_drive = $status{'mount_source'};
  $src_drive =~ s/\d//g;
  print "Compare $src_drive à $dest_drive...\n";
  if ($src_drive eq $dest_drive) {
    my $response = get_confirmation("Etes-vous sûr de vouloir restaurer l'image sur le  disque où se trouve l'image source ?\n\nSauf si vous savez ce que vous faites, cela se traduira par la perte de toutes les données sur le disque, y compris l'image de sauvegarde!");
    if ($response ne 'yes') {
      on_main_app_destroy();
      die("Aborting.\n");
    }
  }
  # Get sizes of original and destination drives in bytes
  my $src_bytes = `cat $src_size`;
  $src_bytes =~ s/\D//g;
  print "*** Size of original drive: $src_bytes bytes\n";
  my $bd = `fdisk -l /dev/$dest_drive | grep '/dev/$dest_drive:'`;
  my (undef, $dest_bytes) = split(", ", $bd);
  $dest_bytes =~ s/\D//g;
  print "*** Size of destination drive: $dest_bytes bytes\n";
  if ($src_bytes > $dest_bytes) {
    my $diff_mb = int((($src_bytes - $dest_bytes) / 1024) / 1024);
    fatal_crash("The drive you are attempting to restore to is $diff_mb MB smaller than the original. You must restore to a drive that is the same size or larger than the original.");
  }
  open INFILE, $src_parts or fatal_crash("Could not read from $src_parts! Aborting.");
  my @partlist = <INFILE>;
  close INFILE;
  my $response = get_confirmation("Êtes-vous sûr de vouloir restaurer l'image sur /dev/$dest_drive? Ceci réécrira les données de façon définitive sur ce disque!");
  if ($response ne 'yes') {
    on_main_app_destroy();
    die("Aborting.\n");
  }
  system("umount /mnt/$dest_drive?* 2>&1");
  sleep(0.5);
  print "*** Writing MBR to $dest_drive\n";
  set_status("Writing master boot record to destination drive...");
  system("dd of=/dev/$dest_drive if=$src_mbr bs=32768 count=1; sync;");
  sleep(0.5);  
  print "*** Restoring partition table to $dest_drive\n";
  set_status("Writing extended partition table to destination drive...");
  system("sfdisk -fx /dev/$dest_drive < $src_sfdisk; sync");
  sleep(0.5);
  print "*** Reloading partition table from $dest_drive\n";
  set_status("Reloading new partition table from destination drive...");
  system("umount /mnt/$dest_drive?* 2>&1");
  system("sfdisk -R /dev/$dest_drive");
  sleep(1);
  $status{'start'} = time();
  $status{'part_list'} = \@partlist;
  $status{'last_part'} = $partlist[-1];
  $status{'total_parts'} = scalar(@partlist);
  $status{'current_part'} = 0;
  my $total_bytes = 0;
  # Verify all partitions now exist; crash otherwise
  foreach my $part (@{$status{'part_list'}}) {
    chomp($part);
    my ($dn, $pn) = get_drivenum_partnum($part);
    my $pid = $dest_drive.$pn;
    print "*** Verifying that /dev/$pid exists and can be restored to: ";
    my $part_check = `file /dev/$pid | grep 'block special' | wc -l`;
    if ($part_check==1) {
      print "OK\n";
    } else {
      print "FAIL\n";
      fatal_crash("La table des partitions n'a pas été restaurée correctement. L'image de sauvegarde peut contenir toutes vos données, mais sans table de partitions valide les données ne peuvent être restaurées.");
    }
    my $bytes = `cat /sys/block/$dest_drive/$dest_drive$pn/size`;
    chomp($bytes);
    $bytes = abs(int($bytes*512));
    print "*** Device $dest_drive$pn is $bytes bytes...\n";
    $drives{$dest_drive}{'parts'}{$pn}{'bytes'} = $bytes;
    $drives{$dest_drive}{'parts'}{$pn}{'size'} = format_size($bytes);
    $status{'total_bytes'} += $bytes;
  }
  $status{'done_bytes'} = 0;
  print "*** Restoring ".$status{'total_bytes'}." total bytes of partitioned space.\n";
  Glib::Timeout->add(50, \&update_restore_progress);
  refresh_window();
}

sub update_restore_progress {
  # Update the restore progress bar
  our $PROGRESS;
  our %drives;
  our %status;
  our $builder;
  my $progress_bar = $builder->get_object('restore_progress');
  my $data = undef;
  our $i;
  my $part = $status{'part_list'}[$status{'current_part'}];
  chomp($part);
  my ($dn, $pn) = get_drivenum_partnum($part); 
  my $src = $builder->get_object('restore_file')->get_filename();
  my $dest_drive = $builder->get_object('restore_dest')->get_active_text();
  $src =~ s/\.backup$//g;
  $src =~ s/ /\\ /g;
  my $src_mbr = $src.'.mbr';
  # See if the filehandle is open
  if (!defined($PROGRESS)) {
    # No command is being executed
    my $tool = "partclone.restore";
    system("umount /mnt/$dest_drive$pn 2>&1; dd if=/dev/zero of=/dev/$dest_drive$pn bs=1K count=1000; sync");
    set_status("Préparation de la restauration du disque $dn, partition $pn...");
    print "*** Processing $part using $tool...\n";
    system("umount /dev/$part 2>&1");
    print "*** Executing: ( cat /$src"."_part$pn.* | gzip -d -c | $tool -F -L /partclone.log -O /dev/$dest_drive$pn ) 2>&1 |\n";
    open $PROGRESS, "( cat /$src"."_part$pn.* | gzip -d -c | $tool -F -L /partclone.log -O /dev/$dest_drive$pn ) 2>&1 |";
    sleep(0.5);
    return TRUE;
  } else {
    # Read available data and append it to $status{'command_output'} until a newline is reached
    my $char = '';
    do {
      $char = getc($PROGRESS);
      if (!defined($char)) {
        return 1;
      } else {
        if (($char eq "\n") || ($char eq "\r") || (ord($char)==27)) {
          undef($char);
        } else {
          $status{'command_output'} .= $char;
        }
      }
    } while (defined($char));
  }
  # Try to read command progress and append it to the data
  $data = $status{'command_output'};
  undef($status{'command_output'});
  if (defined($data)) {
    # Remove newlines and lines that are all spaces
    $data =~ s/\n|\r|\[A|^\s+$//g;
    refresh_window();
    $i++;
    # Split it up and show the results    
    if ($data =~ m/Starting to/) {
      # Starting a filesystem
      my (undef, $current_part) = split(/\(\/dev\//, $data);
      $current_part =~ s/\).*$//g;
      $status{'current_part_device'} = $current_part;
      print ">>> Starting to restore $status{'current_part_device'}\n";
      print ">>> List of partitions:\n";
      foreach my $item (@{$status{'part_list'}}) {
        my $msg = '';
        if ($item eq $current_part) { $msg = "(Processing)"; }
        print "\t* $item $msg\n";
      }
    } elsif ($data =~ m/Device size: /) {
      # Device size reported
      my (undef, $dev_size) = split(/: /, $data);
      $dev_size =~ s/^\s+|\s+$//g;
      print(">>> Device size: $dev_size\n");
    } elsif ($data =~ m/Space in use: /) {
      # Device used space reported
      my (undef, $dev_used) = split(/: /, $data);
      $dev_used =~ s/^\s+|\s+$//g;
      print(">>> Space in use: $dev_used\n");
    } elsif ($data =~ m/^.*\.c.*: /) {
      # A warning was encountered
      my (undef, $warning) = split(/: /, $data);
      print(">>> Warning: $warning\n");
      set_status("Warning: $warning");
    } elsif ($data =~ m/Reading Super Block/) {
      # Reading super block
      print(">>> Reading super block\n");
      set_status("Lecture des informations pour le disque $dn, partition $pn...");
    } elsif ($data =~ m/Calculating bitmap/) {
      # Reading super block
      print(">>> Calculating bitmap\n");
      set_status("Calcul de la taille de l'image pour le disque $dn, partition $pn...");
    } elsif ($data =~ m/Elapsed.+,$/) {
      # Restore progress line
      my $elapsed = substr($data,9,8);
      my $remaining = substr($data,30,8);
      my $pct = substr($data,51,6);
      my $rate = substr($data,60,12);
      $pct =~ s/\s+//g;
      $rate =~ s/\s+//g;
      print ">>> (RESTORE) Ecoulé: *$elapsed* | Restant: *$remaining* | Pourcentage: *$pct* | Vitesse: *$rate*\n";
      $builder->get_object('restore_progress_status')->set_text("Partition ".($status{'current_part'}+1)." de ".$status{'total_parts'}." (".$pct."%)\n$elapsed Ecoulé\n$remaining Restant");
      # Base overall percentage on number of total bytes
      my $current_bytes = int(($pct/100) * $drives{$dest_drive}{'parts'}{$pn}{'bytes'}) + $status{'done_bytes'};
      my $overall_pct = ($current_bytes / $status{'total_bytes'}) + (($pct / 100) / $status{'total_bytes'});
      if ($overall_pct > 1) { $overall_pct = 1; }
      set_status("Restaure la partition ".($status{'current_part'}+1)." (".$drives{$dest_drive}{'parts'}{$pn}{'size'}.") at $rate");
      print ">>> Disque: $src | Partition: $pn | Octets courants: $current_bytes | Global: $overall_pct\n";
      $progress_bar->set_fraction($overall_pct);
      $progress_bar->set_text(sprintf("%.2f", $overall_pct * 100)."% Complété");
    } elsif ($data =~ m/not_used.+/) {
      # Bitmap reading progress line, no longer appears to be reported
      # Elapsed: 00:00:02, Remaining: 00:02:25, Completed:   1.36%,   1.83GB/min,
      my $elapsed = substr($data,9,8);
      my $remaining = substr($data,30,8);
      my $pct = substr($data,51,6);
      $pct =~ s/\s+//g;
      print ">>> (BITMAP) Ecoulé: *$elapsed* | Restant: *$remaining* | Pourcentage: *$pct*\n";
      $builder->get_object('restore_progress_status')->set_text('Lecture des informations de la partiton '.($status{'current_part'}+1)." de ".$status{'total_parts'});
      set_status("Lecture de l'image pour le disque $dn, partition $pn ($pct% done, $remaining remaining)");
    } elsif ($data =~ m/Cloned successfully/) {
      set_status("Restauration terminée.");
      print ">>> Restauration $status{'current_part_device'} terminée.\n";
      print "\tCurrent part: ".($status{'current_part'}+1)."\n";
      print "\tTotal parts: $status{'total_parts'}\n";
      if ($status{'current_part'}==$status{'total_parts'}-1 ) {
        $builder->get_object('restore_progress_status')->set_text('');
        $progress_bar->set_fraction(1);
        set_status("Operation completed successfully.");
        my @lines = split(/\n/, $data);
        my $start = FALSE;
        $builder->get_object('restore_button_cancel')->set_label('Quitter');
        refresh_window();
        # Unmount stuff
        system("sync");
        sleep(0.5);
        print "*** Writing MBR to $dest_drive\n";
        set_status("Rewriting master boot record to destination drive...");
        system("dd of=/dev/$dest_drive if=$src_mbr bs=32768 count=1; sync;");
        sleep(0.5);
        system("umount ".MOUNT_POINT);
        my $elapsed = sprintf("%.1f", (time()-$status{'start'})/60);
        print ">>> Operation complete.\n";
        set_status("Restore complete.");
        beeper('done');
        message_box("Sauvegarde restaurée en $elapsed minutes.");
        system("notify-send -i ".APP_ICON." 'Sauvegarde restaurée avec succès.'");
        return 0;
      } else {
        close $PROGRESS;
        undef($PROGRESS);
        # Need to add the size of the partition to the total bytes completed
        $status{'done_bytes'} += $drives{$dest_drive}{'parts'}{$pn}{'bytes'};
        print ">>> Backed up ".$status{'done_bytes'}." of ".$status{'total_bytes'}." total bytes.\n";
        $status{'current_part'}++;
        print "Advancing to next partition.\n";
        return TRUE;
      }
    } else {
      if (length($data)>0) {
        print "$data\n";
        if ($data =~ m/error|warning/i) {
          error_message($data);
        }
      }
    }
  }
  return TRUE;   
}


##### Form Logic #######################################################
sub get_confirmation {
  # Get confirmation from a yes/no dialog
  our $builder;
  my $question = "Êtes-vous sûr?";
	if (defined($_[0])) { $question = $_[0]; }
  my $dialog = Gtk2::MessageDialog->new($builder->get_object('main_app'),
    'destroy-with-parent',
    'warning', # message type
    'yes-no', # which set of buttons?
    $question);
  my $response = $dialog->run;
  $dialog->destroy;
  return $response;
}

sub next_tab {
  # Show the next tab of a Backup or Restore procedure
  our $builder;
  my $current_tab = $builder->get_object('main_tabs')->get_current_page();
  if ($current_tab==1) {
    # Backup mode
    $builder->get_object('backup_tabs')->next_page();
    my $backup_tab = $builder->get_object('backup_tabs')->get_current_page();
    backup_step($backup_tab+1);
  } elsif ($current_tab==2) {
    # Restore mode
    $builder->get_object('restore_tabs')->next_page();
    my $restore_tab = $builder->get_object('restore_tabs')->get_current_page();
    restore_step($restore_tab+1);
  } elsif ($current_tab==3) {
    # USB Installer mode
    $builder->get_object('usb_tabs')->next_page();
    my $usb_tab = $builder->get_object('usb_tabs')->get_current_page();
    usb_step($usb_tab+1);
  }
}

sub set_main_title {
  # Set the main title
  my $title = $_[0];
  our $builder->get_object('main_title')->set_text($title);
}

sub set_subtitle {
  # Set the subtitle, which is a description of the current step
  my $subtitle = $_[0];
  our $builder->get_object('main_subtitle')->set_text($subtitle);
}

sub set_cursor {
  # Set the mouse pointer cursor
  my $cursor = $_[0];
  our $builder->get_object('main_app')->window()->set_cursor(Gtk2::Gdk::Cursor->new($cursor));
}

sub set_status {
  # Set the status message at the bottom of the window
  my $status = $_[0];
  if (!defined($status)) { $status = "Ready."; }
  our $builder->get_object('main_statusbar')->push(0, $status);
  refresh_window();
}

sub set_busy {
  # Disable the application to indicate that we are busy working
  if ($_[0] ne '') {
    set_cursor("watch");
    set_status($_[0]);
    our $builder->get_object('main_app')->set_sensitive(FALSE);
    show_busy_bar($_[0]);
  } else {
    set_cursor("arrow");
    set_status('Done.');
    our $builder->get_object('main_app')->set_sensitive(TRUE);
    our $busy_bar->destroy();
  }
  refresh_window();
}

sub show_busy_bar {
  # Show a pulsing progress bar
  my $message = $_[0];
  if (!defined($message)) { $message = "Veuillez patienter..."; }
  my $parent = our $builder->get_object('main_app');
  our $busy_bar = Gtk2::Dialog->new('Veuillez patienter...', $parent,
    'destroy-with-parent',
    'gtk-cancel' => 'none'
  );
  my $pgb = Gtk2::ProgressBar->new();
  $pgb->set_text($message);
  my $abox = Gtk2::Alignment->new(.50, .50, 1, 1);
  $abox->set_padding(30, 30, 30, 30);
  $busy_bar->vbox->add($abox);
  $abox->add($pgb);
  $busy_bar->signal_connect(response => sub { $_[0]->destroy });
  $busy_bar->show_all;
  $busy_bar->window()->set_cursor(Gtk2::Gdk::Cursor->new('watch'));
  Glib::Timeout->add(100, \&update_busy_bar, $pgb);
}

sub update_busy_bar {
  # Update the pulsing progress bar
  my ($progress_bar) = @_;
  $progress_bar->pulse;
  return TRUE;
}

sub refresh_window {
  # Refresh the application window
  while (Gtk2->events_pending()) { Gtk2->main_iteration(); }
}

sub fatal_crash {
  # Crash out with a fatal error message
  my $parent = our $builder->get_object('main_app');
  my $message = $_[0].chr(13).chr(13)."Impossible de continuer. Le programme vamaintenant s'arrêter.";
  my $dialog = Gtk2::MessageDialog->new($parent, 'modal', 'error', 'cancel', $message);
  beeper('warning');
  $dialog->run;
  $dialog->destroy;
  on_main_app_destroy();
  die("Fatal Error: $message\n");
}

sub error_message {
  # Show a non-fatal error message
  my $parent = our $builder->get_object('main_app');
  my $message = $_[0];
  my $dialog = Gtk2::MessageDialog->new($parent, 'modal', 'warning', 'ok', $message);
  beeper('error');
  $dialog->run;
  $dialog->destroy;
}

sub message_box {
  # Show an informational message
  my $parent = our $builder->get_object('main_app');
  my $message = $_[0];
  my $dialog = Gtk2::MessageDialog->new($parent, 'modal', 'info', 'ok', $message);
  $dialog->run;
  $dialog->destroy;
}

sub on_main_app_destroy {
  # Close the application and all backup binaries
  my $bins = `find /usr/sbin/ -type f -name 'partclone.*' -printf '%f\n'`;
  my @bins = split(/\n/, $bins);
  foreach (@bins) {
    system('( killall -9 $_ 2>&1 ) > /dev/null');
  }
  our $PROGRESS;
  if (defined($PROGRESS)) { close($PROGRESS); }
  Gtk2->main_quit();
}


##### Core program functions ###########################################
sub save_file {
  # Save a string to the specified file
  my $data = $_[0];
  my $outfile = $_[1];
  $outfile =~ s/\\//g;
  open(OUT, ">$outfile") or die("Could not open $outfile for writing!\n");
  print OUT $data;
  close OUT;
}

sub which_backup_tool {
  # Determine which backup tool to call based on the FS type
  my $fs = $_[0];
  my $tool = "partclone.";
  if ($fs =~ m/ext/) {
    $tool .= "extfs";
  } elsif ($fs =~ m/fat/) {
    $tool .= "fat";
  } elsif ($fs =~ m/ntfs/) {
    $tool .= "ntfs";
  } elsif ($fs =~ m/hfs/) {
    $tool .= "hfsp";
  } elsif ($fs =~ m/jfs/) {
    $tool .= "jfs";
  } elsif ($fs =~ m/reiser4/) {
    $tool .= "reiser4";
  } elsif ($fs =~ m/reiserfs/) {
    $tool .= "reiserfs";
  } elsif ($fs =~ m/ufs/) {
    $tool .= "ufs";
  } elsif ($fs =~ m/vmfs/) {
    $tool .= "vmfs";
  } elsif ($fs =~ m/xfs/) {
    $tool .= "xfs";
  } else {
    $tool .= "dd";
  }
  return $tool;
}

sub mount_data {
  # Mount the data device that backup images are stored on
  our %shares;
  our %status;
  system("mkdir -p ".MOUNT_POINT);
  system("umount ".MOUNT_POINT." 2>&1");
  our $builder;
  for ($status{'mount_type'}) {
    if (/DEV/) {
      # Mount a regular drive
      $status{'mount_source'} =~ s/\|//g;
      system("mount /dev/$status{'mount_source'} ".MOUNT_POINT);
      print "* Executing: mount /dev/$status{'mount_source'} ".MOUNT_POINT."\n";
    } elsif (/SMB/) {
      # Mount a network share
      my @smbargs = ();
      if ($status{'mount_user'} ne '') { push(@smbargs, "username=".$status{'mount_user'}); }
      if ($status{'mount_pass'} ne '') { push(@smbargs, "password=".$status{'mount_pass'}); }
      if ($status{'mount_domain'} ne '') { push(@smbargs, "dom=".$status{'mount_domain'}); }
      my $args = join(',', @smbargs);
      if ($args eq '') { $args = "guest"; }
      system("mount.cifs '$status{'mount_source'}' ".MOUNT_POINT." -o $args");
      print "* Executing: mount.cifs '$status{'mount_source'}' ".MOUNT_POINT." -o $args\n";
    } elsif (/FTP/) {
      # Mount an FTP server
      my $ftpargs = '';
      if ($status{'mount_user'} ne '') { $ftpargs = "-o user=".$status{'mount_user'}; }
      if ($status{'mount_pass'} ne '') { $ftpargs .= ":".$status{'mount_pass'}; }
      system("curlftpfs $status{'mount_source'} ".MOUNT_POINT." $ftpargs");
      print "* Executing: curlftpfs $status{'mount_source'} ".MOUNT_POINT." $ftpargs\n";
    } else {
      fatal_crash("Le dispositif '$status{'mount_type'}' is not valid.  N'auriez-vous pas oublié de sélectionner une source de sauvegarde?");
    }
  }
  my $mp = MOUNT_POINT;
  my $mounted = `mount | grep '$mp' | wc -l`;
  return $mounted;
}

sub set_usb_dropdown {
  # Get all physical drives that are USB drives
  set_drive_dropdown('usb_dest', 'usb');
}

sub set_drive_dropdown {
  # Get all physical drives
  set_busy("Getting list of drives...");
  our %drives;
  if (keys(%drives)==0) { fatal_crash('Could not locate any drives attached to your computer.'); }
  my $cbo = our $builder->get_object($_[0]);
  my $mdl = $cbo->get_model();
  $mdl->clear();
  my ($dev, $desc) = '';
  for my $drive (sort keys %drives) {
    $dev = $drives{$drive}{'device'};
    $desc = $drives{$drive}{'desc'};
    if (length($desc)>MAX_WIDTH+3) { $desc = substr($desc,0,MAX_WIDTH).'...'; }
    if ((defined($_[1])) && ($_[1] eq 'usb')) {
      if ($drives{$drive}{'type'} eq 'usb') {
        $mdl->set($mdl->append, 0, $dev, 1, $desc);
      }
    } else {
      $mdl->set($mdl->append, 0, $dev, 1, $desc);
    }
  }
  my $num_drives = $mdl->iter_n_children();
  if ($num_drives==0) { fatal_crash('No drives available.'); }
  $cbo->set_active(0);
  set_busy(FALSE);
}

sub set_partition_dropdown {
  # Populate the partition list for the given combobox
  our %drives;
  if (keys(%drives)==0) { fatal_crash('Could not locate any partitions on your computer.'); }
  my $stage = '';
  my @blacklist = ();
  if ($_[0] =~ m/backup/) {
    # We're backing up
    $stage = 'backup';
    @blacklist = get_selected_partitions('backup_partitions');
  } elsif ($_[0] =~ m/restore/) {
    # We're restoring
    $stage = 'restore';
  }
  my $cbo = our $builder->get_object($_[0]);
  my $mdl = $cbo->get_model();
  $mdl->clear();
  for my $drive (sort keys %drives) {
    for my $data (sort keys %{ $drives{$drive}{'parts'} }) {
      my $num_parts = keys(%{ $drives{$drive}{'parts'} });
      my $dn = $drives{$drive}{'drivenum'};
      my $pn = $data;
      my $part = $drives{$drive}{'parts'}{$data}{'partition'};
      my $fs = $drives{$drive}{'parts'}{$data}{'fstype'};
      my $label = $drives{$drive}{'parts'}{$data}{'label'};
      my $size = $drives{$drive}{'parts'}{$data}{'size'};
      if (!defined($size)) { $size = "Unknown size"; }
      my $os = $drives{$drive}{'parts'}{$data}{'os'};
      my $part_desc =  "Disque $dn";
      if ($num_parts > 1) { $part_desc .= ", partition $pn"; }
      $part_desc .= ": ($size $fs)";
      if ($os ne '') { $part_desc .= " $os"; }
      if ($label ne '') { $part_desc .= " $label"; }
      if (grep $_ eq $part, @blacklist) {
        print "*** Omitting backup source $part from destination list\n";
      } else {
        if (length($part_desc)>MAX_WIDTH+3) { $part_desc = substr($part_desc,0,MAX_WIDTH).'...'; }
        $mdl->set($mdl->append, 0, "DEV|$part", 1, $part_desc);
      }
    }
  }
  $cbo->set_active(0);
}

sub set_share_dropdown {  
  # Add the share list to the given combobox
  our %shares;
  my $cbo = our $builder->get_object($_[0]);
  my $mdl = $cbo->get_model();
  $mdl->clear();
  for my $share (sort keys %shares) {
    my $ip = $shares{$share}{'ip'};
    my $host = $shares{$share}{'host'};
    my $folder = $shares{$share}{'folder'};
    my $desc = $shares{$share}{'desc'};
    my $type = $shares{$share}{'type'};
    if (!defined($host)) { $host = $ip; }
    my $share_desc = '';
    if ($type eq 'FTP') {
      $share_desc = "FTP server \U$host";
    } elsif ($type eq 'SMB') {
      $share_desc = "Dossier partagé $folder sur $host";
    }
    if ((defined($desc))&&($desc ne '')) { $share_desc .= " ($desc)"; }
    if (length($share_desc)>MAX_WIDTH+3) { $share_desc = substr($share_desc,0,MAX_WIDTH).'...'; }
    $mdl->set($mdl->append, 0, "$type|$share", 1, $share_desc);
  }
  $mdl->set($mdl->append, 0, "SMB|", 1, "Dossier partagé indiqué ci-dessous");
  $mdl->set($mdl->append, 0, "FTP|", 1, "Serveur FTP indiqué ci-dessous");  
  $cbo->set_active(0);
}

sub get_selected_partitions {
  # Return the partitions that have been selected from a list
  my @selected = ();
  my $tv = our $builder->get_object($_[0]);
  my $mdl = $tv->get_model();
  my $total = $mdl->iter_n_children();
  for (my $i=0; $i<$total; $i++) {
    my $iter = $mdl->get_iter_from_string("$i");
    my ($part, $on) = $mdl->get($iter, 0, 1);
    if ($on) { push(@selected, $part); }
  }
  return @selected;
}

sub set_partition_list {
  # Set the list of partitions with checkboxes
  our %drives;
  if (keys(%drives)==0) { fatal_crash('Could not locate any partitions on your computer.'); }
  my $tv = our $builder->get_object($_[0]);
  my $slist = Gtk2::SimpleList->new_from_treeview(
    $tv,
    'Partition'           => 'text',
    'Sauvegarder'           => 'bool',
    'Description'    => 'text',
  );
  my $tv_col = $tv->get_column(0);
  $tv_col->set_visible(FALSE);
  @{$slist->{data}} = (
    #[ TRUE, "sda2", "Disque 1, Partition 1: (80GB) Windows XP Pro" ],
  );
  print "Obtaining partitions for drive $_[1]...\n";
  for my $drive (sort keys %drives) {
    for my $data (sort keys %{ $drives{$drive}{'parts'} }) {
      my $num_parts = keys(%{ $drives{$drive}{'parts'} });
      my $dn = $drives{$drive}{'drivenum'};
      my $pn = $data;
      my $part = $drives{$drive}{'parts'}{$data}{'partition'};
      my $fs = $drives{$drive}{'parts'}{$data}{'fstype'};
      my $label = $drives{$drive}{'parts'}{$data}{'label'};
      my $size = $drives{$drive}{'parts'}{$data}{'size'};
      if (!defined($size)) { $size = "Unknown size"; }
      my $os = $drives{$drive}{'parts'}{$data}{'os'};
      my $part_desc = "Disque $dn, partition $pn";
      $part_desc .= ": ($size $fs)";
      if ($os ne '') { $part_desc .= " $os"; }
      if ($label ne '') { $part_desc .= ", $label"; }
      if ($drive eq $_[1]) {
        push @{$slist->{data}}, [ $part, TRUE, $part_desc ];
      }
    }
  }
  #$slist->get_selection->set_mode('multiple');
  $slist->get_selection->unselect_all();
}

sub print_drive_list {
  # Print the drive list to the console
  our %drives;
  for my $dev (sort keys %drives) {
    print "*** $dev ***\n";
    for my $drive_key (sort keys %{ $drives{$dev} }) {
      print "\t$drive_key = $drives{$dev}{$drive_key}\n";
      if(ref($drives{$dev}{$drive_key}) eq 'HASH') {
        for my $pn (sort keys %{ $drives{$dev}{$drive_key} }) {
          print "\tPartition $pn:\n";
          for my $pk (sort keys %{ $drives{$dev}{$drive_key}{$pn} }) {
            print "\t\t$pk = $drives{$dev}{$drive_key}{$pn}{$pk}\n"; 
          }
        }
      }
    }
    print "\n";
  }
}

sub print_share_list {
  # Print the share list to the console
  our %shares;
  for my $share (sort keys %shares) {
    print "*** $share ***\n";
    for my $share_key (sort keys %{ $shares{$share} }) {
      print "\t$share_key = $shares{$share}{$share_key}\n";
    }
    print "\n";
  }
}

sub find_local_drives {
  # Set the list of local drives, (only SCSI and USB drives)
  our %drives;
  my $drivelist = `fsarchiver probe 2>&1`;
  chomp($drivelist);
  if ($drivelist eq "") {
    return FALSE;
  } else {
    # Create an array of drives
    $drivelist =~ s/\n\[\=+DEVICE.*//s;
    $drivelist =~ s/\[\=+DISK.*\n//g;
    $drivelist =~ s/^\[|\]$//mg;
    my @list = split(/\n/, $drivelist);
    foreach my $line (@list) {
      # Get each drive's details
      next if $line =~ m/^\n/;
      refresh_window();
      my @drivedata = split(/\]\s\[/, $line);
      my $dev = $drivedata[0];
      $dev =~ s/\s*$//g;
      my $dev_name = $drivedata[1];
      $dev_name =~ s/\s*$//g;
      my $size = $drivedata[2];
      $size =~ s/^\s*//g;
      $size =~ s/\s+//g;
      if (!defined($size)) { $size = 'Unknown'; }
      my ($drivenum) = get_drivenum_partnum($dev);
      my $type = get_drivetype($dev);
      my $desc = "Disque $drivenum ($size): $dev_name";
      if ($type eq 'usb') { $desc = "Disque $drivenum ($size): USB $dev_name"; }
      $drives{$dev} = {
        'device'        => $dev,
        'drivenum'      => $drivenum,
        'size'          => $size,
        'model'         => $dev_name,
        'type'          => $type,
        'desc'          => $desc,
        'parts'         => {},
      };
    }
  }
  if (scalar(keys(%drives))==0) { return FALSE; }
  find_local_partitions();
  return TRUE;
}

sub find_local_partitions {
  # Set the list of local partitions for SCSI and USB drives
  our %drives;
  set_busy("Identification des disques...");
  my $partlist = `fsarchiver probe 2>&1`;
  chomp($partlist);
  if ($partlist =~ m/Failed to detect disks and filesystems/) {
    return FALSE;
  } else {
    # Create an array of partitions
    $partlist =~ s/^.*\[\=+DEVICE/\[\=DEVICE/s;
    $partlist =~ s/\[\=+DEVICE.*\n//g;
    $partlist =~ s/^\[|\]$//mg;
    my @list = split(/\n/, $partlist);
    foreach my $line (@list) {
      # Get each partition's details
      next if $line =~ m/^\n/;
      next if $line =~ m/^loop/;
      next if $line =~ m/^ramzswap/;
      next if $line =~ m/^dm-/;
      next if $line =~ m/^ram/;
      refresh_window();
      my ($partition, $fstype, $label, $size) = split(/\]\s\[/, $line);
      $partition = trim($partition);
      $fstype = trim("\U$fstype");
      $label = trim($label);
      $label = '' if $label eq '<unknown>';
      $size = trim($size);
      $size =~ s/\s+//g;
      my $dev = $partition;
      $dev =~ s/\d//g;
      my ($dn, $pn) = get_drivenum_partnum($partition);
      my $bytes = `cat /sys/block/$dev/$dev$pn/size`;
      chomp($bytes);
      $bytes = abs(int($bytes*512));
      my $part = {
        'partition' => $partition,
        'fstype'    => $fstype,
        'label'     => $label,
        'size'      => $size,
        'bytes'     => $bytes,
        'os'        => '',
      };
      # Save the details in a final list
      $drives{$dev}{'parts'}{$pn} = $part;
    }
    # Identify any operating systems
    refresh_window();
    my $oslist = `os-prober`;
    chomp($oslist);
    if ($oslist ne "") {
      my @list = split(/\n/, $oslist);
      foreach my $line (@list) {
        my ($os_part, $os_name, $os_type, $os_loader) = split(/:/, $line);
        my $dev = $os_part;
        my ($dn, $pn) = get_drivenum_partnum($os_part);
        $dev =~ s/\d|\/dev\///g;
        if ($os_name ne '') {
          $drives{$dev}{'parts'}{$pn}{'os'} = $os_name;
        }
      }
    }
  }
  # Update the drive description to include labels and OS types
  for my $dev (sort keys %drives) {
    if(ref($drives{$dev}{'parts'}) eq 'HASH') {
      for my $pn (sort keys %{ $drives{$dev}{'parts'} }) {
        my $desc = ' ';
        my $label = $drives{$dev}{'parts'}{$pn}{'label'};
        my $os = $drives{$dev}{'parts'}{$pn}{'os'};
        my $size = $drives{$dev}{'parts'}{$pn}{'size'};
        my $fstype = $drives{$dev}{'parts'}{$pn}{'fstype'};
        if ($os ne '') {
          $desc .= "($os, $size $fstype)";
        } else {
          if ($label ne '') {
            $desc .= "($label, $size $fstype)";
          } else {
            $desc .= "($size $fstype)";
          }
        }
        $drives{$dev}{'desc'} .= "$desc";
      }
    }
  }
  set_busy(FALSE);
  return TRUE;
}

sub find_shared_drives {
  # Search for local network shares (FTP/SMB)
  our %shares;
  set_busy("Recherche des répertoires partagés sur le réseau...");
  refresh_window();
  my $smbdata = `smbtree -N | grep '\\\\\\\\\\w*\\\\\\w*[^\\\$] '`;
  refresh_window();
  chomp($smbdata);
  my @list = split(/\n/, $smbdata);
  foreach my $line (@list) {
    # Get list of SMB shared folders
    my ($share, $desc) = split(/  +/, $line);
    $share = trim($share);
    $desc = trim($desc);
    my ($a, $b, $host, $folder) = split(/\\/, $share);
    $host = trim("\U$host");
    $folder = trim("\U$folder");
    #my $ip = `nmblookup $host | grep '^[^q]'`;
    #$ip = trim($ip);
    #$ip =~ s/ .*$//g;
    my $ip = '';
    $shares{"smb://$host/$folder"} = {
      'ip'            => $ip,
      'host'          => $host,
      'folder'        => $folder,
      'desc'          => $desc,
      'type'          => 'SMB',    };
  }
  my $subnet = get_subnet();
  refresh_window();
  system("nmap -p 21 $subnet/24 --open -T5 -oX /tmp/out.xml");
  refresh_window();
  my $xml = new XML::Simple;
  my $ftpdata = $xml->XMLin("/tmp/out.xml");
  foreach my $key (@{$ftpdata->{host}}) {
    my $state = $key->{ports}->{port}->{state}->{state};
    if (!defined($state)) { $state = ''; }
    if ($state eq 'open') {
      my $ip = '';
      my $vendor = '';
      my $hostname = '';
      if (ref($key->{address}) eq 'ARRAY') {
        foreach my $address_key (@{$key->{address}}) {
          if ($address_key->{addrtype} eq 'ipv4') { $ip = $address_key->{addr}; }
          if ($address_key->{addrtype} eq 'mac') { $vendor = $address_key->{vendor}; }
        }
      } else {
        $ip = $key->{address}->{ip};
        $vendor = '';
      }
      if (ref($key->{hostnames}->{hostname}) eq 'ARRAY') {
        foreach my $hostname_key (@{$key->{hostnames}}) {
          if ($hostname_key->{type} eq 'PTR') { $hostname = $hostname_key->{name}; }
        }
      } else {
        $hostname = $key->{hostnames}->{hostname}->{name};
      }
      $shares{"ftp://$ip"} = {
        'ip'            => $ip,
        'host'          => $hostname,
        'folder'        => '',
        'desc'          => $vendor,
        'type'          => 'FTP',
      };
    }
  }
  set_busy(FALSE);
  return TRUE;
}

sub scan_network {
  # Scan the network and add shares to given dropdown
  my $cbo = our $builder->get_object($_[0]);
  my $mdl = $cbo->get_model();
  $mdl->clear();
  find_shared_drives();
  print_share_list();
  set_share_dropdown($_[0]);
}

sub get_drivenum_partnum {
  # Given a /dev/XXX partition, return the drive and partition numbers
  my $dpd = $_[0];
  $dpd =~ s/\/dev\/sd|sd//g;
  my $dn = substr($dpd,0,1);
  $dn = ord("\L$dn")-96;
  my $pn = substr($dpd,1);
  return ($dn, $pn);
}

sub get_drivetype {
  # Given an "sdX" device, return the string that indicates its type
  my $dev = $_[0];
  my $usb = `ls -asl /dev/disk/by-id/ | grep '$dev\$' | grep 'usb' | wc -l`;
  if ($usb==1) {
    return 'usb';
  } else {
    return 'scsi';
  }
}

sub get_subnet {
  # Return current subnet for my IP address
  my $ip = `ifconfig -a | perl -ne 'if ( m/^\\s*inet (?:addr:)?([\\d.]+).*?cast/ ) { print qq(\$1\\n); exit 0; }'`;
  chomp($ip);
  my ($a,$b,$c) = split(/\./, $ip);
  return "$a.$b.$c.0";
}

sub trim {
  # Trim whitespace from both ends of a string
  my $string = shift;
  $string =~ s/^\s+//;
  $string =~ s/\s+$//;
  return $string;
}

sub get_free_space {
  # Return free space left on destination drive
  my $mb = shift;
  my $mount_point = MOUNT_POINT;
  my $cmd = '';
  if ((defined($mb))&&($mb==1)) {
    $cmd = "--block-size=1M";
  } else {
    $cmd = "-h";
  }
  my ($null1, $null2, $null3, $free) = split(/\s+/, `df $cmd $mount_point | tail -n 1`);
  print "Free space: $free\n";
  return $free;
}

sub beeper {
  # Play a system beep pattern
  my $tone = shift;
  my $args = '';
  if ($tone eq 'done') { $args = '-l 100 -f 1200 -n -l 100 -f 1800 -n -l 100 -f 2400'; }
  if ($tone eq 'warning') { $args = '-f 250 -r 3 -l 50'; }
  if ($tone eq 'error') { $args = '-l 1000 -f 100'; }
  system('beep '.$args);
  return 1;
}

sub format_size {
  # Given a size in bytes, make it human-readable
  my $bytes = $_[0];
  my $suffix = "";
  my $x = log($bytes) / log(10);
  if ($x<3) {
    $suffix = " bytes";
  } elsif ($x<6) {
    $x = 3;
    $suffix = "KB";
  } elsif ($x<9) {
    $x = 6;
    $suffix = "MB";
  } elsif ($x<12) {
    $x = 9;
    $suffix = "GB";
  } elsif ($x<15) {
    $x = 12;
    $suffix = "TB";
  } else {
    $x = 15;
    $suffix = "PB";
  }
  return sprintf("%0.1f", $bytes/(10**$x)).$suffix;
}
