ftp(3erl)

πŸ“¦
version 25.3.2.8 Β· 4 versions
, in ubuntu noble

🌸 ftp(3erl) β€” the Erlang Module Definition you've been waiting for 🌸

okay bestie, buckle up, this is ftp, the File Transfer Protocol client for Erlang, and she's iconic. let's get into it πŸ’…

✨ NAME ✨

ftp β€” a File Transfer Protocol client. that's it, that's the vibe. πŸ¦‹

πŸŽ€ DESCRIPTION πŸŽ€

so this module implements a client for file transfer according to a subset of the File Transfer Protocol (FTP) β€” see RFC 959 if you wanna read the whole lore. πŸ“œ

the FTP client always tries to use passive FTP mode first, and only resorts to active FTP mode if that fails. lowkey humble of her. this default behavior can be changed with the start option mode. ✨

an FTP client is always started as part of the ftp application, and the legacy start_service function is deprecated in OTP-24. she's old, we don't use her anymore, periodt. πŸ’€

for a simple example of an FTP session, see the FTP User's Guide. 🫢

in addition to the ordinary functions for receiving and sending files (see recv/2, recv/3, send/2, and send/3) there are functions for receiving remote files as binaries (see recv_bin/2) and for sending binaries to be stored as remote files (see send_bin/3). binaries ate, no cap. πŸ”₯

there's also a whole set of functions for sending and receiving contiguous parts of a file to be stored in a remote file. for send, see send_chunk_start/2, send_chunk/2, and send_chunk_end/1. for receive, see recv_chunk_start/2 and recv_chunk/. chunk by chunk, main character energy. πŸ‘‘

ngl, the return values of the following functions depend much on the implementation of the FTP server at the remote host. in particular, the results from ls and nlist varies. often real errors are not reported as errors by ls, even if, for example, a file or directory does not exist. like, she's lying to you and smiling about it 😭. nlist is usually more strict, but some implementations have the peculiar behaviour of responding with an error if the request is a listing of the contents of a directory that exists but is empty. make it make sense. πŸ™ˆ


πŸ’– FTP CLIENT START/STOP πŸ’–

the FTP client can be started and stopped dynamically in runtime by calling the ftp application API ftp:open(Host, Options) and ftp:close(Client). spawn her, unspawn her, she's flexible like that. ✨

the available configuration options are as follows:

  • {host, Host} β€” where Host = string() | ip_address() β€” who she's talking to πŸ¦‹
  • {port, Port} β€” where Port = integer() > 0. default is 0, which aliases to 21 or 990 when used with {tls_sec_method,ftps}). yes, that parenthesis is in the original, don't come for me πŸ’€
  • {mode, Mode} β€” where Mode = active | passive. default is passive. πŸŽ€
  • {verbose, Verbose} β€” where Verbose = boolean(). determines if the FTP communication is to be verbose or not. default is false. (she's quiet, mysterious, iykyk) 🫢
  • {debug, Debug} β€” where Debug = trace | debug | disable. debugging using the dbg toolkit. default is disable. πŸ”
  • {ipfamily, IpFamily} β€” where IpFamily = inet | inet6 | inet6fb4. with inet6fb4 the client behaves as before, that is, tries to use IPv6, and only if that does not work it uses IPv4). default is inet (IPv4). 🌈
  • {timeout, Timeout} β€” where Timeout = non_neg_integer(). connection time-out. default is 60000 (milliseconds). ⏰
  • {dtimeout, DTimeout} β€” where DTimeout = non_neg_integer() | infinity. data connect time-out β€” the time the client waits for the server to connect to the data socket. default is infinity. (she'll wait forever, that's love) πŸ’œ
  • {progress, Progress} β€” where Progress = ignore | {CBModule, CBFunction, InitProgress}. here CBModule = atom(), CBFunction = atom(), and InitProgress = term(). default is ignore. πŸ“Š

the option progress is intended to be used by applications that want to create some type of progress report, such as a progress bar in a GUI. default for the progress option is ignore, that is, the option is not used. when the progress option is specified, the following happens when ftp:send/[3,4] or ftp:recv/[3,4] are called:

  1. before a file is transferred, the following call is made to indicate the start of the file transfer and how large the file is. the return value of the callback function is to be a new value for the UserProgressTerm that will be used as input the next time the callback function is called.
CBModule:CBFunction(InitProgress, File, {file_size, FileSize})
  1. every time a chunk of bytes is transferred the following call is made:
CBModule:CBFunction(UserProgressTerm, File, {transfer_size,
TransferSize})
  1. at the end of the file the following call is made to indicate the end of the transfer:
CBModule:CBFunction(UserProgressTerm, File, {transfer_size, 0})

the callback function is to be defined as follows:

CBModule:CBFunction(UserProgressTerm, File, Size) -> UserProgressTerm

where:

  • CBModule = CBFunction = atom()
  • UserProgressTerm = term()
  • File = string()
  • Size = {transfer_size, integer()} | {file_size, integer()} | {file_size, unknown}

for remote files, ftp cannot determine the file size in a platform independent way. in this case the size becomes unknown and it is left to the application to determine the size. bestie you're on your own for that one 🫠

πŸ“Œ Note: the callback is made by a middleman process, hence the file transfer is not affected by the code in the progress callback function. if the callback crashes, this is detected by the FTP connection process, which then prints an info-report and goes on as if the progress option was set to ignore. she literally does not care, she moves on πŸ’…

the file transfer type is set to the default of the FTP server when the session is opened. this is usually ASCII mode. ⭐

the current local working directory (compare lpwd/1) is set to the value reported by file:get_cwd/1, the wanted local directory. 🏑

the return value Pid is used as a reference to the newly created FTP client in all other functions, and they are to be called by the process that created the connection. the FTP client process monitors the process that created it and terminates if that process terminates. ride or die, fr fr. 😭🫢


πŸ¦‹ DATA TYPES πŸ¦‹

the following type definitions are used by more than one function in the FTP client API:

  • pid() = identifier of an FTP connection
  • string() = list of ASCII characters
  • shortage_reason() = etnospc | epnospc
  • restriction_reason() = epath | efnamena | elogin | enotbinary β€” all restrictions are not always relevant to all functions
  • common_reason() = econn | eclosed | term() β€” some explanation of what went wrong

πŸ’… EXPORTS πŸ’…

okay here come the functions, this is the part where she really shows off ✨

account(Pid, Account) -> ok | {error, Reason}

types:

Pid = pid()
Account = string()
Reason = eacct | common_reason()

sets the account for an operation, if needed. πŸͺͺ

append(Pid, LocalFile) -> / append(Pid, LocalFile, RemoteFile) -> ok | {error, Reason}

types:

Pid = pid()
LocalFile = RemoteFile = string()
Reason = epath | elogin | etnospc | epnospc | efnamena | common_reason

transfers the file LocalFile to the remote server. if RemoteFile is specified, the name of the remote file that the file is appended to is set to RemoteFile, otherwise to LocalFile. if the file does not exists, it is created. no cap, she'll make it for you 🌱

append_bin(Pid, Bin, RemoteFile) -> ok | {error, Reason}

types:

Pid = pid()
Bin = binary()
RemoteFile = string()
Reason = restriction_reason()| shortage_reason() | common_reason()

transfers the binary Bin to the remote server and appends it to the file RemoteFile. if the file does not exist, it is created. binary girlies rise πŸŽ€

append_chunk(Pid, Bin) -> ok | {error, Reason}

types:

Pid = pid()
Bin = binary()
Reason = echunk | restriction_reason() | common_reason()

transfers the chunk Bin to the remote server, which appends it to the file specified in the call to append_chunk_start/2. 🧩

⚠️ for some errors, for example, file system full, it is necessary to call append_chunk_end to get the proper reason. don't skip that step bestie, i'm begging 😭

append_chunk_start(Pid, File) -> ok | {error, Reason}

types:

Pid = pid()
File = string()
Reason = restriction_reason() | common_reason()

starts the transfer of chunks for appending to the file File at the remote server. if the file does not exist, it is created. πŸš€

append_chunk_end(Pid) -> ok | {error, Reason}

types:

Pid = pid()
Reason = echunk | restriction_reason() | shortage_reason()

stops transfer of chunks for appending to the remote server. the file at the remote server, specified in the call to append_chunk_start/2, is closed by the server. clean up after yourself, iconic behavior πŸ’…

cd(Pid, Dir) -> ok | {error, Reason}

types:

Pid = pid()
Dir = string()
Reason = restriction_reason() | common_reason()

changes the working directory at the remote server to Dir. we're moving, we're moving πŸ¦‹

close(Pid) -> ok

types:

Pid = pid()

ends an FTP session, created using function open. bye bestie, it was real πŸ‘‹

delete(Pid, File) -> ok | {error, Reason}

types:

Pid = pid()
File = string()
Reason = restriction_reason() | common_reason()

deletes the file File at the remote server. gone, reduced to atoms πŸ’€

formaterror(Tag) -> string()

types:

Tag = {error, atom()} | atom()

given an error return value {error, AtomReason}, this function returns a readable string describing the error. so you actually know what went wrong, imagine 🫢

lcd(Pid, Dir) -> ok | {error, Reason}

types:

Pid = pid()
Dir = string()
Reason = restriction_reason()

changes the working directory to Dir for the local client. local queen behavior 🏠

lpwd(Pid) -> {ok, Dir}

types:

Pid = pid()

returns the current working directory at the local client. where are we? right here bestie πŸ’–

ls(Pid) -> / ls(Pid, Pathname) -> {ok, Listing} | {error, Reason}

types:

Pid = pid()
Pathname = string()
Listing = string()
Reason = restriction_reason() | common_reason()

returns a list of files in long format. πŸ“‹

Pathname can be a directory, a group of files, or a file. the Pathname string can contain wildcards. ✨

ls/1 implies the current remote directory of the user.

the format of Listing depends on the operating system. on UNIX, it is typically produced from the output of the ls -l shell command. classic 🐚

mkdir(Pid, Dir) -> ok | {error, Reason}

types:

Pid = pid()
Dir = string()
Reason = restriction_reason() | common_reason()

creates the directory Dir at the remote server. making space for your stuff, we love to see it 🌸

nlist(Pid) -> / nlist(Pid, Pathname) -> {ok, Listing} | {error, Reason}

types:

Pid = pid()
Pathname = string()
Listing = string()
Reason = restriction_reason() | common_reason()

returns a list of files in short format. short and sweet, like a text message πŸ’Œ

Pathname can be a directory, a group of files, or a file. the Pathname string can contain wildcards. 🌈

nlist/1 implies the current remote directory of the user.


to be continued bestie, there's more and it's equally iconic πŸ’œ

The format of Listing is a stream of filenames where each filename is separated by <CRLF> or <NL>. And like β€” contrary to function ls, the whole point of nlist is to let a program chew through filename info automatically, no human eyeballs required. iykyk πŸ’…


🫢 open(Host) -> {ok, Pid} | {error, Reason}

🫢 open(Host, Opts) -> {ok, Pid} | {error, Reason}

Types (copying these exactly, bestie, don't come for me):

Host = string() | ip_address()
Opts = options()
options() = [option()]
option() = start_option() | open_option()
start_option() = {verbose, verbose()} | {debug, debug()}
verbose() = boolean() (default is false)
debug() = disable | debug | trace (default is disable)
open_option() = {ipfamily, ipfamily()} | {port, port()} |
{mode, mode()} | {tls, tls_options()} | {tls_sec_method,
tls_sec_method()} | {tls_ctrl_session_reuse, boolean()
(default is false)} | {timeout, timeout()} | {dtimeout,
dtimeout()} | {progress, progress()} | {sock_ctrl,
sock_opts()} | {sock_data_act, sock_opts()} | {sock_data_pass,
sock_opts()}
ipfamily() = inet | inet6 | inet6fb4 (default is inet)
port() = non_neg_integer() (default is 0 which aliases to 21
or 990 when used with {tls_sec_method,ftps})
mode() = active | passive (default is passive)
tls_options() = [ssl:tls_option()]
tls_sec_method() = ftps | ftpes (default is ftpes)
sock_opts() = [gen_tcp:option() except for ipv6_v6only,
active, packet, mode, packet_size and header
timeout() = integer() > 0 (default is 60000 milliseconds)
dtimeout() = integer() > 0 | infinity (default is infinity)
progress() = ignore | {module(), function(), initial_data()}
(default is ignore)
module() = atom()
function() = atom()
initial_data() = term()
Reason = ehost | term()

So this is the main character of the whole page: it starts a FTP client process and opens a session with the FTP server at Host. Two birds, one slay ✨

Okay but let me walk you through the options because there's lore:

  • {tls, tls_options()} β€” if it's present, the FTP session gets transported over tls (ftps, see RFC 4217). The list tls_options() can legitimately be empty, no cap. The function ssl:connect/3 is what secures both the control connection and the data sessions. Both. πŸ’…
  • {tls_sec_method, tls_sec_method()} β€” defaults to ftpes. Set it to ftps and it connects immediately with SSL instead of upgrading with STARTTLS. Heads up though: this suboption is ignored unless the suboption tls is also set. Ignored!! Don't be out here setting it alone and wondering why nothing happened 😭
  • {tls_ctrl_session_reuse, boolean()} β€” defaults to false. Set it to true and the client will re-use the TLS session from the control channel on the data channel, which, like, many FTP servers enforce as a whole thing (proposed and implemented first by vsftpd). Respect the lore 🫢
  • sock_ctrl, sock_data_act, sock_data_pass β€” these pass options down to the underlying transport layer (tcp). Default for sock_ctrl is []. Both sock_data_act and sock_data_pass use the value of sock_ctrl as their default value. ✨

A session opened in this way is closed using function close. Don't ghost it, close it properly.


⭐ pwd(Pid) -> {ok, Dir} | {error, Reason}

Types:

Pid = pid()
Reason = restriction_reason() | common_reason()

Returns the current working directory at the remote server. That's it, that's the function, and honestly? iconic.


πŸŽ€ recv(Pid, RemoteFile) ->

πŸŽ€ recv(Pid, RemoteFile, LocalFile) -> ok | {error, Reason}

Types:

Pid = pid()
RemoteFile = LocalFile = string()
Reason = restriction_reason() | common_reason() |
file_write_error_reason()
file_write_error_reason() = see file:write/2

Transfers the file RemoteFile from the remote server onto the local client's file system. If you pass LocalFile, the local file is LocalFile; if you don't, it's RemoteFile. Simple as 🌸

⚠️ And listen β€” if the file write fails (for example, enospc), the command is aborted and you get {error, file_write_error_reason()} back. But here's the messy part: the file is not removed. So you might be left with a sad little partial file. Clean up after yourself fr fr.

πŸ¦‹ recv_bin(Pid, RemoteFile) -> {ok, Bin} | {error, Reason}

Types:

Pid = pid()
Bin = binary()
RemoteFile = string()
Reason = restriction_reason() | common_reason()

Transfers the file RemoteFile from the remote server and hands it to you as a binary. No disk drama, just vibes and bytes πŸ’œ


🧚 recv_chunk_start(Pid, RemoteFile) -> ok | {error, Reason}

Types:

Pid = pid()
RemoteFile = string()
Reason = restriction_reason() | common_reason()

Starts transfer of the file RemoteFile from the remote server. It's giving "opening credits" β€” the actual chunks come next πŸ‘‘


🧚 recv_chunk(Pid) -> ok | {ok, Bin} | {error, Reason}

Types:

Pid = pid()
Bin = binary()
Reason = restriction_reason() | common_reason()

Receives a chunk of the remote file (the RemoteFile from recv_chunk_start). The return values mean the following, and yes, they're all important:

  • ok = the transfer is complete. πŸŽ‰
  • {ok, Bin} = just another chunk of the file. keep going bestie
  • {error, Reason} = transfer failed. 😭

🌈 rename(Pid, Old, New) -> ok | {error, Reason}

Types:

Pid = pid()
CurrFile = NewFile = string()
Reason = restriction_reason() | common_reason()

Renames Old to New at the remote server. Glow-up, but for filenames ✨


🌈 rmdir(Pid, Dir) -> ok | {error, Reason}

Types:

Pid = pid()
Dir = string()
Reason = restriction_reason() | common_reason()

Removes directory Dir at the remote server. Bye bestie πŸ’€


πŸ’– send(Pid, LocalFile) ->

πŸ’– send(Pid, LocalFile, RemoteFile) -> ok | {error, Reason}

Types:

Pid = pid()
LocalFile = RemoteFile = string()
Reason = restriction_reason() | common_reason() |
shortage_reason()

Transfers the file LocalFile to the remote server. If you specify RemoteFile, the name of the remote file gets set to RemoteFile β€” otherwise it's LocalFile. Ship it 🫢


πŸ’– send_bin(Pid, Bin, RemoteFile) -> ok | {error, Reason}

Types:

Pid = pid()
Bin = binary()
RemoteFile = string()
Reason = restriction_reason() | common_reason() |
shortage_reason()

Transfers the binary Bin into the file RemoteFile at the remote server. Binary in, file out, no notes πŸ’…


πŸ“ send_chunk(Pid, Bin) -> ok | {error, Reason}

Types:

Pid = pid()
Bin = binary()
Reason = echunk | restriction_reason() | common_reason()

Transfers the chunk Bin to the remote server, which writes it into the file you specified in the call to send_chunk_start/2.

πŸ‘€ Okay important: for some errors β€” file system full, for example β€” it is necessary to call send_chunk_end to get the proper reason. So don't panic at the first error, finish the ritual first πŸ”₯

πŸ“ send_chunk_start(Pid, File) -> ok | {error, Reason}

Types:

Pid = pid()
File = string()
Reason = restriction_reason() | common_reason()

Starts transfer of chunks into the file File at the remote server. Chunk era begins 🌸


πŸ“ send_chunk_end(Pid) -> ok | {error, Reason}

Types:

Pid = pid()
Reason = restriction_reason() | common_reason() |
shortage_reason()

Stops transfer of chunks to the remote server. The file at the remote server β€” the one you specified in the call to send_chunk_start/2 β€” is closed by the server. And that's a wrap, periodt 🎬


πŸ‘‘ start_service(ServiceConfig) -> {ok, Pid} | {error, Reason}

Types:

ServiceConfig = [{Option, Value}]
Option = property()
Value = term()

Dynamically starts an FTP session after the ftp application has already been started. Very "I'll join the group chat later" energy.

πŸ“Œ Note: as long as the ftp application is operational, the FTP sessions are supervised and can be soft code upgraded. Supervised AND soft-code-upgradable, we love a stable queen πŸ‘‘

πŸ‘‘ stop_service(Reference) -> ok | {error, Reason}

Types:

Reference = pid() | term() - service-specified reference
Reason = term()

Stops a started FTP session. Leave the chat, no hard feelings πŸ’œ


πŸŽ€ type(Pid, Type) -> ok | {error, Reason}

Types:

Pid = pid()
Type = ascii | binary
Reason = etype | restriction_reason() | common_reason()

Sets the file transfer type to ascii or binary. When an FTP session is opened, the default transfer type of the server is used β€” most often ascii, which is the default according to RFC 959. So the server picks first, and then you can ✨rebrand✨ it.


πŸ¦‹ user(Pid, User, Password) -> ok | {error, Reason}

Types:

Pid = pid()
User = Password = string()
Reason = euser | common_reason()

Performs login of User with Password. The classic. 🫢


πŸ¦‹ user(Pid, User, Password, Account) -> ok | {error, Reason}

Types:

Pid = pid()
User = Password = string()
Reason = euser | common_reason()

Performs login of User with Password to the account specified by Account. Fancy account edition, iykyk ✨


⭐ quote(Pid, Command) -> [FTPLine]

Types:

Pid = pid()
Command = string()
FTPLine = string()
πŸ“Œ Note: the telnet end of line characters from the FTP protocol definition, CRLF β€” for example, "\r\n" β€” have been removed. Gone. Not invited. πŸ’…

Sends an arbitrary FTP command and returns verbatim a list of the lines sent back by the FTP server. This function exists so your application can reach the FTP commands that are server-specific, or that this FTP client just can't provide. It's the backstage pass πŸ”₯

πŸ“Œ Note: FTP commands requiring a data connection cannot be successfully issued with this function. So don't try it. It will not slay. 😭

πŸ’€ ERRORS πŸ’€

okay so when stuff goes wrong (and bestie, sometimes it WILL go wrong 😭), formaterror/1 hands you back one of these reason atoms plus a diagnostic string. Here's the full lineup β€” no skips, we're reading the whole menu πŸ“

  • echunk β€” synchronization error during chunk sending, and it's giving one of these two vibes:
    • someone called send_chunk/2 or send_chunk_end/1 before ever calling send_chunk_start/2
    • someone called another transfer function mid-chunk-sending, meaning before send_chunk_end/1 was called
  • eclosed β€” the session is closed. bye bestie πŸ‘‹
  • econn β€” the connection to the remote server got closed prematurely 😬
  • ehost β€” host not found, FTP server not found, or the FTP server straight-up rejected the connection πŸ™ˆ
  • elogin β€” user is not logged in
  • enotbinary β€” the term is not a binary
  • epath β€” no such file or directory, or the directory already exists, or permission denied (the classic trio honestly πŸ’…)
  • etype β€” no such type
  • euser β€” invalid username or password 🚫
  • etnospc β€” insufficient storage space in system [452]
  • epnospc β€” exceeded storage allocation (for current directory or dataset) [552]
  • efnamena β€” filename not allowed [553]
fr fr, those bracketed numbers are the FTP reply codes and they are exactly as written β€” don't go rounding them or "fixing" them, iykyk ✨

🌈 SEE ALSO 🌈

go touch these other pages, they're iconic in their own right πŸ‘‘

  • file(3erl)
  • filename(3erl)
  • J. Postel and J. Reynolds: File Transfer Protocol (RFC 959) β€” the literal blueprint, the main character of the whole thing πŸ‘‘

and that's the page, bestie!! 🫢 you made it to the end, I'm so proud πŸ˜­πŸ’– this whole thing comes to you courtesy of Ericsson AB, and it's the ftp 1.1.4 manual page β€” that's ftp(3erl) to its friends. mwah, go write some FTP code, slay πŸ’…βœ¨πŸŽ€

πŸ“¦ where this page comes from