File Coverage

blib/lib/DataStore/CAS/FS/Exporter.pm
Criterion Covered Total %
statement 64 126 50.7
branch 22 128 17.1
condition 2 36 5.5
subroutine 14 27 51.8
pod 3 6 50.0
total 105 323 32.5


line stmt bran cond sub pod time code
1             package DataStore::CAS::FS::Exporter;
2 2     2   2305 use 5.008;
  2         6  
  2         68  
3 2     2   768 use Moo;
  2         788985  
  2         16  
4 2     2   9490 use Try::Tiny;
  2         1962  
  2         119  
5 2     2   14 use Carp;
  2         4  
  2         130  
6 2     2   834 use File::Spec::Functions 'catfile', 'catdir', 'splitpath', 'catpath';
  2         1018  
  2         164  
7 2     2   16 use Fcntl ':mode';
  2         9  
  2         979  
8              
9             our $VERSION= '0.011000';
10              
11             # ABSTRACT: Copy files from DataStore::CAS::FS to real filesystem.
12              
13              
14             our %_flag_defaults;
15             BEGIN {
16 2     2   20 %_flag_defaults= (
17             die_on_unsupported => 1,
18             die_on_creation_error => 1,
19             die_on_metadata_error => 1,
20             utf8_filenames => 1,
21             );
22 2         12 for (keys %_flag_defaults) {
23 8 50   0 0 825 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  
  0         0  
  0         0  
  0         0  
  0         0  
24             }
25             }
26             sub _flag_defaults {
27 1     1   5 \%_flag_defaults;
28             }
29              
30             has flags => ( is => 'rw', default => sub { {} } );
31             has unix_user_cache => ( is => 'rw', default => sub { {} } );
32             has unix_group_cache => ( is => 'rw', default => sub { {} } );
33              
34              
35             sub BUILD {
36 1     1 0 12 my ($self, $args)= @_;
37 1         7 my $flags= $self->flags;
38 1         6 my $defaults= $self->_flag_defaults;
39 1         8 for (keys %$defaults) {
40 4 50       32 $flags->{$_}= delete $args->{$_}
41             if exists $args->{$_};
42 4 50       27 $flags->{$_}= $_flag_defaults{$_}
43             unless defined $flags->{$_};
44             }
45             defined $defaults->{$_} || croak "Unknown flag: '$_'"
46 1   33     17 for keys %$flags;
47             $self->can($_) || croak "Unknown attribute: '$_'"
48 1   0     42 for keys %$args;
49             }
50              
51             sub export_tree {
52 1     1 0 6 my ($self, $virt_path, $real_path)= @_;
53              
54 1 50       19 $virt_path->isa('DataStore::CAS::FS::Path')
55             or croak "Virtual path must be an instance of DataStore::CAS::FS::Path";
56              
57 1 50       15 -e $real_path
58             and croak "The destination path must not already exist";
59 1 50       287 if (utf8::is_utf8($real_path)) {
60 0 0       0 $self->utf8_filenames? utf8::encode($real_path) : utf8::downgrade($real_path);
61             }
62 1         8 $self->_extract_recursive($virt_path, $real_path);
63 1         12 1;
64             }
65              
66             sub _extract_recursive {
67 3     3   103 my ($self, $src, $real_path)= @_;
68 3         21 my $dirent= $src->dirent;
69 3         22 my $dest_fh= $self->_create_dirent($dirent, $real_path);
70 3 100       166 if ($dirent->type eq 'file') {
    50          
71             # Copy file
72 2 50       112 if (!defined $dirent->ref) {
    50          
73 0         0 warn "File \"".$dirent->name."\" was not stored. Exporting as empty file.\n";
74             } elsif ($dirent->ref ne $src->filesystem->hash_of_null) {
75 0         0 my $err;
76             try {
77 0     0   0 my $src_fh= $src->open;
78 0         0 my ($buf, $got);
79 0         0 while ($got= read($src_fh, $buf, 1024*1024)) {
80 0 0       0 (print $dest_fh $buf) or die "write: $!\n";
81             }
82 0 0       0 defined $got or die "read: $!\n";
83 0 0       0 close $src_fh or die "close: $!\n";
84 0 0       0 close $dest_fh or die "close: $!\n";
85             } catch {
86 0     0   0 chomp( $err= "$_" );
87 0         0 };
88 0 0       0 $self->_handle_creation_error("copy to \"$real_path\": $err")
89             if defined $err;
90             }
91             } elsif ($dirent->type eq 'dir') {
92 1         10 for ($src->readdir) {
93 2         8 my $sysname= "$_";
94 2 0       24 $self->utf8_filenames? utf8::encode($sysname) : utf8::downgrade($sysname)
    50          
95             if utf8::is_utf8($sysname);
96 2         15 $self->_extract_recursive($src->path($_), File::Spec->catdir($real_path, $sysname))
97             }
98             }
99 3         139 $self->_apply_metadata($dirent, $real_path);
100             }
101              
102             sub _create_dirent {
103 3     3   9 my ($self, $entry, $path)= @_;
104 3         153 my $t= $entry->type;
105 3 100 0     53 if ($t eq 'file') {
    50          
    0          
    0          
    0          
    0          
106 2 50       441 open(my $dest_fh, '>:raw', $path)
107             or $self->_handle_creation_error("open($path): $!");
108 2         11 return $dest_fh;
109             } elsif ($t eq 'dir') {
110 1 50       9 mkdir $path
111             or $self->_handle_creation_error("mkdir($path): $!");
112             } elsif ($t eq 'symlink') {
113 0 0       0 symlink $entry->ref, $path
114             or $self->_handle_creation_error("symlink($path): $!");
115             } elsif ($t eq 'blockdev' || $t eq 'chardev') {
116 0         0 my ($major, $minor)= split /,/, $entry->ref;
117 0 0 0     0 defined $major && length $major && defined $minor && length $minor
      0        
      0        
118             or die "mknod($path): Invalid device notation \"".$entry->ref."\"\n";
119 0         0 $self->_mknod($self, $path, $entry, $major, $minor);
120             } elsif ($t eq 'pipe') {
121 0         0 $self->_mknod($self, $path, $entry, 0, 0);
122             } elsif ($t eq 'socket') {
123 0         0 require Socket;
124 0         0 my $sock;
125 0 0 0     0 socket($sock, Socket::PF_UNIX(), Socket::SOCK_STREAM(), 0)
126             && bind($sock, sockaddr_un($path))
127             or $self->_handle_creation_error("socket/bind($path): $!");
128             } else {
129 0         0 $self->_handle_creation_error("Unsupported directory entry type \"$t\" for $path");
130             }
131 1         256 return undef;
132             }
133              
134             sub _apply_metadata {
135 3     3   8 my ($self, $entry, $path)= @_;
136 3 50       136 if (defined (my $mode= $entry->unix_mode)) {
137 0 0       0 chmod($mode & ~Fcntl::S_IFMT(), $path)
138             or $self->_handle_metadata_error("chmod($path): $!");
139             }
140              
141 3         132 my ($uid, $gid)= ($entry->unix_uid, $entry->unix_gid);
142 3 50       129 if (defined (my $u= $entry->unix_user)) {
143 0         0 my $cache= $self->unix_user_cache;
144 0 0 0     0 exists $cache->{$u}? (defined $cache->{$u} and ($uid= $cache->{$u}))
    0          
145             : defined( $cache->{$u}= getgrnam($u) )? $uid= $cache->{$u}
146             : $self->_handle_metadata_error("Can't resolve username '$u'");
147             }
148 3 50       115 if (defined (my $g= $entry->unix_group)) {
149 0         0 my $cache= $self->unix_group_cache;
150 0 0 0     0 exists $cache->{$g}? (defined $cache->{$g} and ($gid= $cache->{$g}))
    0          
151             : defined( $cache->{$g}= getgrnam($g) )? $gid= $cache->{$g}
152             : $self->_handle_metadata_error("Can't resolve username '$g'");
153             }
154 3 0 0     26 chown( (defined $uid? $uid : -1), (defined $gid? $gid : -1), $path )
    0 33        
    50          
155             || $self->_handle_metadata_error("chown($uid, $gid, $path): $!")
156             if defined $uid || defined $gid;
157              
158 3         124 my $mtime= $entry->modify_ts;
159 3 50       116 if (defined $mtime) {
160 0           my $atime= $entry->access_ts;
161 0 0         defined $atime or $atime= $mtime;
162 0 0         utime($atime, $mtime, $path)
163             or $self->_handle_metadata_error("utime($atime, $mtime, $path): $!");
164             }
165             }
166              
167             sub _handle_metadata_error {
168 0     0     my ($self, $msg)= @_;
169 0 0         die $msg."\n" if $self->{flags}{die_on_metadata_error};
170 0           warn $msg."\n";
171             }
172              
173             sub _handle_creation_error {
174 0     0     my ($self, $msg)= @_;
175 0 0         die $msg."\n" if $self->{flags}{die_on_creation_error};
176 0           warn $msg."\n";
177             }
178              
179             sub _mknod {
180 0 0 0 0     my $fn= (try { require Unix::Mknod; 1; } catch { undef })? \&_mknod_perl
  0 0   0      
  0            
  0            
181             : (`mknod --version` && $? == 0)? \&_mknod_system
182             : \&_mknod_unsupported;
183 2     2   14 no warnings 'redefine';
  2         3  
  2         961  
184 0           *_mknod= $fn;
185 0           goto $fn;
186             }
187              
188             sub _mknod_perl {
189 0     0     my ($self, $path, $entry, $major, $minor)= @_;
190 0 0         my $mode= ($entry->type eq 'blockdev')? S_IFBLK|0600
    0          
    0          
191             : ($entry->type eq 'chardev')? S_IFCHR|0600
192             : ($entry->type eq 'pipe')? S_IFIFO|0600
193             : die "Unsupported type ".$entry->type;
194 0 0         0 == Unix::Mknod::mknod($path, $mode, Unix::Mknod::makedev($major, $minor))
195             or $self->_handle_creation_error("mknod($path, $mode, ".Unix::Mknod::makedev($major, $minor)."): $!");
196             }
197              
198             sub _mknod_system {
199 0     0     my ($self, $path, $dirent, $major, $minor)= @_;
200 0 0         if ($dirent->type eq 'pipe') {
201 0 0         system('mkfifo', $path) == 0 || die "exec(mkfifo, $path): $!\n";
202 0 0         $? == 0 || $self->_handle_creation_error("mkfifo($path) exited ".($? & 127? "on signal ".($? & 127) : "with ".($? >> 8)));
    0          
203             } else {
204 0 0         my $t= $dirent->type eq 'blockdev'? 'b' : 'c';
205 0 0         system('mknod', $path, $t, $major, $minor) == 0 or die "exec(mknod, $path, $t, $major, $minor): $!\n";
206 0 0         $? == 0 || $self->_handle_creation_error("mknod($path) exited ".($? & 127? "on signal ".($? & 127) : "with ".($? >> 8)));
    0          
207             }
208             }
209              
210             sub _mknod_unsupported {
211 0     0     my ($self, $path)= @_;
212 0 0         $self->die_on_unsupported?
213             die "mknod($path): Module Unix::Mknod is not installed and mknod(1) is not in the PATH\n"
214             : warn "Skipping mknod($path)\n";
215             }
216              
217             1;
218              
219             __END__