File Coverage

blib/lib/Doit/File.pm
Criterion Covered Total %
statement 91 105 86.6
branch 48 66 72.7
condition 9 16 56.2
subroutine 10 10 100.0
pod 1 3 33.3
total 159 200 79.5


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2017,2018,2021 Slaven Rezic. All rights reserved.
7             # This package is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9             #
10             # Mail: slaven@rezic.de
11             # WWW: http://www.rezic.de/eserte/
12             #
13              
14             package Doit::File;
15              
16 1     1   11 use strict;
  1         2  
  1         30  
17 1     1   6 use warnings;
  1         8  
  1         40  
18             our $VERSION = '0.024';
19              
20 1     1   5 use Doit::Log;
  1         3  
  1         50  
21 1     1   6 use Doit::Util qw(copy_stat new_scope_cleanup);
  1         2  
  1         572  
22              
23 1     1 0 11 sub new { bless {}, shift }
24 1     1 0 4 sub functions { qw(file_atomic_write) }
25              
26             sub file_atomic_write {
27 20     20 1 68 my($doit, $file, $code, %opts) = @_;
28              
29 20 100       54 if (!defined $file) {
30 1         3 error "File parameter is missing";
31             }
32 19 100       71 if (!defined $code) {
    100          
33 1         4 error "Code parameter is missing";
34             } elsif (ref $code ne 'CODE') {
35 1         3 error "Code parameter should be an anonymous subroutine or subroutine reference";
36             }
37              
38 17         106 require File::Basename;
39 17         57 require Cwd;
40 17         648 my $dest_dir = File::Basename::dirname($file);
41 17         44 eval { $dest_dir = Cwd::realpath($dest_dir) }; # may fail on some platforms (e.g. Windows) if $dest_dir does not exist
  17         403  
42              
43 17   100     101 my $tmp_suffix = delete $opts{tmpsuffix} || '.tmp';
44 17         33 my $tmp_dir = delete $opts{tmpdir};
45 17 100       40 if (!defined $tmp_dir) {
46 14 100 66     169 if (defined $dest_dir && -d $dest_dir) {
47 13         36 $tmp_dir = $dest_dir;
48             } else {
49 1 50       3 if (eval { require File::Spec; 1 }) {
  1         6  
  1         3  
50 1         36 $tmp_dir = File::Spec->tmpdir;
51             } else {
52 0         0 $tmp_dir = '/tmp';
53             }
54             }
55             }
56 17         28 my $mode = delete $opts{mode};
57 17         30 my $check_change = delete $opts{check_change};
58 17 100       44 error "Unhandled options: " . join(" ", %opts) if %opts;
59              
60 16         43 my($tmp_fh,$tmp_file);
61 16         0 my(@cleanup_files, @cleanup_fhs);
62             my $tempfile_scope = new_scope_cleanup {
63 16     16   29 for my $cleanup_fh (@cleanup_fhs) { # required on Windows, otherwise unlink won't work
64 15 100       60 close $cleanup_fh if fileno($cleanup_fh);
65             }
66 16         31 for my $cleanup_file (@cleanup_files) {
67 15 100       520 unlink $cleanup_file if -e $cleanup_file;
68             }
69 16         118 };
70 16 100       38 if ($tmp_dir eq '/dev/full') {
71             # This is just used for testing error on close()
72 1         3 $tmp_file = '/dev/full';
73 1 50       48 open $tmp_fh, '>', $tmp_file
74             or error "Can't write to $tmp_file: $!";
75             } else {
76 15         71 require File::Temp;
77 15         59 ($tmp_fh,$tmp_file) = File::Temp::tempfile(SUFFIX => $tmp_suffix, DIR => $tmp_dir, EXLOCK => 0);
78 15         5286 push @cleanup_files, $tmp_file;
79 15         29 push @cleanup_fhs, $tmp_fh;
80 15 100       32 if (defined $mode) {
81 2         11 $doit->chmod({quiet => 1}, $mode, $tmp_file);
82             } else {
83 13         150 $doit->chmod({quiet => 1}, 0666 & ~umask, $tmp_file);
84             }
85 15 100       128 if ($tmp_dir ne $dest_dir) {
86 3         33 my @stat_destdir = stat $dest_dir;
87 3 100       11 if (@stat_destdir) { # may fail in dry-run mode if $dest_dir is missing
88 2 100 33     37 if ($^O =~ /bsd/ || $^O eq 'darwin' || ($stat_destdir[2] & 02000)) {
      66        
89 1         6 $doit->chown({quiet => 1 }, undef, $stat_destdir[5], $tmp_file);
90             }
91             }
92             }
93             }
94 16         25 my $same_fs = do {
95 16         182 my $tmp_dev = (stat($tmp_file))[0];
96 16         187 my $dest_dev = (stat($dest_dir))[0];
97 1     1   10 no warnings 'uninitialized'; # $dest_dev may be undefined in dry-run mode
  1         2  
  1         501  
98 16         69 $tmp_dev == $dest_dev;
99             };
100              
101 16 100       37 if ($same_fs) {
102 14 100       168 if (-e $file) {
103 8         47 copy_stat $file, $tmp_file, ownership => 1, mode => !defined $mode;
104             }
105             } else {
106 2         574 require File::Copy; # for move()
107             }
108              
109 16         2753 eval { $code->($tmp_fh, $tmp_file) };
  16         50  
110 16 100       13075 if ($@) {
111 1         6 error $@;
112             }
113              
114 15 50       41 if ($] < 5.010001) { $! = 0 }
  0         0  
115             $tmp_fh->close
116 15 100       72 or error "Error while closing temporary file $tmp_file: $!";
117 14 50 33     700 if ($] < 5.010001 && $! != 0) { # at least perl 5.8.8 and 5.8.9 are buggy and do not detect errors at close time --- 5.10.1 is correct
118 0         0 error "Error while closing temporary file $tmp_file: $!";
119             }
120              
121 14 100       49 if ($check_change) {
122 3         841 require File::Compare;
123 3 100       1236 if (File::Compare::compare($tmp_file, $file) == 0) {
124             # unchanged
125 1         197 return 0;
126             }
127             }
128              
129 13 100       268 if ($same_fs) {
130 12         45 _make_writeable($doit, $file, 'rename');
131 12         54 $doit->rename($tmp_file, $file);
132             } else {
133 1         6 my @dest_stat;
134 1 50       13 if (-e $file) {
135 0 0       0 @dest_stat = stat($file)
136             or warning "Cannot stat $file: $! (cannot preserve permissions)"; # XXX should this be an error?
137 0         0 _make_writeable($doit, $file, 'File::Copy::move');
138             }
139 1         11 $doit->move($tmp_file, $file);
140 1 50       29 if (@dest_stat) { # In dry-run mode effectively a noop
    50          
141 0 0       0 $dest_stat[2] = $mode if defined $mode;
142 0         0 copy_stat [@dest_stat], $file, ownership => 1, mode => 1;
143             } elsif (defined $mode) {
144 0 0       0 $dest_stat[2] = $mode if defined $mode;
145 0         0 copy_stat [@dest_stat], $file, mode => 1;
146             }
147             }
148              
149 13         134 return 1;
150             }
151              
152             sub _make_writeable {
153 12     12   39 my($doit, $file, $for) = @_;
154 12 50 50     74 return if $for eq 'rename' && !Doit::IS_WIN; # don't need to do anything
155 0           my @s = stat($file);
156 0 0         return if !@s; # not stat-able -> file does not exist yet?
157 0           my $old_mode = $s[2] & 07777;
158 0 0         return if ($old_mode & 0200); # already writable
159 0           $doit->chmod(($old_mode | 0200), $file);
160             }
161              
162             1;
163              
164             __END__