%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2011. 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).
%% 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,
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()
}).
-type shortage_reason() :: 'etnospc' | 'epnospc'.
-type restriction_reason() :: 'epath' | 'efnamena' | 'elogin' | 'enotbinary'.
-type common_reason() :: 'econn' | 'eclosed' | term().
-type file_write_error_reason() :: term(). % See file:write for more info
%%%=========================================================================
%%% 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.
%%--------------------------------------------------------------------------
-spec open(Host :: string() | inet:ip_address()) ->
{'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}.
%% <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, []).
-spec open(Host :: string() | inet:ip_address(), Opts :: list()) ->
{'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}.
%% <BACKWARD-COMPATIBILLITY>
open(Host, Port) when is_integer(Port) ->
open(Host, [{port, Port}]);
%% </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.
%%--------------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------------
-spec user(Pid :: pid(),
User :: string(),
Pass :: string()) ->
'ok' | {'error', Reason :: 'euser' | common_reason()}.
user(Pid, User, Pass) ->
call(Pid, {user, User, Pass}, atom).
-spec user(Pid :: pid(),
User :: string(),
Pass :: string(),
Acc :: string()) ->
'ok' | {'error', Reason :: 'euser' | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec account(Pid :: pid(), Acc :: string()) ->
'ok' | {'error', Reason :: 'eacct' | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec pwd(Pid :: pid()) ->
{'ok', Dir :: string()} |
{'error', Reason :: restriction_reason() | common_reason()}.
pwd(Pid) ->
call(Pid, pwd, ctrl).
%%--------------------------------------------------------------------------
%% lpwd(Pid) -> {ok, Dir}
%% Pid = pid()
%% Dir = string()
%%
%% Description: Get the current working directory at local server.
%%--------------------------------------------------------------------------
-spec lpwd(Pid :: pid()) ->
{'ok', Dir :: string()}.
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.
%%--------------------------------------------------------------------------
-spec cd(Pid :: pid(), Dir :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec lcd(Pid :: pid(), Dir :: string()) ->
'ok' | {'error', Reason :: restriction_reason()}.
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.
%%--------------------------------------------------------------------------
-spec ls(Pid :: pid()) ->
{'ok', Listing :: string()} |
{'error', Reason :: restriction_reason() | common_reason()}.
ls(Pid) ->
ls(Pid, "").
-spec ls(Pid :: pid(), Dir :: string()) ->
{'ok', Listing :: string()} |
{'error', Reason :: restriction_reason() | common_reason()}.
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
%%--------------------------------------------------------------------------
-spec nlist(Pid :: pid()) ->
{'ok', Listing :: string()} |
{'error', Reason :: restriction_reason() | common_reason()}.
nlist(Pid) ->
nlist(Pid, "").
-spec nlist(Pid :: pid(), Pathname :: string()) ->
{'ok', Listing :: string()} |
{'error', Reason :: restriction_reason() | common_reason()}.
nlist(Pid, Dir) ->
call(Pid, {dir, short, Dir}, string).
%%--------------------------------------------------------------------------
%% rename(Pid, Old, New) -> ok | {error, epath} | {error, elogin}
%% | {error, econn}
%% Pid = pid()
%% CurrFile = NewFile = string()
%%
%% Description: Rename a file at remote server.
%%--------------------------------------------------------------------------
-spec rename(Pid :: pid(), Old :: string(), New :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
rename(Pid, Old, New) ->
call(Pid, {rename, Old, New}, string).
%%--------------------------------------------------------------------------
%% delete(Pid, File) -> ok | {error, epath} | {error, elogin} |
%% {error, econn}
%% Pid = pid()
%% File = string()
%%
%% Description: Remove file at remote server.
%%--------------------------------------------------------------------------
-spec delete(Pid :: pid(), File :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec mkdir(Pid :: pid(), Dir :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec rmdir(Pid :: pid(), Dir :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec type(Pid :: pid(), Type :: ascii | binary) ->
'ok' |
{'error', Reason :: 'etype' | restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec recv(Pid :: pid(), RemoteFileName :: string()) ->
'ok' | {'error', Reason :: restriction_reason() |
common_reason() |
file_write_error_reason()}.
recv(Pid, RemotFileName) ->
recv(Pid, RemotFileName, RemotFileName).
-spec recv(Pid :: pid(),
RemoteFileName :: string(),
LocalFileName :: string()) ->
'ok' | {'error', Reason :: term()}.
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.
%%--------------------------------------------------------------------------
-spec recv_bin(Pid :: pid(),
RemoteFile :: string()) ->
{'ok', Bin :: binary()} |
{'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec recv_chunk_start(Pid :: pid(),
RemoteFile :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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
%%--------------------------------------------------------------------------
-spec recv_chunk(Pid :: pid()) ->
'ok' |
{'ok', Bin :: binary()} |
{'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec send(Pid :: pid(), LocalFileName :: string()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
send(Pid, LocalFileName) ->
send(Pid, LocalFileName, LocalFileName).
-spec send(Pid :: pid(),
LocalFileName :: string(),
RemoteFileName :: string()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
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.
%%--------------------------------------------------------------------------
-spec send_bin(Pid :: pid(), Bin :: binary(), RemoteFile :: string()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
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.
%%--------------------------------------------------------------------------
-spec send_chunk_start(Pid :: pid(), RemoteFile :: string()) ->
'ok' | {'error', Reason :: restriction_reason() | common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec append_chunk_start(Pid :: pid(), RemoteFile :: string()) ->
'ok' | {'error', Reason :: term()}.
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.
%%--------------------------------------------------------------------------
-spec send_chunk(Pid :: pid(), Bin :: binary()) ->
'ok' |
{'error', Reason :: 'echunk' |
restriction_reason() |
common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec append_chunk(Pid :: pid(), Bin :: binary()) ->
'ok' |
{'error', Reason :: 'echunk' |
restriction_reason() |
common_reason()}.
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.
%%--------------------------------------------------------------------------
-spec send_chunk_end(Pid :: pid()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
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.
%%--------------------------------------------------------------------------
-spec append_chunk_end(Pid :: pid()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
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
%%--------------------------------------------------------------------------
-spec append(Pid :: pid(), LocalFileName :: string()) ->
'ok' |
{'error', Reason :: 'epath' |
'elogin' |
'etnospc' |
'epnospc' |
'efnamena' | common_reason()}.
append(Pid, LocalFileName) ->
append(Pid, LocalFileName, LocalFileName).
-spec append(Pid :: pid(),
LocalFileName :: string(),
RemoteFileName :: string()) ->
'ok' | {'error', Reason :: term()}.
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.
%%--------------------------------------------------------------------------
-spec append_bin(Pid :: pid(),
Bin :: binary(),
RemoteFile :: string()) ->
'ok' |
{'error', Reason :: restriction_reason() |
common_reason() |
shortage_reason()}.
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) -> list()
%% Pid = pid()
%% Cmd = string()
%%
%% Description: Send arbitrary ftp command.
%%--------------------------------------------------------------------------
-spec quote(Pid :: pid(), Cmd :: string()) -> list().
quote(Pid, Cmd) when is_list(Cmd) ->
call(Pid, {quote, Cmd}, atom).
%%--------------------------------------------------------------------------
%% close(Pid) -> ok
%% Pid = pid()
%%
%% Description: End the ftp session.
%%--------------------------------------------------------------------------
-spec close(Pid :: pid()) -> 'ok'.
close(Pid) ->
cast(Pid, close),
ok.
%%--------------------------------------------------------------------------
%% formaterror(Tag) -> string()
%% Tag = atom() | {error, atom()}
%%
%% Description: Return diagnostics.
%%--------------------------------------------------------------------------
-spec formaterror(Tag :: term()) -> string().
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({_, {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).