File Coverage

blib/lib/File/Append/TempFile.pm
Criterion Covered Total %
statement 77 101 76.2
branch 20 42 47.6
condition 4 12 33.3
subroutine 13 14 92.8
pod 9 9 100.0
total 123 178 69.1


line stmt bran cond sub pod time code
1             package File::Append::TempFile;
2              
3 2     2   12150 use 5.006;
  2         9  
  2         72  
4 2     2   10 use strict;
  2         3  
  2         69  
5 2     2   21 use warnings;
  2         3  
  2         80  
6              
7             =head1 NAME
8              
9             File::Append::TempFile - Perl extension for appending data to files
10              
11             =head1 SYNOPSIS
12              
13             use File::Append::TempFile;
14              
15             $f = new File::Append::TempFile();
16             $f->begin_work('/etc/hosts') or die "Appending: ".$f->err();
17             $f->add_line("127.0.0.2 localhvost\n");
18             $f->commit();
19              
20             $f->begin_work('/etc/hosts') or die "Appending: ".$f->err();
21             $f->add_line("...\n");
22             $f->rollback();
23              
24             =head1 DESCRIPTION
25              
26             The C module provides an OOP interface to appending
27             data to files using a temporary file, in order to ensure the atomicity of
28             the updates.
29              
30             An append session is initiated by invoking the C method and
31             passing it the name of the file. At this point, a temporary file is
32             created in the same directory as the original file and the original's
33             contents is copied to the temporary. More data is added to the temporary
34             file using the C method. When done appending, the C
35             method will atomically move the temporary file over the original.
36             If something goes wrong, the C method will remove the temporary
37             file without affecting the original in any way.
38              
39             =cut
40              
41 2     2   10 use Fcntl;
  2         3  
  2         3445  
42              
43             our @ISA = qw();
44              
45             our $VERSION = '0.05';
46              
47             our $debug = 0;
48              
49             my %tempfiles;
50              
51             =head1 METHODS
52              
53             The C class defines the following methods:
54              
55             =over 4
56              
57             =item new ()
58              
59             Create a new C object. No file processing is
60             done at this point.
61              
62             =cut
63              
64             sub new
65             {
66 1     1 1 840 my $proto = shift;
67 1   33     13 my $class = ref $proto || $proto;
68 1         3 my $self;
69              
70 1         8 $self = { 'fname' => undef, 'tmpfname' => undef, 'f' => undef,
71             'err' => undef, 'debug' => undef };
72 1         3 bless $self, $class;
73 1         8 $tempfiles{$self} = $self;
74 1         4 return $self;
75             }
76              
77             =item err ( [MESSAGE] )
78              
79             Set or obtain an error message describing the last error that occurred
80             during the processing of the current C object.
81              
82             =cut
83              
84             sub err($ $)
85             {
86 0     0 1 0 my ($self, $err) = @_;
87              
88 0 0       0 $self->{'err'} = $err if @_ > 1;
89 0         0 return $self->{'err'};
90             }
91              
92             =item diag ([FLAG])
93              
94             Set or obtain the diagnostic output flag. If it is set, the methods
95             will display diagnostic information on the standard error stream.
96              
97             =cut
98              
99             sub diag($ $)
100             {
101 3     3 1 622 my ($self, $debug) = @_;
102              
103 3 100       11 $self->{'debug'} = $debug if @_ > 1;
104 3         11 return $self->{'debug'};
105             }
106              
107             =item begin_work (FILENAME)
108              
109             Creates a temporary file in the same directory as the specified one and
110             copies the original's contents over to the new file. Further data may
111             be added using the C method and then either stored as the
112             original with the C method, or discarded with the C
113             method.
114              
115             =cut
116              
117             sub begin_work($ $)
118             {
119 3     3 1 574 my ($self, $fname) = @_;
120 3         6 my ($tmpfname, $orig, $f);
121 0         0 my @stat;
122              
123 3 50       12 if ($self->{'f'}) {
124 0 0       0 return undef unless $self->rollback();
125             }
126 3         9 $self->{'fname'} = $self->{'tmpfname'} = $self->{'f'} = undef;
127              
128 3 50       113 if (!open($orig, '<', $fname)) {
129 0         0 $self->err("Opening $fname: $!");
130 0         0 return undef;
131             }
132 3         36 @stat = stat $orig;
133 3         72 $tmpfname = sprintf '%s.%05d.%04d', $fname, $$, int rand 10000;
134 3 50       229 if (!sysopen($f, $tmpfname, O_WRONLY | O_EXCL | O_CREAT, 0600)) {
135 0         0 $self->err("Creating temporary file $tmpfname: $!");
136 0         0 return undef;
137             }
138 3 50       11 return undef unless $self->do_copy($orig, $f);
139 3         66 close $orig;
140              
141 3         8 $self->{'fname'} = $fname;
142 3         5 $self->{'tmpfname'} = $tmpfname;
143 3         5 $self->{'f'} = $f;
144 3         11 $self->{'stat'} = [ @stat ];
145 3         25 return 1;
146             }
147              
148             =item add_line (DATA)
149              
150             Append data to the temporary file. This does not affect the original in
151             any way until C is invoked.
152              
153             =cut
154              
155             sub add_line($ $)
156             {
157 4     4 1 766 my ($self, $line) = @_;
158 4         8 my $f = $self->{'f'};
159              
160 4 50       11 if (!defined($f)) {
161 0         0 $self->err("Cannot add_line() to an unopened tempfile");
162 0         0 return undef;
163             }
164 4         19 $self->debug("RDBG about to add a line to $self->{tmpfname} for $self->{fname}\n");
165 4 50       14 if (!(print $f $line)) {
166 0         0 $self->err("Could not add to the tempfile: $!");
167 0         0 return undef;
168             }
169 4         15 return 1;
170             }
171              
172             =item commit ()
173              
174             Replace the original file with the temporary copy, to which data may have
175             been added using C.
176              
177             B This method uninitializes the C object,
178             that is, removes B association between it and the original file and
179             even file name! The next method invoked on this C
180             object should be C.
181              
182             =cut
183              
184             sub commit($)
185             {
186 1     1 1 186 my ($self) = @_;
187              
188 1 50 33     13 if (!defined($self->{'f'}) || !defined($self->{'tmpfname'}) ||
      33        
189             !defined($self->{'fname'})) {
190 0         0 $self->err("Cannot commit an unopened tempfile");
191 0         0 return undef;
192             }
193 1         8 $self->debug("RDBG about to commit $self->{tmpfname} to $self->{fname}\n");
194              
195             # Fix stuff up
196 1 50       4 if (defined($self->{'stat'})) {
197             # Mode
198 1 50       34 if (!chmod($self->{'stat'}->[2], $self->{'tmpfname'})) {
199 0         0 $self->err("Could not chmod $self->{stat}->[2] ".
200             $self->{'tmpfname'}.": $!");
201 0         0 return undef;
202             }
203             # Owner & group
204 1 50       27 if (!chown($self->{'stat'}->[4], $self->{'stat'}->[5],
205             $self->{'tmpfname'})) {
206 0         0 $self->err("Could not chown $self->{stat}->[4], ".
207             $self->{'stat'}->[5].", $self->{tmpfname}: $!");
208 0         0 return undef;
209             }
210             }
211            
212 1 50       90 if (!rename($self->{'tmpfname'}, $self->{'fname'})) {
213 0         0 $self->err("Renaming $self->{tmpfname} to $self->{fname}: $!");
214 0         0 return undef;
215             }
216 1         55 close $self->{'f'};
217 1         8 $self->debug("RDBG successfully committed $self->{tmpfname} to $self->{fname}\n");
218 1         3 $self->{'fname'} = $self->{'tmpfname'} = $self->{'f'} = undef;
219 1         6 return 1;
220             }
221              
222             =item rollback ()
223              
224             Discard all the changes made to the temporary copy and remove it. This
225             does not affect the original file in any way.
226              
227             B This method uninitializes the C object,
228             that is, removes B association between it and the original file and
229             even file name! The next method invoked on this C
230             object should be C.
231              
232             =cut
233              
234             sub rollback($)
235             {
236 2     2 1 186 my ($self) = @_;
237              
238 2         13 $self->debug(ref($self)."->rollback() for $self->{fname}\n");
239 2 50       7 if (defined($self->{'tmpfname'})) {
240 2         11 $self->debug("RDBG removing $self->{tmpfname}\n");
241 2 50       96 if (!unlink($self->{'tmpfname'})) {
242 0         0 $self->err("Removing $self->{tmpfname}: $!");
243 0         0 return undef;
244             }
245 2         5 undef $self->{'tmpname'};
246             }
247 2 50       7 if (defined($self->{'f'})) {
248 2         21 $self->debug("RDBG closing the file\n");
249 2         150 close $self->{'f'};
250 2         7 undef $self->{'f'};
251             }
252 2         5 undef $self->{'fname'};
253 2         6 $self->debug("RDBG rollback seems complete\n");
254 2         13 return 1;
255             }
256              
257             =back
258              
259             There are also several methods used internally by the
260             C routines:
261              
262             =over 4
263              
264             =item debug (MESSAGE)
265              
266             Display a diagnostic message to the standard error stream if the output
267             of diagnostic messages has been enabled.
268              
269             =cut
270              
271             sub debug($ $)
272             {
273 14     14 1 21 my ($self, $msg) = @_;
274              
275 14 50 33     60 if ($self->{'debug'} || $debug) {
276 0         0 print STDERR $msg;
277             }
278             }
279              
280             =item do_copy (ORIG TEMP)
281              
282             Actually perform the copying of the original file data into the temporary
283             file at C time. This allows derived classes to modify
284             the file structure if needed.
285              
286             The two parameters are the file handles for the original and the
287             temporary file.
288              
289             =cut
290              
291             sub do_copy($ $ $)
292             {
293 3     3 1 5 my ($self, $orig, $f) = @_;
294            
295 3         40 while (<$orig>) {
296 8         69 print $f $_;
297             }
298 3         9 return 1;
299             }
300              
301             END
302             {
303 1 50   1   282 print STDERR "RDBG File::Append::TempFile END block\n" if $debug;
304 1 50       5 print STDERR "RDBG ".keys(%tempfiles)."\n" if $debug;
305 1         4 foreach (keys %tempfiles) {
306 1 50       8 $tempfiles{$_}->rollback() if $tempfiles{$_}->{'tmpfname'};
307             }
308             }
309              
310             =back
311              
312             =head1 SEE ALSO
313              
314             The C website:
315              
316             http://devel.ringlet.net/sysutils/file-append-tempfile/
317              
318             =head1 BUGS
319              
320             =over 4
321              
322             =item * Note that the original file may have changed between C
323             and C - those changes B be lost!
324              
325             =back
326              
327             =head1 AUTHOR
328              
329             Peter Pentchev, Eroam@ringlet.netE
330              
331             =head1 COPYRIGHT AND LICENSE
332              
333             Copyright (C) 2006 by Peter Pentchev.
334              
335             This library is free software; you can redistribute it and/or modify
336             it under the same terms as Perl itself, either Perl version 5.8.7 or,
337             at your option, any later version of Perl 5 you may have available.
338              
339             $Ringlet: TempFile.pm 1635 2007-12-27 14:06:01Z roam $
340              
341             =cut
342              
343             1;