#!/home/dakkar/perl5/perlbrew/perls/perl-5.20.2/bin/perl
use lib '/home/dakkar/.perlbrew/libs/perl-5.20.2@dakkar/lib/perl5';
use strict;
use warnings;
use IO::Socket::INET; # for locking
use Net::DBus;
use Net::DBus::Reactor;
use File::Temp;
use Cwd 'realpath';

my $lock=IO::Socket::INET->new(LocalAddr=>'127.0.0.1',LocalPort=>9999,Type=>SOCK_STREAM,Listen=>1)
    or die 'already running';

sub add_to_fstab {
    my ($dev_path,$mount_point,$fs_type,$opts)=@_;

    my @orig_stat=stat('/etc/fstab');
    my $tfh=File::Temp->new(DIR=>'/etc');
    $tfh->unlink_on_destroy(1);

    while(1) {
        seek $tfh,0,0;
        truncate $tfh,0;
        open my $in_fh,'<','/etc/fstab';
        while (my $line=<$in_fh>) {
            next if $line =~ m{\A \s* \#}smx;
            my ($in_dev,$in_path,$in_fs,$in_opts)=
                ($line=~m{\A \s* (\S+) \s+ (\S+) \s+ (\S+) \s+ (\S+) }smx)
                    or next;
            if (-e $in_dev
                    && (realpath($in_dev) eq realpath($dev_path))) {
                # it's already in there, abort
                return $in_path;
            }
        }
        continue {
            print {$tfh} $line;
        };
        print {$tfh} "$dev_path $mount_point $fs_type $opts 0 0 # dakkar\n";
        if ((stat('/etc/fstab'))[9] == $orig_stat[9]) {
            rename "$tfh",'/etc/fstab';
            chmod $orig_stat[2],'/etc/fstab';
            chown @orig_stat[4,5],'/etc/fstab';
            return;
        }
        # fstab has changed, re-run the whole thing
    }
}

sub remove_from_fstab {
    my ($mount_point)=@_;

    my @orig_stat=stat('/etc/fstab');
    my $tfh=File::Temp->new(DIR=>'/etc');
    $tfh->unlink_on_destroy(1);

    my $was_there=0;

    while(1) {
        seek $tfh,0,0;
        truncate $tfh,0;
        open my $in_fh,'<','/etc/fstab';
        while (my $line=<$in_fh>) {
            next if $line =~ m{\A \s* \#}smx;
            my ($in_dev,$in_path,$in_fs,$in_opts,$comms)=
                ($line=~m{\A \s* (\S+) \s+ (\S+) \s+ (\S+) \s+ (\S+) \s+ \d+ \s+ \d+ (?: \s* \# (.*) )?}smx);
            if (defined($comms) && ($comms eq " dakkar\n")
                    && ($in_path eq $mount_point)) {
                # ok, it's the right line, kill it
                $line='';$was_there=1;
            }
        }
        continue {
            print {$tfh} $line;
        };
        if ((stat('/etc/fstab'))[9] == $orig_stat[9]) {
            rename "$tfh",'/etc/fstab';
            chmod $orig_stat[2],'/etc/fstab';
            chown @orig_stat[4,5],'/etc/fstab';
            return 1 if $was_there;
            return;
        }
        # fstab has changed, re-run the whole thing
    }
}

my %mounted;

sub sanitize {
    my ($path)=@_;

    $path=~s{\s}{-}g;
    $path=~s{/+}{_}g;

    return $path;
}

sub str_from_bytearray {
    my ($aref) = @_;

    return $aref unless ref($aref);

    return pack 'C*',@{$aref||[]};
}

sub device_added {
    my ($node,$interfaces_and_properties) = @_;

    my $fs = $interfaces_and_properties->{'org.freedesktop.UDisks2.Filesystem'};
    return unless $fs;
    return if @{ $fs->{MountPoints} || [] }; # mounted already

    return if exists $mounted{$node};

    my $block = $interfaces_and_properties->{'org.freedesktop.UDisks2.Block'};

    my $uuid = str_from_bytearray($block->{IdUuid});
    my $dev_path= str_from_bytearray($block->{Device})
        || ($uuid?'/dev/disk/by-uuid/'.$uuid:undef)
        || 'unknown';

    if ($dev_path eq 'unknown') {
        warn "unknown path for $node\n";
        return;
    }

    my $label = str_from_bytearray($block->{IdLabel});
    my $fstype = str_from_bytearray($block->{IdType});

    return unless defined $fstype;

    my $mountpoint='/mnt/'.(sanitize($label)||$uuid);

    if (-e $mountpoint) {
        $mountpoint.='-0';
        while (-e $mountpoint) {++$mountpoint};
    }

    my $manual_mountpoint=add_to_fstab($dev_path,$mountpoint,$fstype,'users,noauto,noatime,nodiratime,nosuid,nodev');

    $mountpoint=$manual_mountpoint if defined $manual_mountpoint;

    $mounted{$node}=$mountpoint;
    mkdir $mountpoint;
    chown -1,scalar(getgrnam('plugdev')),$mountpoint;
    chmod 0775,$mountpoint;

    return;
}

sub device_removed {
    my ($node,$interfaces) = @_;

    if (exists $mounted{$node}) {
        system('umount',$mounted{$node});
        if (remove_from_fstab($mounted{$node})) {
            rmdir $mounted{$node};
        }
        delete $mounted{$node};
    }
    return;
}

my $reactor=Net::DBus::Reactor->main();

my $bus=Net::DBus->system();
my $udisks=$bus->get_service('org.freedesktop.UDisks2');
my $manager = $udisks->get_object('/org/freedesktop/UDisks2');

$manager->connect_to_signal('InterfacesAdded',\&device_added);
$manager->connect_to_signal('InterfacesRemoved',\&device_removed);

$reactor->run();

__END__

=head1 NAME

hal-automounter - a simple UDisks2-DBUS client that creates fstab entries for hot-plugged volumes

=head1 SYNOPSIS

  sudo hal-automounter

=head1 AUTHOR

Gianni Ceccarelli <dakkar@thenautilus.net>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012-2017 by Gianni Ceccarelli.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, version 3.

=cut
