File Coverage

blib/lib/DataStore/CAS/FS/Importer.pm
Criterion Covered Total %
statement 176 268 65.6
branch 66 160 41.2
condition 12 66 18.1
subroutine 32 67 47.7
pod 17 19 89.4
total 303 580 52.2


line stmt bran cond sub pod time code
1             package DataStore::CAS::FS::Importer;
2 2     2   3054 use 5.008;
  2         5  
3 2     2   383 use Moo;
  2         8785  
  2         7  
4 2     2   1207 use Carp;
  2         2  
  2         96  
5 2     2   408 use Try::Tiny;
  2         858  
  2         94  
6 2     2   777 use File::Spec::Functions 'catfile', 'catdir', 'splitpath', 'catpath';
  2         1020  
  2         112  
7 2     2   9 use Fcntl;
  2         2  
  2         436  
8 2     2   329 use DataStore::CAS::FS::InvalidUTF8;
  2         3  
  2         36  
9 2     2   352 use DataStore::CAS::FS::DirCodec;
  2         3  
  2         177  
10              
11             our $VERSION= '0.011000';
12              
13             # ABSTRACT: Copy files from filesystem into DataStore::CAS::FS.
14              
15              
16             our %_flag_defaults;
17             BEGIN {
18 2     2   21 %_flag_defaults= (
19             die_on_dir_error => 1,
20             die_on_file_error => 1,
21             die_on_hint_error => 0,
22             die_on_metadata_error => 0,
23             collect_metadata_ts => 1,
24             collect_access_ts => 0,
25             collect_unix_perm => 1,
26             collect_unix_misc => 0,
27             collect_acl => 0,
28             collect_ext_attr => 0,
29             follow_symlink => 0,
30             cross_mountpoints => 0,
31             reuse_digests => 1,
32             utf8_filenames => 1,
33             );
34 2         8 for (keys %_flag_defaults) {
35 28 50   0 1 1779 eval "sub $_ { \$_[0]{flags}{$_}= \$_[1] if \@_ > 1; \$_[0]{flags}{$_} }; 1" or die $@
  0 0   0 1 0  
  0 0   0 1 0  
  0 0   0 1 0  
  0 0   0 1 0  
  0 0   0 1 0  
  0 0   3 1 0  
  0 50   0 1 0  
  0 0   0 1 0  
  0 0   0 1 0  
  0 0   0 0 0  
  0 0   8 1 0  
  0 50   3 1 0  
  3 50   9 1 7  
  3 50       18  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  8         18  
  8         122  
  3         9  
  3         4  
  9         26  
  9         25  
36             }
37             }
38             sub _flag_defaults {
39 2     2   3 \%_flag_defaults;
40             }
41              
42             has dir_format => ( is => 'rw', default => sub { 'universal' } );
43             has filter => ( is => 'rw' );
44             has flags => ( is => 'rw', default => sub { { } } );
45             has unix_uid_cache => ( is => 'rw', default => sub { {} } );
46             has unix_gid_cache => ( is => 'rw', default => sub { {} } );
47             has _hint_check_fn => ( is => 'rwp' );
48              
49             sub _handle_hint_error {
50 0 0   0   0 croak $_[1] if $_[0]->die_on_hint_error;
51 0         0 warn "$_[1]\n";
52             }
53              
54             sub _handle_file_error {
55 0 0   0   0 croak $_[1] if $_[0]->die_on_file_error;
56 0         0 warn "$_[1]\n";
57             }
58              
59             sub _handle_dir_error {
60 0 0   0   0 croak $_[1] if $_[0]->die_on_dir_error;
61 0         0 warn "$_[1]\n";
62             }
63              
64             sub _handle_metadata_error {
65 0 0   0   0 croak $_[1] if $_[0]->die_on_metadata_error;
66 0         0 warn "$_[1]\n";
67             }
68              
69              
70             sub BUILD {
71 2     2 0 7 my ($self, $args)= @_;
72 2         9 my $flags= $self->flags;
73 2         4 my $defaults= $self->_flag_defaults;
74 2         9 for (keys %$defaults) {
75             $flags->{$_}= delete $args->{$_}
76 28 50       30 if exists $args->{$_};
77             $flags->{$_}= $_flag_defaults{$_}
78 28 50       46 unless defined $flags->{$_};
79             }
80             defined $defaults->{$_} || croak "Unknown flag: '$_'"
81 2   33     26 for keys %$flags;
82             $self->can($_) || croak "Unknown attribute: '$_'"
83 2   0     12 for keys %$args;
84             }
85              
86             # locally-scoped to the device number which we should stay on
87             our $_DEVICE_CONSTRAINT;
88              
89              
90             sub import_tree {
91 1     1 1 1 my ($self, $src, $dest)= @_;
92            
93 1 50       3 my $stat= $self->_stat($src)
94             or croak "Source does not exist";
95              
96 1 50 33     28 local $_DEVICE_CONSTRAINT= $stat->dev
97             unless defined $_DEVICE_CONSTRAINT or $self->cross_mountpoints;
98              
99 1         2 $self->_build__hint_check_fn;
100              
101 1         2 my $ent_name= $self->_entname_from_path($src);
102 1         3 my $ent= $self->_import_directory_entry($dest->filesystem->store, $src, $ent_name, $stat, $dest);
103 1         4 $dest->filesystem->set_path($dest->path_names, $ent);
104 1         6 1;
105             }
106              
107              
108             sub import_directory {
109 1     1 1 389 my ($self, $cas, $path, $hint)= @_;
110              
111 1 50       3 my $stat= $self->_stat($path)
112             or croak "Source does not exist";
113              
114 1 50 33     24 local $_DEVICE_CONSTRAINT= $stat->dev
115             unless defined $_DEVICE_CONSTRAINT or $self->cross_mountpoints;
116              
117 1         2 $self->_build__hint_check_fn;
118              
119 1         3 $self->_import_directory($cas, $path, $hint);
120             }
121              
122             sub _import_directory {
123 2     2   3 my ($self, $cas, $path, $hint)= @_;
124 2 50       5 my $names= $self->_readdir($path)
125             or return undef;
126 2         2 my @entries;
127 2         5 my $filter= $self->filter;
128 2         4 for my $ent_name (@$names) {
129 4         22 my $ent_path= catfile($path, $ent_name);
130 4         75 my $stat= $self->_stat($ent_path);
131              
132 4 50       103 if ($self->utf8_filenames) {
133 4         19 $ent_name= DataStore::CAS::FS::InvalidUTF8->decode_utf8($ent_name);
134             } else {
135 0         0 utf8::upgrade($ent_name);
136             }
137              
138 4 50       8 my $keep= $filter? $filter->($ent_name, $ent_path, $stat) : 1;
139 4 50       12 next unless $keep;
140              
141             # Check for crossing mount point.
142 4 50 33     11 if (defined $_DEVICE_CONSTRAINT && $stat->dev ne $_DEVICE_CONSTRAINT) {
    50          
143             # TODO: log skipped mount points
144             # Metadata comes from mounted filesystem, so ignore it
145 0         0 push @entries, { type => 'dir', name => $ent_name };
146             }
147             # If keep is < 0, store the metadata but not the file/dir
148             elsif ($keep < 0) {
149 0         0 push @entries, $self->collect_dirent_metadata($ent_path, $ent_name, $stat);
150             }
151             # Else recursively store the whole thing
152             else {
153 4         11 push @entries, $self->_import_directory_entry($cas, $ent_path, $ent_name, $stat, $hint);
154             }
155             }
156 2         13 return DataStore::CAS::FS::DirCodec->put($cas, $self->dir_format, \@entries, {} );
157             }
158              
159              
160             sub import_directory_entry {
161 1     1 1 54 my ($self, $cas, $path, $ent_name, $stat, $hint)= @_;
162              
163 1 50 33     4 $stat||= $self->_stat($path)
164             or croak "Source does not exist";
165              
166 1         4 $self->_build__hint_check_fn;
167              
168 1 50 33     22 local $_DEVICE_CONSTRAINT= $stat->dev
169             unless defined $_DEVICE_CONSTRAINT or $self->cross_mountpoints;
170              
171 1 50       4 $ent_name= $self->_entname_from_path($path)
172             unless defined $ent_name;
173 1         3 return DataStore::CAS::FS::DirEnt->new(
174             $self->_import_directory_entry($cas, $path, $ent_name, $stat, $hint)
175             );
176             }
177              
178             sub _import_directory_entry {
179 6     6   14 my ($self, $cas, $ent_path, $ent_name, $stat, $hint)= @_;
180 6 50       9 my $attrs= $self->collect_dirent_metadata($ent_path, $ent_name, $stat)
181             or croak "Path does not exist: '$ent_path'";
182 6 100       20 if ($attrs->{type} eq 'file') {
    50          
183 5 50 33     9 if ($hint && $self->_can_reuse_hash($attrs, $hint)) {
184 0         0 $attrs->{ref}= $hint->ref;
185             } else {
186 5         4 my $err;
187 5     0   29 $attrs->{ref}= try { $cas->put_file($ent_path); } catch { $err= $_; undef; };
  5         159  
  0         0  
  0         0  
188 5 50       845 $self->_handle_file_error("Error while importing file '$ent_path': $err")
189             if defined $err;
190             }
191             }
192             elsif ($attrs->{type} eq 'dir') {
193 1 50 33     8 if (defined $_DEVICE_CONSTRAINT && $stat->dev ne $_DEVICE_CONSTRAINT) {
194             # TODO: log skipped mount points
195             } else {
196 1 50 33     4 local $_DEVICE_CONSTRAINT= $stat->dev
197             unless defined $_DEVICE_CONSTRAINT || $self->cross_mountpoints;
198              
199 1         1 my $subdir_hint;
200 1 50       2 if (defined $hint) {
201 1         1 my $err;
202             try {
203 1     1   32 $subdir_hint= $hint->path_if_exists($attrs->{name});
204 1 50       3 $subdir_hint->resolve
205             if $subdir_hint;
206             } catch {
207 0     0   0 $err= $_;
208 1         7 };
209 1 50       12 $self->_handle_hint_error("Error while loading virtual path '".$hint->resolved_canonical_path.'/'.$attrs->{name}."': $err")
210             if defined $err;
211             }
212 1         2 $attrs->{ref}= $self->_import_directory($cas, $ent_path, $subdir_hint);
213             }
214             }
215 6         61 return $attrs;
216             }
217              
218              
219             our %_ModeToType;
220             # Making this a function allows other code to call it in a BEGIN block if needed
221             sub _build_ModeToType {
222 2     2   4 local $@;
223 2         2 eval { $_ModeToType{Fcntl::S_IFREG()}= 'file' };
  2         4  
224 2         2 eval { $_ModeToType{Fcntl::S_IFDIR()}= 'dir' };
  2         2  
225 2         2 eval { $_ModeToType{Fcntl::S_IFLNK()}= 'symlink' };
  2         2  
226 2         2 eval { $_ModeToType{Fcntl::S_IFBLK()}= 'blockdev' };
  2         2  
227 2         2 eval { $_ModeToType{Fcntl::S_IFCHR()}= 'chardev' };
  2         2  
228 2         4 eval { $_ModeToType{Fcntl::S_IFIFO()}= 'pipe' };
  2         1  
229 2         3 eval { $_ModeToType{Fcntl::S_IFWHT()}= 'whiteout' };
  2         35  
230 2         4 eval { $_ModeToType{Fcntl::S_IFSOCK()}= 'socket' };
  2         4  
231             }
232              
233             _build_ModeToType();
234              
235             sub collect_dirent_metadata {
236 7     7 1 70 my ($self, $path, $ent_name, $stat)= @_;
237            
238 7 50 66     22 $stat ||= $self->_stat($path)
239             or return undef;
240              
241 7 100       16 $ent_name= $self->_entname_from_path($path)
242             unless defined $ent_name;
243            
244             my %attrs= (
245 7         43 type => ($_ModeToType{$stat->[2] & Fcntl::S_IFMT()}),
246             name => $ent_name,
247             size => $stat->[7],
248             modify_ts => $stat->[9],
249             );
250 7 50       11 if (!defined $attrs{type}) {
251 0         0 $self->_handle_dir_error("Type of dirent is unknown: ".($stat->[2] & Fcntl::S_IFMT()));
252 0         0 $attrs{type}= 'file';
253             }
254 7 50       13 if ($self->{flags}{collect_unix_perm}) {
255 7         19 $attrs{unix_mode}= ($stat->[2] & ~Fcntl::S_IFMT());
256 7         12 my $uid= $attrs{unix_uid}= $stat->[4];
257 7 50       22 if (my $cache= $self->unix_uid_cache) {
258 7 100       12 if (!exists $cache->{$uid}) {
259 1         480 my $name= getpwuid($uid);
260 1 50       37 if (!defined $name) {
    50          
261 0         0 $self->_handle_metadata_error("No username for UID $uid");
262             } elsif ($self->utf8_filenames) {
263 1         4 $name= DataStore::CAS::FS::InvalidUTF8->decode_utf8($name);
264             } else {
265 0         0 utf8::upgrade($name);
266             }
267 1         3 $cache->{$uid}= $name;
268             }
269             $attrs{unix_user}= $cache->{$uid}
270 7 50       18 if defined $cache->{$uid};
271             }
272 7         17 my $gid= $attrs{unix_gid}= $stat->[5];
273 7 50       16 if (my $cache= $self->unix_gid_cache) {
274 7 100       11 if (!exists $cache->{$gid}) {
275 1         72 my $name= getgrgid($gid);
276 1 50       31 if (!defined $name) {
    50          
277 0         0 $self->_handle_metadata_error("No groupname for GID $gid");
278             } elsif ($self->utf8_filenames) {
279 1         4 $name= DataStore::CAS::FS::InvalidUTF8->decode_utf8($name);
280             } else {
281 0         0 utf8::upgrade($name);
282             }
283 1         3 $cache->{$gid}= $name;
284             }
285             $attrs{unix_group}= $cache->{$gid}
286 7 50       24 if defined $cache->{$gid};
287             }
288             }
289 7 50       12 if ($self->{flags}{collect_metadata_ts}) {
290 7         10 $attrs{metadata_ts}= $stat->[10];
291             }
292 7 50       10 if ($self->{flags}{collect_access_ts}) {
293 0         0 $attrs{access_ts}= $stat->[8];
294             }
295 7 50       11 if ($self->{flags}{collect_unix_misc}) {
296 0         0 $attrs{unix_dev}= $stat->[0];
297 0         0 $attrs{unix_inode}= $stat->[1];
298 0         0 $attrs{unix_nlink}= $stat->[3];
299 0         0 $attrs{unix_blocksize}= $stat->[11];
300 0         0 $attrs{unix_blockcount}= $stat->[12];
301             }
302 7 50       11 if ($self->{flags}{collect_acl}) {
303             # TODO
304             }
305 7 50       11 if ($self->{flags}{collect_ext_attr}) {
306             # TODO
307             }
308 7 100 33     32 if ($attrs{type} eq 'dir') {
    50          
    50          
309 1         3 delete $attrs{size};
310             }
311             elsif ($attrs{type} eq 'symlink') {
312 0         0 $attrs{ref}= readlink $path;
313             }
314             elsif ($attrs{type} eq 'blockdev' or $attrs{type} eq 'chardev') {
315 0         0 $attrs{ref}= $self->_split_dev_node($stat->[6]);
316             }
317 7         26 \%attrs;
318             }
319              
320             sub _build__hint_check_fn {
321 3     3   4 my $self= shift;
322 3         60 my $reuse= $self->reuse_digests;
323 3 0       11 return $self->{_hint_check_fn}= $reuse > 1?
    50          
    50          
324             ($reuse > 2? \&_hint_check_ctime : \&_hint_check_mtime)
325             : ($reuse > 0? \&_hint_check_size : \&_hint_check_none);
326             }
327              
328             sub _hint_check_none {
329 0     0   0 return undef;
330             }
331             sub _hint_check_size {
332 0     0   0 my ($self, $attrs, $hint)= @_;
333 0 0 0     0 return undef unless defined $hint && defined $hint->ref;
334 0         0 my ($size, $h_size)= ($attrs->{size}, $hint->size);
335 0   0     0 return defined $size && defined $h_size && $size eq $h_size;
336             }
337             sub _hint_check_mtime {
338 0     0   0 my ($self, $attrs, $hint)= @_;
339 0 0 0     0 return undef unless defined $hint && defined $hint->ref;
340 0         0 my ($size, $h_size)= ($attrs->{size}, $hint->size);
341 0 0 0     0 return undef unless defined $size && defined $h_size && $size eq $h_size;
      0        
342 0         0 my ($modify_ts, $h_modify_ts)= ($attrs->{modify_ts}, $hint->modify_ts);
343 0   0     0 return defined $modify_ts && defined $h_modify_ts && $modify_ts eq $h_modify_ts;
344             }
345             sub _hint_check_ctime {
346 0     0   0 my ($self, $attrs, $hint)= @_;
347 0 0 0     0 return undef unless defined $hint && defined $hint->ref;
348 0         0 my ($size, $h_size)= ($attrs->{size}, $hint->size);
349 0 0 0     0 return undef unless defined $size && defined $h_size && $size eq $h_size;
      0        
350 0         0 my ($modify_ts, $h_modify_ts)= ($attrs->{metadata_ts}, $hint->metadata_ts);
351 0   0     0 return defined $modify_ts && defined $h_modify_ts && $modify_ts eq $h_modify_ts;
352             }
353              
354             sub _entname_from_path {
355 3     3   3 my ($self, $path)= @_;
356 3         9 my (undef, undef, $ent_name)= splitpath($path);
357 3 50       140 if ($self->utf8_filenames) {
358 3         13 $ent_name= DataStore::CAS::FS::InvalidUTF8->decode_utf8($ent_name);
359             } else {
360 0         0 utf8::upgrade($ent_name);
361             }
362 3         6 $ent_name;
363             }
364              
365             sub _split_dev_node {
366 0     0   0 ($_[1] >> 8).','.($_[1] & 0xFF);
367             }
368              
369             sub _stat {
370 0     0   0 my $fn= \&_stat_unix;
371 2     2   10 no warnings 'redefine';
  2         4  
  2         239  
372 0         0 *_stat= $fn;
373 0         0 goto $fn;
374             }
375              
376             sub _stat_unix {
377 8     8   11 my ($self, $path)= @_;
378 8 50       193 my @stat= $self->follow_symlink? stat($path) : lstat($path);
379 8 50       144 unless (@stat) {
380 0         0 $self->_handle_dir_error("Can't stat '$path': $!");
381 0         0 return undef;
382             }
383 8         53 bless \@stat, 'DataStore::CAS::FS::Importer::FastStat';
384             }
385              
386             sub _readdir {
387 0     0   0 my $fn= \&_readdir_unix;
388 2     2   6 no warnings 'redefine';
  2         3  
  2         267  
389 0         0 *_readdir= $fn;
390 0         0 goto $fn;
391             }
392              
393             sub _readdir_unix {
394 2     2   2 my ($self, $path)= @_;
395 2         3 my $dh;
396 2 50       8 if (!opendir($dh, $path)) {
397 0         0 $self->_handle_dir_error("opendir($path): $!");
398 0         0 return undef;
399             }
400              
401 2 100       111 my @names= grep { $_ ne '.' && $_ ne '..' } readdir($dh);
  8         34  
402              
403 2 50       15 if (!closedir $dh) {
404 0         0 $self->_handle_dir_error("closedir($path): $!");
405 0         0 return undef;
406             }
407              
408 2         9 \@names;
409             }
410              
411             package DataStore::CAS::FS::Importer::FastStat;
412 2     2   24 use strict;
  2         2  
  2         38  
413 2     2   5 use warnings;
  2         2  
  2         385  
414              
415              
416 8     8   25 sub dev { $_[0][0] }
417 0     0     sub ino { $_[0][1] }
418 0     0     sub mode { $_[0][2] }
419 0     0     sub nlink { $_[0][3] }
420 0     0     sub uid { $_[0][4] }
421 0     0     sub gid { $_[0][5] }
422 0     0     sub rdev { $_[0][6] }
423 0     0     sub size { $_[0][7] }
424 0     0     sub atime { $_[0][8] }
425 0     0     sub mtime { $_[0][9] }
426 0     0     sub ctime { $_[0][10] }
427 0     0     sub blksize { $_[0][11] }
428 0     0     sub blocks { $_[0][12] }
429              
430             1;
431              
432             __END__