File Coverage

blib/lib/Doit/File.pm
Criterion Covered Total %
statement 106 120 88.3
branch 60 78 76.9
condition 14 21 66.6
subroutine 11 11 100.0
pod 2 4 50.0
total 193 234 82.4


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2017,2018,2021,2023,2026 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   427 use strict;
  5         9  
  5         163  
17 5     5   21 use warnings;
  5         6  
  5         272  
18             our $VERSION = '0.027';
19              
20 5     5   22 use Doit::Log;
  5         8  
  5         347  
21 5     5   24 use Doit::Util qw(copy_stat new_scope_cleanup);
  5         6  
  5         2867  
22              
23 5     5 0 45 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 33     33 1 126 my($doit, $file, $code, %opts) = @_;
28              
29 33 100       143 if (!defined $file) {
30 1         5 error "File parameter is missing";
31             }
32 32 100       189 if (!defined $code) {
    100          
33 1         4 error "Code parameter is missing";
34             } elsif (ref $code ne 'CODE') {
35 1         6 error "Code parameter should be an anonymous subroutine or subroutine reference";
36             }
37              
38 30         264 require File::Basename;
39 30         106 require Cwd;
40 30         1958 my $dest_dir = File::Basename::dirname($file);
41 30         77 eval { $dest_dir = Cwd::realpath($dest_dir) }; # may fail on some platforms (e.g. Windows) if $dest_dir does not exist
  30         812  
42              
43 30   100     216 my $tmp_suffix = delete $opts{tmpsuffix} || '.tmp';
44 30         114 my $tmp_dir = delete $opts{tmpdir};
45 30 100       106 if (!defined $tmp_dir) {
46 27 100 66     391 if (defined $dest_dir && -d $dest_dir) {
47 26         56 $tmp_dir = $dest_dir;
48             } else {
49 1 50       3 if (eval { require File::Spec; 1 }) {
  1         17  
  1         5  
50 1         38 $tmp_dir = File::Spec->tmpdir;
51             } else {
52 0         0 $tmp_dir = '/tmp';
53             }
54             }
55             }
56 30         71 my $mode = delete $opts{mode};
57 30         69 my $check_change = delete $opts{check_change};
58 30         51 my $show_diff = delete $opts{show_diff};
59 30 100       104 error "Unhandled options: " . join(" ", %opts) if %opts;
60              
61 29         115 my($tmp_fh,$tmp_file);
62 29         0 my(@cleanup_files, @cleanup_fhs);
63             my $tempfile_scope = new_scope_cleanup {
64 29     29   72 for my $cleanup_fh (@cleanup_fhs) { # required on Windows, otherwise unlink won't work
65 28 100       105 close $cleanup_fh if fileno($cleanup_fh);
66             }
67 29         59 for my $cleanup_file (@cleanup_files) {
68 28 100       1519 unlink $cleanup_file if -e $cleanup_file;
69             }
70 29         283 };
71 29 100       104 if ($tmp_dir eq '/dev/full') {
72             # This is just used for testing error on close()
73 1         8 $tmp_file = '/dev/full';
74 1 50       56 open $tmp_fh, '>', $tmp_file
75             or error "Can't write to $tmp_file: $!";
76             } else {
77 28         158 require File::Temp;
78 28         235 ($tmp_fh,$tmp_file) = File::Temp::tempfile(SUFFIX => $tmp_suffix, DIR => $tmp_dir, EXLOCK => 0);
79 28         14625 push @cleanup_files, $tmp_file;
80 28         59 push @cleanup_fhs, $tmp_fh;
81 28 100       78 if (defined $mode) {
82 2         16 $doit->chmod({quiet => 1}, $mode, $tmp_file);
83             } else {
84 26         359 $doit->chmod({quiet => 1}, 0666 & ~umask, $tmp_file);
85             }
86 28 100       303 if ($tmp_dir ne $dest_dir) {
87 3         27 my @stat_destdir = stat $dest_dir;
88 3 100       9 if (@stat_destdir) { # may fail in dry-run mode if $dest_dir is missing
89 2 100 33     33 if ($^O =~ /bsd/ || $^O eq 'darwin' || ($stat_destdir[2] & 02000)) {
      66        
90 1         7 $doit->chown({quiet => 1 }, undef, $stat_destdir[5], $tmp_file);
91             }
92             }
93             }
94             }
95 29         45 my $same_fs = do {
96 29         302 my $tmp_dev = (stat($tmp_file))[0];
97 29         202 my $dest_dev = (stat($dest_dir))[0];
98 5     5   32 no warnings 'uninitialized'; # $dest_dev may be undefined in dry-run mode
  5         7  
  5         3590  
99 29         76 $tmp_dev == $dest_dev;
100             };
101              
102 29 100       64 if ($same_fs) {
103 27 100       644 if (-e $file) {
104 17         102 copy_stat $file, $tmp_file, ownership => 1, mode => !defined $mode;
105             }
106             } else {
107 2         623 require File::Copy; # for move()
108             }
109              
110 29         2980 eval { $code->($tmp_fh, $tmp_file) };
  29         129  
111 29 100       5030 if ($@) {
112 1         5 error $@;
113             }
114              
115 28 100       115 if (defined fileno($tmp_fh)) { # it's undef if the filehandle was already closed in the callback
116 27 50       75 if ($] < 5.010001) { $! = 0 }
  0         0  
117             $tmp_fh->close
118 27 100       297 or error "Error while closing temporary file $tmp_file: $!";
119 26 50 33     1685 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
120 0         0 error "Error while closing temporary file $tmp_file: $!";
121             }
122             }
123              
124 27 100       85 if ($check_change) {
125 3         638 require File::Compare;
126 3 100       971 if (File::Compare::compare($tmp_file, $file) == 0) {
127             # unchanged
128 1         194 return 0;
129             }
130             }
131              
132 26 100       474 if ($same_fs) {
133 25         129 _make_writeable($doit, $file, 'rename');
134 25         296 $doit->rename({show_diff=>$show_diff}, $tmp_file, $file);
135             } else {
136 1         9 my @dest_stat;
137 1 50       17 if (-e $file) {
138 0 0       0 @dest_stat = stat($file)
139             or warning "Cannot stat $file: $! (cannot preserve permissions)"; # XXX should this be an error?
140 0         0 _make_writeable($doit, $file, 'File::Copy::move');
141             }
142 1         25 $doit->move({show_diff=>$show_diff}, $tmp_file, $file);
143 1 50       23 if (@dest_stat) { # In dry-run mode effectively a noop
    50          
144 0 0       0 $dest_stat[2] = $mode if defined $mode;
145 0         0 copy_stat [@dest_stat], $file, ownership => 1, mode => 1;
146             } elsif (defined $mode) {
147 0 0       0 $dest_stat[2] = $mode if defined $mode;
148 0         0 copy_stat [@dest_stat], $file, mode => 1;
149             }
150             }
151              
152 26         468 return 1;
153             }
154              
155             sub _make_writeable {
156 25     25   93 my($doit, $file, $for) = @_;
157 25 50 50     187 return if $for eq 'rename' && !Doit::IS_WIN; # don't need to do anything
158 0         0 my @s = stat($file);
159 0 0       0 return if !@s; # not stat-able -> file does not exist yet?
160 0         0 my $old_mode = $s[2] & 07777;
161 0 0       0 return if ($old_mode & 0200); # already writable
162 0         0 $doit->chmod(($old_mode | 0200), $file);
163             }
164              
165             sub file_digest_matches {
166 27     27 1 83 my(undef, $file, $digest, $type, %options) = @_;
167 27         45 my $got_digest_ref = delete $options{got_digest};
168 27 100 100     169 error "Option got_digest needs to point to a scalar reference"
169             if $got_digest_ref && ref $got_digest_ref ne 'SCALAR';
170 26 100       65 error "Unhandled options: " . join(" ", %options) if %options;
171              
172 25 100       413 return 0 if ! -r $file; # shortcut
173 23   100     78 $type ||= 'MD5';
174 23         2339 require Digest::file;
175 23         6500 my $got_digest = eval { Digest::file::digest_file_hex($file, $type) };
  23         94  
176 23 100       12716 if (!$got_digest) {
177 1         12 error "Cannot get digest $type from $file: $@";
178             }
179 22 100       66 $$got_digest_ref = $got_digest if $got_digest_ref;
180 22         135 $got_digest eq $digest;
181             }
182              
183             1;
184              
185             __END__