File Coverage

blib/lib/App/DistSync.pm
Criterion Covered Total %
statement 39 490 7.9
branch 0 262 0.0
condition 0 199 0.0
subroutine 13 31 41.9
pod 6 13 46.1
total 58 995 5.8


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