File Coverage

blib/lib/Rex/Commands/PerlSync.pm
Criterion Covered Total %
statement 155 176 88.0
branch 30 58 51.7
condition 16 40 40.0
subroutine 23 23 100.0
pod 0 2 0.0
total 224 299 74.9


line stmt bran cond sub pod time code
1             package Rex::Commands::PerlSync;
2             $Rex::Commands::PerlSync::VERSION = '0.001';
3 1     1   228104 use v5.12.5;
  1         5  
4 1     1   7 use warnings;
  1         2  
  1         122  
5              
6             require Rex::Exporter;
7 1     1   7 use base qw(Rex::Exporter);
  1         3  
  1         677  
8 1     1   13865 use vars qw(@EXPORT);
  1         2  
  1         56  
9              
10 1     1   888 use Rex::Commands;
  1         84222  
  1         10  
11 1     1   3167 use Rex::Commands::MD5;
  1         29537  
  1         14  
12 1     1   198 use Rex::Commands::Fs;
  1         12046  
  1         14  
13 1     1   1618 use Rex::Commands::File;
  1         24043  
  1         9  
14 1     1   402 use Rex::Commands::Download;
  1         76775  
  1         23  
15 1     1   272 use Rex::Helper::Path;
  1         10  
  1         166  
16 1     1   28 use Rex::Helper::Encode;
  1         3292  
  1         124  
17 1     1   17 use Text::Glob 'match_glob';
  1         2314  
  1         3356  
18              
19             @EXPORT = qw(sync_up sync_down);
20             $Text::Glob::strict_wildcard_slash = 0;
21              
22             sub sync_up
23             {
24 2     2 0 431071 my ($source, $dest, @option) = @_;
25              
26 2         14 my $options = {};
27              
28 2 100       18 if (ref($option[0])) {
29 1         7 $options = $option[0];
30             }
31             else {
32 1         66 $options = {@option};
33             }
34              
35             # default is, parsing templates (*.tpl) files
36 2         30 $options->{parse_templates} = TRUE;
37              
38 2         28 $source = resolv_path($source);
39 2         21 $dest = resolv_path($dest);
40              
41             #
42             # 0. normalize local path
43             #
44 2         40 $source = get_file_path($source, caller);
45              
46             #
47             # first, build excludes list
48             #
49              
50 2   100     670 my $excludes = $options->{exclude} ||= [];
51 2 50       17 $excludes = [$excludes] unless ref($excludes) eq 'ARRAY';
52              
53 2         6 my @excluded_files = @{$excludes};
  2         7  
54              
55             my $check_exclude_file = sub {
56 11     11   33 my ($file) = @_;
57 11         72 $file =~ s{^/}{};
58              
59 11         37 for my $cmp (@excluded_files) {
60 13 100       1968 return 1 if match_glob($cmp, $file);
61             }
62              
63 9         190 return 0;
64 2         31 };
65              
66             #
67             # second, get all files on source side (minus excludes)
68             #
69 2         24 my @local_files = _get_local_files($source, $check_exclude_file);
70              
71             #
72             # third, get all files from destination side (minus excludes)
73             #
74              
75 2         33 my @remote_files = _get_remote_files($dest, $check_exclude_file);
76              
77             #
78             # fourth, get the difference
79             #
80              
81 2         111 my @diff = _diff_files(\@local_files, \@remote_files);
82              
83             #
84             # fifth, upload the different files
85             #
86              
87 2         5 my @uploaded_files;
88 2         6 for my $file (@diff) {
89 6         121 my ($dir) = ($file->{path} =~ m/(.*)\/[^\/]+$/);
90 6         92 my ($remote_dir) = ($file->{name} =~ m/\/(.*)\/[^\/]+$/);
91              
92 6         11 my (%dir_stat, %file_stat);
93             LOCAL {
94 6     6   5131 %dir_stat = stat($dir);
95 6         2176 %file_stat = stat($file->{path});
96 6         158 };
97              
98             # check for overwrites
99 6         1913 my %file_perm = (mode => $file_stat{mode});
100 6 0 33     54 if (exists $options->{files} && exists $options->{files}->{mode}) {
101 0         0 $file_perm{mode} = $options->{files}->{mode};
102             }
103              
104 6 0 33     21 if (exists $options->{files} && exists $options->{files}->{owner}) {
105 0         0 $file_perm{owner} = $options->{files}->{owner};
106             }
107              
108 6 0 33     22 if (exists $options->{files} && exists $options->{files}->{group}) {
109 0         0 $file_perm{group} = $options->{files}->{group};
110             }
111              
112 6         21 my %dir_perm = (mode => $dir_stat{mode});
113 6 0 33     31 if (
114             exists $options->{directories}
115             && exists $options->{directories}->{mode}
116             )
117             {
118 0         0 $dir_perm{mode} = $options->{directories}->{mode};
119             }
120              
121 6 0 33     21 if (
122             exists $options->{directories}
123             && exists $options->{directories}->{owner}
124             )
125             {
126 0         0 $dir_perm{owner} = $options->{directories}->{owner};
127             }
128              
129 6 0 33     19 if (
130             exists $options->{directories}
131             && exists $options->{directories}->{group}
132             )
133             {
134 0         0 $dir_perm{group} = $options->{directories}->{group};
135             }
136             ## /check for overwrites
137              
138 6 100       23 if ($remote_dir) {
139 4         75 mkdir "$dest/$remote_dir", %dir_perm;
140             }
141              
142             Rex::Logger::debug(
143 6         56253 "(sync_up) Uploading $file->{path} to $dest/$file->{name}"
144             );
145 6 50 33     95 if ($file->{path} =~ m/\.tpl$/ && $options->{parse_templates}) {
146 0         0 my $file_name = $file->{name};
147 0         0 $file_name =~ s/\.tpl$//;
148              
149             file "$dest/" . $file_name,
150 0         0 content => template($file->{path}),
151             %file_perm;
152              
153 0         0 push @uploaded_files, "$dest/$file_name";
154             }
155             else {
156             file "$dest/" . $file->{name},
157             source => $file->{path},
158 6         128 %file_perm;
159              
160 6         6239307 push @uploaded_files, "$dest/" . $file->{name};
161             }
162             }
163              
164 2 0 33     268 if (
      33        
165             exists $options->{on_change}
166             && ref $options->{on_change} eq "CODE"
167             && scalar(@uploaded_files) > 0
168             )
169             {
170 0         0 Rex::Logger::debug("Calling on_change hook of sync_up");
171 0         0 $options->{on_change}->(map { $dest . $_->{name} } @diff);
  0         0  
172             }
173              
174             }
175              
176             sub sync_down
177             {
178 2     2 0 40885 my ($source, $dest, @option) = @_;
179              
180 2         16 my $options = {};
181              
182 2 100       26 if (ref($option[0])) {
183 1         18 $options = $option[0];
184             }
185             else {
186 1         10 $options = {@option};
187             }
188              
189 2         26 $source = resolv_path($source);
190 2         34 $dest = resolv_path($dest);
191              
192             #
193             # first, build excludes list
194             #
195              
196 2   100     53 my $excludes = $options->{exclude} ||= [];
197 2 50       33 $excludes = [$excludes] unless ref($excludes) eq 'ARRAY';
198              
199 2         11 my @excluded_files = @{$excludes};
  2         15  
200              
201             my $check_exclude_file = sub {
202 11     11   42 my ($file) = @_;
203 11         68 $file =~ s{^/}{};
204              
205 11         46 for my $cmp (@excluded_files) {
206 13 100       1144 return 1 if match_glob($cmp, $file);
207             }
208              
209 9         262 return 0;
210 2         51 };
211              
212             #
213             # second, get all files on dest side (minus excludes)
214             #
215 2         134 my @local_files = _get_local_files($dest, $check_exclude_file);
216              
217             #
218             # third, get all files from source side (minus excludes)
219             #
220              
221 2         51 my @remote_files = _get_remote_files($source, $check_exclude_file);
222              
223             #
224             # fourth, get the difference
225             #
226              
227 2         35 my @diff = _diff_files(\@remote_files, \@local_files);
228              
229             #
230             # fifth, download the different files
231             #
232              
233 2         14 for my $file (@diff) {
234 6         68764 my ($dir) = ($file->{path} =~ m/(.*)\/[^\/]+$/);
235 6         71 my ($remote_dir) = ($file->{name} =~ m/\/(.*)\/[^\/]+$/);
236              
237 6         24 my (%dir_stat, %file_stat);
238 6         93 %dir_stat = stat($dir);
239 6         3703 %file_stat = stat($file->{path});
240              
241             LOCAL {
242 6 100   6   5087 if ($remote_dir) {
243 4         49 mkdir "$dest/$remote_dir", mode => $dir_stat{mode};
244             }
245 6         1823 };
246              
247 6         59774 Rex::Logger::debug(
248             "(sync_down) Downloading $file->{path} to $dest/$file->{name}"
249             );
250 6         137 download($file->{path}, "$dest/$file->{name}");
251              
252             LOCAL {
253 6     6   5915 chmod $file_stat{mode}, "$dest/$file->{name}";
254 6         66201 };
255             }
256              
257 2 0 33     37124 if (
      33        
258             exists $options->{on_change}
259             && ref $options->{on_change} eq "CODE"
260             && scalar(@diff) > 0
261             )
262             {
263 0         0 Rex::Logger::debug("Calling on_change hook of sync_down");
264 0 0       0 if (substr($dest, -1) eq "/") {
265 0         0 $dest = substr($dest, 0, -1);
266             }
267 0         0 $options->{on_change}->(map { $dest . $_->{name} } @diff);
  0         0  
268             }
269              
270             }
271              
272             sub _get_local_files
273             {
274 4     4   14 my ($source, $exclude_sub) = @_;
275              
276 4 50       103 if (!-d $source) { die("$source : no such directory."); }
  0         0  
277              
278 4         17 my @dirs = ($source);
279 4         27 my @local_files;
280             LOCAL {
281 4     4   3156 for my $dir (@dirs) {
282 7         496430 for my $entry (list_files($dir)) {
283 11 50       1566135 next if ($entry eq ".");
284 11 50       34 next if ($entry eq "..");
285              
286 11         27 my $name = "$dir/$entry";
287 11         137 $name =~ s/^\Q$source\E//;
288 11 100       60 next if $exclude_sub->($name);
289              
290 9 100       74 if (is_dir("$dir/$entry")) {
291 3         1207 push(@dirs, "$dir/$entry");
292 3         20 next;
293             }
294             push(
295 6         1695 @local_files,
296             {
297             name => $name,
298             path => "$dir/$entry",
299             md5 => md5("$dir/$entry"),
300             }
301             );
302              
303             }
304             }
305 4         95 };
306              
307 4         928012 return @local_files;
308             }
309              
310             sub _get_remote_files
311             {
312 4     4   31 my ($dest, $exclude_sub) = @_;
313              
314 4 50       56 if (!is_dir($dest)) { die("$dest : no such directory."); }
  0         0  
315              
316 4         1544 my @remote_dirs = ($dest);
317 4         16 my @remote_files;
318              
319 4         24 for my $dir (@remote_dirs) {
320 7         492200 for my $entry (list_files($dir)) {
321 11 50       1425628 next if ($entry eq ".");
322 11 50       62 next if ($entry eq "..");
323              
324 11         29 my $name = "$dir/$entry";
325 11         169 $name =~ s/^\Q$dest\E//;
326 11 100       113 next if $exclude_sub->($name);
327              
328 9 100       107 if (is_dir("$dir/$entry")) {
329 3         996 push(@remote_dirs, "$dir/$entry");
330 3         28 next;
331             }
332              
333             push(
334 6         1816 @remote_files,
335             {
336             name => $name,
337             path => "$dir/$entry",
338             md5 => md5("$dir/$entry"),
339             }
340             );
341             }
342             }
343              
344 4         937761 return @remote_files;
345             }
346              
347             sub _diff_files
348             {
349 4     4   21 my ($files1, $files2) = @_;
350 4         15 my %checksums;
351             my @diff;
352              
353 4         40 for my $file (@{$files2}) {
  4         13  
354 0         0 $checksums{$file->{name}} = $file->{md5};
355             }
356              
357 4         35 for my $file (@{$files1}) {
  4         14  
358             push @diff, $file
359 12 50 33     57 if !$checksums{$file->{name}} || $checksums{$file->{name}} ne $file->{md5};
360             }
361              
362 4         27 return @diff;
363             }
364              
365             1;
366              
367             =head1 NAME
368              
369             Rex::Commands::PerlSync - Sync directories, better
370              
371             =head1 DESCRIPTION
372              
373             This module is a L command which synchronizes directories. It is a fork of
374             L with the same interface and a couple of improvements:
375              
376             =over
377              
378             =item
379              
380             Files and directories are now excluded before calculating md5 of the directory,
381             vastly improving the speed if there are a lot of excluded files.
382              
383             =item
384              
385             Excludes now work using the exact path rather than file name, making it easier
386             to exclude files or entire directories.
387              
388             =back
389              
390             =head1 SYNOPSIS
391              
392             use Rex::Commands::PerlSync;
393              
394             task "prepare", "mysystem01", sub {
395             # upload directory recursively to remote system.
396             sync_up "/local/directory", "/remote/directory";
397              
398             sync_up "/local/directory", "/remote/directory", {
399             # setting custom file permissions for every file
400             files => {
401             owner => "foo",
402             group => "bar",
403             mode => 600,
404             },
405             # setting custom directory permissions for every directory
406             directories => {
407             owner => "foo",
408             group => "bar",
409             mode => 700,
410             },
411             exclude => [ '*.tmp' ],
412             parse_templates => TRUE|FALSE,
413             on_change => sub {
414             my (@files_changed) = @_;
415             },
416             };
417              
418             # download a directory recursively from the remote system to the local machine
419             sync_down "/remote/directory", "/local/directory";
420             };
421              
422             =head1 AUTHOR
423              
424             Original code from L - see authors.
425              
426             Modified by Bartosz Jarzyna Ebbrtj.pro@gmail.comE
427              
428             =head1 COPYRIGHT AND LICENSE
429              
430             Copyright (C) 2024 by Bartosz Jarzyna
431              
432             This library is free software; you can redistribute it and/or modify
433             it under the same terms as Perl itself.
434