24d9d8c9317a99198c2e06a8e76a8f1cc2de26e2
[Fuse-DBI] / DBI.pm
1 #!/usr/bin/perl
2
3 package Fuse::DBI;
4
5 use 5.008;
6 use strict;
7 use warnings;
8
9 use POSIX qw(ENOENT EISDIR EINVAL ENOSYS O_RDWR);
10 use Fuse;
11 use DBI;
12 use Carp;
13 use Data::Dumper;
14
15
16 our $VERSION = '0.07';
17
18 =head1 NAME
19
20 Fuse::DBI - mount your database as filesystem and use it
21
22 =head1 SYNOPSIS
23
24   use Fuse::DBI;
25   Fuse::DBI->mount( ... );
26
27 See C<run> below for examples how to set parameters.
28
29 =head1 DESCRIPTION
30
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.
34
35 That will give you possibility to use normal file-system tools (cat, grep, vi)
36 to manipulate data in database.
37
38 It's actually opposite of Oracle's intention to put everything into database.
39
40
41 =head1 METHODS
42
43 =cut
44
45 =head2 mount
46
47 Mount your database as filesystem.
48
49 Let's suppose that your database have table C<files> with following structure:
50
51  id:            int
52  filename:      text
53  size:          int
54  content:       text
55  writable:      boolean
56
57 Following is example how to mount table like that to C</mnt>:
58
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 { ... },
67   });
68
69 Options:
70
71 =over 5
72
73 =item filenames
74
75 SQL query which returns C<id> (unique id for that row), C<filename>,
76 C<size> and C<writable> boolean flag.
77
78 =item read
79
80 SQL query which returns only one column with content of file and has
81 placeholder C<?> for C<id>.
82
83 =item update
84
85 SQL query with two pace-holders, one for new content and one for C<id>.
86
87 =item dsn
88
89 C<DBI> dsn to connect to (contains database driver and name of database).
90
91 =item user
92
93 User with which to connect to database
94
95 =item password
96
97 Password for connecting to database
98
99 =item invalidate
100
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>.
104
105 =item fork
106
107 Optional flag which forks after mount so that executing script will continue
108 running. Implementation is experimental.
109
110 =back
111
112 =cut
113
114 my $dbh;
115 my $sth;
116 my $ctime_start;
117
118 sub read_filenames;
119 sub fuse_module_loaded;
120
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
123 # be a problem.
124 my $fuse_self;
125
126 sub mount {
127         my $class = shift;
128         my $self = {};
129         bless($self, $class);
130
131         my $arg = shift;
132
133         print Dumper($arg);
134
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";
138         }
139
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'});
142
143         # save (some) arguments in self
144         foreach (qw(mount invalidate)) {
145                 $self->{$_} = $arg->{$_};
146         }
147
148         foreach (qw(filenames read update)) {
149                 carp "mount needs '$_' SQL" unless ($arg->{$_});
150         }
151
152         $ctime_start = time();
153
154         my $pid;
155         if ($arg->{'fork'}) {
156                 $pid = fork();
157                 die "fork() failed: $!" unless defined $pid;
158                 # child will return to caller
159                 if ($pid) {
160                         my $counter = 4;
161                         while ($counter && ! $self->is_mounted) {
162                                 select(undef, undef, undef, 0.5);
163                                 $counter--;
164                         }
165                         if ($self->is_mounted) {
166                                 return $self;
167                         } else {
168                                 return undef;
169                         }
170                 }
171         }
172
173         $dbh = DBI->connect($arg->{'dsn'},$arg->{'user'},$arg->{'password'}, {AutoCommit => 0, RaiseError => 1}) || die $DBI::errstr;
174
175         $sth->{'filenames'} = $dbh->prepare($arg->{'filenames'}) || die $dbh->errstr();
176
177         $sth->{'read'} = $dbh->prepare($arg->{'read'}) || die $dbh->errstr();
178         $sth->{'update'} = $dbh->prepare($arg->{'update'}) || die $dbh->errstr();
179
180
181         $self->{'sth'} = $sth;
182
183         $self->{'read_filenames'} = sub { $self->read_filenames };
184         $self->read_filenames;
185
186         $fuse_self = \$self;
187
188         Fuse::main(
189                 mountpoint=>$arg->{'mount'},
190                 getattr=>\&e_getattr,
191                 getdir=>\&e_getdir,
192                 open=>\&e_open,
193                 statfs=>\&e_statfs,
194                 read=>\&e_read,
195                 write=>\&e_write,
196                 utime=>\&e_utime,
197                 truncate=>\&e_truncate,
198                 unlink=>\&e_unlink,
199                 rmdir=>\&e_unlink,
200                 debug=>0,
201         );
202         
203         exit(0) if ($arg->{'fork'});
204
205         return 1;
206
207 };
208
209 =head2 is_mounted
210
211 Check if fuse filesystem is mounted
212
213   if ($mnt->is_mounted) { ... }
214
215 =cut
216
217 sub is_mounted {
218         my $self = shift;
219
220         my $mounted = 0;
221         my $mount = $self->{'mount'} || confess "can't find mount point!";
222         if (open(MTAB, "/etc/mtab")) {
223                 while(<MTAB>) {
224                         $mounted = 1 if (/ $mount fuse /i);
225                 }
226                 close(MTAB);
227         } else {
228                 warn "can't open /etc/mtab: $!";
229         }
230
231         return $mounted;
232 }
233
234
235 =head2 umount
236
237 Unmount your database as filesystem.
238
239   $mnt->umount;
240
241 This will also kill background process which is translating
242 database to filesystem.
243
244 =cut
245
246 sub umount {
247         my $self = shift;
248
249         if ($self->{'mount'} && $self->is_mounted) {
250                 system "fusermount -u ".$self->{'mount'}." 2>&1 >/dev/null" ||
251                         system "sudo umount ".$self->{'mount'} ||
252                         return 0;
253                 return 1;
254         }
255
256         return 0;
257 }
258
259 $SIG{'INT'} = sub {
260         if ($fuse_self && $$fuse_self->umount) {
261                 print STDERR "umount called by SIG INT\n";
262         }
263 };
264
265 $SIG{'QUIT'} = sub {
266         if ($fuse_self && $$fuse_self->umount) {
267                 print STDERR "umount called by SIG QUIT\n";
268         }
269 };
270
271 sub DESTROY {
272         my $self = shift;
273         if ($self->umount) {
274                 print STDERR "umount called by DESTROY\n";
275         }
276 }
277
278 =head2 fuse_module_loaded
279
280 Checks if C<fuse> module is loaded in kernel.
281
282   die "no fuse module loaded in kernel"
283         unless (Fuse::DBI::fuse_module_loaded);
284
285 This function in called by C<mount>, but might be useful alone also.
286
287 =cut
288
289 sub fuse_module_loaded {
290         my $lsmod = `lsmod`;
291         die "can't start lsmod: $!" unless ($lsmod);
292         if ($lsmod =~ m/fuse/s) {
293                 return 1;
294         } else {
295                 return 0;
296         }
297 }
298
299 my %files;
300
301 sub read_filenames {
302         my $self = shift;
303
304         my $sth = $self->{'sth'} || die "no sth argument";
305
306         # create empty filesystem
307         (%files) = (
308                 '.' => {
309                         type => 0040,
310                         mode => 0755,
311                 },
312                 '..' => {
313                         type => 0040,
314                         mode => 0755,
315                 },
316         #       a => {
317         #               cont => "File 'a'.\n",
318         #               type => 0100,
319         #               ctime => time()-2000
320         #       },
321         );
322
323         # fetch new filename list from database
324         $sth->{'filenames'}->execute() || die $sth->{'filenames'}->errstr();
325
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,
332                 };
333
334                 my $d;
335                 foreach (split(m!/!, $row->{'filename'})) {
336                         # first, entry is assumed to be file
337                         if ($d) {
338                                 $files{$d} = {
339                                                 mode => 0755,
340                                                 type => 0040
341                                 };
342                                 $files{$d.'/.'} = {
343                                                 mode => 0755,
344                                                 type => 0040
345                                 };
346                                 $files{$d.'/..'} = {
347                                                 mode => 0755,
348                                                 type => 0040
349                                 };
350                         }
351                         $d .= "/" if ($d);
352                         $d .= "$_";
353                 }
354         }
355
356         print "found ",scalar(keys %files)," files\n";
357 }
358
359
360 sub filename_fixup {
361         my ($file) = shift;
362         $file =~ s,^/,,;
363         $file = '.' unless length($file);
364         return $file;
365 }
366
367 sub e_getattr {
368         my ($file) = filename_fixup(shift);
369         $file =~ s,^/,,;
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;
376
377         my ($modes) = (($files{$file}{type} || 0100)<<9) + $files{$file}{mode};
378
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);
383 }
384
385 sub e_getdir {
386         my ($dirname) = shift;
387         $dirname =~ s!^/!!;
388         # return as many text filenames as you like, followed by the retval.
389         print((scalar keys %files)." files total\n");
390         my %out;
391         foreach my $f (sort keys %files) {
392                 if ($dirname) {
393                         if ($f =~ s/^\Q$dirname\E\///) {
394                                 $out{$f}++ if ($f =~ /^[^\/]+$/);
395                         }
396                 } else {
397                         $out{$f}++ if ($f =~ /^[^\/]+$/);
398                 }
399         }
400         if (! %out) {
401                 $out{'no files? bug?'}++;
402         }
403         print scalar keys %out," files in dir '$dirname'\n";
404         print "## ",join(" ",keys %out),"\n";
405         return (keys %out),0;
406 }
407
408 sub read_content {
409         my ($file,$id) = @_;
410
411         die "read_content needs file and id" unless ($file && $id);
412
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";
418 }
419
420
421 sub e_open {
422         # VFS sanity check; it keeps all the necessary state, not much to do here.
423         my $file = filename_fixup(shift);
424         my $flags = shift;
425
426         return -ENOENT() unless exists($files{$file});
427         return -EISDIR() unless exists($files{$file}{id});
428
429         read_content($file,$files{$file}{id}) unless exists($files{$file}{cont});
430
431         print "open '$file' ",length($files{$file}{cont})," bytes\n";
432         return 0;
433 }
434
435 sub e_read {
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) = @_;
441
442         return -ENOENT() unless exists($files{$file});
443
444         my $len = length($files{$file}{cont});
445
446         print "read '$file' [$len bytes] offset $off length $buf_len\n";
447
448         return -EINVAL() if ($off > $len);
449         return 0 if ($off == $len);
450
451         $buf_len = $len-$off if ($len - $off < $buf_len);
452
453         return substr($files{$file}{cont},$off,$buf_len);
454 }
455
456 sub clear_cont {
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};
463         }
464         print "begin new transaction\n";
465         #$dbh->begin_work || die $dbh->errstr;
466 }
467
468
469 sub update_db {
470         my $file = shift || die;
471
472         $files{$file}{ctime} = time();
473
474         my ($cont,$id) = (
475                 $files{$file}{cont},
476                 $files{$file}{id}
477         );
478
479         if (!$sth->{'update'}->execute($cont,$id)) {
480                 print "update problem: ",$sth->{'update'}->errstr;
481                 clear_cont;
482                 return 0;
483         } else {
484                 if (! $dbh->commit) {
485                         print "ERROR: commit problem: ",$sth->{'update'}->errstr;
486                         clear_cont;
487                         return 0;
488                 }
489                 print "updated '$file' [",$files{$file}{id},"]\n";
490
491                 $$fuse_self->{'invalidate'}->() if (ref $$fuse_self->{'invalidate'});
492         }
493         return 1;
494 }
495
496 sub e_write {
497         my $file = filename_fixup(shift);
498         my ($buffer,$off) = @_;
499
500         return -ENOENT() unless exists($files{$file});
501
502         my $cont = $files{$file}{cont};
503         my $len = length($cont);
504
505         print "write '$file' [$len bytes] offset $off length ",length($buffer),"\n";
506
507         $files{$file}{cont} = "";
508
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);
512
513         $files{$file}{size} = length($files{$file}{cont});
514
515         if (! update_db($file)) {
516                 return -ENOSYS();
517         } else {
518                 return length($buffer);
519         }
520 }
521
522 sub e_truncate {
523         my $file = filename_fixup(shift);
524         my $size = shift;
525
526         print "truncate to $size\n";
527
528         $files{$file}{cont} = substr($files{$file}{cont},0,$size);
529         $files{$file}{size} = $size;
530         return 0
531 };
532
533
534 sub e_utime {
535         my ($atime,$mtime,$file) = @_;
536         $file = filename_fixup($file);
537
538         return -ENOENT() unless exists($files{$file});
539
540         print "utime '$file' $atime $mtime\n";
541
542         $files{$file}{time} = $mtime;
543         return 0;
544 }
545
546 sub e_statfs {
547
548         my $size = 0;
549         my $inodes = 0;
550
551         foreach my $f (keys %files) {
552                 if ($f !~ /(^|\/)\.\.?$/) {
553                         $size += $files{$f}{size} || 0;
554                         $inodes++;
555                 }
556                 print "$inodes: $f [$size]\n";
557         }
558
559         $size = int(($size+1023)/1024);
560
561         my @ret = (255, $inodes+1000, $inodes, $size, $size-10, 1024);
562
563         print "statfs: ",join(",",@ret),"\n";
564
565         return @ret;
566 }
567
568 sub e_unlink {
569         my $file = filename_fixup(shift);
570
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'}->();
575 #               return 0;
576         if (exists( $files{$file} )) {
577                 print "unlink '$file' will invalidate cache\n";
578                 read_content($file,$files{$file}{id});
579                 return 0;
580         }
581
582         return -ENOENT();
583 }
584 1;
585 __END__
586
587 =head1 EXPORT
588
589 Nothing.
590
591 =head1 BUGS
592
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.
596
597 =head1 SEE ALSO
598
599 C<FUSE (Filesystem in USErspace)> website
600 L<http://fuse.sourceforge.net/>
601
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.
605
606 =head1 AUTHOR
607
608 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
609
610 =head1 COPYRIGHT AND LICENSE
611
612 Copyright (C) 2004 by Dobrica Pavlinusic
613
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.
617
618
619 =cut
620