9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
16 our $VERSION = '0.07';
20 Fuse::DBI - mount your database as filesystem and use it
25 Fuse::DBI->mount( ... );
27 See C<run> below for examples how to set parameters.
31 This module will use C<Fuse> module, part of C<FUSE (Filesystem in USErspace)>
32 available at L<http://fuse.sourceforge.net/> to mount
33 your database as file system.
35 That will give you possibility to use normal file-system tools (cat, grep, vi)
36 to manipulate data in database.
38 It's actually opposite of Oracle's intention to put everything into database.
47 Mount your database as filesystem.
49 Let's suppose that your database have table C<files> with following structure:
57 Following is example how to mount table like that to C</mnt>:
59 my $mnt = Fuse::DBI->mount({
60 'filenames' => 'select id,filename,size,writable from files',
61 'read' => 'select content from files where id = ?',
62 'update' => 'update files set content = ? where id = ?',
63 'dsn' => 'DBI:Pg:dbname=test_db',
64 'user' => 'database_user',
65 'password' => 'database_password',
66 'invalidate' => sub { ... },
75 SQL query which returns C<id> (unique id for that row), C<filename>,
76 C<size> and C<writable> boolean flag.
80 SQL query which returns only one column with content of file and has
81 placeholder C<?> for C<id>.
85 SQL query with two pace-holders, one for new content and one for C<id>.
89 C<DBI> dsn to connect to (contains database driver and name of database).
93 User with which to connect to database
97 Password for connecting to database
101 Optional anonymous code reference which will be executed when data is updated in
102 database. It can be used as hook to delete cache (for example on-disk-cache)
103 which is created from data edited through C<Fuse::DBI>.
107 Optional flag which forks after mount so that executing script will continue
108 running. Implementation is experimental.
119 sub fuse_module_loaded;
121 # evil, evil way to solve this. It makes this module non-reentrant. But, since
122 # fuse calls another copy of this script for each mount anyway, this shouldn't
129 bless($self, $class);
135 unless ($self->fuse_module_loaded) {
136 print STDERR "no fuse module loaded. Trying sudo modprobe fuse!\n";
137 system "sudo modprobe fuse" || die "can't modprobe fuse using sudo!\n";
140 carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
141 carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
143 # save (some) arguments in self
144 foreach (qw(mount invalidate)) {
145 $self->{$_} = $arg->{$_};
148 foreach (qw(filenames read update)) {
149 carp "mount needs '$_' SQL" unless ($arg->{$_});
152 $ctime_start = time();
155 if ($arg->{'fork'}) {
157 die "fork() failed: $!" unless defined $pid;
158 # child will return to caller
161 while ($counter && ! $self->is_mounted) {
162 select(undef, undef, undef, 0.5);
165 if ($self->is_mounted) {
173 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
175 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
177 $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
178 $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
181 $self->{'sth'} = $sth;
183 $self->{'read_filenames'} = sub { $self->read_filenames };
184 $self->read_filenames;
189 mountpoint=>$arg->{'mount'},
190 getattr=>\&e_getattr,
197 truncate=>\&e_truncate,
203 exit(0) if ($arg->{'fork'});
211 Check if fuse filesystem is mounted
213 if ($mnt->is_mounted) { ... }
221 my $mount = $self->{'mount'} || confess "can't find mount point!";
222 if (open(MTAB, "/etc/mtab")) {
224 $mounted = 1 if (/ $mount fuse /i);
228 warn "can't open /etc/mtab: $!";
237 Unmount your database as filesystem.
241 This will also kill background process which is translating
242 database to filesystem.
249 if ($self->{'mount'} && $self->is_mounted) {
250 system "fusermount -u ".$self->{'mount'}." 2>&1 >/dev/null" ||
251 system "sudo umount ".$self->{'mount'} ||
260 if ($fuse_self && $$fuse_self->umount) {
261 print STDERR "umount called by SIG INT\n";
266 if ($fuse_self && $$fuse_self->umount) {
267 print STDERR "umount called by SIG QUIT\n";
274 print STDERR "umount called by DESTROY\n";
278 =head2 fuse_module_loaded
280 Checks if C<fuse> module is loaded in kernel.
282 die "no fuse module loaded in kernel"
283 unless (Fuse::DBI::fuse_module_loaded);
285 This function in called by C<mount>, but might be useful alone also.
289 sub fuse_module_loaded {
291 die "can't start lsmod: $!" unless ($lsmod);
292 if ($lsmod =~ m/fuse/s) {
304 my $sth = $self->{'sth'} || die "no sth argument";
306 # create empty filesystem
317 # cont => "File 'a'.\n",
319 # ctime => time()-2000
323 # fetch new filename list from database
324 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
326 # read them in with sesible defaults
327 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
328 $files{$row->{'filename'}} = {
329 size => $row->{'size'},
330 mode => $row->{'writable'} ? 0644 : 0444,
331 id => $row->{'id'} || 99,
335 foreach (split(m!/!, $row->{'filename'})) {
336 # first, entry is assumed to be file
356 print "found ",scalar(keys %files)," files\n";
363 $file = '.' unless length($file);
368 my ($file) = filename_fixup(shift);
370 $file = '.' unless length($file);
371 return -ENOENT() unless exists($files{$file});
372 my ($size) = $files{$file}{size} || 1024;
373 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,int(($size+1023)/1024),0,0,1,1024);
374 my ($atime, $ctime, $mtime);
375 $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
377 my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
379 # 2 possible types of return values:
380 #return -ENOENT(); # or any other error you care to
381 print "getattr($file) ",join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n";
382 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
386 my ($dirname) = shift;
388 # return as many text filenames as you like, followed by the retval.
389 print((scalar keys %files)." files total\n");
391 foreach my $f (sort keys %files) {
393 if ($f =~ s/^\Q$dirname\E\///) {
394 $out{$f}++ if ($f =~ /^[^\/]+$/);
397 $out{$f}++ if ($f =~ /^[^\/]+$/);
401 $out{'no files? bug?'}++;
403 print scalar keys %out," files in dir '$dirname'\n";
404 print "## ",join(" ",keys %out),"\n";
405 return (keys %out),0;
411 die "read_content needs file and id" unless ($file && $id);
413 $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
414 $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
415 # I should modify ctime only if content in database changed
416 #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
417 print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
422 # VFS sanity check; it keeps all the necessary state, not much to do here.
423 my $file = filename_fixup(shift);
426 return -ENOENT() unless exists($files{$file});
427 return -EISDIR() unless exists($files{$file}{id});
429 read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
431 print "open '$file' ",length($files{$file}{cont})," bytes\n";
436 # return an error numeric, or binary/text string.
437 # (note: 0 means EOF, "0" will give a byte (ascii "0")
438 # to the reading program)
439 my ($file) = filename_fixup(shift);
440 my ($buf_len,$off) = @_;
442 return -ENOENT() unless exists($files{$file});
444 my $len = length($files{$file}{cont});
446 print "read '$file' [$len bytes] offset $off length $buf_len\n";
448 return -EINVAL() if ($off > $len);
449 return 0 if ($off == $len);
451 $buf_len = $len-$off if ($len - $off < $buf_len);
453 return substr($files{$file}{cont},$off,$buf_len);
457 print "transaction rollback\n";
458 $dbh->rollback || die $dbh->errstr;
459 print "invalidate all cached content\n";
460 foreach my $f (keys %files) {
461 delete $files{$f}{cont};
462 delete $files{$f}{ctime};
464 print "begin new transaction\n";
465 #$dbh->begin_work || die $dbh->errstr;
470 my $file = shift || die;
472 $files{$file}{ctime} = time();
479 if (!$sth->{'update'}->execute($cont,$id)) {
480 print "update problem: ",$sth->{'update'}->errstr;
484 if (! $dbh->commit) {
485 print "ERROR: commit problem: ",$sth->{'update'}->errstr;
489 print "updated '$file' [",$files{$file}{id},"]\n";
491 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
497 my $file = filename_fixup(shift);
498 my ($buffer,$off) = @_;
500 return -ENOENT() unless exists($files{$file});
502 my $cont = $files{$file}{cont};
503 my $len = length($cont);
505 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
507 $files{$file}{cont} = "";
509 $files{$file}{cont} .= substr($cont,0,$off) if ($off > 0);
510 $files{$file}{cont} .= $buffer;
511 $files{$file}{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
513 $files{$file}{size} = length($files{$file}{cont});
515 if (! update_db($file)) {
518 return length($buffer);
523 my $file = filename_fixup(shift);
526 print "truncate to $size\n";
528 $files{$file}{cont} = substr($files{$file}{cont},0,$size);
529 $files{$file}{size} = $size;
535 my ($atime,$mtime,$file) = @_;
536 $file = filename_fixup($file);
538 return -ENOENT() unless exists($files{$file});
540 print "utime '$file' $atime $mtime\n";
542 $files{$file}{time} = $mtime;
551 foreach my $f (keys %files) {
552 if ($f !~ /(^|\/)\.\.?$/) {
553 $size += $files{$f}{size} || 0;
556 print "$inodes: $f [$size]\n";
559 $size = int(($size+1023)/1024);
561 my @ret = (255, $inodes+1000, $inodes, $size, $size-10, 1024);
563 print "statfs: ",join(",",@ret),"\n";
569 my $file = filename_fixup(shift);
571 # if (exists( $dirs{$file} )) {
572 # print "unlink '$file' will re-read template names\n";
573 # print Dumper($fuse_self);
574 # $$fuse_self->{'read_filenames'}->();
576 if (exists( $files{$file} )) {
577 print "unlink '$file' will invalidate cache\n";
578 read_content($file,$files{$file}{id});
593 Size information (C<ls -s>) is wrong. It's a problem in upstream Fuse module
594 (for which I'm to blame lately), so when it gets fixes, C<Fuse::DBI> will
595 automagically pick it up.
599 C<FUSE (Filesystem in USErspace)> website
600 L<http://fuse.sourceforge.net/>
602 Example for WebGUI which comes with this distribution in
603 directory C<examples/webgui.pl>. It also contains a lot of documentation
604 about design of this module, usage and limitations.
608 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
610 =head1 COPYRIGHT AND LICENSE
612 Copyright (C) 2004 by Dobrica Pavlinusic
614 This library is free software; you can redistribute it and/or modify
615 it under the same terms as Perl itself, either Perl version 5.8.4 or,
616 at your option, any later version of Perl 5 you may have available.