NAME

Fugu::Curl - download one URL to one file, through curl, wget, or ftp

SYNOPSIS

use Fugu::Curl;

my $curl = Fugu::Curl->new(timeout => 120);

die $curl->error unless $curl->is_available;

$curl->fetch('https://example.org/dl/asset.tgz', "$cache/asset.tgz")
    or die $curl->error;

# A probe for an optional file.
unless ($curl->fetch($url, $path)) {
    return if $curl->status eq 'http' && $curl->code == 404;
    die $curl->error;
}

DESCRIPTION

Fugu::Curl downloads one URL to one file. Core Perl fetches no HTTPS URL: HTTP::Tiny is core, but its TLS needs IO::Socket::SSL, which is not. Every host holds one of three commands instead, and each command carries the TLS stack of the host.

The module picks the first command that the host has, in the order curl, wget, ftp. It runs that command through Fugu::Process, with an argument list and never a shell. The three commands need three flag sets for the same four demands: follow a redirect, fail on an HTTP status of 400 or above, write to a named file, and stop after a time limit. The module holds one classification over the three dialects, so every caller reads one status set.

A failed fetch leaves no file. The command writes to a temporary name in the directory of the destination, and the module renames that file on success. A reader of the destination therefore sees the old file or the new file, and never a partial one.

Every recoverable failure returns undef, and error holds the reason. An absent command is a clean failure, and not a die. The module never logs. The caller decides what to report.

The module verifies no checksum and no signature. Fugu::Signify holds both. It also resumes no download, and it retries nothing: one call runs one command once.

new

new(command => $command, timeout => $seconds) builds a downloader. The method resolves the command once, and it runs no process.

The module resolves the command through Fugu::Process->find_command(), and holds no resolver of its own.

command names the command, as a plain name or as a path. A name that holds a solidus is a path. Without the argument, the module walks $ENV{PATH} over the search list curl, wget, ftp, and it takes the first one that the host has.

The base name of the resolved command names the dialect, because the module holds one flag set for each of the three. A command under another name therefore resolves to nothing, and error says so.

timeout bounds the whole fetch, in seconds. The default is 600. A release asset on a slow link needs minutes, and a stalled connection must not hold a bootstrap forever.

new must not die for an absent command. It sets error instead, and is_available then returns 0.

is_available

is_available() returns 1 when the object resolved a command that it can drive. It returns 0 otherwise. The method runs no process, and it never dies.

command

command() returns the resolved command path, or undef. An operator who installed no downloader needs this answer, and a caller can put it in a diagnostic.

status

status() returns the status of the most recent fetch. It returns undef before the first fetch.

ok - the file arrived at the destination.
http - the server answered with a status of 400 or above.
timeout - the fetch passed the time limit.
network - the fetch failed, and no HTTP status explains it.
absent - no command resolved, or the command never ran.

code

code() returns the HTTP status of the most recent fetch, where the command reported one. It returns undef otherwise.

curl reports the status for every fetch, through --write-out. wget and ftp name the status in their diagnostic of a failed fetch, and they name none after a success. A caller therefore reads code together with status.

error

error() returns the reason of the most recent failure. It returns undef after a success. The message names the command, the URL and the reason, in that order:

curl: https://example.org/dl/asset.tgz: HTTP 404: curl: (22) The requested URL returned error: 404

fetch

fetch($url, $path) downloads the URL to the path. The method returns 1 on success, and undef on every failure.

The command writes to a temporary name in the directory of $path, so the rename that publishes the file stays inside one filesystem. A failure removes that temporary file, and the destination keeps whatever it held before the call.

The rename is the last step. A destination that the module cannot rename over takes the network status, because the status set holds no name for a local failure. The reason names the rename.

arguments

arguments($url, $tmp) returns the argument list that fetch runs for the resolved command, as an array reference. It returns undef when no command resolved. The method runs no process.

The list holds the command itself first, so a caller passes it straight to Fugu::Process->run. Each list carries the four demands of a fetch in the dialect of its command:

curl  --fail --location --silent --show-error --max-time N
      --output TMP --write-out %{http_code} URL
wget  --no-verbose --tries=1 --timeout=N --output-document=TMP URL
ftp   -V -M -w N -o TMP URL

wget and the ftp of OpenBSD follow a redirect and fail on an HTTP error by themselves, so their lists name neither.

The method exists for a test: a host holds one of the three commands, and the list of each dialect must stay readable there. It also lets a caller report the exact command that a fetch ran.

TIME LIMITS

The timeout option names one bound, and the module makes two of it. arguments puts the value in the flag of the command, and fetch gives Fugu::Process the value plus 30 seconds. The command flag therefore fires first, and the process bound catches a command that ignores its own flag.

The timeout status covers both. curl reports a timeout of its own with exit 28. wget and the ftp of OpenBSD report none, so a stall under their own flag takes the network status.

THE ENVIRONMENT

The module gives the child no environment of its own, so the child holds the environment of the parent. http_proxy, https_proxy, ftp_proxy, and no_proxy reach the command, and a host behind a proxy reaches a release through them.

RETURN VALUES

fetch() returns 1 or undef. arguments() returns an array reference or undef. is_available() returns 1 or 0. command(), status(), code() and error() return the state of the object, or undef.

CAVEATS

Each command verifies the certificate of the peer by default, and the module passes no flag that turns the check off. The trust store is the store of the command. The ftp of OpenBSD reads /etc/ssl/cert.pem.

The ftp of a Linux host is another program, and it fetches no HTTP URL. It sits last in the search list, so a host with curl or wget never reaches it.

A caller under taint mode must pass command as an absolute path. $ENV{PATH} is tainted, so a path from the search list cannot reach execve(2) under perl -T.

A caller under pledge(2) needs the rpath, wpath, cpath and proc exec promises: the module writes a file, renames it, and runs a command. The pledge belongs to the program, not to a library method.

SEE ALSO

curl(1), wget(1), ftp(1), Fugu::Process, Fugu::Signify

AUTHORS

Dick Olsson <hi@senzilla.io>