File Coverage

blib/lib/Doit/File.pm
Criterion Covered Total %
statement 87 93 93.5
branch 49 60 81.6
condition 7 13 53.8
subroutine 9 9 100.0
pod 1 3 33.3
total 153 178 85.9


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2017,2018 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   7 use strict;
  1         3  
  1         32  
17 1     1   5 use warnings;
  1         2  
  1         42  
18             our $VERSION = '0.023';
19              
20 1     1   5 use Doit::Log;
  1         3  
  1         56  
21 1     1   5 use Doit::Util qw(copy_stat new_scope_cleanup);
  1         3  
  1         917  
22              
23 1     1 0 11 sub new { bless {}, shift }
24 1     1 0 3 sub functions { qw(file_atomic_write) }
25              
26             sub file_atomic_write {
27 18     18 1 71 my($doit, $file, $code, %opts) = @_;
28              
29 18 100       50 if (!defined $file) {
30 1         5 error "File parameter is missing";
31             }
32 17 100       77 if (!defined $code) {
    100          
33 1         4 error "Code parameter is missing";
34             } elsif (ref $code ne 'CODE') {
35 1         5 error "Code parameter should be an anonymous subroutine or subroutine reference";
36             }
37              
38 15         79 require File::Basename;
39 15         54 require Cwd;
40 15         914 my $dest_dir = Cwd::realpath(File::Basename::dirname($file));
41              
42 15   100     113 my $tmp_suffix = delete $opts{tmpsuffix} || '.tmp';
43 15 100       28 my $tmp_dir = delete $opts{tmpdir}; if (!defined $tmp_dir) { $tmp_dir = $dest_dir }
  15         39  
  12         15  
44 15         29 my $mode = delete $opts{mode};
45 15         21 my $check_change = delete $opts{check_change};
46 15 100       38 error "Unhandled options: " . join(" ", %opts) if %opts;
47              
48 14         30 my($tmp_fh,$tmp_file);
49 14         0 my(@cleanup_files, @cleanup_fhs);
50             my $tempfile_scope = new_scope_cleanup {
51 14     14   28 for my $cleanup_fh (@cleanup_fhs) { # required on Windows, otherwise unlink won't work
52 13 100       48 close $cleanup_fh if fileno($cleanup_fh);
53             }
54 14         26 for my $cleanup_file (@cleanup_files) {
55 13 100       346 unlink $cleanup_file if -e $cleanup_file;
56             }
57 14         89 };
58 14 100       33 if ($tmp_dir eq '/dev/full') {
59             # This is just used for testing error on close()
60 1         2 $tmp_file = '/dev/full';
61 1 50       43 open $tmp_fh, '>', $tmp_file
62             or error "Can't write to $tmp_file: $!";
63             } else {
64 13         63 require File::Temp;
65 13         58 ($tmp_fh,$tmp_file) = File::Temp::tempfile(SUFFIX => $tmp_suffix, DIR => $tmp_dir, EXLOCK => 0);
66 13         4484 push @cleanup_files, $tmp_file;
67 13         19 push @cleanup_fhs, $tmp_fh;
68 13 100       73 if (defined $mode) {
69 2         11 $doit->chmod({quiet => 1}, $mode, $tmp_file);
70             } else {
71 11         136 $doit->chmod({quiet => 1}, 0666 & ~umask, $tmp_file);
72             }
73 13 100       109 if ($tmp_dir ne $dest_dir) {
74 2         26 my @stat_destdir = stat $dest_dir;
75 2 100 33     41 if ($^O =~ /bsd/ || $^O eq 'darwin' || ($stat_destdir[2] & 02000)) {
      66        
76 1         6 $doit->chown({quiet => 1 }, undef, $stat_destdir[5], $tmp_file);
77             }
78             }
79             }
80 14         27 my $same_fs = do {
81 14         159 my $tmp_dev = (stat($tmp_file))[0];
82 14         142 my $dest_dev = (stat($dest_dir))[0];
83 14         56 $tmp_dev == $dest_dev;
84             };
85              
86 14 50       36 if ($same_fs) {
87 0 0       0 if (-e $file) {
88 0         0 copy_stat $file, $tmp_file, ownership => 1, mode => !defined $mode;
89             }
90             } else {
91 14         577 require File::Copy; # for move()
92             }
93              
94 14         2464 eval { $code->($tmp_fh, $tmp_file) };
  14         47  
95 14 100       12292 if ($@) {
96 1         3 error $@;
97             }
98              
99 13 50       35 if ($] < 5.010001) { $! = 0 }
  0         0  
100             $tmp_fh->close
101 13 100       65 or error "Error while closing temporary file $tmp_file: $!";
102 12 50 33     490 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
103 0         0 error "Error while closing temporary file $tmp_file: $!";
104             }
105              
106 12 100       35 if ($check_change) {
107 3         731 require File::Compare;
108 3 100       1197 if (File::Compare::compare($tmp_file, $file) == 0) {
109             # unchanged
110 1         168 return 0;
111             }
112             }
113              
114 11 50       259 if ($same_fs) {
115 0         0 _make_writeable($doit, $file, 'rename');
116 0         0 $doit->rename($tmp_file, $file);
117             } else {
118 11         16 my @dest_stat;
119 11 100       158 if (-e $file) {
120 7 50       1014 @dest_stat = stat($file)
121             or warning "Cannot stat $file: $! (cannot preserve permissions)"; # XXX should this be an error?
122 7         30 _make_writeable($doit, $file, 'File::Copy::move');
123             }
124 11         68 $doit->move($tmp_file, $file);
125 11 100       85 if (@dest_stat) { # In dry-run mode effectively a noop
    100          
126 7 100       20 $dest_stat[2] = $mode if defined $mode;
127 7         44 copy_stat [@dest_stat], $file, ownership => 1, mode => 1;
128             } elsif (defined $mode) {
129 1 50       6 $dest_stat[2] = $mode if defined $mode;
130 1         6 copy_stat [@dest_stat], $file, mode => 1;
131             }
132             }
133              
134 11         65 return 1;
135             }
136              
137             sub _make_writeable {
138 7     7   24 my($doit, $file, $for) = @_;
139 7 50 50     22 return if $for eq 'rename' && !Doit::IS_WIN; # don't need to do anything
140 7         74 my @s = stat($file);
141 7 50       21 return if !@s; # not stat-able -> file does not exist yet?
142 7         15 my $old_mode = $s[2] & 07777;
143 7 100       24 return if ($old_mode & 0200); # already writable
144 1         5 $doit->chmod(($old_mode | 0200), $file);
145             }
146              
147             1;
148              
149             __END__