File Coverage

lib/Rex/Commands/Sync.pm
Criterion Covered Total %
statement 44 186 23.6
branch 0 62 0.0
condition 0 40 0.0
subroutine 15 25 60.0
pod 0 2 0.0
total 59 315 18.7


line stmt bran cond sub pod time code
1             #
2             # (c) Jan Gehring
3             #
4              
5             =head1 NAME
6              
7             Rex::Commands::Sync - Sync directories
8              
9             =head1 DESCRIPTION
10              
11             This module can sync directories between your Rex system and your servers without the need of rsync.
12              
13             =head1 SYNOPSIS
14              
15             use Rex::Commands::Sync;
16              
17             task "prepare", "mysystem01", sub {
18             # upload directory recursively to remote system.
19             sync_up "/local/directory", "/remote/directory";
20              
21             sync_up "/local/directory", "/remote/directory", {
22             # setting custom file permissions for every file
23             files => {
24             owner => "foo",
25             group => "bar",
26             mode => 600,
27             },
28             # setting custom directory permissions for every directory
29             directories => {
30             owner => "foo",
31             group => "bar",
32             mode => 700,
33             },
34             exclude => [ '*.tmp' ],
35             parse_templates => TRUE|FALSE,
36             on_change => sub {
37             my (@files_changed) = @_;
38             },
39             };
40              
41             # download a directory recursively from the remote system to the local machine
42             sync_down "/remote/directory", "/local/directory";
43             };
44              
45             =cut
46              
47             package Rex::Commands::Sync;
48              
49 32     32   475 use v5.12.5;
  32         146  
50 32     32   200 use warnings;
  32         71  
  32         1720  
51              
52             our $VERSION = '1.14.3'; # VERSION
53              
54             require Rex::Exporter;
55 32     32   220 use base qw(Rex::Exporter);
  32         109  
  32         2209  
56 32     32   253 use vars qw(@EXPORT);
  32         145  
  32         1298  
57              
58 32     32   239 use Data::Dumper;
  32         98  
  32         1546  
59 32     32   232 use Rex::Commands;
  32         113  
  32         212  
60 32     32   355 use Rex::Commands::MD5;
  32         133  
  32         262  
61 32     32   232 use Rex::Commands::Fs;
  32         81  
  32         243  
62 32     32   237 use Rex::Commands::File;
  32         96  
  32         268  
63 32     32   238 use Rex::Commands::Download;
  32         92  
  32         258  
64 32     32   252 use Rex::Helper::Path;
  32         106  
  32         2313  
65 32     32   397 use Rex::Helper::Encode;
  32         116  
  32         2685  
66 32     32   413 use JSON::MaybeXS;
  32         79463  
  32         2285  
67 32     32   428 use Text::Glob 'glob_to_regex', 'match_glob';
  32         30218  
  32         2071  
68 32     32   243 use File::Basename 'basename';
  32         58  
  32         67294  
69              
70             @EXPORT = qw(sync_up sync_down);
71             $Text::Glob::strict_wildcard_slash = 0;
72              
73             sub sync_up {
74 0     0 0   my ( $source, $dest, @option ) = @_;
75              
76 0           my $options = {};
77              
78 0 0         if ( ref( $option[0] ) ) {
79 0           $options = $option[0];
80             }
81             else {
82 0           $options = {@option};
83             }
84              
85             # default is, parsing templates (*.tpl) files
86 0           $options->{parse_templates} = TRUE;
87              
88 0           $source = resolv_path($source);
89 0           $dest = resolv_path($dest);
90              
91             #
92             # 0. normalize local path
93             #
94 0           $source = get_file_path( $source, caller );
95              
96             #
97             # first, get all files on source side
98             #
99 0           my @local_files = _get_local_files($source);
100              
101             #print Dumper(\@local_files);
102              
103             #
104             # second, get all files from destination side
105             #
106              
107 0           my @remote_files = _get_remote_files($dest);
108              
109             #print Dumper(\@remote_files);
110              
111             #
112             # third, get the difference
113             #
114              
115 0           my @diff = _diff_files( \@local_files, \@remote_files );
116              
117             #print Dumper(\@diff);
118              
119             #
120             # fourth, build excludes list
121             #
122              
123 0   0       my $excludes = $options->{exclude} ||= [];
124 0 0         $excludes = [$excludes] unless ref($excludes) eq 'ARRAY';
125              
126 0           my @excluded_files = @{$excludes};
  0            
127              
128             #
129             # fifth, upload the different files
130             #
131              
132             my $check_exclude_file = sub {
133 0     0     my ( $file, $cmp ) = @_;
134 0 0         if ( $cmp =~ m/\// ) {
135              
136             # this is a directory exclude
137 0 0 0       if ( match_glob( $cmp, $file ) || match_glob( $cmp, substr( $file, 1 ) ) )
138             {
139 0           return 1;
140             }
141              
142 0           return 0;
143             }
144              
145 0 0         if ( match_glob( $cmp, basename($file) ) ) {
146 0           return 1;
147             }
148              
149 0           return 0;
150 0           };
151              
152 0           my @uploaded_files;
153 0           for my $file (@diff) {
154             next
155             if (
156             scalar(
157 0 0         grep { $check_exclude_file->( $file->{name}, $_ ) } @excluded_files
  0            
158             ) > 0
159             );
160              
161 0           my ($dir) = ( $file->{path} =~ m/(.*)\/[^\/]+$/ );
162 0           my ($remote_dir) = ( $file->{name} =~ m/\/(.*)\/[^\/]+$/ );
163              
164 0           my ( %dir_stat, %file_stat );
165             LOCAL {
166 0     0     %dir_stat = stat($dir);
167 0           %file_stat = stat( $file->{path} );
168 0           };
169              
170             # check for overwrites
171 0           my %file_perm = ( mode => $file_stat{mode} );
172 0 0 0       if ( exists $options->{files} && exists $options->{files}->{mode} ) {
173 0           $file_perm{mode} = $options->{files}->{mode};
174             }
175              
176 0 0 0       if ( exists $options->{files} && exists $options->{files}->{owner} ) {
177 0           $file_perm{owner} = $options->{files}->{owner};
178             }
179              
180 0 0 0       if ( exists $options->{files} && exists $options->{files}->{group} ) {
181 0           $file_perm{group} = $options->{files}->{group};
182             }
183              
184 0           my %dir_perm = ( mode => $dir_stat{mode} );
185 0 0 0       if ( exists $options->{directories}
186             && exists $options->{directories}->{mode} )
187             {
188 0           $dir_perm{mode} = $options->{directories}->{mode};
189             }
190              
191 0 0 0       if ( exists $options->{directories}
192             && exists $options->{directories}->{owner} )
193             {
194 0           $dir_perm{owner} = $options->{directories}->{owner};
195             }
196              
197 0 0 0       if ( exists $options->{directories}
198             && exists $options->{directories}->{group} )
199             {
200 0           $dir_perm{group} = $options->{directories}->{group};
201             }
202             ## /check for overwrites
203              
204 0 0         if ($remote_dir) {
205 0           mkdir "$dest/$remote_dir", %dir_perm;
206             }
207              
208             Rex::Logger::debug(
209 0           "(sync_up) Uploading $file->{path} to $dest/$file->{name}");
210 0 0 0       if ( $file->{path} =~ m/\.tpl$/ && $options->{parse_templates} ) {
211 0           my $file_name = $file->{name};
212 0           $file_name =~ s/\.tpl$//;
213              
214             file "$dest/" . $file_name,
215 0           content => template( $file->{path} ),
216             %file_perm;
217              
218 0           push @uploaded_files, "$dest/$file_name";
219             }
220             else {
221             file "$dest/" . $file->{name},
222             source => $file->{path},
223 0           %file_perm;
224              
225 0           push @uploaded_files, "$dest/" . $file->{name};
226             }
227             }
228              
229 0 0 0       if ( exists $options->{on_change}
      0        
230             && ref $options->{on_change} eq "CODE"
231             && scalar(@uploaded_files) > 0 )
232             {
233 0           Rex::Logger::debug("Calling on_change hook of sync_up");
234 0           $options->{on_change}->( map { $dest . $_->{name} } @diff );
  0            
235             }
236              
237             }
238              
239             sub sync_down {
240 0     0 0   my ( $source, $dest, @option ) = @_;
241              
242 0           my $options = {};
243              
244 0 0         if ( ref( $option[0] ) ) {
245 0           $options = $option[0];
246             }
247             else {
248 0           $options = {@option};
249             }
250              
251 0           $source = resolv_path($source);
252 0           $dest = resolv_path($dest);
253              
254             #
255             # first, get all files on dest side
256             #
257 0           my @local_files = _get_local_files($dest);
258              
259             #print Dumper(\@local_files);
260              
261             #
262             # second, get all files from source side
263             #
264              
265 0           my @remote_files = _get_remote_files($source);
266              
267             #print Dumper(\@remote_files);
268              
269             #
270             # third, get the difference
271             #
272              
273 0           my @diff = _diff_files( \@remote_files, \@local_files );
274              
275             #print Dumper(\@diff);
276              
277             #
278             # fourth, build excludes list
279             #
280              
281 0   0       my $excludes = $options->{exclude} ||= [];
282 0 0         $excludes = [$excludes] unless ref($excludes) eq 'ARRAY';
283              
284 0           my @excluded_files = map { glob_to_regex($_); } @{$excludes};
  0            
  0            
285              
286             #
287             # fifth, download the different files
288             #
289              
290 0           for my $file (@diff) {
291 0 0         next if grep { basename( $file->{name} ) =~ $_ } @excluded_files;
  0            
292              
293 0           my ($dir) = ( $file->{path} =~ m/(.*)\/[^\/]+$/ );
294 0           my ($remote_dir) = ( $file->{name} =~ m/\/(.*)\/[^\/]+$/ );
295              
296 0           my ( %dir_stat, %file_stat );
297 0           %dir_stat = stat($dir);
298 0           %file_stat = stat( $file->{path} );
299              
300             LOCAL {
301 0 0   0     if ($remote_dir) {
302 0           mkdir "$dest/$remote_dir", mode => $dir_stat{mode};
303             }
304 0           };
305              
306 0           Rex::Logger::debug(
307             "(sync_down) Downloading $file->{path} to $dest/$file->{name}");
308 0           download( $file->{path}, "$dest/$file->{name}" );
309              
310             LOCAL {
311 0     0     chmod $file_stat{mode}, "$dest/$file->{name}";
312 0           };
313             }
314              
315 0 0 0       if ( exists $options->{on_change}
      0        
316             && ref $options->{on_change} eq "CODE"
317             && scalar(@diff) > 0 )
318             {
319 0           Rex::Logger::debug("Calling on_change hook of sync_down");
320 0 0         if ( substr( $dest, -1 ) eq "/" ) {
321 0           $dest = substr( $dest, 0, -1 );
322             }
323 0           $options->{on_change}->( map { $dest . $_->{name} } @diff );
  0            
324             }
325              
326             }
327              
328             sub _get_local_files {
329 0     0     my ($source) = @_;
330              
331 0 0         if ( !-d $source ) { die("$source : no such directory."); }
  0            
332              
333 0           my @dirs = ($source);
334 0           my @local_files;
335             LOCAL {
336 0     0     for my $dir (@dirs) {
337 0           for my $entry ( list_files($dir) ) {
338 0 0         next if ( $entry eq "." );
339 0 0         next if ( $entry eq ".." );
340 0 0         if ( is_dir("$dir/$entry") ) {
341 0           push( @dirs, "$dir/$entry" );
342 0           next;
343             }
344              
345 0           my $name = "$dir/$entry";
346 0           $name =~ s/^\Q$source\E//;
347 0           push(
348             @local_files,
349             {
350             name => $name,
351             path => "$dir/$entry",
352             md5 => md5("$dir/$entry"),
353             }
354             );
355              
356             }
357             }
358 0           };
359              
360 0           return @local_files;
361             }
362              
363             sub _get_remote_files {
364 0     0     my ($dest) = @_;
365              
366 0 0         if ( !is_dir($dest) ) { die("$dest : no such directory."); }
  0            
367              
368 0           my @remote_dirs = ($dest);
369 0           my @remote_files;
370              
371 0           for my $dir (@remote_dirs) {
372 0           for my $entry ( list_files($dir) ) {
373 0 0         next if ( $entry eq "." );
374 0 0         next if ( $entry eq ".." );
375 0 0         if ( is_dir("$dir/$entry") ) {
376 0           push( @remote_dirs, "$dir/$entry" );
377 0           next;
378             }
379              
380 0           my $name = "$dir/$entry";
381 0           $name =~ s/^\Q$dest\E//;
382 0           push(
383             @remote_files,
384             {
385             name => $name,
386             path => "$dir/$entry",
387             md5 => md5("$dir/$entry"),
388             }
389             );
390             }
391             }
392              
393 0           return @remote_files;
394             }
395              
396             sub _diff_files {
397 0     0     my ( $files1, $files2 ) = @_;
398 0           my @diff;
399              
400 0           for my $file1 ( @{$files1} ) {
  0            
401             my @data = grep {
402             ( $_->{name} eq $file1->{name} )
403             && ( $_->{md5} eq $file1->{md5} )
404 0 0         } @{$files2};
  0            
  0            
405 0 0         if ( scalar @data == 0 ) {
406 0           push( @diff, $file1 );
407             }
408             }
409              
410 0           return @diff;
411             }
412              
413             1;