File Coverage

blib/lib/App/DistSync.pm
Criterion Covered Total %
statement 57 564 10.1
branch 0 258 0.0
condition 0 255 0.0
subroutine 19 33 57.5
pod 10 10 100.0
total 86 1120 7.6


line stmt bran cond sub pod time code
1             package App::DistSync;
2 1     1   100556 use strict;
  1         2  
  1         50  
3 1     1   6 use warnings;
  1         2  
  1         65  
4 1     1   737 use utf8;
  1         333  
  1         7  
5 1     1   57 use feature qw/say/;
  1         2  
  1         246  
6              
7             =encoding utf-8
8              
9             =head1 NAME
10              
11             App::DistSync - Utility for synchronizing distribution mirrors
12              
13             =head1 SYNOPSIS
14              
15             use App::DistSync;
16              
17             my $ds = App::DistSync->new(
18             dir => "/var/www/www.example.com/dist",
19             pid => $$,
20             timeout => 60,
21             proxy => 'http://http.example.com:8001/',
22             );
23              
24             $ds->init or die "Initialization error";
25              
26             $ds->sync or die "Sync error";
27              
28             =head1 DESCRIPTION
29              
30             Utility for synchronizing distribution mirrors
31              
32             =head1 METHODS
33              
34             This module implements the following methods
35              
36             =head2 new
37              
38             my $ds = new App::DistSync(
39             dir => "/var/www/www.example.com/dist",
40             pid => $$,
41             timeout => 60,
42             proxy => 'http://http.example.com:8001/',
43             );
44              
45             Returns the object
46              
47             =head2 dir
48              
49             my $abs_dir = $ds->dir;
50              
51             Returns absolute pathname of working directory
52              
53             =head2 fetch
54              
55             my $struct = $self->fetch( $URI_STRING, "path/to/file.txt", "/tmp/file.txt" );
56              
57             Fetching file from remote resource by URI and filename.
58             The result will be written to the specified file. For example: "/tmp/file.txt"
59              
60             Returns structure, contains:
61              
62             {
63             status => 1, # Status. 0 - Errors; 1 - OK
64             mtime => 123456789, # Last-Modified in ctime format or 0 in case of errors
65             size => 123, # Content-length
66             code => 200, # HTTP Status code
67             };
68              
69             =head2 init
70              
71             $ds->init or die ("Initialization error");
72              
73             Initializing the mirror in the specified directory
74              
75             =head2 mkmani
76              
77             $ds->mkmani;
78              
79             Generation the new MANIFEST file
80              
81             =head2 pid
82              
83             my $pid = $ds->pid;
84              
85             Returns the pid of current process
86              
87             =head2 status
88              
89             $ds->status;
90              
91             Show statistic information
92              
93             =head2 sync
94              
95             $ds->sync or die ("Sync error");
96              
97             Synchronization of the specified directory with the remote resources (mirrors)
98              
99             =head2 ua
100              
101             my $ua = $ds->ua;
102              
103             Returns the UserAgent instance (LWP::UserAgent)
104              
105             =head2 verbose
106              
107             warn "Error details\n" if $ds->verbose;
108              
109             This method returns verbose flag
110              
111             =head1 HISTORY
112              
113             See C file
114              
115             =head1 TO DO
116              
117             See C file
118              
119             =head1 SEE ALSO
120              
121             L
122              
123             =head1 AUTHOR
124              
125             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
126              
127             =head1 COPYRIGHT
128              
129             Copyright (C) 1998-2026 D&D Corporation
130              
131             =head1 LICENSE
132              
133             This program is distributed under the terms of the Artistic License Version 2.0
134              
135             See the C file or L for details
136              
137             =cut
138              
139             our $VERSION = '1.11';
140              
141             our $DEBUG //= !!$ENV{DISTSYNC_DEBUG};
142              
143 1     1   8 use Carp;
  1         2  
  1         98  
144 1     1   7 use Cwd qw/abs_path getcwd/;
  1         2  
  1         60  
145 1     1   1123 use FindBin qw($Script);
  1         1671  
  1         169  
146 1     1   8 use File::Basename qw/dirname/;
  1         2  
  1         90  
147 1     1   643 use File::Copy qw/mv/;
  1         6727  
  1         96  
148 1     1   11 use File::Spec;
  1         1  
  1         43  
149 1     1   7 use File::Path qw/mkpath/;
  1         3  
  1         97  
150 1     1   627 use Sys::Hostname;
  1         1235  
  1         77  
151 1     1   1113 use URI;
  1         7341  
  1         89  
152 1     1   1153 use LWP::UserAgent qw//;
  1         44851  
  1         38  
153 1     1   18 use HTTP::Request qw//;
  1         1  
  1         14  
154 1     1   3 use HTTP::Date qw//;
  1         2  
  1         11  
155 1     1   3 use HTTP::Status qw//;
  1         1  
  1         20  
156              
157 1         142 use App::DistSync::Util qw/
158             debug qrreconstruct touch spew slurp
159             fdelete read_yaml write_yaml
160             maniread manifind maniwrite
161 1     1   530 /;
  1         4  
162              
163             use constant {
164 1         7489 TEMPFILE => sprintf("distsync_%s.tmp", $$),
165             TIMEOUT => 30,
166             METAFILE => 'META',
167             MANIFEST => 'MANIFEST',
168             MANISKIP => 'MANIFEST.SKIP',
169             MANITEMP => 'MANIFEST.TEMP',
170             MANILOCK => 'MANIFEST.LOCK',
171             MANIDEL => 'MANIFEST.DEL',
172             MIRRORS => 'MIRRORS',
173             README => 'README',
174             SKIPFILES => [qw/
175             META MANIFEST MIRRORS README
176             MANIFEST.SKIP MANIFEST.LOCK MANIFEST.TEMP MANIFEST.DEL
177             /],
178             SKIPMODE => 1,
179             LIMIT => '+1m', # '+1m' Limit gt and lt
180             EXPIRE => '+3d', # '+3d' For deleting
181             FREEZE => '+1d', # '+1d' For META test
182 1     1   6 };
  1         1  
183              
184             # Methods
185             sub new {
186 0     0 1   my $class = shift;
187 0           my %props = @_;
188              
189             # Check directory
190 0   0       my $dir = $props{dir} // getcwd();
191 0 0 0       croak("Directory '$dir' not exists") unless length($dir) && (-d $dir or -l $dir);
      0        
192 0           $props{dir} = $dir = abs_path($dir);
193              
194             # General
195 0           $props{started} = $props{stamp} = time;
196 0   0       $props{pid} ||= $$;
197 0   0       $props{timeout} //= TIMEOUT;
198 0   0       $props{verbose} ||= 0;
199 0   0       $props{insecure} ||= 0;
200 0   0       $props{proxy} //= '';
201 0           $props{url} = '';
202 0           $props{hostname} = hostname();
203              
204             # Files
205 0           $props{file_meta} = File::Spec->catfile($dir, METAFILE);
206 0           $props{file_manifest} = File::Spec->catfile($dir, MANIFEST);
207 0           $props{file_maniskip} = File::Spec->catfile($dir, MANISKIP);
208 0           $props{file_manilock} = File::Spec->catfile($dir, MANILOCK);
209 0           $props{file_manitemp} = File::Spec->catfile($dir, MANITEMP);
210 0           $props{file_manidel} = File::Spec->catfile($dir, MANIDEL);
211 0           $props{file_mirrors} = File::Spec->catfile($dir, MIRRORS);
212 0           $props{file_readme} = File::Spec->catfile($dir, README);
213 0           $props{file_temp} = File::Spec->catfile(File::Spec->tmpdir(), TEMPFILE);
214              
215             # Read META file as YAML
216 0           my $meta = read_yaml($props{file_meta});
217 0           $props{meta} = $meta;
218              
219             # Create current static dates
220             $props{mtime_manifest} = (-e $props{file_manifest}) && -s $props{file_manifest}
221 0 0 0       ? (stat($props{file_manifest}))[9]
222             : 0;
223             $props{mtime_manidel} = (-e $props{file_manidel}) && -s $props{file_manidel}
224 0 0 0       ? (stat($props{file_manidel}))[9]
225             : 0;
226             $props{mtime_mirrors} = (-e $props{file_mirrors}) && -s $props{file_mirrors}
227 0 0 0       ? (stat($props{file_mirrors}))[9]
228             : 0;
229              
230             # Set TimeOut
231 0   0       my $to = _expire($props{timeout} // TIMEOUT);
232 0 0         croak("Can't use specified timeout") unless $to =~ /^[0-9]{1,11}$/;
233              
234             # Instance
235 0           my $self = bless({%props}, $class);
236              
237             # User Agent
238 0           my $ua = $self->{ua} = LWP::UserAgent->new();
239 0 0         $ua->timeout($to) if $to;
240 0           $ua->agent(sprintf("%s/%s", __PACKAGE__, $VERSION));
241 0           $ua->env_proxy;
242 0 0         $ua->proxy(['http', 'https'] => $props{proxy}) if $props{proxy};
243             $ua->ssl_opts(
244             verify_hostname => 0,
245             SSL_verify_mode => 0x00
246 0 0         ) if $props{insecure};
247              
248 0           return $self;
249             }
250 0     0 1   sub verbose { !!shift->{verbose} }
251 0     0 1   sub dir { shift->{dir} }
252 0     0 1   sub pid { shift->{pid} }
253 0     0 1   sub ua { shift->{ua} }
254             sub init { # Initialization
255 0     0 1   my $self = shift;
256 0           my $stamp = scalar(localtime($self->{started}));
257 0           my $status = 1;
258              
259             # MANIFEST.SKIP
260 0           printf "%s... ", $self->{file_maniskip};
261 0 0 0       if (touch($self->{file_maniskip}) && (-e $self->{file_maniskip}) && -z $self->{file_maniskip}) {
      0        
262 0           my @content = (
263             "# Generated on $stamp",
264             "# List of files that should not be synchronized",
265             "#",
266             "# Format of file:",
267             "#",
268             "# dir1/dir2/.../dirn/foo.txt any comment, for example blah-blah-blah",
269             "# bar.txt any comment, for example blah-blah-blah",
270             "# baz.txt",
271             "# 'spaced dir1/foo.txt' any comment, for example blah-blah-blah",
272             "# 'spaced dir1/foo.txt' any comment, for example blah-blah-blah",
273             "# !!perl/regexp (?i-xsm:\\.bak\$) avoid all bak files",
274             "#",
275             "# See also MANIFEST.SKIP file of ExtUtils::Manifest v1.68 or later",
276             "#",
277             "",
278             "# Avoid version control files.",
279             "!!perl/regexp (?i-xsm:\\bRCS\\b)",
280             "!!perl/regexp (?i-xsm:\\bCVS\\b)",
281             "!!perl/regexp (?i-xsm:\\bSCCS\\b)",
282             "!!perl/regexp (?i-xsm:,v\$)",
283             "!!perl/regexp (?i-xsm:\\B\\.svn\\b)",
284             "!!perl/regexp (?i-xsm:\\B\\.git\\b)",
285             "!!perl/regexp (?i-xsm:\\B\\.gitignore\\b)",
286             "!!perl/regexp (?i-xsm:\\b_darcs\\b)",
287             "!!perl/regexp (?i-xsm:\\B\\.cvsignore\$)",
288             "",
289             "# Avoid temp and backup files.",
290             "!!perl/regexp (?i-xsm:~\$)",
291             "!!perl/regexp (?i-xsm:\\.(old|bak|back|tmp|temp|rej)\$)",
292             "!!perl/regexp (?i-xsm:\\#\$)",
293             "!!perl/regexp (?i-xsm:\\b\\.#)",
294             "!!perl/regexp (?i-xsm:\\.#)",
295             "!!perl/regexp (?i-xsm:\\..*\\.sw.?\$)",
296             "",
297             "# Avoid prove files",
298             "!!perl/regexp (?i-xsm:\\B\\.prove\$)",
299             "",
300             "# Avoid MYMETA files",
301             "!!perl/regexp (?i-xsm:^MYMETA\\.)",
302             "",
303             "# Avoid Apache and building files",
304             "!!perl/regexp (?i-xsm:\\B\\.ht.+\$)",
305             "!!perl/regexp (?i-xsm:\\B\\.exists\$)",
306             "",
307             "# Skip TEMP files",
308             "!!perl/regexp (?i-xsm:\\.TEMP\\-\\d+\$)",
309             "\n",
310             );
311 0 0         if (spew($self->{file_maniskip}, join("\n", @content))) {
312 0           say "ok";
313             } else {
314 0           say "fail";
315 0           $status = 0;
316             }
317             } else {
318 0           say "skip";
319             }
320              
321             # MANIFEST.DEL
322 0           printf "%s... ", $self->{file_manidel};
323 0 0 0       if (touch($self->{file_manidel}) && (-e $self->{file_manidel}) && -z $self->{file_manidel}) {
      0        
324 0           my @content = (
325             "# Generated on $stamp",
326             "# List of files that must be deleted. By default, the files will be",
327             "# deleted after 3 days.",
328             "#",
329             "# Format of file:",
330             "#",
331             "# dir1/dir2/.../dirn/foo.txt 1d",
332             "# bar.txt 2M",
333             "# baz.txt",
334             "# 'spaced dir1/foo.txt' 1m",
335             "# 'spaced dir1/foo.txt' 2y",
336             "#",
337             "\n",
338             );
339 0 0         if (spew($self->{file_manidel}, join("\n", @content))) {
340 0           say "ok";
341             } else {
342 0           say "fail";
343 0           $status = 0;
344             }
345             } else {
346 0           say "skip";
347             }
348              
349             # MIRRORS
350 0           printf "%s... ", $self->{file_mirrors};
351 0 0 0       if (touch($self->{file_mirrors}) && (-e $self->{file_mirrors}) && -z $self->{file_mirrors}) {
      0        
352 0           my @content = (
353             "# Generated on $stamp",
354             "# List of addresses (URIs) of remote storage (mirrors).",
355             "# Must be specified at least two mirrors",
356             "#",
357             "# Format of file:",
358             "#",
359             "# http://www.example.com/dir1 any comment, for example blah-blah-blah",
360             "# http://www.example.com/dir2 any comment, for example blah-blah-blah",
361             "# 'http://www.example.com/dir2' any comment, for example blah-blah-blah",
362             "#",
363             "\n",
364             );
365 0 0         if (spew($self->{file_mirrors}, join("\n", @content))) {
366 0           say "ok";
367             } else {
368 0           say "fail";
369 0           $status = 0;
370             }
371             } else {
372 0           say "skip";
373             }
374              
375             # README
376 0           printf "%s... ", $self->{file_readme};
377 0 0 0       if (touch($self->{file_readme}) && (-e $self->{file_readme}) && -z $self->{file_readme}) {
      0        
378 0           my @content = (
379             "# This file contains information about the resource (mirror) in the free form.",
380             "#",
381             "# Initialization date : $stamp",
382             "# Resource's directory : " . $self->dir,
383             "#",
384             "\n",
385             );
386 0 0         if (spew($self->{file_readme}, join("\n", @content))) {
387 0           say "ok";
388             } else {
389 0           say "fail";
390 0           $status = 0;
391             }
392             } else {
393 0           say "skip";
394             }
395              
396 0           return $status;
397             }
398             sub sync { # Synchronization. Main proccess
399 0     0 1   my $self = shift;
400 0           my $status = 0;
401 0           my %skips; # { file => /regexp/|file } List of skipped files
402 0   0       my $manifest = maniread($self->{file_manifest}) // {}; # {file => [epoch, size, wday, month, day, time, year]}
403 0           my %sync_list; # {file => [{url, mtime, size}]} List of files to sync
404             my %delete_list; # {file => count} List of files to delete
405              
406             # Filling the list of exclusion files using the MANIFEST.SKIP file and
407             # the list of system files from the SKIPFILES constant
408             {
409 0           debug("Getting the list of skipped files");
410 0           my @skip_keys = @{(SKIPFILES)};
  0            
411 0           my $maniskip = maniread($self->{file_maniskip}, SKIPMODE); # MANIFEST.SKIP
412 0 0         push @skip_keys, keys %$maniskip if ref($maniskip) eq 'HASH';
413 0           for (@skip_keys) {$skips{$_} = qrreconstruct($_)}
  0            
414 0           debug("Found %d keys in the list of skipped files", scalar(keys %skips));
415             #debug(Data::Dumper::Dumper(\%skips)) && return 0;
416             }
417              
418             # Deleting files listed in the MANIFEST.DEL file but not in the exclusion list
419             {
420 0           debug("Deleting files from list: %s", MANIDEL);
  0            
  0            
421 0           my $delfile = $self->{file_manidel}; # MANIFEST.DEL
422 0           my $deltime = $self->{mtime_manidel}; # Modify time in seconds
423 0   0       my $dellist = maniread($delfile) // {}; # { file => expire };
424 0           my $expire = 0;
425 0           foreach (values %$dellist) {
426 0   0       my $dt = _expire($_->[0] || 0);
427 0           $_ = [$dt];
428 0 0         $expire = $dt if $dt > $expire;
429             }
430 0 0         $expire = _expire(EXPIRE) unless $expire > 0;
431 0 0         debug("The file '$delfile' will expire on %s", scalar(localtime($deltime + $expire)))
432             if $deltime;
433             #debug(Data::Dumper::Dumper($dellist)) && return 0;
434 0 0 0       if ($deltime && (time - $deltime) > $expire) { # MANIFEST.DEL is expired!
435             # Delete files physically if they exist physically and are not on the exclusion list!
436 0           foreach my $k (keys %$dellist) {
437 0 0         if (_skipcheck(\%skips, $k)) { # The file is in the exclusion list.
438 0           debug("> [SKIPPED] %s", $k);
439 0           next;
440             }
441 0           my $f = File::Spec->canonpath(File::Spec->catfile($self->dir, $k));
442 0 0         if (-e $f) {
443 0           fdelete($f);
444 0           debug("> [DELETED] %s", $k);
445             } else {
446 0           debug("> [SKIPPED] %s (%s)", $k, $f);
447             }
448             }
449              
450             # Deleting the MANIFEST.DEL file and immediately creating a new one
451 0           fdelete($delfile);
452 0           touch($delfile);
453             } else {
454 0 0         if ($deltime) {
455 0           debug("Skipped. Deletion is not required yet because the scheduled time has not arrived");
456 0 0         if ($self->verbose) {
457 0           debug(" File : %s", MANIDEL);
458 0           debug(" Created : %s", scalar(localtime($deltime)));
459 0           debug(" Expires : %s", scalar(localtime($deltime + $expire)));
460             }
461             } else {
462 0           debug("Skipped. File %s not exists", MANIDEL);
463             }
464             }
465              
466             # Adding files listed in MANIFEST.DEL to the exclusion list
467 0           for (keys %$dellist) {$skips{$_} = qrreconstruct($_)}
  0            
468             }
469              
470             # Reading the MIRRORS file and deciding whether to synchronize or not
471 0           debug("Synchronization");
472 0   0       my $mirrors_mani = maniread($self->{file_mirrors}) // {}; # MIRRORS
473 0           my @mirrors = sort {$a cmp $b} keys %$mirrors_mani;
  0            
474 0 0         if (scalar(@mirrors)) {
475 0           foreach my $url (@mirrors) {
476 0           debug("RESOURCE \"%s\"", $url);
477              
478             # Downloading the MANIFEST.LOCK file, skipping the mirror resource if this
479             # file was successfully downloaded from the resource
480             {
481 0           debug("Fetching %s", MANILOCK);
482 0           my $fetch_lock = $self->fetch($url, MANILOCK, $self->{file_manitemp});
483 0 0         if ($fetch_lock->{status}) { # Ok
484 0 0         if ($self->_check_lockfile($self->{file_manitemp})) {
485 0           $self->{url} = $url;
486 0           debug("> [SKIPPED] Current resource SHOULD NOT update itself");
487             } else {
488 0           debug("> [SKIPPED] Remote resource is in a state of updating. Please wait");
489             }
490 0           next;
491             }
492             }
493              
494             # Downloading the META file and analyzing the resource (checking the resource
495             # status and update date). If the check fails, the resource is skipped.
496             {
497 0           debug("Fetching %s", METAFILE);
  0            
498 0           my $fetch_meta = $self->fetch($url, METAFILE, $self->{file_manitemp});
499 0 0         if ($fetch_meta->{status}) { # Ok
500 0   0       my $remote_meta = read_yaml($self->{file_manitemp}) // '';
501 0 0 0       if (((ref($remote_meta) eq 'ARRAY') || ref($remote_meta) eq 'YAML::Tiny')) {
502 0   0       $remote_meta = $remote_meta->[0] || {};
503             }
504 0 0 0       unless ($remote_meta && ref($remote_meta) eq 'HASH') {
505 0           debug("> [SKIPPED] Remote resource is unreadable. Please contact the administrator of this resource");
506 0           next;
507             }
508 0 0         if ($remote_meta->{status}) {
509 0   0       my $remote_url = $remote_meta->{url} || $remote_meta->{uri} || '';
510 0   0       my $remote_date = $fetch_meta->{mtime} || 0;
511 0 0         my $remote_datef = $remote_date ? scalar(localtime($remote_date)) : 'UNKNOWN';
512 0 0         my $remote_ok = (time - $remote_date) > _expire(FREEZE) ? 0 : 1;
513 0 0         if ($self->verbose) {
514 0           debug("RESOURCE INFORMATION:");
515 0           debug(" Resource URL : %s", $remote_url);
516 0   0       debug(" Date : %s", $remote_meta->{date} // 'UNKNOWN');
517 0           debug(" Modified : %s", $remote_datef);
518 0   0       debug(" Hostname : %s", $remote_meta->{hostname} // '');
519 0   0       debug(" Directory : %s", $remote_meta->{directory} // '');
520             debug(" Project : %s v%s",
521 0   0       $remote_meta->{project} || ref($self), $remote_meta->{version} // '0.01');
      0        
522 0   0       debug(" Script : %s", $remote_meta->{script} // '');
523 0 0         debug(" Status : %s", $remote_ok ? "OK" : "EXPIRED");
524 0   0       debug(" Time : %d sec", $remote_meta->{'time'} || 0);
525             }
526 0 0         unless ($remote_ok) {
527 0           debug("> [SKIPPED] Remote resource is expired. Last updated: %s", $remote_datef);
528             next
529 0           }
530             } else {
531 0           debug("> [SKIPPED] Remote resource is broken. Please contact the administrator of this resource");
532 0           next;
533             }
534             } else {
535 0           printf STDERR "Can't download \"%s\": %s\n", $fetch_meta->{url}, $fetch_meta->{message};
536             }
537             }
538              
539             # Downloading the MANIFEST file
540             {
541 0           debug("Fetching %s", MANIFEST);
  0            
542 0           my $fetch_mani = $self->fetch($url, MANIFEST, $self->{file_manitemp});
543 0 0         if ($fetch_mani->{status}) {
544 0   0       my $remote_manifest = maniread($self->{file_manitemp}) // {};
545 0           my %mtmp; # {file => count} Temporary work structure
546              
547             # Two manifest lists - local and remote - are merged into a temporary structure
548             # {file => [epoch, size, wday, month, day, time, year]}
549 0           foreach my $k (keys(%$manifest), keys(%$remote_manifest)) {
550 0 0         unless (exists $mtmp{$k}) {
551 0           $mtmp{$k} = 1;
552 0           next;
553             }
554 0   0       my $mt_l = $manifest->{$k}[0] || 0; # Modified time (local, left)
555 0   0       my $mt_r = $remote_manifest->{$k}[0] || 0; # Modified time (remote, right)
556 0 0 0       $mtmp{$k}++ if $mt_l && $mt_r && $mt_l == $mt_r; # =2 if the files are identical
      0        
557             }
558             #debug(Data::Dumper::Dumper(\%mtmp));
559              
560             # Getting the difference between the lists of local and remote files
561             #
562             # [=] The files do not differ; they are identical in both lists
563             # [<] The file exists in the local (left) file list
564             # [>] The file exists in the remote (right) file list
565             # [{] The "newer" file is the one in the local list
566             # [}] The "newer" file is the one in the remote list
567             # [~] The file sizes differ between the lists. This is only reported as information,
568             # since modification times and file presence have higher priority than sizes
569             # [!] A conflict situation. An almost impossible edge case
570             #
571             # The comparison works as follows:
572             # We iterate through the entries of the manifest structures (the left and right lists)
573             # and analyze where the counter value is 1 and where it is 2.
574             # A value of 1 means that the file exists in only one of the file lists - but in which one?
575             # If it's the left list, the line is marked with "<", as described in the legend above;
576             # if it's the right list, the line is marked with ">".
577 0           my $lim = _expire(LIMIT); # 1 min
578 0           foreach my $k (keys %mtmp) {
579 0 0         next unless $mtmp{$k}; # Skip broken records
580 0 0         next unless $mtmp{$k} == 1; # Files are NOT idential
581 0 0 0       if ($manifest->{$k} && $remote_manifest->{$k}) { # Both sides: left and right
    0          
    0          
582 0   0       my $mt_l = $manifest->{$k}[0] || 0;
583 0   0       my $mt_r = $remote_manifest->{$k}[0] || 0;
584 0 0 0       if (($mt_l > $mt_r) && ($mt_l - $mt_r) > $lim) {
585             # Skip! The left (local) file is more than one minute newer than the right one
586             # debug("# [{] %s", $k) if $self->verbose;
587 0 0 0       } if (($mt_l < $mt_r) && ($mt_r - $mt_l) > $lim) {
588             # The right (remote) file is more than one minute newer than the left one
589 0 0         debug("# [}] %s (LOCAL [%s] < REMOTE [%s])", $k,
590             scalar(localtime($mt_l)), scalar(localtime($mt_r))) if $self->verbose;
591             # Add to sync list for downloading
592 0 0         unless (_skipcheck(\%skips, $k)) {
593 0   0       my $ar = $sync_list{$k} //= [];
594             push @$ar, {
595             url => $url,
596             mtime => $remote_manifest->{$k}[0],
597 0           size => $remote_manifest->{$k}[1],
598             };
599             }
600             } else {
601             # Skip! Files are idential
602             #debug("# [=] %s", $k) if $self->verbose;
603             }
604             } elsif ($manifest->{$k}) { # Left side
605             # Skip! No download requiered
606             # debug("# [<] %s", $k) if $self->verbose;
607             } elsif ($remote_manifest->{$k}) { # Right (remote) side
608             # Download required
609 0 0         debug("# [>] %s", $k) if $self->verbose;
610 0 0         unless (_skipcheck(\%skips, $k)) {
611 0   0       my $ar = $sync_list{$k} //= [];
612             push @$ar, {
613             url => $url,
614             mtime => $remote_manifest->{$k}[0],
615 0           size => $remote_manifest->{$k}[1],
616             };
617             }
618             } else {
619 0 0         debug(sprintf("# [!] %s", $k)) if $self->verbose;
620             }
621             }
622              
623             # Ok
624 0           $status = 1;
625             } else {
626 0           debug("> [SKIPPED] Can't download \"%s\"", $fetch_mani->{url});
627 0           printf STDERR "Can't download \"%s\": %s\n", $fetch_mani->{url}, $fetch_mani->{message};
628 0           next;
629             }
630             }
631              
632             # Download the MIRRORS file and add it to the sync list if it is up to date
633             {
634 0           debug("Fetching %s", MIRRORS);
  0            
635 0           my $fetch_mirr = $self->fetch($url, MIRRORS, $self->{file_manitemp});
636 0 0 0       if ($fetch_mirr->{status} && ((-z $self->{file_mirrors}) || $fetch_mirr->{mtime} > $self->{mtime_mirrors})) {
      0        
637 0   0       my $remote_mirr = maniread($self->{file_manitemp}) // {};
638 0   0       my $mcnt = scalar(keys %$remote_mirr) || 0; # Resources count in remote mirror file
639 0 0 0       if ($mcnt && $mcnt > 1) { # 2 and more resources
640 0   0       my $ar = $sync_list{(MIRRORS)} //= [];
641             push @$ar, {
642             url => $url,
643             mtime => $fetch_mirr->{mtime},
644             size => $fetch_mirr->{size},
645 0           };
646             } else {
647 0           debug("> [SKIPPED] File %s on %s contains too few mirrors", MIRRORS, $url);
648             }
649             } else {
650             printf STDERR "Can't download \"%s\": %s\n", $fetch_mirr->{url}, $fetch_mirr->{message}
651 0 0         unless $fetch_mirr->{status};
652             }
653             }
654              
655             # Download MANIFEST.DEL and fill the list to delete the files listed in it
656             {
657 0           debug("Fetching %s", MANIDEL);
  0            
  0            
658 0           my $fetch_dir = $self->fetch($url, MANIDEL, $self->{file_manitemp});
659 0 0         if ($fetch_dir->{status}) {
660 0   0       my $remote_manidel = maniread($self->{file_manitemp}) // {};
661 0           foreach my $k (keys %$remote_manidel) {
662 0 0         unless (_skipcheck(\%skips, $k)) {
663 0   0       $delete_list{$k} //= 0;
664 0           $delete_list{$k}++;
665             }
666             }
667             } else {
668             printf STDERR "Can't download \"%s\": %s\n", $fetch_dir->{url}, $fetch_dir->{message}
669 0           }
670             }
671             } continue {
672 0           fdelete($self->{file_manitemp});
673             }
674             } else {
675 0           $status = 1;
676 0           debug("Skipped. File %s is empty", MIRRORS);
677             }
678              
679             # Deleting files according to the generated list of files to be deleted
680             {
681 0           debug("Deleting files");
682 0           foreach my $k (keys %delete_list) {
683 0           my $f = File::Spec->canonpath(File::Spec->catfile($self->dir, $k));
684 0 0         if (-e $f) {
685 0           fdelete($f);
686 0           debug("> [DELETED] %s", $k);
687             } else {
688 0           debug("> [SKIPPED] %s (%s)", $k, $f);
689             }
690             }
691             }
692             #debug(Data::Dumper::Dumper(\%delete_list));
693              
694             # Iterate through the synchronization list and download all files that
695             # are NOT present in the previously generated deletion list.
696             #debug(Data::Dumper::Dumper(\%sync_list));
697             {
698 0           debug("Downloading files");
  0            
  0            
699 0           my $total = 0; # Size
700 0           my $cnt = 0; # File number
701 0           my $all = scalar(keys %sync_list);
702 0           my $af = '[%0' . length("$all") . 'd/%0' . length("$all") . 'd] %s';
703 0           foreach my $k (sort {lc($a) cmp lc($b)} keys %sync_list) { $cnt++;
  0            
  0            
704 0           debug($af, $cnt, $all, $k);
705 0   0       my $list = $sync_list{$k} // []; # Get list of urls
706 0 0         unless (scalar(@$list)) {
707 0 0         debug("> [SKIPPED] Nothing to do for %s", $k) if $self->verbose;
708 0           next;
709             }
710              
711             # Try to download by list of urls
712 0   0       my $mt_l = $manifest->{$k}[0] || 0; # Modify time of local file
713 0           my $is_downloaded = 0;
714 0   0       foreach my $job (sort {($b->{mtime} || 0) <=> ($a->{mtime} || 0)} @$list) {
  0   0        
715 0 0         last if $is_downloaded;
716 0           my $mt_r = $job->{mtime}; # Modify time of remote file
717 0           my $url = $job->{url}; # URL of remote file
718 0           my $size = $job->{size}; # Size of remote file
719              
720             # Check URL
721 0 0         unless ($url) {
722 0 0         debug("> [SKIPPED] No URL") if $self->verbose;
723 0           next;
724             }
725              
726             # Check size
727 0 0         unless ($size) {
728 0 0         debug("> [SKIPPED] No file size: %s", $url) if $self->verbose;;
729 0           next;
730             }
731              
732             # Check modify time
733 0 0 0       unless ($mt_r || !$mt_l) {
734 0 0         debug("> [SKIPPED] The remote file have undefined modified time: %s", $url) if $self->verbose;
735 0           next;
736             }
737 0 0         if ($mt_l >= $mt_r) {
738 0 0         debug("> [SKIPPED] File is up to date: %s", $url) if $self->verbose;
739 0           next;
740             }
741              
742             # Download
743 0           my $fetch_file = $self->fetch($url, $k, $self->{file_temp});
744 0 0         if ($fetch_file->{status}) {
745 0   0       my $size_fact = $fetch_file->{size} || 0;
746 0 0 0       if ($size_fact && $size_fact == $size) {
747 0 0         debug("> [ OK ] Received %d bytes: %s", $size_fact, $url) if $self->verbose;
748 0           $total += $size_fact;
749 0           $is_downloaded = 1;
750 0           next;
751             }
752             } else {
753 0           printf STDERR "Can't download \"%s\": %s\n", $fetch_file->{url}, $fetch_file->{message};
754             }
755 0 0         debug("> [ ERROR ] Can't fetch %s", $url) if $self->verbose;
756             }
757 0 0         unless ($is_downloaded) {
758 0           debug(("> [FAILED ] Can't download file %s", $k));
759 0           next;
760             }
761              
762             # The file has been downloaded successfully and is already in a temporary file,
763             # ready to be move to the target directory under its own name.
764             {
765 0           my $src = $self->{file_temp}; # From $self->{file_temp}
  0            
766 0           my $dst = File::Spec->canonpath(File::Spec->catfile($self->dir, $k)); # To $k
767              
768             # Create target directory
769 0           my $dir = dirname($dst); # See File::Basename
770 0           my $mkerr;
771 0           mkpath($dir, {verbose => 1, mode => 0777, error => \$mkerr});
772 0 0 0       if ($mkerr && (ref($mkerr) eq 'ARRAY') && scalar(@$mkerr)) {
      0        
773 0           foreach my $e (@$mkerr) {
774 0 0 0       next unless $e && ref($e) eq 'HASH';
775 0           while (my ($_k, $_v) = each %$e) {
776 0           printf STDERR "%s: %s\n", $_k, $_v;
777             }
778             }
779             }
780              
781             # Move file to target directory
782 0           fdelete($dst);
783 0 0         unless (mv($src, $dst)) {
784 0           printf STDERR "Can't move file %s to %s: $!\n", $src, $dst;
785             }
786             }
787             }
788              
789             # Ok
790 0           debug("Received %d bytes", $total);
791             }
792              
793             # Cteating MANIFEST file
794 0           debug("Generating new manifest");
795 0           my $new_manifest = manifind($self->dir);
796              
797             # We select files excluding files listed in the exclusion list
798 0           foreach my $k (keys %$new_manifest) {
799 0           my $nskip = _skipcheck(\%skips, $k);
800 0 0         delete $new_manifest->{$k} if $nskip;
801 0 0         debug("> [%s] %s", $nskip ? "SKIPPED" : " ADDED ", $k);
802             }
803             #debug(Data::Dumper::Dumper($new_manifest));
804              
805             # Save the created file
806 0           debug("Saving manifest to %s", MANIFEST);
807 0 0         return 0 unless maniwrite($self->{file_manifest}, $new_manifest);
808              
809             # Creating new META file
810 0           debug("Generating new META file");
811             # NOTE! The status in the META file is set only after the final directory structure
812             # has been successfully generated. This change distinguishes already "working"
813             # resources from those that have just been initialized.
814 0           my $now = time;
815             my $new_meta = {
816             project => ref($self),
817             version => $self->VERSION,
818             hostname => $self->{hostname},
819             directory => $self->dir,
820             script => $Script,
821             start => $self->{stamp},
822             finish => $now,
823             pid => $self->pid,
824             uri => $self->{url} || 'localhost',
825             url => $self->{url} || 'localhost',
826             date => scalar(localtime(time)),
827             'time' => $now - $self->{stamp},
828 0   0       status => 1,
      0        
829             };
830 0 0         return 0 unless write_yaml($self->{file_meta}, $new_meta);
831              
832             # Return
833 0           return $status;
834             }
835             sub fetch { # Returns structire {status, mtime, size, code, url}
836 0     0 1   my $self = shift;
837 0           my $url = shift; # Base url
838 0           my $obj = shift; # The tail of path
839 0   0       my $file = shift // ''; # File to download
840 0           my $ua = $self->ua;
841              
842             # Empty response
843 0           my $ret = {
844             status => 0, # Status
845             mtime => 0, # Last-Modified in ctime format or 0
846             size => 0, # Content-length
847             code => 0, # Status code
848             message => '',
849             url => '',
850             };
851              
852             # Check file
853 0 0         unless (length($file)) {
854 0           carp "File path to download is not specified";
855 0           return $ret;
856             }
857              
858             # Make new URI
859 0           my $uri = URI->new($url);
860 0           my $curpath = $uri->path();
861 0 0         my $newpath = $curpath . (defined $obj ? "/$obj" : '');
862 0           $newpath =~ s/\/{2,}/\//;
863 0           $uri->path($newpath);
864 0           $ret->{url} = $uri->as_string;
865              
866             # First request: get HEAD information
867 0           my $request = HTTP::Request->new(HEAD => $uri);
868 0           my $response = $ua->request($request);
869 0           my $content_type = scalar $response->header('Content-Type');
870 0           my $document_length = scalar $response->header('Content-Length');
871 0           my $modified_time = HTTP::Date::str2time($response->header('Last-Modified'));
872 0           my $expires = HTTP::Date::str2time($response->header('Expires'));
873 0           my $server = scalar $response->header('Server');
874 0           $ret->{code} = $response->code;
875 0           $ret->{message} = $response->message;
876 0 0         if ($self->verbose) {
877 0 0 0       if (!$DEBUG && !$response->is_success) {
878 0           say sprintf "> HEAD %s", $uri->as_string;
879 0           say sprintf "< %s", $response->status_line;
880             }
881 0           debug("> HEAD %s", $uri->as_string);
882 0           debug("< %s", $response->status_line);
883 0 0         if ($response->is_success) {
884 0   0       debug("< Content-Type : %s", $content_type // '');
885 0   0       debug("< Content-Length : %s", $document_length || 0);
886 0 0         debug("< Last-Modified : %s", $modified_time ? scalar(localtime($modified_time)) : '');
887 0 0         debug("< Expires : %s", $expires ? scalar(localtime($expires)) : '');
888 0   0       debug("< Server : %s", $server // '');
889             } else {
890 0           debug("< Empty response");
891             }
892             }
893              
894             # Status
895 0 0         unless ($response->is_success) {
896 0           debug("Can't fetch %s. %s", $uri->as_string, $response->status_line);
897 0           return $ret;
898             }
899              
900             # Size
901 0   0       $ret->{size} = $document_length || 0;
902              
903             # Modified time
904 0   0       $ret->{mtime} = $modified_time // 0;
905 0 0         unless ($ret->{mtime}) {
906 0           debug("Can't fetch %s. Header 'Last-Modified' not received", $uri->as_string);
907 0           return $ret;
908             }
909              
910             # Safe file mirroring
911 0           my $temp = sprintf "%s.tmp", $file;
912 0 0         if (-e $file) {
913 0 0         unless (mv($file, $temp)) {
914 0           printf STDERR "Can't move file \"%s\" to \"%s\": %s\n", $file, $temp, $!;
915 0           return $ret;
916             }
917             }
918              
919             # Request
920 0           $response = $ua->mirror($uri, $file);
921 0           $ret->{code} = $response->code;;
922 0           $ret->{message} = $response->message;
923 0 0         if ($self->verbose) {
924 0 0         debug("> GET %s", $uri->as_string) or say sprintf "> GET %s", $uri->as_string;
925 0 0         debug("< %s", $response->status_line) or say sprintf "< %s", $response->status_line;
926             }
927 0 0         if ($response->is_success) {
928 0 0 0       if (-e $file && (-s $file) == $ret->{size}) {
929 0           $ret->{status} = 1;
930 0           fdelete($temp);
931             }
932             } else {
933 0           debug("Can't fetch %s. %s", $uri->as_string, $response->status_line);
934 0           return $ret;
935             }
936              
937             # Move temp file to original name
938 0 0 0       if (!$ret->{status} && -e $temp) {
939 0 0         unless (mv($temp, $file)) {
940 0           printf "Can't move file \"%s\" to \"%s\": %s", $temp, $file, $!;
941             }
942             }
943              
944 0           return $ret;
945             }
946             sub status { # Show statistic information
947 0     0 1   my $self = shift;
948              
949             # Read MIRRORS file
950 0   0       my $mirrors_mani = maniread($self->{file_mirrors}) // {}; # MIRRORS
951 0           my @mirrors = sort {$a cmp $b} keys %$mirrors_mani;
  0            
952 0 0         unless (scalar(@mirrors)) {
953 0           say STDERR sprintf "File %s is empty", MIRRORS;
954 0           return;
955             }
956              
957             # Go!
958 0           foreach my $url (@mirrors) {
959 0           say sprintf "RESOURCE \"%s\"", $url;
960 0           my $self_mode = 0;
961              
962             # Downloading the MANIFEST.LOCK file, skipping the mirror resource if this
963             # file was successfully downloaded from the resource
964             {
965 0           debug("Fetching %s", MANILOCK);
966 0           my $fetch_lock = $self->fetch($url, MANILOCK, $self->{file_manitemp});
967 0 0         if ($fetch_lock->{status}) { # Ok
968 0 0         if ($self->_check_lockfile($self->{file_manitemp})) {
969 0           $self->{url} = $url;
970 0           $self_mode = 1;
971             } else {
972 0           say STDERR "Remote resource is in a state of updating. Please wait";
973 0           next;
974             }
975             }
976             }
977              
978             # Downloading the META file and analyzing the resource (checking the resource
979             # status and update date). If the check fails, the resource is skipped.
980             {
981 0           debug("Fetching %s", METAFILE);
  0            
  0            
982 0           my $meta = $self->fetch($url, METAFILE, $self->{file_manitemp});
983 0 0         if ($meta->{status}) { # Ok
984 0   0       my $remote_meta = read_yaml($self->{file_manitemp}) // '';
985 0 0 0       if (((ref($remote_meta) eq 'ARRAY') || ref($remote_meta) eq 'YAML::Tiny')) {
986 0   0       $remote_meta = $remote_meta->[0] || {};
987             }
988 0 0 0       unless ($remote_meta && ref($remote_meta) eq 'HASH') {
989 0           say STDERR "Remote resource is unreadable. Please contact the administrator of this resource";
990 0           next;
991             }
992 0 0         unless ($remote_meta->{status}) {
993 0           say STDERR "Remote resource is broken. Please contact the administrator of this resource";
994 0           next;
995             }
996              
997             # Show information
998 0   0       my $remote_url = $remote_meta->{url} || $remote_meta->{uri} || '';
999 0   0       my $remote_date = $meta->{mtime} || 0;
1000 0 0         my $remote_datef = $remote_date ? scalar(localtime($remote_date)) : 'UNKNOWN';
1001 0 0         my $remote_ok = (time - $remote_date) > _expire(FREEZE) ? 0 : 1;
1002 0 0         say sprintf " Resource URL : %s%s", $remote_url, $self_mode ? " (LOCAL RESOURCE)" : '';
1003 0 0         say sprintf " Status : %s", $remote_ok ? "OK" : "EXPIRED";
1004 0   0       say sprintf " Date : %s", $remote_meta->{date} // 'UNKNOWN';
1005 0           say sprintf " Modified : %s", $remote_datef;
1006 0   0       say sprintf " Hostname : %s", $remote_meta->{hostname} // '';
1007 0   0       say sprintf " Directory : %s", $remote_meta->{directory} // '';
1008 0   0       say sprintf " Project : %s v%s", $remote_meta->{project} || ref($self), $remote_meta->{version} // '';
      0        
1009 0   0       say sprintf " Script : %s", $remote_meta->{script} // $Script;
1010 0   0       say sprintf " Time : %d sec", $remote_meta->{'time'} || 0;
1011 0 0         unless ($remote_ok) {
1012 0           say STDERR sprintf "NOTE! The resource is expired. Last updated: %s", $remote_datef;
1013             next
1014 0           }
1015             } else {
1016 0           printf STDERR "Can't download \"%s\": %s\n", $meta->{url}, $meta->{message};
1017             }
1018             }
1019             }
1020              
1021 0           return 1;
1022             }
1023             sub mkmani {
1024 0     0 1   my $self = shift;
1025 0           my %skips; # { file => /regexp/|file } List of skipped files
1026              
1027             # Filling the list of exclusion files using the MANIFEST.SKIP file and
1028             # the list of system files from the SKIPFILES constant
1029             {
1030 0           debug("Getting the list of skipped files");
1031 0           my @skip_keys = @{(SKIPFILES)};
  0            
1032 0           my $maniskip = maniread($self->{file_maniskip}, SKIPMODE); # MANIFEST.SKIP
1033 0 0         push @skip_keys, keys %$maniskip if ref($maniskip) eq 'HASH';
1034 0           for (@skip_keys) {$skips{$_} = qrreconstruct($_)}
  0            
1035 0           debug("Found %d keys in the list of skipped files", scalar(keys %skips));
1036             }
1037              
1038             # Getting list files from MANIFEST.DEL file but not in the exclusion list
1039             {
1040 0           debug("Getting list files from: %s", MANIDEL);
  0            
  0            
1041 0           my $delfile = $self->{file_manidel}; # MANIFEST.DEL
1042 0   0       my $dellist = maniread($delfile) // {}; # { file => expire };
1043             #debug(Data::Dumper::Dumper($dellist));
1044              
1045             # Check by exclusion list
1046 0           foreach my $k (keys %$dellist) {
1047 0 0         if (_skipcheck(\%skips, $k)) { # The file is in the exclusion list.
1048 0           debug("> [SKIPPED] %s", $k);
1049 0           next;
1050             }
1051              
1052             # Adding files listed in MANIFEST.DEL to the exclusion list
1053 0           $skips{$k} = qrreconstruct($k);
1054             }
1055             #debug(Data::Dumper::Dumper(\%skips));
1056             }
1057              
1058             # Cteating MANIFEST file
1059 0           debug("Generating new manifest");
1060 0           my $new_manifest = manifind($self->dir);
1061              
1062             # We select files excluding files listed in the exclusion list
1063 0           foreach my $k (keys %$new_manifest) {
1064 0           my $nskip = _skipcheck(\%skips, $k);
1065 0 0         delete $new_manifest->{$k} if $nskip;
1066 0 0         debug("> [%s] %s", $nskip ? "SKIPPED" : " ADDED ", $k);
1067             }
1068             #debug(Data::Dumper::Dumper($new_manifest));
1069              
1070             # Save the created file
1071 0           debug("Saving manifest to %s", MANIFEST);
1072 0 0         return 0 unless maniwrite($self->{file_manifest}, $new_manifest);
1073              
1074             # Ok
1075 0           return 1;
1076             }
1077              
1078             sub _check_lockfile { # Checking if a file is private
1079 0     0     my $self = shift;
1080 0           my $file = shift;
1081 0           my $pid = $self->pid;
1082 0 0 0       return 0 unless $file && -e $file;
1083              
1084 0           my $fh;
1085 0 0         unless (open($fh, "<", $file)) {
1086 0           debug("Can't open file %s to read: %s", $file, $!);
1087 0           return 0;
1088             }
1089              
1090 0           my $l;
1091 0   0       chomp($l = <$fh>); $l //= "";
  0            
1092 0 0         unless (close $fh) {
1093 0           debug("Can't close file %s: %s", $file, $!);
1094 0           return 0;
1095             }
1096              
1097 0           my ($r_pid, $r_stamp, $r_name) = split(/#/, $l);
1098 0 0 0       return 0 unless $r_pid && ($r_pid =~ /^[0-9]{1,11}$/);
1099 0 0 0       return 1 if kill(0, $r_pid) && $pid == $r_pid;
1100 0           return 0;
1101             }
1102             sub _show_summary {
1103 0     0     my $self = shift;
1104 0           my $now = time;
1105 0           say "SHORT SUMMARY";
1106 0   0       printf " Local URL : %s\n", $self->{url} // 'undefined';
1107 0           printf " Hostname : %s\n", $self->{hostname};
1108 0           printf " Directory : %s\n", $self->dir;
1109 0 0         printf " Insecure mode : %s\n", $self->{insecure} ? 'Yes' : 'No';
1110 0   0       printf " Proxy : %s\n", $self->{proxy} || 'none';
1111 0           printf " Started : %s\n", scalar(localtime($self->{started}));
1112 0           printf " Finished : %s\n", scalar(localtime($now));
1113 0           printf " Time : %d sec\n", $now - $self->{started};
1114 0           return 1;
1115             }
1116              
1117             # Functions
1118             sub _expire { # Parse expiration time
1119 0   0 0     my $str = shift || 0;
1120              
1121 0 0         return 0 unless defined $str;
1122 0 0         return $1 if $str =~ m/^[-+]?(\d+)$/;
1123              
1124 0           my %_map = (
1125             s => 1,
1126             m => 60,
1127             h => 3600,
1128             d => 86400,
1129             w => 604800,
1130             M => 2592000,
1131             y => 31536000
1132             );
1133              
1134 0           my ($koef, $d) = $str =~ m/^([+-]?\d+)([smhdwMy])$/;
1135 0 0 0       unless ( defined($koef) && defined($d) ) {
1136 0           carp "expire(): couldn't parse '$str' into \$koef and \$d parts. Possible invalid syntax";
1137 0           return 0;
1138             }
1139 0           return $koef * $_map{ $d };
1140             }
1141             sub _skipcheck {
1142 0     0     my $sl = shift; # Link to %skip
1143 0           my $st = shift; # Test string
1144 0 0 0       return 0 unless $sl && defined($st) && ref($sl) eq 'HASH';
      0        
1145 0 0 0       return 1 if exists $sl->{$st} && defined $sl->{$st}; # Catched! - Because a direct match was found
1146              
1147             # Let's run through all the values and look for only regular expressions among them.
1148 0 0         if (grep {(ref($_) eq 'Regexp') && $st =~ $_} values %$sl) {
  0 0          
1149             # Performance optimization. Such tests would be redundant for the next check.
1150 0           $sl->{$st} = 1;
1151              
1152             # Catched!
1153 0           return 1;
1154             }
1155              
1156 0           return 0; # Not Found
1157             }
1158              
1159             1;
1160              
1161             __END__