#!/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
##
##   Basic idea to use gphoto2 bound in to FUSE via the cmdline binary
##   comes from Christopher Lester
#
#
#    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, Net::FTP, Cache::File
# Version: 0.1 (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 Net::FTP;
use Fuse;

use constant DEBUG => 1;

our $VERSION;

$VERSION = '0.2';

#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 = (
		"--cache" => "file",
                "--timeout" => "300",
		@arg_opts,
    );
map { $arg_opts{$_}++ } grep /^[^=]+$/, @opts;

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

my $timeout = $arg_opts{"--timeout"};

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

print "fusegphoto2 $VERSION - 2006 - Damion Yates <Damion.Yates\@bbc.co.uk>\n";
## -original- print "fuseftp $VERSION - 2005 (c) by Marcus Thiesen <marcus\@thiesen.org>\n";

my $filecache;
if ($arg_opts{"--cache"} eq "file") {
    require Cache::File;
    $filecache = new Cache::File( cache_root => $homedir,
				 default_expires => $timeout );
}
if ($arg_opts{"--cache"} eq "memory") {
    require Cache::Memory;
    $filecache = new Cache::Memory( default_expires => $timeout );
}

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;

##
##
##
## So far most of the edits are just removing ftp functionality, 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 ($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.

    my $alldir=`gphoto2 --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 = 0666;
            $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 --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,'..');

    return (@returnfiles, 0);
}

sub cam_getattr {
    my $filename = shift;

    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.

#    my $oldname = shift;
#    my $newname = shift;
#
#    $oldname = catdir($basedir, $oldname);
#    $newname = catdir($basedir, $newname);
#
#    if ($ftp->rename($oldname, $newname)) {
#	my ($dir) = $newname =~ m|(.+)/D+|;
#	delete $dir_seen{$dir} if $dir;
#	return 0;
#    } else {
#	return -EIO();
#    }
}

sub cam_open {
return -ENOENT(); # for the mo
#     my $file = shift;
#     my $flags = shift;
# 
#     $file = catdir($basedir, $file);
# 
#     print "open: $file\n" if DEBUG;
# 
#     if ($flags & (O_WRONLY | O_APPEND)) {
# 	print("opening $file for WRONLY | APPEND\n") if DEBUG;
# 	unless ($filecache->get( $file )) {
# #GOT TO
# 	    my $size = $ftp->size($file);
# 	    if ($size) {
# 		$file_obj{$file} = $ftp->retr($file);
# 		my $data = my_read($file, $size);
# 		$filecache->set($file, $data, 'never');
# 		$file_obj{$file}->close();
# 	    }
# 	}
# 	$file_obj{$file} = $ftp->stor($file);
# 	if ($file_obj{$file}) {
# 	    return 0;
# 	} else {
# 	    print("opening failed\n") if DEBUG;
# 	    return -ENOENT();
# 	}
#     }
# 
#     if ($flags & (O_WRONLY)) {
# 	print("opening $file for WRONLY\n") if DEBUG;
# 	$file_obj{$file} = $ftp->stor($file);
# 	if ($file_obj{$file}) {
# 	    return 0;
# 	} else {
# 	    print("opening failed\n") if DEBUG;
# 	    return -ENOENT();
# 	}
#     }
# 
#     print("opening $file for read\n") if DEBUG;
#     $file_obj{$file} = $ftp->retr($file);
#     if ($file_obj{$file}) {
# 	return 0;
#     } else {
# 	print("opening failed\n") if DEBUG;
# 	return -ENOENT();
#     }
# 
# 
#     return -ENOENT();
}

sub my_read {
return -ENOENT(); # for the mo
#     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 {
return -ENOENT(); # for the mo
#     my $file = shift;
#     my $size = shift;
#     my $offset = shift;
# 
#     $file = catdir($basedir, $file);
# 
#     print "read: $file\n" if DEBUG;
#     if (!$file_obj{$file}) {
# 	return -EIO();
#     }
#     
#     my $data = "";
#     if ($filecache->get( $file )) {
# 	$data = $filecache->get( $file );
# 	if (length($data) < $offset + $size) {
# 	    $data .= my_read($file, $size);
# 	} 
#     } else {
# 	$data = my_read($file, $offset + $size);
#     }
# 
#     $filecache->set($file, $data, $timeout);
#     return substr($data, $offset, $size);
}

sub cam_release {
return -ENOENT(); # for the mo
#     my $file = shift;
# 
#     $file = catdir($basedir, $file);
# 
#     print("release $file\n") if DEBUG;
# 
#     if ($file_obj{$file}) {
# 	$filecache->remove($file);
# 	delete $attr_cache{$file};
# 	$type_cache{$file} = 'f';
# 	my ($dir) = $file =~ m|(.+)/D+|;
# 	delete $dir_seen{$dir} if $dir;
# 	$file_obj{$file}->close;
# 	delete $file_obj{$file};
# 	$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 ($file =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualfile = $2;
    }

    print "delete $actualfile from $base\n" if DEBUG;
 
    my $success=`gphoto2 --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 ($dir =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualdir = $2;
    }

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

    $success=`gphoto2 --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 ($dir =~ m|^(.+)/([^/]+)$|) {
        $base = $1;
        $actualdir = $2;
    }

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

    $success=`gphoto2 --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 {
	return -EIO(); # for the moment.
#     my $file = shift;
# 
#     $file = catdir($basedir, $file);
# 
#     print "flush: $file\n" if DEBUG;
# 
#     my $data = $filecache->get($file);
#     if ($file_obj{$file}) {
# 	if ($data) {
# 	    $file_obj{$file}->write($data, length($data));
# 	    $file_offset{$file} += length($data);
# 	    $filecache->set($file, "", 'never');
# 	}
# 	print "returning from flush\n" if DEBUG;
# 	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;

    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:

  --cache=memory      The default caching uses a file system caching system, if
                      you want to speed things up and won't transfer big files
                      you can use in memory caching.

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

  --foreground        Don't put process into background

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

  --timeout=seconds   Timeout for read cache, files will be stored 'seconds' 
                      seconds in the cache. Defaults to 300.

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

