9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
15 our $VERSION = '0.08';
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.
121 sub fuse_module_loaded;
123 # evil, evil way to solve this. It makes this module non-reentrant. But, since
124 # fuse calls another copy of this script for each mount anyway, this shouldn't
131 bless($self, $class);
137 unless ($self->fuse_module_loaded) {
138 print STDERR "no fuse module loaded. Trying sudo modprobe fuse!\n";
139 system "sudo modprobe fuse" || die "can't modprobe fuse using sudo!\n";
142 carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
143 carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
145 # save (some) arguments in self
146 foreach (qw(mount invalidate)) {
147 $self->{$_} = $arg->{$_};
150 foreach (qw(filenames read update)) {
151 carp "mount needs '$_' SQL" unless ($arg->{$_});
154 $ctime_start = time();
157 if ($arg->{'fork'}) {
159 die "fork() failed: $!" unless defined $pid;
160 # child will return to caller
163 while ($counter && ! $self->is_mounted) {
164 select(undef, undef, undef, 0.5);
167 if ($self->is_mounted) {
175 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
177 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
179 $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
180 $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
183 $self->{'sth'} = $sth;
185 $self->{'read_filenames'} = sub { $self->read_filenames };
186 $self->read_filenames;
191 mountpoint=>$arg->{'mount'},
192 getattr=>\&e_getattr,
199 truncate=>\&e_truncate,
205 exit(0) if ($arg->{'fork'});
213 Check if fuse filesystem is mounted
215 if ($mnt->is_mounted) { ... }
223 my $mount = $self->{'mount'} || confess "can't find mount point!";
224 if (open(MTAB, "/etc/mtab")) {
226 $mounted = 1 if (/ $mount fuse /i);
230 warn "can't open /etc/mtab: $!";
239 Unmount your database as filesystem.
243 This will also kill background process which is translating
244 database to filesystem.
251 if ($self->{'mount'} && $self->is_mounted) {
252 system "( fusermount -u ".$self->{'mount'}." 2>&1 ) >/dev/null";
253 if ($self->is_mounted) {
254 system "sudo umount ".$self->{'mount'} ||
264 if ($fuse_self && $$fuse_self->umount) {
265 print STDERR "umount called by SIG INT\n";
270 if ($fuse_self && $$fuse_self->umount) {
271 print STDERR "umount called by SIG QUIT\n";
278 print STDERR "umount called by DESTROY\n";
282 =head2 fuse_module_loaded
284 Checks if C<fuse> module is loaded in kernel.
286 die "no fuse module loaded in kernel"
287 unless (Fuse::DBI::fuse_module_loaded);
289 This function in called by C<mount>, but might be useful alone also.
293 sub fuse_module_loaded {
295 die "can't start lsmod: $!" unless ($lsmod);
296 if ($lsmod =~ m/fuse/s) {
308 my $sth = $self->{'sth'} || die "no sth argument";
310 # create empty filesystem
321 # cont => "File 'a'.\n",
323 # ctime => time()-2000
327 # fetch new filename list from database
328 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
330 # read them in with sesible defaults
331 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
332 $row->{'filename'} ||= 'NULL-'.$row->{'id'};
333 $files{$row->{'filename'}} = {
334 size => $row->{'size'},
335 mode => $row->{'writable'} ? 0644 : 0444,
336 id => $row->{'id'} || 99,
341 foreach (split(m!/!, $row->{'filename'})) {
342 # first, entry is assumed to be file
362 print "found ",scalar(keys %files)," files\n";
369 $file = '.' unless length($file);
374 my ($file) = filename_fixup(shift);
376 $file = '.' unless length($file);
377 return -ENOENT() unless exists($files{$file});
378 my ($size) = $files{$file}{size} || 0;
379 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,int(($size+BLOCK-1)/BLOCK),0,0,1,BLOCK);
380 my ($atime, $ctime, $mtime);
381 $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
383 my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
385 # 2 possible types of return values:
386 #return -ENOENT(); # or any other error you care to
387 #print "getattr($file) ",join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n";
388 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
392 my ($dirname) = shift;
394 # return as many text filenames as you like, followed by the retval.
395 print((scalar keys %files)." files total\n");
397 foreach my $f (sort keys %files) {
399 if ($f =~ s/^\Q$dirname\E\///) {
400 $out{$f}++ if ($f =~ /^[^\/]+$/);
403 $out{$f}++ if ($f =~ /^[^\/]+$/);
407 $out{'no files? bug?'}++;
409 print scalar keys %out," files in dir '$dirname'\n";
410 print "## ",join(" ",keys %out),"\n";
411 return (keys %out),0;
417 die "read_content needs file and id" unless ($file && $id);
419 $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
420 $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
421 # I should modify ctime only if content in database changed
422 #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
423 print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
428 # VFS sanity check; it keeps all the necessary state, not much to do here.
429 my $file = filename_fixup(shift);
432 return -ENOENT() unless exists($files{$file});
433 return -EISDIR() unless exists($files{$file}{id});
435 read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
437 $files{$file}{cont} ||= '';
438 print "open '$file' ",length($files{$file}{cont})," bytes\n";
443 # return an error numeric, or binary/text string.
444 # (note: 0 means EOF, "0" will give a byte (ascii "0")
445 # to the reading program)
446 my ($file) = filename_fixup(shift);
447 my ($buf_len,$off) = @_;
449 return -ENOENT() unless exists($files{$file});
451 my $len = length($files{$file}{cont});
453 print "read '$file' [$len bytes] offset $off length $buf_len\n";
455 return -EINVAL() if ($off > $len);
456 return 0 if ($off == $len);
458 $buf_len = $len-$off if ($len - $off < $buf_len);
460 return substr($files{$file}{cont},$off,$buf_len);
464 print "transaction rollback\n";
465 $dbh->rollback || die $dbh->errstr;
466 print "invalidate all cached content\n";
467 foreach my $f (keys %files) {
468 delete $files{$f}{cont};
469 delete $files{$f}{ctime};
471 print "begin new transaction\n";
472 #$dbh->begin_work || die $dbh->errstr;
477 my $file = shift || die;
479 $files{$file}{ctime} = time();
486 if (!$sth->{'update'}->execute($cont,$id)) {
487 print "update problem: ",$sth->{'update'}->errstr;
491 if (! $dbh->commit) {
492 print "ERROR: commit problem: ",$sth->{'update'}->errstr;
496 print "updated '$file' [",$files{$file}{id},"]\n";
498 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
504 my $file = filename_fixup(shift);
505 my ($buffer,$off) = @_;
507 return -ENOENT() unless exists($files{$file});
509 my $cont = $files{$file}{cont};
510 my $len = length($cont);
512 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
514 $files{$file}{cont} = "";
516 $files{$file}{cont} .= substr($cont,0,$off) if ($off > 0);
517 $files{$file}{cont} .= $buffer;
518 $files{$file}{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
520 $files{$file}{size} = length($files{$file}{cont});
522 if (! update_db($file)) {
525 return length($buffer);
530 my $file = filename_fixup(shift);
533 print "truncate to $size\n";
535 $files{$file}{cont} = substr($files{$file}{cont},0,$size);
536 $files{$file}{size} = $size;
542 my ($atime,$mtime,$file) = @_;
543 $file = filename_fixup($file);
545 return -ENOENT() unless exists($files{$file});
547 print "utime '$file' $atime $mtime\n";
549 $files{$file}{time} = $mtime;
558 foreach my $f (keys %files) {
559 if ($f !~ /(^|\/)\.\.?$/) {
560 $size += $files{$f}{size} || 0;
563 print "$inodes: $f [$size]\n";
566 $size = int(($size+BLOCK-1)/BLOCK);
568 my @ret = (255, $inodes, 1, $size, $size-1, BLOCK);
570 #print "statfs: ",join(",",@ret),"\n";
576 my $file = filename_fixup(shift);
578 # if (exists( $dirs{$file} )) {
579 # print "unlink '$file' will re-read template names\n";
580 # print Dumper($fuse_self);
581 # $$fuse_self->{'read_filenames'}->();
583 if (exists( $files{$file} )) {
584 print "unlink '$file' will invalidate cache\n";
585 read_content($file,$files{$file}{id});
600 Size information (C<ls -s>) is wrong. It's a problem in upstream Fuse module
601 (for which I'm to blame lately), so when it gets fixes, C<Fuse::DBI> will
602 automagically pick it up.
606 C<FUSE (Filesystem in USErspace)> website
607 L<http://fuse.sourceforge.net/>
609 Example for WebGUI which comes with this distribution in
610 directory C<examples/webgui.pl>. It also contains a lot of documentation
611 about design of this module, usage and limitations.
615 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
617 =head1 COPYRIGHT AND LICENSE
619 Copyright (C) 2004 by Dobrica Pavlinusic
621 This library is free software; you can redistribute it and/or modify
622 it under the same terms as Perl itself, either Perl version 5.8.4 or,
623 at your option, any later version of Perl 5 you may have available.