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 carp "mount needs 'dsn' to connect to (e.g. dsn => 'DBI:Pg:dbname=test')" unless ($arg->{'dsn'});
136 carp "mount needs 'mount' as mountpoint" unless ($arg->{'mount'});
138 # save (some) arguments in self
139 foreach (qw(mount invalidate)) {
140 $self->{$_} = $arg->{$_};
143 foreach (qw(filenames read update)) {
144 carp "mount needs '$_' SQL" unless ($arg->{$_});
147 $ctime_start = time();
150 if ($arg->{'fork'}) {
152 die "fork() failed: $!" unless defined $pid;
153 # child will return to caller
156 while ($counter && ! $self->is_mounted) {
157 select(undef, undef, undef, 0.5);
160 if ($self->is_mounted) {
168 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
170 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
172 $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
173 $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
176 $self->{'sth'} = $sth;
178 $self->{'read_filenames'} = sub { $self->read_filenames };
179 $self->read_filenames;
184 mountpoint=>$arg->{'mount'},
185 getattr=>\&e_getattr,
192 truncate=>\&e_truncate,
198 exit(0) if ($arg->{'fork'});
206 Check if fuse filesystem is mounted
208 if ($mnt->is_mounted) { ... }
216 my $mount = $self->{'mount'} || confess "can't find mount point!";
217 if (open(MTAB, "/etc/mtab")) {
219 $mounted = 1 if (/ $mount fuse /i);
223 warn "can't open /etc/mtab: $!";
232 Unmount your database as filesystem.
236 This will also kill background process which is translating
237 database to filesystem.
244 if ($self->{'mount'} && $self->is_mounted) {
245 system "fusermount -u ".$self->{'mount'}." 2>&1 >/dev/null" || return 0;
253 if ($fuse_self && $$fuse_self->umount) {
254 print STDERR "umount called by SIG INT\n";
259 if ($fuse_self && $$fuse_self->umount) {
260 print STDERR "umount called by SIG QUIT\n";
267 print STDERR "umount called by DESTROY\n";
271 =head2 fuse_module_loaded
273 Checks if C<fuse> module is loaded in kernel.
275 die "no fuse module loaded in kernel"
276 unless (Fuse::DBI::fuse_module_loaded);
278 This function in called by C<mount>, but might be useful alone also.
282 sub fuse_module_loaded {
284 die "can't start lsmod: $!" unless ($lsmod);
285 if ($lsmod =~ m/fuse/s) {
298 my $sth = $self->{'sth'} || die "no sth argument";
300 # create empty filesystem
311 # cont => "File 'a'.\n",
313 # ctime => time()-2000
317 # fetch new filename list from database
318 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
320 # read them in with sesible defaults
321 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
322 $files{$row->{'filename'}} = {
323 size => $row->{'size'},
324 mode => $row->{'writable'} ? 0644 : 0444,
325 id => $row->{'id'} || 99,
329 foreach (split(m!/!, $row->{'filename'})) {
330 # first, entry is assumed to be file
351 print "found ",scalar(keys %files)-scalar(keys %dirs)," files, ",scalar(keys %dirs), " dirs\n";
358 $file = '.' unless length($file);
363 my ($file) = filename_fixup(shift);
365 $file = '.' unless length($file);
366 return -ENOENT() unless exists($files{$file});
367 my ($size) = $files{$file}{size} || 1;
368 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,0,0,1,1024);
369 my ($atime, $ctime, $mtime);
370 $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
372 my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
374 # 2 possible types of return values:
375 #return -ENOENT(); # or any other error you care to
376 #print(join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n");
377 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
381 my ($dirname) = shift;
383 # return as many text filenames as you like, followed by the retval.
384 print((scalar keys %files)." files total\n");
386 foreach my $f (sort keys %files) {
388 if ($f =~ s/^\Q$dirname\E\///) {
389 $out{$f}++ if ($f =~ /^[^\/]+$/);
392 $out{$f}++ if ($f =~ /^[^\/]+$/);
396 $out{'no files? bug?'}++;
398 print scalar keys %out," files in dir '$dirname'\n";
399 print "## ",join(" ",keys %out),"\n";
400 return (keys %out),0;
406 die "read_content needs file and id" unless ($file && $id);
408 $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
409 $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
410 # I should modify ctime only if content in database changed
411 #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
412 print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
417 # VFS sanity check; it keeps all the necessary state, not much to do here.
418 my $file = filename_fixup(shift);
421 return -ENOENT() unless exists($files{$file});
422 return -EISDIR() unless exists($files{$file}{id});
424 read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
426 print "open '$file' ",length($files{$file}{cont})," bytes\n";
431 # return an error numeric, or binary/text string.
432 # (note: 0 means EOF, "0" will give a byte (ascii "0")
433 # to the reading program)
434 my ($file) = filename_fixup(shift);
435 my ($buf_len,$off) = @_;
437 return -ENOENT() unless exists($files{$file});
439 my $len = length($files{$file}{cont});
441 print "read '$file' [$len bytes] offset $off length $buf_len\n";
443 return -EINVAL() if ($off > $len);
444 return 0 if ($off == $len);
446 $buf_len = $len-$off if ($len - $off < $buf_len);
448 return substr($files{$file}{cont},$off,$buf_len);
452 print "transaction rollback\n";
453 $dbh->rollback || die $dbh->errstr;
454 print "invalidate all cached content\n";
455 foreach my $f (keys %files) {
456 delete $files{$f}{cont};
457 delete $files{$f}{ctime};
459 print "begin new transaction\n";
460 #$dbh->begin_work || die $dbh->errstr;
465 my $file = shift || die;
467 $files{$file}{ctime} = time();
474 if (!$sth->{'update'}->execute($cont,$id)) {
475 print "update problem: ",$sth->{'update'}->errstr;
479 if (! $dbh->commit) {
480 print "ERROR: commit problem: ",$sth->{'update'}->errstr;
484 print "updated '$file' [",$files{$file}{id},"]\n";
486 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
492 my $file = filename_fixup(shift);
493 my ($buffer,$off) = @_;
495 return -ENOENT() unless exists($files{$file});
497 my $cont = $files{$file}{cont};
498 my $len = length($cont);
500 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
502 $files{$file}{cont} = "";
504 $files{$file}{cont} .= substr($cont,0,$off) if ($off > 0);
505 $files{$file}{cont} .= $buffer;
506 $files{$file}{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
508 $files{$file}{size} = length($files{$file}{cont});
510 if (! update_db($file)) {
513 return length($buffer);
518 my $file = filename_fixup(shift);
521 print "truncate to $size\n";
523 $files{$file}{cont} = substr($files{$file}{cont},0,$size);
524 $files{$file}{size} = $size;
530 my ($atime,$mtime,$file) = @_;
531 $file = filename_fixup($file);
533 return -ENOENT() unless exists($files{$file});
535 print "utime '$file' $atime $mtime\n";
537 $files{$file}{time} = $mtime;
541 sub e_statfs { return 255, 1, 1, 1, 1, 2 }
544 my $file = filename_fixup(shift);
546 if (exists( $dirs{$file} )) {
547 print "unlink '$file' will re-read template names\n";
548 print Dumper($fuse_self);
549 $$fuse_self->{'read_filenames'}->();
551 } elsif (exists( $files{$file} )) {
552 print "unlink '$file' will invalidate cache\n";
553 read_content($file,$files{$file}{id});
568 Size information (C<ls -s>) is wrong. It's a problem in upstream Fuse module
569 (for which I'm to blame lately), so when it gets fixes, C<Fuse::DBI> will
570 automagically pick it up.
574 C<FUSE (Filesystem in USErspace)> website
575 L<http://fuse.sourceforge.net/>
577 Example for WebGUI which comes with this distribution in
578 directory C<examples/webgui.pl>. It also contains a lot of documentation
579 about design of this module, usage and limitations.
583 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
585 =head1 COPYRIGHT AND LICENSE
587 Copyright (C) 2004 by Dobrica Pavlinusic
589 This library is free software; you can redistribute it and/or modify
590 it under the same terms as Perl itself, either Perl version 5.8.4 or,
591 at your option, any later version of Perl 5 you may have available.