File-AtomicWrite
view release on metacpan or search on metacpan
lib/File/AtomicWrite.pm view on Meta::CPAN
}
return 1;
}
sub _check_min_size {
my ( $tmp_fh, $min_size ) = @_;
# must seek, as OO method allows the fh or filename to be passed off
# and used by who knows what first
seek( $tmp_fh, 0, SEEK_END )
or die("tmp fh seek() error: $!\n");
my $written = tell($tmp_fh);
if ( $written == -1 ) {
die("tmp fh tell() error: $!\n");
} elsif ( $written < $min_size ) {
croak 'bytes written failed to exceed min_size required';
}
return 1;
}
# accepts "0" or "user:group" type ownership details and a filename,
# attempts to set ownership rights on that filename. croak()s if
# anything goes awry
sub _set_ownership {
my ( $filename, $owner ) = @_;
croak 'invalid owner data' if !defined $owner or length $owner < 1;
# defaults if nothing comes of the subsequent parsing
my ( $uid, $gid ) = ( -1, -1 );
my ( $user_name, $group_name ) = split /[:.]/, $owner, 2;
my ( $login, $pass, $user_uid, $user_gid );
# only customize user if have something from caller
if ( defined $user_name and $user_name ne '' ) {
if ( $user_name =~ m/^([0-9]+)$/ ) {
$uid = $1;
} else {
( $login, $pass, $user_uid, $user_gid ) = getpwnam($user_name)
or croak 'user not in password database';
$uid = $user_uid;
}
}
# only customize group if have something from caller
if ( defined $group_name and $group_name ne '' ) {
if ( $group_name =~ m/^([0-9]+)$/ ) {
$gid = $1;
} else {
my ( $group_name, $pass, $group_gid ) = getgrnam($group_name)
or croak 'group not in group database';
$gid = $group_gid;
}
}
my $count = chown( $uid, $gid, $filename );
if ( $count != 1 ) {
die "unable to chown temporary file\n";
}
return 1;
}
1;
__END__
=head1 NAME
File::AtomicWrite - writes files atomically via rename()
=head1 SYNOPSIS
use File::AtomicWrite ();
# oneshot: requires filename and all the input data
# (as a filehandle or scalar ref)
File::AtomicWrite->write_file(
{ file => 'data.dat',
input => $filehandle,
}
);
# how paranoid are you?
File::AtomicWrite->write_file(
{ file => '/etc/passwd',
input => \$scalarref,
CHECKSUM => 1,
min_size => 100,
}
);
# instance interface: use to stream data or to have
# custom signal handlers
use Digest::SHA1;
my $aw = File::AtomicWrite->new(
{ file => 'name',
min_size => 1,
...
}
);
my $digest = Digest::SHA1->new;
my $tmp_fh = $aw->fh;
my $tmp_file = $aw->filename;
print $tmp_fh ...;
$digest->add(...);
$aw->checksum( $digest->hexdigest )->commit;
=head1 DESCRIPTION
This module offers atomic file writes via a temporary file created in
the same directory (and therefore probably the same partition) as the
specified B<file>. After data has been written to the temporary file,
the C<rename> system call is used to replace the target B<file>. The
module optionally supports various sanity checks (B<min_size>,
B<CHECKSUM>) that help ensure the data is written without errors.
Should anything go awry, the module will C<die> or C<croak>. All calls
should be wrapped in C<eval> blocks or better yet L<Try::Tiny>.
lib/File/AtomicWrite.pm view on Meta::CPAN
If this option exists, and B<CHECKSUM> is true, the module will not
create a L<Digest::SHA1> C<hexdigest> of the data being written out to
disk, but instead will rely on the value passed by the caller.
Only for the B<write_file> interface; instead call the B<checksum>
method to supply a C<hexdigest> checksum of the data written when using
the instance interface; see the L</SYNOPSIS> for an example of this.
=item B<CHECKSUM> => I<true or false>
If true, L<Digest::SHA1> will be used to checksum the data read back
from the disk against the checksum derived from the data written out to
the temporary file.
Use the B<checksum> option (or B<checksum> method) to supply a
L<Digest::SHA1> C<hexdigest> checksum. This will spare the module the
task of computing the checksum on the data being written.
Only for the B<write_file> interface.
=item B<min_size> => I<size>
Specify a minimum size (in bytes) that the data written must
exceed. If not, the module throws an error. (It was a process that
wrote out a zero-sized C</etc/passwd> file that prompted the
creation of this module.)
=item B<MKPATH> => I<true or false>
If true, attempt to create the parent directories of B<file> should that
directory not exist. If false (or unset), and the parent directory does
not exist, the module throws an error. If the directory cannot be
created, the module throws an error.
If true, this option will also attempt to create the B<tmpdir>
directory, if that option is set.
=item B<mode> => I<unix mode>
Accepts a Unix mode for C<chmod> to be applied to the file. Usual
throwing of error. If the mode is a string starting with C<0>,
C<oct> is used to convert it:
my $orig_mode = (stat $source_file)[2] & 07777;
...->write_file({ ..., mode => $orig_mode });
my $mode = '0644';
...->write_file({ ..., mode => $mode });
The module does not change C<umask>, nor is there a means to specify
the permissions on directories created if B<MKPATH> is set.
=item B<mtime> => I<mtime>
Accepts C<mtime> timestamp for C<utime> to be applied to the file.
Usual throwing of error.
=item B<owner> => I<unix ownership string>
Accepts similar arguments to chown(1) to be applied via C<chown>
to the file. Usual throwing of error.
...->write_file({ ..., owner => '0' });
...->write_file({ ..., owner => '0:0' });
...->write_file({ ..., owner => 'user:somegroup' });
=item B<safe_level> => I<safe_level value>
Optional means to set the L<File::Temp> module C<safe_level> value.
Consult the L<File::Temp> documentation for more information on
this option.
This value can also be set via the B<safe_level> class method.
=item B<template> => I<File::Temp template>
Template to supply to L<File::Temp>. Defaults to a reasonable value if
unset. NOTE: if customized, the template must contain a sufficient
number of C<X> that suffix the template string, as otherwise
L<File::Temp> will throw an error.
Can also be set via the B<set_template> class method.
=item B<tmpdir> => I<directory>
If set to a directory, the temporary file will be written to this
directory instead of by default to the parent directory of the target
B<file>. If the B<tmpdir> is on a different partition than the parent
directory for B<file>, or if anything else goes awry, the module will
throw an error: rename(2) does not operate across partition boundaries.
This option is advisable when writing files to include directories such
as C</etc/logrotate.d>, as the programs that read include files from
these directories may read even a temporary dot file while it is being
written. To avoid this (slight but non-zero) risk, use the B<tmpdir>
option to write the configuration out in full under a different
directory on the same partition.
=back
=head1 BUGS
No known bugs (lots of potential issues, though, see below).
=head2 Reporting Bugs
L<http://github.com/thrig/File-AtomicWrite>
=head2 Known Issues
See L<perlport> for various portability problems possible with the
C<rename> call. Consult L<rename(2)> or equivalent for caveats. Note
however that L<rename(2)> is used heavily by common programs such as
L<mv(1)> and C<rsync>.
File hard links created by L<ln(1)> will be broken by this module, as
this module has no way of knowing whether any other files link to the
inode of the file being operated on:
% touch afile
( run in 1.570 second using v1.01-cache-2.11-cpan-71847e10f99 )