gftp
GFTP - A Gleam FTP/FTPS client library with full RFC support for both passive and active mode
Gleam FTP (gftp) is a Gleam client library for FTP (File Transfer Protocol) and FTPS (FTP over SSL/TLS) with full RFC 959, RFC 2228, RFC 4217, RFC 2428 and RFC 2389 compliance. It runs on the Erlang VM.
Add gftp to your project
gleam add gftp@1
Quick start
import gftp
import gftp/command/file_type
import gftp/stream
import gftp/result as ftp_result
import gleam/bit_array
import gleam/option.{None}
import gleam/result
pub fn main() {
// Connect and login
let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let assert Ok(_) = gftp.login(client, "user", "password")
// Set binary transfer type
let assert Ok(_) = gftp.transfer_type(client, file_type.Binary)
// Upload a file
let assert Ok(_) = gftp.stor(client, "hello.txt", fn(data_stream) {
stream.send(data_stream, bit_array.from_string("Hello, world!"))
|> result.map_error(ftp_result.Socket)
})
// List current directory
let assert Ok(_entries) = gftp.list(client, None)
// Download a file
let assert Ok(_) = gftp.retr(client, "hello.txt", fn(data_stream) {
let assert Ok(_data) = stream.receive(data_stream, 5000)
Ok(Nil)
})
// Quit and shutdown
let assert Ok(_) = gftp.quit(client)
let assert Ok(_) = gftp.shutdown(client)
}
FTPS (Secure FTP)
To use explicit FTPS, connect normally and then upgrade the connection:
let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let assert Ok(client) = gftp.into_secure(client, ssl_options)
let assert Ok(_) = gftp.login(client, "user", "password")
Data transfer modes
gftp defaults to passive mode. You can switch to active or extended passive mode:
import gftp/mode
// Extended passive mode (RFC 2428, supports IPv6)
let client = gftp.with_mode(client, mode.ExtendedPassive)
// Active mode with 30s timeout
let client = gftp.with_active_mode(client, 30_000)
// Enable NAT workaround for passive mode behind firewalls
let client = gftp.with_nat_workaround(client, True)
Directory listings
Parse structured file metadata from LIST or MLSD output:
import gftp/list as gftp_list
import gftp/list/file
import gleam/list
let assert Ok(lines) = gftp.list(client, None)
let assert Ok(files) = list.try_map(lines, gftp_list.parse_list)
let name = file.name(files |> list.first |> result.unwrap(file.empty()))
Error handling
All operations return FtpResult(a) which is Result(a, FtpError).
Use gftp/result.describe_error for human-readable messages:
import gftp/result
case gftp.cwd(client, "/nonexistent") {
Ok(_) -> // success
Error(err) -> {
let msg = result.describe_error(err)
}
}
Naming convention
Function names follow FTP command names (cwd, pwd, mkd, rmd, dele, retr, stor, etc.)
for direct mapping to the FTP protocol.
Types
A client to interact with an FTP server. This is the main entry point for using gftp.
pub opaque type FtpClient
A function that creates a new stream for the data connection in passive mode.
It takes the socket address and port as arguments and returns a FtpResult containing either a DataStream or an FtpError.
pub type PassiveStreamBuilder =
fn(String, Int) -> Result(stream.DataStream, result.FtpError)
Values
pub fn abor(
ftp_client: FtpClient,
) -> Result(Nil, result.FtpError)
Aborts a file transfer in progress. This is used to cancel an ongoing file transfer operation, such as a file upload or download, and close the data connection associated with that transfer.
This function should only be used when a file transfer is in progress, by calling it
from within the reader or writer function passed to retr, stor or appe, otherwise it may put the client in an inconsistent state.
pub fn appe(
ftp_client: FtpClient,
path: String,
writer: fn(stream.DataStream) -> Result(Nil, result.FtpError),
) -> Result(Nil, result.FtpError)
Append data to a file on the FTP server. If the file doesn’t exist, it will be created.
let assert Ok(_) = gftp.appe(client, "log.txt", fn(data_stream) {
stream.send(data_stream, bit_array.from_string("new log entry\n"))
|> result.map_error(ftp_result.Socket)
})
pub fn cdup(
ftp_client: FtpClient,
) -> Result(Nil, result.FtpError)
Change the current working directory on the FTP server to the parent directory of the current directory.
pub fn clear_command_channel(
ftp_client: FtpClient,
) -> Result(FtpClient, result.FtpError)
Perform clear command channel (CCC). Once the command is performed, the command channel will be encrypted no more. The data stream will still be secure.
pub fn connect(
host: String,
port: Int,
) -> Result(FtpClient, result.FtpError)
Try to connect to the remote server with the default timeout of 30 seconds.
Connects to the FTP server at the specified host and port, reads the server’s welcome message,
and returns an FtpClient instance. You must call login before performing any FTP operations.
let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let assert Ok(_) = gftp.login(client, "user", "password")
pub fn connect_secure_implicit(
host: String,
port: Int,
ssl_options: kafein.WrapOptions,
timeout: Int,
) -> Result(FtpClient, result.FtpError)
Connect to a remote FTPS server using an implicit secure connection (typically port 990).
Warning: implicit FTPS is considered deprecated. Prefer explicit mode with into_secure when possible.
let assert Ok(client) = gftp.connect_secure_implicit("ftp.example.com", 990, ssl_options, 30_000)
let assert Ok(_) = gftp.login(client, "user", "password")
pub fn connect_timeout(
host: String,
port port: Int,
timeout timeout: Int,
) -> Result(FtpClient, result.FtpError)
Try to connect to the remote server with a custom timeout.
The timeout parameter specifies the maximum time to wait for a connection to be established, in milliseconds.
You must call login before performing any FTP operations.
let assert Ok(client) = gftp.connect_timeout("ftp.example.com", 21, timeout: 10_000)
pub fn connect_with_stream(
stream: mug.Socket,
) -> Result(FtpClient, result.FtpError)
Connect to the FTP server using an existing socket stream.
This method doesn’t authenticate with the server, so after a successful connection, you will need to call the login
method to authenticate before performing any FTP operations.
On success, returns a FtpClient instance that can be used to interact with the FTP server.
On failure, returns an FtpError describing the issue.
pub fn custom_command(
ftp_client: FtpClient,
command_str: String,
expected_statuses: List(status.Status),
) -> Result(response.Response, result.FtpError)
Execute a custom FTP command and return the response.
Provide a list of expected status codes to validate the response against.
import gftp/status
let assert Ok(response) = gftp.custom_command(client, "SITE HELP", [status.Help])
pub fn custom_data_command(
ftp_client: FtpClient,
command_str: String,
expected_statuses: List(status.Status),
on_data_stream: fn(stream.DataStream, response.Response) -> Result(
Nil,
result.FtpError,
),
) -> Result(Nil, result.FtpError)
Execute a custom FTP command that uses a data connection.
The on_data_stream callback receives the data stream and the server response.
Use read_lines_from_stream to easily parse line-based output.
import gftp/status
let assert Ok(_) = gftp.custom_data_command(
client,
"LIST -la",
[status.AboutToSend, status.AlreadyOpen],
fn(data_stream, _response) {
let assert Ok(lines) = gftp.read_lines_from_stream(data_stream, 5000)
Ok(Nil)
},
)
pub fn cwd(
ftp_client: FtpClient,
path: String,
) -> Result(Nil, result.FtpError)
Change the current working directory on the FTP server to the specified path.
let assert Ok(_) = gftp.cwd(client, "/pub/data")
pub fn dele(
ftp_client: FtpClient,
path: String,
) -> Result(Nil, result.FtpError)
Delete the file at the specified path on the server.
let assert Ok(_) = gftp.dele(client, "unwanted_file.txt")
pub fn eprt(
ftp_client: FtpClient,
address: String,
port: Int,
ip_version: command.IpVersion,
) -> Result(Nil, result.FtpError)
The EPRT command allows for the specification of an extended address for the data connection. The extended address MUST consist of the network protocol as well as the network and transport addresses
pub fn feat(
ftp_client: FtpClient,
) -> Result(
dict.Dict(String, option.Option(String)),
result.FtpError,
)
Retrieve the features supported by the server (RFC 2389 FEAT command).
Returns a dictionary of feature names to optional parameter strings.
import gleam/dict
let assert Ok(features) = gftp.feat(client)
let supports_mlst = dict.has_key(features, "MLST")
pub fn get_stream(ftp_client: FtpClient) -> stream.DataStream
Get the current data stream of the FTP client. This can be either a TCP stream for plain connections or an SSL stream for secure connections.
pub fn into_secure(
ftp_client: FtpClient,
ssl_options: kafein.WrapOptions,
) -> Result(FtpClient, result.FtpError)
Switch to explicit secure mode (FTPS) using the provided SSL configuration.
Sends AUTH TLS, upgrades the connection to SSL/TLS, sets PBSZ to 0 and PROT to Private. This method does nothing if the connection is already secured.
let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let assert Ok(client) = gftp.into_secure(client, ssl_options)
let assert Ok(_) = gftp.login(client, "user", "password")
pub fn list(
ftp_client: FtpClient,
pathname: option.Option(String),
) -> Result(List(String), result.FtpError)
Execute LIST command which returns the detailed file listing in human readable format.
If pathname is omitted then the list of files in the current directory will be
returned otherwise it will the list of files on pathname.
Parse result
You can parse the output of this command with gftp/list module, which provides a File type
and the parse_list function to parse the output of the LIST command into a list of File structs,
which contain structured information about each file in the listing, such as name, size, permissions, and modification date.
import gftp
import gftp/list as gftp_list
import gleam/list
use lines <- result.try(gftp.list(ftp_client, None))
lines
|> list.try_map(gftp_list.parse_list)
pub fn login(
ftp_client: FtpClient,
username: String,
password: String,
) -> Result(Nil, result.FtpError)
Log in to the FTP server with the provided username and password. This is required before performing any FTP operations after connecting.
let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let assert Ok(_) = gftp.login(client, "user", "password")
pub fn mdtm(
ftp_client: FtpClient,
pathname: String,
) -> Result(timestamp.Timestamp, result.FtpError)
Retrieve the modification time of the file at pathname.
let assert Ok(mtime) = gftp.mdtm(client, "file.txt")
pub fn mkd(
ftp_client: FtpClient,
path: String,
) -> Result(Nil, result.FtpError)
Create a new directory on the server at the specified path.
let assert Ok(_) = gftp.mkd(client, "new_folder")
pub fn mlsd(
ftp_client: FtpClient,
pathname: option.Option(String),
) -> Result(List(String), result.FtpError)
Execute MLSD command which returns the machine-processable listing of a directory.
If pathname is omitted then the list of files in the current directory will be
returned otherwise it will the list of files on pathname.
Parse result
The output of the MLSD command is a machine-readable format that provides detailed information about each file in the listing,
such as name, size, permissions, and modification date, in a standardized format defined by RFC 3659.
You can parse the output of this command with gftp/list module, which provides a File type and the parse_mlsd
function to parse the output of the MLSD command into a list of File structs.
import gftp
import gftp/list as gftp_list
import gleam/list
use lines <- result.try(gftp.mlsd(ftp_client, None))
lines
|> list.try_map(gftp_list.parse_mlsd)
pub fn mlst(
ftp_client: FtpClient,
pathname: option.Option(String),
) -> Result(String, result.FtpError)
Execute MLST command which returns the machine-processable information for a file
Parse result
The output of the MLST command is a machine-readable format that provides detailed information about a file,
such as name, size, permissions, and modification date, in a standardized format defined by RFC 3659.
You can parse the output of this command with gftp/list module, which provides a File type and the parse_mlst
function to parse the output of the MLST command into a File struct.
import gftp
import gftp/list as gftp_list
import gleam/list
use line <- result.try(gftp.mlst(ftp_client, None))
use file <- result.try(gftp_list.parse_mlst(line))
pub fn nlst(
ftp_client: FtpClient,
pathname: option.Option(String),
) -> Result(List(String), result.FtpError)
Execute NLST command which returns the list of file names only.
If pathname is omitted then the list of files in the current directory will be
returned otherwise it will the list of files on pathname.
Parse result
The output of the NLST command is just a list of file names, so it doesn’t require any special parsing like the LIST command.
pub fn noop(
ftp_client: FtpClient,
) -> Result(Nil, result.FtpError)
Send a NOOP command to the FTP server to keep the connection alive or check if the server is responsive.
pub fn opts(
ftp_client: FtpClient,
option: String,
value: option.Option(String),
) -> Result(Nil, result.FtpError)
Set a server option (RFC 2389 OPTS command).
import gleam/option.{Some}
let assert Ok(_) = gftp.opts(client, "UTF8", Some("ON"))
pub fn pwd(
ftp_client: FtpClient,
) -> Result(String, result.FtpError)
Get the current working directory on the FTP server.
let assert Ok(cwd) = gftp.pwd(client)
pub fn quit(
ftp_client: FtpClient,
) -> Result(Nil, result.FtpError)
Quit the current FTP session.
Sends the QUIT command and waits for the server to confirm.
Call shutdown after this to close the underlying socket.
let assert Ok(_) = gftp.quit(client)
let assert Ok(_) = gftp.shutdown(client)
pub fn read_lines_from_stream(
data_stream: stream.DataStream,
timeout: Int,
) -> Result(List(String), result.FtpError)
Read lines from a data stream until the stream is closed by the server or an error occurs.
pub fn rename(
ftp_client: FtpClient,
from_name: String,
to_name: String,
) -> Result(Nil, result.FtpError)
Rename a file on the FTP server.
let assert Ok(_) = gftp.rename(client, "old_name.txt", "new_name.txt")
pub fn rest(
ftp_client: FtpClient,
offset: Int,
) -> Result(Nil, result.FtpError)
Tell the server to resume the transfer from a certain offset. The offset indicates the amount of bytes to skip from the beginning of the file. the REST command does not actually initiate the transfer. After issuing a REST command, the client must send the appropriate FTP command to transfer the file
It is possible to cancel the REST command, sending a REST command with offset 0
pub fn retr(
ftp_client: FtpClient,
path: String,
reader: fn(stream.DataStream) -> Result(Nil, result.FtpError),
) -> Result(Nil, result.FtpError)
Download a file from the FTP server.
The reader callback receives the data stream and should read data from it.
The data stream is automatically closed after the reader returns.
let assert Ok(_) = gftp.retr(client, "file.txt", fn(data_stream) {
let assert Ok(data) = stream.receive(data_stream, 5000)
// process data...
Ok(Nil)
})
pub fn rmd(
ftp_client: FtpClient,
path: String,
) -> Result(Nil, result.FtpError)
Remove the remote directory at the specified path.
let assert Ok(_) = gftp.rmd(client, "old_folder")
pub fn shutdown(
ftp_client: FtpClient,
) -> Result(Nil, result.FtpError)
Close the underlying socket connection to the FTP server.
You should call quit before this to gracefully end the FTP session.
pub fn site(
ftp_client: FtpClient,
sub_command: String,
) -> Result(response.Response, result.FtpError)
Execute a SITE command on the server and return the response.
let assert Ok(response) = gftp.site(client, "CHMOD 755 file.txt")
pub fn size(
ftp_client: FtpClient,
pathname: String,
) -> Result(Int, result.FtpError)
Retrieve the size in bytes of the file at pathname.
let assert Ok(size) = gftp.size(client, "file.txt")
pub fn stor(
ftp_client: FtpClient,
path: String,
writer: fn(stream.DataStream) -> Result(Nil, result.FtpError),
) -> Result(Nil, result.FtpError)
Upload a file to the FTP server.
The writer callback receives the data stream and should write the file data to it.
The data stream is automatically closed after the writer returns.
let assert Ok(_) = gftp.stor(client, "upload.txt", fn(data_stream) {
stream.send(data_stream, bit_array.from_string("file contents"))
|> result.map_error(ftp_result.Socket)
})
pub fn transfer_type(
ftp_client: FtpClient,
file_type: file_type.FileType,
) -> Result(Nil, result.FtpError)
Set the type of file to be transferred (FTP TYPE command).
Use file_type.Binary (or file_type.Image) for binary transfers,
file_type.Ascii(file_type.Default) for text transfers.
import gftp/command/file_type
let assert Ok(_) = gftp.transfer_type(client, file_type.Binary)
pub fn welcome_message(
ftp_client: FtpClient,
) -> option.Option(String)
Get the welcome message from the FTP server, if available.
pub fn with_active_mode(
ftp_client: FtpClient,
timeout: Int,
) -> FtpClient
Enable active mode for the FTP client with the specified timeout (in milliseconds) for the data connection.
In active mode, the client opens a local port and the server connects back to it for data transfer.
let client = gftp.with_active_mode(client, 30_000)
pub fn with_mode(
ftp_client: FtpClient,
mode: mode.Mode,
) -> FtpClient
Set the data transfer mode (Passive, ExtendedPassive, or Active).
import gftp/mode
let client = gftp.with_mode(client, mode.ExtendedPassive)
pub fn with_nat_workaround(
ftp_client: FtpClient,
nat_workaround: Bool,
) -> FtpClient
Enable or disable the NAT workaround for passive mode.
When enabled, the client uses the control connection’s peer address instead of the address returned by the PASV command for data connections. This is useful when the server is behind a NAT and reports its internal IP address.
let client = gftp.with_nat_workaround(client, True)
pub fn with_passive_stream_builder(
ftp_client: FtpClient,
builder: fn(String, Int) -> Result(
stream.DataStream,
result.FtpError,
),
) -> FtpClient
Set the passive stream builder function for the FTP client. This function is used to create a new stream for the data connection in passive mode.