9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
16 our $VERSION = '0.05';
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'}) {
151 $self->{'mounted'} = 1;
153 die "fork() failed: $!" unless defined $pid;
154 # child will return to caller
160 $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
162 $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
164 $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
165 $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
168 $self->{'sth'} = $sth;
170 $self->{'read_filenames'} = sub { $self->read_filenames };
171 $self->read_filenames;
173 $self->{'mounted'} = 1 unless ($arg->{'fork'});
178 mountpoint=>$arg->{'mount'},
179 getattr=>\&e_getattr,
186 truncate=>\&e_truncate,
192 $self->{'mounted'} = 0;
194 exit(0) if ($arg->{'fork'});
202 Unmount your database as filesystem.
206 This will also kill background process which is translating
207 database to filesystem.
214 if ($self->{'mounted'}) {
215 system "fusermount -u ".$self->{'mount'} || warn "umount error: $!" && return 0;
222 print STDERR "umount called by SIG INT\n";
228 return if (! $self->{'mounted'});
229 print STDERR "umount called by DESTROY\n";
233 =head2 fuse_module_loaded
235 Checks if C<fuse> module is loaded in kernel.
237 die "no fuse module loaded in kernel"
238 unless (Fuse::DBI::fuse_module_loaded);
240 This function in called by C<mount>, but might be useful alone also.
244 sub fuse_module_loaded {
246 die "can't start lsmod: $!" unless ($lsmod);
247 if ($lsmod =~ m/fuse/s) {
260 my $sth = $self->{'sth'} || die "no sth argument";
262 # create empty filesystem
269 # cont => "File 'a'.\n",
271 # ctime => time()-2000
275 # fetch new filename list from database
276 $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
278 # read them in with sesible defaults
279 while (my $row = $sth->{'filenames'}->fetchrow_hashref() ) {
280 $files{$row->{'filename'}} = {
281 size => $row->{'size'},
282 mode => $row->{'writable'} ? 0644 : 0444,
283 id => $row->{'id'} || 99,
287 foreach (split(m!/!, $row->{'filename'})) {
288 # first, entry is assumed to be file
309 print "found ",scalar(keys %files)-scalar(keys %dirs)," files, ",scalar(keys %dirs), " dirs\n";
316 $file = '.' unless length($file);
321 my ($file) = filename_fixup(shift);
323 $file = '.' unless length($file);
324 return -ENOENT() unless exists($files{$file});
325 my ($size) = $files{$file}{size} || 1;
326 my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,0,0,1,1024);
327 my ($atime, $ctime, $mtime);
328 $atime = $ctime = $mtime = $files{$file}{ctime} || $ctime_start;
330 my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
332 # 2 possible types of return values:
333 #return -ENOENT(); # or any other error you care to
334 #print(join(",",($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks)),"\n");
335 return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
339 my ($dirname) = shift;
341 # return as many text filenames as you like, followed by the retval.
342 print((scalar keys %files)." files total\n");
344 foreach my $f (sort keys %files) {
346 if ($f =~ s/^\Q$dirname\E\///) {
347 $out{$f}++ if ($f =~ /^[^\/]+$/);
350 $out{$f}++ if ($f =~ /^[^\/]+$/);
354 $out{'no files? bug?'}++;
356 print scalar keys %out," files in dir '$dirname'\n";
357 print "## ",join(" ",keys %out),"\n";
358 return (keys %out),0;
364 die "read_content needs file and id" unless ($file && $id);
366 $sth->{'read'}->execute($id) || die $sth->{'read'}->errstr;
367 $files{$file}{cont} = $sth->{'read'}->fetchrow_array;
368 # I should modify ctime only if content in database changed
369 #$files{$file}{ctime} = time() unless ($files{$file}{ctime});
370 print "file '$file' content [",length($files{$file}{cont})," bytes] read in cache\n";
375 # VFS sanity check; it keeps all the necessary state, not much to do here.
376 my $file = filename_fixup(shift);
379 return -ENOENT() unless exists($files{$file});
380 return -EISDIR() unless exists($files{$file}{id});
382 read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
384 print "open '$file' ",length($files{$file}{cont})," bytes\n";
389 # return an error numeric, or binary/text string.
390 # (note: 0 means EOF, "0" will give a byte (ascii "0")
391 # to the reading program)
392 my ($file) = filename_fixup(shift);
393 my ($buf_len,$off) = @_;
395 return -ENOENT() unless exists($files{$file});
397 my $len = length($files{$file}{cont});
399 print "read '$file' [$len bytes] offset $off length $buf_len\n";
401 return -EINVAL() if ($off > $len);
402 return 0 if ($off == $len);
404 $buf_len = $len-$off if ($len - $off < $buf_len);
406 return substr($files{$file}{cont},$off,$buf_len);
410 print "transaction rollback\n";
411 $dbh->rollback || die $dbh->errstr;
412 print "invalidate all cached content\n";
413 foreach my $f (keys %files) {
414 delete $files{$f}{cont};
415 delete $files{$f}{ctime};
417 print "begin new transaction\n";
418 #$dbh->begin_work || die $dbh->errstr;
423 my $file = shift || die;
425 $files{$file}{ctime} = time();
432 if (!$sth->{'update'}->execute($cont,$id)) {
433 print "update problem: ",$sth->{'update'}->errstr;
437 if (! $dbh->commit) {
438 print "ERROR: commit problem: ",$sth->{'update'}->errstr;
442 print "updated '$file' [",$files{$file}{id},"]\n";
444 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
450 my $file = filename_fixup(shift);
451 my ($buffer,$off) = @_;
453 return -ENOENT() unless exists($files{$file});
455 my $cont = $files{$file}{cont};
456 my $len = length($cont);
458 print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
460 $files{$file}{cont} = "";
462 $files{$file}{cont} .= substr($cont,0,$off) if ($off > 0);
463 $files{$file}{cont} .= $buffer;
464 $files{$file}{cont} .= substr($cont,$off+length($buffer),$len-$off-length($buffer)) if ($off+length($buffer) < $len);
466 $files{$file}{size} = length($files{$file}{cont});
468 if (! update_db($file)) {
471 return length($buffer);
476 my $file = filename_fixup(shift);
479 print "truncate to $size\n";
481 $files{$file}{cont} = substr($files{$file}{cont},0,$size);
482 $files{$file}{size} = $size;
488 my ($atime,$mtime,$file) = @_;
489 $file = filename_fixup($file);
491 return -ENOENT() unless exists($files{$file});
493 print "utime '$file' $atime $mtime\n";
495 $files{$file}{time} = $mtime;
499 sub e_statfs { return 255, 1, 1, 1, 1, 2 }
502 my $file = filename_fixup(shift);
504 if (exists( $dirs{$file} )) {
505 print "unlink '$file' will re-read template names\n";
506 print Dumper($fuse_self);
507 $$fuse_self->{'read_filenames'}->();
509 } elsif (exists( $files{$file} )) {
510 print "unlink '$file' will invalidate cache\n";
511 read_content($file,$files{$file}{id});
526 C<FUSE (Filesystem in USErspace)> website
527 L<http://sourceforge.net/projects/avf>
529 Example for WebGUI which comes with this distribution in
530 directory C<examples/webgui.pl>. It also contains a lot of documentation
531 about design of this module, usage and limitations.
535 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
537 =head1 COPYRIGHT AND LICENSE
539 Copyright (C) 2004 by Dobrica Pavlinusic
541 This library is free software; you can redistribute it and/or modify
542 it under the same terms as Perl itself, either Perl version 5.8.4 or,
543 at your option, any later version of Perl 5 you may have available.