0.08: support for filenames which are null (it will ne named NULL-id)
[Fuse-DBI] / DBI.pm
diff --git a/DBI.pm b/DBI.pm
index bb42967..89889bd 100755 (executable)
--- a/DBI.pm
+++ b/DBI.pm
@@ -12,8 +12,10 @@ use DBI;
 use Carp;
 use Data::Dumper;
 
 use Carp;
 use Data::Dumper;
 
+our $VERSION = '0.08';
 
 
-our $VERSION = '0.03';
+# block size for this filesystem
+use constant BLOCK => 1024;
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -24,15 +26,15 @@ 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
 
 This module will use C<Fuse> module, part of C<FUSE (Filesystem in USErspace)>
 
 =head1 DESCRIPTION
 
 This module will use C<Fuse> module, part of C<FUSE (Filesystem in USErspace)>
-available at L<http://sourceforge.net/projects/avf> to mount
+available at L<http://fuse.sourceforge.net/> to mount
 your database as file system.
 
 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 +48,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;
@@ -78,13 +134,17 @@ sub mount {
 
        print Dumper($arg);
 
 
        print Dumper($arg);
 
+       unless ($self->fuse_module_loaded) {
+               print STDERR "no fuse module loaded. Trying sudo modprobe fuse!\n";
+               system "sudo modprobe fuse" || die "can't modprobe fuse using sudo!\n";
+       }
+
        carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
        carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
 
        # save (some) arguments in self
        foreach (qw(mount invalidate)) {
                $self->{$_} = $arg->{$_};
        carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
        carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
 
        # save (some) arguments in self
        foreach (qw(mount invalidate)) {
                $self->{$_} = $arg->{$_};
-               $fuse_self->{$_} = $arg->{$_};
        }
 
        foreach (qw(filenames read update)) {
        }
 
        foreach (qw(filenames read update)) {
@@ -99,19 +159,34 @@ sub mount {
                die "fork() failed: $!" unless defined $pid;
                # child will return to caller
                if ($pid) {
                die "fork() failed: $!" unless defined $pid;
                # child will return to caller
                if ($pid) {
-                       return $self;
+                       my $counter = 4;
+                       while ($counter && ! $self->is_mounted) {
+                               select(undef, undef, undef, 0.5);
+                               $counter--;
+                       }
+                       if ($self->is_mounted) {
+                               return $self;
+                       } else {
+                               return undef;
+                       }
                }
        }
 
        $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;
 
+       $fuse_self = \$self;
+
        Fuse::main(
                mountpoint=>$arg->{'mount'},
                getattr=>\&e_getattr,
        Fuse::main(
                mountpoint=>$arg->{'mount'},
                getattr=>\&e_getattr,
@@ -123,15 +198,42 @@ 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,
        );
-
+       
        exit(0) if ($arg->{'fork'});
 
        return 1;
 
 };
 
        exit(0) if ($arg->{'fork'});
 
        return 1;
 
 };
 
+=head2 is_mounted
+
+Check if fuse filesystem is mounted
+
+  if ($mnt->is_mounted) { ... }
+
+=cut
+
+sub is_mounted {
+       my $self = shift;
+
+       my $mounted = 0;
+       my $mount = $self->{'mount'} || confess "can't find mount point!";
+       if (open(MTAB, "/etc/mtab")) {
+               while(<MTAB>) {
+                       $mounted = 1 if (/ $mount fuse /i);
+               }
+               close(MTAB);
+       } else {
+               warn "can't open /etc/mtab: $!";
+       }
+
+       return $mounted;
+}
+
+
 =head2 umount
 
 Unmount your database as filesystem.
 =head2 umount
 
 Unmount your database as filesystem.
@@ -146,20 +248,35 @@ database to filesystem.
 sub umount {
        my $self = shift;
 
 sub umount {
        my $self = shift;
 
-       system "fusermount -u ".$self->{'mount'} || croak "umount error: $!";
+       if ($self->{'mount'} && $self->is_mounted) {
+               system "( fusermount -u ".$self->{'mount'}." 2>&1 ) >/dev/null";
+               if ($self->is_mounted) {
+                       system "sudo umount ".$self->{'mount'} ||
+                       return 0;
+               }
+               return 1;
+       }
 
 
-       return 1;
+       return 0;
 }
 
 }
 
-#$SIG{'INT'} = sub {
-#      print STDERR "umount called by SIG INT\n";
-#      umount;
-#};
+$SIG{'INT'} = sub {
+       if ($fuse_self && $$fuse_self->umount) {
+               print STDERR "umount called by SIG INT\n";
+       }
+};
+
+$SIG{'QUIT'} = sub {
+       if ($fuse_self && $$fuse_self->umount) {
+               print STDERR "umount called by SIG QUIT\n";
+       }
+};
 
 sub DESTROY {
        my $self = shift;
 
 sub DESTROY {
        my $self = shift;
-       print STDERR "umount called by DESTROY\n";
-       $self->umount;
+       if ($self->umount) {
+               print STDERR "umount called by DESTROY\n";
+       }
 }
 
 =head2 fuse_module_loaded
 }
 
 =head2 fuse_module_loaded
@@ -169,7 +286,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
 
@@ -184,17 +301,22 @@ sub fuse_module_loaded {
 }
 
 my %files;
 }
 
 my %files;
-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) = (
                '.' => {
                        type => 0040,
                        mode => 0755,
                },
        # create empty filesystem
        (%files) = (
                '.' => {
                        type => 0040,
                        mode => 0755,
                },
+               '..' => {
+                       type => 0040,
+                       mode => 0755,
+               },
        #       a => {
        #               cont => "File 'a'.\n",
        #               type => 0100,
        #       a => {
        #               cont => "File 'a'.\n",
        #               type => 0100,
@@ -207,18 +329,19 @@ sub read_filenames {
 
        # read them in with sesible defaults
        while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
 
        # read them in with sesible defaults
        while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
+               $row->{'filename'} ||= 'NULL-'.$row->{'id'};
                $files{$row->{'filename'}} = {
                        size => $row->{'size'},
                        mode => $row->{'writable'} ? 0644 : 0444,
                        id => $row->{'id'} || 99,
                };
 
                $files{$row->{'filename'}} = {
                        size => $row->{'size'},
                        mode => $row->{'writable'} ? 0644 : 0444,
                        id => $row->{'id'} || 99,
                };
 
+
                my $d;
                foreach (split(m!/!, $row->{'filename'})) {
                        # first, entry is assumed to be file
                        if ($d) {
                                $files{$d} = {
                my $d;
                foreach (split(m!/!, $row->{'filename'})) {
                        # first, entry is assumed to be file
                        if ($d) {
                                $files{$d} = {
-                                               size => $dirs{$d}++,
                                                mode => 0755,
                                                type => 0040
                                };
                                                mode => 0755,
                                                type => 0040
                                };
@@ -236,7 +359,7 @@ sub read_filenames {
                }
        }
 
                }
        }
 
-       print "found ",scalar(keys %files)-scalar(keys %dirs)," files, ",scalar(keys %dirs), " dirs\n";
+       print "found ",scalar(keys %files)," files\n";
 }
 
 
 }
 
 
@@ -252,8 +375,8 @@ sub e_getattr {
        $file =~ s,^/,,;
        $file = '.' unless length($file);
        return -ENOENT() unless exists($files{$file});
        $file =~ s,^/,,;
        $file = '.' unless length($file);
        return -ENOENT() unless exists($files{$file});
-       my ($size) = $files{$file}{size} || 1;
-       my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,0,0,1,1024);
+       my ($size) = $files{$file}{size} || 0;
+       my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,int(($size+BLOCK-1)/BLOCK),0,0,1,BLOCK);
        my ($atime, $ctime, $mtime);
        $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
 
        my ($atime, $ctime, $mtime);
        $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
 
@@ -261,7 +384,7 @@ sub e_getattr {
 
        # 2 possible types of return values:
        #return -ENOENT(); # or any other error you care to
 
        # 2 possible types of return values:
        #return -ENOENT(); # or any other error you care to
-       #print(join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n");
+       #print "getattr($file) ",join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n";
        return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
 }
 
        return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
 }
 
@@ -273,7 +396,7 @@ sub e_getdir {
        my %out;
        foreach my $f (sort keys %files) {
                if ($dirname) {
        my %out;
        foreach my $f (sort keys %files) {
                if ($dirname) {
-                       if ($f =~ s/^\E$dirname\Q\///) {
+                       if ($f =~ s/^\Q$dirname\E\///) {
                                $out{$f}++ if ($f =~ /^[^\/]+$/);
                        }
                } else {
                                $out{$f}++ if ($f =~ /^[^\/]+$/);
                        }
                } else {
@@ -295,6 +418,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";
 }
 
@@ -309,6 +434,7 @@ sub e_open {
 
        read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
 
 
        read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
 
+       $files{$file}{cont} ||= '';
        print "open '$file' ",length($files{$file}{cont})," bytes\n";
        return 0;
 }
        print "open '$file' ",length($files{$file}{cont})," bytes\n";
        return 0;
 }
@@ -340,6 +466,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;
@@ -368,7 +495,7 @@ sub update_db {
                }
                print "updated '$file' [",$files{$file}{id},"]\n";
 
                }
                print "updated '$file' [",$files{$file}{id},"]\n";
 
-               $fuse_self->{'invalidate'}->() if (ref $fuse_self->{'invalidate'});
+               $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
        }
        return 1;
 }
        }
        return 1;
 }
@@ -423,18 +550,43 @@ sub e_utime {
        return 0;
 }
 
        return 0;
 }
 
-sub e_statfs { return 255, 1, 1, 1, 1, 2 }
+sub e_statfs {
 
 
-sub e_unlink {
-       my $file = filename_fixup(shift);
+       my $size = 0;
+       my $inodes = 0;
 
 
-       return -ENOENT() unless exists($files{$file});
+       foreach my $f (keys %files) {
+               if ($f !~ /(^|\/)\.\.?$/) {
+                       $size += $files{$f}{size} || 0;
+                       $inodes++;
+               }
+               print "$inodes: $f [$size]\n";
+       }
 
 
-       print "unlink '$file' will invalidate cache\n";
+       $size = int(($size+BLOCK-1)/BLOCK);
 
 
-       read_content($file,$files{$file}{id});
+       my @ret = (255, $inodes, 1, $size, $size-1, BLOCK);
 
 
-       return 0;
+       #print "statfs: ",join(",",@ret),"\n";
+
+       return @ret;
+}
+
+sub e_unlink {
+       my $file = filename_fixup(shift);
+
+#      if (exists( $dirs{$file} )) {
+#              print "unlink '$file' will re-read template names\n";
+#              print Dumper($fuse_self);
+#              $$fuse_self->{'read_filenames'}->();
+#              return 0;
+       if (exists( $files{$file} )) {
+               print "unlink '$file' will invalidate cache\n";
+               read_content($file,$files{$file}{id});
+               return 0;
+       }
+
+       return -ENOENT();
 }
 1;
 __END__
 }
 1;
 __END__
@@ -443,10 +595,20 @@ __END__
 
 Nothing.
 
 
 Nothing.
 
+=head1 BUGS
+
+Size information (C<ls -s>) is wrong. It's a problem in upstream Fuse module
+(for which I'm to blame lately), so when it gets fixes, C<Fuse::DBI> will
+automagically pick it up.
+
 =head1 SEE ALSO
 
 C<FUSE (Filesystem in USErspace)> website
 =head1 SEE ALSO
 
 C<FUSE (Filesystem in USErspace)> website
-L<http://sourceforge.net/projects/avf>
+L<http://fuse.sourceforge.net/>
+
+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
 
 
 =head1 AUTHOR