|  line  | 
 stmt  | 
 bran  | 
 cond  | 
 sub  | 
 pod  | 
 time  | 
 code  | 
| 
1
 | 
  
 
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 package File::RsyBak;  | 
| 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our $DATE = '2019-03-11'; # DATE  | 
| 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our $VERSION = '0.361'; # VERSION  | 
| 
5
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
6
 | 
2
 | 
 
 | 
 
 | 
  
2
  
 | 
 
 | 
158366
 | 
 use 5.010001;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
23
 | 
    | 
| 
7
 | 
2
 | 
 
 | 
 
 | 
  
2
  
 | 
 
 | 
11
 | 
 use strict;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
4
 | 
    | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
53
 | 
    | 
| 
8
 | 
2
 | 
 
 | 
 
 | 
  
2
  
 | 
 
 | 
12
 | 
 use warnings;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3
 | 
    | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
57
 | 
    | 
| 
9
 | 
2
 | 
 
 | 
 
 | 
  
2
  
 | 
 
 | 
2878
 | 
 use Log::ger;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
99
 | 
    | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
9
 | 
    | 
| 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
11
 | 
2
 | 
 
 | 
 
 | 
  
2
  
 | 
 
 | 
852
 | 
 use File::chdir;  | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3054
 | 
    | 
| 
 
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
4544
 | 
    | 
| 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
13
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 require Exporter;  | 
| 
14
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our @ISA       = qw(Exporter);  | 
| 
15
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our @EXPORT_OK = qw(backup);  | 
| 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
17
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 our %SPEC;  | 
| 
18
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
19
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _parse_path {  | 
| 
20
 | 
25
 | 
 
 | 
 
 | 
  
25
  
 | 
 
 | 
2152
 | 
     require Cwd;  | 
| 
21
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
22
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
71
 | 
     my ($path) = @_;  | 
| 
23
 | 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
216
 | 
     $path =~ s!/+$!!;  | 
| 
24
 | 
25
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
129
 | 
     if ($path =~ m!^(\S+)::([^/]+)/?(.*)$!) {  | 
| 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
25
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         return {  | 
| 
26
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
0
 | 
             raw=>$path, remote=>1, host=>$1,  | 
| 
27
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             proto=>"rsync", module=>$2, path=>$3,  | 
| 
28
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         };  | 
| 
29
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     } elsif ($path =~ m!([^@]+)?\@?(^\S+):(.*)$!) {  | 
| 
30
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         return {  | 
| 
31
 | 
5
 | 
 
 | 
 
 | 
 
 | 
 
 | 
34
 | 
             raw=>$path, remote=>1, host=>$2,  | 
| 
32
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             user=>$1, proto=>"ssh", path=>$3,  | 
| 
33
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         };  | 
| 
34
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     } else {  | 
| 
35
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         return {  | 
| 
36
 | 
20
 | 
 
 | 
 
 | 
 
 | 
 
 | 
910
 | 
             raw=>$path, remote=>0, path=>$path,  | 
| 
37
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             abs_path=>Cwd::abs_path($path)  | 
| 
38
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         };  | 
| 
39
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
40
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
41
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
42
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 # rsync requires all source to be local, or remote (same host). check sources  | 
| 
43
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 # before we run rsync, so we can report the error and die earlier.  | 
| 
44
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _check_sources {  | 
| 
45
 | 
12
 | 
 
 | 
 
 | 
  
12
  
 | 
 
 | 
62
 | 
     my ($sources) = @_;  | 
| 
46
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
47
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
30
 | 
     my $all_local = 1;  | 
| 
48
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
41
 | 
     for (@$sources) {  | 
| 
49
 | 
15
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
69
 | 
         if ($_->{remote}) { $all_local = 0; last }  | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
6
 | 
    | 
| 
 
 | 
3
 | 
 
 | 
 
 | 
 
 | 
 
 | 
5
 | 
    | 
| 
50
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
51
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
52
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
26
 | 
     my $all_remote = 1;  | 
| 
53
 | 
12
 | 
 
 | 
 
 | 
 
 | 
 
 | 
29
 | 
     for (@$sources) {  | 
| 
54
 | 
14
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
39
 | 
         if (!$_->{remote}) { $all_remote = 0; last }  | 
| 
 
 | 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
13
 | 
    | 
| 
 
 | 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
22
 | 
    | 
| 
55
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
56
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
57
 | 
12
 | 
  
100
  
 | 
  
100
  
 | 
 
 | 
 
 | 
135
 | 
     return [400, "Sources must be all local or all remote"]  | 
| 
58
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         unless $all_remote || $all_local;  | 
| 
59
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
60
 | 
11
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
38
 | 
     if ($all_remote) {  | 
| 
61
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
3
 | 
         my $host;  | 
| 
62
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
5
 | 
         for (@$sources) {  | 
| 
63
 | 
4
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
14
 | 
             $host //= $_->{host};  | 
| 
64
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             return [400, "Remote sources must all be from the same machine"]  | 
| 
65
 | 
4
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
13
 | 
                 if $host ne $_->{host};  | 
| 
66
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
67
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
68
 | 
10
 | 
 
 | 
 
 | 
 
 | 
 
 | 
72
 | 
     [200, "OK"];  | 
| 
69
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
70
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
71
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 $SPEC{backup} = {  | 
| 
72
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     v             => 1.1,  | 
| 
73
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     summary       =>  | 
| 
74
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         'Backup files/directories with histories, using rsync',  | 
| 
75
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     args          => {  | 
| 
76
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         source           => {  | 
| 
77
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Director(y|ies) to backup',  | 
| 
78
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             #schema       => ['any*'   => {  | 
| 
79
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             #    of => ['str*', ['array*' => {of=>'str*'}]]  | 
| 
80
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             #}],  | 
| 
81
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => 'str*', # temp, because in pericmd when specifying as arg#0, there is a warning of JSON decoding failure  | 
| 
82
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             req          => 1,  | 
| 
83
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             pos          => 0,  | 
| 
84
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
85
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         target           => {  | 
| 
86
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Backup destination',  | 
| 
87
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => ['str*'   => {}],  | 
| 
88
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             req          => 1,  | 
| 
89
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             pos          => 1,  | 
| 
90
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
91
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         histories        => {  | 
| 
92
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Histories/history levels',  | 
| 
93
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => ['array' => {  | 
| 
94
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 default      => [-7, 4, 3],  | 
| 
95
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 of           => 'int*',  | 
| 
96
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }],  | 
| 
97
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
98
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
99
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Specifies number of backup histories to keep for level 1, 2, and so on. If  | 
| 
100
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 number is negative, specifies number of days to keep instead (regardless of  | 
| 
101
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 number of histories).  | 
| 
102
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
103
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
104
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
105
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         extra_dir        => {  | 
| 
106
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      =>  | 
| 
107
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 'Whether to force creation of source directory in target',  | 
| 
108
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => ['bool'   => {}],  | 
| 
109
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
110
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
111
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 If set to 1, then backup(source => '/a', target => '/backup/a') will create  | 
| 
112
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 another 'a' directory in target, i.e. /backup/a/current/a. Otherwise, contents  | 
| 
113
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 of a/ will be directly copied under /backup/a/current/.  | 
| 
114
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
115
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Will always be set to 1 if source is more than one, but default to 0 if source  | 
| 
116
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 is a single directory. You can set this to 1 to so that behaviour when there is  | 
| 
117
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 a single source is the same as behaviour when there are several sources.  | 
| 
118
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
119
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
120
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
121
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         backup           => {  | 
| 
122
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Whether to do backup or not',  | 
| 
123
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => [bool     => {  | 
| 
124
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 default      => 1,  | 
| 
125
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }],  | 
| 
126
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
127
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
128
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 If backup=1 and rotate=0 then will only create new backup without rotating  | 
| 
129
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 histories.  | 
| 
130
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
131
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
132
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
133
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         rotate           => {  | 
| 
134
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Whether to do rotate after backup or not',  | 
| 
135
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => [bool     => {  | 
| 
136
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 default      => 1,  | 
| 
137
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }],  | 
| 
138
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
139
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
140
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 If backup=0 and rotate=1 then will only do history rotating.  | 
| 
141
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
142
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
143
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
144
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         extra_rsync_opts => {  | 
| 
145
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             summary      => 'Pass extra options to rsync command',  | 
| 
146
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             schema       => [array    => {  | 
| 
147
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 of           => 'str*',  | 
| 
148
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }],  | 
| 
149
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
150
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
151
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Extra options to pass to rsync command when doing backup. Note that the options  | 
| 
152
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 will be shell quoted, , so you should pass it unquoted, e.g. ['--exclude',  | 
| 
153
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 '/Program Files'].  | 
| 
154
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
155
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
156
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
157
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     },  | 
| 
158
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
159
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     examples => [  | 
| 
160
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         {  | 
| 
161
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             argv         => ['/home/jajang/mydata','/backup/jajang/mydata'],  | 
| 
162
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             test         => 0,  | 
| 
163
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             'x.doc.show_result' => 0,  | 
| 
164
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             description  => <<'_',  | 
| 
165
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
166
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 Backup /home/jajang/mydata to /backup/jajang/mydata using the default number of  | 
| 
167
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 histories ([-7, 4, 3]).  | 
| 
168
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
169
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 _  | 
| 
170
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         },  | 
| 
171
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     ],  | 
| 
172
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
173
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     deps => {  | 
| 
174
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         all => [  | 
| 
175
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             {prog => 'nice'},  | 
| 
176
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             {prog => 'rsync'}, # XXX not needed if backup=0  | 
| 
177
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             {prog => 'rm'},    # XXX not needed if rotate=0  | 
| 
178
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         ],  | 
| 
179
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     },  | 
| 
180
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 };  | 
| 
181
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub backup {  | 
| 
182
 | 
8
 | 
 
 | 
 
 | 
  
8
  
 | 
  
1
  
 | 
4042288
 | 
     require File::Flock::Retry;  | 
| 
183
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1321
 | 
     require File::Path;  | 
| 
184
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
98
 | 
     require File::Which;  | 
| 
185
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
186
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
86
 | 
     my %args = @_;  | 
| 
187
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
188
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # XXX schema  | 
| 
189
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
82
 | 
     my $source    = $args{source} or return [400, "Please specify source"];  | 
| 
190
 | 
8
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
79
 | 
     my @sources   = ref($source) eq 'ARRAY' ? @$source : ($source);  | 
| 
191
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
54
 | 
     for (@sources) { $_ = _parse_path($_) }  | 
| 
 
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
65
 | 
    | 
| 
192
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
102
 | 
     my $res = _check_sources(\@sources);  | 
| 
193
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
46
 | 
     return $res unless $res->[0] == 200;  | 
| 
194
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
33
 | 
     my $target    = $args{target} or return [400, "Please specify target"];  | 
| 
195
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
27
 | 
     $target       = _parse_path($target);  | 
| 
196
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $target->{remote} and  | 
| 
197
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
40
 | 
         return [400, "Sorry, target can't be remote at the moment"];  | 
| 
198
 | 
8
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
43
 | 
     my $histories = $args{histories} // [-7, 4, 3];  | 
| 
199
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
46
 | 
     ref($histories) eq 'ARRAY' or return [400, "histories must be array"];  | 
| 
200
 | 
8
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
86
 | 
     my $backup    = $args{backup} // 1;  | 
| 
201
 | 
8
 | 
 
 | 
  
 50
  
 | 
 
 | 
 
 | 
44
 | 
     my $rotate    = $args{rotate} // 1;  | 
| 
202
 | 
8
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
60
 | 
     my $extra_dir = $args{extra_dir} || (@sources > 1);  | 
| 
203
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
204
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # sanity  | 
| 
205
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
119
 | 
     my $rsync_path = File::Which::which("rsync")  | 
| 
206
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         or return [500, "Can't find rsync in PATH"];  | 
| 
207
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
208
 | 
8
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
1447
 | 
     unless (-d $target->{abs_path}) {  | 
| 
209
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
36
 | 
         log_debug("Creating target directory %s ...", $target->{abs_path});  | 
| 
210
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         File::Path::make_path($target->{abs_path})  | 
| 
211
 | 
4
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
1029
 | 
             or return [500, "Error: Can't create target directory ".  | 
| 
212
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 "$target->{abs_path}: $!"];  | 
| 
213
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
214
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
215
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
142
 | 
     my $lock = File::Flock::Retry->lock("$target->{abs_path}/.lock");  | 
| 
216
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
217
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
1724
 | 
     if ($backup) {  | 
| 
218
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         _backup(  | 
| 
219
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             \@sources, $target,  | 
| 
220
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             {  | 
| 
221
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 extra_dir        => $extra_dir,  | 
| 
222
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 extra_rsync_opts => $args{extra_rsync_opts},  | 
| 
223
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
65
 | 
             });  | 
| 
224
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
225
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
226
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
292
 | 
     if ($rotate) {  | 
| 
227
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
100
 | 
         _rotate($target->{abs_path}, $histories);  | 
| 
228
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
229
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
230
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
360
 | 
     [200, "OK"];  | 
| 
231
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
232
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
233
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _backup {  | 
| 
234
 | 
8
 | 
 
 | 
 
 | 
  
8
  
 | 
 
 | 
640
 | 
     require POSIX;  | 
| 
235
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
6870
 | 
     require String::ShellQuote; String::ShellQuote->import;  | 
| 
 
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
608
 | 
    | 
| 
236
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
237
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
31
 | 
     my ($sources, $target, $opts) = @_;  | 
| 
238
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     log_info("Starting backup %s ==> %s ...",  | 
| 
239
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
48
 | 
                 [map {$_->{raw}} @$sources], $target);  | 
| 
 
 | 
9
 | 
 
 | 
 
 | 
 
 | 
 
 | 
97
 | 
    | 
| 
240
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
40
 | 
     my $cmd;  | 
| 
241
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     $cmd = join(  | 
| 
242
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         "",  | 
| 
243
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         "nice -n19 rsync ",  | 
| 
244
 | 
2
 | 
 
 | 
 
 | 
 
 | 
 
 | 
100
 | 
         ($opts->{extra_rsync_opts} ? map { (shell_quote($_), " ") }  | 
| 
245
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
13
 | 
              @{$opts->{extra_rsync_opts}} : ()),  | 
| 
246
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         "-a --del --force --ignore-errors --ignore-existing ",  | 
| 
247
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         (log_is_debug() ? "-v " : ""),  | 
| 
248
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         ((-e "$target->{abs_path}/current") ?  | 
| 
249
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
              "--link-dest ".shell_quote("$target->{abs_path}/current")." "  | 
| 
250
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                  : ""),  | 
| 
251
 | 
8
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
42
 | 
         map({ (shell_quote($_->{raw}), ($opts->{extra_dir} ? "" : "/"), " ") }  | 
| 
 
 | 
9
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
682
 | 
    | 
| 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
 
 | 
 
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
252
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 @$sources),  | 
| 
253
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         shell_quote("$target->{abs_path}/.tmp/"),  | 
| 
254
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     );  | 
| 
255
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
632
 | 
     log_debug("Running rsync ...");  | 
| 
256
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
45
 | 
     log_trace("system(): $cmd");  | 
| 
257
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
368365
 | 
     system $cmd;  | 
| 
258
 | 
8
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
294
 | 
     log_warn("rsync didn't succeed ($?)".  | 
| 
259
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                    ", please recheck") if $?;  | 
| 
260
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
261
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     # but continue anyway, half backups are better than nothing  | 
| 
262
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
263
 | 
8
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
505
 | 
     if (-e "$target->{abs_path}/current") {  | 
| 
264
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
87
 | 
         my $tspath = "$target->{abs_path}/.current.timestamp";  | 
| 
265
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
175
 | 
         my @st     = stat($tspath);  | 
| 
266
 | 
4
 | 
 
 | 
  
 66
  
 | 
 
 | 
 
 | 
526
 | 
         my $tstamp = POSIX::strftime(  | 
| 
267
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             "%Y-%m-%d\@%H:%M:%S+00",  | 
| 
268
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             gmtime( $st[9] || time() )); # timestamp might not exist yet  | 
| 
269
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
152
 | 
         log_debug("rename $target->{abs_path}/current ==> ".  | 
| 
270
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         "hist.$tstamp ...");  | 
| 
271
 | 
4
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
270
 | 
         unless (rename "$target->{abs_path}/current",  | 
| 
272
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 "$target->{abs_path}/hist.$tstamp") {  | 
| 
273
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
57
 | 
             log_warn("Failed renaming $target->{abs_path}/current ==> ".  | 
| 
274
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                          "hist.$tstamp: $!");  | 
| 
275
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
276
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
45
 | 
         log_debug("touch $tspath ...");  | 
| 
277
 | 
4
 | 
 
 | 
 
 | 
 
 | 
 
 | 
101
 | 
         system "touch ".shell_quote($tspath);  | 
| 
278
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
279
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
280
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
13277
 | 
     log_debug("rename $target->{abs_path}/.tmp ==> current ...");  | 
| 
281
 | 
8
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
576
 | 
     unless (rename "$target->{abs_path}/.tmp",  | 
| 
282
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             "$target->{abs_path}/current") {  | 
| 
283
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
53
 | 
         log_warn("Failed renaming $target->{abs_path}/.tmp ==> current: $!");  | 
| 
284
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
285
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
286
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
110
 | 
     log_info("Finished backup %s ==> %s", $sources, $target);  | 
| 
287
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
288
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
289
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 sub _rotate {  | 
| 
290
 | 
8
 | 
 
 | 
 
 | 
  
8
  
 | 
 
 | 
183
 | 
     require String::ShellQuote; String::ShellQuote->import;  | 
| 
 
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1229
 | 
    | 
| 
291
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1000
 | 
     require Time::Local;  | 
| 
292
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
293
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1969
 | 
     my ($target, $histories) = @_;  | 
| 
294
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
44
 | 
     log_info("Rotating backup histories in %s (%s) ...",  | 
| 
295
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 $target, $histories);  | 
| 
296
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
297
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
351
 | 
     local $CWD = $target; # throws exception when failed  | 
| 
298
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
299
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
941
 | 
     my $now = time();  | 
| 
300
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
78
 | 
     for my $level (1 .. @$histories) {  | 
| 
301
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
53
 | 
         my $is_highest_level  = $level == @$histories;  | 
| 
302
 | 
16
 | 
  
100
  
 | 
 
 | 
 
 | 
 
 | 
131
 | 
         my $prefix            = "hist" . ($level == 1 ? '' : $level);  | 
| 
303
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
126
 | 
         my $prefix_next_level = "hist" . ($level + 1);  | 
| 
304
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
59
 | 
         my $n                 = $histories->[$level - 1];  | 
| 
305
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
28
 | 
         my $moved             = 0;  | 
| 
306
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
307
 | 
16
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
47
 | 
         if ($n > 0) {  | 
| 
308
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
139
 | 
             log_debug("Only keeping $n level-$level histories ...");  | 
| 
309
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
1329
 | 
             my @f = reverse sort grep { !/\.tmp$/ } glob "$prefix.*";  | 
| 
 
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
107
 | 
    | 
| 
310
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             #untaint for @f;  | 
| 
311
 | 
16
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
73
 | 
             my $any_tagged = (grep {/t$/} @f) ? 1 : 0;  | 
| 
 
 | 
8
 | 
 
 | 
 
 | 
 
 | 
 
 | 
56
 | 
    | 
| 
312
 | 
16
 | 
 
 | 
 
 | 
 
 | 
 
 | 
130
 | 
             for my $f (@f[ $n .. @f - 1 ]) {  | 
| 
313
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
24
 | 
                 my ($st, $tagged) = $f =~ /[^.]+\.(.+?)(t)?$/;  | 
| 
314
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
12
 | 
                 my $f2 = "$prefix_next_level.$st";  | 
| 
315
 | 
1
 | 
  
 50
  
 | 
  
 33
  
 | 
 
 | 
 
 | 
93
 | 
                 if (!$is_highest_level &&  | 
| 
 
 | 
 
 | 
 
 | 
  
 33
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
 
 | 
 
 | 
 
 | 
  
 33
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
316
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         !$moved && ($tagged || !$any_tagged)) {  | 
| 
317
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
18
 | 
                     log_debug("Moving history level: $f -> $f2");  | 
| 
318
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
54
 | 
                     rename $f, $f2;  | 
| 
319
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
14
 | 
                     $moved++;  | 
| 
320
 | 
1
 | 
  
 50
  
 | 
 
 | 
 
 | 
 
 | 
10
 | 
                     if ($f ne $f[0]) {  | 
| 
321
 | 
1
 | 
 
 | 
 
 | 
 
 | 
 
 | 
37
 | 
                         rename $f[0], "$f[0]t";  | 
| 
322
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     }  | 
| 
323
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 } else {  | 
| 
324
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     log_debug("Removing history: $f ...");  | 
| 
325
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     system "nice -n19 rm -rf " . shell_quote($f);  | 
| 
326
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 }  | 
| 
327
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }  | 
| 
328
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         } else {  | 
| 
329
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             $n = -$n;  | 
| 
330
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             log_debug("Only keeping $n day(s) of level-$level histories ...");  | 
| 
331
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             my @f = reverse sort grep { !/\.tmp$/ } glob "$prefix.*";  | 
| 
 
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
332
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             my $any_tagged = ( grep {/t$/} @f ) ? 1 : 0;  | 
| 
 
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
333
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             for my $f (@f) {  | 
| 
334
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 my ($st, $tagged) = $f =~ /[^.]+\.(.+?)(t)?$/;  | 
| 
335
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 my $f2 = "$prefix_next_level.$st";  | 
| 
336
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 my $t;  | 
| 
337
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 $st =~ /(\d\d\d\d)-(\d\d)-(\d\d)\@(\d\d):(\d\d):(\d\d)\+00/;  | 
| 
338
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 $t = Time::Local::timegm($6, $5, $4, $3, $2 - 1, $1) if $1;  | 
| 
339
 | 
  
0
  
 | 
  
  0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
                 unless ($st && $t) {  | 
| 
340
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     log_warn("Wrong format of history, ignored: $f");  | 
| 
341
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     next;  | 
| 
342
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 }  | 
| 
343
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 if ($t > $now) {  | 
| 
344
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     log_warn("History in the future, ignored: $f");  | 
| 
345
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     next;  | 
| 
346
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 }  | 
| 
347
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 my $delta = ($now - $t) / 86400;  | 
| 
348
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 if ($delta > $n) {  | 
| 
349
 | 
  
0
  
 | 
  
  0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
                     if (!$is_highest_level &&  | 
| 
 
 | 
 
 | 
 
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
 
 | 
 
 | 
 
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
350
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                             !$moved && ( $tagged || !$any_tagged)) {  | 
| 
351
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         log_debug("Moving history level: $f -> $f2");  | 
| 
352
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         rename $f, $f2;  | 
| 
353
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         $moved++;  | 
| 
354
 | 
  
0
  
 | 
  
  0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         if ($f ne $f[0]) {  | 
| 
355
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                             rename $f[0], "$f[0]t";  | 
| 
356
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         }  | 
| 
357
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     } else {  | 
| 
358
 | 
  
0
  
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         log_debug("Removing history: $f ...");  | 
| 
359
 | 
0
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                         system "nice -n19 rm -rf " . shell_quote($f);  | 
| 
360
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                     }  | 
| 
361
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
                 }  | 
| 
362
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
             }  | 
| 
363
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
         }  | 
| 
364
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
     }  | 
| 
365
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 }  | 
| 
366
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
367
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 1;  | 
| 
368
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 # ABSTRACT: Backup files/directories with histories, using rsync  | 
| 
369
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
    | 
| 
370
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 
 | 
 __END__  |