/[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 54 - (show annotations)
Tue Apr 26 19:57:51 2005 UTC (17 years, 3 months ago) by dpavlin
File size: 13485 byte(s)
better support for zero-sized files

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

Properties

Name Value
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.26