Current section

Files

Jump to
ftpfilez README.md
Raw

README.md

[![Test](https://github.com/mworrell/ftpfilez/actions/workflows/test.yml/badge.svg)](https://github.com/mworrell/ftpfilez/actions/workflows/test.yml)

[![Hex.pm Version](https://img.shields.io/hexpm/v/ftpfilez.svg)](https://hex.pm/packages/ftpfilez)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/ftpfilez.svg)](https://hex.pm/packages/ftpfilez)

ftpfilez
=======

Really simple FTPS client - only put, get and delete.

This client is used in combination with filezcache and zotonic.

Distinction with other FTPS clients is:

 * Only get, put and delete are supported
 * put of files and/or binaries
 * get with optional streaming function, to be able to stream to the filezcache
 * asynchronous jobs sharing a bounded connection pool
 * missing directories in the target path are automatically created
 * persistent, health-checked connections with a maximum of four per server

**NOTA BENE** FTPS is enabled by default, because you really shouldn't use plain FTP anymore. Plain FTP can be
enabled explicitly with `tls => false`, primarily for local testing.

Example
-------

```erlang
rebar3 shell
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling ftpfilez
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe]

Eshell V11.1  (abort with ^G)
1> application:ensure_all_started(ftpfilez).
{ok,[ftpfilez]}
2> Cfg = #{ username => <<"anonymous">>, password => <<"test@example.com">> }.
{<<"anonymous">>, <<"test@example.com">>}
3> ftpfilez:put(Cfg, <<"ftps://ftp.example.com/LICENSE">>, {filename, "LICENSE"}).
ok
4> ftpfilez:stream(Cfg, <<"ftps://ftp.example.com/LICENSE">>, fun(X) -> io:format("!! ~p~n", [X]) end).
!! stream_start
!! <<"\n    Apache License\n", ...>>
!! eof
5> ftpfilez:delete(Cfg, <<"ftps://ftp.example.com/LICENSE">>).
ok
```

Request Queue
-------------

Requests use a shared pool of persistent FTP connections. The pool keeps at most four connections open per server
and queues requests while all four connections for that server are busy. Connections are matched by username and
the other connection settings. When a different username needs a connection, the least recently used idle connection
for that server is selected. The pool first tries to authenticate the existing connection with the new username, so
the TCP/TLS connection remains open when the server supports switching users. If that fails, the connection is closed
and reopened with the new credentials. If all existing connections are busy, the pool opens another connection up to
the per-server limit.

The pool monitors request callers. If a caller stops while its request is queued, that request is removed immediately.
Caller monitors are removed when requests finish.

Connections are checked before and after use. If a server closes an idle connection, its worker reconnects when the
next request needs it. A connection or login failure is returned directly to the caller, and a short
backoff prevents a queued burst from repeatedly attempting the same failing connection.

Idle connections are closed after five minutes. The pool manager performs the close synchronously before processing
another checkout, avoiding a race between idle expiry and connection reuse. The timeout can be configured with the
`connection_idle_timeout_ms` application setting. A synchronous close is limited to five seconds; an unresponsive
connection owner is killed. This limit can be configured with `connection_close_timeout_ms`.

The `get`, `put` and `delete` requests can be queued. A function or pid can be given as a callback for the job result.
Streaming downloads can also be queued with `queue_stream` or `queue_stream_id`.
Both synchronous and queued operations use the same pool, including connection setup and authentication.

Streaming callbacks run in the process calling `stream/3`, including function and MFA callbacks.
For queued streams, this is the job process. The pool worker waits for each callback to finish
before reading the next chunk, preserving event order and backpressure. Callback exceptions
are raised in the caller, and the interrupted FTP connection is closed before reuse. If the
caller stops while a callback is pending, the worker releases that connection as well.
This preserves compatibility with process-owned resources such as `filezcache` writers.
`max_connections` sets the limit per host and port (default four, capped at four);
queued job processes waiting for a connection do not consume FTP connections.
Use `ftpfilez:status()` to inspect waiting requests and active connections.

Example:

```erlang
6> {ok, ReqId, JobPid} = ftpfilez:queue_put(Cfg, <<"ftps://ftp.example.com/LICENSE">>, {filename, "LICENSE"}, fun(ReqId,Result) -> io:format("!! ~p~n", [ Result ]) end).
{ok,#Ref<0.0.0.3684>,<0.854.0>}
```

The returned `JobPid` is the pid of the process in the ftpfilez queue supervisor.
The callback can be a function (arity 2), `{M,F,A}` or a pid.

If the callback is a pid then it will receive the message `{ftpfilez_done, ReqId, Result}`.

Pool Status
-----------

`ftpfilez:status/0` returns a sanitized snapshot of all connection workers, including their `idle`, `busy`, or
`disconnected` state, host, port, username and TLS mode. It also reports the number of waiting and in-flight requests.
Passwords and TLS options are not included.

## Checking with deprecated OTP FTP APIs

Deprecation checks remain enabled by default. To allow the FTP client APIs that
this library intentionally uses, run:

```sh
make xref dialyzer FTPFILEZ_ALLOW_DEPRECATED_FTP=1
# Or invoke rebar3 directly:
FTPFILEZ_ALLOW_DEPRECATED_FTP=1 rebar3 xref
FTPFILEZ_ALLOW_DEPRECATED_FTP=1 rebar3 dialyzer
```

This suppresses xref warnings for the existing FTP functions used by the library
and their compiler deprecation warnings (also emitted during Dialyzer's compile
step). Dialyzer type analysis, other deprecation warnings, and undefined-function
checks remain enabled. Removed FTP functions are not ignored. Omit the option or
set it to `0` to restore the default checks.