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   3933 use Evo '-Export *; -Class; ::Stat; -Path; Carp croak; -Path';
  8         19  
  8         57  
3             die "Win isn't supported yet. Pull requests are welcome!" if $^O eq 'MSWin32';
4              
5 8     8   56 use Fcntl qw(:seek O_RDWR O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC O_APPEND O_EXCL :flock);
  8         16  
  8         1370  
6 8     8   62 use Evo 'File::Spec; File::Path; Cwd() abs_path; File::Basename fileparse; Symbol()';
  8         15  
  8         33  
7 8     8   3236 use Time::HiRes ();
  8         7751  
  8         186  
8 8     8   50 use Evo 'List::Util first; File::Copy ()';
  8         13  
  8         144  
9 8     8   57 use Errno qw(EAGAIN EWOULDBLOCK);
  8         18  
  8         947  
10 8     8   48 use Scalar::Util;
  8         15  
  8         1168  
11              
12             sub SKIP_HIDDEN : Export : prototype() {
13 7 50   7   67 sub($dir) {
  7 50       17  
  7         13  
  7         10  
14 7         48 my @dirs = File::Spec->splitdir($dir);
15 7         47 $dirs[-1] !~ /^\./;
16 5     5 1 45 };
17 8     8   50 }
  8         16  
  8         72  
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 18 sub cd ($self, $rel) {
  2 50       9  
  2         5  
  2         6  
  2         4  
28 2         19 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 1241 sub path2real ($self, $rel) {
  568 50       1147  
  568         826  
  568         1007  
  568         726  
33 568         2444 Evo::Path->from_string($rel, $self->root . '')->to_string;
34             }
35              
36 78 50   78 0 742 sub exists ($self, $path) {
  78 50       178  
  78         131  
  78         111  
  78         107  
37 78         158 -e $self->path2real($path);
38             }
39              
40              
41 14 50   14 0 105 sub mkdir ($self, $path, $perm = undef) {
  14 50       49  
  14         29  
  14         28  
  14         29  
  14         26  
42 14         39 my $real = $self->path2real($path);
43 14 100       1095 &CORE::mkdir($real, defined $perm ? $perm : ()) or croak "$real: $!";
    100          
44             }
45              
46 72 50   72 0 318 sub make_tree ($self, $path, $perms = undef) {
  72 50       242  
  72         161  
  72         128  
  72         111  
  72         109  
47 72         178 my $real = $self->path2real($path);
48 72         334 my %opts = (error => \my $err);
49 72 50       200 $opts{chmod} = $perms if defined $perms;
50 72         7431 File::Path::make_path($real, \%opts);
51 72 100       389 croak join('; ', map { $_->%* } @$err) if @$err; # TODO: test
  1         130  
52             }
53              
54 7 50   7 0 54 sub symlink ($self, $to_path, $link_path) {
  7 50       23  
  7         14  
  7         13  
  7         13  
  7         12  
55 7 100       21 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 26 sub link ($self, $to_path, $link_path) {
  3 50       34  
  3         7  
  3         6  
  3         5  
  3         8  
60 3 50       9 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       16  
  4         9  
  4         8  
  4         8  
65 4         11 -l $self->path2real($path);
66             }
67              
68              
69 2 50   2 0 26 sub utimes ($self, $path, $atime = undef, $mtime = undef) {
  2 50       11  
  2         8  
  2         6  
  2         4  
  2         5  
  2         7  
70 2         7 my $real = $self->path2real($path);
71 2 100 100     239 utime($atime // undef, $mtime // undef, $real) or croak "utimes $path: $!";
      100        
72             }
73              
74 55 50   55 0 1614 sub close ($self, $fh) {
  55 50       131  
  55         99  
  55         80  
  55         75  
75 55         396 close $fh;
76             }
77              
78 143 50   143 1 367 sub stat ($self, $path) {
  143 50       310  
  143         244  
  143         210  
  143         199  
79 143         206 my %opts;
80 143 100       288 my @stat = Time::HiRes::stat $self->path2real($path) or croak "stat $path: $!";
81 141         969 @opts{qw(dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks)} = @stat;
82 141         2578 Evo::Fs::Stat->new(%opts, _data => \@stat);
83             }
84              
85 1 50   1 1 14 sub rename ($self, $old, $new) {
  1 50       6  
  1         3  
  1         5  
  1         4  
  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 3376 sub sysopen ($, $, $, $, @) {
  129         185  
109 129 100       519 croak "Bad mode $_[3]" unless exists $open_map{$_[3]};
110 128 100       379 &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 1159 sub sysseek ($self, $fh, $pos, $whence = 'start') {
  13 50       33  
  13 100       59  
  13         23  
  13         21  
  13         36  
  13         21  
116 13 100       269 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 272 sub syswrite ($, $, $, @) { # other lengh, scalar offset
  73         107  
121 73         127 shift;
122 73   66     1641 &CORE::syswrite(@_) // croak "Can't write: $!";
123             }
124              
125 49 50   49 1 825 sub sysread ($, $, $, $, @) { # @other = string offset
  49         80  
126 49         81 shift;
127 49   66     438 &CORE::sysread(@_) // croak "Can't read: $!";
128             }
129              
130 31 50   31 0 897 sub unlink ($self, $path) {
  31 50       81  
  31         63  
  31         59  
  31         48  
131 31 100       82 unlink $self->path2real($path) or croak "$path $!";
132             }
133              
134 7 50   7 0 41 sub remove_tree ($self, $path, $opts = {}) {
  7 50       25  
  7 100       17  
  7         18  
  7         27  
  7         17  
135 7         28 my $real = $self->path2real($path);
136 7 50       36 croak "remove_tree $real: Not a directory" unless $self->stat($path)->is_dir;
137 6         2262 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 118 sub ls ($self, $path) {
  43 50       95  
  43         72  
  43         55  
  43         63  
142 43         88 my $real = $self->path2real($path);
143 43 100       1125 opendir(my $dh, $real) || croak "Can't opendir $real: $!";
144 42 100       609 my @result = grep { $_ ne '.' && $_ ne '..' } readdir($dh);
  149         603  
145 42         230 closedir $dh;
146 42         221 @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 314 sub flock ($self, $fh, $flag) {
  112 50       260  
  112         203  
  112         169  
  112         192  
  112         177  
159 112 100       492 croak "Bad flag $flag" unless exists $flock_map{$flag};
160 111         559 my $res = flock($fh, $flock_map{$flag});
161 111 100 100     451 croak "$!" unless $res || $! == EAGAIN || $! == EWOULDBLOCK;
      66        
162 110         200 $res;
163             }
164              
165 62 50   62 0 283 sub open ($self, $path, $mode, @rest) {
  62         150  
  62         98  
  62         187  
  62         188  
  62         99  
166 62 50 66     1389 $self->make_tree((fileparse($path))[1]) unless ($mode eq 'r' && $mode eq 'r+');
167 62         257 $self->sysopen(my $fh, $path, $mode, @rest);
168 60         566 $fh;
169             }
170              
171              
172 2 50   2 1 10 sub append ($self, $path, $) {
  2 50       5  
  2         3  
  2         4  
  2         2  
173 2         5 my $fh = $self->open($path, 'a');
174 2         8 $self->flock($fh, 'ex');
175 2         7 $self->syswrite($fh, $_[2]);
176 2         8 $self->flock($fh, 'un');
177 2         15 CORE::close $fh;
178 2         4 return;
179             }
180              
181             # don't copy 3rd arg
182 39 50   39 1 212 sub write ($self, $path, $) {
  39 50       94  
  39         60  
  39         67  
  39         59  
183 39         117 my $fh = $self->open($path, 'w');
184 39         131 $self->flock($fh, 'ex');
185 39         125 $self->syswrite($fh, $_[2]);
186 39         129 $self->flock($fh, 'un');
187 39         236 CORE::close $fh;
188 39         127 return;
189             }
190              
191 18 50   18 1 55 sub read_ref ($self, $path) {
  18 50       53  
  18         35  
  18         37  
  18         30  
192 18         59 my $fh = $self->open($path, 'r');
193 18         97 $self->flock($fh, 'sh');
194 18         80 $self->sysread($fh, \my $buf, $self->stat($path)->size);
195 18         112 $self->flock($fh, 'un');
196 18         151 CORE::close $fh;
197 18         150 \$buf;
198             }
199              
200 17 50   17 1 391 sub read ($self, $path) {
  17 50       57  
  17         35  
  17         37  
  17         36  
201 17         59 $self->read_ref($path)->$*;
202             }
203              
204 10 50   10 1 175 sub write_many ($self, %map) {
  10 50       35  
  10         18  
  10         53  
  10         18  
205 10         51 $self->write($_, $map{$_}) for keys %map;
206 10         41 $self;
207             }
208              
209 3 50   3 1 29 sub find_files ($self, $start, $fhs_fn, $pick = undef) {
  3 50       7  
  3         5  
  3         6  
  3         4  
  3         8  
  3         5  
210 3         5 my %seen;
211 15 50   15   63 my $fn = sub ($path) {
  15 50       31  
  15         21  
  15         23  
212 15         28 my $stat = $self->stat($path);
213 15 100       43 return unless $stat->is_file;
214 6         17 $fhs_fn->($path);
215 3         8 };
216 3         9 $self->traverse($start, $fn, $pick);
217             }
218              
219             # make faster?
220 12 50   12 1 84 sub traverse ($self, $start, $fn, $pick_d = undef) {
  12 50       42  
  12         19  
  12         21  
  12         17  
  12         19  
  12         21  
221              
222 12 100       42 $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         80 my @stack = map { Evo::Path->new(base => $_); } map {
227 12         30 my $path = $_;
  15         24  
228 15         40 my $stat = $self->stat($path);
229 15 100       165 $seen_dirs{($stat->dev, '-', $stat->ino)}++ ? () : ($path);
230             } reverse $start->@*;
231              
232 12         39 while (@stack) {
233 41         78 my $cur_dir = pop @stack;
234              
235 41         63 my (@dirs, @children);
236 41         102 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         181 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     195 && !$seen_dirs{$stat->dev, '-', $stat->ino}++
248             && (!$pick_d || $pick_d->($path));
249              
250 62 100       508 unshift @dirs, $path if $bool;
251 62 100       406 push @children, $path if !$seen_children{$stat->dev, '-', $stat->ino}++;
252              
253             }
254 41         129 $fn->($_) for @children;
255 41         2499 push @stack, @dirs;
256             }
257             }
258              
259 8 50   8   25 my sub _copy_file ($self, $from, $to) {
  8 50       19  
  8         15  
  8         14  
  8         13  
  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 29 sub copy_dir ($self, $from, $to) {
  4 50       16  
  4         10  
  4         10  
  4         7  
  4         9  
264 4         47 $to = Evo::Path->new(base => $to);
265 4         16 $self->make_tree($to);
266 4         15 my @stack = ($from);
267 4         17 while (@stack) {
268 4         9 my $cur_dir = shift @stack;
269 12         22 $self->traverse(
270             $cur_dir,
271 12 50   12   308 sub($path) {
  12 50       46  
  12         19  
272 12         27 my $stat = $self->stat($path);
273 12         65 my $dest = $to->append(join '/', $path->children->@*);
274 12 100       61 if ($stat->is_dir) {
    50          
275 5 100       13 $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         42 );
284             }
285             }
286              
287 1 50   1 1 6 sub copy_file ($self, $from, $to) {
  1 50       5  
  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 23478 sub FSROOT : Export {$FSROOT}
  8     1   22  
  8         51  
  1         16  
296              
297              
298             1;
299              
300             __END__