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