-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 => <>, 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) -> <> 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, <<<<<<<>/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 => <>, 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 => <>, 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 => <>, 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 => <>, 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).