Current section
Files
Jump to
Current section
Files
src/gftp.erl
-module(gftp).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gftp.gleam").
-export([welcome_message/1, with_active_mode/2, with_passive_stream_builder/2, with_data_timeout/2, with_mode/2, with_nat_workaround/2, get_stream/1, shutdown/1, read_response_in/2, list/2, data_command/2, into_secure/2, login/3, clear_command_channel/1, cwd/2, cdup/1, pwd/1, noop/1, eprt/4, mkd/2, transfer_type/2, quit/1, rename/3, rmd/2, dele/2, rest/2, abor/1, nlst/2, mlsd/2, mlst/2, mdtm/2, size/2, feat/1, opts/3, site/2, custom_command/3, connect_with_stream/1, connect_timeout/3, connect/2, connect_secure_implicit/4, finalize_data_command/2, retr/3, stor/3, appe/3, custom_data_command/4, read_lines_from_stream/2]).
-export_type([ftp_client/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" GFTP - A Gleam FTP/FTPS client library with full RFC support for both passive and active mode\n"
"\n"
" Gleam FTP (gftp) is a Gleam client library for FTP (File Transfer Protocol) and FTPS (FTP over SSL/TLS) with full\n"
" RFC 959, RFC 2228, RFC 4217, RFC 2428 and RFC 2389 compliance. It runs on the Erlang VM.\n"
"\n"
" ## Add gftp to your project\n"
"\n"
" ```bash\n"
" gleam add gftp@2\n"
" ```\n"
"\n"
" ## Quick start\n"
"\n"
" ```gleam\n"
" import gftp\n"
" import gftp/file_type\n"
" import gftp/stream\n"
" import gftp/result as ftp_result\n"
" import gleam/bit_array\n"
" import gleam/option.{None}\n"
" import gleam/result\n"
"\n"
" pub fn main() {\n"
" // Connect and login\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
"\n"
" // Set binary transfer type\n"
" let assert Ok(_) = gftp.transfer_type(client, file_type.Binary)\n"
"\n"
" // Upload a file\n"
" let assert Ok(_) = gftp.stor(client, \"hello.txt\", fn(data_stream) {\n"
" stream.send(data_stream, bit_array.from_string(\"Hello, world!\"))\n"
" |> result.map_error(ftp_result.Socket)\n"
" })\n"
"\n"
" // List current directory\n"
" let assert Ok(_entries) = gftp.list(client, None)\n"
"\n"
" // Download a file\n"
" let assert Ok(_) = gftp.retr(client, \"hello.txt\", fn(data_stream) {\n"
" let assert Ok(_data) = stream.receive(data_stream, 5000)\n"
" Ok(Nil)\n"
" })\n"
"\n"
" // Quit and shutdown\n"
" let assert Ok(_) = gftp.quit(client)\n"
" let assert Ok(_) = gftp.shutdown(client)\n"
" }\n"
" ```\n"
"\n"
" ## OTP actor (safe message-based streaming)\n"
"\n"
" For message-based, non-blocking data transfers, use `gftp/actor` which wraps\n"
" the FTP client in an OTP actor. The actor serializes all operations and\n"
" rejects control commands while a data channel is open (`DataTransferInProgress`).\n"
"\n"
" ```gleam\n"
" import gftp\n"
" import gftp/actor as ftp_actor\n"
"\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(started) = ftp_actor.start(client)\n"
" let handle = started.data\n"
"\n"
" let assert Ok(_) = ftp_actor.login(handle, \"user\", \"password\")\n"
" let assert Ok(data_stream) = ftp_actor.open_retr(handle, \"file.txt\")\n"
" // ... receive data via stream.receive_next_packet_as_message ...\n"
" let assert Ok(_) = ftp_actor.close_data_channel(handle, data_stream)\n"
" let assert Ok(_) = ftp_actor.quit(handle)\n"
" ```\n"
"\n"
" ## FTPS (Secure FTP)\n"
"\n"
" To use explicit FTPS, connect normally and then upgrade the connection:\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(client) = gftp.into_secure(client, ssl_options)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
" ```\n"
"\n"
" ## Data transfer modes\n"
"\n"
" gftp defaults to passive mode. You can switch to active or extended passive mode:\n"
"\n"
" ```gleam\n"
" import gftp/mode\n"
"\n"
" // Extended passive mode (RFC 2428, supports IPv6)\n"
" let client = gftp.with_mode(client, mode.ExtendedPassive)\n"
"\n"
" // Active mode with 30s timeout\n"
" let client = gftp.with_active_mode(client, 30_000)\n"
"\n"
" // Enable NAT workaround for passive mode behind firewalls\n"
" let client = gftp.with_nat_workaround(client, True)\n"
" ```\n"
"\n"
" ## Directory listings\n"
"\n"
" Parse structured file metadata from LIST or MLSD output:\n"
"\n"
" ```gleam\n"
" import gftp/list as gftp_list\n"
" import gftp/list/file\n"
" import gleam/list\n"
"\n"
" let assert Ok(lines) = gftp.list(client, None)\n"
" let assert Ok(files) = list.try_map(lines, gftp_list.parse_list)\n"
" let name = file.name(files |> list.first |> result.unwrap(file.empty()))\n"
" ```\n"
"\n"
" ## Error handling\n"
"\n"
" All operations return `FtpResult(a)` which is `Result(a, FtpError)`.\n"
" Use `gftp/result.describe_error` for human-readable messages:\n"
"\n"
" ```gleam\n"
" import gftp/result\n"
"\n"
" case gftp.cwd(client, \"/nonexistent\") {\n"
" Ok(_) -> // success\n"
" Error(err) -> {\n"
" let msg = result.describe_error(err)\n"
" }\n"
" }\n"
" ```\n"
"\n"
" ## Naming convention\n"
"\n"
" Function names follow FTP command names (`cwd`, `pwd`, `mkd`, `rmd`, `dele`, `retr`, `stor`, etc.)\n"
" for direct mapping to the FTP protocol.\n"
"\n"
).
-opaque ftp_client() :: {ftp_client,
gftp@stream:data_stream(),
gftp@mode:mode(),
boolean(),
gleam@option:option(binary()),
fun((binary(), integer()) -> {ok, gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()}),
gleam@option:option(kafein:wrap_options()),
integer()}.
-file("src/gftp.gleam", 272).
?DOC(" Get the welcome message from the FTP server, if available.\n").
-spec welcome_message(ftp_client()) -> gleam@option:option(binary()).
welcome_message(Ftp_client) ->
erlang:element(5, Ftp_client).
-file("src/gftp.gleam", 283).
?DOC(
" Enable active mode for the FTP client with the specified timeout (in milliseconds) for the data connection.\n"
"\n"
" In active mode, the client opens a local port and the server connects back to it for data transfer.\n"
"\n"
" ```gleam\n"
" let client = gftp.with_active_mode(client, 30_000)\n"
" ```\n"
).
-spec with_active_mode(ftp_client(), integer()) -> ftp_client().
with_active_mode(Ftp_client, Timeout) ->
{ftp_client,
erlang:element(2, Ftp_client),
{active, Timeout},
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
erlang:element(7, Ftp_client),
erlang:element(8, Ftp_client)}.
-file("src/gftp.gleam", 289).
?DOC(
" Set the passive stream builder function for the FTP client.\n"
" This function is used to create a new stream for the data connection in passive mode.\n"
).
-spec with_passive_stream_builder(
ftp_client(),
fun((binary(), integer()) -> {ok, gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()})
) -> ftp_client().
with_passive_stream_builder(Ftp_client, Builder) ->
{ftp_client,
erlang:element(2, Ftp_client),
erlang:element(3, Ftp_client),
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
Builder,
erlang:element(7, Ftp_client),
erlang:element(8, Ftp_client)}.
-file("src/gftp.gleam", 302).
?DOC(
" Set the timeout in milliseconds for data channel operations (reads/writes on the data stream).\n"
" Defaults to 30000 (30 seconds).\n"
"\n"
" ```gleam\n"
" let client = gftp.with_data_timeout(client, 60_000)\n"
" ```\n"
).
-spec with_data_timeout(ftp_client(), integer()) -> ftp_client().
with_data_timeout(Ftp_client, Timeout) ->
{ftp_client,
erlang:element(2, Ftp_client),
erlang:element(3, Ftp_client),
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
erlang:element(7, Ftp_client),
Timeout}.
-file("src/gftp.gleam", 313).
?DOC(
" Set the data transfer mode (Passive, ExtendedPassive, or Active).\n"
"\n"
" ```gleam\n"
" import gftp/mode\n"
"\n"
" let client = gftp.with_mode(client, mode.ExtendedPassive)\n"
" ```\n"
).
-spec with_mode(ftp_client(), gftp@mode:mode()) -> ftp_client().
with_mode(Ftp_client, Mode) ->
{ftp_client,
erlang:element(2, Ftp_client),
Mode,
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
erlang:element(7, Ftp_client),
erlang:element(8, Ftp_client)}.
-file("src/gftp.gleam", 326).
?DOC(
" Enable or disable the NAT workaround for passive mode.\n"
"\n"
" When enabled, the client uses the control connection's peer address instead of the address\n"
" returned by the PASV command for data connections. This is useful when the server is behind\n"
" a NAT and reports its internal IP address.\n"
"\n"
" ```gleam\n"
" let client = gftp.with_nat_workaround(client, True)\n"
" ```\n"
).
-spec with_nat_workaround(ftp_client(), boolean()) -> ftp_client().
with_nat_workaround(Ftp_client, Nat_workaround) ->
{ftp_client,
erlang:element(2, Ftp_client),
erlang:element(3, Ftp_client),
Nat_workaround,
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
erlang:element(7, Ftp_client),
erlang:element(8, Ftp_client)}.
-file("src/gftp.gleam", 423).
?DOC(" 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.\n").
-spec get_stream(ftp_client()) -> gftp@stream:data_stream().
get_stream(Ftp_client) ->
erlang:element(2, Ftp_client).
-file("src/gftp.gleam", 587).
?DOC(
" Close the underlying socket connection to the FTP server.\n"
"\n"
" You should call `quit` before this to gracefully end the FTP session.\n"
).
-spec shutdown(ftp_client()) -> {ok, nil} | {error, gftp@result:ftp_error()}.
shutdown(Ftp_client) ->
_pipe = erlang:element(2, Ftp_client),
_pipe@1 = gftp@stream:shutdown(_pipe),
gleam@result:map_error(_pipe@1, fun(Field@0) -> {socket, Field@0} end).
-file("src/gftp.gleam", 1131).
?DOC(" Strip a trailing 0x0A (LF) byte if present, since {packet, line} includes it.\n").
-spec strip_trailing_lf(bitstring()) -> bitstring().
strip_trailing_lf(Bytes) ->
Size = erlang:byte_size(Bytes),
case Size > 0 of
true ->
case gleam_stdlib:bit_array_slice(Bytes, Size - 1, 1) of
{ok, <<16#0A>>} ->
Trimmed@1 = case gleam_stdlib:bit_array_slice(
Bytes,
0,
Size - 1
) of
{ok, Trimmed} -> Trimmed;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"gftp"/utf8>>,
function => <<"strip_trailing_lf"/utf8>>,
line => 1137,
value => _assert_fail,
start => 39491,
'end' => 39551,
pattern_start => 39502,
pattern_end => 39513})
end,
Trimmed@1;
_ ->
Bytes
end;
false ->
Bytes
end.
-file("src/gftp.gleam", 1118).
?DOC(
" Read a line from the provided data stream with the specified timeout and return it as a string.\n"
" Expects the stream to be in {packet, line} mode, where recv returns one complete line per call.\n"
).
-spec read_line_with(gftp@stream:data_stream(), integer()) -> {ok, binary()} |
{error, gftp@result:ftp_error()}.
read_line_with(Data_stream, Timeout) ->
case gftp@stream:'receive'(Data_stream, Timeout) of
{ok, Bytes} ->
Bytes@1 = strip_trailing_lf(Bytes),
_pipe = Bytes@1,
_pipe@1 = gleam@bit_array:to_string(_pipe),
gleam@result:map_error(_pipe@1, fun(_) -> bad_response end);
{error, E} ->
{error, {socket, E}}
end.
-file("src/gftp.gleam", 1112).
?DOC(" Read a line from the control connection stream and return it as a string along with bytes read.\n").
-spec read_line(ftp_client()) -> {ok, binary()} |
{error, gftp@result:ftp_error()}.
read_line(Ftp_client) ->
read_line_with(erlang:element(2, Ftp_client), erlang:element(8, Ftp_client)).
-file("src/gftp.gleam", 1167).
?DOC(" Read the `Status` code from the response line and convert it to a `Status` type.\n").
-spec status_from_bytes(binary()) -> {ok, gftp@status:status()} |
{error, gftp@result:ftp_error()}.
status_from_bytes(Line) ->
_pipe = Line,
_pipe@1 = gleam@string:slice(_pipe, 0, 3),
_pipe@2 = gleam_stdlib:parse_int(_pipe@1),
_pipe@3 = gleam@result:map(_pipe@2, fun gftp@status:from_int/1),
gleam@result:map_error(_pipe@3, fun(_) -> bad_response end).
-file("src/gftp.gleam", 1176).
?DOC(" Write data to stream with the command to perform\n").
-spec perform(ftp_client(), gftp@internal@command:command()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
perform(Ftp_client, Command) ->
_pipe = Command,
_pipe@1 = gftp@internal@command:to_string(_pipe),
_pipe@2 = (fun(S) -> <<S/binary, "\r\n"/utf8>> end)(_pipe@1),
_pipe@3 = gleam_stdlib:identity(_pipe@2),
_pipe@4 = (fun(Data) ->
gftp@stream:send(erlang:element(2, Ftp_client), Data)
end)(_pipe@3),
gleam@result:map_error(_pipe@4, fun(Field@0) -> {socket, Field@0} end).
-file("src/gftp.gleam", 1188).
?DOC(
" Default implementation of the `PassiveStreamBuilder` function type.\n"
" \n"
" It just connects to the specified host and port using a TCP stream and wraps it in a `DataStream` type.\n"
).
-spec default_passive_stream_builder(binary(), integer()) -> {ok,
gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()}.
default_passive_stream_builder(Host, Port) ->
_pipe = mug:new(Host, Port),
_pipe@1 = mug:connect(_pipe),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {connection_error, Field@0} end
),
gleam@result:map(_pipe@2, fun(Socket) -> {tcp, Socket} end).
-file("src/gftp.gleam", 1287).
?DOC(
" Build the PORT command argument string from an IP address and port.\n"
" Format: \"h1,h2,h3,h4,p1,p2\" where h1-h4 are IP octets and p1,p2 are port MSB,LSB.\n"
).
-spec build_active_port_arg(binary(), integer()) -> binary().
build_active_port_arg(Ip, Port) ->
Ip_part = gleam@string:replace(Ip, <<"."/utf8>>, <<","/utf8>>),
Msb = Port div 256,
Lsb = Port rem 256,
<<<<<<<<Ip_part/binary, ","/utf8>>/binary,
(erlang:integer_to_binary(Msb))/binary>>/binary,
","/utf8>>/binary,
(erlang:integer_to_binary(Lsb))/binary>>.
-file("src/gftp.gleam", 1270).
?DOC(
" Send the appropriate PORT or EPRT command to the server based on\n"
" the IP version of the local address.\n"
).
-spec send_port_command(ftp_client(), binary(), integer(), boolean()) -> {ok,
nil} |
{error, gftp@result:ftp_error()}.
send_port_command(Ftp_client, Local_ip, Listen_port, Is_ipv6) ->
case Is_ipv6 of
true ->
perform(Ftp_client, {eprt, Local_ip, Listen_port, v6});
false ->
Port_string = build_active_port_arg(Local_ip, Listen_port),
perform(Ftp_client, {port, Port_string})
end.
-file("src/gftp.gleam", 1389).
?DOC(
" Build a data stream for the data connection in passive mode, using the provided host and port.\n"
" \n"
" If TLS options are set in the `FtpClient`, it will upgrade the stream to a secure connection using SSL/TLS before returning it.\n"
).
-spec build_data_channel_stream(ftp_client(), binary(), integer()) -> {ok,
gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()}.
build_data_channel_stream(Ftp_client, Host, Port) ->
gleam@result:'try'(
(erlang:element(6, Ftp_client))(Host, Port),
fun(Stream) -> case erlang:element(7, Ftp_client) of
{some, Ssl_options} ->
_pipe = Stream,
_pipe@1 = gftp@stream:upgrade_to_ssl(_pipe, Ssl_options),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {tls, Field@0} end
);
none ->
{ok, Stream}
end end
).
-file("src/gftp.gleam", 1343).
?DOC(" Parse the passive mode address and port from the server's response to the `PASV` command.\n").
-spec parse_passive_address_from_response(gftp@response:response()) -> {ok,
{binary(), integer()}} |
{error, gftp@result:ftp_error()}.
parse_passive_address_from_response(Response) ->
Regex@1 = case gleam@regexp:from_string(
<<"\\((\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+)\\)"/utf8>>
) of
{ok, Regex} -> Regex;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"gftp"/utf8>>,
function => <<"parse_passive_address_from_response"/utf8>>,
line => 1346,
value => _assert_fail,
start => 46255,
'end' => 46313,
pattern_start => 46266,
pattern_end => 46275})
end,
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Response@1) ->
case gftp@internal@utils:re_matches(Regex@1, Response@1) of
{ok,
[{some, A},
{some, B},
{some, C},
{some, D},
{some, Msb},
{some, Lsb}]} ->
Address = gleam@string:join([A, B, C, D], <<"."/utf8>>),
gleam@result:'try'(
begin
_pipe = Msb,
_pipe@1 = gleam_stdlib:parse_int(_pipe),
gleam@result:map_error(
_pipe@1,
fun(_) -> bad_response end
)
end,
fun(Msb@1) ->
gleam@result:'try'(
begin
_pipe@2 = Lsb,
_pipe@3 = gleam_stdlib:parse_int(_pipe@2),
gleam@result:map_error(
_pipe@3,
fun(_) -> bad_response end
)
end,
fun(Lsb@1) ->
Port = begin
_pipe@4 = Msb@1,
_pipe@5 = erlang:'bsl'(_pipe@4, 8),
erlang:'bor'(_pipe@5, Lsb@1)
end,
{ok, {Address, Port}}
end
)
end
);
_ ->
{error, bad_response}
end
end
).
-file("src/gftp.gleam", 1373).
?DOC(" Parse the extended passive mode port from the server's response to the `EPSV` command.\n").
-spec parse_epsv_address_from_response(gftp@response:response()) -> {ok,
integer()} |
{error, gftp@result:ftp_error()}.
parse_epsv_address_from_response(Response) ->
Regex@1 = case gleam@regexp:from_string(<<"\\(\\|\\|\\|(\\d+)\\|\\)"/utf8>>) of
{ok, Regex} -> Regex;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"gftp"/utf8>>,
function => <<"parse_epsv_address_from_response"/utf8>>,
line => 1374,
value => _assert_fail,
start => 47157,
'end' => 47215,
pattern_start => 47168,
pattern_end => 47177})
end,
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Response@1) ->
case gftp@internal@utils:re_matches(Regex@1, Response@1) of
{ok, [{some, Port_str}]} ->
_pipe = Port_str,
_pipe@1 = gleam_stdlib:parse_int(_pipe),
gleam@result:map_error(_pipe@1, fun(_) -> bad_response end);
_ ->
{error, bad_response}
end
end
).
-file("src/gftp.gleam", 1214).
?DOC(" Execute a data command in active mode, which requires the client to listen for an incoming connection from the server for the data transfer.\n").
-spec data_command_active(
ftp_client(),
gftp@internal@command:command(),
integer()
) -> {ok, gftp@stream:data_stream()} | {error, gftp@result:ftp_error()}.
data_command_active(Ftp_client, Command, Timeout) ->
gleam@result:'try'(
begin
_pipe = gftp@stream:local_address(erlang:element(2, Ftp_client)),
gleam@result:map_error(_pipe, fun(Field@0) -> {socket, Field@0} end)
end,
fun(_use0) ->
{Local_ip, _} = _use0,
Is_ipv6 = gftp@internal@utils:is_ipv6_address(Local_ip),
Ip_family = case Is_ipv6 of
true ->
ipv6;
false ->
ipv4
end,
gleam@result:'try'(
begin
_pipe@1 = listener_ffi:listen(Ip_family),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {socket, Field@0} end
)
end,
fun(Listen_socket) ->
gleam@result:'try'(
begin
_pipe@2 = listener_ffi:listener_port(Listen_socket),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {socket, Field@0} end
)
end,
fun(Listen_port) ->
gleam@result:'try'(
send_port_command(
Ftp_client,
Local_ip,
Listen_port,
Is_ipv6
),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) ->
gleam@result:'try'(
perform(Ftp_client, Command),
fun(_) ->
Accept_result = begin
_pipe@3 = listener_ffi:accept(
Listen_socket,
Timeout
),
gleam@result:map_error(
_pipe@3,
fun(Field@0) -> {socket, Field@0} end
)
end,
listener_ffi:close(
Listen_socket
),
gleam@result:'try'(
Accept_result,
fun(Accepted_socket) ->
Data_stream = {tcp,
Accepted_socket},
case erlang:element(
7,
Ftp_client
) of
{some,
Ssl_options} ->
_pipe@4 = Data_stream,
_pipe@5 = gftp@stream:upgrade_to_ssl(
_pipe@4,
Ssl_options
),
gleam@result:map_error(
_pipe@5,
fun(Field@0) -> {tls, Field@0} end
);
none ->
{ok,
Data_stream}
end
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 1064).
?DOC(
" Key function to read a response from the server and check if it matches the expected status code.\n"
" \n"
" It returns error if it fails to read a response or if the response status code doesn't match the expected status.\n"
" \n"
" It internally just calls `read_response_in` with a single expected status code\n"
).
-spec read_response(ftp_client(), gftp@status:status()) -> {ok,
gftp@response:response()} |
{error, gftp@result:ftp_error()}.
read_response(Ftp_client, Expected_status) ->
read_response_in(Ftp_client, [Expected_status]).
-file("src/gftp.gleam", 1075).
?DOC(
" Read a response from the server and check if it matches any of the expected status codes.\n"
"\n"
" **WARNING:** This function is intended for internal use by `gftp/actor` and\n"
" `gftp/internal/data_channel`. It may change without notice in future versions.\n"
).
-spec read_response_in(ftp_client(), list(gftp@status:status())) -> {ok,
gftp@response:response()} |
{error, gftp@result:ftp_error()}.
read_response_in(Ftp_client, Expected_statuses) ->
gleam@result:'try'(
read_line(Ftp_client),
fun(Line) ->
gleam@result:'try'(
status_from_bytes(Line),
fun(Status) ->
Is_multiline = gleam@string:slice(Line, 3, 1) =:= <<"-"/utf8>>,
gleam@result:'try'(case Is_multiline of
false ->
{ok, [Line]};
true ->
First_3_bytes = begin
_pipe = Line,
_pipe@1 = gleam@string:slice(_pipe, 0, 3),
gleam_stdlib:identity(_pipe@1)
end,
Final_line = gleam@bit_array:append(
First_3_bytes,
<<16#20>>
),
Expected = case gleam@list:contains(
Expected_statuses,
system
) of
true ->
[gleam@bit_array:append(
First_3_bytes,
<<"-"/utf8>>
),
Final_line];
false ->
[Final_line]
end,
read_response_body([Line], Ftp_client, Expected)
end, fun(Body) ->
Body@1 = begin
_pipe@2 = Body,
_pipe@3 = gleam@string:join(
_pipe@2,
<<"\n"/utf8>>
),
gleam_stdlib:identity(_pipe@3)
end,
Response = {response, Status, Body@1},
case gleam@list:contains(Expected_statuses, Status) of
true ->
{ok, Response};
false ->
{error, {unexpected_response, Response}}
end
end)
end
)
end
).
-file("src/gftp.gleam", 1147).
?DOC(" Read the response body from the server, handling multi-line responses if necessary.\n").
-spec read_response_body(list(binary()), ftp_client(), list(bitstring())) -> {ok,
list(binary())} |
{error, gftp@result:ftp_error()}.
read_response_body(Acc, Ftp_client, Expected) ->
gleam@result:'try'(
read_line(Ftp_client),
fun(Line) ->
case (string:length(Line) < 5) orelse not gleam@list:contains(
Expected,
begin
_pipe = Line,
_pipe@1 = gleam@string:slice(_pipe, 0, 4),
gleam_stdlib:identity(_pipe@1)
end
) of
true ->
read_response_body([Line | Acc], Ftp_client, Expected);
false ->
_pipe@2 = [Line | Acc],
_pipe@3 = lists:reverse(_pipe@2),
{ok, _pipe@3}
end
end
).
-file("src/gftp.gleam", 769).
?DOC(
" Execute `LIST` command which returns the detailed file listing in human readable format.\n"
" If `pathname` is omitted then the list of files in the current directory will be\n"
" returned otherwise it will the list of files on `pathname`.\n"
"\n"
" ### Parse result\n"
"\n"
" You can parse the output of this command with `gftp/list` module, which provides a `File` type\n"
" and the `parse_list` function to parse the output of the `LIST` command into a list of `File` structs,\n"
" which contain structured information about each file in the listing, such as name, size, permissions, and modification date.\n"
"\n"
" ```gleam\n"
" import gftp\n"
" import gftp/list as gftp_list\n"
" import gleam/list\n"
"\n"
" use lines <- result.try(gftp.list(ftp_client, None))\n"
" lines\n"
" |> list.try_map(gftp_list.parse_list)\n"
" ```\n"
).
-spec list(ftp_client(), gleam@option:option(binary())) -> {ok, list(binary())} |
{error, gftp@result:ftp_error()}.
list(Ftp_client, Pathname) ->
stream_lines(Ftp_client, {list, Pathname}, about_to_send).
-file("src/gftp.gleam", 1429).
?DOC(
" Execute a data command which returns list of strings in a separate stream.\n"
" \n"
" This is basically a convenience function to execute a data command and read the response as a list of lines,\n"
" which is useful for commands like `LIST` or `NLST` that return directory listings.\n"
).
-spec stream_lines(
ftp_client(),
gftp@internal@command:command(),
gftp@status:status()
) -> {ok, list(binary())} | {error, gftp@result:ftp_error()}.
stream_lines(Ftp_client, Command, Open_code) ->
gleam@result:'try'(
data_command(Ftp_client, Command),
fun(Data_stream) ->
gleam@result:'try'(
read_response_in(Ftp_client, [Open_code, already_open]),
fun(_) ->
gleam@result:'try'(
read_lines_from_stream(
Data_stream,
erlang:element(8, Ftp_client)
),
fun(Lines) ->
gleam@result:'try'(
finalize_data_command(Ftp_client, Data_stream),
fun(_) -> {ok, Lines} end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 1202).
?DOC(
" Execute a command that uses a separate data stream.\n"
"\n"
" **WARNING:** This function is intended for internal use by `gftp/actor` and\n"
" `gftp/internal/data_channel`. It may change without notice in future versions.\n"
).
-spec data_command(ftp_client(), gftp@internal@command:command()) -> {ok,
gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()}.
data_command(Ftp_client, Command) ->
case erlang:element(3, Ftp_client) of
{active, Timeout} ->
data_command_active(Ftp_client, Command, Timeout);
passive ->
data_command_passive(Ftp_client, Command);
extended_passive ->
data_command_extended_passive(Ftp_client, Command)
end.
-file("src/gftp.gleam", 343).
?DOC(
" Switch to explicit secure mode (FTPS) using the provided SSL configuration.\n"
"\n"
" Sends AUTH TLS, upgrades the connection to SSL/TLS, sets PBSZ to 0 and PROT to Private.\n"
" This method does nothing if the connection is already secured.\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(client) = gftp.into_secure(client, ssl_options)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
" ```\n"
).
-spec into_secure(ftp_client(), kafein:wrap_options()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
into_secure(Ftp_client, Ssl_options) ->
gleam@result:'try'(
perform(Ftp_client, auth),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, auth_ok),
fun(_) ->
gleam@result:'try'(
begin
_pipe = erlang:element(2, Ftp_client),
_pipe@1 = gftp@stream:upgrade_to_ssl(
_pipe,
Ssl_options
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {tls, Field@0} end
)
end,
fun(Stream) ->
Ftp_client@1 = {ftp_client,
Stream,
erlang:element(3, Ftp_client),
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
{some, Ssl_options},
erlang:element(8, Ftp_client)},
gleam@result:'try'(
perform(Ftp_client@1, {pbsz, 0}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client@1, command_ok),
fun(_) ->
gleam@result:'try'(
perform(
Ftp_client@1,
{prot, private}
),
fun(_) ->
gleam@result:'try'(
read_response(
Ftp_client@1,
command_ok
),
fun(_) ->
{ok, Ftp_client@1}
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 434).
?DOC(
" Log in to the FTP server with the provided username and password.\n"
" This is required before performing any FTP operations after connecting.\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
" ```\n"
).
-spec login(ftp_client(), binary(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
login(Ftp_client, Username, Password) ->
gleam@result:'try'(
perform(Ftp_client, {user, Username}),
fun(_) ->
gleam@result:'try'(
read_response_in(Ftp_client, [logged_in, need_password]),
fun(Auth_response) -> case erlang:element(2, Auth_response) of
logged_in ->
{ok, nil};
need_password ->
gleam@result:'try'(
perform(Ftp_client, {pass, Password}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, logged_in),
fun(_) -> {ok, nil} end
)
end
);
_ ->
{error, {unexpected_response, Auth_response}}
end end
)
end
).
-file("src/gftp.gleam", 458).
?DOC(
" Perform clear command channel (CCC).\n"
" Once the command is performed, the command channel will be encrypted no more.\n"
" The data stream will still be secure.\n"
).
-spec clear_command_channel(ftp_client()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
clear_command_channel(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, clear_command_channel),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) ->
Ftp_client@1 = {ftp_client,
gftp@stream:downgrade_to_tcp(
erlang:element(2, Ftp_client)
),
erlang:element(3, Ftp_client),
erlang:element(4, Ftp_client),
erlang:element(5, Ftp_client),
erlang:element(6, Ftp_client),
erlang:element(7, Ftp_client),
erlang:element(8, Ftp_client)},
{ok, Ftp_client@1}
end
)
end
).
-file("src/gftp.gleam", 476).
?DOC(
" Change the current working directory on the FTP server to the specified path.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.cwd(client, \"/pub/data\")\n"
" ```\n"
).
-spec cwd(ftp_client(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
cwd(Ftp_client, Path) ->
gleam@result:'try'(
perform(Ftp_client, {cwd, Path}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, requested_file_action_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 484).
?DOC(" Change the current working directory on the FTP server to the parent directory of the current directory.\n").
-spec cdup(ftp_client()) -> {ok, nil} | {error, gftp@result:ftp_error()}.
cdup(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, cdup),
fun(_) ->
gleam@result:'try'(
read_response_in(
Ftp_client,
[command_ok, requested_file_action_ok]
),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 501).
?DOC(
" Get the current working directory on the FTP server.\n"
"\n"
" ```gleam\n"
" let assert Ok(cwd) = gftp.pwd(client)\n"
" ```\n"
).
-spec pwd(ftp_client()) -> {ok, binary()} | {error, gftp@result:ftp_error()}.
pwd(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, pwd),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, path_created),
fun(Response) ->
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Body) -> _pipe = Body,
_pipe@1 = gftp@internal@utils:extract_str(
_pipe,
<<"\""/utf8>>
),
gleam@result:map_error(
_pipe@1,
fun(_) -> bad_response end
) end
)
end
)
end
).
-file("src/gftp.gleam", 512).
?DOC(" Send a NOOP command to the FTP server to keep the connection alive or check if the server is responsive.\n").
-spec noop(ftp_client()) -> {ok, nil} | {error, gftp@result:ftp_error()}.
noop(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, noop),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 521).
?DOC(
" The EPRT command allows for the specification of an extended address for the data connection.\n"
" The extended address MUST consist of the network protocol as well as the network and transport addresses\n"
).
-spec eprt(ftp_client(), binary(), integer(), gftp@mode:ip_version()) -> {ok,
nil} |
{error, gftp@result:ftp_error()}.
eprt(Ftp_client, Address, Port, Ip_version) ->
gleam@result:'try'(
perform(Ftp_client, {eprt, Address, Port, Ip_version}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 541).
?DOC(
" Create a new directory on the server at the specified path.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.mkd(client, \"new_folder\")\n"
" ```\n"
).
-spec mkd(ftp_client(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
mkd(Ftp_client, Path) ->
gleam@result:'try'(
perform(Ftp_client, {mkd, Path}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, path_created),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 558).
?DOC(
" Set the type of file to be transferred (FTP `TYPE` command).\n"
"\n"
" Use `file_type.Binary` (or `file_type.Image`) for binary transfers,\n"
" `file_type.Ascii(file_type.Default)` for text transfers.\n"
"\n"
" ```gleam\n"
" import gftp/file_type\n"
"\n"
" let assert Ok(_) = gftp.transfer_type(client, file_type.Binary)\n"
" ```\n"
).
-spec transfer_type(ftp_client(), gftp@file_type:file_type()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
transfer_type(Ftp_client, File_type) ->
gleam@result:'try'(
perform(Ftp_client, {type, File_type}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 577).
?DOC(
" Quit the current FTP session.\n"
"\n"
" Sends the `QUIT` command and waits for the server to confirm.\n"
" Call `shutdown` after this to close the underlying socket.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.quit(client)\n"
" let assert Ok(_) = gftp.shutdown(client)\n"
" ```\n"
).
-spec quit(ftp_client()) -> {ok, nil} | {error, gftp@result:ftp_error()}.
quit(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, quit),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, closing),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 598).
?DOC(
" Rename a file on the FTP server.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.rename(client, \"old_name.txt\", \"new_name.txt\")\n"
" ```\n"
).
-spec rename(ftp_client(), binary(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
rename(Ftp_client, From_name, To_name) ->
gleam@result:'try'(
perform(Ftp_client, {rename_from, From_name}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, request_file_pending),
fun(_) ->
gleam@result:'try'(
perform(Ftp_client, {rename_to, To_name}),
fun(_) ->
gleam@result:'try'(
read_response(
Ftp_client,
requested_file_action_ok
),
fun(_) -> {ok, nil} end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 616).
?DOC(
" Remove the remote directory at the specified path.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.rmd(client, \"old_folder\")\n"
" ```\n"
).
-spec rmd(ftp_client(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
rmd(Ftp_client, Path) ->
gleam@result:'try'(
perform(Ftp_client, {rmd, Path}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, requested_file_action_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 628).
?DOC(
" Delete the file at the specified path on the server.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.dele(client, \"unwanted_file.txt\")\n"
" ```\n"
).
-spec dele(ftp_client(), binary()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
dele(Ftp_client, Path) ->
gleam@result:'try'(
perform(Ftp_client, {dele, Path}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, requested_file_action_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 641).
?DOC(
" Tell the server to resume the transfer from a certain offset. The offset indicates the amount of bytes to skip\n"
" from the beginning of the file.\n"
" the REST command does not actually initiate the transfer.\n"
" After issuing a REST command, the client must send the appropriate FTP command to transfer the file\n"
"\n"
" It is possible to cancel the REST command, sending a REST command with offset 0\n"
).
-spec rest(ftp_client(), integer()) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
rest(Ftp_client, Offset) ->
gleam@result:'try'(
perform(Ftp_client, {rest, Offset}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, request_file_pending),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 654).
?DOC(
" Aborts a file transfer in progress.\n"
" This is used to cancel an ongoing file transfer operation,\n"
" such as a file upload or download, and close the data connection associated with that transfer.\n"
" \n"
" This function should only be used when a file transfer is in progress, by calling it\n"
" from within the reader or writer function passed to `retr`, `stor` or `appe`, otherwise it may put the client in an inconsistent state.\n"
).
-spec abor(ftp_client()) -> {ok, nil} | {error, gftp@result:ftp_error()}.
abor(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, abor),
fun(_) ->
gleam@result:'try'(
read_response_in(
Ftp_client,
[transfer_aborted, closing_data_connection]
),
fun(Response) -> case erlang:element(2, Response) of
transfer_aborted ->
_pipe = Ftp_client,
_pipe@1 = read_response(
_pipe,
closing_data_connection
),
gleam@result:replace(_pipe@1, nil);
_ ->
{ok, nil}
end end
)
end
).
-file("src/gftp.gleam", 783).
?DOC(
" Execute `NLST` command which returns the list of file names only.\n"
" If `pathname` is omitted then the list of files in the current directory will be\n"
" returned otherwise it will the list of files on `pathname`.\n"
"\n"
" ### Parse result\n"
"\n"
" 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.\n"
).
-spec nlst(ftp_client(), gleam@option:option(binary())) -> {ok, list(binary())} |
{error, gftp@result:ftp_error()}.
nlst(Ftp_client, Pathname) ->
stream_lines(Ftp_client, {nlst, Pathname}, about_to_send).
-file("src/gftp.gleam", 811).
?DOC(
" Execute `MLSD` command which returns the machine-processable listing of a directory.\n"
" If `pathname` is omitted then the list of files in the current directory will be\n"
" returned otherwise it will the list of files on `pathname`.\n"
"\n"
" ### Parse result\n"
"\n"
" The output of the `MLSD` command is a machine-readable format that provides detailed information about each file in the listing,\n"
" such as name, size, permissions, and modification date, in a standardized format defined by RFC 3659.\n"
"\n"
" You can parse the output of this command with `gftp/list` module, which provides a `File` type and the `parse_mlsd`\n"
" function to parse the output of the `MLSD` command into a list of `File` structs.\n"
"\n"
" ```gleam\n"
" import gftp\n"
" import gftp/list as gftp_list\n"
" import gleam/list\n"
"\n"
" use lines <- result.try(gftp.mlsd(ftp_client, None))\n"
" lines\n"
" |> list.try_map(gftp_list.parse_mlsd)\n"
" ```\n"
).
-spec mlsd(ftp_client(), gleam@option:option(binary())) -> {ok, list(binary())} |
{error, gftp@result:ftp_error()}.
mlsd(Ftp_client, Pathname) ->
stream_lines(Ftp_client, {mlsd, Pathname}, about_to_send).
-file("src/gftp.gleam", 836).
?DOC(
" Execute `MLST` command which returns the machine-processable information for a file\n"
" \n"
" ### Parse result\n"
" \n"
" The output of the `MLST` command is a machine-readable format that provides detailed information about a file,\n"
" such as name, size, permissions, and modification date, in a standardized format defined by RFC 3659.\n"
" \n"
" You can parse the output of this command with `gftp/list` module, which provides a `File` type and the `parse_mlst`\n"
" function to parse the output of the `MLST` command into a `File` struct.\n"
" \n"
" ```gleam\n"
" import gftp\n"
" import gftp/list as gftp_list\n"
" import gleam/list\n"
" \n"
" use line <- result.try(gftp.mlst(ftp_client, None))\n"
" use file <- result.try(gftp_list.parse_mlst(line))\n"
" ```\n"
).
-spec mlst(ftp_client(), gleam@option:option(binary())) -> {ok, binary()} |
{error, gftp@result:ftp_error()}.
mlst(Ftp_client, Pathname) ->
gleam@result:'try'(
perform(Ftp_client, {mlst, Pathname}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, requested_file_action_ok),
fun(Response) ->
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Body) -> _pipe = Body,
_pipe@1 = gleam@string:split(_pipe, <<"\r\n"/utf8>>),
_pipe@2 = gleam@list:drop(_pipe@1, 1),
_pipe@3 = gleam@list:first(_pipe@2),
gleam@result:map_error(
_pipe@3,
fun(_) -> bad_response end
) end
)
end
)
end
).
-file("src/gftp.gleam", 859).
?DOC(
" Retrieve the modification time of the file at `pathname`.\n"
"\n"
" ```gleam\n"
" let assert Ok(mtime) = gftp.mdtm(client, \"file.txt\")\n"
" ```\n"
).
-spec mdtm(ftp_client(), binary()) -> {ok, gleam@time@timestamp:timestamp()} |
{error, gftp@result:ftp_error()}.
mdtm(Ftp_client, Pathname) ->
Regex@1 = case gleam@regexp:from_string(
<<"\\b(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})\\b"/utf8>>
) of
{ok, Regex} -> Regex;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"gftp"/utf8>>,
function => <<"mdtm"/utf8>>,
line => 860,
value => _assert_fail,
start => 30260,
'end' => 30313,
pattern_start => 30271,
pattern_end => 30280})
end,
gleam@result:'try'(
perform(Ftp_client, {mdtm, Pathname}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, file),
fun(Response) ->
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Body) ->
case gftp@internal@utils:re_matches(Regex@1, Body) of
{ok,
[{some, Year},
{some, Month},
{some, Day},
{some, Hours},
{some, Minutes},
{some, Seconds}]} ->
gleam@result:'try'(
gftp@internal@utils:parse_int(Year),
fun(Year@1) ->
gleam@result:'try'(
gftp@internal@utils:parse_month(
Month
),
fun(Month@1) ->
gleam@result:'try'(
gftp@internal@utils:parse_int(
Day
),
fun(Day@1) ->
gleam@result:'try'(
gftp@internal@utils:parse_int(
Hours
),
fun(Hours@1) ->
gleam@result:'try'(
gftp@internal@utils:parse_int(
Minutes
),
fun(
Minutes@1
) ->
gleam@result:'try'(
gftp@internal@utils:parse_int(
Seconds
),
fun(
Seconds@1
) ->
{ok,
gleam@time@timestamp:from_calendar(
{date,
Year@1,
Month@1,
Day@1},
{time_of_day,
Hours@1,
Minutes@1,
Seconds@1,
0},
tempo@duration:new(
0,
second
)
)}
end
)
end
)
end
)
end
)
end
)
end
);
_ ->
{error, bad_response}
end
end
)
end
)
end
).
-file("src/gftp.gleam", 901).
?DOC(
" Retrieve the size in bytes of the file at `pathname`.\n"
"\n"
" ```gleam\n"
" let assert Ok(size) = gftp.size(client, \"file.txt\")\n"
" ```\n"
).
-spec size(ftp_client(), binary()) -> {ok, integer()} |
{error, gftp@result:ftp_error()}.
size(Ftp_client, Pathname) ->
Regex@1 = case gleam@regexp:from_string(<<"\\s+(\\d+)\\s*$"/utf8>>) of
{ok, Regex} -> Regex;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"gftp"/utf8>>,
function => <<"size"/utf8>>,
line => 902,
value => _assert_fail,
start => 31591,
'end' => 31644,
pattern_start => 31602,
pattern_end => 31611})
end,
gleam@result:'try'(
perform(Ftp_client, {size, Pathname}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, file),
fun(Response) ->
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Body) ->
case gftp@internal@utils:re_matches(Regex@1, Body) of
{ok, [{some, Size}]} ->
gftp@internal@utils:parse_int(Size);
_ ->
{error, bad_response}
end
end
)
end
)
end
).
-file("src/gftp.gleam", 923).
?DOC(
" Retrieve the features supported by the server (RFC 2389 FEAT command).\n"
"\n"
" Returns a dictionary of feature names to optional parameter strings.\n"
"\n"
" ```gleam\n"
" import gleam/dict\n"
"\n"
" let assert Ok(features) = gftp.feat(client)\n"
" let supports_mlst = dict.has_key(features, \"MLST\")\n"
" ```\n"
).
-spec feat(ftp_client()) -> {ok,
gleam@dict:dict(binary(), gleam@option:option(binary()))} |
{error, gftp@result:ftp_error()}.
feat(Ftp_client) ->
gleam@result:'try'(
perform(Ftp_client, feat),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, system),
fun(Response) ->
gleam@result:'try'(
gftp@internal@utils:response_to_string(Response),
fun(Body) ->
Lines = gleam@string:split(Body, <<"\n"/utf8>>),
case Lines of
[Single] ->
case gftp@internal@command@feat:is_last_line(
Single
) of
true ->
{ok, maps:new()};
false ->
{error, bad_response}
end;
[_ | Rest] ->
Feature_lines = begin
_pipe = Rest,
_pipe@1 = lists:reverse(_pipe),
_pipe@2 = gleam@list:drop(_pipe@1, 1),
lists:reverse(_pipe@2)
end,
gftp@internal@command@feat:parse_features(
Feature_lines
);
[] ->
{ok, maps:new()}
end
end
)
end
)
end
).
-file("src/gftp.gleam", 956).
?DOC(
" Set a server option (RFC 2389 OPTS command).\n"
"\n"
" ```gleam\n"
" import gleam/option.{Some}\n"
"\n"
" let assert Ok(_) = gftp.opts(client, \"UTF8\", Some(\"ON\"))\n"
" ```\n"
).
-spec opts(ftp_client(), binary(), gleam@option:option(binary())) -> {ok, nil} |
{error, gftp@result:ftp_error()}.
opts(Ftp_client, Option, Value) ->
gleam@result:'try'(
perform(Ftp_client, {opts, Option, Value}),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, command_ok),
fun(_) -> {ok, nil} end
)
end
).
-file("src/gftp.gleam", 972).
?DOC(
" Execute a SITE command on the server and return the response.\n"
"\n"
" ```gleam\n"
" let assert Ok(response) = gftp.site(client, \"CHMOD 755 file.txt\")\n"
" ```\n"
).
-spec site(ftp_client(), binary()) -> {ok, gftp@response:response()} |
{error, gftp@result:ftp_error()}.
site(Ftp_client, Sub_command) ->
gleam@result:'try'(
perform(Ftp_client, {site, Sub_command}),
fun(_) -> read_response_in(Ftp_client, [command_ok, help]) end
).
-file("src/gftp.gleam", 986).
?DOC(
" Execute a custom FTP command and return the response.\n"
"\n"
" Provide a list of expected status codes to validate the response against.\n"
"\n"
" ```gleam\n"
" import gftp/status\n"
"\n"
" let assert Ok(response) = gftp.custom_command(client, \"SITE HELP\", [status.Help])\n"
" ```\n"
).
-spec custom_command(ftp_client(), binary(), list(gftp@status:status())) -> {ok,
gftp@response:response()} |
{error, gftp@result:ftp_error()}.
custom_command(Ftp_client, Command_str, Expected_statuses) ->
gleam@result:'try'(
perform(Ftp_client, {custom, Command_str}),
fun(_) -> read_response_in(Ftp_client, Expected_statuses) end
).
-file("src/gftp.gleam", 1045).
?DOC(
" Read the server's welcome message (status 220 Ready) and return\n"
" an updated FtpClient with the welcome_message field populated.\n"
).
-spec read_welcome_message(ftp_client()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
read_welcome_message(Client) ->
gftp@stream:set_line_mode(erlang:element(2, Client)),
case read_response(Client, ready) of
{ok, Resp} ->
Welcome_msg = case gftp@response:to_string(Resp) of
{ok, Msg} ->
{some, Msg};
{error, _} ->
none
end,
{ok,
{ftp_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
Welcome_msg,
erlang:element(6, Client),
erlang:element(7, Client),
erlang:element(8, Client)}};
{error, Err} ->
{error, Err}
end.
-file("src/gftp.gleam", 256).
?DOC(
" Connect to the FTP server using an existing socket stream.\n"
" \n"
" This method doesn't authenticate with the server, so after a successful connection, you will need to call the `login`\n"
" method to authenticate before performing any FTP operations.\n"
" \n"
" On success, returns a `FtpClient` instance that can be used to interact with the FTP server.\n"
" On failure, returns an `FtpError` describing the issue.\n"
).
-spec connect_with_stream(mug:socket()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
connect_with_stream(Stream) ->
Client = {ftp_client,
{tcp, Stream},
passive,
false,
none,
fun default_passive_stream_builder/2,
none,
30000},
read_welcome_message(Client).
-file("src/gftp.gleam", 233).
?DOC(
" Try to connect to the remote server with a custom timeout.\n"
"\n"
" The `timeout` parameter specifies the maximum time to wait for a connection to be established, in milliseconds.\n"
" You must call `login` before performing any FTP operations.\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect_timeout(\"ftp.example.com\", 21, timeout: 10_000)\n"
" ```\n"
).
-spec connect_timeout(binary(), integer(), integer()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
connect_timeout(Host, Port, Timeout) ->
_pipe = {connection_options, Host, Port, Timeout, ipv6_preferred},
_pipe@1 = mug:connect(_pipe),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {connection_error, Field@0} end
),
gleam@result:'try'(_pipe@2, fun(Socket) -> connect_with_stream(Socket) end).
-file("src/gftp.gleam", 220).
?DOC(
" Try to connect to the remote server with the default timeout of 30 seconds.\n"
"\n"
" Connects to the FTP server at the specified `host` and `port`, reads the server's welcome message,\n"
" and returns an `FtpClient` instance. You must call `login` before performing any FTP operations.\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect(\"ftp.example.com\", 21)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
" ```\n"
).
-spec connect(binary(), integer()) -> {ok, ftp_client()} |
{error, gftp@result:ftp_error()}.
connect(Host, Port) ->
connect_timeout(Host, Port, 30000).
-file("src/gftp.gleam", 382).
?DOC(
" Connect to a remote FTPS server using an implicit secure connection (typically port 990).\n"
"\n"
" Warning: implicit FTPS is considered deprecated. Prefer explicit mode with `into_secure` when possible.\n"
"\n"
" After the TLS handshake and welcome message, this function automatically sends\n"
" `PBSZ 0` and `PROT P` to enable data channel protection per RFC 4217.\n"
"\n"
" ```gleam\n"
" let assert Ok(client) = gftp.connect_secure_implicit(\"ftp.example.com\", 990, ssl_options, 30_000)\n"
" let assert Ok(_) = gftp.login(client, \"user\", \"password\")\n"
" ```\n"
).
-spec connect_secure_implicit(
binary(),
integer(),
kafein:wrap_options(),
integer()
) -> {ok, ftp_client()} | {error, gftp@result:ftp_error()}.
connect_secure_implicit(Host, Port, Ssl_options, Timeout) ->
gleam@result:'try'(
begin
_pipe = {connection_options, Host, Port, Timeout, ipv6_preferred},
_pipe@1 = mug:connect(_pipe),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {connection_error, Field@0} end
)
end,
fun(Socket) ->
gleam@result:'try'(
begin
_pipe@2 = kafein:wrap(Ssl_options, Socket),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {tls, Field@0} end
)
end,
fun(Ssl_socket) ->
Client = {ftp_client,
{ssl, Ssl_socket, Socket},
passive,
false,
none,
fun default_passive_stream_builder/2,
{some, Ssl_options},
30000},
gleam@result:'try'(
read_welcome_message(Client),
fun(Client@1) ->
gleam@result:'try'(
perform(Client@1, {pbsz, 0}),
fun(_) ->
gleam@result:'try'(
read_response(Client@1, command_ok),
fun(_) ->
gleam@result:'try'(
perform(
Client@1,
{prot, private}
),
fun(_) ->
gleam@result:'try'(
read_response(
Client@1,
command_ok
),
fun(_) ->
{ok, Client@1}
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 1296).
?DOC(
" Execute a data command in passive mode, which requires the client to connect to the server at the specified address\n"
" and port for the data transfer.\n"
).
-spec data_command_passive(ftp_client(), gftp@internal@command:command()) -> {ok,
gftp@stream:data_stream()} |
{error, gftp@result:ftp_error()}.
data_command_passive(Ftp_client, Command) ->
gleam@result:'try'(
perform(Ftp_client, pasv),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, passive_mode),
fun(Response) ->
gleam@result:'try'(
parse_passive_address_from_response(Response),
fun(_use0) ->
{Address, Port} = _use0,
gleam@result:'try'(
perform(Ftp_client, Command),
fun(_) ->
gleam@result:'try'(
case erlang:element(4, Ftp_client) of
false ->
{ok, {Address, Port}};
true ->
_pipe = erlang:element(
2,
Ftp_client
),
_pipe@1 = gftp@stream:peer_address(
_pipe
),
_pipe@2 = gleam@result:map(
_pipe@1,
fun(Peer_addr) ->
{erlang:element(
1,
Peer_addr
),
Port}
end
),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {socket, Field@0} end
)
end,
fun(_use0@1) ->
{Address@1, Port@1} = _use0@1,
build_data_channel_stream(
Ftp_client,
Address@1,
Port@1
)
end
)
end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 1322).
?DOC(
" Execute a data command in extended passive mode, which requires the client to connect to the server at the specified address\n"
" and port for the data transfer.\n"
).
-spec data_command_extended_passive(
ftp_client(),
gftp@internal@command:command()
) -> {ok, gftp@stream:data_stream()} | {error, gftp@result:ftp_error()}.
data_command_extended_passive(Ftp_client, Command) ->
gleam@result:'try'(
perform(Ftp_client, epsv),
fun(_) ->
gleam@result:'try'(
read_response(Ftp_client, extended_passive_mode),
fun(Response) ->
gleam@result:'try'(
parse_epsv_address_from_response(Response),
fun(Port) ->
gleam@result:'try'(
perform(Ftp_client, Command),
fun(_) ->
gleam@result:'try'(
begin
_pipe = erlang:element(
2,
Ftp_client
),
_pipe@1 = gftp@stream:peer_address(
_pipe
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {socket, Field@0} end
)
end,
fun(_use0) ->
{Address, _} = _use0,
build_data_channel_stream(
Ftp_client,
Address,
Port
)
end
)
end
)
end
)
end
)
end
).
-file("src/gftp.gleam", 1409).
?DOC(
" Finalize a data command by closing the data stream and reading the server's final response.\n"
"\n"
" **WARNING:** This function is intended for internal use by `gftp/actor` and\n"
" `gftp/internal/data_channel`. It may change without notice in future versions.\n"
).
-spec finalize_data_command(ftp_client(), gftp@stream:data_stream()) -> {ok,
nil} |
{error, gftp@result:ftp_error()}.
finalize_data_command(Ftp_client, Data_stream) ->
_ = gftp@stream:shutdown(Data_stream),
_pipe = read_response_in(
Ftp_client,
[closing_data_connection, requested_file_action_ok]
),
gleam@result:replace(_pipe, nil).
-file("src/gftp.gleam", 686).
?DOC(
" Download a file from the FTP server.\n"
"\n"
" The `reader` callback receives the data stream and should read data from it.\n"
" The data stream is automatically closed after the reader returns.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.retr(client, \"file.txt\", fn(data_stream) {\n"
" let assert Ok(data) = stream.receive(data_stream, 5000)\n"
" // process data...\n"
" Ok(Nil)\n"
" })\n"
" ```\n"
).
-spec retr(
ftp_client(),
binary(),
fun((gftp@stream:data_stream()) -> {ok, nil} |
{error, gftp@result:ftp_error()})
) -> {ok, nil} | {error, gftp@result:ftp_error()}.
retr(Ftp_client, Path, Reader) ->
gleam@result:'try'(
data_command(Ftp_client, {retr, Path}),
fun(Data_stream) ->
gleam@result:'try'(
read_response_in(Ftp_client, [about_to_send, already_open]),
fun(_) ->
gleam@result:'try'(
Reader(Data_stream),
fun(_) ->
finalize_data_command(Ftp_client, Data_stream)
end
)
end
)
end
).
-file("src/gftp.gleam", 712).
?DOC(
" Upload a file to the FTP server.\n"
"\n"
" The `writer` callback receives the data stream and should write the file data to it.\n"
" The data stream is automatically closed after the writer returns.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.stor(client, \"upload.txt\", fn(data_stream) {\n"
" stream.send(data_stream, bit_array.from_string(\"file contents\"))\n"
" |> result.map_error(ftp_result.Socket)\n"
" })\n"
" ```\n"
).
-spec stor(
ftp_client(),
binary(),
fun((gftp@stream:data_stream()) -> {ok, nil} |
{error, gftp@result:ftp_error()})
) -> {ok, nil} | {error, gftp@result:ftp_error()}.
stor(Ftp_client, Path, Writer) ->
gleam@result:'try'(
data_command(Ftp_client, {stor, Path}),
fun(Data_stream) ->
gleam@result:'try'(
read_response_in(Ftp_client, [about_to_send, already_open]),
fun(_) ->
gleam@result:'try'(
Writer(Data_stream),
fun(_) ->
finalize_data_command(Ftp_client, Data_stream)
end
)
end
)
end
).
-file("src/gftp.gleam", 735).
?DOC(
" Append data to a file on the FTP server. If the file doesn't exist, it will be created.\n"
"\n"
" ```gleam\n"
" let assert Ok(_) = gftp.appe(client, \"log.txt\", fn(data_stream) {\n"
" stream.send(data_stream, bit_array.from_string(\"new log entry\\n\"))\n"
" |> result.map_error(ftp_result.Socket)\n"
" })\n"
" ```\n"
).
-spec appe(
ftp_client(),
binary(),
fun((gftp@stream:data_stream()) -> {ok, nil} |
{error, gftp@result:ftp_error()})
) -> {ok, nil} | {error, gftp@result:ftp_error()}.
appe(Ftp_client, Path, Writer) ->
gleam@result:'try'(
data_command(Ftp_client, {appe, Path}),
fun(Data_stream) ->
gleam@result:'try'(
read_response_in(Ftp_client, [about_to_send, already_open]),
fun(_) ->
gleam@result:'try'(
Writer(Data_stream),
fun(_) ->
finalize_data_command(Ftp_client, Data_stream)
end
)
end
)
end
).
-file("src/gftp.gleam", 1013).
?DOC(
" Execute a custom FTP command that uses a data connection.\n"
"\n"
" The `on_data_stream` callback receives the data stream and the server response.\n"
" Use `read_lines_from_stream` to easily parse line-based output.\n"
"\n"
" ```gleam\n"
" import gftp/status\n"
"\n"
" let assert Ok(_) = gftp.custom_data_command(\n"
" client,\n"
" \"LIST -la\",\n"
" [status.AboutToSend, status.AlreadyOpen],\n"
" fn(data_stream, _response) {\n"
" let assert Ok(lines) = gftp.read_lines_from_stream(data_stream, 5000)\n"
" Ok(Nil)\n"
" },\n"
" )\n"
" ```\n"
).
-spec custom_data_command(
ftp_client(),
binary(),
list(gftp@status:status()),
fun((gftp@stream:data_stream(), gftp@response:response()) -> {ok, nil} |
{error, gftp@result:ftp_error()})
) -> {ok, nil} | {error, gftp@result:ftp_error()}.
custom_data_command(Ftp_client, Command_str, Expected_statuses, On_data_stream) ->
gleam@result:'try'(
data_command(Ftp_client, {custom, Command_str}),
fun(Data_stream) ->
gleam@result:'try'(
read_response_in(Ftp_client, Expected_statuses),
fun(Response) ->
gleam@result:'try'(
On_data_stream(Data_stream, Response),
fun(_) ->
finalize_data_command(Ftp_client, Data_stream)
end
)
end
)
end
).
-file("src/gftp.gleam", 1448).
?DOC(" Recursive loop for `read_lines_from_stream`\n").
-spec read_lines_from_stream_loop(
list(binary()),
gftp@stream:data_stream(),
integer()
) -> {ok, list(binary())} | {error, gftp@result:ftp_error()}.
read_lines_from_stream_loop(Acc, Data_stream, Timeout) ->
case read_line_with(Data_stream, Timeout) of
{ok, Line} ->
read_lines_from_stream_loop([Line | Acc], Data_stream, Timeout);
{error, {socket, closed}} ->
_pipe = Acc,
_pipe@1 = lists:reverse(_pipe),
{ok, _pipe@1};
{error, E} ->
{error, E}
end.
-file("src/gftp.gleam", 1031).
?DOC(" Read lines from a data stream until the stream is closed by the server or an error occurs.\n").
-spec read_lines_from_stream(gftp@stream:data_stream(), integer()) -> {ok,
list(binary())} |
{error, gftp@result:ftp_error()}.
read_lines_from_stream(Data_stream, Timeout) ->
gftp@stream:set_line_mode(Data_stream),
read_lines_from_stream_loop([], Data_stream, Timeout).