9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
16 our $VERSION = '0.04';
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://sourceforge.net/projects/avf> 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
159 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
161 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
163 $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
164 $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
167 $self->{'sth'} = $sth;
169 $self->{'read_filenames'} = sub { $self->read_filenames };
170 $self->read_filenames;
172 $self->{'mounted'} = 1;
177 mountpoint=>$arg->{'mount'},
178 getattr=>\&e_getattr,
185 truncate=>\&e_truncate,
191 $self->{'mounted'} = 0;
193 exit(0) if ($arg->{'fork'});
201 Unmount your database as filesystem.
205 This will also kill background process which is translating
206 database to filesystem.
213 if ($self->{'mounted'}) {
214 system "fusermount -u ".$self->{'mount'} || croak "umount error: $!";
221 print STDERR "umount called by SIG INT\n";
227 return if (! $self->{'mounted'});
228 print STDERR "umount called by DESTROY\n";
232 =head2 fuse_module_loaded
234 Checks if C<fuse> module is loaded in kernel.
236 die "no fuse module loaded in kernel"
237 unless (Fuse::DBI::fuse_module_loaded);
239 This function in called by C<mount>, but might be useful alone also.
243 sub fuse_module_loaded {
245 die "can't start lsmod: $!" unless ($lsmod);
246 if ($lsmod =~ m/fuse/s) {
259 my $sth = $self->{'sth'} || die "no sth argument";
261 # create empty filesystem
268 # cont => "File 'a'.\n",
270 # ctime => time()-2000
274 # fetch new filename list from database
275 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
277 # read them in with sesible defaults
278 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
279 $files{$row->{'filename'}} = {
280 size => $row->{'size'},
281 mode => $row->{'writable'} ? 0644 : 0444,
282 id => $row->{'id'} || 99,
286 foreach (split(m!/!, $row->{'filename'})) {
287 # first, entry is assumed to be file
308 print "found ",scalar(keys %files)-scalar(keys %dirs)," files, ",scalar(keys %dirs), " dirs\n";
315 $file = '.' unless length($file);
320 my ($file) = filename_fixup(shift);
322 $file = '.' unless length($file);
323 return -ENOENT() unless exists($files{$file});
324 my ($size) = $files{$file}{size} || 1;
325 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,0,0,1,1024);
326 my ($atime, $ctime, $mtime);
327 $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
329 my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
331 # 2 possible types of return values:
332 #return -ENOENT(); # or any other error you care to
333 #print(join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n");
334 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
338 my ($dirname) = shift;
340 # return as many text filenames as you like, followed by the retval.
341 print((scalar keys %files)." files total\n");
343 foreach my $f (sort keys %files) {
345 if ($f =~ s/^\Q$dirname\E\///) {
346 $out{$f}++ if ($f =~ /^[^\/]+$/);
349 $out{$f}++ if ($f =~ /^[^\/]+$/);
353 $out{'no files? bug?'}++;
355 print scalar keys %out," files in dir '$dirname'\n";
356 print "## ",join(" ",keys %out),"\n";
357 return (keys %out),0;
363 die "read_content needs file and id" unless ($file && $id);
365 $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
366 $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
367 # I should modify ctime only if content in database changed
368 #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
369 print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
374 # VFS sanity check; it keeps all the necessary state, not much to do here.
375 my $file = filename_fixup(shift);
378 return -ENOENT() unless exists($files{$file});
379 return -EISDIR() unless exists($files{$file}{id});
381 read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
383 print "open '$file' ",length($files{$file}{cont})," bytes\n";
388 # return an error numeric, or binary/text string.
389 # (note: 0 means EOF, "0" will give a byte (ascii "0")
390 # to the reading program)
391 my ($file) = filename_fixup(shift);
392 my ($buf_len,$off) = @_;
394 return -ENOENT() unless exists($files{$file});
396 my $len = length($files{$file}{cont});
398 print "read '$file' [$len bytes] offset $off length $buf_len\n";
400 return -EINVAL() if ($off > $len);
401 return 0 if ($off == $len);
403 $buf_len = $len-$off if ($len - $off < $buf_len);
405 return substr($files{$file}{cont},$off,$buf_len);
409 print "transaction rollback\n";
410 $dbh->rollback || die $dbh->errstr;
411 print "invalidate all cached content\n";
412 foreach my $f (keys %files) {
413 delete $files{$f}{cont};
414 delete $files{$f}{ctime};
416 print "begin new transaction\n";
417 #$dbh->begin_work || die $dbh->errstr;
422 my $file = shift || die;
424 $files{$file}{ctime} = time();
431 if (!$sth->{'update'}->execute($cont,$id)) {
432 print "update problem: ",$sth->{'update'}->errstr;
436 if (! $dbh->commit) {
437 print "ERROR: commit problem: ",$sth->{'update'}->errstr;
441 print "updated '$file' [",$files{$file}{id},"]\n";
443 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
449 my $file = filename_fixup(shift);
450 my ($buffer,$off) = @_;
452 return -ENOENT() unless exists($files{$file});
454 my $cont = $files{$file}{cont};
455 my $len = length($cont);
457 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
459 $files{$file}{cont} = "";
461 $files{$file}{cont} .= substr($cont,0,$off) if ($off > 0);
462 $files{$file}{cont} .= $buffer;
463 $files{$file}{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
465 $files{$file}{size} = length($files{$file}{cont});
467 if (! update_db($file)) {
470 return length($buffer);
475 my $file = filename_fixup(shift);
478 print "truncate to $size\n";
480 $files{$file}{cont} = substr($files{$file}{cont},0,$size);
481 $files{$file}{size} = $size;
487 my ($atime,$mtime,$file) = @_;
488 $file = filename_fixup($file);
490 return -ENOENT() unless exists($files{$file});
492 print "utime '$file' $atime $mtime\n";
494 $files{$file}{time} = $mtime;
498 sub e_statfs { return 255, 1, 1, 1, 1, 2 }
501 my $file = filename_fixup(shift);
503 if (exists( $dirs{$file} )) {
504 print "unlink '$file' will re-read template names\n";
505 print Dumper($fuse_self);
506 $$fuse_self->{'read_filenames'}->();
508 } elsif (exists( $files{$file} )) {
509 print "unlink '$file' will invalidate cache\n";
510 read_content($file,$files{$file}{id});
525 C<FUSE (Filesystem in USErspace)> website
526 L<http://sourceforge.net/projects/avf>
528 Example for WebGUI which comes with this distribution in
529 directory C<examples/webgui.pl>. It also contains a lot of documentation
530 about design of this module, usage and limitations.
534 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
536 =head1 COPYRIGHT AND LICENSE
538 Copyright (C) 2004 by Dobrica Pavlinusic
540 This library is free software; you can redistribute it and/or modify
541 it under the same terms as Perl itself, either Perl version 5.8.4 or,
542 at your option, any later version of Perl 5 you may have available.