modify ctime only when writing to file, prevents message "file has changed"
[Fuse-DBI] / DBI.pm
diff --git a/DBI.pm b/DBI.pm
index 3929145..c94a5e3 100755 (executable)
--- a/DBI.pm
+++ b/DBI.pm
@@ -13,7 +13,7 @@ use Carp;
 use Data::Dumper;
 
 
 use Data::Dumper;
 
 
-our $VERSION = '0.03';
+our $VERSION = '0.04';
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -24,7 +24,7 @@ Fuse::DBI - mount your database as filesystem and use it
   use Fuse::DBI;
   Fuse::DBI->mount( ... );
 
   use Fuse::DBI;
   Fuse::DBI->mount( ... );
 
-See C<run> below for examples how to set parametars.
+See C<run> below for examples how to set parameters.
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
@@ -32,7 +32,7 @@ This module will use C<Fuse> module, part of C<FUSE (Filesystem in USErspace)>
 available at L<http://sourceforge.net/projects/avf> to mount
 your database as file system.
 
 available at L<http://sourceforge.net/projects/avf> to mount
 your database as file system.
 
-That will give you posibility to use normal file-system tools (cat, grep, vi)
+That will give you possibility to use normal file-system tools (cat, grep, vi)
 to manipulate data in database.
 
 It's actually opposite of Oracle's intention to put everything into database.
 to manipulate data in database.
 
 It's actually opposite of Oracle's intention to put everything into database.
@@ -46,15 +46,69 @@ It's actually opposite of Oracle's intention to put everything into database.
 
 Mount your database as filesystem.
 
 
 Mount your database as filesystem.
 
+Let's suppose that your database have table C<files> with following structure:
+
+ id:           int
+ filename:     text
+ size:         int
+ content:      text
+ writable:     boolean
+
+Following is example how to mount table like that to C</mnt>:
+
   my $mnt = Fuse::DBI->mount({
   my $mnt = Fuse::DBI->mount({
-       filenames => 'select name from files_table as filenames',
-       read => 'sql read',
-       update => 'sql update',
-       dsn => 'DBI:Pg:dbname=webgui',
-       user => 'database_user',
-       password => 'database_password'
+       'filenames' => 'select id,filename,size,writable from files',
+       'read' => 'select content from files where id = ?',
+       'update' => 'update files set content = ? where id = ?',
+       'dsn' => 'DBI:Pg:dbname=test_db',
+       'user' => 'database_user',
+       'password' => 'database_password',
+       'invalidate' => sub { ... },
   });
 
   });
 
+Options:
+
+=over 5
+
+=item filenames
+
+SQL query which returns C<id> (unique id for that row), C<filename>,
+C<size> and C<writable> boolean flag.
+
+=item read
+
+SQL query which returns only one column with content of file and has
+placeholder C<?> for C<id>.
+
+=item update
+
+SQL query with two pace-holders, one for new content and one for C<id>.
+
+=item dsn
+
+C<DBI> dsn to connect to (contains database driver and name of database).
+
+=item user
+
+User with which to connect to database
+
+=item password
+
+Password for connecting to database
+
+=item invalidate
+
+Optional anonymous code reference which will be executed when data is updated in
+database. It can be used as hook to delete cache (for example on-disk-cache)
+which is created from data edited through C<Fuse::DBI>.
+
+=item fork
+
+Optional flag which forks after mount so that executing script will continue
+running. Implementation is experimental.
+
+=back
+
 =cut
 
 my $dbh;
 =cut
 
 my $dbh;
@@ -64,6 +118,11 @@ my $ctime_start;
 sub read_filenames;
 sub fuse_module_loaded;
 
 sub read_filenames;
 sub fuse_module_loaded;
 
+# evil, evil way to solve this. It makes this module non-reentrant. But, since
+# fuse calls another copy of this script for each mount anyway, this shouldn't
+# be a problem.
+my $fuse_self;
+
 sub mount {
        my $class = shift;
        my $self = {};
 sub mount {
        my $class = shift;
        my $self = {};
@@ -77,7 +136,9 @@ sub mount {
        carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
 
        # save (some) arguments in self
        carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
 
        # save (some) arguments in self
-       $self->{$_} = $arg->{$_} foreach (qw(mount));
+       foreach (qw(mount invalidate)) {
+               $self->{$_} = $arg->{$_};
+       }
 
        foreach (qw(filenames read update)) {
                carp "mount needs '$_' SQL" unless ($arg->{$_});
 
        foreach (qw(filenames read update)) {
                carp "mount needs '$_' SQL" unless ($arg->{$_});
@@ -97,13 +158,21 @@ sub mount {
 
        $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
 
 
        $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
 
-       $sth->{filenames} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
+       $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
 
        $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
        $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
 
 
        $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
        $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
 
+
+       $self->{'sth'} = $sth;
+
+       $self->{'read_filenames'} = sub { $self->read_filenames };
        $self->read_filenames;
 
        $self->read_filenames;
 
+       $self->{'mounted'} = 1;
+
+       $fuse_self = \$self;
+
        Fuse::main(
                mountpoint=>$arg->{'mount'},
                getattr=>\&e_getattr,
        Fuse::main(
                mountpoint=>$arg->{'mount'},
                getattr=>\&e_getattr,
@@ -115,8 +184,11 @@ sub mount {
                utime=>\&e_utime,
                truncate=>\&e_truncate,
                unlink=>\&e_unlink,
                utime=>\&e_utime,
                truncate=>\&e_truncate,
                unlink=>\&e_unlink,
+               rmdir=>\&e_unlink,
                debug=>0,
        );
                debug=>0,
        );
+       
+       $self->{'mounted'} = 0;
 
        exit(0) if ($arg->{'fork'});
 
 
        exit(0) if ($arg->{'fork'});
 
@@ -138,11 +210,25 @@ database to filesystem.
 sub umount {
        my $self = shift;
 
 sub umount {
        my $self = shift;
 
-       system "fusermount -u ".$self->{'mount'} || croak "umount error: $!";
+       if ($self->{'mounted'}) {
+               system "fusermount -u ".$self->{'mount'} || croak "umount error: $!";
+       }
 
        return 1;
 }
 
 
        return 1;
 }
 
+$SIG{'INT'} = sub {
+       print STDERR "umount called by SIG INT\n";
+       umount;
+};
+
+sub DESTROY {
+       my $self = shift;
+       return if (! $self->{'mounted'});
+       print STDERR "umount called by DESTROY\n";
+       $self->umount;
+}
+
 =head2 fuse_module_loaded
 
 Checks if C<fuse> module is loaded in kernel.
 =head2 fuse_module_loaded
 
 Checks if C<fuse> module is loaded in kernel.
@@ -150,7 +236,7 @@ Checks if C<fuse> module is loaded in kernel.
   die "no fuse module loaded in kernel"
        unless (Fuse::DBI::fuse_module_loaded);
 
   die "no fuse module loaded in kernel"
        unless (Fuse::DBI::fuse_module_loaded);
 
-This function in called by L<mount>, but might be useful alone also.
+This function in called by C<mount>, but might be useful alone also.
 
 =cut
 
 
 =cut
 
@@ -170,6 +256,8 @@ my %dirs;
 sub read_filenames {
        my $self = shift;
 
 sub read_filenames {
        my $self = shift;
 
+       my $sth = $self->{'sth'} || die "no sth argument";
+
        # create empty filesystem
        (%files) = (
                '.' => {
        # create empty filesystem
        (%files) = (
                '.' => {
@@ -276,6 +364,8 @@ sub read_content {
 
        $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
        $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
 
        $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
        $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
+       # I should modify ctime only if content in database changed
+       #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
        print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
 }
 
        print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
 }
 
@@ -321,6 +411,7 @@ sub clear_cont {
        print "invalidate all cached content\n";
        foreach my $f (keys %files) {
                delete $files{$f}{cont};
        print "invalidate all cached content\n";
        foreach my $f (keys %files) {
                delete $files{$f}{cont};
+               delete $files{$f}{ctime};
        }
        print "begin new transaction\n";
        #$dbh->begin_work || die $dbh->errstr;
        }
        print "begin new transaction\n";
        #$dbh->begin_work || die $dbh->errstr;
@@ -348,6 +439,8 @@ sub update_db {
                        return 0;
                }
                print "updated '$file' [",$files{$file}{id},"]\n";
                        return 0;
                }
                print "updated '$file' [",$files{$file}{id},"]\n";
+
+               $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
        }
        return 1;
 }
        }
        return 1;
 }
@@ -407,13 +500,18 @@ sub e_statfs { return 255, 1, 1, 1, 1, 2 }
 sub e_unlink {
        my $file = filename_fixup(shift);
 
 sub e_unlink {
        my $file = filename_fixup(shift);
 
-       return -ENOENT() unless exists($files{$file});
-
-       print "unlink '$file' will invalidate cache\n";
-
-       read_content($file,$files{$file}{id});
+       if (exists( $dirs{$file} )) {
+               print "unlink '$file' will re-read template names\n";
+               print Dumper($fuse_self);
+               $$fuse_self->{'read_filenames'}->();
+               return 0;
+       } elsif (exists( $files{$file} )) {
+               print "unlink '$file' will invalidate cache\n";
+               read_content($file,$files{$file}{id});
+               return 0;
+       }
 
 
-       return 0;
+       return -ENOENT();
 }
 1;
 __END__
 }
 1;
 __END__
@@ -427,6 +525,10 @@ Nothing.
 C<FUSE (Filesystem in USErspace)> website
 L<http://sourceforge.net/projects/avf>
 
 C<FUSE (Filesystem in USErspace)> website
 L<http://sourceforge.net/projects/avf>
 
+Example for WebGUI which comes with this distribution in
+directory C<examples/webgui.pl>. It also contains a lot of documentation
+about design of this module, usage and limitations.
+
 =head1 AUTHOR
 
 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
 =head1 AUTHOR
 
 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>