File Coverage

blib/lib/App/DistSync.pm
Criterion Covered Total %
statement 45 495 9.0
branch 0 262 0.0
condition 0 199 0.0
subroutine 15 33 45.4
pod 12 12 100.0
total 72 1001 7.1


line stmt bran cond sub pod time code
1             package App::DistSync; # $Id: DistSync.pm 27 2019-07-23 11:26:37Z abalama $
2 1     1   66207 use warnings;
  1         10  
  1         34  
3 1     1   5 use strict;
  1         3  
  1         18  
4 1     1   642 use utf8;
  1         15  
  1         5  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             App::DistSync - Utility synchronization of the mirror distribution-sites
11              
12             =head1 VERSION
13              
14             Version 1.06
15              
16             =head1 SYNOPSIS
17              
18             use App::DistSync;
19              
20             my $ds = new App::DistSync(
21             dir => "/var/www/www.example.com/dist",
22             pid => $$,
23             );
24              
25             $ds->init or die ("Initialization error");
26              
27             $ds->sync or die ("Sync error");
28              
29             =head1 DESCRIPTION
30              
31             Utility synchronization of the mirror distribution-sites
32              
33             =head2 METHODS
34              
35             =over 4
36              
37             =item new
38              
39             my $ds = new App::DistSync(
40             dir => "/var/www/www.example.com/dist",
41             pid => $$,
42             );
43              
44             Returns the object
45              
46             =item init
47              
48             $ds->init or die ("Initialization error");
49              
50             Initializing the mirror in the specified directory
51              
52             =item sync
53              
54             $ds->sync or die ("Sync error");
55              
56             Synchronization of the specified directory with the remote resources (mirrors)
57              
58             =back
59              
60             =head2 SHARED FUNCTIONS
61              
62             =over 4
63              
64             =item fdelete
65              
66             my $status = fdelete( $file );
67              
68             Deleting a file if it exists
69              
70             =item fetch
71              
72             my $struct = fetch( $URI_STRING, "path/to/file.txt", "/tmp/file.txt" );
73              
74             Fetching file from remote resource by URI and filename.
75             The result will be written to the specified file. For example: "/tmp/file.txt"
76              
77             Function returns structure, contains:
78              
79             {
80             status => 1, # Status. 0 - Errors; 1 - OK
81             mtime => 123456789, # Last-Modified in ctime format or 0 in case of errors
82             size => 123, # Content-length
83             code => 200, # HTTP Status code
84             };
85              
86             =item touch
87              
88             my $status = touch( $file );
89              
90             Makes files exist, with current timestamp.
91             See original in L
92              
93             =back
94              
95             =head2 PRIVATE FUNCTIONS
96              
97             =over 4
98              
99             =item debug
100              
101             show debug information
102              
103             =item manifind, maniread, maniwrite
104              
105             Working with manifest file
106              
107             =item read_yaml, write_yaml
108              
109             Working with YAML files
110              
111             =back
112              
113             =head1 HISTORY
114              
115             See C file
116              
117             =head1 DEPENDENCIES
118              
119             L
120              
121             =head1 TO DO
122              
123             See C file
124              
125             =head1 BUGS
126              
127             * none noted
128              
129             =head1 SEE ALSO
130              
131             L
132              
133             =head1 AUTHOR
134              
135             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
136              
137             =head1 COPYRIGHT
138              
139             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
140              
141             =head1 LICENSE
142              
143             This program is free software; you can redistribute it and/or
144             modify it under the same terms as Perl itself.
145              
146             See C file and L
147              
148             =cut
149              
150 1     1   57 use vars qw/$VERSION/;
  1         1  
  1         71  
151             $VERSION = '1.06';
152              
153 1     1   6 use Carp;
  1         2  
  1         62  
154 1     1   7 use File::Basename;
  1         1  
  1         115  
155 1     1   458 use File::Copy qw/ mv /;
  1         4539  
  1         59  
156 1     1   7 use File::Spec;
  1         2  
  1         19  
157 1     1   6 use File::Find;
  1         1  
  1         57  
158 1     1   6 use File::Path;
  1         2  
  1         61  
159 1     1   553 use YAML::Tiny;
  1         5608  
  1         64  
160 1     1   559 use URI;
  1         4549  
  1         36  
161 1     1   465 use LWP::Simple qw/$ua head mirror/;
  1         60780  
  1         9  
162              
163 1     1   236 use base qw/Exporter/;
  1         2  
  1         545  
164             our @EXPORT = qw/
165             debug
166             /; # Auto
167             our @EXPORT_OK = qw/
168             debug
169             touch
170             fdelete
171             read_yaml
172             write_yaml
173             maniread
174             maniwrite
175             fetch
176             /; # Manual
177              
178             use constant {
179             TEMPFILE => sprintf("distsync_%s.tmp", $$),
180             TIMEOUT => 30,
181             METAFILE => 'META',
182             MANIFEST => 'MANIFEST',
183             MANISKIP => 'MANIFEST.SKIP',
184             MANITEMP => 'MANIFEST.TEMP',
185             MANILOCK => 'MANIFEST.LOCK',
186             MANIDEL => 'MANIFEST.DEL',
187             MIRRORS => 'MIRRORS',
188             README => 'README',
189             SKIPFILES => [qw/
190             META
191             MANIFEST
192             MANIFEST.SKIP
193             MANIFEST.LOCK
194             MANIFEST.TEMP
195             MANIFEST.DEL
196             MIRRORS
197             README
198             /],
199             SKIPMODE => 1,
200             LIMIT => '+1m', # '+1m' Limit gt and lt
201             EXPIRE => '+3d', # '+3d' For deleting
202             FREEZE => '+1d', # '+1d' For META test
203             QRTYPES => {
204 0           '' => sub { qr{$_[0]} },
205 0           x => sub { qr{$_[0]}x },
206 0           i => sub { qr{$_[0]}i },
207 0           s => sub { qr{$_[0]}s },
208 0           m => sub { qr{$_[0]}m },
209 0           ix => sub { qr{$_[0]}ix },
210 0           sx => sub { qr{$_[0]}sx },
211 0           mx => sub { qr{$_[0]}mx },
212 0           si => sub { qr{$_[0]}si },
213 0           mi => sub { qr{$_[0]}mi },
214 0           ms => sub { qr{$_[0]}sm },
215 0           six => sub { qr{$_[0]}six },
216 0           mix => sub { qr{$_[0]}mix },
217 0           msx => sub { qr{$_[0]}msx },
218 0           msi => sub { qr{$_[0]}msi },
219 0           msix => sub { qr{$_[0]}msix },
220             },
221 1     1   9 };
  1         2  
  1         6375  
222              
223             our $DEBUG = 0;
224              
225             # Methods
226             sub new {
227 0     0 1   my $class = shift;
228 0           my %props = @_;
229              
230 0           $props{stamp} = time;
231 0   0       $props{pid} ||= $$;
232 0   0       $props{timeout} //= TIMEOUT;
233              
234             # Directories check
235 0           my $dir = $props{dir};
236 0 0 0       carp("Can't select directory") && return unless defined $dir;
237 0           $props{file_meta} = File::Spec->catfile($dir, METAFILE);
238 0           $props{file_manifest} = File::Spec->catfile($dir, MANIFEST);
239 0           $props{file_maniskip} = File::Spec->catfile($dir, MANISKIP);
240 0           $props{file_manilock} = File::Spec->catfile($dir, MANILOCK);
241 0           $props{file_manitemp} = File::Spec->catfile($dir, MANITEMP);
242 0           $props{file_manidel} = File::Spec->catfile($dir, MANIDEL);
243 0           $props{file_mirrors} = File::Spec->catfile($dir, MIRRORS);
244 0           $props{file_readme} = File::Spec->catfile($dir, README);
245 0           $props{file_temp} = File::Spec->catfile(File::Spec->tmpdir(), TEMPFILE);
246              
247             # Read META file as YAML
248 0           my $meta = read_yaml($props{file_meta});
249 0           $props{meta} = $meta;
250              
251             # Create current static dates
252             $props{mtime_manifest} = (-e $props{file_manifest}) && -s $props{file_manifest}
253 0 0 0       ? (stat($props{file_manifest}))[9]
254             : 0;
255             $props{mtime_manidel} = (-e $props{file_manidel}) && -s $props{file_manidel}
256 0 0 0       ? (stat($props{file_manidel}))[9]
257             : 0;
258             $props{mtime_mirrors} = (-e $props{file_mirrors}) && -s $props{file_mirrors}
259 0 0 0       ? (stat($props{file_mirrors}))[9]
260             : 0;
261              
262             # Read MANIFEST, MANIFEST.SKIP, MANIFEST.DEL files
263 0           $props{manifest} = maniread($props{file_manifest});
264 0           $props{maniskip} = maniread($props{file_maniskip}, SKIPMODE);
265 0           $props{manidel} = maniread($props{file_manidel});
266 0           $props{mirrors} = maniread($props{file_mirrors});
267              
268             # TimeOut
269 0           my $to = $props{timeout};
270 0 0 0       if ($to && $to =~ /^[0-9]{1,11}$/) {
271 0           $ua->timeout($to);
272             } else {
273 0           croak(sprintf("Can't use specified timeout value: %s", $to));
274             }
275              
276 0           my $self = bless({%props}, $class);
277 0           return $self;
278             }
279             sub init { # Initialization
280 0     0 1   my $self = shift;
281 0           my $stamp = scalar(localtime($self->{stamp}));
282              
283             # MANIFEST.SKIP
284 0 0         touch($self->{file_maniskip}) or return 0;
285 0 0 0       if (-e $self->{file_maniskip} && -z $self->{file_maniskip}) {
286 0 0         open FILE, ">", $self->{file_maniskip} or return 0;
287 0           printf FILE join("\n",
288             "# Generated on %s",
289             "# List of files that should not be synchronized",
290             "#",
291             "# Format of file:",
292             "#",
293             "# dir1/dir2/.../dirn/foo.txt any comment, for example blah-blah-blah",
294             "# bar.txt any comment, for example blah-blah-blah",
295             "# baz.txt",
296             "# 'spaced dir1/foo.txt' any comment, for example blah-blah-blah",
297             "# 'spaced dir1/foo.txt' any comment, for example blah-blah-blah",
298             "# !!perl/regexp (?i-xsm:\\.bak\$) avoid all bak files",
299             "#",
300             "# See also MANIFEST.SKIP file of ExtUtils::Manifest v1.68 or later",
301             "#",
302             "",
303             "# Avoid version control files.",
304             "!!perl/regexp (?i-xsm:\\bRCS\\b)",
305             "!!perl/regexp (?i-xsm:\\bCVS\\b)",
306             "!!perl/regexp (?i-xsm:\\bSCCS\\b)",
307             "!!perl/regexp (?i-xsm:,v\$)",
308             "!!perl/regexp (?i-xsm:\\B\\.svn\\b)",
309             "!!perl/regexp (?i-xsm:\\B\\.git\\b)",
310             "!!perl/regexp (?i-xsm:\\B\\.gitignore\\b)",
311             "!!perl/regexp (?i-xsm:\\b_darcs\\b)",
312             "!!perl/regexp (?i-xsm:\\B\\.cvsignore\$)",
313             "",
314             "# Avoid temp and backup files.",
315             "!!perl/regexp (?i-xsm:~\$)",
316             "!!perl/regexp (?i-xsm:\\.(old|bak|tmp|rej)\$)",
317             "!!perl/regexp (?i-xsm:\\#\$)",
318             "!!perl/regexp (?i-xsm:\\b\\.#)",
319             "!!perl/regexp (?i-xsm:\\.#)",
320             "!!perl/regexp (?i-xsm:\\..*\\.sw.?\$)",
321             "",
322             "# Avoid prove files",
323             "!!perl/regexp (?i-xsm:\\B\\.prove\$)",
324             "",
325             "# Avoid MYMETA files",
326             "!!perl/regexp (?i-xsm:^MYMETA\\.)",
327             "",
328             "# Avoid Apache and building files",
329             "!!perl/regexp (?i-xsm:\\B\\.ht.+\$)",
330             "!!perl/regexp (?i-xsm:\\B\\.exists\$)",
331             "\n",
332             ), $stamp;
333 0           close FILE;
334             }
335              
336             # MANIFEST.DEL
337 0 0         touch($self->{file_manidel}) or return 0;
338 0 0 0       if (-e $self->{file_manidel} && -z $self->{file_manidel}) {
339 0 0         open FILE, ">", $self->{file_manidel} or return 0;
340 0           printf FILE join("\n",
341             "# Generated on %s",
342             "# List of files that must be deleted. By default, the files will be",
343             "# deleted after 3 days.",
344             "#",
345             "# Format of file:",
346             "#",
347             "# dir1/dir2/.../dirn/foo.txt 1d",
348             "# bar.txt 2M",
349             "# baz.txt",
350             "# 'spaced dir1/foo.txt' 1m",
351             "# 'spaced dir1/foo.txt' 2y",
352             "#",
353             "\n",
354             ), $stamp;
355 0           close FILE;
356             }
357              
358             # MIRRORS
359 0 0         touch($self->{file_mirrors}) or return 0;
360 0 0 0       if (-e $self->{file_mirrors} && -z $self->{file_mirrors}) {
361 0 0         open FILE, ">", $self->{file_mirrors} or return 0;
362 0           printf FILE join("\n",
363             "# Generated on %s",
364             "# List of addresses (URIs) of remote storage (mirrors).",
365             "# Must be specified at least two mirrors",
366             "#",
367             "# Format of file:",
368             "#",
369             "# http://www.example.com/dir1 any comment, for example blah-blah-blah",
370             "# http://www.example.com/dir2 any comment, for example blah-blah-blah",
371             "# 'http://www.example.com/dir2' any comment, for example blah-blah-blah",
372             "#",
373             "\n",
374             ), $stamp;
375 0           close FILE;
376             }
377              
378             # README
379 0 0         touch($self->{file_readme}) or return 0;
380 0 0 0       if (-e $self->{file_readme} && -z $self->{file_readme}) {
381 0 0         open FILE, ">", $self->{file_readme} or return 0;
382             printf FILE join("\n",
383             "# This file contains information about the resource (mirror) in the free form.",
384             "#",
385             "# Initialization date : %s",
386             "# Resource's directory : %s",
387             "#",
388             "\n",
389 0           ), $stamp, $self->{dir};
390 0           close FILE;
391             }
392              
393 0           return 1;
394             }
395             sub sync { # Synchronization. Main proccess
396 0     0 1   my $self = shift;
397 0           my $status = 0; # Статус операции для META
398              
399             # Создаем список исключений на базе прочитанного ранее SKIP + системные файлы
400 0           my @skip_keys = @{(SKIPFILES)};
  0            
401 0 0         push @skip_keys, keys %{($self->{maniskip})} if ref($self->{maniskip}) eq 'HASH';
  0            
402 0           my %skips; for (@skip_keys) {$skips{$_} = _qrreconstruct($_)}
  0            
  0            
403             #debug(Data::Dumper::Dumper(\%skips)) && return 0;
404              
405             # Удяляем файлы перечисленные в .DEL
406 0           debug("Deleting of declared files");
407 0           my $dellist = $self->{manidel};
408 0           my $expire = _expire(0);
409 0 0 0       if ($dellist && ref($dellist) eq 'HASH') {
410 0           foreach (values %$dellist) {
411 0   0       my $dt = _expire($_->[0] || 0);
412 0           $_ = [$dt];
413 0 0         $expire = $dt if $dt > $expire;
414             }
415             #debug(Data::Dumper::Dumper($dellist));
416             }
417 0 0         $expire = _expire(EXPIRE) unless $expire > 0;
418 0           debug(sprintf("Expires at %s", scalar(localtime(time + $expire))));
419 0           my $delfile = $self->{file_manidel};
420 0           my $deltime = $self->{mtime_manidel};
421 0 0 0       if ($deltime && (time - $deltime) > $expire) {
422              
423             # Удаляем файлы физически, если они есть физически и их нет в SKIP файле!
424 0           foreach my $k (keys %$dellist) {
425 0 0         if (_skipcheck(\%skips, $k)) { # Файл есть в списке исклюений
426 0           debug(sprintf("> [SKIPPED] %s", $k));
427             } else {
428 0           my $f = File::Spec->canonpath(File::Spec->catfile($self->{dir}, $k));
429 0 0         if (-e $f) {
430 0           fdelete($f);
431 0           debug(sprintf("> [DELETED] %s", $k));
432             } else {
433 0           debug(sprintf("> [MISSING] %s (%s)", $k, $f));
434             }
435             }
436             }
437              
438 0           fdelete($delfile); # Удаляем файл MANIFEST.DEL
439 0           touch($delfile); # Создаем новый файл MANIFEST.DEL
440             } else {
441 0 0         if ($deltime) {
442 0           debug(sprintf("Deleting is skipped. File %s\n\tcreated\t%s;\n\tnow\t%s;\n\texpires\t%s",
443             MANIDEL,
444             scalar(localtime($deltime)),
445             scalar(localtime(time)),
446             scalar(localtime($deltime + $expire)),
447             ));
448             } else {
449 0           debug(sprintf("Deleting is skipped. Missing file %s", MANIDEL))
450             }
451             }
452              
453             # Добавляем в список исключений на базе прочитанного ранее SKIP - DEL файлы
454 0 0         my @del_keys = keys %$dellist if ref($dellist) eq 'HASH';
455 0           for (@del_keys) {$skips{$_} = _qrreconstruct($_)}
  0            
456              
457             ################
458             # Синхронизация
459             ################
460 0           my %sync_list; # Синхронизационный список
461             my %delete_list; # Список на удаление
462              
463             # Чтение MIRRORS и принятие решения - делать синхронизацию или нет
464 0           debug("Synchronization");
465 0           my $mirror_list = $self->{mirrors};
466 0 0         my @mirrors = sort {$a cmp $b} keys %$mirror_list if ref($mirror_list) eq 'HASH';
  0            
467 0 0         if (@mirrors) {
468 0           foreach my $url (@mirrors) {
469 0           debug(sprintf("\nRESOURCE %s",$url));
470              
471             # Получение .LOCK файла, пропуск если он имеется
472 0           debug(sprintf("Fetching %s file", MANILOCK));
473 0           my $fetch_lock = fetch($url, MANILOCK, $self->{file_manitemp});
474 0 0         if ($fetch_lock->{status}) {
475 0 0         if ($self->_check_lock($self->{file_manitemp})) {
476 0           $self->{uri} = $url;
477 0           debug("> [SKIPPED] Current resource SHOULD NOT update itself");
478             } else {
479 0           debug("> [SKIPPED] Remote resource is in a state of updating. Please wait");
480             }
481 0           next;
482             }
483             #debug(Data::Dumper::Dumper($fetch_data));
484              
485             # Получение удаленного META и анализ его на status = 1. Иначе, пропуск данного ресурса
486 0           debug(sprintf("Fetching %s file", METAFILE));
487 0           my $fetch_meta = fetch($url, METAFILE, $self->{file_manitemp});
488 0 0         if ($fetch_meta->{status}) {
489 0           my $remote_meta = read_yaml($self->{file_manitemp});
490 0 0 0       if ($remote_meta && ((ref($remote_meta) eq 'ARRAY') || ref($remote_meta) eq 'YAML::Tiny')) {
    0 0        
      0        
491 0   0       $remote_meta = $remote_meta->[0] || {};
492             } elsif ($remote_meta && ref($remote_meta) eq 'HASH') {
493             # OK
494             } else {
495             #debug(Data::Dumper::Dumper(ref($remote_meta),$remote_meta));
496 0           debug("> [SKIPPED] Remote resource is unreadable. Please contact the administrator of this resource");
497 0           next;
498             }
499             #debug(Data::Dumper::Dumper($remote_meta));
500 0 0 0       if ($remote_meta && $remote_meta->{status}) {
501 0   0       my $remote_uri = $remote_meta->{uri} || 'localhost';
502 0   0       my $remote_date = $fetch_meta->{mtime} || 0;
503 0 0         my $remote_ok = (time - $remote_date) > _expire(FREEZE) ? 0 : 1;
504             debug(sprintf("REMOTE RESOURCE:"
505             ."\n\tResource:\t%s"
506             ."\n\tDate:\t\t%s"
507             ."\n\tModified:\t%s"
508             ."\n\tStatus:\t\t%s",
509             $remote_uri,
510 0 0         defined $remote_meta->{date} ? $remote_meta->{date} : 'UNKNOWN',
    0          
    0          
511             $remote_date ? scalar(localtime($remote_date)) : 'UNKNOWN',
512             $remote_ok ? "OK" : "EXPIRED"
513             ));
514 0 0         unless ($remote_ok) {
515 0 0         debug(sprintf("> [SKIPPED] Remote resource is expired. Last updated: %s",
516             $remote_date ? scalar(localtime($remote_date)) : 'UNKNOWN'
517             ));
518             next
519 0           }
520             } else {
521 0           debug("> [SKIPPED] Remote resource is in negative state. Please contact the administrator of this resource");
522 0           next;
523             }
524             }
525              
526             # Получение удаленного MANIFEST
527 0           debug(sprintf("Fetching %s file", MANIFEST));
528 0           my $fetch_mani = fetch($url, MANIFEST, $self->{file_manitemp});
529 0 0         if ($fetch_mani->{status}) {
530             # Читаем файл в отдельную структуру
531 0           my $remote_manifest = maniread($self->{file_manitemp});
532 0           my $local_manifest = $self->{manifest};
533 0           my %mtmp;
534              
535             # Два списка объединяются во временную структуру
536 0           foreach my $k (keys(%$local_manifest), keys(%$remote_manifest)) {
537 0 0         if ($mtmp{$k}) {
538 0   0       my $mt_l = $local_manifest->{$k}[0] || 0;
539 0   0       my $mt_r = $remote_manifest->{$k}[0] || 0;
540 0 0 0       $mtmp{$k}++ if $mt_l && $mt_r && $mt_l == $mt_r;
      0        
541             } else {
542 0           $mtmp{$k} = 1
543             }
544             #debug(Data::Dumper::Dumper($mt_l,$mt_r));
545             }
546              
547             # Полуаем разницумоих и удаленных файлов
548             # [<] Есть строка в левом файле
549             # [>] есть строка в правом файле
550             # [{] Более "свежий" в левом файле
551             # [}] Более "свежий" в првом файле
552             # [~] Отличаются размеры файлов в строке. Просто вывод информации об этом,
553             # т.к. более приоритетными являются даты модификации и наличие.
554             #
555             # Сравнение делается так:
556             # пробегамся по полученному хэшу и смотрим где инкремент равен 1!
557             # Там где 1 - значит данный файл есть в одном из файлов, в каком? если
558             # в левом, помечается что в левом, иначе в правом
559 0           foreach my $k (keys %mtmp) {
560 0 0 0       next unless $mtmp{$k} && $mtmp{$k} == 1;
561 0 0 0       if ($local_manifest->{$k} && $remote_manifest->{$k}) {
    0          
    0          
562 0   0       my $mt_l = $local_manifest->{$k}[0] || 0;
563 0   0       my $mt_r = $remote_manifest->{$k}[0] || 0;
564 0 0 0       if (($mt_l > $mt_r) && ($mt_l - $mt_r) > _expire(LIMIT)) {
565             # debug(sprintf("> [{] %s", $k));
566 0 0 0       } if (($mt_l < $mt_r) && ($mt_r - $mt_l) > _expire(LIMIT)) {
567 0           debug(sprintf("> [}] %s (LOC: %s < RMT: %s)", $k,
568             scalar(localtime($mt_l)),
569             scalar(localtime($mt_r)),
570             ));
571             # Скачиваем т.к. там свежее
572 0 0         unless (_skipcheck(\%skips, $k)) {
573 0   0       my $ar = $sync_list{$k} || [];
574             push @$ar, {
575             uri => $url,
576             mtime => $remote_manifest->{$k}[0],
577 0           size => $remote_manifest->{$k}[1],
578             };
579 0           $sync_list{$k} = $ar;
580             }
581             } else {
582             #debug(sprintf("> [=] %s", $k));
583             }
584             } elsif ($local_manifest->{$k}) {
585             # debug(sprintf("> [<] %s", $k));
586             } elsif ($remote_manifest->{$k}) {
587 0           debug(sprintf("> [>] %s", $k));
588             # Скачиваем, т.к. у нас такого нет
589 0 0         unless (_skipcheck(\%skips, $k)) {
590 0   0       my $ar = $sync_list{$k} || [];
591             push @$ar, {
592             uri => $url,
593             mtime => $remote_manifest->{$k}[0],
594 0           size => $remote_manifest->{$k}[1],
595             };
596 0           $sync_list{$k} = $ar;
597             }
598             } else {
599 0           debug(sprintf("> [!] %s", $k));
600             }
601             }
602 0           $status = 1; # Удалось связаться с ресурсом, значит он доступен
603             } else {
604             debug(sprintf("> [MISSING] File %s not fetched. Status code: %s",
605             MANIFEST,
606 0   0       $fetch_mani->{code} || 'UNDEFINED',
607             ));
608             #debug(Data::Dumper::Dumper($fetch_mani));
609 0           next;
610             }
611              
612             # Пробегаемся по MIRRORS удаленным файлам и добавляем его к общему списку на обновление
613 0           debug(sprintf("Fetching %s file", MIRRORS));
614 0           my $fetch_mirr = fetch($url, MIRRORS, $self->{file_manitemp});
615 0 0 0       if ($fetch_mirr->{status} && ((-z $self->{file_mirrors}) || $fetch_mirr->{mtime} > $self->{mtime_mirrors})) {
      0        
616             # Читаем файл в отдельную структуру
617 0           my $remote_mirr = maniread($self->{file_manitemp});
618             # Добаляем файл на скачку, если там есть два или более зеркал
619 0   0       my $mcnt = scalar(keys %$remote_mirr) || 0;
620 0 0 0       if ($mcnt && $mcnt > 1) {
621 0           my $k = MIRRORS;
622 0   0       my $ar = $sync_list{$k} || [];
623             push @$ar, {
624             uri => $url,
625             mtime => $fetch_mirr->{mtime},
626             size => $fetch_mirr->{size},
627 0           };
628 0           $sync_list{$k} = $ar;
629             } else {
630 0           debug(sprintf("> [SKIPPED] File %s on %s contains too few mirrors", MIRRORS, $url));
631             }
632             }
633              
634             # Пробегаемся по .DEL удаленным файлам и получаем список для принудительного удаления
635 0           debug(sprintf("Fetching %s file", MANIDEL));
636 0           my $fetch_dir = fetch($url, MANIDEL, $self->{file_manitemp});
637 0 0         if ($fetch_dir->{status}) {
638             # Читаем файл в отдельную структуру
639 0           my $remote_manidel = maniread($self->{file_manitemp});
640 0           foreach my $k (keys %$remote_manidel) {
641 0 0         unless (_skipcheck(\%skips, $k)) {
642 0 0         $delete_list{$k} ? ($delete_list{$k}++) : ($delete_list{$k} = 1)
643             }
644             }
645             }
646             } continue {
647 0           fdelete($self->{file_manitemp});
648             }
649             } else {
650 0           carp(sprintf("File %s is empty", MIRRORS));
651 0           $status = 1; # Факт невозможности получить зеркала не является признаком того что ресурс
652             # отработал с ошибками
653             }
654              
655             # Удаляем принудительно файлы полученного списка
656             #debug(Data::Dumper::Dumper(\%delete_list));
657 0           debug("Deleting files");
658 0           foreach my $k (keys %delete_list) {
659 0           my $f = File::Spec->canonpath(File::Spec->catfile($self->{dir}, $k));
660 0 0         if (-e $f) {
661 0           fdelete($f);
662 0           debug(sprintf("> [DELETED] %s", $k));
663             } else {
664 0           debug(sprintf("> [MISSING] %s (%s)", $k, $f));
665             }
666             }
667              
668             # Проходим по sync_list и скачиваем файлы, но которых НЕТ в списке на удаление
669 0           debug("Downloading files");
670             #debug(Data::Dumper::Dumper(\%sync_list));
671 0           my $total = 0;
672 0           my $cnt = 0;
673 0           my $all = scalar(keys %sync_list);
674 0           foreach my $k (sort {lc $a cmp lc $b} keys %sync_list) {$cnt++;
  0            
  0            
675 0           debug(sprintf("%03d/%03d %s", $cnt, $all, $k));
676 0           my $list = $sync_list{$k};
677 0 0 0       if ($list && ref($list) eq 'ARRAY') {
678 0   0       my $mt_l = $self->{manifest}{$k}[0] || 0;
679 0           my $dwldd = 0;
680 0           my $skipped = 0;
681 0   0       foreach my $job (sort {($b->{mtime} || 0) <=> ($a->{mtime} || 0)} @$list) {
  0   0        
682 0 0         last if $dwldd; # Выход, если скачали!
683 0           my $mt_r = $job->{mtime};
684 0           my $url = $job->{uri};
685 0           my $size = $job->{size};
686 0 0         unless ($url) {
687 0           debug(sprintf("\t[SKIPPED] No URI"));
688 0           next;
689             }
690 0 0         unless ($size) {
691 0           debug(sprintf("\t[SKIPPED] No data, %s", $url));
692 0           next;
693             }
694 0 0 0       unless ($mt_r || !$mt_l) {
695 0           debug(sprintf("\t[SKIPPED] The remote file have undefined modified date, %s", $url));
696 0           next;
697             }
698 0 0         if ($mt_l >= $mt_r) {
699 0           debug(sprintf("\t[SKIPPED] File is up to date, %s", $url));
700 0           $skipped = 1;
701 0           next;
702             }
703              
704             # Все проверки прошли, скачиваем
705 0           my $fetch_file = fetch($url, $k, $self->{file_temp});
706 0 0         if ($fetch_file->{status}) {
707 0   0       my $size_fact = $fetch_file->{size} || 0;
708 0 0 0       if ($size_fact && $size_fact == $size) {
709 0           debug(sprintf("\t[ OK ] Received %d bytes, %s", $size_fact, $url));
710 0           $total += $size_fact;
711 0           $dwldd = 1;
712             } else {
713 0           debug(sprintf("\t[ ERROR ] Can't fetch file [%s], %s",
714             $url
715             ));
716             }
717             } else {
718             debug(sprintf("\t[ ERROR ] Can't fetch file [%s], %s",
719 0 0         $fetch_file->{code} ? $fetch_file->{code} : 'UNDEFINED',
720             $url
721             ));
722             }
723              
724             }
725              
726 0 0         if ($dwldd) { # Файл скачен и лежит во временном файле
727             # Откуда : $self->{file_temp}
728             # Куда : $k
729 0           my $src = $self->{file_temp};
730 0           my $dst = File::Spec->canonpath(File::Spec->catfile($self->{dir}, $k));
731              
732             # Создаем директорию азначения
733 0           my $dir = dirname($dst); # See File::Basename
734 0           my $mkerr;
735 0           mkpath($dir, {
736             verbose => 1,
737             mode => 0777,
738             error => \$mkerr,
739             });
740 0 0 0       if ($mkerr && (ref($mkerr) eq 'ARRAY') && @$mkerr) {
      0        
741 0           foreach my $e (@$mkerr) {
742 0 0 0       next unless $e && ref($e) eq 'HASH';
743 0           while (my ($_k, $_v) = each %$e) {
744 0           carp(sprintf("%s: %s", $_k, $_v));
745             }
746             }
747             #debug(Data::Dumper::Dumper($mkerr));
748             }
749             #debug(sprintf("--> %s >>> %s", $src, $dst));
750             #debug(sprintf("--> %s >>> %s", $dst, $dir));
751              
752             # Переносим файлы по назначению
753 0           fdelete($dst);
754 0 0         unless (mv($src, $dst)) {
755 0           debug(sprintf("\t[ ERROR ] Can't move file %s to %s", $src, $dst));
756 0           carp($!);
757             }
758             } else {
759 0 0         debug(sprintf("\t[FAILED ] Can't fetch file %s", $k)) unless $skipped;
760             }
761              
762             #debug($mt_l);
763             } else {
764 0           debug(sprintf("\t[SKIPPED] Nothing to do for %s", $k));
765             }
766             }
767 0           debug(sprintf("Received %d bytes", $total));
768              
769             # Формируем новый MANIFEST
770 0           debug("Creating new manifest");
771 0           my $new_manifest = manifind($self->{dir});
772              
773             # Отбираем файлы исключая исключения
774 0           foreach my $k (keys %$new_manifest) {
775 0           my $nskip = _skipcheck(\%skips, $k);
776 0 0         delete $new_manifest->{$k} if $nskip;
777 0 0         debug(sprintf("> [%s] %s", $nskip ? "SKIPPED" : " ADDED ", $k));
778             }
779             #debug(Data::Dumper::Dumper($new_manifest));
780              
781             # Пишем сам файл
782 0           debug("Saving manifest to file ".MANIFEST);
783 0 0         return 0 unless maniwrite($self->{file_manifest}, $new_manifest);
784              
785             # Формируем новый META
786 0           debug("Creating new META file");
787             my $new_meta = {
788             last_start => $self->{stamp},
789             last_finish => time,
790             last_pid => $self->{pid},
791 0   0       uri => $self->{uri} || 'localhost',
792             date => scalar(localtime(time)),
793             status => 1, # $status,
794             # статус META выставляется только по факту успешного формирования итоговой структуры
795             # катаклога. Это изменение отличает мета-файл от только что инициализированного.
796             # Внесенные изменения см. #468
797             };
798 0 0         return 0 unless write_yaml($self->{file_meta}, $new_meta);
799              
800 0           return $status;
801             }
802             sub _check_lock { # Проверка факта, что файл является собственным
803 0     0     my $self = shift;
804 0           my $file = shift;
805 0 0 0       return 0 unless $file && -e $file;
806              
807 0           local *RD_LOCK_FILE;
808 0 0         unless (open(RD_LOCK_FILE, "<", $file)) {
809 0           carp(sprintf("Can't open file %s to read: %s", $file, $!));
810 0           return 0;
811             }
812              
813 0           my $l;
814 0 0         chomp($l = ); $l = "" unless defined $l;
  0            
815 0 0         unless (close RD_LOCK_FILE) {
816 0           carp(sprintf("Can't close file %s: %s", $file, $!));
817 0           return 0;
818             }
819              
820 0           my ($r_pid, $r_stamp, $r_name) = split(/#/, $l);
821 0 0 0       if ($r_pid && ($r_pid =~ /^[0-9]{1,11}$/) && kill(0, $r_pid)) {
      0        
822 0 0         return 1 if $self->{pid} == $r_pid;
823             }
824 0           return 0;
825             }
826              
827             # Functions
828             sub debug {
829 0 0   0 1   print STDOUT @_ ? @_ : '',"\n" if $DEBUG;
    0          
830 0           1;
831             }
832             sub touch {
833 0     0 1   my $file = shift;
834 0 0         return 0 unless defined $file;
835 0           local *FILE;
836 0 0         unless (open(FILE, ">>", $file)) {
837 0           carp(sprintf("Can't write file %s: %s",$file, $!));
838 0           return 0;
839             }
840 0 0         unless (close(FILE)) {
841 0           carp(sprintf("Can't close file %s: %s",$file, $!));
842 0           return 0;
843             }
844 0           my $t = time;
845 0 0         unless (utime($t,$t,$file)) {
846 0           carp(sprintf("Can't touch file %s: %s",$file, $!));
847 0           return 0;
848             }
849 0           return 1;
850             }
851             sub fdelete {
852 0     0 1   my $file = shift;
853 0 0 0       return 0 unless defined $file && -e $file;
854 0 0         unless (unlink($file)) {
855 0           carp(sprintf("Can't delete file %s: %s",$file, $!)) ;
856 0           return 0;
857             }
858 0           return 1;
859             }
860             sub read_yaml {
861 0     0 1   my $file = shift;
862 0 0         return [] unless defined $file;
863 0 0 0       return [] unless (-e $file) && -r $file;
864 0           my $yaml = new YAML::Tiny;
865 0           my $data = $yaml->read($file);
866 0 0         return [] unless $data;
867 0           return $data;
868             }
869             sub write_yaml {
870 0     0 1   my $file = shift;
871 0           my $data = shift;
872 0 0         return 0 unless defined $file;
873 0 0         return 0 unless defined $data;
874 0           my $yaml = new YAML::Tiny( $data );
875 0           $yaml->write( $file );
876 0           return 1;
877             }
878             sub maniread { # Reading data from MANEFEST, MIRRORS and MANEFEST.* files
879             # Original see Ext::Utils::maniread
880 0     0 1   my $mfile = shift;
881 0           my $skipflag = shift;
882              
883 0           my $read = {};
884 0 0 0       return $read unless defined($mfile) && (-e $mfile) && (-r $mfile) && (-s $mfile);
      0        
      0        
885 0           local *M;
886 0 0         unless (open M, "<", $mfile){
887 0           carp("Problem opening $mfile: $!");
888 0           return $read;
889             }
890 0           local $_;
891 0           while (){
892 0           chomp;
893 0 0         next if /^\s*#/;
894 0           my($file, $args);
895              
896 0 0 0       if ($skipflag && $_ =~ /^\s*\!\!perl\/regexp\s*/i) { # Working in SkipMode
897             #s/\r//;
898             #$_ =~ qr{^\s*\!\!perl\/regexp\s*(?:(?:'([^\\']*(?:\\.[^\\']*)*)')|([^#\s]\S*))?(?:(?:\s*)|(?:\s+(.*?)\s*))$};
899             #$args = $3;
900             #my $file = $2;
901             #if ( defined($1) ) {
902             # $file = $1;
903             # $file =~ s/\\(['\\])/$1/g;
904             #}
905 0 0         unless (($file, $args) = /^'(\\[\\']|.+)+'\s*(.*)/) {
906 0           ($file, $args) = /^(^\s*\!\!perl\/regexp\s*\S+)\s*(.*)/;
907             }
908             } else {
909             # filename may contain spaces if enclosed in ''
910             # (in which case, \\ and \' are escapes)
911 0 0         if (($file, $args) = /^'(\\[\\']|.+)+'\s*(.*)/) {
912 0           $file =~ s/\\([\\'])/$1/g;
913             } else {
914 0           ($file, $args) = /^(\S+)\s*(.*)/;
915             }
916             }
917 0 0         next unless $file;
918 0 0         $read->{$file} = [defined $args ? split(/\s+/,$args) : ""];
919             }
920 0           close M;
921 0           return $read;
922             }
923             sub manifind {
924 0     0 1   my $dir = shift;
925 0 0 0       carp("Can't specified directory") && return {} unless defined($dir) && -e $dir;
      0        
926              
927 0           my $found = {};
928 0           my $base = File::Spec->canonpath($dir);
929             #my ($volume,$sdirs,$sfile) = File::Spec->splitpath( $base );
930              
931             my $wanted = sub {
932 0     0     my $path = File::Spec->canonpath($_);
933 0           my $name = File::Spec->abs2rel( $path, $base );
934 0           my $fdir = File::Spec->canonpath($File::Find::dir);
935 0 0         return if -d $_;
936              
937 0           my $key = join("/", File::Spec->splitdir(File::Spec->catfile($name)));
938 0   0       $found->{$key} = {
      0        
939             mtime => (stat($_))[9] || 0,
940             size => (-s $_) || 0,
941             dir => $fdir,
942             path => $path,
943             file => File::Spec->abs2rel( $path, $fdir ),
944             };
945 0           };
946              
947             # We have to use "$File::Find::dir/$_" in preprocess, because
948             # $File::Find::name is unavailable.
949             # Also, it's okay to use / here, because MANIFEST files use Unix-style
950             # paths.
951 0           find({
952             wanted => $wanted,
953             no_chdir => 1,
954             }, $dir);
955              
956 0           return $found;
957             }
958             sub maniwrite {
959 0     0 1   my $file = shift;
960 0           my $mani = shift;
961 0 0 0       carp("Can't specified file") && return 0 unless defined($file);
962 0 0 0       carp("Can't specified manifest-hash") && return 0 unless defined($mani) && ref($mani) eq 'HASH';
      0        
963 0           my $file_bak = $file.".bak";
964              
965 0           rename $file, $file_bak;
966 0           local *M;
967              
968 0 0         unless (open M, ">", $file){
969 0           carp("Can't open file $file: $!");
970 0           rename $file_bak, $file;
971 0           return 0;
972             }
973              
974             # Stamp
975 0           print M "###########################################\n";
976 0           printf M "# File created at %s\n", scalar(localtime(time()));
977 0           print M "# Please, do NOT edit this file directly!!\n";
978 0           print M "###########################################\n\n";
979              
980 0           foreach my $f (sort { lc $a cmp lc $b } keys %$mani) {
  0            
981 0           my $d = $mani->{$f};
982             my $text = sprintf("%s\t%s\t%s",
983             $d->{mtime} || 0,
984             $d->{size} || 0,
985 0 0 0       $d->{mtime} ? scalar(localtime($d->{mtime})) : 'UNKNOWN',
      0        
986             );
987 0           my $tabs = (8 - (length($f)+1)/8);
988 0 0         $tabs = 1 if $tabs < 1;
989 0 0         $tabs = 0 unless $text;
990 0 0         if ($f =~ /\s/) {
991 0           $f =~ s/([\\'])/\\$1/g;
992 0           $f = "'$f'";
993             }
994 0           print M $f, "\t" x $tabs, $text, "\n";
995             }
996 0           close M;
997              
998 0           unlink $file_bak;
999              
1000 0           return 1;
1001             }
1002             sub fetch($$$) { # Returns structire
1003 0     0 1   my $url = shift;
1004 0           my $obj = shift;
1005 0           my $file = shift;
1006              
1007 0           my $ret = {
1008             status => 0, # Status
1009             mtime => 0, # Last-Modified in ctime format or 0
1010             size => 0, # tContent-length
1011             code => 0, # Status code
1012             };
1013              
1014             # Форирование URI
1015 0           my $uri = new URI($url);
1016 0           my $curpath = $uri->path();
1017 0 0         my $newpath = $curpath . (defined $obj ? "/$obj" : ''); $newpath =~ s/\/{2,}/\//;
  0            
1018 0           $uri->path($newpath);
1019 0           $ret->{uri} = $uri->as_string;
1020              
1021             # Проверка на файл
1022 0 0         unless (defined $file) {
1023 0           carp(sprintf("File to store is not defined"));
1024 0           return $ret;
1025             }
1026              
1027             # Первоначальный запрос на существование
1028 0           my ($content_type, $document_length, $modified_time, $expires, $server) = head($uri);
1029 0 0         debug(sprintf("HEAD Response:"
    0          
    0          
    0          
1030             ."\n\tContent-type:\t%s"
1031             ."\n\tContent-length:\t%s"
1032             ."\n\tModified:\t%s"
1033             ."\n\tServer:\t\t%s",
1034             defined $content_type ? $content_type : '',
1035             defined $document_length ? $document_length : 0,
1036             defined $modified_time ? scalar(localtime($modified_time)) : '',
1037             defined $server ? $server : ''
1038             ));
1039              
1040             # Анализ. Если всё плохо, выход
1041 0 0         if ($document_length) {
1042 0           $ret->{size} = $document_length;
1043             } else {
1044 0           return $ret;
1045             }
1046 0 0         if ($modified_time) {
1047 0           $ret->{mtime} = $modified_time;
1048             } else {
1049 0           carp(sprintf("Can't fetch resource %s. Header Last-Modified not returned", $uri->as_string));
1050 0           return $ret;
1051             }
1052              
1053             # Принимаем файл
1054 0           fdelete($file);
1055 0           my $code = mirror($uri, $file);
1056 0           $ret->{code} = $code;
1057 0 0 0       if (($code >= 200) && ($code < 400)) {
1058 0 0 0       if (-e $file && -s $file) {
1059 0           $ret->{status} = 1;
1060             }
1061             }
1062              
1063 0           return $ret;
1064             }
1065             sub _expire { # Перевод в expires
1066 0   0 0     my $str = shift || 0;
1067              
1068 0 0         return 0 unless defined $str;
1069 0 0         return $1 if $str =~ m/^[-+]?(\d+)$/;
1070              
1071 0           my %_map = (
1072             s => 1,
1073             m => 60,
1074             h => 3600,
1075             d => 86400,
1076             w => 604800,
1077             M => 2592000,
1078             y => 31536000
1079             );
1080              
1081 0           my ($koef, $d) = $str =~ m/^([+-]?\d+)([smhdwMy])$/;
1082 0 0 0       unless ( defined($koef) && defined($d) ) {
1083 0           carp "expire(): couldn't parse '$str' into \$koef and \$d parts. Possible invalid syntax";
1084 0           return 0;
1085             }
1086 0           return $koef * $_map{ $d };
1087             }
1088             sub _qrreconstruct {
1089             # Возвращает регулярное выражение (QR-строку)
1090             # Функция позаимствованая из YAML::Type::regexp пакета YAML::Types, немного переделанная для
1091             # адаптации нужд!!
1092             # На вход подается примерно следующее:
1093             # !!perl/regexp (?i-xsm:^\s*(error|fault|no))
1094             # это является регуляркой вида:
1095             # qr/^\s*(error|fault|no)/i
1096 0     0     my $node = shift;
1097 0 0         return undef unless defined $node;
1098 0 0         return $node unless $node =~ /^\s*\!\!perl\/regexp\s*/i;
1099 0           $node =~ s/\s*\!\!perl\/regexp\s*//i;
1100 0 0         return qr{$node} unless $node =~ /^\(\?([\^\-xism]*):(.*)\)\z/s;
1101 0           my ($flags, $re) = ($1, $2);
1102 0           $flags =~ s/-.*//;
1103 0           $flags =~ s/^\^//;
1104 0   0 0     my $sub = QRTYPES->{$flags} || sub { qr{$_[0]} };
  0            
1105 0           return $sub->($re);
1106             }
1107             sub _skipcheck {
1108 0     0     my $sl = shift; # Link to %skip
1109 0           my $st = shift; # Test string
1110 0 0 0       return 0 unless $sl && defined($st) && ref($sl) eq 'HASH';
      0        
1111 0 0 0       return 1 if exists $sl->{$st} && defined $sl->{$st}; # Исключение нашли! Т.к. нашлось прямое соответствие
1112              
1113             # Пробегаемся по всем значениям и ищем среди них только регулярки
1114 0 0         if (grep {(ref($_) eq 'Regexp') && $st =~ $_} values %$sl) {
  0 0          
1115 0           $sl->{$st} = 1; # Для очередной проверки данные проверки будут уже излишними. Оптимизация производительности
1116 0           return 1
1117             }
1118              
1119 0           return 0; # Not Found
1120             }
1121              
1122             1;
1123              
1124             __END__