File Coverage

blib/lib/Evo/Fs.pm
Criterion Covered Total %
statement 317 318 99.6
branch 122 194 62.8
condition 19 25 76.0
subroutine 45 45 100.0
pod 19 32 59.3
total 522 614 85.0


line stmt bran cond sub pod time code
1             package Evo::Fs;
2 8     8   3708 use Evo '-Export *; -Class; ::Stat; -Path; Carp croak; -Path';
  8         19  
  8         60  
3             die "Win isn't supported yet. Pull requests are welcome!" if $^O eq 'MSWin32';
4              
5 8     8   66 use Fcntl qw(:seek O_RDWR O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC O_APPEND O_EXCL :flock);
  8         20  
  8         1550  
6 8     8   58 use Evo 'File::Spec; File::Path; Cwd() abs_path; File::Basename fileparse; Symbol()';
  8         14  
  8         33  
7 8     8   3557 use Time::HiRes ();
  8         8348  
  8         203  
8 8     8   53 use Evo 'List::Util first; File::Copy ()';
  8         18  
  8         118  
9 8     8   62 use Errno qw(EAGAIN EWOULDBLOCK);
  8         21  
  8         1122  
10 8     8   56 use Scalar::Util;
  8         21  
  8         1284  
11              
12             sub SKIP_HIDDEN : Export : prototype() {
13 7 50   7   71 sub($dir) {
  7 50       18  
  7         13  
  7         11  
14 7         41 my @dirs = File::Spec->splitdir($dir);
15 7         43 $dirs[-1] !~ /^\./;
16 5     5 1 37 };
17 8     8   60 }
  8         19  
  8         82  
18              
19              
20             # ========= CLASS =========
21              
22              
23             has root =>
24             check sub($v) { File::Spec->file_name_is_absolute($v) ? 1 : (0, "root should be absolute") };
25              
26              
27 2 50   2 1 17 sub cd ($self, $rel) {
  2 50       10  
  2         7  
  2         6  
  2         5  
28 2         20 my $root = Evo::Path->from_string('', $self->root . '')->append_unsafe($rel)->to_string;
29 2         22 ref($self)->new(root => $root);
30             }
31              
32 568 50   568 1 1246 sub path2real ($self, $rel) {
  568 50       1129  
  568         854  
  568         801  
  568         871  
33 568         2485 Evo::Path->from_string($rel, $self->root . '')->to_string;
34             }
35              
36 78 50   78 0 705 sub exists ($self, $path) {
  78 50       179  
  78         122  
  78         113  
  78         110  
37 78         159 -e $self->path2real($path);
38             }
39              
40              
41 14 50   14 0 100 sub mkdir ($self, $path, $perm = undef) {
  14 50       48  
  14         29  
  14         27  
  14         31  
  14         23  
42 14         35 my $real = $self->path2real($path);
43 14 100       988 &CORE::mkdir($real, defined $perm ? $perm : ()) or croak "$real: $!";
    100          
44             }
45              
46 72 50   72 0 293 sub make_tree ($self, $path, $perms = undef) {
  72 50       181  
  72         133  
  72         126  
  72         121  
  72         105  
47 72         158 my $real = $self->path2real($path);
48 72         315 my %opts = (error => \my $err);
49 72 50       196 $opts{chmod} = $perms if defined $perms;
50 72         7139 File::Path::make_path($real, \%opts);
51 72 100       400 croak join('; ', map { $_->%* } @$err) if @$err; # TODO: test
  1         126  
52             }
53              
54 7 50   7 0 61 sub symlink ($self, $to_path, $link_path) {
  7 50       24  
  7         15  
  7         14  
  7         15  
  7         12  
55 7 100       17 CORE::symlink($self->path2real($to_path), $self->path2real($link_path))
56             or croak "symlink $to_path $link_path: $!";
57             }
58              
59 3 50   3 0 29 sub link ($self, $to_path, $link_path) {
  3 50       12  
  3         7  
  3         8  
  3         6  
  3         7  
60 3 50       11 CORE::link($self->path2real($to_path), $self->path2real($link_path))
61             or croak "hardlink $to_path $link_path: $!";
62             }
63              
64 4 50   4 0 17 sub is_symlink ($self, $path) {
  4 50       12  
  4         7  
  4         9  
  4         7  
65 4         11 -l $self->path2real($path);
66             }
67              
68              
69 2 50   2 0 20 sub utimes ($self, $path, $atime = undef, $mtime = undef) {
  2 50       13  
  2         8  
  2         5  
  2         6  
  2         5  
  2         6  
70 2         8 my $real = $self->path2real($path);
71 2 100 100     197 utime($atime // undef, $mtime // undef, $real) or croak "utimes $path: $!";
      100        
72             }
73              
74 55 50   55 0 1799 sub close ($self, $fh) {
  55 50       123  
  55         95  
  55         81  
  55         88  
75 55         425 close $fh;
76             }
77              
78 143 50   143 1 373 sub stat ($self, $path) {
  143 50       328  
  143         225  
  143         211  
  143         202  
79 143         204 my %opts;
80 143 100       286 my @stat = Time::HiRes::stat $self->path2real($path) or croak "stat $path: $!";
81 141         935 @opts{qw(dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks)} = @stat;
82 141         2663 Evo::Fs::Stat->new(%opts, _data => \@stat);
83             }
84              
85 1 50   1 1 11 sub rename ($self, $old, $new) {
  1 50       4  
  1         2  
  1         3  
  1         3  
  1         3  
86 1 50       5 rename $self->path2real($old), $self->path2real($new) or croak "rename $!";
87             }
88              
89              
90             my %seek_map = (start => SEEK_SET, cur => SEEK_CUR, end => SEEK_END,);
91              
92             my %open_map = (
93             r => O_RDONLY,
94             'r+' => O_RDWR,
95              
96             w => O_WRONLY | O_CREAT | O_TRUNC,
97             wx => O_WRONLY | O_CREAT | O_EXCL,
98             'w+' => O_RDWR | O_CREAT | O_TRUNC,
99             'wx+' => O_RDWR | O_CREAT | O_EXCL,
100             a => O_WRONLY | O_CREAT | O_APPEND,
101             ax => O_WRONLY | O_CREAT | O_APPEND | O_EXCL,
102              
103             'a+' => O_RDWR | O_CREAT | O_APPEND,
104             'ax+' => O_RDWR | O_CREAT | O_APPEND | O_EXCL,
105             );
106              
107             # self, fh, path, mode, perm?
108 129 50   129 1 3588 sub sysopen ($, $, $, $, @) {
  129         198  
109 129 100       543 croak "Bad mode $_[3]" unless exists $open_map{$_[3]};
110 128 100       396 &CORE::sysopen($_[1], $_[0]->path2real($_[2]), $open_map{$_[3]}, (defined($_[4]) ? $_[4] : ()))
    100          
111             or croak "sysopen: $!";
112             }
113              
114              
115 13 50   13 1 975 sub sysseek ($self, $fh, $pos, $whence = 'start') {
  13 50       36  
  13 100       26  
  13         22  
  13         23  
  13         37  
  13         23  
116 13 100       310 croak "Bad whence $whence" unless exists $seek_map{$whence};
117 12   33     81 &CORE::sysseek($fh, $pos, $seek_map{$whence}) // croak "Can't sysseek $!";
118             }
119              
120 73 50   73 1 280 sub syswrite ($, $, $, @) { # other lengh, scalar offset
  73         119  
121 73         117 shift;
122 73   66     1765 &CORE::syswrite(@_) // croak "Can't write: $!";
123             }
124              
125 49 50   49 1 766 sub sysread ($, $, $, $, @) { # @other = string offset
  49         78  
126 49         75 shift;
127 49   66     456 &CORE::sysread(@_) // croak "Can't read: $!";
128             }
129              
130 31 50   31 0 648 sub unlink ($self, $path) {
  31 50       91  
  31         69  
  31         66  
  31         53  
131 31 100       88 unlink $self->path2real($path) or croak "$path $!";
132             }
133              
134 7 50   7 0 36 sub remove_tree ($self, $path, $opts = {}) {
  7 50       24  
  7 100       18  
  7         16  
  7         27  
  7         13  
135 7         21 my $real = $self->path2real($path);
136 7 50       32 croak "remove_tree $real: Not a directory" unless $self->stat($path)->is_dir;
137 6         2222 File::Path::remove_tree($real, {%$opts, error => \my $err});
138 6 50       54 croak join('; ', map { $_->%* } @$err) if @$err; # TODO: test
  0         0  
139             }
140              
141 43 50   43 0 119 sub ls ($self, $path) {
  43 50       98  
  43         65  
  43         63  
  43         63  
142 43         96 my $real = $self->path2real($path);
143 43 100       1101 opendir(my $dh, $real) || croak "Can't opendir $real: $!";
144 42 100       607 my @result = grep { $_ ne '.' && $_ ne '..' } readdir($dh);
  149         590  
145 42         244 closedir $dh;
146 42         219 @result;
147             }
148              
149             my %flock_map = (
150             ex => LOCK_EX,
151             ex_nb => LOCK_EX | LOCK_NB,
152             sh => LOCK_SH,
153             sh_nb => LOCK_SH | LOCK_NB,
154             un => LOCK_UN
155             );
156              
157              
158 112 50   112 0 284 sub flock ($self, $fh, $flag) {
  112 50       242  
  112         166  
  112         158  
  112         170  
  112         159  
159 112 100       454 croak "Bad flag $flag" unless exists $flock_map{$flag};
160 111         502 my $res = flock($fh, $flock_map{$flag});
161 111 100 100     379 croak "$!" unless $res || $! == EAGAIN || $! == EWOULDBLOCK;
      66        
162 110         236 $res;
163             }
164              
165 62 50   62 0 323 sub open ($self, $path, $mode, @rest) {
  62         146  
  62         145  
  62         99  
  62         186  
  62         104  
166 62 50 66     1621 $self->make_tree((fileparse($path))[1]) unless ($mode eq 'r' && $mode eq 'r+');
167 62         313 $self->sysopen(my $fh, $path, $mode, @rest);
168 60         292 $fh;
169             }
170              
171              
172 2 50   2 1 17 sub append ($self, $path, $) {
  2 50       9  
  2         7  
  2         6  
  2         5  
173 2         7 my $fh = $self->open($path, 'a');
174 2         11 $self->flock($fh, 'ex');
175 2         46 $self->syswrite($fh, $_[2]);
176 2         18 $self->flock($fh, 'un');
177 2         22 CORE::close $fh;
178 2         7 return;
179             }
180              
181             # don't copy 3rd arg
182 39 50   39 1 239 sub write ($self, $path, $) {
  39 50       109  
  39         72  
  39         64  
  39         68  
183 39         97 my $fh = $self->open($path, 'w');
184 39         132 $self->flock($fh, 'ex');
185 39         120 $self->syswrite($fh, $_[2]);
186 39         146 $self->flock($fh, 'un');
187 39         242 CORE::close $fh;
188 39         130 return;
189             }
190              
191 18 50   18 1 59 sub read_ref ($self, $path) {
  18 50       55  
  18         37  
  18         39  
  18         35  
192 18         51 my $fh = $self->open($path, 'r');
193 18         71 $self->flock($fh, 'sh');
194 18         138 $self->sysread($fh, \my $buf, $self->stat($path)->size);
195 18         113 $self->flock($fh, 'un');
196 18         119 CORE::close $fh;
197 18         131 \$buf;
198             }
199              
200 17 50   17 1 466 sub read ($self, $path) {
  17 50       58  
  17         35  
  17         41  
  17         35  
201 17         56 $self->read_ref($path)->$*;
202             }
203              
204 10 50   10 1 196 sub write_many ($self, %map) {
  10 50       32  
  10         20  
  10         53  
  10         18  
205 10         94 $self->write($_, $map{$_}) for keys %map;
206 10         46 $self;
207             }
208              
209 3 50   3 1 28 sub find_files ($self, $start, $fhs_fn, $pick = undef) {
  3 50       13  
  3         7  
  3         6  
  3         5  
  3         5  
  3         4  
210 3         3 my %seen;
211 15 50   15   67 my $fn = sub ($path) {
  15 50       28  
  15         22  
  15         21  
212 15         30 my $stat = $self->stat($path);
213 15 100       49 return unless $stat->is_file;
214 6         19 $fhs_fn->($path);
215 3         10 };
216 3         9 $self->traverse($start, $fn, $pick);
217             }
218              
219             # make faster?
220 12 50   12 1 104 sub traverse ($self, $start, $fn, $pick_d = undef) {
  12 50       39  
  12         23  
  12         25  
  12         21  
  12         23  
  12         22  
221              
222 12 100       44 $start = [$start] unless ref $start eq 'ARRAY';
223 12         28 my %seen_dirs; # don't go into the same dir twice
224             my %seen_children; # don't fire the same file twice
225              
226 14         89 my @stack = map { Evo::Path->new(base => $_); } map {
227 12         37 my $path = $_;
  15         27  
228 15         37 my $stat = $self->stat($path);
229 15 100       177 $seen_dirs{($stat->dev, '-', $stat->ino)}++ ? () : ($path);
230             } reverse $start->@*;
231              
232 12         40 while (@stack) {
233 41         79 my $cur_dir = pop @stack;
234              
235 41         67 my (@dirs, @children);
236 41         106 foreach my $cur_child (sort $self->ls($cur_dir)) {
237              
238 63         214 my $path = $cur_dir->append($cur_child);
239 63 100       176 next unless $self->exists($path); # broken link
240 62         180 my $stat = $self->stat($path);
241              
242              
243             my $bool
244             = $stat->is_dir
245             && $stat->can_exec
246             && $stat->can_read
247 62   100     252 && !$seen_dirs{$stat->dev, '-', $stat->ino}++
248             && (!$pick_d || $pick_d->($path));
249              
250 62 100       513 unshift @dirs, $path if $bool;
251 62 100       446 push @children, $path if !$seen_children{$stat->dev, '-', $stat->ino}++;
252              
253             }
254 41         133 $fn->($_) for @children;
255 41         2568 push @stack, @dirs;
256             }
257             }
258              
259 8 50   8   28 my sub _copy_file ($self, $from, $to) {
  8 50       21  
  8         13  
  8         11  
  8         14  
  8         14  
260 8 50       20 File::Copy::cp $self->path2real($from), $self->path2real($to) or die "Copy failed: $!";
261             }
262              
263 4 50   4 1 27 sub copy_dir ($self, $from, $to) {
  4 50       15  
  4         9  
  4         9  
  4         8  
  4         9  
264 4         44 $to = Evo::Path->new(base => $to);
265 4         15 $self->make_tree($to);
266 4         14 my @stack = ($from);
267 4         15 while (@stack) {
268 4         13 my $cur_dir = shift @stack;
269 12         20 $self->traverse(
270             $cur_dir,
271 12 50   12   354 sub($path) {
  12 50       48  
  12         15  
272 12         34 my $stat = $self->stat($path);
273 12         60 my $dest = $to->append(join '/', $path->children->@*);
274 12 100       42 if ($stat->is_dir) {
    50          
275 5 100       17 $self->mkdir($dest) unless $self->exists($dest);
276             }
277             elsif ($stat->is_file) {
278 7         21 _copy_file($self, $path, $dest);
279             }
280              
281             #else { croak "Can't copy $path, not a dir neither a file"; }
282             }
283 4         39 );
284             }
285             }
286              
287 1 50   1 1 7 sub copy_file ($self, $from, $to) {
  1 50       6  
  1         2  
  1         2  
  1         2  
  1         2  
288 1         16 $self->make_tree((fileparse($to))[1]);
289 1         4 _copy_file($self, $from, $to);
290             }
291              
292             # ========= MODULE =========
293              
294             my $FSROOT = __PACKAGE__->new(root => '/');
295 8     8 1 26706 sub FSROOT : Export {$FSROOT}
  8     1   23  
  8         51  
  1         16  
296              
297              
298             1;
299              
300             __END__