File Coverage

blib/lib/Doit/File.pm
Criterion Covered Total %
statement 104 118 88.1
branch 58 76 76.3
condition 14 21 66.6
subroutine 11 11 100.0
pod 2 4 50.0
total 189 230 82.1


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2017,2018,2021,2023 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 5     5   690 use strict;
  5         13  
  5         200  
17 5     5   26 use warnings;
  5         9  
  5         313  
18             our $VERSION = '0.025';
19              
20 5     5   25 use Doit::Log;
  5         10  
  5         428  
21 5     5   29 use Doit::Util qw(copy_stat new_scope_cleanup);
  5         11  
  5         3267  
22              
23 5     5 0 47 sub new { bless {}, shift }
24 5     5 0 19 sub functions { qw(file_atomic_write file_digest_matches) }
25              
26             sub file_atomic_write {
27 28     28 1 107 my($doit, $file, $code, %opts) = @_;
28              
29 28 100       106 if (!defined $file) {
30 1         3 error "File parameter is missing";
31             }
32 27 100       189 if (!defined $code) {
    100          
33 1         3 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 25         209 require File::Basename;
39 25         124 require Cwd;
40 25         1576 my $dest_dir = File::Basename::dirname($file);
41 25         73 eval { $dest_dir = Cwd::realpath($dest_dir) }; # may fail on some platforms (e.g. Windows) if $dest_dir does not exist
  25         657  
42              
43 25   100     201 my $tmp_suffix = delete $opts{tmpsuffix} || '.tmp';
44 25         55 my $tmp_dir = delete $opts{tmpdir};
45 25 100       72 if (!defined $tmp_dir) {
46 22 100 66     371 if (defined $dest_dir && -d $dest_dir) {
47 21         58 $tmp_dir = $dest_dir;
48             } else {
49 1 50       11 if (eval { require File::Spec; 1 }) {
  1         9  
  1         10  
50 1         35 $tmp_dir = File::Spec->tmpdir;
51             } else {
52 0         0 $tmp_dir = '/tmp';
53             }
54             }
55             }
56 25         54 my $mode = delete $opts{mode};
57 25         41 my $check_change = delete $opts{check_change};
58 25 100       79 error "Unhandled options: " . join(" ", %opts) if %opts;
59              
60 24         72 my($tmp_fh,$tmp_file);
61 24         0 my(@cleanup_files, @cleanup_fhs);
62             my $tempfile_scope = new_scope_cleanup {
63 24     24   57 for my $cleanup_fh (@cleanup_fhs) { # required on Windows, otherwise unlink won't work
64 23 100       94 close $cleanup_fh if fileno($cleanup_fh);
65             }
66 24         46 for my $cleanup_file (@cleanup_files) {
67 23 100       1337 unlink $cleanup_file if -e $cleanup_file;
68             }
69 24         246 };
70 24 100       85 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       53 open $tmp_fh, '>', $tmp_file
74             or error "Can't write to $tmp_file: $!";
75             } else {
76 23         133 require File::Temp;
77 23         191 ($tmp_fh,$tmp_file) = File::Temp::tempfile(SUFFIX => $tmp_suffix, DIR => $tmp_dir, EXLOCK => 0);
78 23         12244 push @cleanup_files, $tmp_file;
79 23         61 push @cleanup_fhs, $tmp_fh;
80 23 100       76 if (defined $mode) {
81 2         26 $doit->chmod({quiet => 1}, $mode, $tmp_file);
82             } else {
83 21         334 $doit->chmod({quiet => 1}, 0666 & ~umask, $tmp_file);
84             }
85 23 100       297 if ($tmp_dir ne $dest_dir) {
86 3         34 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     51 if ($^O =~ /bsd/ || $^O eq 'darwin' || ($stat_destdir[2] & 02000)) {
      66        
89 1         5 $doit->chown({quiet => 1 }, undef, $stat_destdir[5], $tmp_file);
90             }
91             }
92             }
93             }
94 24         71 my $same_fs = do {
95 24         374 my $tmp_dev = (stat($tmp_file))[0];
96 24         225 my $dest_dev = (stat($dest_dir))[0];
97 5     5   38 no warnings 'uninitialized'; # $dest_dev may be undefined in dry-run mode
  5         8  
  5         4268  
98 24         77 $tmp_dev == $dest_dev;
99             };
100              
101 24 100       69 if ($same_fs) {
102 22 100       362 if (-e $file) {
103 14         81 copy_stat $file, $tmp_file, ownership => 1, mode => !defined $mode;
104             }
105             } else {
106 2         663 require File::Copy; # for move()
107             }
108              
109 24         3920 eval { $code->($tmp_fh, $tmp_file) };
  24         108  
110 24 100       4075 if ($@) {
111 1         4 error $@;
112             }
113              
114 23 50       76 if ($] < 5.010001) { $! = 0 }
  0         0  
115             $tmp_fh->close
116 23 100       242 or error "Error while closing temporary file $tmp_file: $!";
117 22 50 33     1458 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 22 100       127 if ($check_change) {
122 3         857 require File::Compare;
123 3 100       1391 if (File::Compare::compare($tmp_file, $file) == 0) {
124             # unchanged
125 1         203 return 0;
126             }
127             }
128              
129 21 100       390 if ($same_fs) {
130 20         93 _make_writeable($doit, $file, 'rename');
131 20         195 $doit->rename($tmp_file, $file);
132             } else {
133 1         3 my @dest_stat;
134 1 50       15 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         14 $doit->move($tmp_file, $file);
140 1 50       13 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 21         275 return 1;
150             }
151              
152             sub _make_writeable {
153 20     20   59 my($doit, $file, $for) = @_;
154 20 50 50     133 return if $for eq 'rename' && !Doit::IS_WIN; # don't need to do anything
155 0         0 my @s = stat($file);
156 0 0       0 return if !@s; # not stat-able -> file does not exist yet?
157 0         0 my $old_mode = $s[2] & 07777;
158 0 0       0 return if ($old_mode & 0200); # already writable
159 0         0 $doit->chmod(($old_mode | 0200), $file);
160             }
161              
162             sub file_digest_matches {
163 27     27 1 114 my(undef, $file, $digest, $type, %options) = @_;
164 27         70 my $got_digest_ref = delete $options{got_digest};
165 27 100 100     150 error "Option got_digest needs to point to a scalar reference"
166             if $got_digest_ref && ref $got_digest_ref ne 'SCALAR';
167 26 100       103 error "Unhandled options: " . join(" ", %options) if %options;
168              
169 25 100       637 return 0 if ! -r $file; # shortcut
170 23   100     116 $type ||= 'MD5';
171 23         3377 require Digest::file;
172 23         8520 my $got_digest = eval { Digest::file::digest_file_hex($file, $type) };
  23         119  
173 23 100       15831 if (!$got_digest) {
174 1         16 error "Cannot get digest $type from $file: $@";
175             }
176 22 100       86 $$got_digest_ref = $got_digest if $got_digest_ref;
177 22         186 $got_digest eq $digest;
178             }
179              
180             1;
181              
182             __END__