=head1 NAME

Doit::File - commands for file creation

=head1 SYNOPSIS

    use Doit;
    my $doit = Doit->init;
    $doit->add_component('file');

    $doit->file_atomic_write('/path/to/file', sub {
        my $fh = shift;
        print $fh "Hello, world!\n";
    });

    $doit->file_atomic_write('/path/to/file', sub {
        my($fh, $filename) = @_;
	$doit->system("a_system_cmd > $filename");
    }, tmpsuffix => '.my.tmp');

=head1 DESCRIPTION

B<Doit::File> is a L<Doit> component providing methods for file
creation. It has to be added to a script using L<Doit's add_component|Doit/add_component>:

    $doit->add_component('file');

=head2 DOIT COMMANDS

The following commands are added to the Doit runner object:

=head3 file_atomic_write

    $doit->file_atomic_write($filename, $code, optkey => optval ...)

Create a file I<$filename> using a code reference I<$code> in an
atomic way. This command creates a temporary file first which is used
for the writes. If no errors happen during the code execution (i.e. no
perl-level exceptions when running the code reference, and no system
errors like "no space left on device"), then the temporary file is
renamed atomically to the final destination.

The code reference takes two parameters: a filehandle to write to and
the filename of a temporary file. Don't close the filehandle;
C<file_atomic_write> takes care of closing it (and fails if something
goes wrong).

The command always returns true, as the file is normally always
written, but see the L</check_change> option below.

Options are specified as named parameters. Possible options are:

=over

=item C<< tmpdir => I<directory> >>

Change the directory where the temporary file is created. Default is
the same directory as the final destination. The directory must
already exist. Change this if a system cannot tolerate the existence
of stray temporay files, and setting the C<tmpsuffix> option (see
below) does not help. Example:

    $doit->file_atomic_write('/path/to/filename', \&writer, tmpdir => '/tmp');

B<NOTE:> if the temporary file is created in another file system than
the final destination file, then the final rename is C<not> atomic
(i.e. C<File::Copy::move> is used instead of C<rename>). For example,
on many systems F</tmp> is located on a separate file system (root fs
or a special tmpfs) than the rest of the system.

=item C<< tmpsuffix => I<suffix> >>

Change the suffix used for the temporary file. Default is C<.tmp>.
Change the suffix if a system may tolerate the existence of stray
temporary files if special suffixes are used. For example, in a
directory controlled by Debian's L<run-parts(8)> programm it can help
to use C<.dpkg-tmp> as the tempory file suffix.

=item C<< mode => I<mode> >>

Set permissions of the final destination file, using the
L<perlfunc/chmod> syntax.

If not used, then the permissions would be as creating a normal
non-executable file, which usually takes L<perlfunc/umask> into
account.

=item C<< check_change => I<bool> >>

If set to a true value, then two things are done:

=over

=item * a comparison between the old file and the newly created
temporary is done, and if there's no difference, then the old file
will be left untouched

=item * if the old file was not changed, then the command returns a
false value

=back

=item C<< show_diff => I<bool> >>

If set to a true value, then a diff output is shown in the case the
destination file already exists. The default is to not run a diff.

=back

B<NOTES>

The temporary file creation is done using L<File::Temp>. The temporary
files are removed as early as possible, even in the case of
exceptions. Only left-overs may happen if the script is killed from
outside (e.g. SIGINT, SIGKILL...). A possible solution to prevent
left-overs on CTRL-C is to a define a signal handler like this:

    $SIG{INT} = sub { exit };

Some of L<File::Temp>'s standard behaviors are changed here: mode is
different (see above), and C<EXLOCK> is not set.

If the C<tmpdir> option is used, then the group of the created
temporary file may differ as it was created in the final destination
directory (e.g. in presence of setgid bits, or on BSD systems if the
directory belongs to another group). Some precautions are made to fix
this, at least for some common operating systems, but this is far from
perfect.

While running this function two versions of the file will exist
simultaneously on the disk: the old version (if it's an overwrite) and
the new version. This may be problematic if the files are large and
the diskspace limited.

The C<tmpdir> option may be set to F</dev/full>; in this case the
temporary file will be set to F</dev/full>. This is only useful in
test scripts for testing the C<ENOSPC> (see L<errno(3)> or
L<errno(2)>) case.

Special handling is implemented for older perls (5.8.x), as a failing
L<close|perlfunc/close> is not correctly detected here.

=head4 Notes on dry-run behavior

In dry-run mode the callback is executed normally and may write to the
provided filehandle or temporary filename. Since the final rename to
the destination file is not performed, the destination remains
untouched.

This is especially useful together with the C<show_diff> option. Normal
write operations (such as C<print>) are therefore safe. When using
C<Doit> commands, prefer informational variants such as
C<info_system()> if writing to the temporary filename.

=head3 file_digest_matches (informational command)

    my $bool = $doit->file_digest_matches($file, $digest[, $digest_algorithm[, got_digest => \$got_digest]]);

Return true if the given I<$file> exists and it's current digest
(using algorithm C<$digest_algorithm> which defaults to C<MD5>)
matches the specified I<$digest>. Fail if an unhandled
C<$digest_algorithm> is specified. Otherwise false is returned.

Other possible digest algorithms are listed in L<Digest>. Note that
some of the listed algorithms may require additional CPAN modules.
Core perl usually has C<MD5>, C<SHA-1>, C<SHA-256>, C<SHA-384> and
C<SHA-512> available.

If the C<got_digest> option is specified, then it should point to a
scalar reference, which will be filled with the digest of the file.

=head2 TODO

=over

=item Catch signals for temporary file cleanup?

It would be nice if signals like SIGINT would be caught and created
temporary files removed. But this would require some framework support
from <Doit>.

=item Automatic cleanup of leftover temporary files?

It would be nice to have an option to detect and remove temporary
files from earlier runs. The rule could be time-based (.tmp files
older than I<n> days), and maybe a check if the files are actively
used (e.g. with L<lsof(8)>) could be done.

=item C<tmpdir> option with relative directories?

L<rsync(1)> creates temporary directories named C<.~tmp~> as
subdirectories of the final destination paths. This is useful as
atomic L<rename(2)> may always be used in this case. Specification
could look like this:

    tmpdir => "./.~tmp~"

In this case B<file_atomic_write> should take care of temporary
directory creation and removal (but what if there are multiple
simultaneous writers?).

=item Use invisible temporary files

On recent Linux kernels it's possible to L<open(2)> a file with the
C<O_TMPFILE> flag, which would create an invisible file, and make it
"visible" using L<linkat(2)> (see
L<https://stackoverflow.com/questions/4171713/relinking-an-anonymous-unlinked-but-open-file>).
It would be nice if B<file_atomic_write> would offer such a solution.

=item file_atomic_directory?

How would an implementation for atomic directory writes could look
like?

=back

=head1 AUTHOR

Slaven Rezic <srezic@cpan.org>

=head1 COPYRIGHT

Copyright (c) 2017,2018,2021,2023,2026 Slaven Rezic. All rights reserved.
This module is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

L<Doit>, L<File::Temp>, L<Digest>, L<Digest::file>, L<Digest::MD5>.

=cut
