File Coverage

blib/lib/Fuse/TagLayer.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Fuse::TagLayer;
2              
3             # use strict;
4 1     1   20446 use warnings;
  1         2  
  1         36  
5              
6 1     1   131123 use Data::Dumper;
  1         132853  
  1         68  
7 1     1   422 use File::ExtAttr ();
  0            
  0            
8             use File::Find ();
9             use File::Basename ();
10             use Fcntl qw(SEEK_SET);
11             use POSIX qw(S_ISDIR ENOENT EISDIR EINVAL ENOSYS);
12             use Encode;
13              
14             our $VERSION = '0.12';
15             our $self;
16             our $numbers_regex = qr/^\d+$/;
17              
18             sub new {
19             my $class = shift;
20              
21             $self = bless({
22             @_
23             }, $class);
24              
25             $self->{uid} ||= 0;
26             $self->{gid} ||= 0;
27              
28             print "TagLayer: version:$VERSION, debug:$self->{debug}\n" if $self->{debug} > 1;
29              
30             if($self->{backend} eq 'PurePerl'){
31             require Fuse::TagLayer::PurePerl;
32             Fuse::TagLayer::PurePerl->import();
33             }else{
34             require Fuse::TagLayer::SQLite;
35             Fuse::TagLayer::SQLite->import();
36             }
37              
38             print "TagLayer: Building TagLayer tags database, using '$self->{backend}' backend...\n" if $self->{debug};
39             ## init db backend
40             db_init( debug => $self->{debug} );
41              
42             ## prepare a mountpoint regex
43             my $mntre = quotemeta($self->{mountpoint});
44             $self->{mountpoint_regex} = qr/^$mntre/;
45              
46             ## build SQL tables
47             # table:file_tags
48             File::Find::find({ wanted => \&wanted }, $self->{realdir});
49             db_sync(); # we assume backends to be non-auto-committing
50              
51             # table:tags
52             for (keys %{ $self->{global_tags} }){
53             print "## TagLayer::new: $_, ". $self->{global_tags}->{$_} ." \n" if $self->{debug} > 2;
54             db_tags_add($_, $self->{global_tags}->{$_});
55              
56             $self->{tags_cnt}++;
57             db_sync() if $self->{tags_cnt} && ($self->{tags_cnt} % 250) == 0;
58             }
59             delete($self->{global_tags});
60             db_sync();
61              
62             $self->{db_epoch} = time();
63              
64             print "TagLayer: processed ".($self->{files_cnt}||0)." files with ".($self->{tags_cnt}||0)." tags.\n" if $self->{debug};
65              
66             return $self;
67             }
68              
69             sub mount {
70             my $self = shift;
71              
72             print '## TagLayer: mount() self:'.Dumper($self) if $self->{debug};
73              
74             ## check local mount point
75             if(!-d $self->{mountpoint}){
76             die 'Fuse::TagLayer: Mountpoint '.$self->{mountpoint}.' does not exists!';
77             }
78              
79             Fuse::main(
80             mountpoint => $self->{mountpoint},
81             threaded => $self->{threaded} ? 1 : 0,
82             debug => $self->{debug} > 2 ? 1 : 0,
83              
84             readdir => "Fuse::TagLayer::virt_readdir",
85             getattr => "Fuse::TagLayer::virt_getattr",
86             open => "Fuse::TagLayer::real_open",
87             read => "Fuse::TagLayer::real_read",
88             release => "Fuse::TagLayer::real_release",
89             statfs => "Fuse::TagLayer::virt_statfs",
90             );
91             return;
92             }
93              
94             sub dirpath_to_tags {
95             ## explode path into tags
96             # if path comes from wanted, it returns unclean "tags",
97             # if path comes from our paths, tags should already be cleaned
98              
99             my @pathtags = split(/\//,shift);
100             shift(@pathtags); # root path means no dirtags
101              
102             return @pathtags;
103             }
104              
105             sub wanted {
106             ## if our mountpoint is within the realdir, ignore ourself
107             return if $File::Find::dir =~ $self->{mountpoint_regex};
108              
109             ## only dirs with files qualify
110             return if !-f $File::Find::name;
111              
112             my $realdir = $self->{realdir};
113             my $rel_dir = $File::Find::dir;
114             $rel_dir =~ s/^$realdir//;
115              
116             my @tags;
117             ## dir tags
118             @tags = dirpath_to_tags($rel_dir) unless $self->{no_tags_from_path};
119              
120             if($self->{more_tags}){
121             my $filename = lc($_);
122             $filename =~ s/(\.[a-zA-Z0-9]{2,5})$//;
123             if(my $suffix = $1){
124             $suffix =~ s/jpeg/jpg/;
125             push(@tags, 'zsuffix-'.$suffix);
126             }
127              
128             my @newtags = split(/[^\p{L}\p{N}]/,$filename); # matches all (Unicode) characters that are neither letters nor numbers
129             for(@newtags){
130             next unless defined $_;
131             next if length($_) < 2;
132             next if $self->{ignore_numbers_only} && $_ =~ $numbers_regex;
133             push(@tags, $_);
134             }
135             }
136              
137             ## xattr tags
138             if(!$self->{no_tags_from_xattr}){
139             if(my $xattrtags = File::ExtAttr::getfattr( $File::Find::dir.'/'.$_, 'tags') ){
140             $xattrtags = decode_utf8($xattrtags);
141             my @newtags = split(/,\s*/,$xattrtags);
142             for(@newtags){
143             next unless defined $_;
144             next if length($_) < 2;
145             next if $self->{ignore_numbers_only} && $_ =~ $numbers_regex;
146             push(@tags, $_);
147             }
148             }
149             }
150              
151             # clean and dedup, as there might be duplicates after cleansing
152             my %tags;
153             for(@tags){
154             my $tag_cleaned = lc($_);
155             $tag_cleaned =~ s/[^\p{L}\p{N}]//g; # matches all (Unicode) characters that are neither letters nor numbers
156             next if length($tag_cleaned) < 2;
157             next if $self->{ignore_numbers_only} && $tag_cleaned =~ $numbers_regex;
158             $tags{$tag_cleaned}++;
159              
160             $self->{global_tags}->{$tag_cleaned}++;
161             }
162              
163             # insert "/path/to", "filename", "tags as csv string"
164             db_files_add( $File::Find::dir, $_, keys(%tags) );
165             $self->{files_cnt}++;
166             # print "File: $self->{files_cnt}: $File::Find::dir, $_, ".join(", ", keys %tags)."\n";
167              
168             if($self->{files_cnt} && ($self->{files_cnt} % 250) == 0){
169             db_sync();
170             print " $self->{files_cnt} files processed\n" if $self->{debug};
171             }
172             }
173              
174             ## note the singular "file", as it should return only one file
175             sub file_by_tagpath {
176             my ($basename,$directory) = File::Basename::fileparse(shift);
177              
178             # 1st: only by tags
179             my @pathtags = dirpath_to_tags($directory);
180             print "file_by_tagpath: directory:$directory ; basename:$basename ; pathtags:@pathtags\n" if $self->{debug};
181              
182             return undef if !@pathtags;
183              
184             my ($files_for_tags, $subtags) = db_files_for_tags(@pathtags);
185             ## print "PREFAIL: tags: @tags (".@tags.") ;; SELECT `file`,`basename` FROM `file_tags` WHERE $sql_files;\n";
186             # my $pre = database()->selectall_arrayref("SELECT `file`,`basename` FROM `file_tags` WHERE $sql_files; ", {Columns=>[1,2]}); # push first two rows into arrayref
187              
188             return undef if !@$files_for_tags;
189              
190             # 2nd: by basename
191             my @files;
192             for(@$files_for_tags){
193             my ($thisbasename,$thisdirectory) = File::Basename::fileparse($_);
194             push(@files, $_) if $thisbasename eq $basename;
195             }
196              
197             print "TagLayer: ++ WARNING ++ file_by_tagpath($basename,@_) found multiple files: @files\n" if @files > 1;
198              
199             return @files ? shift(@files) : undef;
200             }
201              
202             sub virt_readdir {
203             my ($path,$offset) = @_;
204              
205             my (@dirs,@files);
206             if($path eq '/'){
207             ## return all tags:
208             @dirs = @{ db_tags_all() };
209             }else{
210             ## return a list of:
211             ## 1. all files tagged with the tags found in the path
212             ## 2. Sub-dirs (tags left) found in theses files but not yet applied
213             my @pathtags = dirpath_to_tags($path);
214              
215             my ($files_for_tags, $subtags) = db_files_for_tags(@pathtags);
216              
217             for(@$files_for_tags){
218             ($basename,$directory) = File::Basename::fileparse($_);
219             push(@files, $basename);
220             }
221             @dirs = keys %$subtags;
222             }
223              
224             print "## virt_readdir: $path: sub-tags left (as dirs):@dirs ; files:".scalar(@files)."\n" if $self->{debug};
225             print "## virt_readdir: \n ".join("\n ",@files)."\n" if $self->{debug} > 1 && @files;
226             return (@dirs || @files) ? ((@dirs,@files), 0) : 0;
227             }
228              
229             sub real_getattr {
230             my $file = shift; # we have real paths in the db anyway
231             print "real_getattr: file:$file\n" if $self->{debug};
232             my (@list) = lstat($file);
233             return -ENOENT() unless @list; # "-ENOENT" was "-$!", but if we compare both in Dumper, "-$!" is a string, and ENOENT is numeric
234             return @list;
235             }
236              
237             sub virt_getattr {
238             my ($path) = shift;
239             print "## virt_getattr: path:$path => " if $self->{debug};
240              
241             return -ENOENT() unless $self->{tags_cnt};
242              
243             # my $cnt = () = $path =~ /\//g; # from an older approach, to find out how deep we are in the tag-path
244              
245             ## find which file exactly is meant here
246             my $file = file_by_tagpath($path);
247              
248             if($file){
249             return real_getattr( $file );
250             }else{
251             print "## virt_getattr: path:$path ; file_by_tagpath() returned \n" if $self->{debug};
252             my ($modes) = (0040<<9) + 0775;
253             my ($dev, $ino, $rdev, $blocks, $uid, $gid, $nlink, $blksize) = (0,0,0,1,$self->{uid},$self->{gid},1,1024);
254             my $size = 0;
255             $blocks = $size;
256             my ($atime, $ctime, $mtime);
257             $atime = $ctime = $mtime = $self->{db_epoch};
258              
259             return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
260             }
261             return -ENOENT(); # never
262             }
263              
264             sub real_open {
265             my ($path,$mode) = @_;
266              
267             ## find which file exactly is meant here
268             my $file = file_by_tagpath($path);
269              
270             return -ENOSYS() if !$file;
271             return -ENOENT() unless -e $file;
272              
273             my $fh;
274             sysopen($fh,$file,$mode) or return -$!;
275              
276             return (0, $fh);
277             }
278              
279             sub real_read {
280             my ($path,$bufsize,$off,$fh) = @_;
281              
282             my $rv = -ENOSYS();
283              
284             if(seek($fh,$off,SEEK_SET)) {
285             read($fh,$rv,$bufsize);
286             }
287              
288             return $rv;
289             }
290              
291             sub real_release {
292             my ($path,$mode,$fh) = @_;
293              
294             close($fh) or return -$!;
295              
296             return 0;
297             }
298              
299             sub virt_statfs { return 255, 1, 1, 1, 1, 2 }
300              
301             sub umount {
302             db_disconnect();
303             }
304              
305              
306             1;
307              
308             __END__