#!/usr/bin/perl -w

##   Kludgy patches to fuseftp perl script, by: Damion Yates
##   (damion.yates@siemens.com / damion.yates\@bbc.co.uk)  20060213
##   original license notice left below.
##
##   Large chunks of this code are completely unaltered from the
##   work by Marcus Thiesen on fuseftp, including some comments
##
#
#
#    Copyright 2005, Marcus Thiesen (marcus@thiesen.org) All rights reserved.
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of either:
#
#    a) the GNU General Public License as published by the Free Software
#    Foundation; either version 1, or (at your option) any later
#       version, or
#
#    b) the "Artistic License" which comes with Perl.
#
#    On Debian GNU/Linux systems, the complete text of the GNU General
#    Public License can be found in `/usr/share/common-licenses/GPL' and
#    the Artistic Licence in `/usr/share/common-licenses/Artistic'.
#
#    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.  
#
#########################################################################
#
# A userspace filesystem for gphoto2
# Usage: fusegphoto2 mountpoint &
# 
# Prereq: Fuse, Date::Parse;
# Version: 0.3 (based on version 0.7 of fuseftp)
# 
#########################################################################

use strict;
use warnings;

#core
use POSIX qw(:errno_h :fcntl_h);
use File::Spec::Functions;

#preregs
use Date::Parse;
use Fuse;

use constant DEBUG => 0;

our $VERSION;

$VERSION = '0.3';

#initial stuff
my $homedir = catdir($ENV{HOME},'.fusegphoto2');
mkdir $homedir unless -d $homedir;

#get command line arguments
my @opts = grep /^-/, @ARGV;
my @other = grep /^[^-]/, @ARGV;
my $mountpoint = shift @other;

my @arg_opts = map { split( /=/, $_ ) } grep( /=/, @opts);
my %arg_opts = (
        @arg_opts,
    );
map { $arg_opts{$_}++ } grep /^[^=]+$/, @opts;

if ($arg_opts{"-h"} || $arg_opts{"--help"}) {
    print join "", <DATA>;
    exit(0);
}

my $xvpics = 1 if ($arg_opts{"--xvpics"});

unless (defined $mountpoint) {
    print("$0 [options] mountpoint\n");
    exit 1;
}

print "fusegphoto2 $VERSION - 2006 - Damion Yates <Damion.Yates\@bbc.co.uk>\n";

my $basedir = '/';

## TODO - probe for single camera, any problems die before we try to
##        do anything

die "Mountpoint $mountpoint does not exist or is busy\n" unless -d $mountpoint;

my %attr_cache = ();
my %type_cache = ( $basedir =>  'd' );
my %dir_seen = ();
my %file_obj = ();
my %link_cache = ();
my %file_offset = ();

my $time = time;

##
## Now come most of the changes.  This is similar I guess, to editing the
## example.pl but I felt I needed the caching that Marcus Thiesen introduced.
##

#subs
sub cam_getdir {
    my $dir = shift;
    print "called getdir for '$dir'\n" if DEBUG;
    my (@files,@returnfiles,@dirs);
    my ($xv,$dfilename,$dsize,$dperms,$ddate);
## used to do a basic -L -l which lists dirs and files, but a full list of
## attributes is requested far too often and spawning gphoto2 each file
## completely blows chunks.  I should implement a cache check here, but
## for the moment --show-info 1-16384 isn't too slow.

    if ($xvpics) {
        $xv=1 if ($dir =~ s/\/Dxvpics//g);
    }

    my $alldir=`gphoto2 --port=usb: --no-recurse -f $dir --show-info 1-16384 2>/dev/null`;
    @files = split (/\nInformation/,$alldir);
    foreach (@files) {

        if (($dfilename,$dsize,$dperms,$ddate) = m/^ on file '([^']+)'.*\nFile:.*\n  Name:.*\n  Mime.*\n  Size:\s+(\d+)\s+.*\n  Dow.*\n  Permissions:\s+(.*)\n  Time:\s+(.*)\nThum.*/s) {

            #print "filename: $dfilename size: $dsize perms: $dperms date: $ddate\n" if DEBUG;
            my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);

            my $type = 0100;
            my $modebits = ($dperms=~/delete/) ? 0644 : 0444;
            $mode = ($type << 9) + $modebits;
            $nlink = 1;
            $uid = $<;
            ($gid) = split / /, $(;
            $rdev = 0;
            $atime = str2time("$ddate");
            $size = $dsize;
            $mtime = $atime;
            $ctime = $atime;
            $blksize = 1024;
            $blocks = 1;
            $dev = 0;
            $ino = 0;
            $attr_cache{catdir($dir,$dfilename)} = [$dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks];
            $type_cache{catdir($dir,$dfilename)}="f";
            push (@returnfiles,$dfilename);
        }
    }

    @dirs = `gphoto2 --port=usb: --quiet --no-recurse -f $dir -l 2>/dev/null`;

    foreach my $entry (@dirs) {
        next if ($entry !~ /^ - /);
        chomp($entry);
        $entry=~s/^ - //g;
        $type_cache{catdir($dir,$entry)}="d";
        push (@returnfiles,$entry);
    }    
    $type_cache{catdir($dir,'.')}="d";
    push (@returnfiles,'.');
    $type_cache{catdir($dir,'..')}="d";
    push (@returnfiles,'..');

    if (!$xv && $xvpics) {
        $type_cache{catdir($dir,'.xvpics')}="d";
        push (@returnfiles,'.xvpics');
    }

    return (@returnfiles, 0);
}

sub cam_getattr {
    my $filename = shift;
    my $xv;

    if ($xvpics) {
        $xv=1 if ($filename =~ s/\/Dxvpics//g);
    }

    if (!exists $attr_cache{$filename}) {
        my $base = $filename;
    
        if ($filename =~ m|^(.+)/([^/]+)$|) {
            $base = $1;
        }
    
        if (! exists $dir_seen{$base} ) {
            cam_getdir($base);
    
    ## this fills %attr_cache for files.
    
            $dir_seen{$base}++;
        }
    
        if ((!$type_cache{$filename}) && $filename ne $base) {
            $attr_cache{$filename} = undef;
            print "returning ENOENT for $filename\n" if DEBUG;
            return -ENOENT();
        }
    
        if ($type_cache{$filename} eq 'd') {
            my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
            my $type = 0040;
            my $modebits = 0755;
            $mode = ($type << 9) + $modebits;
            $nlink = 1;
            $uid = $<;
            ($gid) = split / /, $(;
            $rdev = 0;
            $atime = $time; # as good as anything
            $size = 512;    # looks good for dir sizes
            $mtime = $atime;
            $ctime = $atime;
            $blksize = 1024;
            $blocks = 1;
            $dev = 0;
            $ino = 0;
            $attr_cache{$filename} = [$dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks];
            print "returning attr for $filename\n" if DEBUG;
            return @{$attr_cache{$filename}};
    
        } else {

## well we only support dirs and files, so it _has_ to have
## been cached by the getdir() earlier.

        if (defined $attr_cache{$filename}) {
            print "returning attr for $filename\n" if DEBUG;
            return @{$attr_cache{$filename}};
            } else {
                return -ENOENT();
            }
        }
    } else {
        print "returning attr for $filename (cached)\n" if DEBUG;
        if (defined $attr_cache{$filename}) {
            return @{$attr_cache{$filename}};
        } else {
            return -ENOENT();
        }
    }
}

sub cam_rename {
    return -EIO();

## not implementing rename() for good reason.

}

sub cam_open {
    my $file = shift;
    my $flags = shift;
    my ($getparam,$base,$actualfile);

    $getparam="p";

    print "open: $file\n" if DEBUG;

    return -EACCES() if ($flags & (O_WRONLY));

    print("opening $file for read\n") if DEBUG;

    $file =~ m|^(.+)/([^/]+)$| or return -ENOENT();
    $base = $1;
    $actualfile = $2; 
    $getparam="t" if ($xvpics && ($base =~ s/\/Dxvpics//g));

#    open($file_obj{$file},"gphoto2 --port=usb: --quiet --stdout -f $base -p $actualfile 2>/dev/null|");

## the weird code layout is due to the change in how I pull thumbnails, it's
## a pain atm.  I hope to switch to imagemagick but this will have to do.

## inefficient in memory usage, but it'll do for the moment.

    if ($getparam eq 'p') {
        $file_obj{$file}=`gphoto2 --port=usb: --quiet --stdout -f $base -$getparam $actualfile 2>/dev/null`;
    } else {
        $file_obj{$file}=`gphoto2 --port=usb: --quiet --stdout -f $base -$getparam $actualfile |mogrify -scale 80x60 -format p7 jpeg:- 2>/dev/null`;
    }

    if ($file_obj{$file}) {
        return 0;
    } else {
        print("opening failed\n") if DEBUG;
        return -ENOENT();
    }
}

sub my_read {
#     my $file = shift;
#     my $size = shift;
# 
#     print("reading $size from $file\n") if DEBUG;
# 
#     my $retval = "";
#     my $buffer = "";
#     my $read_total = 0;
#     my $read_bytes;
#     while ($read_total < $size) {
#     my $read_bytes = $file_obj{$file}->read($buffer, $size);
#     last if ($read_bytes == 0); #EOF
#     $read_total += $read_bytes;
#     $retval .= $buffer;
#     }
# 
#     print ("done ($read_total)\n") if DEBUG;
#     return $retval;
}

sub cam_read {
    my $file = shift;
    my $size = shift;
    my $offset = shift;

    print "read: $file\n" if DEBUG;
    if (!$file_obj{$file}) {
        return -EIO();
    }
    return substr($file_obj{$file}, $offset, $size);
}

sub cam_release {
    my $file = shift;

    print("release $file\n") if DEBUG;

    if ($file_obj{$file}) {

## these are only useful if the file was opened for write.

#        delete $attr_cache{$file};
#        $type_cache{$file} = 'f';
#        my ($dir) = $file =~ m|(.+)/D+|;
#        delete $dir_seen{$dir} if $dir;

        delete $file_obj{$file};

## not sure about this yet.
#        $file_offset{$file} = 0;

        return 0;
    } else {
        warn "Trying to close not open file $file\n";
        return -EIO();
    }
}

sub cam_readlink {
    return -EIO(); # Unlikely to ever cater for links.
# 
#     my $file = shift;
#     my $dir;
# 
#     $file = catdir($basedir, $file);
# 
#     if (!exists $link_cache{$file}) {
#     print "readlink: $file\n" if DEBUG;
# 
#     if ($file =~ m|(^/D+/).+|) {
#         $dir = $1;
#     }
#     $dir = '/' unless $dir;
# 
#     my @lines = $ftp->dir($dir);
# 
#     my $cfile = $file;
#     $cfile =~ s|.*/||;
# 
#     foreach my $line (@lines) {
#         print $line . "\n" if DEBUG;
#         if ($line =~ $cfile) {
#         my ($link,$target) = split /\s*->\s*/, $line;
#         $target =~ s|^$basedir||;
#         $link_cache{$file} = $target;
#         return $target;
#         }
#     }
#     return -EIO();
#     } else {
#     return $link_cache{$file};
#     }
}

sub cam_unlink {
 
    my $file = shift;
    my ($base,$actualfile);
 
    if ($xvpics && ($file =~ m/Dxvpics/)) {
        return -EIO(); #don't permit deleting anything in .xvpics
    }

    if ($file =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualfile = $2;
    }

    print "delete $actualfile from $base\n" if DEBUG;
 
    my $success=`gphoto2 --port=usb: --quiet --no-recurse -f $base --delete-file $actualfile 2>/dev/null`;
    if (!$success) {
        delete $dir_seen{$base};
        delete $type_cache{$file};
        delete $attr_cache{$file};
        return 0; 
    } else {
        return -EIO();
    }
}

sub cam_rmdir {
    my $dir = shift;
    my $success;
    my ($base,$actualdir);

    if ($xvpics && ($dir =~ m/Dxvpics/)) {
        return -EIO(); # don't permit anything around a .xvpics dir
    }
 
    if ($dir =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualdir = $2;
    }

    print "removing dir $actualdir in $base\n" if DEBUG;

    $success=`gphoto2 --port=usb: --quiet --no-recurse -f "$base" --rmdir "$actualdir" 2>/dev/null`;

    if (!$success) {
        delete $dir_seen{$dir};
        delete $dir_seen{$base};
        delete $type_cache{$dir};
        delete $attr_cache{$dir};
        return 0;
    } else {
        return -EIO();
    }
}

sub cam_mkdir {
    my $dir = shift;
    my $success;
    my ($base,$actualdir);

    if ($xvpics && ($dir =~ m/Dxvpics/)) {
        return -EIO(); # don't permit anything around a .xvpics dir
    }
 
    if ($dir =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualdir = $2;
    }

    print "making dir $actualdir in $base\n" if DEBUG;

    $success=`gphoto2 --port=usb: --quiet --no-recurse -f "$base" --mkdir "$actualdir" 2>/dev/null`;

    if (!$success) {
        delete $dir_seen{$base};
        delete $dir_seen{$dir};
        delete $type_cache{$dir};
        delete $attr_cache{$dir};
        cam_getdir($base);
        return 0;
    } else {
        return -EIO();
    }
}

sub cam_write {
    return -EIO(); # for the moment
#     my $file = shift;
#     my $buffer = shift;
#     my $offset = shift;
# 
#     $file = catdir($basedir, $file);
# 
#     $file_offset{$file} = 0 unless $file_offset{$file};
# 
#     print "write $file (offset $offset)\n" if DEBUG;
# 
#     my $data = $filecache->get($file);
#     $data = "" unless defined $data;
# 
#     $offset = $offset - $file_offset{$file};
# 
#     if ($offset == 0) {
#     $data = $buffer;
#     } else {
#     substr $data, $offset, length($buffer), $buffer;
#     }
# 
#     $filecache->set($file, $data, 'never');
# 
#     print "done\n" if DEBUG;
# 
#     return length($buffer);
}

sub cam_flush {
    my $file = shift;
    print "flush: $file\n" if DEBUG;
    if ($file_obj{$file}) {
        return 0;
    } else {
        warn "Trying to flush not open file $file\n";
        return -EIO();
    }
}

sub cam_mknod{
    my $file = shift;
    my $mode = shift;
    my $device = shift;

    print "mknod $file\n" if DEBUG;

    return -EIO(); # for the mo.

#    cam_open($file, O_WRONLY);
#    cam_write($file, "", 0);
#    cam_flush($file);
#    cam_release($file);
#
#    return 0;
}

sub cam_truncate{
    return -EIO(); # for the mo.
#     my $file = shift;
#     my $offset = shift;
# 
#     print "truncate $file (offset $offset)\n" if DEBUG;
# 
#     if ($offset != 0) {
#     cam_open($file, O_RDONLY);
#     my $data = cam_read($file, $offset, 0);
#     cam_flush($file);
#     cam_release($file);
#     cam_open($file, O_WRONLY);
#     cam_write($file, $data, length($data));
#     cam_flush($file);
#     cam_release($file);
#     } else {
#     $ftp->delete(catdir($basedir, $file));
#     cam_mknod($file, 0, 0);
#     }
# 
#     print "finished truncate $file\n" if DEBUG;
# 
#     return 0;
}

#run fuse
my @extraargs;
push @extraargs, ("debug", 1) if (exists $arg_opts{"--debug"});
push @extraargs, ("mountopts", $arg_opts{"--options"}) if ($arg_opts{"--options"});

unless (DEBUG || $arg_opts{'--foreground'} || $arg_opts{"--debug"})
{
    # fork and exit parent process to put FuseFtp into background
    print "Backgrounding...\n";
    fork() and exit(0);
}

Fuse::main(mountpoint => $mountpoint,

       getdir => \&cam_getdir,
       getattr => \&cam_getattr,
       open => \&cam_open,
       read => \&cam_read,
       release => \&cam_release,
       readlink => \&cam_readlink,
       rename => \&cam_rename,
       unlink => \&cam_unlink,
       rmdir => \&cam_rmdir,
       mkdir => \&cam_mkdir,
       write => \&cam_write,
       flush => \&cam_flush,
       mknod => \&cam_mknod,
       truncate => \&cam_truncate,
       @extraargs,
       );

__DATA__

Usage: fusegphoto2 [options] mountpoint 

where options is one of:

  --debug             Enable FUSE debugging messages
                      (implies --foreground)

  --xvpics            Enable virtual .xvpics/ directory for xv thumbnails

  --foreground        Don't put process into background

  --options=opt[,opt] Pass options to FUSE
                      allow_others: allow access by other users

Mountpoint is the directory where the camera's filesystem will be mounted into
the filesystem.

