File Coverage

blib/lib/File/AtomicWrite.pm
Criterion Covered Total %
statement 157 221 71.0
branch 74 134 55.2
condition 22 48 45.8
subroutine 24 28 85.7
pod 8 8 100.0
total 285 439 64.9


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