File Coverage

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


line stmt bran cond sub pod time code
1             package Evo::Fs;
2 8     8   3988 use Evo '-Export *; -Class; ::Stat; -Path; Carp croak; -Path';
  8         19  
  8         53  
3             die "Win isn't supported yet. Pull requests are welcome!" if $^O eq 'MSWin32';
4              
5 8     8   57 use Fcntl qw(:seek O_RDWR O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC O_APPEND O_EXCL :flock);
  8         16  
  8         1318  
6 8     8   47 use Evo 'File::Spec; File::Path; Cwd() abs_path; File::Basename fileparse; Symbol()';
  8         15  
  8         29  
7 8     8   3005 use Time::HiRes ();
  8         7501  
  8         234  
8 8     8   48 use Evo 'List::Util first; File::Copy ()';
  8         17  
  8         121  
9 8     8   50 use Errno qw(EAGAIN EWOULDBLOCK);
  8         15  
  8         983  
10 8     8   284 use Scalar::Util;
  8         19  
  8         1086  
11              
12             sub SKIP_HIDDEN : Export : prototype() {
13 7 50   7   70 sub($dir) {
  7 50       18  
  7         11  
  7         11  
14 7         41 my @dirs = File::Spec->splitdir($dir);
15 7         39 $dirs[-1] !~ /^\./;
16 5     5 1 37 };
17 8     8   121 }
  8         16  
  8         60  
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 14 sub cd ($self, $rel) {
  2 50       9  
  2         5  
  2         5  
  2         4  
28 2         14 my $root = Evo::Path->from_string('', $self->root . '')->append_unsafe($rel)->to_string;
29 2         23 ref($self)->new(root => $root);
30             }
31              
32 568 50   568 1 1188 sub path2real ($self, $rel) {
  568 50       1095  
  568         793  
  568         831  
  568         718  
33 568         2474 Evo::Path->from_string($rel, $self->root . '')->to_string;
34             }
35              
36 78 50   78 0 588 sub exists ($self, $path) {
  78 50       172  
  78         109  
  78         111  
  78         112  
37 78         140 -e $self->path2real($path);
38             }
39              
40              
41 14 50   14 0 94 sub mkdir ($self, $path, $perm = undef) {
  14 50       44  
  14         26  
  14         26  
  14         27  
  14         23  
42 14         32 my $real = $self->path2real($path);
43 14 100       904 &CORE::mkdir($real, defined $perm ? $perm : ()) or croak "$real: $!";
    100          
44             }
45              
46 72 50   72 0 196 sub make_tree ($self, $path, $perms = undef) {
  72 50       158  
  72         106  
  72         119  
  72         102  
  72         86  
47 72         151 my $real = $self->path2real($path);
48 72         277 my %opts = (error => \my $err);
49 72 50       175 $opts{chmod} = $perms if defined $perms;
50 72         6363 File::Path::make_path($real, \%opts);
51 72 100       362 croak join('; ', map { $_->%* } @$err) if @$err; # TODO: test
  1         86  
52             }
53              
54 7 50   7 0 51 sub symlink ($self, $to_path, $link_path) {
  7 50       21  
  7         12  
  7         17  
  7         13  
  7         12  
55 7 100       16 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 30 sub link ($self, $to_path, $link_path) {
  3 50       13  
  3         8  
  3         8  
  3         6  
  3         9  
60 3 50       8 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 16 sub is_symlink ($self, $path) {
  4 50       10  
  4         6  
  4         9  
  4         5  
65 4         10 -l $self->path2real($path);
66             }
67              
68              
69 2 50   2 0 19 sub utimes ($self, $path, $atime = undef, $mtime = undef) {
  2 50       36  
  2         6  
  2         3  
  2         6  
  2         4  
  2         5  
70 2         6 my $real = $self->path2real($path);
71 2 100 100     150 utime($atime // undef, $mtime // undef, $real) or croak "utimes $path: $!";
      100        
72             }
73              
74 55 50   55 0 1471 sub close ($self, $fh) {
  55 50       122  
  55         107  
  55         82  
  55         76  
75 55         364 close $fh;
76             }
77              
78 143 50   143 1 325 sub stat ($self, $path) {
  143 50       291  
  143         199  
  143         205  
  143         178  
79 143         190 my %opts;
80 143 100       278 my @stat = Time::HiRes::stat $self->path2real($path) or croak "stat $path: $!";
81 141         822 @opts{qw(dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks)} = @stat;
82 141         2272 Evo::Fs::Stat->new(%opts, _data => \@stat);
83             }
84              
85 1 50   1 1 10 sub rename ($self, $old, $new) {
  1 50       4  
  1         3  
  1         2  
  1         3  
  1         2  
86 1 50       2 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 2970 sub sysopen ($, $, $, $, @) {
  129         193  
109 129 100       466 croak "Bad mode $_[3]" unless exists $open_map{$_[3]};
110 128 100       309 &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 903 sub sysseek ($self, $fh, $pos, $whence = 'start') {
  13 50       43  
  13 100       24  
  13         19  
  13         20  
  13         35  
  13         19  
116 13 100       188 croak "Bad whence $whence" unless exists $seek_map{$whence};
117 12   33     76 &CORE::sysseek($fh, $pos, $seek_map{$whence}) // croak "Can't sysseek $!";
118             }
119              
120 73 50   73 1 252 sub syswrite ($, $, $, @) { # other lengh, scalar offset
  73         107  
121 73         109 shift;
122 73   66     1768 &CORE::syswrite(@_) // croak "Can't write: $!";
123             }
124              
125 49 50   49 1 674 sub sysread ($, $, $, $, @) { # @other = string offset
  49         74  
126 49         71 shift;
127 49   66     409 &CORE::sysread(@_) // croak "Can't read: $!";
128             }
129              
130 31 50   31 0 545 sub unlink ($self, $path) {
  31 50       77  
  31         50  
  31         51  
  31         53  
131 31 100       74 unlink $self->path2real($path) or croak "$path $!";
132             }
133              
134 7 50   7 0 29 sub remove_tree ($self, $path, $opts = {}) {
  7 50       23  
  7 100       12  
  7         13  
  7         24  
  7         13  
135 7         18 my $real = $self->path2real($path);
136 7 50       27 croak "remove_tree $real: Not a directory" unless $self->stat($path)->is_dir;
137 6         1893 File::Path::remove_tree($real, {%$opts, error => \my $err});
138 6 50       52 croak join('; ', map { $_->%* } @$err) if @$err; # TODO: test
  0         0  
139             }
140              
141 43 50   43 0 114 sub ls ($self, $path) {
  43 50       88  
  43         64  
  43         59  
  43         56  
142 43         86 my $real = $self->path2real($path);
143 43 100       1001 opendir(my $dh, $real) || croak "Can't opendir $real: $!";
144 42 100       552 my @result = grep { $_ ne '.' && $_ ne '..' } readdir($dh);
  149         557  
145 42         227 closedir $dh;
146 42         218 @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 279 sub flock ($self, $fh, $flag) {
  112 50       230  
  112         212  
  112         142  
  112         163  
  112         153  
159 112 100       420 croak "Bad flag $flag" unless exists $flock_map{$flag};
160 111         446 my $res = flock($fh, $flock_map{$flag});
161 111 100 100     373 croak "$!" unless $res || $! == EAGAIN || $! == EWOULDBLOCK;
      66        
162 110         182 $res;
163             }
164              
165 62 50   62 0 239 sub open ($self, $path, $mode, @rest) {
  62         98  
  62         96  
  62         135  
  62         167  
  62         100  
166 62 50 66     1176 $self->make_tree((fileparse($path))[1]) unless ($mode eq 'r' && $mode eq 'r+');
167 62         215 $self->sysopen(my $fh, $path, $mode, @rest);
168 60         279 $fh;
169             }
170              
171              
172 2 50   2 1 12 sub append ($self, $path, $) {
  2 50       5  
  2         3  
  2         4  
  2         3  
173 2         9 my $fh = $self->open($path, 'a');
174 2         11 $self->flock($fh, 'ex');
175 2         10 $self->syswrite($fh, $_[2]);
176 2         9 $self->flock($fh, 'un');
177 2         18 CORE::close $fh;
178 2         8 return;
179             }
180              
181             # don't copy 3rd arg
182 39 50   39 1 211 sub write ($self, $path, $) {
  39 50       91  
  39         60  
  39         63  
  39         58  
183 39         92 my $fh = $self->open($path, 'w');
184 39         120 $self->flock($fh, 'ex');
185 39         105 $self->syswrite($fh, $_[2]);
186 39         119 $self->flock($fh, 'un');
187 39         223 CORE::close $fh;
188 39         115 return;
189             }
190              
191 18 50   18 1 44 sub read_ref ($self, $path) {
  18 50       46  
  18         26  
  18         27  
  18         30  
192 18         41 my $fh = $self->open($path, 'r');
193 18         53 $self->flock($fh, 'sh');
194 18         54 $self->sysread($fh, \my $buf, $self->stat($path)->size);
195 18         87 $self->flock($fh, 'un');
196 18         102 CORE::close $fh;
197 18         102 \$buf;
198             }
199              
200 17 50   17 1 364 sub read ($self, $path) {
  17 50       49  
  17         30  
  17         29  
  17         23  
201 17         40 $self->read_ref($path)->$*;
202             }
203              
204 10 50   10 1 170 sub write_many ($self, %map) {
  10 50       35  
  10         19  
  10         52  
  10         18  
205 10         48 $self->write($_, $map{$_}) for keys %map;
206 10         40 $self;
207             }
208              
209 3 50   3 1 36 sub find_files ($self, $start, $fhs_fn, $pick = undef) {
  3 50       8  
  3         7  
  3         6  
  3         5  
  3         4  
  3         5  
210 3         5 my %seen;
211 15 50   15   91 my $fn = sub ($path) {
  15 50       31  
  15         24  
  15         17  
212 15         32 my $stat = $self->stat($path);
213 15 100       50 return unless $stat->is_file;
214 6         19 $fhs_fn->($path);
215 3         11 };
216 3         10 $self->traverse($start, $fn, $pick);
217             }
218              
219             # make faster?
220 12 50   12 1 88 sub traverse ($self, $start, $fn, $pick_d = undef) {
  12 50       31  
  12         21  
  12         16  
  12         26  
  12         19  
  12         18  
221              
222 12 100       96 $start = [$start] unless ref $start eq 'ARRAY';
223 12         23 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         80 my @stack = map { Evo::Path->new(base => $_); } map {
227 12         36 my $path = $_;
  15         27  
228 15         33 my $stat = $self->stat($path);
229 15 100       161 $seen_dirs{($stat->dev, '-', $stat->ino)}++ ? () : ($path);
230             } reverse $start->@*;
231              
232 12         42 while (@stack) {
233 41         73 my $cur_dir = pop @stack;
234              
235 41         59 my (@dirs, @children);
236 41         105 foreach my $cur_child (sort $self->ls($cur_dir)) {
237              
238 63         188 my $path = $cur_dir->append($cur_child);
239 63 100       166 next unless $self->exists($path); # broken link
240 62         158 my $stat = $self->stat($path);
241              
242              
243             my $bool
244             = $stat->is_dir
245             && $stat->can_exec
246             && $stat->can_read
247 62   66     203 && !$seen_dirs{$stat->dev, '-', $stat->ino}++
248             && (!$pick_d || $pick_d->($path));
249              
250 62 100       480 unshift @dirs, $path if $bool;
251 62 100       435 push @children, $path if !$seen_children{$stat->dev, '-', $stat->ino}++;
252              
253             }
254 41         140 $fn->($_) for @children;
255 41         2099 push @stack, @dirs;
256             }
257             }
258              
259 8 50   8   23 my sub _copy_file ($self, $from, $to) {
  8 50       18  
  8         13  
  8         11  
  8         11  
  8         11  
260 8 50       18 File::Copy::cp $self->path2real($from), $self->path2real($to) or die "Copy failed: $!";
261             }
262              
263 4 50   4 1 24 sub copy_dir ($self, $from, $to) {
  4 50       11  
  4         8  
  4         8  
  4         8  
  4         5  
264 4         29 $to = Evo::Path->new(base => $to);
265 4         12 $self->make_tree($to);
266 4         11 my @stack = ($from);
267 4         12 while (@stack) {
268 4         9 my $cur_dir = shift @stack;
269 12         18 $self->traverse(
270             $cur_dir,
271 12 50   12   273 sub($path) {
  12 50       24  
  12         19  
272 12         22 my $stat = $self->stat($path);
273 12         53 my $dest = $to->append(join '/', $path->children->@*);
274 12 100       37 if ($stat->is_dir) {
    50          
275 5 100       13 $self->mkdir($dest) unless $self->exists($dest);
276             }
277             elsif ($stat->is_file) {
278 7         19 _copy_file($self, $path, $dest);
279             }
280              
281             #else { croak "Can't copy $path, not a dir neither a file"; }
282             }
283 4         28 );
284             }
285             }
286              
287 1 50   1 1 5 sub copy_file ($self, $from, $to) {
  1 50       4  
  1         2  
  1         2  
  1         2  
  1         1  
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 22309 sub FSROOT : Export {$FSROOT}
  8     1   17  
  8         37  
  1         12  
296              
297              
298             1;
299              
300             __END__