File Coverage

blib/lib/App/DistSync/Util.pm
Criterion Covered Total %
statement 39 230 16.9
branch 0 106 0.0
condition 0 64 0.0
subroutine 13 27 48.1
pod 12 12 100.0
total 64 439 14.5


line stmt bran cond sub pod time code
1             package App::DistSync::Util;
2 1     1   8 use strict;
  1         2  
  1         42  
3 1     1   5 use warnings;
  1         1  
  1         50  
4 1     1   4 use utf8;
  1         1  
  1         5  
5              
6             =encoding utf8
7              
8             =head1 NAME
9              
10             App::DistSync::Util - The App::DistSync utilities
11              
12             =head1 SYNOPSIS
13              
14             use App::DistSync::Util;
15              
16             =head1 DESCRIPTION
17              
18             Exported utility functions
19              
20             =head2 debug
21              
22             debug("Foo bar baz");
23             debug("Foo %s baz", "bar");
24             debug("Foo %s %s", "bar", "baz");
25              
26             Show debug information to STDERR
27              
28             =head2 fdelete
29              
30             my $status = fdelete( $file );
31              
32             Deleting a file if it exists
33              
34             =head2 manifind
35              
36             my $files_struct = manifind($dir); # { ... }
37              
38             Read direactory and returns file structure
39              
40             =head2 maniread
41              
42             my $mani_struct = maniread($file, skipflag); # { ... }
43              
44             Read file as manifest and returns hash structure
45              
46             =head2 maniwrite
47              
48             maniwrite($file, $mani_struct);
49              
50             This function writes manifest structure to manifest file
51              
52             =head2 qrreconstruct
53              
54             my $r = qrreconstruct('!!perl/regexp (?i-xsm:^\s*(error|fault|no))');
55             # Translate to:
56             # qr/^\s*(error|fault|no)/i
57              
58             Returns regular expression (QR) by perl/regexp string. YAML form of definition
59              
60             my $r = qrreconstruct('perl/regexp (?i-xsm:^\s*(error|fault|no))');
61             # Translate to:
62             # qr/^\s*(error|fault|no)/i
63              
64             Not-YAML form of definition
65              
66             my $r = qrreconstruct('regexp (?i-xsm:^\s*(error|fault|no))');
67             # Translate to:
68             # qr/^\s*(error|fault|no)/i
69              
70             Short form of definition
71              
72             See also L of L
73              
74             =head2 read_yaml
75              
76             my $yaml = read_yaml($yaml_file);
77              
78             Read YAML file
79              
80             =head2 slurp
81              
82             my $data = slurp($file, %args);
83             my $data = slurp($file, { %args });
84             slurp($file, { buffer => \my $data });
85             my $data = slurp($file, { binmode => ":raw:utf8" });
86              
87             Reads file $filename into a scalar
88              
89             my $data = slurp($file, { binmode => ":unix" });
90              
91             Reads file in fast, unbuffered, raw mode
92              
93             my $data = slurp($file, { binmode => ":unix:encoding(UTF-8)" });
94              
95             Reads file with UTF-8 encoding
96              
97             By default it returns this scalar. Can optionally take these named arguments:
98              
99             =over 4
100              
101             =item binmode
102              
103             Set the layers to read the file with. The default will be something sensible on your platform
104              
105             =item block_size
106              
107             Set the buffered block size in bytes, default to 1048576 bytes (1 MiB)
108              
109             =item buffer
110              
111             Pass a reference to a scalar to read the file into, instead of returning it by value.
112             This has performance benefits
113              
114             =back
115              
116             See also L to writing data to file
117              
118             =head2 spew
119              
120             spew($file, $data, %args);
121             spew($file, $data, { %args });
122             spew($file, \$data, { %args });
123             spew($file, \@data, { %args });
124             spew($file, $data, { binmode => ":raw:utf8" });
125              
126             Writes data to a file atomically. The only argument is C, which is passed to
127             C on the handle used for writing.
128              
129             Can optionally take these named arguments:
130              
131             =over 4
132              
133             =item append
134              
135             This argument is a boolean option, defaulted to false (C<0>).
136             Setting this argument to true (C<1>) will cause the data to be be written at the end of the current file.
137             Internally this sets the sysopen mode flag C
138              
139             =item binmode
140              
141             Set the layers to write the file with. The default will be something sensible on your platform
142              
143             =item locked
144              
145             This argument is a boolean option, defaulted to false (C<0>).
146             Setting this argument to true (C<1>) will ensure an that existing file will not be overwritten
147              
148             =item mode
149              
150             This numeric argument sets the default mode of opening files to write.
151             By default this argument to C<(O_WRONLY | O_CREAT)>.
152             Please DO NOT set this argument unless really necessary!
153              
154             =item perms
155              
156             This argument sets the permissions of newly-created files.
157             This value is modified by your process's umask and defaults to 0666 (same as sysopen)
158              
159             =back
160              
161             See also L to reading data from file
162              
163             =head2 tms
164              
165             print tms();
166              
167             This function returns current time and PID in format, for eg.:
168              
169             [Sat Dec 6 19:09:54 2025] [533052]
170              
171             =head2 touch
172              
173             touch( "file" ) or die "Can't touch file";
174              
175             Makes file exist, with current timestamp
176              
177             See L
178              
179             =head2 write_yaml
180              
181             write_yaml($yaml_file, $yaml);
182              
183             Write YAML file
184              
185             =head1 HISTORY
186              
187             See C file
188              
189             =head1 TO DO
190              
191             See C file
192              
193             =head1 SEE ALSO
194              
195             L
196              
197             =head1 AUTHOR
198              
199             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
200              
201             =head1 COPYRIGHT
202              
203             Copyright (C) 1998-2026 D&D Corporation
204              
205             =head1 LICENSE
206              
207             This program is distributed under the terms of the Artistic License Version 2.0
208              
209             See the C file or L for details
210              
211             =cut
212              
213 1     1   55 use Carp;
  1         2  
  1         176  
214              
215             our $DEBUG //= !!$ENV{DISTSYNC_DEBUG};
216              
217 1     1   405 use IO::File qw//;
  1         8468  
  1         36  
218 1     1   559 use POSIX qw/ :fcntl_h /;
  1         6670  
  1         5  
219 1     1   1620 use Fcntl qw/ O_WRONLY O_CREAT O_APPEND O_EXCL SEEK_END /;
  1         2  
  1         86  
220 1     1   7 use File::Spec;
  1         1  
  1         37  
221 1     1   5 use File::Find;
  1         1  
  1         48  
222 1     1   4 use File::Path;
  1         1  
  1         75  
223 1     1   769 use YAML::Tiny;
  1         6319  
  1         101  
224              
225 1     1   18 use base qw/Exporter/;
  1         2  
  1         362  
226             our @EXPORT = (qw/
227             debug tms
228             /);
229             our @EXPORT_OK = (qw/
230             qrreconstruct
231             touch slurp spew
232             fdelete
233             read_yaml write_yaml
234             maniread manifind maniwrite
235             /, @EXPORT);
236              
237             use constant {
238             QRTYPES => {
239 0           '' => sub { qr{$_[0]} },
240 0           x => sub { qr{$_[0]}x },
241 0           i => sub { qr{$_[0]}i },
242 0           s => sub { qr{$_[0]}s },
243 0           m => sub { qr{$_[0]}m },
244 0           ix => sub { qr{$_[0]}ix },
245 0           sx => sub { qr{$_[0]}sx },
246 0           mx => sub { qr{$_[0]}mx },
247 0           si => sub { qr{$_[0]}si },
248 0           mi => sub { qr{$_[0]}mi },
249 0           ms => sub { qr{$_[0]}sm },
250 0           six => sub { qr{$_[0]}six },
251 0           mix => sub { qr{$_[0]}mix },
252 0           msx => sub { qr{$_[0]}msx },
253 0           msi => sub { qr{$_[0]}msi },
254 0           msix => sub { qr{$_[0]}msix },
255             },
256 1     1   6 };
  1         10  
  1         2457  
257              
258             sub debug {
259 0 0   0 1   return unless $DEBUG;
260 0 0         my $txt = (scalar(@_) == 1) ? shift(@_) : sprintf(shift(@_), @_);
261 0           warn $txt, "\n";
262 0           return 1;
263             }
264 0     0 1   sub tms { sprintf "[%s] [%d]", scalar(localtime(time())), $$ }
265             sub qrreconstruct { # See app/paysrelay
266             # Returns regular expression (QR)
267             # Gets from YAML::Type::regexp of YAML::Types
268             # To input:
269             # !!perl/regexp (?i-xsm:^\s*(error|fault|no))
270             # Translate to:
271             # qr/^\s*(error|fault|no)/i
272 0     0 1   my $v = shift;
273 0 0         return undef unless defined $v;
274 0 0         return $v unless $v =~ /^\s*\!{0,2}(perl\/)?regexp\s*/i;
275 0           $v =~ s/\s*\!{0,2}(perl\/)?regexp\s*//i;
276 0 0         return qr{$v} unless $v =~ /^\(\?([\^\-uxism]*):(.*)\)\z/s;
277 0           my ($flags, $re) = ($1, $2);
278 0           $flags =~ s/-.*//; # remove all after '-'
279 0           $flags =~ s/^\^//; # remove start-symbol
280 0           $flags =~ tr/u//d; # remove u modifier
281 0   0 0     my $sub = QRTYPES->{$flags} || sub { qr{$_[0]} };
  0            
282 0           return $sub->($re);
283             }
284             sub slurp {
285 0   0 0 1   my $file = shift // '';
286 0 0         my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 0          
287 0 0 0       return unless length($file) && -r $file;
288 0           my $cleanup = 1;
289             # Open filehandle
290 0           my $fh;
291 0 0         if (ref($file)) {
292 0           $fh = $file;
293 0           $cleanup = 0; # Disable closing filehandle for passed filehandle
294             } else {
295 0           $fh = IO::File->new($file, "r");
296 0 0         unless (defined $fh) {
297 0           carp qq/Can't open file "$file": $!/;
298 0           return;
299             }
300             }
301             # Set binmode layer
302 0   0       my $bm = $args->{binmode} // ':raw'; # read in :raw by default
303 0           $fh->binmode($bm);
304             # Set buffer
305 0           my $buf;
306 0   0       my $buf_ref = $args->{buffer} // \$buf;
307 0           ${$buf_ref} = ''; # Set empty string to buffer
  0            
308 0   0       my $blk_size = $args->{block_size} || 1024 * 1024; # Set block size (1 MiB)
309             # Read whole file
310 0           my ($pos, $ret) = (0, 0);
311 0           while ($ret = $fh->read(${$buf_ref}, $blk_size, $pos)) {
  0            
312 0 0         $pos += $ret if defined $ret;
313             }
314 0 0         unless (defined $ret) {
315 0           carp qq/Can't read from file "$file": $!/;
316 0           return;
317             }
318             # Close filehandle
319 0 0         $fh->close if $cleanup; # automatically closes the file
320             # Return content if no buffer specified
321 0 0         return if defined $args->{buffer};
322 0           return ${$buf_ref};
  0            
323             }
324             sub spew {
325 0   0 0 1   my $file = shift // '';
326 0   0       my $data = shift // '';
327 0 0         my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 0          
328 0           my $cleanup = 1;
329             # Get binmode layer, mode and perms
330 0   0       my $bm = $args->{binmode} // ':raw'; # read in :raw by default
331 0   0       my $perms = $args->{perms} // 0666; # set file permissions
332 0   0       my $mode = $args->{mode} // O_WRONLY | O_CREAT;
333 0 0         $mode |= O_APPEND if $args->{append};
334 0 0         $mode |= O_EXCL if $args->{locked};
335             # Open filehandle
336 0           my $fh;
337 0 0         if (ref($file)) {
338 0           $fh = $file;
339 0           $cleanup = 0; # Disable closing filehandle for passed filehandle
340             } else {
341 0           $fh = IO::File->new($file, $mode, $perms);
342 0 0         unless (defined $fh) {
343 0           carp qq/Can't open file "$file": $!/;
344 0           return;
345             }
346             }
347             # Set binmode layer
348 0           $fh->binmode($bm);
349             # Set buffer
350 0           my $buf;
351 0           my $buf_ref = \$buf;
352 0 0         if (ref($data) eq 'SCALAR') {
    0          
353 0           $buf_ref = $data;
354             } elsif (ref($data) eq 'ARRAY') {
355 0           ${$buf_ref} = join '', @$data;
  0            
356             } else {
357 0           $buf_ref = \$data;
358             }
359             # Seek, print, truncate and close
360 0 0         $fh->seek(0, SEEK_END) if $args->{append}; # SEEK_END == 2
361 0 0         $fh->print(${$buf_ref}) or return;
  0            
362 0 0         $fh->truncate($fh->tell) if $cleanup;
363 0 0         $fh->close if $cleanup;
364 0           return 1;
365             }
366             sub touch {
367 0   0 0 1   my $fn = shift // '';
368 0 0         return 0 unless length($fn);
369 0           my $t = time;
370 0           my $ostat = open my $fh, '>>', $fn;
371 0 0         unless ($ostat) {
372 0           printf STDERR "Can't touch file \"%s\": %s\n", $fn, $!;
373 0           return 0;
374             }
375 0 0         close $fh if $ostat;
376 0           utime($t, $t, $fn);
377 0           return 1;
378             }
379             sub fdelete {
380 0     0 1   my $file = shift;
381 0 0 0       return 0 unless defined $file && -e $file;
382 0 0         unless (unlink($file)) {
383 0           printf STDERR "Can't delete file \"%s\": %s\n", $file, $!;
384 0           return 0;
385             }
386 0           return 1;
387             }
388             sub read_yaml {
389 0     0 1   my $file = shift;
390 0 0         return [] unless defined $file;
391 0 0 0       return [] unless (-e $file) && -r $file;
392 0           my $yaml = YAML::Tiny->new;
393 0           my $data = $yaml->read($file);
394 0 0         return [] unless $data;
395 0           return $data;
396             }
397             sub write_yaml {
398 0     0 1   my $file = shift;
399 0           my $data = shift;
400 0 0         return 0 unless defined $file;
401 0 0         return 0 unless defined $data;
402 0           my $yaml = YAML::Tiny->new($data);
403 0           $yaml->write($file);
404 0           return 1;
405             }
406             sub maniread { # Reading data from MANEFEST, MIRRORS and MANEFEST.* files
407             # Original see Ext::Utils::maniread
408 0     0 1   my $mfile = shift;
409 0           my $skipflag = shift;
410              
411 0           my $read = {};
412 0 0 0       return $read unless defined($mfile) && (-e $mfile) && (-r $mfile) && (-s $mfile);
      0        
      0        
413 0           my $fh;
414 0 0         unless (open $fh, "<", $mfile){
415 0           printf STDERR "Can't open file \"%s\": %s\n", $mfile, $!;
416 0           return $read;
417             }
418 0           local $_;
419 0           while (<$fh>){
420 0           chomp;
421 0 0         next if /^\s*#/;
422 0           my($file, $args);
423              
424 0 0 0       if ($skipflag && $_ =~ /^\s*\!\!perl\/regexp\s*/i) { # Working in SkipMode
425             #s/\r//;
426             #$_ =~ qr{^\s*\!\!perl\/regexp\s*(?:(?:'([^\\']*(?:\\.[^\\']*)*)')|([^#\s]\S*))?(?:(?:\s*)|(?:\s+(.*?)\s*))$};
427             #$args = $3;
428             #my $file = $2;
429             #if ( defined($1) ) {
430             # $file = $1;
431             # $file =~ s/\\(['\\])/$1/g;
432             #}
433 0 0         unless (($file, $args) = /^'(\\[\\']|.+)+'\s*(.*)/) {
434 0           ($file, $args) = /^(^\s*\!\!perl\/regexp\s*\S+)\s*(.*)/;
435             }
436             } else {
437             # filename may contain spaces if enclosed in ''
438             # (in which case, \\ and \' are escapes)
439 0 0         if (($file, $args) = /^'(\\[\\']|.+)+'\s*(.*)/) {
440 0           $file =~ s/\\([\\'])/$1/g;
441             } else {
442 0           ($file, $args) = /^(\S+)\s*(.*)/;
443             }
444             }
445 0 0         next unless $file;
446 0 0         $read->{$file} = [defined $args ? split(/\s+/,$args) : ""];
447             }
448 0           close $fh;
449 0           return $read;
450             }
451             sub manifind {
452 0     0 1   my $dir = shift;
453 0 0 0       carp("Can't specified directory") && return {} unless defined($dir) && -e $dir;
      0        
454              
455 0           my $found = {};
456 0           my $base = File::Spec->canonpath($dir);
457             #my ($volume,$sdirs,$sfile) = File::Spec->splitpath( $base );
458              
459             my $wanted = sub {
460 0     0     my $path = File::Spec->canonpath($_);
461 0           my $name = File::Spec->abs2rel( $path, $base );
462 0           my $fdir = File::Spec->canonpath($File::Find::dir);
463 0 0         return if -d $_;
464              
465 0           my $key = join("/", File::Spec->splitdir(File::Spec->catfile($name)));
466 0   0       $found->{$key} = {
      0        
467             mtime => (stat($_))[9] || 0,
468             size => (-s $_) || 0,
469             dir => $fdir,
470             path => $path,
471             file => File::Spec->abs2rel( $path, $fdir ),
472             };
473 0           };
474              
475             # We have to use "$File::Find::dir/$_" in preprocess, because
476             # $File::Find::name is unavailable.
477             # Also, it's okay to use / here, because MANIFEST files use Unix-style
478             # paths.
479 0           find({
480             wanted => $wanted,
481             no_chdir => 1,
482             }, $dir);
483              
484 0           return $found;
485             }
486             sub maniwrite {
487 0     0 1   my $file = shift;
488 0           my $mani = shift;
489 0 0 0       carp("Can't specified file") && return 0 unless defined($file);
490 0 0 0       carp("Can't specified manifest-hash") && return 0 unless defined($mani) && ref($mani) eq 'HASH';
      0        
491 0           my $file_bak = $file.".bak";
492              
493 0           rename $file, $file_bak;
494 0           my $fh;
495              
496 0 0         unless (open $fh, ">", $file){
497 0           printf STDERR "Can't open file \"%s\": %s\n", $file, $!;
498 0           rename $file_bak, $file;
499 0           return 0;
500             }
501              
502             # Stamp
503 0           print $fh "###########################################\n";
504 0           printf $fh "# File created at %s\n", scalar(localtime(time()));
505 0           print $fh "# Please, do NOT edit this file directly!!\n";
506 0           print $fh "###########################################\n\n";
507              
508 0           foreach my $f (sort { lc $a cmp lc $b } keys %$mani) {
  0            
509 0           my $d = $mani->{$f};
510             my $text = sprintf("%s\t%s\t%s",
511             $d->{mtime} || 0,
512             $d->{size} || 0,
513 0 0 0       $d->{mtime} ? scalar(localtime($d->{mtime})) : 'UNKNOWN',
      0        
514             );
515 0           my $tabs = (8 - (length($f)+1)/8);
516 0 0         $tabs = 1 if $tabs < 1;
517 0 0         $tabs = 0 unless $text;
518 0 0         if ($f =~ /\s/) {
519 0           $f =~ s/([\\'])/\\$1/g;
520 0           $f = "'$f'";
521             }
522 0           print $fh $f, "\t" x $tabs, $text, "\n";
523             }
524 0           close $fh;
525              
526 0           unlink $file_bak;
527              
528 0           return 1;
529             }
530              
531             1;
532              
533             __END__