9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
15 our $VERSION = '0.09_1';
17 # block size for this filesystem
18 use constant BLOCK => 1024;
22 Fuse::DBI - mount your database as filesystem and use it
27 Fuse::DBI->mount( ... );
29 See C<run> below for examples how to set parameters.
33 This module will use C<Fuse> module, part of C<FUSE (Filesystem in USErspace)>
34 available at L<http://fuse.sourceforge.net/> to mount
35 your database as file system.
37 That will give you possibility to use normal file-system tools (cat, grep, vi)
38 to manipulate data in database.
40 It's actually opposite of Oracle's intention to put everything into database.
49 Mount your database as filesystem.
51 Let's suppose that your database have table C<files> with following structure:
59 Following is example how to mount table like that to C</mnt>:
61 my $mnt = Fuse::DBI->mount({
62 'filenames' => 'select id,filename,size,writable from files',
63 'read' => 'select content from files where id = ?',
64 'update' => 'update files set content = ? where id = ?',
65 'dsn' => 'DBI:Pg:dbname=test_db',
66 'user' => 'database_user',
67 'password' => 'database_password',
68 'invalidate' => sub { ... },
77 SQL query which returns C<id> (unique id for that row), C<filename>,
78 C<size> and C<writable> boolean flag.
82 SQL query which returns only one column with content of file and has
83 placeholder C<?> for C<id>.
87 SQL query with two pace-holders, one for new content and one for C<id>.
91 C<DBI> dsn to connect to (contains database driver and name of database).
95 User with which to connect to database
99 Password for connecting to database
103 Optional anonymous code reference which will be executed when data is updated in
104 database. It can be used as hook to delete cache (for example on-disk-cache)
105 which is created from data edited through C<Fuse::DBI>.
109 Optional flag which forks after mount so that executing script will continue
110 running. Implementation is experimental.
114 There is also alternative way which can generate C<read> and C<update>
117 my $mnt = Fuse::DBI->mount({
118 'filenames' => 'select id,filename,size,writable from files',
120 my ($path,$file) = @_;
121 return( 'select content from files where id = ?', $file->{row}->{id} );
124 my ($path,$file) = @_;
125 return( 'update files set content = ? where id = ?', $file->{row}->{id} );
127 'dsn' => 'DBI:Pg:dbname=test_db',
128 'user' => 'database_user',
129 'password' => 'database_password',
130 'invalidate' => sub { ... },
140 sub fuse_module_loaded;
142 # evil, evil way to solve this. It makes this module non-reentrant. But, since
143 # fuse calls another copy of this script for each mount anyway, this shouldn't
152 bless($self, $class);
158 unless ($self->fuse_module_loaded) {
159 print STDERR "no fuse module loaded. Trying sudo modprobe fuse!\n";
160 system "sudo modprobe fuse" || die "can't modprobe fuse using sudo!\n";
163 carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
164 carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
166 # save (some) arguments in self
167 foreach (qw(mount invalidate)) {
168 $self->{$_} = $arg->{$_};
171 foreach (qw(filenames read update)) {
172 carp "mount needs '$_' SQL" unless ($arg->{$_});
175 $ctime_start = time();
178 if ($arg->{'fork'}) {
180 die "fork() failed: $!" unless defined $pid;
181 # child will return to caller
184 while ($counter && ! $self->is_mounted) {
185 select(undef, undef, undef, 0.5);
188 if ($self->is_mounted) {
196 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
198 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
200 $self->{'sth'} = $sth;
201 $self->{'dbh'} = $dbh;
203 $self->{'read_filenames'} = sub { $self->read_filenames };
204 $self->read_filenames;
206 foreach my $op (qw/read update/) {
207 if (ref($arg->{ $op }) ne 'CODE') {
208 $self->{ $op . '_ref' } = sub {
210 return ($arg->{ $op }, $row->{'id'});
213 $self->{ $op . '_ref' } = $arg->{ $op };
220 mountpoint=>$arg->{'mount'},
221 getattr=>\&e_getattr,
228 truncate=>\&e_truncate,
234 exit(0) if ($arg->{'fork'});
242 Check if fuse filesystem is mounted
244 if ($mnt->is_mounted) { ... }
252 my $mount = $self->{'mount'} || confess "can't find mount point!";
253 if (open(MTAB, "/etc/mtab")) {
255 $mounted = 1 if (/ $mount fuse /i);
259 warn "can't open /etc/mtab: $!";
268 Unmount your database as filesystem.
272 This will also kill background process which is translating
273 database to filesystem.
280 if ($self->{'mount'} && $self->is_mounted) {
281 system "( fusermount -u ".$self->{'mount'}." 2>&1 ) >/dev/null";
283 if ($self->is_mounted) {
284 system "sudo umount ".$self->{'mount'} ||
294 if ($fuse_self && $fuse_self->can('umount')) {
295 print STDERR "umount called by SIG INT\n";
300 if ($fuse_self && $fuse_self->can('umount')) {
301 print STDERR "umount called by SIG QUIT\n";
308 print STDERR "umount called by DESTROY\n";
312 =head2 fuse_module_loaded
314 Checks if C<fuse> module is loaded in kernel.
316 die "no fuse module loaded in kernel"
317 unless (Fuse::DBI::fuse_module_loaded);
319 This function in called by C<mount>, but might be useful alone also.
323 sub fuse_module_loaded {
325 die "can't start lsmod: $!" unless ($lsmod);
326 if ($lsmod =~ m/fuse/s) {
338 my $sth = $self->{'sth'} || die "no sth argument";
340 # create empty filesystem
351 # cont => "File 'a'.\n",
353 # ctime => time()-2000
357 # fetch new filename list from database
358 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
360 # read them in with sesible defaults
361 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
362 $row->{'filename'} ||= 'NULL-'.$row->{'id'};
363 $files->{$row->{'filename'}} = {
364 size => $row->{'size'},
365 mode => $row->{'writable'} ? 0644 : 0444,
366 id => $row->{'id'} || undef,
372 foreach (split(m!/!, $row->{'filename'})) {
373 # first, entry is assumed to be file
379 $files->{$d.'/.'} = {
383 $files->{$d.'/..'} = {
393 print "found ",scalar(keys %{$files})," files\n";
400 $file = '.' unless length($file);
405 my ($file) = filename_fixup(shift);
407 $file = '.' unless length($file);
408 return -ENOENT() unless exists($files->{$file});
409 my ($size) = $files->{$file}->{size} || 0;
410 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,int(($size+BLOCK-1)/BLOCK),0,0,1,BLOCK);
411 my ($atime, $ctime, $mtime);
412 $atime = $ctime = $mtime = $files->{$file}->{ctime} || $ctime_start;
414 my ($modes) = (($files->{$file}->{type} || 0100)<<9) + $files->{$file}->{mode};
416 # 2 possible types of return values:
417 #return -ENOENT(); # or any other error you care to
418 #print "getattr($file) ",join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n";
419 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
423 my ($dirname) = shift;
425 # return as many text filenames as you like, followed by the retval.
426 print((scalar keys %{$files})." files total\n");
428 foreach my $f (sort keys %{$files}) {
430 if ($f =~ s/^\Q$dirname\E\///) {
431 $out{$f}++ if ($f =~ /^[^\/]+$/);
434 $out{$f}++ if ($f =~ /^[^\/]+$/);
438 $out{'no files? bug?'}++;
440 print scalar keys %out," files in dir '$dirname'\n";
441 print "## ",join(" ",keys %out),"\n";
442 return (keys %out),0;
446 my $file = shift || die "need file";
448 warn "# read_content($file)\n" if ($debug);
450 my @args = $fuse_self->{'read_ref'}->($files->{$file});
451 my $sql = shift @args || die "need SQL for $file";
453 $fuse_self->{'read_sth'}->{$sql} ||= $fuse_self->{dbh}->prepare($sql) || die $dbh->errstr();
454 my $sth = $fuse_self->{'read_sth'}->{$sql} || die;
456 $sth->execute(@args) || die $sth->errstr;
457 $files->{$file}->{cont} = $sth->fetchrow_array;
458 # I should modify ctime only if content in database changed
459 #$files->{$file}->{ctime} = time() unless ($files->{$file}->{ctime});
460 print "file '$file' content [",length($files->{$file}->{cont})," bytes] read in cache\n";
465 # VFS sanity check; it keeps all the necessary state, not much to do here.
466 my $file = filename_fixup(shift);
469 return -ENOENT() unless exists($files->{$file});
470 return -EISDIR() unless exists($files->{$file}->{id});
472 read_content($file,$files->{$file}->{id}) unless exists($files->{$file}->{cont});
474 $files->{$file}->{cont} ||= '';
475 print "open '$file' ",length($files->{$file}->{cont})," bytes\n";
480 # return an error numeric, or binary/text string.
481 # (note: 0 means EOF, "0" will give a byte (ascii "0")
482 # to the reading program)
483 my ($file) = filename_fixup(shift);
484 my ($buf_len,$off) = @_;
486 return -ENOENT() unless exists($files->{$file});
488 my $len = length($files->{$file}->{cont});
490 print "read '$file' [$len bytes] offset $off length $buf_len\n";
492 return -EINVAL() if ($off > $len);
493 return 0 if ($off == $len);
495 $buf_len = $len-$off if ($len - $off < $buf_len);
497 return substr($files->{$file}->{cont},$off,$buf_len);
501 print "transaction rollback\n";
502 $dbh->rollback || die $dbh->errstr;
503 print "invalidate all cached content\n";
504 foreach my $f (keys %{$files}) {
505 delete $files->{$f}->{cont};
506 delete $files->{$f}->{ctime};
508 print "begin new transaction\n";
509 #$dbh->begin_work || die $dbh->errstr;
514 my $file = shift || die "need file";
516 $files->{$file}->{ctime} = time();
519 $files->{$file}->{cont},
520 $files->{$file}->{id}
523 my @args = $fuse_self->{'update_ref'}->($files->{$file});
525 my $sql = shift @args || die "need SQL for $file";
527 unshift @args, $files->{$file}->{cont} if ($#args == 0);
529 warn "## SQL: $sql\n# files->{$file} = ", Dumper($files->{$file}), $/ if ($debug);
531 my $sth = $fuse_self->{'update_sth'}->{$sql}
532 ||= $fuse_self->{dbh}->prepare($sql)
533 || die $dbh->errstr();
535 if (!$sth->execute(@args)) {
536 print "update problem: ",$sth->errstr;
540 if (! $dbh->commit) {
541 print "ERROR: commit problem: ",$sth->errstr;
545 print "updated '$file' [",$files->{$file}->{id},"]\n";
547 $fuse_self->{'invalidate'}->() if ($fuse_self->can('invalidate'));
553 my $file = filename_fixup(shift);
554 my ($buffer,$off) = @_;
556 return -ENOENT() unless exists($files->{$file});
558 my $cont = $files->{$file}->{cont};
559 my $len = length($cont);
561 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
563 $files->{$file}->{cont} = "";
565 $files->{$file}->{cont} .= substr($cont,0,$off) if ($off > 0);
566 $files->{$file}->{cont} .= $buffer;
567 $files->{$file}->{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
569 $files->{$file}->{size} = length($files->{$file}->{cont});
571 if (! update_db($file)) {
574 return length($buffer);
579 my $file = filename_fixup(shift);
582 print "truncate to $size\n";
584 $files->{$file}->{cont} = substr($files->{$file}->{cont},0,$size);
585 $files->{$file}->{size} = $size;
591 my ($atime,$mtime,$file) = @_;
592 $file = filename_fixup($file);
594 return -ENOENT() unless exists($files->{$file});
596 print "utime '$file' $atime $mtime\n";
598 $files->{$file}->{time} = $mtime;
607 foreach my $f (keys %{$files}) {
608 if ($f !~ /(^|\/)\.\.?$/) {
609 $size += $files->{$f}->{size} || 0;
612 print "$inodes: $f [$size]\n";
615 $size = int(($size+BLOCK-1)/BLOCK);
617 my @ret = (255, $inodes, 1, $size, $size-1, BLOCK);
619 #print "statfs: ",join(",",@ret),"\n";
625 my $file = filename_fixup(shift);
627 # if (exists( $dirs{$file} )) {
628 # print "unlink '$file' will re-read template names\n";
629 # print Dumper($fuse_self);
630 # $fuse_self->{'read_filenames'}->();
632 if (exists( $files->{$file} )) {
633 print "unlink '$file' will invalidate cache\n";
634 read_content($file,$files->{$file}->{id});
649 Size information (C<ls -s>) is wrong. It's a problem in upstream Fuse module
650 (for which I'm to blame lately), so when it gets fixes, C<Fuse::DBI> will
651 automagically pick it up.
655 C<FUSE (Filesystem in USErspace)> website
656 L<http://fuse.sourceforge.net/>
658 Example for WebGUI which comes with this distribution in
659 directory C<examples/webgui.pl>. It also contains a lot of documentation
660 about design of this module, usage and limitations.
664 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
666 =head1 COPYRIGHT AND LICENSE
668 Copyright (C) 2004 by Dobrica Pavlinusic
670 This library is free software; you can redistribute it and/or modify
671 it under the same terms as Perl itself, either Perl version 5.8.4 or,
672 at your option, any later version of Perl 5 you may have available.