File Coverage

blib/lib/IO/File/AtomicChange.pm
Criterion Covered Total %
statement 80 95 84.2
branch 24 44 54.5
condition 3 3 100.0
subroutine 18 21 85.7
pod 2 8 25.0
total 127 171 74.2


line stmt bran cond sub pod time code
1             package IO::File::AtomicChange;
2              
3 7     7   642435 use strict;
  7         20  
  7         288  
4 7     7   38 use warnings;
  7         13  
  7         531  
5              
6             our $VERSION = '0.05';
7              
8 7     7   42 use base qw(IO::File);
  7         17  
  7         1719  
9 7     7   13023 use Carp;
  7         16  
  7         560  
10 7     7   3856 use File::Temp qw(:mktemp);
  7         23218  
  7         1343  
11 7     7   1070 use File::Copy;
  7         2934  
  7         516  
12 7     7   6859 use File::Sync;
  7         32332  
  7         7504  
13              
14             sub new {
15 20     20 1 31346     my $class = shift;
16 20         239     my $self = $class->SUPER::new();
17 20         916     $self->_temp_file("");
18 20         80     $self->_target_file("");
19 20         75     $self->_backup_dir("");
20 20 50       128     $self->open(@_) if @_;
21 20         109762     $self;
22             }
23              
24             sub _accessor {
25 174     174   371     my($self, $tag, $val) = @_;
26 174 100       440     ${*$self}{$tag} = $val if $val;
  44         170  
27 174         313     return ${*$self}{$tag};
  174         5891  
28             }
29 59     59   221 sub _temp_file { return shift->_accessor("io_file_atomicchange_temp", @_) }
30 69     69   240 sub _target_file { return shift->_accessor("io_file_atomicchange_path", @_) }
31 46     46   195 sub _backup_dir { return shift->_accessor("io_file_atomicchange_back", @_) }
32              
33             sub DESTROY {
34 20 100   20   3166     carp "[CAUTION] disposed object without closing file handle." unless $_[0]->_closed;
35             }
36              
37             sub open {
38 20     20 1 53     my ($self, $path, $mode, $opt) = @_;
39 20 50       95     ref($self) or $self = $self->new;
40              
41             # Because we do rename(2) atomically, temporary file must be in same
42             # partion with target file.
43 20         154     my $temp = mktemp("${path}.XXXXXX");
44 20         5891     $self->_temp_file($temp);
45 20         61     $self->_target_file($path);
46              
47 20 100       538     copy_preserving_attr($path, $temp) if -f $path;
48 20 100       89     if (exists $opt->{backup_dir}) {
49 4 50       99         unless (-d $opt->{backup_dir}) {
50 0         0             croak "no such directory: $opt->{backup_dir}";
51                     }
52 4         22         $self->_backup_dir($opt->{backup_dir});
53                 }
54              
55 20 50       167     $self->SUPER::open($temp, $mode) ? $self : undef;
56             }
57              
58             sub _closed {
59 39     39   140     my $self = shift;
60 39         91     my $tag = "io_file_atomicchange_closed";
61              
62 39         75     my $oldval = ${*$self}{$tag};
  39         250  
63 39 100       160     ${*$self}{$tag} = shift if @_;
  19         93  
64 39         838     return $oldval;
65             }
66              
67             sub close {
68 19     19 0 3360     my ($self, $die) = @_;
69 19 50       104     File::Sync::fsync($self) or croak "fsync: $!";
70 19 50       1676550     unless ($self->_closed(1)) {
71 19 50       304         if ($self->SUPER::close()) {
72              
73 19 100 100     1985             $self->backup if ($self->_backup_dir && -f $self->_target_file);
74              
75 19 0       238             rename($self->_temp_file, $self->_target_file)
    50          
76                             or ($die ? croak "close (rename) atomic file: $!\n" : return);
77                     } else {
78 0 0       0             $die ? croak "close atomic file: $!\n" : return;
79                     }
80                 }
81 19         120     1;
82             }
83              
84             sub copy_modown_to_temp {
85 0     0 0 0     my($self) = @_;
86              
87 0         0     my($mode, $uid, $gid) = (stat($self->_target_file))[2,4,5];
88 0         0     chown $uid, $gid, $self->_temp_file;
89 0         0     chmod $mode, $self->_temp_file;
90             }
91              
92             sub backup {
93 3     3 0 9     my($self) = @_;
94              
95 3         47     require Path::Class;
96 3         3259     require POSIX;
97 3         23880     require Time::HiRes;
98              
99 3         6636     my $basename = Path::Class::file($self->_target_file)->basename;
100              
101 3         531     my $backup_file;
102 3         9     my $n = 0;
103 3         16     while ($n < 7) {
104 3 50       14         $backup_file = sprintf("%s/%s_%s.%d_%d%s",
105                                            $self->_backup_dir,
106                                            $basename,
107                                            POSIX::strftime("%Y-%m-%d_%H%M%S",localtime()),
108                                            (Time::HiRes::gettimeofday())[1],
109                                            $$,
110                                            ($n == 0 ? "" : ".$n"),
111                                           );
112 3 50       141         last unless -f $backup_file;
113 0         0         $n++;
114                 }
115 3 50       57     croak "already exists backup file: $backup_file" if -f $backup_file;
116              
117 3         15     copy_preserving_attr($self->_target_file, $backup_file);
118             }
119              
120              
121             sub delete {
122 0     0 0 0     my $self = shift;
123 0 0       0     unless ($self->_closed(1)) {
124 0         0         $self->SUPER::close();
125 0         0         return unlink($self->_temp_file);
126                 }
127 0         0     1;
128             }
129              
130             sub detach {
131 0     0 0 0     my $self = shift;
132 0 0       0     $self->SUPER::close() unless ($self->_closed(1));
133 0         0     1;
134             }
135              
136             sub copy_preserving_attr {
137 15     15 0 45     my($from, $to) = @_;
138              
139 15 50       97     File::Copy::copy($from, $to) or croak $!;
140              
141 15         7362     my($mode, $uid, $gid, $atime, $mtime) = (stat($from))[2,4,5,8,9];
142 15         561     chown $uid, $gid, $to;
143 15         450     chmod $mode, $to;
144 15         415     utime $atime, $mtime, $to;
145 15         44     1;
146             }
147              
148              
149             1;
150             __END__
151            
152             =encoding utf-8
153            
154             =begin html
155            
156             <a href="https://travis-ci.org/hirose31/IO-File-AtomicChange"><img src="https://travis-ci.org/hirose31/IO-File-AtomicChange.png?branch=master" alt="Build Status" /></a>
157             <a href="https://coveralls.io/r/hirose31/IO-File-AtomicChange?branch=master"><img src="https://coveralls.io/repos/hirose31/IO-File-AtomicChange/badge.png?branch=master" alt="Coverage Status" /></a>
158            
159             =end html
160            
161             =head1 NAME
162            
163             IO::File::AtomicChange - change content of a file atomically
164            
165             =head1 SYNOPSIS
166            
167             truncate and write to temporary file. When you call $fh->close, replace
168             target file with temporary file preserved permission and owner (if
169             possible).
170            
171             use IO::File::AtomicChange;
172            
173             my $fh = IO::File::AtomicChange->new("foo.conf", "w");
174             $fh->print("# create new file\n");
175             $fh->print("foo\n");
176             $fh->print("bar\n");
177             $fh->close; # MUST CALL close EXPLICITLY
178            
179             If you specify "backup_dir", save original file into backup directory (like
180             "/var/backup/foo.conf_YYYY-MM-DD_HHMMSS_PID") before replace.
181            
182             my $fh = IO::File::AtomicChange->new("foo.conf", "a",
183             { backup_dir => "/var/backup/" });
184             $fh->print("# append\n");
185             $fh->print("baz\n");
186             $fh->print("qux\n");
187             $fh->close; # MUST CALL close EXPLICITLY
188            
189             =head1 DESCRIPTION
190            
191             IO::File::AtomicChange is intended for people who need to update files
192             reliably and atomically.
193            
194             For example, in the case of generating a configuration file, you should be
195             careful about aborting generator program or be loaded by other program
196             in halfway writing.
197            
198             IO::File::AtomicChange free you from such a painful situation and boring code.
199            
200             =head1 INTERNAL
201            
202             * open
203             1. fix filename of temporary file by mktemp.
204             2. if target file already exists, copy target file to temporary file preserving permission and owner.
205             3. open temporary file and return its file handle.
206            
207             * write
208             1. write date into temporary file.
209            
210             * close
211             1. close temporary file.
212             2. if target file exists and specified "backup_dir" option, copy target file into backup directory preserving permission and owner, mtime.
213             3. rename temporary file to target file.
214            
215             =head1 CAVEATS
216            
217             You must call "$fh->close" explicitly when commit changes.
218            
219             Currently, "close $fh" or "undef $fh" don't affect target file. So if you
220             exit without calling "$fh->close", CHANGES ARE DISCARDED.
221            
222             =head1 AUTHOR
223            
224             HIROSE Masaaki E<lt>hirose31 _at_ gmail.comE<gt>
225            
226             =head1 THANKS TO
227            
228             kazuho gave me many shrewd advice.
229            
230             =head1 REPOSITORY
231            
232             L<https://github.com/hirose31/IO-File-AtomicChange>
233            
234             git clone git://github.com/hirose31/IO-File-AtomicChange.git
235            
236             patches and collaborators are welcome.
237            
238             =head1 SEE ALSO
239            
240             L<IO::File>, L<IO::AtomicFile>, L<File::AtomicWrite>
241            
242             =head1 COPYRIGHT & LICENSE
243            
244             Copyright HIROSE Masaaki 2009-
245            
246             This library is free software; you can redistribute it and/or modify
247             it under the same terms as Perl itself.
248            
249             =cut
250            
251             =begin comment
252            
253             =head0 SPECIAL THANKS TO
254            
255             typester recommended brand new style "SEE ALSO" section.
256            
257             =head0 IMHO
258            
259             * IO::AtomicFile
260             * same name of temporary file.
261             several processes update a one file, temporary file is mangled.
262             * close in DESTROY block.
263             leave halfway writing when die in writing process.
264             $fh->print("begin write\n");
265             $fh->print(generate_contents()); # call die in generate_contents()
266             $fh->print("EOF"); # this is not written...
267            
268             * File::AtomicWrite
269             * shared $tmp_fh globally?
270            
271             =end comment
272            
273             # for Emacsen
274             # Local Variables:
275             # mode: cperl
276             # cperl-indent-level: 4
277             # cperl-close-paren-offset: -4
278             # cperl-indent-parens-as-block: t
279             # indent-tabs-mode: nil
280             # coding: utf-8
281             # End:
282            
283             # vi: set ts=4 sw=4 sts=0 et ft=perl fenc=utf-8 ff=unix :
284