aboutsummaryrefslogtreecommitdiffstats
path: root/lib/inets/src/ftp/ftp.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/inets/src/ftp/ftp.erl')
-rw-r--r--lib/inets/src/ftp/ftp.erl2009
1 files changed, 2009 insertions, 0 deletions
diff --git a/lib/inets/src/ftp/ftp.erl b/lib/inets/src/ftp/ftp.erl
new file mode 100644
index 0000000000..534fcae675
--- /dev/null
+++ b/lib/inets/src/ftp/ftp.erl
@@ -0,0 +1,2009 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1997-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+%% Description: This module implements an ftp client, RFC 959.
+%% It also supports ipv6 RFC 2428.
+
+-module(ftp).
+
+-behaviour(gen_server).
+-behaviour(inets_service).
+
+-deprecated({open, 3, next_major_release}).
+-deprecated({force_active, 1, next_major_release}).
+
+%% API - Client interface
+-export([cd/2, close/1, delete/2, formaterror/1,
+ lcd/2, lpwd/1, ls/1, ls/2,
+ mkdir/2, nlist/1, nlist/2,
+ open/1, open/2, open/3, force_active/1,
+ pwd/1, quote/2,
+ recv/2, recv/3, recv_bin/2,
+ recv_chunk_start/2, recv_chunk/1,
+ rename/3, rmdir/2,
+ send/2, send/3, send_bin/3,
+ send_chunk_start/2, send_chunk/2, send_chunk_end/1,
+ type/2, user/3, user/4, account/2,
+ append/3, append/2, append_bin/3,
+ append_chunk/2, append_chunk_end/1, append_chunk_start/2, info/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2,
+ handle_info/2, terminate/2, code_change/3]).
+
+%% supervisor callbacks
+-export([start_link/1, start_link/2]).
+
+%% Behavior callbacks
+-export([start_standalone/1, start_service/1,
+ stop_service/1, services/0, service_info/1]).
+
+-include("ftp_internal.hrl").
+
+%% Constante used in internal state definition
+-define(CONNECTION_TIMEOUT, 60*1000).
+-define(DEFAULT_MODE, passive).
+-define(PROGRESS_DEFAULT, ignore).
+
+%% Internal Constants
+-define(FTP_PORT, 21).
+-define(FILE_BUFSIZE, 4096).
+
+%% Internal state
+-record(state, {
+ csock = undefined, % socket() - Control connection socket
+ dsock = undefined, % socket() - Data connection socket
+ verbose = false, % boolean()
+ ldir = undefined, % string() - Current local directory
+ type = ftp_server_default, % atom() - binary | ascii
+ chunk = false, % boolean() - Receiving data chunks
+ mode = ?DEFAULT_MODE, % passive | active
+ timeout = ?CONNECTION_TIMEOUT, % integer()
+ %% Data received so far on the data connection
+ data = <<>>, % binary()
+ %% Data received so far on the control connection
+ %% {BinStream, AccLines}. If a binary sequence
+ %% ends with ?CR then keep it in the binary to
+ %% be able to detect if the next received byte is ?LF
+ %% and hence the end of the response is reached!
+ ctrl_data = {<<>>, [], start}, % {binary(), [bytes()], LineStatus}
+ %% pid() - Client pid (note not the same as "From")
+ owner = undefined,
+ client = undefined, % "From" to be used in gen_server:reply/2
+ %% Function that activated a connection and maybe some
+ %% data needed further on.
+ caller = undefined, % term()
+ ipfamily, % inet | inet6 | inet6fb4
+ progress = ignore % ignore | pid()
+ }).
+
+
+%%%=========================================================================
+%%% API - CLIENT FUNCTIONS
+%%%=========================================================================
+
+%%--------------------------------------------------------------------------
+%% open(HostOrOtpList, <Port>, <Flags>) -> {ok, Pid} | {error, ehost}
+%% HostOrOtpList = string() | [{option_list, Options}]
+%% Port = integer(),
+%% Flags = [Flag],
+%% Flag = verbose | debug | trace
+%%
+%% Description: Start an ftp client and connect to a host.
+%%--------------------------------------------------------------------------
+
+%% <BACKWARD-COMPATIBILLITY>
+open({option_list, Options}) when is_list(Options) ->
+ try
+ {ok, StartOptions} = start_options(Options),
+ {ok, OpenOptions} = open_options(Options),
+ case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of
+ {ok, Pid} ->
+ call(Pid, {open, ip_comm, OpenOptions}, plain);
+ Error1 ->
+ Error1
+ end
+ catch
+ throw:Error2 ->
+ Error2
+ end;
+%% </BACKWARD-COMPATIBILLITY>
+
+open(Host) ->
+ open(Host, []).
+
+%% <BACKWARD-COMPATIBILLITY>
+open(Host, Port) when is_integer(Port) ->
+ open(Host, [{port, Port}]);
+%% </BACKWARD-COMPATIBILLITY>
+
+%% <BACKWARD-COMPATIBILLITY>
+open(Host, [H|_] = Flags) when is_atom(H) ->
+ open(Host, ?FTP_PORT, Flags);
+%% </BACKWARD-COMPATIBILLITY>
+
+open(Host, Opts) when is_list(Opts) ->
+ ?fcrt("open", [{host, Host}, {opts, Opts}]),
+ try
+ {ok, StartOptions} = start_options(Opts),
+ ?fcrt("open", [{start_options, StartOptions}]),
+ {ok, OpenOptions} = open_options([{host, Host}|Opts]),
+ ?fcrt("open", [{open_options, OpenOptions}]),
+ case start_link(StartOptions, []) of
+ {ok, Pid} ->
+ ?fcrt("open - ok", [{pid, Pid}]),
+ call(Pid, {open, ip_comm, OpenOptions}, plain);
+ Error1 ->
+ ?fcrt("open - error", [{error1, Error1}]),
+ Error1
+ end
+ catch
+ throw:Error2 ->
+ ?fcrt("open - error", [{error2, Error2}]),
+ Error2
+ end.
+
+
+%% <BACKWARD-COMPATIBILLITY>
+open(Host, Port, Flags) when is_integer(Port) andalso is_list(Flags) ->
+ ?fcrt("open", [{host, Host}, {port, Port}, {flags, Flags}]),
+ try
+ {ok, StartOptions} = start_options([{flags, Flags}]),
+ ?fcrt("open", [{start_options, StartOptions}]),
+ {ok, OpenOptions} = open_options([{host, Host}, {port, Port}|Flags]),
+ ?fcrt("open", [{open_options, OpenOptions}]),
+ case ftp_sup:start_child([[{client, self()} | StartOptions], []]) of
+ {ok, Pid} ->
+ ?fcrt("open - ok", [{pid, Pid}]),
+ call(Pid, {open, ip_comm, OpenOptions}, plain);
+ Error1 ->
+ ?fcrt("open - error", [{error1, Error1}]),
+ Error1
+ end
+ catch
+ throw:Error2 ->
+ Error2
+ end.
+%% </BACKWARD-COMPATIBILLITY>
+
+
+
+
+
+%%--------------------------------------------------------------------------
+%% user(Pid, User, Pass, <Acc>) -> ok | {error, euser} | {error, econn}
+%% | {error, eacct}
+%% Pid = pid(),
+%% User = Pass = Acc = string()
+%%
+%% Description: Login with or without a supplied account name.
+%%--------------------------------------------------------------------------
+user(Pid, User, Pass) ->
+ call(Pid, {user, User, Pass}, atom).
+
+user(Pid, User, Pass, Acc) ->
+ call(Pid, {user, User, Pass, Acc}, atom).
+
+%%--------------------------------------------------------------------------
+%% account(Pid, Acc) -> ok | {error, eacct}
+%% Pid = pid()
+%% Acc= string()
+%%
+%% Description: Set a user Account.
+%%--------------------------------------------------------------------------
+account(Pid, Acc) ->
+ call(Pid, {account, Acc}, atom).
+
+%%--------------------------------------------------------------------------
+%% pwd(Pid) -> {ok, Dir} | {error, elogin} | {error, econn}
+%% Pid = pid()
+%% Dir = string()
+%%
+%% Description: Get the current working directory at remote server.
+%%--------------------------------------------------------------------------
+pwd(Pid) ->
+ call(Pid, pwd, ctrl).
+
+%%--------------------------------------------------------------------------
+%% lpwd(Pid) -> {ok, Dir} | {error, elogin}
+%% Pid = pid()
+%% Dir = string()
+%%
+%% Description: Get the current working directory at local server.
+%%--------------------------------------------------------------------------
+lpwd(Pid) ->
+ call(Pid, lpwd, string).
+
+%%--------------------------------------------------------------------------
+%% cd(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn}
+%% Pid = pid()
+%% Dir = string()
+%%
+%% Description: Change current working directory at remote server.
+%%--------------------------------------------------------------------------
+cd(Pid, Dir) ->
+ call(Pid, {cd, Dir}, atom).
+
+%%--------------------------------------------------------------------------
+%% lcd(Pid, Dir) -> ok | {error, epath}
+%% Pid = pid()
+%% Dir = string()
+%%
+%% Description: Change current working directory for the local client.
+%%--------------------------------------------------------------------------
+lcd(Pid, Dir) ->
+ call(Pid, {lcd, Dir}, string).
+
+%%--------------------------------------------------------------------------
+%% ls(Pid) -> Result
+%% ls(Pid, <Dir>) -> Result
+%%
+%% Pid = pid()
+%% Dir = string()
+%% Result = {ok, Listing} | {error, Reason}
+%% Listing = string()
+%% Reason = epath | elogin | econn
+%%
+%% Description: Returns a list of files in long format.
+%%--------------------------------------------------------------------------
+ls(Pid) ->
+ ls(Pid, "").
+ls(Pid, Dir) ->
+ call(Pid, {dir, long, Dir}, string).
+
+%%--------------------------------------------------------------------------
+%% nlist(Pid) -> Result
+%% nlist(Pid, Pathname) -> Result
+%%
+%% Pid = pid()
+%% Pathname = string()
+%% Result = {ok, Listing} | {error, Reason}
+%% Listing = string()
+%% Reason = epath | elogin | econn
+%%
+%% Description: Returns a list of files in short format
+%%--------------------------------------------------------------------------
+nlist(Pid) ->
+ nlist(Pid, "").
+nlist(Pid, Dir) ->
+ call(Pid, {dir, short, Dir}, string).
+
+%%--------------------------------------------------------------------------
+%% rename(Pid, CurrFile, NewFile) -> ok | {error, epath} | {error, elogin}
+%% | {error, econn}
+%% Pid = pid()
+%% CurrFile = NewFile = string()
+%%
+%% Description: Rename a file at remote server.
+%%--------------------------------------------------------------------------
+rename(Pid, CurrFile, NewFile) ->
+ call(Pid, {rename, CurrFile, NewFile}, string).
+
+%%--------------------------------------------------------------------------
+%% delete(Pid, File) -> ok | {error, epath} | {error, elogin} |
+%% {error, econn}
+%% Pid = pid()
+%% File = string()
+%%
+%% Description: Remove file at remote server.
+%%--------------------------------------------------------------------------
+delete(Pid, File) ->
+ call(Pid, {delete, File}, string).
+
+%%--------------------------------------------------------------------------
+%% mkdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn}
+%% Pid = pid(),
+%% Dir = string()
+%%
+%% Description: Make directory at remote server.
+%%--------------------------------------------------------------------------
+mkdir(Pid, Dir) ->
+ call(Pid, {mkdir, Dir}, atom).
+
+%%--------------------------------------------------------------------------
+%% rmdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn}
+%% Pid = pid(),
+%% Dir = string()
+%%
+%% Description: Remove directory at remote server.
+%%--------------------------------------------------------------------------
+rmdir(Pid, Dir) ->
+ call(Pid, {rmdir, Dir}, atom).
+
+%%--------------------------------------------------------------------------
+%% type(Pid, Type) -> ok | {error, etype} | {error, elogin} | {error, econn}
+%% Pid = pid()
+%% Type = ascii | binary
+%%
+%% Description: Set transfer type.
+%%--------------------------------------------------------------------------
+type(Pid, Type) ->
+ call(Pid, {type, Type}, atom).
+
+%%--------------------------------------------------------------------------
+%% recv(Pid, RemoteFileName <LocalFileName>) -> ok | {error, epath} |
+%% {error, elogin} | {error, econn}
+%% Pid = pid()
+%% RemoteFileName = LocalFileName = string()
+%%
+%% Description: Transfer file from remote server.
+%%--------------------------------------------------------------------------
+recv(Pid, RemotFileName) ->
+ recv(Pid, RemotFileName, RemotFileName).
+
+recv(Pid, RemotFileName, LocalFileName) ->
+ call(Pid, {recv, RemotFileName, LocalFileName}, atom).
+
+%%--------------------------------------------------------------------------
+%% recv_bin(Pid, RemoteFile) -> {ok, Bin} | {error, epath} | {error, elogin}
+%% | {error, econn}
+%% Pid = pid()
+%% RemoteFile = string()
+%% Bin = binary()
+%%
+%% Description: Transfer file from remote server into binary.
+%%--------------------------------------------------------------------------
+recv_bin(Pid, RemoteFile) ->
+ call(Pid, {recv_bin, RemoteFile}, bin).
+
+%%--------------------------------------------------------------------------
+%% recv_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath}
+%% | {error, econn}
+%% Pid = pid()
+%% RemoteFile = string()
+%%
+%% Description: Start receive of chunks of remote file.
+%%--------------------------------------------------------------------------
+recv_chunk_start(Pid, RemoteFile) ->
+ call(Pid, {recv_chunk_start, RemoteFile}, atom).
+
+%%--------------------------------------------------------------------------
+%% recv_chunk(Pid, RemoteFile) -> ok | {ok, Bin} | {error, Reason}
+%% Pid = pid()
+%% RemoteFile = string()
+%%
+%% Description: Transfer file from remote server into binary in chunks
+%%--------------------------------------------------------------------------
+recv_chunk(Pid) ->
+ call(Pid, recv_chunk, atom).
+
+%%--------------------------------------------------------------------------
+%% send(Pid, LocalFileName <RemotFileName>) -> ok | {error, epath}
+%% | {error, elogin}
+%% | {error, econn}
+%% Pid = pid()
+%% LocalFileName = RemotFileName = string()
+%%
+%% Description: Transfer file to remote server.
+%%--------------------------------------------------------------------------
+send(Pid, LocalFileName) ->
+ send(Pid, LocalFileName, LocalFileName).
+
+send(Pid, LocalFileName, RemotFileName) ->
+ call(Pid, {send, LocalFileName, RemotFileName}, atom).
+
+%%--------------------------------------------------------------------------
+%% send_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin}
+%% | {error, enotbinary} | {error, econn}
+%% Pid = pid()
+%% Bin = binary()
+%% RemoteFile = string()
+%%
+%% Description: Transfer a binary to a remote file.
+%%--------------------------------------------------------------------------
+send_bin(Pid, Bin, RemoteFile) when is_binary(Bin) ->
+ call(Pid, {send_bin, Bin, RemoteFile}, atom);
+send_bin(_Pid, _Bin, _RemoteFile) ->
+ {error, enotbinary}.
+
+%%--------------------------------------------------------------------------
+%% send_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath}
+%% | {error, econn}
+%% Pid = pid()
+%% RemoteFile = string()
+%%
+%% Description: Start transfer of chunks to remote file.
+%%--------------------------------------------------------------------------
+send_chunk_start(Pid, RemoteFile) ->
+ call(Pid, {send_chunk_start, RemoteFile}, atom).
+
+%%--------------------------------------------------------------------------
+%% append_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} |
+%% {error, epath} | {error, econn}
+%% Pid = pid()
+%% RemoteFile = string()
+%%
+%% Description: Start append chunks of data to remote file.
+%%--------------------------------------------------------------------------
+append_chunk_start(Pid, RemoteFile) ->
+ call(Pid, {append_chunk_start, RemoteFile}, atom).
+
+%%--------------------------------------------------------------------------
+%% send_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary}
+%% | {error, echunk} | {error, econn}
+%% Pid = pid()
+%% Bin = binary().
+%%
+%% Purpose: Send chunk to remote file.
+%%--------------------------------------------------------------------------
+send_chunk(Pid, Bin) when is_binary(Bin) ->
+ call(Pid, {transfer_chunk, Bin}, atom);
+send_chunk(_Pid, _Bin) ->
+ {error, enotbinary}.
+
+%%--------------------------------------------------------------------------
+%% append_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary}
+%% | {error, echunk} | {error, econn}
+%% Pid = pid()
+%% Bin = binary()
+%%
+%% Description: Append chunk to remote file.
+%%--------------------------------------------------------------------------
+append_chunk(Pid, Bin) when is_binary(Bin) ->
+ call(Pid, {transfer_chunk, Bin}, atom);
+append_chunk(_Pid, _Bin) ->
+ {error, enotbinary}.
+
+%%--------------------------------------------------------------------------
+%% send_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk}
+%% | {error, econn}
+%% Pid = pid()
+%%
+%% Description: End sending of chunks to remote file.
+%%--------------------------------------------------------------------------
+send_chunk_end(Pid) ->
+ call(Pid, chunk_end, atom).
+
+%%--------------------------------------------------------------------------
+%% append_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk}
+%% | {error, econn}
+%% Pid = pid()
+%%
+%% Description: End appending of chunks to remote file.
+%%--------------------------------------------------------------------------
+append_chunk_end(Pid) ->
+ call(Pid, chunk_end, atom).
+
+%%--------------------------------------------------------------------------
+%% append(Pid, LocalFileName, RemotFileName) -> ok | {error, epath}
+%% | {error, elogin} | {error, econn}
+%% Pid = pid()
+%% LocalFileName = RemotFileName = string()
+%%
+%% Description: Append the local file to the remote file
+%%--------------------------------------------------------------------------
+append(Pid, LocalFileName) ->
+ append(Pid, LocalFileName, LocalFileName).
+
+append(Pid, LocalFileName, RemotFileName) ->
+ call(Pid, {append, LocalFileName, RemotFileName}, atom).
+
+%%--------------------------------------------------------------------------
+%% append_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin}
+%% | {error, enotbinary} | {error, econn}
+%% Pid = pid()
+%% Bin = binary()
+%% RemoteFile = string()
+%%
+%% Purpose: Append a binary to a remote file.
+%%--------------------------------------------------------------------------
+append_bin(Pid, Bin, RemoteFile) when is_binary(Bin) ->
+ call(Pid, {append_bin, Bin, RemoteFile}, atom);
+append_bin(_Pid, _Bin, _RemoteFile) ->
+ {error, enotbinary}.
+
+%%--------------------------------------------------------------------------
+%% quote(Pid, Cmd) -> ok
+%% Pid = pid()
+%% Cmd = string()
+%%
+%% Description: Send arbitrary ftp command.
+%%--------------------------------------------------------------------------
+quote(Pid, Cmd) when is_list(Cmd) ->
+ call(Pid, {quote, Cmd}, atom).
+
+%%--------------------------------------------------------------------------
+%% close(Pid) -> ok
+%% Pid = pid()
+%%
+%% Description: End the ftp session.
+%%--------------------------------------------------------------------------
+close(Pid) ->
+ cast(Pid, close),
+ ok.
+
+%%--------------------------------------------------------------------------
+%% force_active(Pid) -> ok
+%% Pid = pid()
+%%
+%% Description: Force connection to use active mode.
+%%--------------------------------------------------------------------------
+force_active(Pid) ->
+ error_logger:info_report("This function is deprecated use the mode flag "
+ "instead"),
+ call(Pid, force_active, atom).
+
+%%--------------------------------------------------------------------------
+%% formaterror(Tag) -> string()
+%% Tag = atom() | {error, atom()}
+%%
+%% Description: Return diagnostics.
+%%--------------------------------------------------------------------------
+formaterror(Tag) ->
+ ftp_response:error_string(Tag).
+
+info(Pid) ->
+ call(Pid, info, list).
+
+
+%%%========================================================================
+%%% Behavior callbacks
+%%%========================================================================
+start_standalone(Options) ->
+ try
+ {ok, StartOptions} = start_options(Options),
+ {ok, OpenOptions} = open_options(Options),
+ case start_link(StartOptions, []) of
+ {ok, Pid} ->
+ call(Pid, {open, ip_comm, OpenOptions}, plain);
+ Error1 ->
+ Error1
+ end
+ catch
+ throw:Error2 ->
+ Error2
+ end.
+
+start_service(Options) ->
+ try
+ {ok, StartOptions} = start_options(Options),
+ {ok, OpenOptions} = open_options(Options),
+ case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of
+ {ok, Pid} ->
+ call(Pid, {open, ip_comm, OpenOptions}, plain);
+ Error1 ->
+ Error1
+ end
+ catch
+ throw:Error2 ->
+ Error2
+ end.
+
+stop_service(Pid) ->
+ close(Pid).
+
+services() ->
+ [{ftpc, Pid} || {_, Pid, _, _} <-
+ supervisor:which_children(ftp_sup)].
+service_info(Pid) ->
+ {ok, Info} = call(Pid, info, list),
+ {ok, [proplists:lookup(mode, Info),
+ proplists:lookup(local_port, Info),
+ proplists:lookup(peer, Info),
+ proplists:lookup(peer_port, Info)]}.
+
+
+%% This function extracts the start options from the
+%% Valid options:
+%% debug,
+%% verbose
+%% ipfamily
+%% priority
+%% flags (for backward compatibillity)
+start_options(Options) ->
+ ?fcrt("start_options", [{options, Options}]),
+ case lists:keysearch(flags, 1, Options) of
+ {value, {flags, Flags}} ->
+ Verbose = lists:member(verbose, Flags),
+ IsTrace = lists:member(trace, Flags),
+ IsDebug = lists:member(debug, Flags),
+ DebugLevel =
+ if
+ (IsTrace =:= true) ->
+ trace;
+ IsDebug =:= true ->
+ debug;
+ true ->
+ disable
+ end,
+ {ok, [{verbose, Verbose},
+ {debug, DebugLevel},
+ {priority, low}]};
+ false ->
+ ValidateVerbose =
+ fun(true) -> true;
+ (false) -> true;
+ (_) -> false
+ end,
+ ValidateDebug =
+ fun(trace) -> true;
+ (debug) -> true;
+ (disable) -> true;
+ (_) -> false
+ end,
+ ValidatePriority =
+ fun(low) -> true;
+ (normal) -> true;
+ (high) -> true;
+ (_) -> false
+ end,
+ ValidOptions =
+ [{verbose, ValidateVerbose, false, false},
+ {debug, ValidateDebug, false, disable},
+ {priority, ValidatePriority, false, low}],
+ validate_options(Options, ValidOptions, [])
+ end.
+
+
+%% This function extracts and validates the open options from the
+%% Valid options:
+%% mode
+%% host
+%% port
+%% timeout
+%% progress
+open_options(Options) ->
+ ?fcrt("open_options", [{options, Options}]),
+ ValidateMode =
+ fun(active) -> true;
+ (passive) -> true;
+ (_) -> false
+ end,
+ ValidateHost =
+ fun(Host) when is_list(Host) ->
+ true;
+ (Host) when is_tuple(Host) andalso
+ ((size(Host) =:= 4) orelse (size(Host) =:= 8)) ->
+ true;
+ (_) ->
+ false
+ end,
+ ValidatePort =
+ fun(Port) when is_integer(Port) andalso (Port > 0) -> true;
+ (_) -> false
+ end,
+ ValidateIpFamily =
+ fun(inet) -> true;
+ (inet6) -> true;
+ (inet6fb4) -> true;
+ (_) -> false
+ end,
+ ValidateTimeout =
+ fun(Timeout) when is_integer(Timeout) andalso (Timeout > 0) -> true;
+ (_) -> false
+ end,
+ ValidateProgress =
+ fun(ignore) ->
+ true;
+ ({Mod, Func, _InitProgress}) when is_atom(Mod) andalso
+ is_atom(Func) ->
+ true;
+ (_) ->
+ false
+ end,
+ ValidOptions =
+ [{mode, ValidateMode, false, ?DEFAULT_MODE},
+ {host, ValidateHost, true, ehost},
+ {port, ValidatePort, false, ?FTP_PORT},
+ {ipfamily, ValidateIpFamily, false, inet},
+ {timeout, ValidateTimeout, false, ?CONNECTION_TIMEOUT},
+ {progress, ValidateProgress, false, ?PROGRESS_DEFAULT}],
+ validate_options(Options, ValidOptions, []).
+
+validate_options([], [], Acc) ->
+ ?fcrt("validate_options -> done", [{acc, Acc}]),
+ {ok, lists:reverse(Acc)};
+validate_options([], ValidOptions, Acc) ->
+ ?fcrt("validate_options -> done",
+ [{valid_options, ValidOptions}, {acc, Acc}]),
+ %% Check if any mandatory options are missing!
+ case [{Key, Reason} || {Key, _, true, Reason} <- ValidOptions] of
+ [] ->
+ Defaults =
+ [{Key, Default} || {Key, _, _, Default} <- ValidOptions],
+ {ok, lists:reverse(Defaults ++ Acc)};
+ [{_, Reason}|_Missing] ->
+ throw({error, Reason})
+ end;
+validate_options([{Key, Value}|Options], ValidOptions, Acc) ->
+ ?fcrt("validate_options -> check",
+ [{key, Key}, {value, Value}, {acc, Acc}]),
+ case lists:keysearch(Key, 1, ValidOptions) of
+ {value, {Key, Validate, _, Default}} ->
+ case (catch Validate(Value)) of
+ true ->
+ ?fcrt("validate_options -> check - accept", []),
+ NewValidOptions = lists:keydelete(Key, 1, ValidOptions),
+ validate_options(Options, NewValidOptions,
+ [{Key, Value} | Acc]);
+ _ ->
+ ?fcrt("validate_options -> check - reject",
+ [{default, Default}]),
+ NewValidOptions = lists:keydelete(Key, 1, ValidOptions),
+ validate_options(Options, NewValidOptions,
+ [{Key, Default} | Acc])
+ end;
+ false ->
+ validate_options(Options, ValidOptions, Acc)
+ end;
+validate_options([_|Options], ValidOptions, Acc) ->
+ validate_options(Options, ValidOptions, Acc).
+
+
+
+%%%========================================================================
+%%% gen_server callback functions
+%%%========================================================================
+
+%%-------------------------------------------------------------------------
+%% init(Args) -> {ok, State} | {ok, State, Timeout} | {stop, Reason}
+%% Description: Initiates the erlang process that manages a ftp connection.
+%%-------------------------------------------------------------------------
+init(Options) ->
+ process_flag(trap_exit, true),
+
+ %% Keep track of the client
+ {value, {client, Client}} = lists:keysearch(client, 1, Options),
+ erlang:monitor(process, Client),
+
+ %% Make sure inet is started
+ inet_db:start(),
+
+ %% Where are we
+ {ok, Dir} = file:get_cwd(),
+
+ %% Maybe activate dbg
+ case key_search(debug, Options, disable) of
+ trace ->
+ dbg:tracer(),
+ dbg:p(all, [call]),
+ dbg:tpl(ftp, [{'_', [], [{return_trace}]}]),
+ dbg:tpl(ftp_response, [{'_', [], [{return_trace}]}]),
+ dbg:tpl(ftp_progress, [{'_', [], [{return_trace}]}]);
+ debug ->
+ dbg:tracer(),
+ dbg:p(all, [call]),
+ dbg:tp(ftp, [{'_', [], [{return_trace}]}]),
+ dbg:tp(ftp_response, [{'_', [], [{return_trace}]}]),
+ dbg:tp(ftp_progress, [{'_', [], [{return_trace}]}]);
+ _ ->
+ %% Keep silent
+ ok
+ end,
+
+ %% Verbose?
+ Verbose = key_search(verbose, Options, false),
+
+ %% IpFamily?
+ IpFamily = key_search(ipfamily, Options, inet),
+
+ State = #state{owner = Client,
+ verbose = Verbose,
+ ipfamily = IpFamily,
+ ldir = Dir},
+
+ %% Set process prio
+ Priority = key_search(priority, Options, low),
+ process_flag(priority, Priority),
+
+ %% And we are done
+ {ok, State}.
+
+
+%%--------------------------------------------------------------------------
+%% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% Description: Handle incoming requests.
+%%-------------------------------------------------------------------------
+
+%% Anyone can ask this question
+handle_call({_, info}, _, #state{verbose = Verbose,
+ mode = Mode,
+ timeout = Timeout,
+ ipfamily = IpFamily,
+ csock = Socket,
+ progress = Progress} = State) ->
+ {ok, {_, LocalPort}} = inet:sockname(Socket),
+ {ok, {Address, Port}} = inet:peername(Socket),
+ Options = [{verbose, Verbose},
+ {ipfamily, IpFamily},
+ {mode, Mode},
+ {peer, Address},
+ {peer_port, Port},
+ {local_port, LocalPort},
+ {timeout, Timeout},
+ {progress, Progress}],
+ {reply, {ok, Options}, State};
+
+%% But everything else must come from the owner
+handle_call({Pid, _}, _, #state{owner = Owner} = State) when Owner =/= Pid ->
+ {reply, {error, not_connection_owner}, State};
+
+handle_call({_, {open, ip_comm, Opts}}, From, State) ->
+ ?fcrd("handle_call(open)", [{opts, Opts}]),
+ case key_search(host, Opts, undefined) of
+ undefined ->
+ {stop, normal, {error, ehost}, State};
+ Host ->
+ Mode = key_search(mode, Opts, ?DEFAULT_MODE),
+ Port = key_search(port, Opts, ?FTP_PORT),
+ Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT),
+ Progress = key_search(progress, Opts, ignore),
+
+ State2 = State#state{client = From,
+ mode = Mode,
+ progress = progress(Progress)},
+
+ ?fcrd("handle_call(open) -> setup ctrl connection with",
+ [{host, Host}, {port, Port}, {timeout, Timeout}]),
+ case setup_ctrl_connection(Host, Port, Timeout, State2) of
+ {ok, State3, WaitTimeout} ->
+ ?fcrd("handle_call(open) -> ctrl connection setup done",
+ [{waittimeout, WaitTimeout}]),
+ {noreply, State3, WaitTimeout};
+ {error, Reason} ->
+ ?fcrd("handle_call(open) -> ctrl connection setup failed",
+ [{reason, Reason}]),
+ gen_server:reply(From, {error, ehost}),
+ {stop, normal, State2#state{client = undefined}}
+ end
+ end;
+
+handle_call({_, {open, ip_comm, Host, Opts}}, From, State) ->
+ Mode = key_search(mode, Opts, ?DEFAULT_MODE),
+ Port = key_search(port, Opts, ?FTP_PORT),
+ Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT),
+ Progress = key_search(progress, Opts, ignore),
+
+ State2 = State#state{client = From,
+ mode = Mode,
+ progress = progress(Progress)},
+
+ case setup_ctrl_connection(Host, Port, Timeout, State2) of
+ {ok, State3, WaitTimeout} ->
+ {noreply, State3, WaitTimeout};
+ {error, _Reason} ->
+ gen_server:reply(From, {error, ehost}),
+ {stop, normal, State2#state{client = undefined}}
+ end;
+
+handle_call({_, force_active}, _, State) ->
+ {reply, ok, State#state{mode = active}};
+
+handle_call({_, {user, User, Password}}, From,
+ #state{csock = CSock} = State) when (CSock =/= undefined) ->
+ handle_user(User, Password, "", State#state{client = From});
+
+handle_call({_, {user, User, Password, Acc}}, From,
+ #state{csock = CSock} = State) when (CSock =/= undefined) ->
+ handle_user(User, Password, Acc, State#state{client = From});
+
+handle_call({_, {account, Acc}}, From, State)->
+ handle_user_account(Acc, State#state{client = From});
+
+handle_call({_, pwd}, From, #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("PWD", [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From, caller = pwd}};
+
+handle_call({_, lpwd}, From, #state{ldir = LDir} = State) ->
+ {reply, {ok, LDir}, State#state{client = From}};
+
+handle_call({_, {cd, Dir}}, From, #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("CWD ~s", [Dir])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From, caller = cd}};
+
+handle_call({_,{lcd, Dir}}, _From, #state{ldir = LDir0} = State) ->
+ LDir = filename:absname(Dir, LDir0),
+ case file:read_file_info(LDir) of %% FIX better check that LDir is a dir.
+ {ok, _ } ->
+ {reply, ok, State#state{ldir = LDir}};
+ _ ->
+ {reply, {error, epath}, State}
+ end;
+
+handle_call({_, {dir, Len, Dir}}, {_Pid, _} = From,
+ #state{chunk = false} = State) ->
+ setup_data_connection(State#state{caller = {dir, Dir, Len},
+ client = From});
+handle_call({_, {rename, CurrFile, NewFile}}, From,
+ #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("RNFR ~s", [CurrFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {rename, NewFile}, client = From}};
+
+handle_call({_, {delete, File}}, {_Pid, _} = From,
+ #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("DELE ~s", [File])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From}};
+
+handle_call({_, {mkdir, Dir}}, From, #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("MKD ~s", [Dir])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From}};
+
+handle_call({_,{rmdir, Dir}}, From, #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd("RMD ~s", [Dir])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From}};
+
+handle_call({_,{type, Type}}, From, #state{chunk = false}
+ = State) ->
+ case Type of
+ ascii ->
+ send_ctrl_message(State, mk_cmd("TYPE A", [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = type, type = ascii,
+ client = From}};
+ binary ->
+ send_ctrl_message(State, mk_cmd("TYPE I", [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = type, type = binary,
+ client = From}};
+ _ ->
+ {reply, {error, etype}, State}
+ end;
+
+handle_call({_,{recv, RemoteFile, LocalFile}}, From,
+ #state{chunk = false, ldir = LocalDir} = State) ->
+ progress_report({remote_file, RemoteFile}, State),
+ NewLocalFile = filename:absname(LocalFile, LocalDir),
+
+ case file_open(NewLocalFile, write) of
+ {ok, Fd} ->
+ setup_data_connection(State#state{client = From,
+ caller =
+ {recv_file,
+ RemoteFile, Fd}});
+ {error, _What} ->
+ {reply, {error, epath}, State}
+ end;
+
+handle_call({_, {recv_bin, RemoteFile}}, From, #state{chunk = false} =
+ State) ->
+ setup_data_connection(State#state{caller = {recv_bin, RemoteFile},
+ client = From});
+
+handle_call({_,{recv_chunk_start, RemoteFile}}, From, #state{chunk = false}
+ = State) ->
+ setup_data_connection(State#state{caller = {start_chunk_transfer,
+ "RETR", RemoteFile},
+ client = From});
+
+handle_call({_, recv_chunk}, _, #state{chunk = false} = State) ->
+ {reply, {error, "ftp:recv_chunk_start/2 not called"}, State};
+
+handle_call({_, recv_chunk}, From, #state{chunk = true} = State) ->
+ activate_data_connection(State),
+ {noreply, State#state{client = From, caller = recv_chunk}};
+
+handle_call({_, {send, LocalFile, RemoteFile}}, From,
+ #state{chunk = false, ldir = LocalDir} = State) ->
+ progress_report({local_file, filename:absname(LocalFile, LocalDir)},
+ State),
+ setup_data_connection(State#state{caller = {transfer_file,
+ {"STOR",
+ LocalFile, RemoteFile}},
+ client = From});
+handle_call({_, {append, LocalFile, RemoteFile}}, From,
+ #state{chunk = false} = State) ->
+ setup_data_connection(State#state{caller = {transfer_file,
+ {"APPE",
+ LocalFile, RemoteFile}},
+ client = From});
+handle_call({_, {send_bin, Bin, RemoteFile}}, From,
+ #state{chunk = false} = State) ->
+ setup_data_connection(State#state{caller = {transfer_data,
+ {"STOR", Bin, RemoteFile}},
+ client = From});
+handle_call({_,{append_bin, Bin, RemoteFile}}, From,
+ #state{chunk = false} = State) ->
+ setup_data_connection(State#state{caller = {transfer_data,
+ {"APPE", Bin, RemoteFile}},
+ client = From});
+handle_call({_, {send_chunk_start, RemoteFile}}, From, #state{chunk = false}
+ = State) ->
+ setup_data_connection(State#state{caller = {start_chunk_transfer,
+ "STOR", RemoteFile},
+ client = From});
+handle_call({_, {append_chunk_start, RemoteFile}}, From, #state{chunk = false}
+ = State) ->
+ setup_data_connection(State#state{caller = {start_chunk_transfer,
+ "APPE", RemoteFile},
+ client = From});
+handle_call({_, {transfer_chunk, Bin}}, _, #state{chunk = true} = State) ->
+ send_data_message(State, Bin),
+ {reply, ok, State};
+
+handle_call({_, chunk_end}, From, #state{chunk = true} = State) ->
+ close_data_connection(State),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From, dsock = undefined,
+ caller = end_chunk_transfer, chunk = false}};
+
+handle_call({_, {quote, Cmd}}, From, #state{chunk = false} = State) ->
+ send_ctrl_message(State, mk_cmd(Cmd, [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{client = From, caller = quote}};
+
+handle_call({_, _Req}, _From, #state{csock = CSock} = State)
+ when (CSock =:= undefined) ->
+ {reply, {error, not_connected}, State};
+
+handle_call(_, _, #state{chunk = true} = State) ->
+ {reply, {error, echunk}, State};
+
+%% Catch all - This can only happen if the application programmer writes
+%% really bad code that violates the API.
+handle_call(Request, _Timeout, State) ->
+ {stop, {'API_violation_connection_closed', Request},
+ {error, {connection_terminated, 'API_violation'}}, State}.
+
+%%--------------------------------------------------------------------------
+%% handle_cast(Request, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handles cast messages.
+%%-------------------------------------------------------------------------
+handle_cast({Pid, close}, #state{owner = Pid} = State) ->
+ send_ctrl_message(State, mk_cmd("QUIT", [])),
+ close_ctrl_connection(State),
+ close_data_connection(State),
+ {stop, normal, State#state{csock = undefined, dsock = undefined}};
+
+handle_cast({Pid, close}, State) ->
+ Report = io_lib:format("A none owner process ~p tried to close an "
+ "ftp connection: ~n", [Pid]),
+ error_logger:info_report(Report),
+ {noreply, State};
+
+%% Catch all - This can oly happen if the application programmer writes
+%% really bad code that violates the API.
+handle_cast(Msg, State) ->
+ {stop, {'API_violation_connection_closed', Msg}, State}.
+
+%%--------------------------------------------------------------------------
+%% handle_info(Msg, State) -> {noreply, State} | {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handles tcp messages from the ftp-server.
+%% Note: The order of the function clauses is significant.
+%%--------------------------------------------------------------------------
+
+handle_info(timeout, #state{caller = open} = State) ->
+ {stop, timeout, State};
+
+handle_info(timeout, State) ->
+ {noreply, State};
+
+%%% Data socket messages %%%
+handle_info({tcp, Socket, Data},
+ #state{dsock = Socket,
+ caller = {recv_file, Fd}} = State) ->
+ file_write(binary_to_list(Data), Fd),
+ progress_report({binary, Data}, State),
+ activate_data_connection(State),
+ {noreply, State};
+
+handle_info({tcp, Socket, Data}, #state{dsock = Socket, client = From,
+ caller = recv_chunk}
+ = State) ->
+ gen_server:reply(From, {ok, Data}),
+ {noreply, State#state{client = undefined, data = <<>>}};
+
+handle_info({tcp, Socket, Data}, #state{dsock = Socket} = State) ->
+ activate_data_connection(State),
+ {noreply, State#state{data = <<(State#state.data)/binary,
+ Data/binary>>}};
+
+handle_info({tcp_closed, Socket}, #state{dsock = Socket,
+ caller = {recv_file, Fd}}
+ = State) ->
+ file_close(Fd),
+ progress_report({transfer_size, 0}, State),
+ activate_ctrl_connection(State),
+ {noreply, State#state{dsock = undefined, data = <<>>}};
+
+handle_info({tcp_closed, Socket}, #state{dsock = Socket, client = From,
+ caller = recv_chunk}
+ = State) ->
+ gen_server:reply(From, ok),
+ {noreply, State#state{dsock = undefined, client = undefined,
+ data = <<>>, caller = undefined,
+ chunk = false}};
+
+handle_info({tcp_closed, Socket}, #state{dsock = Socket, caller = recv_bin,
+ data = Data} = State) ->
+ activate_ctrl_connection(State),
+ {noreply, State#state{dsock = undefined, data = <<>>,
+ caller = {recv_bin, Data}}};
+
+handle_info({tcp_closed, Socket}, #state{dsock = Socket, data = Data,
+ caller = {handle_dir_result, Dir}}
+ = State) ->
+ activate_ctrl_connection(State),
+ {noreply, State#state{dsock = undefined,
+ caller = {handle_dir_result, Dir, Data},
+% data = <<?CR,?LF>>}};
+ data = <<>>}};
+
+handle_info({tcp_error, Socket, Reason}, #state{dsock = Socket,
+ client = From} = State) ->
+ gen_server:reply(From, {error, Reason}),
+ close_data_connection(State),
+ {noreply, State#state{dsock = undefined, client = undefined,
+ data = <<>>, caller = undefined, chunk = false}};
+
+%%% Ctrl socket messages %%%
+handle_info({tcp, Socket, Data}, #state{csock = Socket,
+ verbose = Verbose,
+ caller = Caller,
+ client = From,
+ ctrl_data = {CtrlData, AccLines,
+ LineStatus}}
+ = State) ->
+ case ftp_response:parse_lines(<<CtrlData/binary, Data/binary>>,
+ AccLines, LineStatus) of
+ {ok, Lines, NextMsgData} ->
+ verbose(Lines, Verbose, 'receive'),
+ CtrlResult = ftp_response:interpret(Lines),
+ case Caller of
+ quote ->
+ gen_server:reply(From, string:tokens(Lines, [?CR, ?LF])),
+ {noreply, State#state{client = undefined,
+ caller = undefined,
+ ctrl_data = {NextMsgData, [],
+ start}}};
+ _ ->
+ handle_ctrl_result(CtrlResult,
+ State#state{ctrl_data =
+ {NextMsgData, [], start}})
+ end;
+ {continue, NewCtrlData} ->
+ activate_ctrl_connection(State),
+ {noreply, State#state{ctrl_data = NewCtrlData}}
+ end;
+
+handle_info({tcp_closed, Socket}, #state{csock = Socket}) ->
+ %% If the server closes the control channel it is
+ %% the expected behavior that connection process terminates.
+ exit(normal); %% User will get error message from terminate/2
+
+handle_info({tcp_error, Socket, Reason}, _) ->
+ Report =
+ io_lib:format("tcp_error on socket: ~p for reason: ~p~n",
+ [Socket, Reason]),
+ error_logger:error_report(Report),
+ %% If tcp does not work the only option is to terminate,
+ %% this is the expected behavior under these circumstances.
+ exit(normal); %% User will get error message from terminate/2
+
+%% Monitor messages - if the process owning the ftp connection goes
+%% down there is no point in continuing.
+handle_info({'DOWN', _Ref, _Type, _Process, normal}, State) ->
+ {stop, normal, State#state{client = undefined}};
+
+handle_info({'DOWN', _Ref, _Type, _Process, shutdown}, State) ->
+ {stop, normal, State#state{client = undefined}};
+
+handle_info({'DOWN', _Ref, _Type, _Process, timeout}, State) ->
+ {stop, normal, State#state{client = undefined}};
+
+handle_info({'DOWN', _Ref, _Type, Process, Reason}, State) ->
+ {stop, {stopped, {'EXIT', Process, Reason}},
+ State#state{client = undefined}};
+
+handle_info({'EXIT', Pid, Reason}, #state{progress = Pid} = State) ->
+ Report = io_lib:format("Progress reporting stopped for reason ~p~n",
+ Reason),
+ error_logger:info_report(Report),
+ {noreply, State#state{progress = ignore}};
+
+%% Catch all - throws away unknown messages (This could happen by "accident"
+%% so we do not want to crash, but we make a log entry as it is an
+%% unwanted behaviour.)
+handle_info(Info, State) ->
+ Report = io_lib:format("ftp : ~p : Unexpected message: ~p\n",
+ [self(), Info]),
+ error_logger:info_report(Report),
+ {noreply, State}.
+
+%%--------------------------------------------------------------------------
+%% terminate/2 and code_change/3
+%%--------------------------------------------------------------------------
+terminate(normal, State) ->
+ %% If terminate reason =/= normal the progress reporting process will
+ %% be killed by the exit signal.
+ progress_report(stop, State),
+ do_termiante({error, econn}, State);
+terminate(Reason, State) ->
+ Report = io_lib:format("Ftp connection closed due to: ~p~n", [Reason]),
+ error_logger:error_report(Report),
+ do_termiante({error, eclosed}, State).
+
+do_termiante(ErrorMsg, State) ->
+ close_data_connection(State),
+ close_ctrl_connection(State),
+ case State#state.client of
+ undefined ->
+ ok;
+ From ->
+ gen_server:reply(From, ErrorMsg)
+ end,
+ ok.
+
+code_change(_Vsn, State1, upgrade_from_pre_5_12) ->
+ {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout,
+ Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress} = State1,
+ IpFamily =
+ if
+ (IPv6Disable =:= true) ->
+ inet;
+ true ->
+ inet6fb4
+ end,
+ State2 = #state{csock = CSock,
+ dsock = DSock,
+ verbose = Verbose,
+ ldir = LDir,
+ type = Type,
+ chunk = Chunk,
+ mode = Mode,
+ timeout = Timeout,
+ data = Data,
+ ctrl_data = CtrlData,
+ owner = Owner,
+ client = Client,
+ caller = Caller,
+ ipfamily = IpFamily,
+ progress = Progress},
+ {ok, State2};
+
+code_change(_Vsn, State1, downgrade_to_pre_5_12) ->
+ #state{csock = CSock,
+ dsock = DSock,
+ verbose = Verbose,
+ ldir = LDir,
+ type = Type,
+ chunk = Chunk,
+ mode = Mode,
+ timeout = Timeout,
+ data = Data,
+ ctrl_data = CtrlData,
+ owner = Owner,
+ client = Client,
+ caller = Caller,
+ ipfamily = IpFamily,
+ progress = Progress} = State1,
+ IPv6Disable =
+ if
+ (IpFamily =:= inet) ->
+ true;
+ true ->
+ false
+ end,
+ State2 =
+ {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout,
+ Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress},
+ {ok, State2};
+
+code_change(_Vsn, State, _Extra) ->
+ {ok, State}.
+
+
+%%%=========================================================================
+%% Start/stop
+%%%=========================================================================
+%%--------------------------------------------------------------------------
+%% start_link([Opts, GenServerOptions]) -> {ok, Pid} | {error, Reason}
+%%
+%% Description: Callback function for the ftp supervisor. It is called
+%% : when start_service/1 calls ftp_sup:start_child/1 to start an
+%% : instance of the ftp process. Also called by start_standalone/1
+%%--------------------------------------------------------------------------
+start_link([Opts, GenServerOptions]) ->
+ start_link(Opts, GenServerOptions).
+
+start_link(Opts, GenServerOptions) ->
+ case lists:keysearch(client, 1, Opts) of
+ {value, _} ->
+ %% Via the supervisor
+ gen_server:start_link(?MODULE, Opts, GenServerOptions);
+ false ->
+ Opts2 = [{client, self()} | Opts],
+ gen_server:start_link(?MODULE, Opts2, GenServerOptions)
+ end.
+
+
+%%% Stop functionality is handled by close/1
+
+%%%========================================================================
+%%% Internal functions
+%%%========================================================================
+
+%%--------------------------------------------------------------------------
+%%% Help functions to handle_call and/or handle_ctrl_result
+%%--------------------------------------------------------------------------
+%% User handling
+handle_user(User, Password, Acc, State) ->
+ send_ctrl_message(State, mk_cmd("USER ~s", [User])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {handle_user, Password, Acc}}}.
+
+handle_user_passwd(Password, Acc, State) ->
+ send_ctrl_message(State, mk_cmd("PASS ~s", [Password])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {handle_user_passwd, Acc}}}.
+
+handle_user_account(Acc, State) ->
+ send_ctrl_message(State, mk_cmd("ACCT ~s", [Acc])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = handle_user_account}}.
+
+
+%%--------------------------------------------------------------------------
+%% handle_ctrl_result
+%%--------------------------------------------------------------------------
+%%--------------------------------------------------------------------------
+%% Handling of control connection setup
+handle_ctrl_result({pos_compl, _}, #state{caller = open, client = From}
+ = State) ->
+ gen_server:reply(From, {ok, self()}),
+ {noreply, State#state{client = undefined,
+ caller = undefined }};
+handle_ctrl_result({_, Lines}, #state{caller = open} = State) ->
+ ctrl_result_response(econn, State, {error, Lines});
+
+%%--------------------------------------------------------------------------
+%% Data connection setup active mode
+handle_ctrl_result({pos_compl, _Lines},
+ #state{mode = active,
+ caller = {setup_data_connection,
+ {LSock, Caller}}} = State) ->
+ handle_caller(State#state{caller = Caller, dsock = {lsock, LSock}});
+
+handle_ctrl_result({Status, Lines},
+ #state{mode = active,
+ caller = {setup_data_connection, {LSock, _}}}
+ = State) ->
+ close_connection(LSock),
+ ctrl_result_response(Status, State, {error, Lines});
+
+%% Data connection setup passive mode
+handle_ctrl_result({pos_compl, Lines},
+ #state{mode = passive,
+ ipfamily = inet6,
+ client = From,
+ caller = {setup_data_connection, Caller},
+ csock = CSock,
+ timeout = Timeout}
+ = State) ->
+ [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")),
+ {ok, {IP, _}} = inet:peername(CSock),
+ case connect(IP, list_to_integer(PortStr), Timeout, State) of
+ {ok, _, Socket} ->
+ handle_caller(State#state{caller = Caller, dsock = Socket});
+ {error, _Reason} = Error ->
+ gen_server:reply(From, Error),
+ {noreply, State#state{client = undefined, caller = undefined}}
+ end;
+
+handle_ctrl_result({pos_compl, Lines},
+ #state{mode = passive,
+ ipfamily = inet,
+ client = From,
+ caller = {setup_data_connection, Caller},
+ timeout = Timeout} = State) ->
+
+ {_, [?LEFT_PAREN | Rest]} =
+ lists:splitwith(fun(?LEFT_PAREN) -> false; (_) -> true end, Lines),
+ {NewPortAddr, _} =
+ lists:splitwith(fun(?RIGHT_PAREN) -> false; (_) -> true end, Rest),
+ [A1, A2, A3, A4, P1, P2] =
+ lists:map(fun(X) -> list_to_integer(X) end,
+ string:tokens(NewPortAddr, [$,])),
+ IP = {A1, A2, A3, A4},
+ Port = (P1 * 256) + P2,
+ case connect(IP, Port, Timeout, State) of
+ {ok, _, Socket} ->
+ handle_caller(State#state{caller = Caller, dsock = Socket});
+ {error, _Reason} = Error ->
+ gen_server:reply(From, Error),
+ {noreply,State#state{client = undefined, caller = undefined}}
+ end;
+
+%% FTP server does not support passive mode: try to fallback on active mode
+handle_ctrl_result(_,
+ #state{mode = passive,
+ caller = {setup_data_connection, Caller}} = State) ->
+ setup_data_connection(State#state{mode = active, caller = Caller});
+
+
+%%--------------------------------------------------------------------------
+%% User handling
+handle_ctrl_result({pos_interm, _},
+ #state{caller = {handle_user, PassWord, Acc}} = State) ->
+ handle_user_passwd(PassWord, Acc, State);
+handle_ctrl_result({Status, _},
+ #state{caller = {handle_user, _, _}} = State) ->
+ ctrl_result_response(Status, State, {error, euser});
+
+%% Accounts
+handle_ctrl_result({pos_interm_acct, _},
+ #state{caller = {handle_user_passwd, Acc}} = State)
+ when Acc =/= "" ->
+ handle_user_account(Acc, State);
+handle_ctrl_result({Status, _},
+ #state{caller = {handle_user_passwd, _}} = State) ->
+ ctrl_result_response(Status, State, {error, euser});
+
+%%--------------------------------------------------------------------------
+%% Print current working directory
+handle_ctrl_result({pos_compl, Lines},
+ #state{caller = pwd, client = From} = State) ->
+ Dir = pwd_result(Lines),
+ gen_server:reply(From, {ok, Dir}),
+ {noreply, State#state{client = undefined, caller = undefined}};
+
+%%--------------------------------------------------------------------------
+%% Directory listing
+handle_ctrl_result({pos_prel, _}, #state{caller = {dir, Dir}} = State) ->
+ NewState = accept_data_connection(State),
+ activate_data_connection(NewState),
+ {noreply, NewState#state{caller = {handle_dir_result, Dir}}};
+
+handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, Dir,
+ Data}, client = From}
+ = State) ->
+ case Dir of
+ "" -> % Current directory
+ gen_server:reply(From, {ok, Data}),
+ {noreply, State#state{client = undefined,
+ caller = undefined}};
+ _ ->
+ %% <WTF>
+ %% Dir cannot be assumed to be a dir. It is a string that
+ %% could be a dir, but could also be a file or even a string
+ %% containing wildcards (*).
+ %%
+ %% %% If there is only one line it might be a directory with one
+ %% %% file but it might be an error message that the directory
+ %% %% was not found. So in this case we have to endure a little
+ %% %% overhead to be able to give a good return value. Alas not
+ %% %% all ftp implementations behave the same and returning
+ %% %% an error string is allowed by the FTP RFC.
+ %% case lists:dropwhile(fun(?CR) -> false;(_) -> true end,
+ %% binary_to_list(Data)) of
+ %% L when (L =:= [?CR, ?LF]) orelse (L =:= []) ->
+ %% send_ctrl_message(State, mk_cmd("PWD", [])),
+ %% activate_ctrl_connection(State),
+ %% {noreply,
+ %% State#state{caller = {handle_dir_data, Dir, Data}}};
+ %% _ ->
+ %% gen_server:reply(From, {ok, Data}),
+ %% {noreply, State#state{client = undefined,
+ %% caller = undefined}}
+ %% end
+ %% </WTF>
+ gen_server:reply(From, {ok, Data}),
+ {noreply, State#state{client = undefined,
+ caller = undefined}}
+ end;
+
+handle_ctrl_result({pos_compl, Lines},
+ #state{caller = {handle_dir_data, Dir, DirData}} =
+ State) ->
+ OldDir = pwd_result(Lines),
+ send_ctrl_message(State, mk_cmd("CWD ~s", [Dir])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {handle_dir_data_second_phase, OldDir,
+ DirData}}};
+handle_ctrl_result({Status, _},
+ #state{caller = {handle_dir_data, _, _}} = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+
+handle_ctrl_result(S={_Status, _},
+ #state{caller = {handle_dir_result, _, _}} = State) ->
+ %% OTP-5731, macosx
+ ctrl_result_response(S, State, {error, epath});
+
+handle_ctrl_result({pos_compl, _},
+ #state{caller = {handle_dir_data_second_phase, OldDir,
+ DirData}} = State) ->
+ send_ctrl_message(State, mk_cmd("CWD ~s", [OldDir])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {handle_dir_data_third_phase, DirData}}};
+handle_ctrl_result({Status, _},
+ #state{caller = {handle_dir_data_second_phase, _, _}}
+ = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+handle_ctrl_result(_, #state{caller = {handle_dir_data_third_phase, DirData},
+ client = From} = State) ->
+ gen_server:reply(From, {ok, DirData}),
+ {noreply, State#state{client = undefined, caller = undefined}};
+
+handle_ctrl_result({Status, _}, #state{caller = cd} = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+
+handle_ctrl_result(Status={epath, _}, #state{caller = {dir,_}} = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+
+%%--------------------------------------------------------------------------
+%% File renaming
+handle_ctrl_result({pos_interm, _}, #state{caller = {rename, NewFile}}
+ = State) ->
+ send_ctrl_message(State, mk_cmd("RNTO ~s", [NewFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = rename_second_phase}};
+
+handle_ctrl_result({Status, _},
+ #state{caller = {rename, _}} = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+
+handle_ctrl_result({Status, _},
+ #state{caller = rename_second_phase} = State) ->
+ ctrl_result_response(Status, State, {error, epath});
+
+%%--------------------------------------------------------------------------
+%% File handling - recv_bin
+handle_ctrl_result({pos_prel, _}, #state{caller = recv_bin} = State) ->
+ NewState = accept_data_connection(State),
+ activate_data_connection(NewState),
+ {noreply, NewState};
+
+handle_ctrl_result({pos_compl, _}, #state{caller = {recv_bin, Data},
+ client = From} = State) ->
+ gen_server:reply(From, {ok, Data}),
+ close_data_connection(State),
+ {noreply, State#state{client = undefined, caller = undefined}};
+
+handle_ctrl_result({Status, _}, #state{caller = recv_bin} = State) ->
+ close_data_connection(State),
+ ctrl_result_response(Status, State#state{dsock = undefined},
+ {error, epath});
+
+handle_ctrl_result({Status, _}, #state{caller = {recv_bin, _}} = State) ->
+ close_data_connection(State),
+ ctrl_result_response(Status, State#state{dsock = undefined},
+ {error, epath});
+%%--------------------------------------------------------------------------
+%% File handling - start_chunk_transfer
+handle_ctrl_result({pos_prel, _}, #state{client = From,
+ caller = start_chunk_transfer}
+ = State) ->
+ NewState = accept_data_connection(State),
+ gen_server:reply(From, ok),
+ {noreply, NewState#state{chunk = true, client = undefined,
+ caller = undefined}};
+%%--------------------------------------------------------------------------
+%% File handling - recv_file
+handle_ctrl_result({pos_prel, _}, #state{caller = {recv_file, _}} = State) ->
+ NewState = accept_data_connection(State),
+ activate_data_connection(NewState),
+ {noreply, NewState};
+
+handle_ctrl_result({Status, _}, #state{caller = {recv_file, Fd}} = State) ->
+ file_close(Fd),
+ close_data_connection(State),
+ ctrl_result_response(Status, State#state{dsock = undefined},
+ {error, epath});
+%%--------------------------------------------------------------------------
+%% File handling - transfer_*
+handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_file, Fd}}
+ = State) ->
+ NewState = accept_data_connection(State),
+ send_file(Fd, NewState);
+
+handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_data, Bin}}
+ = State) ->
+ NewState = accept_data_connection(State),
+ send_data_message(NewState, Bin),
+ close_data_connection(NewState),
+ activate_ctrl_connection(NewState),
+ {noreply, NewState#state{caller = transfer_data_second_phase,
+ dsock = undefined}};
+%%--------------------------------------------------------------------------
+%% Default
+handle_ctrl_result({Status, Lines}, #state{client = From} = State)
+ when From =/= undefined ->
+ ctrl_result_response(Status, State, {error, Lines}).
+
+%%--------------------------------------------------------------------------
+%% Help functions to handle_ctrl_result
+%%--------------------------------------------------------------------------
+ctrl_result_response(pos_compl, #state{client = From} = State, _) ->
+ gen_server:reply(From, ok),
+ {noreply, State#state{client = undefined, caller = undefined}};
+
+ctrl_result_response(Status, #state{client = From} = State, _)
+ when (Status =:= etnospc) orelse
+ (Status =:= epnospc) orelse
+ (Status =:= efnamena) orelse
+ (Status =:= econn) ->
+%Status == etnospc; Status == epnospc; Status == econn ->
+ gen_server:reply(From, {error, Status}),
+%% {stop, normal, {error, Status}, State#state{client = undefined}};
+ {stop, normal, State#state{client = undefined}};
+
+ctrl_result_response(_, #state{client = From} = State, ErrorMsg) ->
+ gen_server:reply(From, ErrorMsg),
+ {noreply, State#state{client = undefined, caller = undefined}}.
+
+%%--------------------------------------------------------------------------
+handle_caller(#state{caller = {dir, Dir, Len}} = State) ->
+ Cmd = case Len of
+ short -> "NLST";
+ long -> "LIST"
+ end,
+ case Dir of
+ "" ->
+ send_ctrl_message(State, mk_cmd(Cmd, ""));
+ _ ->
+ send_ctrl_message(State, mk_cmd(Cmd ++ " ~s", [Dir]))
+ end,
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {dir, Dir}}};
+
+handle_caller(#state{caller = {recv_bin, RemoteFile}} = State) ->
+ send_ctrl_message(State, mk_cmd("RETR ~s", [RemoteFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = recv_bin}};
+
+handle_caller(#state{caller = {start_chunk_transfer, Cmd, RemoteFile}} =
+ State) ->
+ send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = start_chunk_transfer}};
+
+handle_caller(#state{caller = {recv_file, RemoteFile, Fd}} = State) ->
+ send_ctrl_message(State, mk_cmd("RETR ~s", [RemoteFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {recv_file, Fd}}};
+
+handle_caller(#state{caller = {transfer_file, {Cmd, LocalFile, RemoteFile}},
+ ldir = LocalDir, client = From} = State) ->
+ case file_open(filename:absname(LocalFile, LocalDir), read) of
+ {ok, Fd} ->
+ send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {transfer_file, Fd}}};
+ {error, _} ->
+ gen_server:reply(From, {error, epath}),
+ {noreply, State#state{client = undefined, caller = undefined,
+ dsock = undefined}}
+ end;
+
+handle_caller(#state{caller = {transfer_data, {Cmd, Bin, RemoteFile}}} =
+ State) ->
+ send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {transfer_data, Bin}}}.
+
+%% ----------- FTP SERVER COMMUNICATION -------------------------
+
+%% Connect to FTP server at Host (default is TCP port 21)
+%% in order to establish a control connection.
+setup_ctrl_connection(Host, Port, Timeout, State) ->
+ MsTime = millisec_time(),
+ case connect(Host, Port, Timeout, State) of
+ {ok, IpFam, CSock} ->
+ NewState = State#state{csock = CSock, ipfamily = IpFam},
+ activate_ctrl_connection(NewState),
+ case Timeout - (millisec_time() - MsTime) of
+ Timeout2 when (Timeout2 >= 0) ->
+ {ok, NewState#state{caller = open}, Timeout2};
+ _ ->
+ %% Oups: Simulate timeout
+ {ok, NewState#state{caller = open}, 0}
+ end;
+ Error ->
+ Error
+ end.
+
+setup_data_connection(#state{mode = active,
+ caller = Caller,
+ csock = CSock} = State) ->
+ IntToString = fun(Element) -> integer_to_list(Element) end,
+
+ case (catch inet:sockname(CSock)) of
+ {ok, {{_, _, _, _, _, _, _, _} = IP, _}} ->
+ {ok, LSock} =
+ gen_tcp:listen(0, [{ip, IP}, {active, false},
+ inet6, binary, {packet, 0}]),
+ {ok, Port} = inet:port(LSock),
+ Cmd = mk_cmd("EPRT |2|~s:~s:~s:~s:~s:~s:~s:~s|~s|",
+ lists:map(IntToString,
+ tuple_to_list(IP) ++ [Port])),
+ send_ctrl_message(State, Cmd),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {setup_data_connection,
+ {LSock, Caller}}}};
+ {ok, {{_,_,_,_} = IP, _}} ->
+ {ok, LSock} = gen_tcp:listen(0, [{ip, IP}, {active, false},
+ binary, {packet, 0}]),
+ {ok, Port} = inet:port(LSock),
+ {IP1, IP2, IP3, IP4} = IP,
+ {Port1, Port2} = {Port div 256, Port rem 256},
+ send_ctrl_message(State,
+ mk_cmd("PORT ~w,~w,~w,~w,~w,~w",
+ [IP1, IP2, IP3, IP4, Port1, Port2])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {setup_data_connection,
+ {LSock, Caller}}}}
+ end;
+
+setup_data_connection(#state{mode = passive, ipfamily = inet6,
+ caller = Caller} = State) ->
+ send_ctrl_message(State, mk_cmd("EPSV", [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {setup_data_connection, Caller}}};
+
+setup_data_connection(#state{mode = passive, ipfamily = inet,
+ caller = Caller} = State) ->
+ send_ctrl_message(State, mk_cmd("PASV", [])),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = {setup_data_connection, Caller}}}.
+
+
+%% setup_data_connection(#state{mode = passive, ip_v6_disabled = false,
+%% caller = Caller} = State) ->
+%% send_ctrl_message(State, mk_cmd("EPSV", [])),
+%% activate_ctrl_connection(State),
+%% {noreply, State#state{caller = {setup_data_connection, Caller}}};
+
+%% setup_data_connection(#state{mode = passive, ip_v6_disabled = true,
+%% caller = Caller} = State) ->
+%% send_ctrl_message(State, mk_cmd("PASV", [])),
+%% activate_ctrl_connection(State),
+%% {noreply, State#state{caller = {setup_data_connection, Caller}}}.
+
+
+connect(Host, Port, Timeout, #state{ipfamily = inet = IpFam}) ->
+ connect2(Host, Port, IpFam, Timeout);
+
+connect(Host, Port, Timeout, #state{ipfamily = inet6 = IpFam}) ->
+ connect2(Host, Port, IpFam, Timeout);
+
+connect(Host, Port, Timeout, #state{ipfamily = inet6fb4}) ->
+ case inet:getaddr(Host, inet6) of
+ {ok, {0, 0, 0, 0, 0, 16#ffff, _, _} = IPv6} ->
+ case inet:getaddr(Host, inet) of
+ {ok, IPv4} ->
+ IpFam = inet,
+ connect2(IPv4, Port, IpFam, Timeout);
+
+ _ ->
+ IpFam = inet6,
+ connect2(IPv6, Port, IpFam, Timeout)
+ end;
+
+ {ok, IPv6} ->
+ IpFam = inet6,
+ connect2(IPv6, Port, IpFam, Timeout);
+
+ _ ->
+ case inet:getaddr(Host, inet) of
+ {ok, IPv4} ->
+ IpFam = inet,
+ connect2(IPv4, Port, IpFam, Timeout);
+ Error ->
+ Error
+ end
+ end.
+
+connect2(Host, Port, IpFam, Timeout) ->
+ Opts = [IpFam, binary, {packet, 0}, {active, false}],
+ case gen_tcp:connect(Host, Port, Opts, Timeout) of
+ {ok, Sock} ->
+ {ok, IpFam, Sock};
+ Error ->
+ Error
+ end.
+
+
+
+accept_data_connection(#state{mode = active,
+ dsock = {lsock, LSock}} = State) ->
+ {ok, Socket} = gen_tcp:accept(LSock),
+ gen_tcp:close(LSock),
+ State#state{dsock = Socket};
+
+accept_data_connection(#state{mode = passive} = State) ->
+ State.
+
+send_ctrl_message(#state{csock = Socket, verbose = Verbose}, Message) ->
+ %% io:format("send control message: ~n~p~n", [lists:flatten(Message)]),
+ verbose(lists:flatten(Message),Verbose,send),
+ send_message(Socket, Message).
+
+send_data_message(#state{dsock = Socket}, Message) ->
+ send_message(Socket, Message).
+
+send_message(Socket, Message) ->
+ case gen_tcp:send(Socket, Message) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ Report = io_lib:format("gen_tcp:send/2 failed for "
+ "reason ~p~n", [Reason]),
+ error_logger:error_report(Report),
+ %% If tcp does not work the only option is to terminate,
+ %% this is the expected behavior under these circumstances.
+ exit(normal) %% User will get error message from terminate/2
+ end.
+
+activate_ctrl_connection(#state{csock = Socket, ctrl_data = {<<>>, _, _}}) ->
+ activate_connection(Socket);
+activate_ctrl_connection(#state{csock = Socket}) ->
+ %% We have already received at least part of the next control message,
+ %% that has been saved in ctrl_data, process this first.
+ self() ! {tcp, Socket, <<>>}.
+
+activate_data_connection(#state{dsock = Socket}) ->
+ activate_connection(Socket).
+
+activate_connection(Socket) ->
+ inet:setopts(Socket, [{active, once}]).
+
+close_ctrl_connection(#state{csock = undefined}) ->
+ ok;
+close_ctrl_connection(#state{csock = Socket}) ->
+ close_connection(Socket).
+
+close_data_connection(#state{dsock = undefined}) ->
+ ok;
+close_data_connection(#state{dsock = {lsock, Socket}}) ->
+ close_connection(Socket);
+close_data_connection(#state{dsock = Socket}) ->
+ close_connection(Socket).
+
+close_connection(Socket) ->
+ gen_tcp:close(Socket).
+
+%% ------------ FILE HANDELING ----------------------------------------
+
+send_file(Fd, State) ->
+ case file_read(Fd) of
+ {ok, N, Bin} when N > 0->
+ send_data_message(State, Bin),
+ progress_report({binary, Bin}, State),
+ send_file(Fd, State);
+ {ok, _, _} ->
+ file_close(Fd),
+ close_data_connection(State),
+ progress_report({transfer_size, 0}, State),
+ activate_ctrl_connection(State),
+ {noreply, State#state{caller = transfer_file_second_phase,
+ dsock = undefined}};
+ {error, Reason} ->
+ gen_server:reply(State#state.client, {error, Reason}),
+ {stop, normal, State#state{client = undefined}}
+ end.
+
+file_open(File, Option) ->
+ file:open(File, [raw, binary, Option]).
+
+file_close(Fd) ->
+ file:close(Fd).
+
+file_read(Fd) ->
+ case file:read(Fd, ?FILE_BUFSIZE) of
+ {ok, Bytes} ->
+ {ok, size(Bytes), Bytes};
+ eof ->
+ {ok, 0, []};
+ Other ->
+ Other
+ end.
+
+file_write(Bytes, Fd) ->
+ file:write(Fd, Bytes).
+
+%% -------------- MISC ----------------------------------------------
+
+call(GenServer, Msg, Format) ->
+ call(GenServer, Msg, Format, infinity).
+call(GenServer, Msg, Format, Timeout) ->
+ Req = {self(), Msg},
+ case (catch gen_server:call(GenServer, Req, Timeout)) of
+ {ok, Bin} when is_binary(Bin) andalso (Format =:= string) ->
+ {ok, binary_to_list(Bin)};
+ {'EXIT', _} ->
+ {error, eclosed};
+ Result ->
+ Result
+ end.
+
+cast(GenServer, Msg) ->
+ gen_server:cast(GenServer, {self(), Msg}).
+
+mk_cmd(Fmt, Args) ->
+ [io_lib:format(Fmt, Args)| [?CR, ?LF]]. % Deep list ok.
+
+pwd_result(Lines) ->
+ {_, [?DOUBLE_QUOTE | Rest]} =
+ lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Lines),
+ {Dir, _} =
+ lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Rest),
+ Dir.
+
+%% is_verbose(Params) ->
+%% check_param(verbose, Params).
+
+%% is_debug(Flags) ->
+%% check_param(debug, Flags).
+
+%% is_trace(Flags) ->
+%% check_param(trace, Flags).
+
+%% is_ipv6_disabled(Flags) ->
+%% check_param(ip_v6_disabled, Flags).
+
+%% check_param(Param, Params) ->
+%% lists:member(Param, Params).
+
+key_search(Key, List, Default) ->
+ case lists:keysearch(Key, 1, List) of
+ {value, {_,Val}} ->
+ Val;
+ false ->
+ Default
+ end.
+
+%% check_option(Pred, Value, Default) ->
+%% case Pred(Value) of
+%% true ->
+%% Value;
+%% false ->
+%% Default
+%% end.
+
+verbose(Lines, true, Direction) ->
+ DirStr =
+ case Direction of
+ send ->
+ "Sending: ";
+ _ ->
+ "Receiving: "
+ end,
+ Str = string:strip(string:strip(Lines, right, ?LF), right, ?CR),
+ erlang:display(DirStr++Str);
+verbose(_, false,_) ->
+ ok.
+
+progress(Options) ->
+ ftp_progress:start_link(Options).
+
+progress_report(_, #state{progress = ignore}) ->
+ ok;
+progress_report(stop, #state{progress = ProgressPid}) ->
+ ftp_progress:stop(ProgressPid);
+progress_report({binary, Data}, #state{progress = ProgressPid}) ->
+ ftp_progress:report(ProgressPid, {transfer_size, size(Data)});
+progress_report(Report, #state{progress = ProgressPid}) ->
+ ftp_progress:report(ProgressPid, Report).
+
+
+millisec_time() ->
+ {A,B,C} = erlang:now(),
+ A*1000000000+B*1000+(C div 1000).