NAME

Fugu::File - file operations for a daemon and its tools

SYNOPSIS

use Fugu::File;

my $data = Fugu::File->read('/etc/mydaemon.conf');

Fugu::File->write_atomic('/var/db/myapp/key', $key, mode => 0600);
Fugu::File->write_json('/var/db/myapp/state.json', $state,
    mode => 0600);

Fugu::File->ensure_dir('/var/db/myapp');

DESCRIPTION

Fugu::File holds the file operations that a daemon and its tools repeat. Every method is a class method over a path. The module keeps no state.

A recoverable failure returns undef and reports through Fugu::Log. A class-method module has no object to hold an error accessor, so the log is where the reason goes.

The write methods set the mode at the open, before the first byte. A chmod(2) after the write leaves a window in which a secret is world-readable, and that window is the whole point of the mode.

read

read($path) returns the whole content of the file, as bytes. The method returns undef when the file does not open. An empty file reads as the empty string, which is not the same answer.

write

write($path, $data, %args) writes the bytes to the file. An existing file goes away first, so the mode argument means what it says instead of yielding to the mode the old file had.

This is the argument:

mode

The file mode. The default is 0644.

write_atomic

write_atomic($path, $data, %args) writes through a temporary file in the same directory, then renames over the target. A reader sees the old content or the new content, and never a half-written file. The argument is the same as for write.

The temporary file is a sibling of the target, because rename(2) is atomic only inside one filesystem.

read_json

read_json($path) reads and decodes a JSON file. The method returns undef when the file is absent, empty, or not valid JSON. A state file that a crash truncated is a recoverable condition: the caller starts over.

write_json

write_json($path, $ref, %args) encodes and writes a JSON file with write_atomic. The argument is the same.

The encoding is canonical, so the same data always gives the same bytes. Thus a difference between two state files is a difference in the state.

ensure_dir

ensure_dir($path, %args) makes sure the directory exists.

The method refuses a symlink and refuses a path that exists as something else. Both are conditions that a daemon must not write through: a symlink puts the daemon's files where the owner of the link decided.

This is the argument:

mode

The mode for directories that the method creates. The default is 0755.

expand_tilde

expand_tilde($path) replaces a leading tilde with the home directory. The method leaves every other path unchanged, and leaves ~user alone: this module does not resolve other users.

share_path

share_path($relative, %args) resolves a file that ships with a distribution. The search tries, in order: an explicit root, the checkout that holds the caller's module file, the working directory, and the installed share tree of the named distribution under @INC. The method returns the first path that exists, or undef.

These are the arguments:

root

A directory to search before the others.

from

The caller's module file, normally __FILE__. The directory above the deepest lib component of its path is the root of the checkout that ships the data.

dist

The distribution name. An installed distribution keeps its share files under auto/share/dist/<name>, with the leading share/ component of $relative removed.

atomic_dir

atomic_dir($target, $code) builds a directory tree beside $target and publishes it with one rename(2). $code receives the temporary directory and returns true when the tree is complete. A false return, a die, or a signal discards the tree.

The method refuses to overwrite an existing target. A rename over a populated directory is not atomic, so a caller that wants a replacement removes the old tree itself.

A build can run for minutes over a large file. The method therefore installs handlers for SIGINT and SIGTERM for the length of the call, and an END block covers the rest.

sweep_temp

sweep_temp($parent) removes the leftover build directories under $parent. A kill that no handler can catch leaves them. The method returns the count it removed.

valid_name

valid_name($name) reports if the string is safe as one path component.

The check refuses an empty name, a name longer than 255 characters, a name that holds a path separator or a NUL byte, and the two directory entries . and ... Every one of them turns a name that came from a caller into a path that reaches outside the directory it was meant for.

RETURN VALUES

read returns the content of the file. read_json returns the decoded structure. Both return undef on failure.

write, write_atomic, write_json and ensure_dir return 1 on success and undef on failure.

atomic_dir returns $target on success and undef otherwise. sweep_temp returns a count. valid_name returns 1 or 0. expand_tilde and share_path return a path.

EXAMPLES

This example stores a key that only its owner may read:

Fugu::File->ensure_dir('/var/db/myapp', mode => 0700)
    or die "cannot prepare the state directory\n";

Fugu::File->write_atomic('/var/db/myapp/ltsk', $secret,
    mode => 0600)
    or die "cannot store the key\n";

This example builds a cache entry that is never half-published:

Fugu::File->atomic_dir("$cache/$key", sub ($tmp) {
    convert($disk, "$tmp/base") or return 0;
    Fugu::File->write_json("$tmp/meta.json", $meta, mode => 0600)
        or return 0;
    return 1;
});

ERRORS

No method dies. Each one reports a failure through its return value and a message in the log.

SEE ALSO

chmod(2), rename(2), Fugu::Log, Fugu::StateFile, JSON::PP

AUTHORS

Dick Olsson <hi@senzilla.io>

CAVEATS

read holds the whole file in memory. Do not use it for a file without a bound.

write_atomic is atomic against a reader, not against another writer. Two processes that write the same path at the same time both succeed, and the last rename wins. Use Fugu::Pidfile or a lock of your own where that matters.