David Bushong
Résumé
Picture Gallery
Software Tidbits
Links
Contact Me

Software Tidbits

#!/usr/bin/perl

## Variables
# global
use vars qw($camroot $sudo $umount $cam $tmp $mounted $in_alert
	    $cancel_xfer $winbox %choice @rows $dest_dir $picbox $pbshell
	    %entry $path $total %action %size $dialog $text $prog $pid
	    $done $first %opt $start_time $last_etr);
# local to main()
my ($jhead, $defpath, $win, $button, $vbox, $hbox, $group, $result, @pics, $n, 
    $cols, $c, $row, $mount);

## Configuration
$sudo		= '/usr/local/bin/sudo';
$mount		= '/sbin/mount';
($umount = $mount) =~ s/mount$/umount/;
$jhead		= '/usr/local/bin/jhead';

## Defaults
$camroot 	= '/camera';
$cam		= '100olymp';
$defpath	= '/export/tmp/pics/%Y-%m-%d-%H:%M';

## Includes
use strict;
use Gtk '-init';
use Gtk::Gdk::Pixbuf;
use File::Path;
use File::Copy;
use File::Basename;
use POSIX 'strftime';
use Data::Dumper;
use Getopt::Std;

## Options
getopts('hvr:c:p:', \%opt);
die <<EOF if $opt{h} || @ARGV;
usage: @{[ basename $0 ]} [-v] [-r camroot] [-c camera] [-p outpath]
	-v: verbose mode
	-r: set the root mountpoint of the camera fs; default: $camroot
	-c: set the camera dir name; default: $cam
	    this means the default input dir is $camroot/dcim/$cam
	-p: set the output path which will be passed through strftime;
	    default is $defpath
EOF
$camroot    = $opt{r} if defined $opt{r};
$cam	    = $opt{c} if defined $opt{c};
$defpath    = $opt{p} if defined $opt{p};

## Camera Setup
$| = 1;
unless (-d "$camroot/dcim") {
    print "$camroot not mounted; trying to mount with sudo: " if $opt{v};
    system($sudo, $mount, $camroot);
    if ($? >> 8) {
	print "failed: $!\n" if $opt{v};
	exit(1);
    }
    print "succeded\n" if $opt{v};
    $mounted = 1;
}
chdir("$camroot/dcim/$cam") || die "couldn't cd to '$camroot/dcim/$cam': $!\n";
opendir(DOT, '.');
@pics = grep(/\.jpg$/i, readdir(DOT));
closedir(DOT);
die "no pics found in '$camroot/dcim/$cam'; quitting\n" unless @pics;

## Temp Dir
$tmp = sprintf('/tmp/%s-%x', basename($0), rand(0xffffffff));
mkpath([$tmp]) || die "couldn't make tmpdir '$tmp': $!\n";

## State Setup
$in_alert	= 0;
$cancel_xfer	= 0;

## GUI Setup
sub TRUE  { Gtk->true }
sub FALSE { Gtk->false }

$win = new Gtk::Window;
$win->set_policy(FALSE, FALSE, TRUE);
$win->signal_connect('delete_event', \&quit);
$win->signal_connect('destroy_event', \&quit);
$win->set_title("Camera Image Viewer");


$winbox = Gtk::VBox->new(FALSE, 0);
$pbshell = Gtk::VBox->new(FALSE, 0);

$winbox->add($pbshell);
&draw_pics;

$path = Gtk::Entry->new(80);
$path->set_text(strftime($defpath, localtime));
$winbox->add($path);

$winbox->add(Gtk::HSeparator->new);

$button = Gtk::Button->new_with_label('Run');
$button->signal_connect('clicked', \&run);
$winbox->add($button);

$win->add($winbox);
$win->show_all;

Gtk->main;

## Subroutines

sub quit {
    rmtree([$tmp]);

    if ($mounted) {
	print "unmounting $camroot: " if $opt{v};
	chdir('/');
	system($sudo, $umount, $camroot);
	if ($opt{v}) {
	    if ($? >> 8) {
		print "failed: $!\n";
	    } else {
		print "succeeded\n";
	    }
	}
    }

    Gtk->main_quit;
}

sub run {
    $total = 0;
    %action = ();
    my $del;

    %action = ();
    for (sort keys %choice) {
	if ($choice{$_}{Copy}->get_active) {
	    $action{$_} = 'copy';
	} elsif ($choice{$_}{Move}->get_active) {
	    $action{$_} = 'move';
	} elsif ($choice{$_}{Delete}->get_active) {
	    print qq(delete "$_"\n) if $opt{v};
	    if (unlink($_)) {
		$del = 1;
	    } else {
		&alert("Couldn't delete '$camroot/dcim/$cam/$_': $!");
	    }
	    next;
	} else {    ## Leave
	    next;
	}

	$size{$_} = -s;
	$total += $size{$_};
    }

    unless (%action) {
	&redraw_pics if $del;
	return;
    }

    $first = 1;
    $dest_dir = $path->get_text;
    mkpath([$dest_dir]);
    $start_time = time;
    $last_etr = 0;

    unless ($pid = fork) {
	for (sort keys %action) {
	    if ($action{$_} eq 'move') {
		print qq(move "$_" -> "$dest_dir/$_"\n) if $opt{v};
		move($_, "$dest_dir/$_", 131072);	## 128k buffer
	    } elsif ($action{$_} eq 'copy') {
		print qq(copy "$_" -> "$dest_dir/$_"\n) if $opt{v};
		copy($_, "$dest_dir/$_", 131072);	## 128k buffer
	    } else {
		print "unknown action '$action{$_}' for '$_'" if $opt{v};
		last;
	    }
	}

	Gtk->_exit(0);
    }

    Gtk->timeout_add(250, \&xfer_callback);
}

sub xfer_callback {
    my ($current, $bytes, $button, $sum, $etr);

    if ($cancel_xfer) {
	$cancel_xfer = 0;
	kill(15, $pid);
	$dialog->destroy;
	return;
    }

    for (sort keys %action) {
	$bytes = -s "$dest_dir/$_";

	if ($bytes == $size{$_}) {
	    $done += $bytes;
	    delete $action{$_};
	} else {
	    $current = $_;
	    last;
	}
    }

    $sum = $done + $bytes;

    return TRUE unless $sum;

    if ($first) {
	$dialog = new Gtk::Dialog;
	$dialog->set_modal(TRUE);

	$button = Gtk::Button->new_with_label('Cancel');
	$button->signal_connect('clicked', sub { $cancel_xfer = 1 });
	$button->signal_connect('delete_event', \&Gtk::true);
	$dialog->action_area->pack_start($button, FALSE, FALSE, 3);

	$text = new Gtk::Label;
	$dialog->vbox->pack_start($text, FALSE, FALSE, 5);

	$dialog->vbox->add(Gtk::HSeparator->new);

	$prog = new Gtk::ProgressBar;
	$dialog->vbox->add($prog);

	$dialog->show_all;

	$first = 0;
    }

    if (defined $current) {
	$etr = ($total - $sum) * (time - $start_time) / $sum;
	if ($last_etr && $etr > $last_etr) {
	    $etr = $last_etr;
	} else {
	    $last_etr = $etr;
	}
	$etr = ($etr > 60 * 60 * 24) 
	     ? '???'
	     : strftime('%T', gmtime($etr));
	$text->set_text("Current: $action{$current}: $current\n"
	    . "Estimated Remaining: $etr");
	$prog->update($sum / $total);

	return TRUE;
    } else {
	$dialog->destroy;
	&redraw_pics;

	return FALSE;
    }
}

sub alert {
    my ($mesg, $text, $func) = @_;
    my ($dialog, $button);

    $text = 'OK' unless length $text;
    $func ||= sub { };

    $in_alert = 1;
    $dialog = new Gtk::Dialog;
    $dialog->set_modal(TRUE);

    $button = Gtk::Button->new_with_label("  $text  ");
    $button->signal_connect('clicked', \&alert_func, $func);
    $button->signal_connect('delete_event', \&Gtk::true);

    $dialog->action_area->pack_start($button, FALSE, FALSE, 3);
    $dialog->vbox->pack_start(
	Gtk::Label->new("\n  $mesg  \n"), FALSE, FALSE, 5);

    $dialog->show_all;
}

sub alert_func {
    my ($self, $func) = @_;

    $self->get_parent_window->destroy;
    &$func;
    $in_alert = 0; 
}

sub draw_pics {
    my (@pics, $n, $cols, $c, $pic, $button);

    $picbox = Gtk::VBox->new(FALSE, 0);

    opendir(DOT, '.');
    @pics = sort grep(/\.jpg$/, readdir(DOT));
    closedir(DOT);

    $n = @pics;
    $cols = int((3+sqrt(9+352*$n))/22+0.5);

    $c = 0;
    @rows = ();
    %choice = ();

    for $pic (@pics) {
	$hbox = Gtk::HBox->new(FALSE, 0);
	$vbox = Gtk::VBox->new(FALSE, 0);

	$group = Gtk::RadioButton->new_with_label('Leave');
	$vbox->add($group);

	for (qw(Copy Move Delete)) {
	    $button = Gtk::RadioButton->new_with_label_from_widget($group, $_);
	    $vbox->add($button);
	    $choice{$pic}{$_} = $button;
	}

	$hbox->add($vbox);

	unless (-e "$tmp/$pic") {
	    open(JHEAD, '-|') || exec($jhead, '-st', "$tmp/$pic", $pic); 
	    $result = <JHEAD>;
	    close(JHEAD);

	    next if $? >> 8 || $result !~ /^Created:/;
	}

	$button = new Gtk::Button;
	$button->set_relief(2);
	$button->signal_connect('clicked', \&preview, $pic);
	$button->add(Gtk::Pixmap->new(Gtk::Gdk::Pixbuf->new_from_file(
	    "$tmp/$pic")->render_pixmap_and_mask(1), undef));

	$hbox->add($button);
	$entry{$pic} = $hbox;

	if ($c++ == 0) {
	    $row = Gtk::HBox->new(FALSE, 0);
	    push(@rows, $row);
	    $picbox->add($row);
	    $picbox->add(Gtk::HSeparator->new);
	}

	$row->pack_start($entry{$pic}, FALSE, FALSE, 0);
	$row->pack_start(Gtk::VSeparator->new, FALSE, FALSE, 0);
	$c %= $cols;
    }

    $picbox->show_all;
    $pbshell->add($picbox);
}

sub redraw_pics {
    $pbshell->remove($picbox);
    &draw_pics;
}

sub preview {
    my ($self, $pic) = @_;
    my ($prev, $pwin, $scroll);

    $pwin = new Gtk::Window;
    $pwin->set_policy(FALSE, TRUE, FALSE);
    $pwin->set_title("Camera Image Viewer: Preview '$pic'");
    $pwin->set_default_size(640, 480);

    $scroll = new Gtk::ScrolledWindow;
    $scroll->add_with_viewport(Gtk::Pixmap->new(Gtk::Gdk::Pixbuf->new_from_file(
	$pic)->render_pixmap_and_mask(1), undef));

    $pwin->add($scroll);
    $pwin->show_all;
}