File Coverage

blib/lib/cPanel/SyncUtil.pm
Criterion Covered Total %
statement 193 287 67.2
branch 89 184 48.3
condition 21 68 30.8
subroutine 22 29 75.8
pod 6 6 100.0
total 331 574 57.6


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