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__ |