File Coverage

blib/lib/File/AtomicWrite.pm
Criterion Covered Total %
statement 160 226 70.8
branch 67 122 54.9
condition 29 63 46.0
subroutine 23 27 85.1
pod 8 8 100.0
total 287 446 64.3


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Like IO::AtomicWrite, except uses File::Temp to create the temporary
4             # file, and offers various degrees of more paranoid write handling, and
5             # means to set Unix file permissions and ownerships on the resulting
6             # file. Run perldoc(1) on this module for more information.
7              
8             package File::AtomicWrite;
9              
10 4     4   266519 use strict;
  4         34  
  4         115  
11 4     4   22 use warnings;
  4         7  
  4         114  
12              
13 4     4   22 use Carp qw/croak/;
  4         7  
  4         199  
14 4     4   25 use File::Basename qw/dirname/;
  4         7  
  4         402  
15 4     4   32 use File::Path qw/mkpath/;
  4         8  
  4         265  
16 4     4   2814 use File::Temp qw/tempfile/;
  4         84392  
  4         244  
17 4     4   30 use IO::Handle;
  4         8  
  4         9874  
18              
19             our $VERSION = '1.20';
20              
21             # Default options
22             my %default_params = ( MKPATH => 0, template => ".tmp.XXXXXXXXXX" );
23              
24             ######################################################################
25             #
26             # Class methods
27              
28             # Accepts output filename, perhaps optional tmp file template, and
29             # a filehandle or scalar ref, and handles all the details in a
30             # single shot.
31             sub write_file {
32 23     23 1 23959 my $class = shift;
33 23   100     71 my $user_params = shift || {};
34              
35 23 100       65 if ( !exists $user_params->{input} ) {
36 2         270 croak "missing 'input' option";
37             }
38              
39 21         46 my ( $tmp_fh, $tmp_filename, $params_ref, $digest ) = _init($user_params);
40              
41             # Attempt cleanup if things go awry (use the OO interface and custom
42             # signal handlers of your own if this is a problem)
43 19     0   367 local $SIG{TERM} = sub { _cleanup( $tmp_fh, $tmp_filename ); exit };
  0         0  
  0         0  
44 19     0   269 local $SIG{INT} = sub { _cleanup( $tmp_fh, $tmp_filename ); exit };
  0         0  
  0         0  
45 19     2   238 local $SIG{__DIE__} = sub { _cleanup( $tmp_fh, $tmp_filename ) };
  2         35  
46              
47 19         60 my $input_ref = ref $params_ref->{input};
48 19 50 33     63 unless ( $input_ref eq 'SCALAR' or $input_ref eq 'GLOB' ) {
49 0         0 croak "invalid type for input option: " . ref $input_ref;
50             }
51              
52 19         35 my $input = $params_ref->{input};
53 19 50       41 if ( $input_ref eq 'SCALAR' ) {
    0          
54 19 50       185 unless ( print $tmp_fh $$input ) {
55 0         0 my $save_errstr = $!;
56 0         0 _cleanup( $tmp_fh, $tmp_filename );
57 0         0 croak "error printing to temporary file: $save_errstr";
58             }
59 19 100 100     73 if ( exists $params_ref->{CHECKSUM}
60             and !exists $params_ref->{checksum} ) {
61 1         101 $digest->add($$input);
62             }
63              
64             } elsif ( $input_ref eq 'GLOB' ) {
65 0         0 while ( my $line = <$input> ) {
66 0 0       0 unless ( print $tmp_fh $line ) {
67 0         0 my $save_errstr = $!;
68 0         0 _cleanup( $tmp_fh, $tmp_filename );
69 0         0 croak "error printing to temporary file: $save_errstr";
70             }
71              
72 0 0 0     0 if ( exists $params_ref->{CHECKSUM}
73             and !exists $params_ref->{checksum} ) {
74 0         0 $digest->add($$input);
75             }
76             }
77             }
78              
79 19         60 _resolve( $tmp_fh, $tmp_filename, $params_ref, $digest );
80             }
81              
82             sub new {
83 8     8 1 1004721 my $class = shift;
84 8         20 my $self = {};
85 8   100     33 my $user_param = shift || {};
86              
87             croak "option 'input' only for write_file class method"
88 8 50       24 if exists $user_param->{input};
89              
90 8         30 @{$self}{qw/_tmp_fh _tmp_filename _params _digest/} = _init($user_param);
  7         28  
91              
92 7         28 bless $self, $class;
93 7         21 return $self;
94             }
95              
96             sub safe_level {
97 0     0 1 0 my $class = shift;
98 0   0     0 my $level = shift || croak 'safe_level() requires a value';
99 0         0 File::Temp->safe_level($level);
100             }
101              
102             sub set_template {
103 1     1 1 941 my $class = shift;
104 1   33     5 my $template = shift || croak 'set_template() requires a template';
105 1         3 $default_params{template} = $template;
106 1         5 return;
107             }
108              
109             ######################################################################
110             #
111             # Instance methods
112              
113             sub fh {
114 4     4 1 421 shift->{_tmp_fh};
115             }
116              
117             sub filename {
118 4     4 1 355 shift->{_tmp_filename};
119             }
120              
121             sub checksum {
122 1     1 1 38 my $self = shift;
123             $self->{_params}->{checksum} = shift
124 1   33     8 || croak 'checksum requires an argument';
125              
126 1 50       6 if ( !$self->{_digest} ) {
127 1         3 $self->{_params}->{CHECKSUM} = 1;
128 1         4 $self->{_digest} = _init_checksum( $self->{_params} );
129             }
130              
131 1         6 return $self;
132             }
133              
134             sub commit {
135 4     4 1 662 my $self = shift;
136 4         9 _resolve( @{$self}{qw/_tmp_fh _tmp_filename _params _digest/} );
  4         16  
137             }
138              
139             sub DESTROY {
140 7     7   4345 my $self = shift;
141 7         19 _cleanup( @{$self}{qw/_tmp_fh _tmp_filename/} );
  7         22  
142             }
143              
144             # For when (if) things go awry
145             sub _cleanup {
146 10     10   26 my ( $tmp_fh, $tmp_filename ) = @_;
147             # recommended by perlport(1) prior to unlink/rename calls
148 10 50       67 close $tmp_fh if defined $tmp_fh;
149 10 50       506 unlink $tmp_filename if defined $tmp_filename;
150             }
151              
152             sub _init {
153 29   50 29   69 my $user_params = shift || {};
154 29         138 my $params_ref = { %default_params, %$user_params };
155              
156 29 100 66     166 if ( !exists $params_ref->{file}
157             or !defined $params_ref->{file} ) {
158 1         200 croak q{missing 'file' option};
159             }
160              
161 28         65 my $digest = _init_checksum($params_ref);
162              
163 28         1157 $params_ref->{_dir} = dirname( $params_ref->{file} );
164 28 100       483 if ( !-d $params_ref->{_dir} ) {
165 2         9 _mkpath( $params_ref->{MKPATH}, $params_ref->{_dir} );
166             }
167              
168 27 100       98 if ( exists $params_ref->{tmpdir} ) {
169 2 100 66     31 if ( !-d $params_ref->{tmpdir}
170             and $params_ref->{tmpdir} ne $params_ref->{_dir} ) {
171 1         5 _mkpath( $params_ref->{MKPATH}, $params_ref->{tmpdir} );
172              
173             # partition sanity check
174 1         4 my @dev_ids = map { ( stat $params_ref->{$_} )[0] } qw/_dir tmpdir/;
  2         26  
175 1 50       5 if ( $dev_ids[0] != $dev_ids[1] ) {
176 0         0 croak 'tmpdir and file directory on different partitions';
177             }
178             }
179             } else {
180 25         62 $params_ref->{tmpdir} = $params_ref->{_dir};
181             }
182              
183 27 50       65 if ( exists $params_ref->{safe_level} ) {
184 0         0 File::Temp->safe_level( $params_ref->{safe_level} );
185             }
186              
187             my ( $tmp_fh, $tmp_filename ) = tempfile(
188             $params_ref->{template},
189             DIR => $params_ref->{tmpdir},
190 27         116 UNLINK => 0
191             );
192 26 50       8146 if ( !defined $tmp_fh ) {
193 0         0 die "unable to obtain temporary filehandle\n";
194             }
195              
196 26 50 33     134 if ( exists $params_ref->{binmode_layer}
    100 66        
197             and defined $params_ref->{binmode_layer} ) {
198 0         0 binmode( $tmp_fh, $params_ref->{binmode_layer} );
199             } elsif ( exists $params_ref->{BINMODE} and $params_ref->{BINMODE} ) {
200 1         4 binmode($tmp_fh);
201             }
202              
203 26         78 return $tmp_fh, $tmp_filename, $params_ref, $digest;
204             }
205              
206             sub _init_checksum {
207 29     29   47 my $params_ref = shift;
208 29         45 my $digest = 0;
209              
210 29 100 66     114 if ( exists $params_ref->{CHECKSUM} and $params_ref->{CHECKSUM} ) {
211 3         8 eval { require Digest::SHA1; };
  3         20  
212 3 50       11 if ($@) {
213 0         0 croak 'cannot checksum as lack Digest::SHA1';
214             }
215 3         20 $digest = Digest::SHA1->new;
216             } else {
217             # so can rely on 'exists' test elsewhere hereafter
218 26         61 delete $params_ref->{CHECKSUM};
219             }
220              
221 29         64 return $digest;
222             }
223              
224             sub _resolve {
225 23     23   37 my $tmp_fh = shift;
226 23         41 my $tmp_filename = shift;
227 23         31 my $params_ref = shift;
228 23         38 my $digest = shift;
229              
230 23 100 100     71 if ( exists $params_ref->{CHECKSUM}
231             and !exists $params_ref->{checksum} ) {
232 1         10 $params_ref->{checksum} = $digest->hexdigest;
233             }
234              
235             # Help the bits reach the disk
236 23 50       661 $tmp_fh->flush() or die "flush() error: $!\n";
237             # TODO may need eval or exclude on other platforms
238 23 50       155 if ( $^O !~ m/Win32/ ) {
239 23 50       100030 $tmp_fh->sync() or die "sync() error: $!\n";
240             }
241              
242 23         138 eval {
243 23 100       86 if ( exists $params_ref->{min_size} ) {
244 2         9 _check_min_size( $tmp_fh, $params_ref->{min_size} );
245             }
246 22 100       83 if ( exists $params_ref->{CHECKSUM} ) {
247 3         20 _check_checksum( $tmp_fh, $params_ref->{checksum} );
248             }
249             };
250 23 100       54 if ($@) {
251 1         4 _cleanup( $tmp_fh, $tmp_filename );
252 1         9 die $@;
253             }
254              
255             # recommended by perlport(1) prior to unlink/rename calls.
256             #
257             # TODO I've seen false positives from close() calls, though certain
258             # file systems only report errors at close() time. If someone can
259             # document a false positive, instead create an option and let the
260             # caller decide.
261 22 50       297 close($tmp_fh) or die "problem closing filehandle: $!\n";
262              
263             # spare subsequent useless close attempts, if any
264 22         59 undef $tmp_fh;
265              
266 22 100       54 if ( exists $params_ref->{mode} ) {
267 4         11 my $mode = $params_ref->{mode};
268 4 50 33     45 croak 'invalid mode data'
269             if !defined $mode
270             or $mode !~ m/^[0-9]+$/;
271              
272 4 100       19 my $int_mode = substr( $mode, 0, 1 ) eq '0' ? oct($mode) : ( $mode + 0 );
273              
274 4         96 my $count = chmod( $int_mode, $tmp_filename );
275 4 50       19 if ( $count != 1 ) {
276 0         0 my $save_errstr = $!;
277 0         0 _cleanup( $tmp_fh, $tmp_filename );
278 0         0 die "unable to chmod temporary file: $save_errstr\n";
279             }
280             }
281              
282 22 50       51 if ( exists $params_ref->{owner} ) {
283 0         0 eval { _set_ownership( $tmp_filename, $params_ref->{owner} ); };
  0         0  
284 0 0       0 if ($@) {
285 0         0 _cleanup( $tmp_fh, $tmp_filename );
286 0         0 die $@;
287             }
288             }
289              
290 22 100       51 if ( exists $params_ref->{mtime} ) {
291             croak 'invalid mtime data'
292             if !defined $params_ref->{mtime}
293 1 50 33     17 or $params_ref->{mtime} !~ m/^[0-9]+$/;
294              
295 1         21 my ($file_atime) = ( stat $tmp_filename )[8];
296 1         26 my $count = utime( $file_atime, $params_ref->{mtime}, $tmp_filename );
297 1 50       5 if ( $count != 1 ) {
298 0         0 my $save_errstr = $!;
299 0         0 _cleanup( $tmp_fh, $tmp_filename );
300 0         0 die "unable to utime temporary file: $save_errstr\n";
301             }
302             }
303              
304             # If the file does not exist, but the backup does;
305             # the backup is left unmodified
306 22 100 100     106 if ( exists $params_ref->{backup} && -f $params_ref->{file} ) {
307             croak 'invalid backup suffix'
308             if !defined $params_ref->{backup}
309 1 50 33     12 or $params_ref->{backup} eq '';
310              
311             # The backup file will be hardlinked in same directory as original
312 1         5 my $backup_filename = $params_ref->{file} . $params_ref->{backup};
313 1 50       13 if ( -f $backup_filename ) {
314 1         66 my $count = unlink($backup_filename);
315 1 50       6 if ( $count != 1 ) {
316 0         0 my $save_errstr = $!;
317 0         0 _cleanup( $tmp_fh, $tmp_filename );
318 0         0 die "unable to unlink existing backup file: $save_errstr\n";
319             }
320             }
321              
322             # Make hardlink -- Haiku OS does not appear to support these;
323             # http://www.cpantesters.org/cpan/report/7c2a3994-bc30-11e8-83ca-8681f4fbe649
324             # which is warned about in the perlport POD
325 1 50       66 if ( !link( $params_ref->{file}, $backup_filename ) ) {
326 0         0 my $save_errstr = $!;
327 0         0 _cleanup( $tmp_fh, $tmp_filename );
328 0         0 die "unable to link existing file to backup file: $save_errstr\n";
329             }
330             }
331              
332 22 50       1110 unless ( rename( $tmp_filename, $params_ref->{file} ) ) {
333 0         0 my $save_errstr = $!;
334 0         0 _cleanup( $tmp_fh, $tmp_filename );
335 0         0 croak "unable to rename file: $save_errstr";
336             }
337              
338             # spare subsequent useless unlink attempts, if any
339 22         76 undef $tmp_filename;
340              
341 22         686 return 1;
342             }
343              
344             sub _mkpath {
345 3     3   7 my $mkpath = shift;
346 3         5 my $directory = shift;
347              
348 3 100       9 if ($mkpath) {
349 2         648 mkpath($directory);
350 2 50       33 if ( !-d $directory ) {
351 0         0 croak "could not create parent directory";
352             }
353             } else {
354 1         118 croak "parent directory does not exist";
355             }
356              
357 2         5 return 1;
358             }
359              
360             sub _check_checksum {
361 3     3   9 my $tmp_fh = shift;
362 3         8 my $checksum = shift;
363              
364 3 50       34 seek( $tmp_fh, 0, 0 )
365             or die("tmp fh seek() error: $!\n");
366              
367 3         26 my $digest = Digest::SHA1->new;
368 3         50 $digest->addfile($tmp_fh);
369              
370 3         21 my $on_disk_checksum = $digest->hexdigest;
371              
372 3 50       13 if ( $on_disk_checksum ne $checksum ) {
373 0         0 croak 'temporary file SHA1 hexdigest does not match supplied checksum';
374             }
375              
376 3         19 return 1;
377             }
378              
379             sub _check_min_size {
380 2     2   5 my $tmp_fh = shift;
381 2         3 my $min_size = shift;
382              
383             # Must seek, as OO method allows the fh or filename to be passed off
384             # and used by who knows what first.
385 2 50       23 seek( $tmp_fh, 0, 2 )
386             or die("tmp fh seek() error: $!\n");
387              
388 2         8 my $written = tell($tmp_fh);
389 2 50       9 if ( $written == -1 ) {
    100          
390 0         0 die("tmp fh tell() error: $!\n");
391             } elsif ( $written < $min_size ) {
392 1         193 croak 'bytes written failed to exceed min_size required';
393             }
394              
395 1         3 return 1;
396             }
397              
398             # Accepts "0" or "user:group" type ownership details and a filename,
399             # attempts to set ownership rights on that filename. croak()s if
400             # anything goes awry.
401             sub _set_ownership {
402 0     0     my $filename = shift;
403 0           my $owner = shift;
404              
405 0 0 0       croak 'invalid owner data' if !defined $owner or length $owner < 1;
406              
407             # defaults if nothing comes of the subsequent parsing
408 0           my ( $uid, $gid ) = ( -1, -1 );
409              
410 0           my ( $user_name, $group_name ) = split /[:.]/, $owner, 2;
411              
412 0           my ( $login, $pass, $user_uid, $user_gid );
413              
414             # Only customize user if have something from caller
415 0 0 0       if ( defined $user_name and $user_name ne '' ) {
416 0 0         if ( $user_name =~ m/^([0-9]+)$/ ) {
417 0           $uid = $1;
418             } else {
419 0 0         ( $login, $pass, $user_uid, $user_gid ) = getpwnam($user_name)
420             or croak 'user not in password database';
421 0           $uid = $user_uid;
422             }
423             }
424              
425             # Only customize group if have something from caller
426 0 0 0       if ( defined $group_name and $group_name ne '' ) {
427 0 0         if ( $group_name =~ m/^([0-9]+)$/ ) {
428 0           $gid = $1;
429             } else {
430 0 0         my ( $group_name, $pass, $group_gid ) = getgrnam($group_name)
431             or croak 'group not in group database';
432 0           $gid = $group_gid;
433             }
434             }
435              
436 0           my $count = chown( $uid, $gid, $filename );
437 0 0         if ( $count != 1 ) {
438 0           die "unable to chown temporary file\n";
439             }
440              
441 0           return 1;
442             }
443              
444             42;
445             __END__