File Coverage

blib/lib/cPanel/SyncUtil.pm
Criterion Covered Total %
statement 202 296 68.2
branch 89 184 48.3
condition 21 68 30.8
subroutine 23 30 76.6
pod 6 6 100.0
total 341 584 58.3


line stmt bran cond sub pod time code
1             package cPanel::SyncUtil;
2              
3 1     1   13811 use strict;
  1         1  
  1         27  
4 1     1   4 use warnings;
  1         1  
  1         22  
5 1     1   3 use Carp ();
  1         3  
  1         9  
6 1     1   3 use File::Spec ();
  1         1  
  1         12  
7 1     1   490 use File::Slurp ();
  1         9964  
  1         20  
8 1     1   6 use File::Find ();
  1         1  
  1         9  
9 1     1   447 use Digest::MD5::File ();
  1         50582  
  1         35  
10 1     1   1143 use Digest::SHA ();
  1         3161  
  1         25  
11 1     1   6 use Cwd ();
  1         2  
  1         11  
12 1     1   716 use Archive::Tar ();
  1         89415  
  1         3694  
13              
14             our $VERSION = '0.8';
15              
16             our %ignore_name = (
17             '.git' => 1,
18             '.svn' => 1,
19             );
20              
21             require Exporter;
22             our @ISA = qw(Exporter);
23             our @EXPORT_OK = qw(
24             build_cpanelsync
25             get_mode_string
26             get_mode_string_preserve_setuid
27             compress_files
28             _write_file
29             _read_dir
30             _read_dir_recursively
31             _lock
32             _unlock
33             _safe_cpsync_dir
34             _chown_pwd_recursively
35             _chown_recursively
36             _raw_dir
37             _sync_touchlock_pwd
38             _get_opts_hash
39             );
40              
41             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
42              
43             our $bzip;
44              
45             sub get_mode_string {
46 36     36 1 835 my ($file) = @_;
47              
48 36   50     501 my $perms = ( stat($file) )[2] || 0;
49 36         65 $perms = $perms & 0777;
50 36         215 return sprintf( '%o', $perms ); # Stringify the octal.
51             }
52              
53             sub get_mode_string_preserve_setuid {
54 0     0 1 0 my ($file) = @_;
55              
56 0   0     0 my $perms = ( stat($file) )[2] || 0;
57 0 0       0 if ( !-l _ ) {
58 0         0 $perms = $perms & 04777;
59             }
60             else {
61 0         0 $perms = $perms & 0777;
62             }
63 0         0 return sprintf( '%o', $perms ); # Stringify the octal.
64             }
65              
66 17     17   3364 sub _write_file { goto &File::Slurp::write_file; }
67              
68 3     3   183 sub _read_dir { goto &File::Slurp::read_dir; }
69              
70             # my() not our() so that they can't be [easily] changed))
71             # order by: type, then length, then case insensitive name
72             # readdir could/will be slightly different than entry because entry
73             # has varying meta data (mode, target, etc) so length and name are pidgy,
74             # not critical for operation as the point of sort holds its integrity
75             my $sort_cpanelsync_entries = sub {
76             substr( $a, 0, 1 ) cmp substr( $b, 0, 1 ) || length($a) <=> length($b) || uc($a) cmp uc($b) || $a cmp $b;
77             };
78             my %type;
79             my $sort_readdir = sub {
80             $type{$a} ||= ( -l $a ? 'l' : ( -d $a ? 'd' : 'f' ) );
81             $type{$b} ||= ( -l $b ? 'l' : ( -d $b ? 'd' : 'f' ) );
82             $type{$a} cmp $type{$b} || length($a) <=> length($b) || uc($a) cmp uc($b) || $a cmp $b;
83             };
84              
85             sub __sort_test {
86 2     2   65 my ( $type, @args ) = @_;
87 2 100       5 if ( $type == 1 ) {
88 1 50       3 %type = ref( $args[-1] ) eq 'HASH' ? %{ pop @args } : ();
  1         7  
89 1         6 return sort $sort_readdir @args;
90             }
91             else {
92 1         4 return sort $sort_cpanelsync_entries @args;
93             }
94             }
95              
96             sub _read_dir_recursively {
97 5     5   687 my $dir = shift;
98 5 50 33     79 return if ( !$dir || !-d $dir );
99 5         7 my @files;
100             my $wanted = sub {
101 30 100   30   181 return if $File::Find::name eq '.';
102              
103 28         292 my ($filename) = reverse( File::Spec->splitpath($File::Find::name) );
104 28 100 100     159 if ( exists $ignore_name{$File::Find::name} || exists $ignore_name{$filename} ) {
105 2         7 $File::Find::prune = 1;
106 2         54 return;
107             }
108              
109 26         34 my $clean = $File::Find::name;
110 26         66 $clean =~ s/\/+$//; # so that -l and -d are not confused
111              
112 26         679 push @files, $clean;
113              
114             # if (-l $clean) {
115             # push @links, $File::Find::name;
116             # }
117             # elsif(-d $clean) {
118             # push @dirs, $File::Find::name;
119             # }
120             # else {
121             # push @files, $clean;
122             # }
123 5         29 };
124 5         293 File::Find::find( { 'wanted' => $wanted, 'no_chdir' => 1, 'follow' => 0, }, $dir );
125              
126             # my @results = (sort $sort_readdir_notype @dirs), (sort $sort_readdir_notype @files), (sort $sort_readdir_notype @links);
127 5 50       41 return wantarray ? ( sort $sort_readdir @files ) : [ sort $sort_readdir @files ];
128             }
129              
130             sub _lock {
131 0     0   0 for (@_) {
132 0 0       0 next if !-d $_;
133 0         0 _write_file( File::Spec->catfile( $_, '.cpanelsync.lock' ), 'locked' );
134             }
135             }
136              
137             sub _unlock {
138 0     0   0 for (@_) {
139 0 0       0 next if !-d $_;
140 0         0 _write_file( File::Spec->catfile( $_, '.cpanelsync.lock' ), '' );
141             }
142             }
143              
144             sub _safe_cpsync_dir {
145 0     0   0 my $dir = shift;
146 0 0 0     0 return 1
      0        
      0        
      0        
147             if defined $dir
148             && $dir !~ m/\.bak$/
149             && $dir !~ m/^\./
150             && -d $dir
151             && !-l $dir;
152 0         0 return 0;
153             }
154              
155             sub _chown_pwd_recursively {
156 0     0   0 my ( $user, $group ) = @_;
157 0         0 _chown_recursively( $user, $group, '.' );
158             }
159              
160             sub _chown_recursively {
161 0     0   0 my ( $user, $group, $dir );
162              
163 0 0       0 if ( @_ == 3 ) {
    0          
164 0         0 ( $user, $group, $dir ) = @_;
165             }
166             elsif ( @_ == 2 ) {
167 0         0 ( $user, $dir ) = @_;
168             }
169             else {
170 0         0 Carp::croak('improper arguments');
171             }
172              
173 0 0       0 my $chown = defined $group ? "$user:$group" : $user;
174 0 0       0 Carp::croak 'User [and group] must be ^\w+$' if $chown !~ m{^\w+(\:\w+)?$};
175              
176 0 0       0 Carp::croak "Invalid directory $dir" if !-d $dir;
177              
178 0         0 system 'chown', '-R', $chown, $dir;
179             }
180              
181             sub _raw_dir {
182 3     3   2195 my ( $base, $archive, $verbose, @files ) = @_;
183 3 50       18 my $args_hr = ref($verbose) ? $verbose : { 'verbose' => $verbose };
184              
185 3 50       12 my $bz2_opt = $args_hr->{'verbose'} ? '-fkv' : '-fk';
186 3         7525 my $pwd = Cwd::cwd();
187 3 50       92 if ( !-d $base ) {
    50          
188 0         0 Carp::cluck "Invalid base directory $base";
189 0         0 return;
190             }
191             elsif ( !chdir $base ) {
192 0         0 Carp::cluck "Unable to chdir to directory $base: $!";
193 0         0 return;
194             }
195              
196 3 100       50 if ( !-d $archive ) {
    50          
197 1         10 $! = 20;
198 1         27 return;
199             }
200             elsif ( $archive eq '.' ) {
201 0         0 Carp::cluck "Current directory '.' cannot be used as the archive destination";
202 0         0 return;
203             }
204             else {
205 2         42 my $tar = Archive::Tar->new();
206 2         80 foreach my $file ( _read_dir($archive) ) {
207 18 100 66     3758 if ( $file =~ m{\.bz2$} && !-e $file . '.bz2.bz2' ) { # I don't believe this is correct
208 8         18 next;
209             }
210 10         79 $tar->add_files("$archive/$file");
211             }
212 2         801 $tar->write( $archive . '.tar' );
213 2         9567 system 'bzip2', $bz2_opt, $archive . '.tar';
214 2         323 unlink $archive . '.tar';
215             }
216              
217 2 50       47 if ( !chdir $archive ) {
218 0         0 Carp::cluck "Unable to complete process. Unable to chdir to $archive: $!";
219 0         0 return;
220             }
221 2 100       22 if (@files) {
222 1         9 foreach my $file (@files) {
223 3 100       5394 system 'bzip2', $bz2_opt, $file if -f $file;
224             }
225 1         26 cPanel::SyncUtil::_sync_touchlock_pwd($args_hr);
226             }
227             else {
228 1         16 cPanel::SyncUtil::_sync_touchlock_pwd($args_hr);
229             }
230              
231 2 50       50 if ( !chdir $pwd ) {
232 0         0 Carp::cluck "Failed to return back to directory $pwd: $!";
233 0         0 return;
234             }
235 2         52 return 1;
236             }
237              
238             sub _sync_touchlock_pwd {
239 3     3   742 my $verbose = $_[0];
240 3 100       54 my $args_hr = ref($verbose) ? $verbose : { 'verbose' => $verbose };
241              
242 3         17 $|++;
243 3         44 require Cwd;
244 3         37 my $cwd = Cwd::getcwd();
245              
246 3         88 print "$0 [$> $< : $cwd] Building .cpanelsync file...\n";
247              
248 3         7873 my @files = split( /\n/, `find .` );
249              
250 3         29 my %oldmd5s;
251 3 50       82 if ( -e '.cpanelsync' ) {
252 0 0       0 open my $cps_fh, '<', '.cpanelsync' or die "$cwd/.cpanelsync read failed: $!";
253 0         0 while (<$cps_fh>) {
254 0         0 chomp;
255 0         0 my ( $ftype, $rfile, $perm, $extra ) = split( /===/, $_ );
256 0 0       0 $oldmd5s{$rfile} = $extra if $ftype eq 'f';
257             }
258 0         0 close $cps_fh;
259             }
260              
261 3 50       274 open my $cpsw_fh, '>', '.cpanelsync' or die "$cwd/.cpanelsync write failed: $!";
262              
263             FILE:
264 3         32 foreach my $file (@files) {
265 34 50 33     542 if ( $file =~ /\/\.cpanelsync$/ || $file =~ /\/\.cpanelsync.lock$/ ) {
    50          
266 0         0 next FILE;
267             }
268             elsif ( $file =~ m/===/ ) {
269 0         0 Carp::cluck "improper file name detected: $file\n";
270 0         0 next FILE;
271             }
272              
273 34 100       146 if ( $file =~ /\.bz2$/ ) {
274 8         18 my $tfile = $file;
275 8         29 $tfile =~ s/\.bz2$//g;
276 8 50 33     213 next FILE if -e $file && -e $tfile;
277             }
278              
279 26 50 0     148 my $perms = ref( $args_hr->{'get_mode_string'} ) eq 'CODE' ? ( $args_hr->{'get_mode_string'}->($file) || 0 ) : get_mode_string($file);
280              
281 26 50       448 if ( -l $file ) {
    100          
282 0         0 my $point = readlink($file);
283 0         0 print {$cpsw_fh} "l===$file===$perms===$point\n";
  0         0  
284             }
285             elsif ( -d $file ) {
286 9         10 print {$cpsw_fh} "d===$file===$perms\n";
  9         64  
287             }
288             else {
289 17 50 33     148 print "Warning: zero sized file $file\n" if -z $file && $args_hr->{'verbose'};
290 17         39 my $mtime = ( stat(_) )[9];
291 17         139 my $md5sum = Digest::MD5::File::file_md5_hex($file);
292 17         2239 my $sha = Digest::SHA->new('512');
293 17         409 $sha->addfile($file);
294 17         1668 my $sha512 = $sha->hexdigest;
295 17 50 33     369 if ( exists $oldmd5s{$file} && $md5sum ne $oldmd5s{$file} ) {
    100          
296 0         0 unlink $file . '.bz2';
297 0         0 system( 'bzip2', '-kf', $file );
298             }
299             elsif ( -e $file . '.bz2' ) {
300 8 50       36 if ( $mtime > ( stat(_) )[9] ) {
301 0         0 unlink $file . '.bz2';
302 0         0 system( 'bzip2', '-kf', $file );
303             }
304             }
305             else {
306 9         24930 system( 'bzip2', '-kf', $file );
307             }
308 17         124 print {$cpsw_fh} "f===$file===$perms===$md5sum===$sha512\n";
  17         304  
309             }
310             }
311 3         43 print {$cpsw_fh} ".\n";
  3         8  
312 3         98 close $cpsw_fh;
313              
314 3         9739 system qw(bzip2 -fk .cpanelsync);
315              
316 3         85 print "Done\n";
317              
318 3         6466 system qw(touch .cpanelsync.lock);
319              
320 3         124 return 1; # make more robust
321             }
322              
323             sub _get_opts_hash {
324 0     0   0 require Getopt::Std;
325 0         0 my ( $args, $opts_ref ) = @_;
326              
327 0 0       0 $opts_ref = {} if ref $opts_ref ne 'HASH';
328 0         0 Getopt::Std::getopts( $args, $opts_ref );
329              
330 0 0       0 return wantarray ? %{$opts_ref} : $opts_ref;
  0         0  
331             }
332              
333             sub build_cpanelsync {
334 2     2 1 700 my ( $dir, $verbose ) = @_;
335 2 100       10 my $args_hr = ref($verbose) ? $verbose : { 'verbose' => $verbose };
336              
337 2         2 my $is_ok = 1;
338 2 50 33     32 if ( !$dir || !-d $dir ) {
339 0         0 Carp::croak "Invalid directory";
340             }
341              
342 2 50       8 print "$0 [$> $< : $dir] Building .cpanelsync file...\n" if $args_hr->{'verbose'};
343              
344 2         9 my $pwd = Cwd::getcwd();
345 2 50       13 if ( !chdir $dir ) {
346 0         0 Carp::croak "Unable to chdir to $dir: $!";
347             }
348              
349 2         6 my @files = _read_dir_recursively('.');
350              
351 2         4 my %oldmd5s;
352 2 50       24 if ( -e '.cpanelsync' ) {
353 0 0       0 open my $cps_fh, '<', '.cpanelsync' or Carp::croak "$dir/.cpanelsync read failed: $!";
354 0         0 while ( my $line = readline $cps_fh ) {
355 0 0       0 next if $line !~ m/^f/;
356 0         0 chomp $line;
357 0         0 my ( $ftype, $rfile, $perm, $extra ) = split( /===/, $line );
358 0         0 $oldmd5s{$rfile} = $extra;
359             }
360 0         0 close $cps_fh;
361             }
362              
363 2 50       119 open my $cpsw_fh, '>', '.cpanelsync' or Carp::croak "$dir/.cpanelsync write failed: $!";
364              
365             FILE:
366 2         10 foreach my $file (@files) {
367 10 50 33     101 next FILE if ( $file eq './.cpanelsync' || $file eq './.cpanelsync.lock' );
368              
369 10 50       39 if ( $file =~ m/===/ ) {
    50          
370 0         0 Carp::cluck "Encountered improper file name: $file";
371 0         0 next FILE;
372             }
373              
374             # Skip cpanelsync compressed files
375             elsif ( $file =~ m/\.bz2$/ ) {
376 0         0 my $tfile = $file;
377 0         0 $tfile =~ s/\.bz2$//g;
378 0 0       0 next FILE if -e $tfile;
379             }
380              
381 10 100 50     39 my $perms = ref( $args_hr->{'get_mode_string'} ) eq 'CODE' ? ( $args_hr->{'get_mode_string'}->($file) || 0 ) : get_mode_string($file);
382              
383 10 50       131 if ( -l $file ) {
    100          
384 0         0 my $point = readlink($file);
385 0         0 print {$cpsw_fh} "l===$file===$perms===$point\n";
  0         0  
386             }
387             elsif ( -d $file ) {
388 2 50       4 print {$cpsw_fh} "d===$file===$perms\n" or Carp::croak "Unable write $dir/.cpanelsync: $!";
  2         32  
389             }
390             else {
391 8 50 33     57 print "Warning: zero sized file $file\n" if -z $file && $args_hr->{'verbose'};
392 8         16 my $mtime = ( stat(_) )[9];
393 8         28 my $md5sum = Digest::MD5::File::file_md5_hex($file);
394 8         759 my $sha = Digest::SHA->new('512');
395 8         131 $sha->addfile($file);
396 8         615 my $sha512 = $sha->hexdigest;
397 8 50 33     188 if ( exists $oldmd5s{$file} && $md5sum ne $oldmd5s{$file} ) { # unlink archive if file changed
    50 33        
398 0         0 unlink $file . '.bz2';
399             }
400             elsif ( -e $file . '.bz2' && $mtime > ( stat(_) )[9] ) { # unlink archive if file is newer than archive
401 0         0 unlink $file . '.bz2';
402             }
403 8 50       10 print {$cpsw_fh} "f===$file===$perms===$md5sum===$sha512\n" or Carp::croak "Unable write $dir/.cpanelsync: $!";
  8         63  
404             }
405             }
406 2 50       20 print {$cpsw_fh} ".\n" or Carp::croak "Unable write $dir/.cpanelsync: $!";
  2         7  
407 2 50       64 close $cpsw_fh or Carp::croak "Unable to properly save $dir/.cpanelsync: $!";
408              
409 2 50       116 if ( open my $lock_fh, '>>', '.cpanelsync.lock' ) {
410 2         3 print {$lock_fh} '';
  2         4  
411 2         13 close $lock_fh;
412             }
413             else {
414 0         0 Carp::cluck "Unable to touch $dir/.cpanelsync.lock: $!";
415 0         0 $is_ok = 0;
416             }
417              
418 2 50       27 if ( !chdir $pwd ) {
419 0         0 Carp::cluck "Failed to return to $pwd: $!";
420 0         0 $is_ok = 0;
421             }
422              
423 2 50       7 print "Done\n" if $args_hr->{'verbose'};
424 2         16 return $is_ok;
425             }
426              
427             sub compress_files {
428 1     1 1 1217 my ( $dir, $verbose ) = @_;
429 1 50 33     19 if ( !$dir || !-d $dir ) {
430 0         0 Carp::croak "Invalid directory";
431             }
432              
433 1         7 my $cpanelsync = File::Spec->catfile( $dir, '.cpanelsync' );
434 1         6 my $cpanelsync_lock = File::Spec->catfile( $dir, '.cpanelsync.lock' );
435 1 0 33     17 if ( !-e $cpanelsync || -z _ || !-e $cpanelsync_lock ) {
      33        
436 1         2 build_cpanelsync( $dir, $verbose );
437             }
438              
439 1         2686 my $pwd = Cwd::cwd();
440              
441 1 50       19 if ( !chdir $dir ) {
442 0         0 Carp::croak "Unable to chdir to directory $dir: $!";
443             }
444              
445 1         8 my @to_bzip_files = get_files_from_cpanelsync('.cpanelsync');
446 1         3 foreach my $file ( @to_bzip_files, '.cpanelsync' ) {
447 5 50       32 next if $file =~ m/\.bz2$/;
448 5 50       58 if ( -e $file . '.bz2' ) {
449 0         0 my $archive_mtime = ( stat(_) )[9];
450 0 0       0 if ( ( stat($file) )[9] > $archive_mtime ) {
451 0 0       0 unlink $file . '.bz2' or Carp::cluck "Unable to remove old archive $file.bz2: $!";
452             }
453             else {
454 0         0 next; # Only update files if the archive mtime is less than the source
455             }
456             }
457 5 50       51 if ( !-e $file ) {
458 0         0 Carp::croak "Missing file $file";
459             }
460 5 50       76 bzip_file( $file, $verbose ) or Carp::croak "Failed to compress $file";
461             }
462              
463 1         40 my $tar = Archive::Tar->new();
464 1         37 foreach my $file (@to_bzip_files) {
465 4         1466 $tar->add_files($file);
466             }
467 1 50       391 if ( !chdir $pwd ) {
468 0         0 Carp::croak "Unable to chdir to directory $pwd: $!";
469             }
470 1         12 $tar->write( $dir . '.tar' );
471 1 50       1648 bzip_file( $dir . '.tar', $verbose ) or Carp::croak "Failed to compress $dir.tar";
472 1         96 unlink $dir . '.tar';
473              
474 1         70 return 1;
475             }
476              
477             sub get_files_from_cpanelsync {
478 1     1 1 2 my $cpanelsync_file = shift;
479 1 50 33     15 if ( !$cpanelsync_file || !-e $cpanelsync_file ) {
480 0 0       0 if ( -e '.cpanelsync' ) {
481 0         0 $cpanelsync_file = '.cpanelsync';
482             }
483             else {
484 0         0 Carp::croak "Unable to locate cpanelsync file";
485             }
486             }
487 1         3 my @files;
488 1 50       31 if ( open my $cpanelsync_fh, '<', $cpanelsync_file ) {
489 1         12 while ( my $line = readline $cpanelsync_fh ) {
490 6 100       24 next if $line !~ m/^f/;
491 4         14 my ( $ftype, $rfile, $perm, $extra ) = split( /===/, $line );
492 4         14 push @files, $rfile;
493             }
494 1         6 close $cpanelsync_fh;
495             }
496             else {
497 0         0 Carp::croak "Unable to read $cpanelsync_file: $!";
498             }
499 1 50       13 return wantarray ? @files : \@files;
500             }
501              
502             sub bzip_file {
503 6     6 1 18 my ( $file, $verbose ) = @_;
504 6 50       18 my $bz2_opt = $verbose ? '-fkv' : '-fk';
505 6 50       56 return if !-f $file;
506 6 100       21 if ( !$bzip ) {
507 1         4 _get_bzip_binary();
508             }
509 6         17531 system $bzip, $bz2_opt, $file;
510 6 50       204 return if !-e $file . '.bz2';
511 6         151 return 1;
512             }
513              
514             sub _get_bzip_binary {
515 1 50   1   3 return $bzip if $bzip;
516 1         5 foreach my $dir ( split( /:/, $ENV{'PATH'} ) ) {
517 6 100       88 if ( -x File::Spec->catfile( $dir, 'bzip2' ) ) {
518 1         8 $bzip = File::Spec->catfile( $dir, 'bzip2' );
519 1         3 last;
520             }
521             }
522 1 50       3 if ( !$bzip ) {
523 0         0 Carp::croak "Missing bzip2";
524             }
525 1         2 return $bzip;
526             }
527              
528             1;
529              
530             __END__