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   6 use strict;
  1         3  
  1         28  
17 1     1   5 use warnings;
  1         2  
  1         33  
18             our $VERSION = '0.024';
19              
20 1     1   4 use Doit::Log;
  1         2  
  1         41  
21 1     1   5 use Doit::Util qw(copy_stat new_scope_cleanup);
  1         2  
  1         476  
22              
23 1     1 0 9 sub new { bless {}, shift }
24 1     1 0 3 sub functions { qw(file_atomic_write) }
25              
26             sub file_atomic_write {
27 20     20 1 60 my($doit, $file, $code, %opts) = @_;
28              
29 20 100       47 if (!defined $file) {
30 1         4 error "File parameter is missing";
31             }
32 19 100       63 if (!defined $code) {
    100          
33 1         3 error "Code parameter is missing";
34             } elsif (ref $code ne 'CODE') {
35 1         2 error "Code parameter should be an anonymous subroutine or subroutine reference";
36             }
37              
38 17         74 require File::Basename;
39 17         43 require Cwd;
40 17         521 my $dest_dir = File::Basename::dirname($file);
41 17         36 eval { $dest_dir = Cwd::realpath($dest_dir) }; # may fail on some platforms (e.g. Windows) if $dest_dir does not exist
  17         335  
42              
43 17   100     126 my $tmp_suffix = delete $opts{tmpsuffix} || '.tmp';
44 17         28 my $tmp_dir = delete $opts{tmpdir};
45 17 100       33 if (!defined $tmp_dir) {
46 14 100 66     173 if (defined $dest_dir && -d $dest_dir) {
47 13         29 $tmp_dir = $dest_dir;
48             } else {
49 1 50       30 if (eval { require File::Spec; 1 }) {
  1         10  
  1         7  
50 1         36 $tmp_dir = File::Spec->tmpdir;
51             } else {
52 0         0 $tmp_dir = '/tmp';
53             }
54             }
55             }
56 17         24 my $mode = delete $opts{mode};
57 17         23 my $check_change = delete $opts{check_change};
58 17 100       38 error "Unhandled options: " . join(" ", %opts) if %opts;
59              
60 16         33 my($tmp_fh,$tmp_file);
61 16         0 my(@cleanup_files, @cleanup_fhs);
62             my $tempfile_scope = new_scope_cleanup {
63 16     16   22 for my $cleanup_fh (@cleanup_fhs) { # required on Windows, otherwise unlink won't work
64 15 100       50 close $cleanup_fh if fileno($cleanup_fh);
65             }
66 16         22 for my $cleanup_file (@cleanup_files) {
67 15 100       442 unlink $cleanup_file if -e $cleanup_file;
68             }
69 16         96 };
70 16 100       42 if ($tmp_dir eq '/dev/full') {
71             # This is just used for testing error on close()
72 1         7 $tmp_file = '/dev/full';
73 1 50       41 open $tmp_fh, '>', $tmp_file
74             or error "Can't write to $tmp_file: $!";
75             } else {
76 15         58 require File::Temp;
77 15         49 ($tmp_fh,$tmp_file) = File::Temp::tempfile(SUFFIX => $tmp_suffix, DIR => $tmp_dir, EXLOCK => 0);
78 15         4193 push @cleanup_files, $tmp_file;
79 15         23 push @cleanup_fhs, $tmp_fh;
80 15 100       29 if (defined $mode) {
81 2         10 $doit->chmod({quiet => 1}, $mode, $tmp_file);
82             } else {
83 13         128 $doit->chmod({quiet => 1}, 0666 & ~umask, $tmp_file);
84             }
85 15 100       103 if ($tmp_dir ne $dest_dir) {
86 3         31 my @stat_destdir = stat $dest_dir;
87 3 100       12 if (@stat_destdir) { # may fail in dry-run mode if $dest_dir is missing
88 2 100 33     33 if ($^O =~ /bsd/ || $^O eq 'darwin' || ($stat_destdir[2] & 02000)) {
      66        
89 1         7 $doit->chown({quiet => 1 }, undef, $stat_destdir[5], $tmp_file);
90             }
91             }
92             }
93             }
94 16         23 my $same_fs = do {
95 16         155 my $tmp_dev = (stat($tmp_file))[0];
96 16         139 my $dest_dev = (stat($dest_dir))[0];
97 1     1   6 no warnings 'uninitialized'; # $dest_dev may be undefined in dry-run mode
  1         2  
  1         408  
98 16         60 $tmp_dev == $dest_dev;
99             };
100              
101 16 100       40 if ($same_fs) {
102 14 100       129 if (-e $file) {
103 8         40 copy_stat $file, $tmp_file, ownership => 1, mode => !defined $mode;
104             }
105             } else {
106 2         430 require File::Copy; # for move()
107             }
108              
109 16         2335 eval { $code->($tmp_fh, $tmp_file) };
  16         45  
110 16 100       10359 if ($@) {
111 1         4 error $@;
112             }
113              
114 15 50       42 if ($] < 5.010001) { $! = 0 }
  0         0  
115             $tmp_fh->close
116 15 100       60 or error "Error while closing temporary file $tmp_file: $!";
117 14 50 33     593 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       41 if ($check_change) {
122 3         707 require File::Compare;
123 3 100       1012 if (File::Compare::compare($tmp_file, $file) == 0) {
124             # unchanged
125 1         208 return 0;
126             }
127             }
128              
129 13 100       228 if ($same_fs) {
130 12         34 _make_writeable($doit, $file, 'rename');
131 12         48 $doit->rename($tmp_file, $file);
132             } else {
133 1         2 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         12 $doit->move($tmp_file, $file);
140 1 50       11 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         102 return 1;
150             }
151              
152             sub _make_writeable {
153 12     12   31 my($doit, $file, $for) = @_;
154 12 50 50     57 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__