/[fuse_dbi]/trunk/DBI.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/DBI.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 51 - (show annotations)
Sat Nov 27 14:02:18 2004 UTC (19 years, 4 months ago) by dpavlin
File size: 13365 byte(s)
Improvements in getattr and statfs: du will not return meaningful values and
df will return something which is not as wrong as it was (but, still not
correct).

Fuse::DBI will not try to load kernel module using sudo, and try to umount
using sudo umount if fusermount -u fails (as it happends with current CVS
version of fuse).

New webgui test target in Makefile which work as test on my local machine
(and hopefully on any with webgui default installation).

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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26