File Coverage

blib/lib/File/BackupCopy.pm
Criterion Covered Total %
statement 104 136 76.4
branch 31 56 55.3
condition 4 11 36.3
subroutine 19 21 90.4
pod 4 8 50.0
total 162 232 69.8


line stmt bran cond sub pod time code
1             package File::BackupCopy;
2 5     5   178876 use strict;
  5         34  
  5         219  
3 5     5   33 use warnings;
  5         8  
  5         178  
4 5     5   2668 use File::Copy;
  5         12295  
  5         311  
5 5     5   35 use File::Temp 'tempfile';
  5         11  
  5         197  
6 5     5   29 use File::Basename;
  5         10  
  5         2441  
7 5     5   35 use File::Spec;
  5         12  
  5         83  
8 5     5   21 use Exporter;
  5         11  
  5         152  
9 5     5   26 use re '/aa';
  5         9  
  5         370  
10 5     5   31 use Carp;
  5         9  
  5         324  
11 5     5   36 use Errno;
  5         16  
  5         603  
12              
13             our $VERSION = '1.02';
14             our @ISA = qw(Exporter);
15             our @EXPORT = qw(BACKUP_NONE
16             BACKUP_SINGLE
17             BACKUP_SIMPLE
18             BACKUP_NUMBERED
19             BACKUP_AUTO
20             backup_copy);
21              
22             our @EXPORT_OK = qw(backup_copy_simple backup_copy_numbered backup_copy_auto);
23              
24             use constant {
25 5         5895 BACKUP_NONE => 0, # No backups at all (none,off)
26             BACKUP_SINGLE => 1, # Always make single backups (never,simple)
27             BACKUP_SIMPLE => 1,
28             BACKUP_NUMBERED => 2, # Always make numbered backups (t,numbered)
29             BACKUP_AUTO => 3 # Make numbered if numbered backups exist,
30             # simple otherwise (nil,existing)
31 5     5   39 };
  5         9  
32              
33             my %envtrans = (
34             none => BACKUP_NONE,
35             off => BACKUP_NONE,
36             never => BACKUP_SIMPLE,
37             simple => BACKUP_SIMPLE,
38             t => BACKUP_NUMBERED,
39             numbered => BACKUP_NUMBERED,
40             nil => BACKUP_AUTO,
41             existing => BACKUP_AUTO
42             );
43              
44             my %backup_func = (
45             BACKUP_NONE() => sub {},
46             BACKUP_SIMPLE() => \&backup_copy_simple,
47             BACKUP_NUMBERED() => \&backup_copy_numbered,
48             BACKUP_AUTO() => \&backup_copy_auto
49             );
50              
51             sub backup_copy {
52 22     22 1 8338 my $file = shift;
53              
54 22         43 my ($type, %opts);
55 22 100       104 if (@_ == 1) {
    50          
56 5         12 $type = shift;
57             } elsif (@_ % 2 == 0) {
58 17         45 %opts = @_;
59 17         37 $type = delete $opts{type};
60             } else {
61 0         0 croak "wrong number of arguments";
62             }
63              
64 22 100       57 unless (defined($type)) {
65 12   100     43 my $v = $ENV{VERSION_CONTROL} || BACKUP_AUTO;
66 12 100       34 if (exists($envtrans{$v})) {
67 9         19 $type = $envtrans{$v};
68             } else {
69 3         7 $type = BACKUP_AUTO;
70             }
71             }
72 22         46 &{$backup_func{$type}}($file, %opts);
  22         75  
73             }
74              
75             sub _backup_copy_error {
76 8     8   1288 my ($error, $msg) = @_;
77 8 100       26 if ($error) {
78 4         12 $$error = $msg;
79 4         25 return undef;
80             }
81 4         1313 confess $msg;
82             }
83              
84             sub backup_copy_simple {
85 20     20 1 1909 my $file_name = shift;
86 20         84 local %_ = @_;
87 20         51 my $error = delete $_{error};
88 20         36 my $dir = delete $_{dir};
89 20 50       72 croak "unrecognized keyword arguments" if keys %_;
90 20         43 my $backup_name = $file_name . '~';
91 20 100       44 if ($dir) {
92 11         434 $backup_name = File::Spec->catfile($dir, basename($backup_name));
93             }
94 20 100       144 copy($file_name, $backup_name)
95             or return _backup_copy_error($error,
96             "failed to copy $file_name to $backup_name: $!");
97 14         4779 return $backup_name;
98             }
99              
100             sub backup_copy_internal {
101 27     27 0 49 my $file_name = shift;
102              
103 27         108 my ($if_exists, $error, $dir);
104 27 50       107 if (@_ == 1) {
    50          
105 0         0 $if_exists = shift;
106             } elsif (@_ % 2 == 0) {
107 27         78 local %_ = @_;
108 27         58 $if_exists = delete $_{if_exists};
109 27         51 $error = delete $_{error};
110 27         48 $dir = delete $_{dir};
111 27 50       115 croak "unrecognized keyword arguments" if keys %_;
112             } else {
113 0         0 croak "wrong number of arguments";
114             }
115            
116 27 100       488 my $backup_stub = $dir ? File::Spec->catfile($dir, basename($file_name))
117             : $file_name;
118 4         19 my $num = (sort { $b <=> $a }
119             map {
120 27 50       1746 if (/.+\.~(\d+)~$/) {
  13         111  
121 13         74 $1
122             } else {
123             ()
124 0         0 }
125             } glob("$backup_stub.~*~"))[0];
126              
127 27 100       118 if (defined($num)) {
128 9         16 ++$num;
129             } else {
130 18 100       65 return backup_copy_simple($file_name, error => $error, dir => $dir)
131             if $if_exists;
132 8         42 $num = '1';
133             }
134            
135 17   66     39 my ($fh, $tempname) = eval { tempfile(DIR => $dir || dirname($file_name)) };
  17         610  
136 17 100       6107 if ($@) {
137 2         9 return _backup_copy_error($error, $@);
138             }
139              
140 15 50       64 copy($file_name, $fh)
141             or return _backup_copy_error($error,
142             "failed to make a temporary copy of $file_name: $!");
143 15         3633 close $fh;
144            
145 15         73 my $backup_name = rename_backup($tempname, $backup_stub, $num, $error);
146 15 50       58 unless ($backup_name) {
147 0 0       0 unlink($tempname) or carp("can't unlink $tempname: $!");
148             }
149 15         112 return $backup_name;
150             }
151              
152             # The rename_backup function performs the final stage of numbered backup
153             # creation: atomical rename of the temporary backup file to the actual
154             # backup name.
155             # The calling sequence is:
156             # rename_backup($tempfile, $backup_stub, $num, $error)
157             # where $tempfile is the name of the temporary file holding the backup,
158             # $backup_stub is the name of the backup file without the actual
159             # numbered suffix (may contain directory components,
160             # if required).
161             # $num is the first unused backup number,
162             # $error is the reference to error message storage or undef.
163             # The function creates the new backup file name from $backup_stub and
164             # $num and attempts to rename $tempfile to it. If the rename failed
165             # because such file already exists (i.e. another process created it in
166             # between), the function increases the $num and retries. The process
167             # continues until the rename succeeds or a fatal error is encountered,
168             # whichever occurs first.
169             #
170             # Three versions of the function are provided. The right one to use
171             # is selected when the module is loaded:
172              
173             BEGIN {
174 5 50 0 5   27 if (eval { symlink("",""); 1 }) {
  5 0       83  
  5         30  
175 5         2877 *{rename_backup} = \&rename_backup_posix;
176 0         0 } elsif ($^O eq 'MSWin32' && eval { require Win32API::File }) {
177 0         0 Win32API::File->import(qw(MoveFile fileLastError));
178 0         0 *{rename_backup} = \&rename_backup_win32;
179             } else {
180 0         0 warn "using last resort rename method susceptible to a race condition";
181 0         0 *{rename_backup} = \&rename_backup_last_resort;
182             }
183             }
184              
185             # rename_backup_posix - rename_backup for POSIX systems.
186             # -------------------
187             # In order to ensure atomic rename, the temporary file is first
188             # symlinked to the desired backup name. This will fail if the
189             # name already exists, in which case the function will try next
190             # backup number. Once the symlink is created, temporary file
191             # is renamed to it. This operation will silently destroy the
192             # symlink and replace it with the backup file.
193             sub rename_backup_posix {
194 15     15 0 55 my ($tempfilename, $backup_stub, $num, $error) = @_;
195 15         29 my $backup_name;
196 15         23 while (1) {
197 15         50 $backup_name = "$backup_stub.~$num~";
198 15 50       581 last if symlink($tempfilename, $backup_name);
199 0 0       0 unless ($!{EEXIST}) {
200 0         0 return _backup_copy_error($error,
201             "can't link $tempfilename to $backup_name: $!");
202             }
203 0         0 ++$num;
204             }
205            
206 15 50       1239 unless (rename($tempfilename, $backup_name)) {
207 0         0 return _backup_copy_error($error,
208             "can't rename temporary file to $backup_name: $!");
209             }
210 15         104 return $backup_name;
211             }
212              
213             # rename_backup_win32 - rename_backup for MSWin32 systems with Win32API::File
214             # -------------------
215             # This function is used if Win32API::File was loaded successfully. It uses
216             # the MoveFile function to ensure atomic renames.
217             sub rename_backup_win32 {
218 0     0 0 0 my ($tempfilename, $backup_stub, $num, $error) = @_;
219 0         0 my $backup_name;
220 0         0 while (1) {
221 0         0 $backup_name = "$backup_stub.~$num~";
222 0 0       0 last if MoveFile($tempfilename, $backup_name);
223             # 80 - ERROR_FILE_EXISTS
224             # - "The file exists."
225             # 183 - ERROR_ALREADY_EXISTS
226             # - "Cannot create a file when that file already exists."
227 0 0 0     0 unless (fileLastError() == 80 || fileLastError() == 183) {
228 0         0 return _backup_copy_error($error,
229             "can't rename $tempfilename to $backup_name: $^E");
230             }
231 0         0 ++$num;
232             }
233 0         0 return $backup_name;
234             }
235              
236             # rename_backup_last_resort - a weaker version for the rest of systems
237             # -------------------------
238             # It is enabled on systems not offering the symlink function (except where
239             # Win32API::File can be used). This version uses a combination of -f test
240             # and rename. It suffers from an obvious race condition which occurs in
241             # the time window between these.
242             sub rename_backup_last_resort {
243 0     0 0 0 my ($tempfilename, $backup_stub, $num, $error) = @_;
244 0         0 my $backup_name;
245 0         0 while (1) {
246 0         0 $backup_name = "$backup_stub.~$num~";
247 0 0       0 unless (-f $backup_name) {
248 0 0       0 last if rename($tempfilename, $backup_name);
249 0         0 return _backup_copy_error($error,
250             "can't rename temporary file to $backup_name: $!");
251             }
252 0         0 ++$num;
253             }
254 0         0 return $backup_name;
255             }
256              
257             sub backup_copy_numbered {
258 10     10 1 2376 my ($file_name, %opts) = @_;
259 10         28 $opts{if_exists} = 0;
260 10         42 backup_copy_internal($file_name, %opts);
261             }
262              
263             sub backup_copy_auto {
264 17     17 1 2733 my ($file_name, %opts) = @_;
265 17         45 $opts{if_exists} = 1;
266 17         60 backup_copy_internal($file_name, %opts);
267             }
268            
269             1;
270             __END__