GFTP - Gleam FTP Client Library

license-mit repo-stars Package Version Hex Docs

ci

Overview

Gleam FTP (gftp) is a Gleam client library for FTP (File Transfer Protocol) and FTPS (FTP over SSL/TLS) with full RFC compliance. It runs on the Erlang VM and provides a simple, type-safe API for all common FTP operations.

Based on the Rust FTP library suppaftp.

Features

Requirements

Installation

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)
}

Usage Guide

Connecting

import gftp

// Connect with default 30s timeout
let assert Ok(client) = gftp.connect("ftp.example.com", 21)

// Connect with custom timeout (in milliseconds)
let assert Ok(client) = gftp.connect_timeout("ftp.example.com", 21, timeout: 10_000)

FTPS (Secure FTP)

Explicit FTPS (recommended)

Connect over plain FTP, then upgrade the connection to TLS:

import gftp
import kafein

let assert Ok(client) = gftp.connect("ftp.example.com", 21)
let ssl_options = kafein.WrapOptions(
  server_name_indication: kafein.SniEnabled("ftp.example.com"),
  // ... other TLS options
)
let assert Ok(client) = gftp.into_secure(client, ssl_options)
let assert Ok(_) = gftp.login(client, "user", "password")

Implicit FTPS (legacy)

Connect directly over TLS (typically on port 990):

import gftp
import kafein

let ssl_options = kafein.WrapOptions(
  server_name_indication: kafein.SniEnabled("ftp.example.com"),
  // ... other TLS options
)
let assert Ok(client) = gftp.connect_secure_implicit("ftp.example.com", 990, ssl_options, 30_000)

Data Transfer Modes

gftp defaults to passive mode, which works in most environments. You can switch modes as needed:

import gftp
import gftp/mode

// Passive mode (default) - client connects to server for data transfer
let client = gftp.with_mode(client, mode.Passive)

// Extended passive mode (RFC 2428) - required by some servers, supports IPv6
let client = gftp.with_mode(client, mode.ExtendedPassive)

// Active mode - server connects back to client (30s timeout for the connection)
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 Operations

import gftp
import gleam/option.{None, Some}

// Print working directory
let assert Ok(cwd) = gftp.pwd(client)

// Change directory
let assert Ok(_) = gftp.cwd(client, "/pub/data")

// Go to parent directory
let assert Ok(_) = gftp.cdup(client)

// Create and remove directories
let assert Ok(_) = gftp.mkd(client, "new_folder")
let assert Ok(_) = gftp.rmd(client, "old_folder")

File Operations

import gftp
import gftp/stream
import gftp/result as ftp_result
import gleam/bit_array
import gleam/result

// Upload a file
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)
})

// Download a file
let assert Ok(_) = gftp.retr(client, "download.txt", fn(data_stream) {
  let assert Ok(data) = stream.receive(data_stream, 5000)
  // process data...
  Ok(Nil)
})

// Append to a file
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)
})

// Delete a file
let assert Ok(_) = gftp.dele(client, "old_file.txt")

// Rename a file
let assert Ok(_) = gftp.rename(client, "old_name.txt", "new_name.txt")

// Get file size and modification time
let assert Ok(size) = gftp.size(client, "file.txt")
let assert Ok(mtime) = gftp.mdtm(client, "file.txt")

Directory Listings

gftp provides multiple listing commands and parsers for structured output:

import gftp
import gftp/list as gftp_list
import gftp/list/file
import gftp/list/file_type
import gleam/list
import gleam/option.{None, Some}

// LIST command (human-readable format)
let assert Ok(lines) = gftp.list(client, None)
let assert Ok(files) = list.try_map(lines, gftp_list.parse_list)

// MLSD command (machine-readable, RFC 3659)
let assert Ok(lines) = gftp.mlsd(client, None)
let assert Ok(files) = list.try_map(lines, gftp_list.parse_mlsd)

// MLST command (single file info, RFC 3659)
let assert Ok(line) = gftp.mlst(client, Some("file.txt"))
let assert Ok(f) = gftp_list.parse_mlst(line)

// NLST command (file names only)
let assert Ok(names) = gftp.nlst(client, None)

// Access file metadata
let name = file.name(f)
let size = file.size(f)
let modified = file.modified(f)
let is_dir = file_type.is_directory(file.file_type(f))

Server Features

import gftp
import gleam/dict
import gleam/option.{None, Some}

// Discover server capabilities (RFC 2389)
let assert Ok(features) = gftp.feat(client)

// Check if a specific feature is supported
case dict.get(features, "MLST") {
  Ok(Some(params)) -> // MLST supported with params
  Ok(None) -> // MLST supported without params
  Error(_) -> // MLST not supported
}

// Set server options
let assert Ok(_) = gftp.opts(client, "UTF8", Some("ON"))

Error Handling

All operations return FtpResult(a), which is Result(a, FtpError):

import gftp
import gftp/result

case gftp.cwd(client, "/nonexistent") {
  Ok(_) -> // success
  Error(err) -> {
    // Get a human-readable error description
    let description = result.describe_error(err)
    // Match on specific error types
    case err {
      result.UnexpectedResponse(response) -> // server rejected the command
      result.ConnectionError(_) -> // connection failed
      result.Tls(_) -> // TLS error
      result.Socket(_) -> // socket error
      result.BadResponse -> // malformed server response
      _ -> // other errors
    }
  }
}

Custom Commands

For server-specific commands not covered by the API:

import gftp
import gftp/status

// Send a custom command
let assert Ok(response) = gftp.custom_command(client, "SITE CHMOD 755 file.txt", [status.CommandOk])

// Send a custom command that uses a data connection
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)
    // process lines...
    Ok(Nil)
  },
)

Naming Convention

Function names follow FTP command names for familiarity with the protocol: cwd (Change Working Directory), pwd (Print Working Directory), mkd (Make Directory), rmd (Remove Directory), dele (Delete), retr (Retrieve), stor (Store), appe (Append), nlst (Name List), mdtm (Modification Time), etc.

API Documentation

Full API documentation is available at https://hexdocs.pm/gftp.

Development

gleam build        # Build the project
gleam test         # Run unit tests
gleam format       # Format code

Integration tests

Integration tests run against a real FTP server inside a Docker container. You need Docker installed and running on your machine to execute them.

GFTP_INTEGRATION_TESTS=1 gleam test

To also run active mode tests:

GFTP_INTEGRATION_TESTS=1 GFTP_ACTIVE_MODE_TESTS=1 gleam test

License

This project is licensed under the MIT License. See the LICENSE file for details.

Search Document