diff options
Diffstat (limited to 'lib/ssh/src/ssh_sftp.erl')
-rwxr-xr-x | lib/ssh/src/ssh_sftp.erl | 1148 |
1 files changed, 1148 insertions, 0 deletions
diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl new file mode 100755 index 0000000000..cbfa208f6f --- /dev/null +++ b/lib/ssh/src/ssh_sftp.erl @@ -0,0 +1,1148 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-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: SFTP protocol front-end + +-module(ssh_sftp). + +-behaviour(ssh_channel). + +-include_lib("kernel/include/file.hrl"). +-include("ssh.hrl"). +-include("ssh_xfer.hrl"). + +%% API + +-export([start_channel/1, start_channel/2, start_channel/3, stop_channel/1]). + +-export([open/3, opendir/2, close/2, readdir/2, pread/4, read/3, + open/4, opendir/3, close/3, readdir/3, pread/5, read/4, + apread/4, aread/3, pwrite/4, write/3, apwrite/4, awrite/3, + pwrite/5, write/4, + position/3, real_path/2, read_file_info/2, get_file_info/2, + position/4, real_path/3, read_file_info/3, get_file_info/3, + write_file_info/3, read_link_info/2, read_link/2, make_symlink/3, + write_file_info/4, read_link_info/3, read_link/3, make_symlink/4, + rename/3, delete/2, make_dir/2, del_dir/2, send_window/1, + rename/4, delete/3, make_dir/3, del_dir/3, send_window/2, + recv_window/1, list_dir/2, read_file/2, write_file/3, + recv_window/2, list_dir/3, read_file/3, write_file/4]). + +%% Deprecated +-export([connect/1, connect/2, connect/3, stop/1]). + +-deprecated({connect, 1, next_major_release}). +-deprecated({connect, 2, next_major_release}). +-deprecated({connect, 3, next_major_release}). +-deprecated({stop, 1, next_major_release}). + +%% ssh_channel callbacks +-export([init/1, handle_call/3, handle_msg/2, handle_ssh_msg/2, terminate/2]). +%% TODO: Should be placed elsewhere ssh_sftpd should not call functions in ssh_sftp! +-export([info_to_attr/1, attr_to_info/1]). + +-record(state, + { + xf, + rep_buf = <<>>, + req_id, + req_list = [], %% {ReqId, Fun} + inf %% list of fileinf + }). + +-record(fileinf, + { + handle, + offset, + size, + mode + }). + +-define(FILEOP_TIMEOUT, infinity). + +-define(NEXT_REQID(S), + S#state { req_id = (S#state.req_id + 1) band 16#ffffffff}). + +-define(XF(S), S#state.xf). +-define(REQID(S), S#state.req_id). + +%%==================================================================== +%% API +%%==================================================================== +start_channel(Cm) when is_pid(Cm) -> + start_channel(Cm, []); +start_channel(Host) when is_list(Host) -> + start_channel(Host, []). +start_channel(Cm, Opts) when is_pid(Cm) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:attach(Cm, []) of + {ok, ChannelId, Cm} -> + case ssh_channel:start(Cm, ChannelId, + ?MODULE, [Cm, ChannelId, Timeout]) of + {ok, Pid} -> + case wait_for_version_negotiation(Pid, Timeout) of + ok -> + {ok, Pid}; + TimeOut -> + TimeOut + end; + {error, Reason} -> + {error, Reason}; + ignore -> + {error, ignore} + end; + Error -> + Error + end; + +start_channel(Host, Opts) -> + start_channel(Host, 22, Opts). +start_channel(Host, Port, Opts) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:connect(Host, Port, proplists:delete(timeout, Opts)) of + {ok, ChannelId, Cm} -> + case ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, + ChannelId, Timeout]) of + {ok, Pid} -> + case wait_for_version_negotiation(Pid, Timeout) of + ok -> + {ok, Pid, Cm}; + TimeOut -> + TimeOut + end; + {error, Reason} -> + {error, Reason}; + ignore -> + {error, ignore} + end; + Error -> + Error + end. + +stop_channel(Pid) -> + case process_info(Pid, [trap_exit]) of + [{trap_exit, Bool}] -> + process_flag(trap_exit, true), + link(Pid), + exit(Pid, ssh_sftp_stop_channel), + receive + {'EXIT', Pid, normal} -> + ok + after 5000 -> + exit(Pid, kill), + receive + {'EXIT', Pid, killed} -> + ok + end + end, + process_flag(trap_exit, Bool), + ok; + undefined -> + ok + end. + +wait_for_version_negotiation(Pid, Timeout) -> + call(Pid, wait_for_version_negotiation, Timeout). + +open(Pid, File, Mode) -> + open(Pid, File, Mode, ?FILEOP_TIMEOUT). +open(Pid, File, Mode, FileOpTimeout) -> + call(Pid, {open, false, File, Mode}, FileOpTimeout). + +opendir(Pid, Path) -> + opendir(Pid, Path, ?FILEOP_TIMEOUT). +opendir(Pid, Path, FileOpTimeout) -> + call(Pid, {opendir, false, Path}, FileOpTimeout). + +close(Pid, Handle) -> + close(Pid, Handle, ?FILEOP_TIMEOUT). +close(Pid, Handle, FileOpTimeout) -> + call(Pid, {close,false,Handle}, FileOpTimeout). + +readdir(Pid,Handle) -> + readdir(Pid,Handle, ?FILEOP_TIMEOUT). +readdir(Pid,Handle, FileOpTimeout) -> + call(Pid, {readdir,false,Handle}, FileOpTimeout). + +pread(Pid, Handle, Offset, Len) -> + pread(Pid, Handle, Offset, Len, ?FILEOP_TIMEOUT). +pread(Pid, Handle, Offset, Len, FileOpTimeout) -> + call(Pid, {pread,false,Handle, Offset, Len}, FileOpTimeout). + +read(Pid, Handle, Len) -> + read(Pid, Handle, Len, ?FILEOP_TIMEOUT). +read(Pid, Handle, Len, FileOpTimeout) -> + call(Pid, {read,false,Handle, Len}, FileOpTimeout). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +apread(Pid, Handle, Offset, Len) -> + call(Pid, {pread,true,Handle, Offset, Len}, infinity). + +%% TODO this ought to be a cast! +aread(Pid, Handle, Len) -> + call(Pid, {read,true,Handle, Len}, infinity). + +pwrite(Pid, Handle, Offset, Data) -> + pwrite(Pid, Handle, Offset, Data, ?FILEOP_TIMEOUT). +pwrite(Pid, Handle, Offset, Data, FileOpTimeout) -> + call(Pid, {pwrite,false,Handle,Offset,Data}, FileOpTimeout). + +write(Pid, Handle, Data) -> + write(Pid, Handle, Data, ?FILEOP_TIMEOUT). +write(Pid, Handle, Data, FileOpTimeout) -> + call(Pid, {write,false,Handle,Data}, FileOpTimeout). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +apwrite(Pid, Handle, Offset, Data) -> + call(Pid, {pwrite,true,Handle,Offset,Data}, infinity). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +awrite(Pid, Handle, Data) -> + call(Pid, {write,true,Handle,Data}, infinity). + +position(Pid, Handle, Pos) -> + position(Pid, Handle, Pos, ?FILEOP_TIMEOUT). +position(Pid, Handle, Pos, FileOpTimeout) -> + call(Pid, {position, Handle, Pos}, FileOpTimeout). + +real_path(Pid, Path) -> + real_path(Pid, Path, ?FILEOP_TIMEOUT). +real_path(Pid, Path, FileOpTimeout) -> + call(Pid, {real_path, false, Path}, FileOpTimeout). + +read_file_info(Pid, Name) -> + read_file_info(Pid, Name, ?FILEOP_TIMEOUT). +read_file_info(Pid, Name, FileOpTimeout) -> + call(Pid, {read_file_info,false,Name}, FileOpTimeout). + +get_file_info(Pid, Handle) -> + get_file_info(Pid, Handle, ?FILEOP_TIMEOUT). +get_file_info(Pid, Handle, FileOpTimeout) -> + call(Pid, {get_file_info,false,Handle}, FileOpTimeout). + +write_file_info(Pid, Name, Info) -> + write_file_info(Pid, Name, Info, ?FILEOP_TIMEOUT). +write_file_info(Pid, Name, Info, FileOpTimeout) -> + call(Pid, {write_file_info,false,Name, Info}, FileOpTimeout). + +read_link_info(Pid, Name) -> + read_link_info(Pid, Name, ?FILEOP_TIMEOUT). +read_link_info(Pid, Name, FileOpTimeout) -> + call(Pid, {read_link_info,false,Name}, FileOpTimeout). + +read_link(Pid, LinkName) -> + read_link(Pid, LinkName, ?FILEOP_TIMEOUT). +read_link(Pid, LinkName, FileOpTimeout) -> + case call(Pid, {read_link,false,LinkName}, FileOpTimeout) of + {ok, [{Name, _Attrs}]} -> + {ok, Name}; + ErrMsg -> + ErrMsg + end. + +make_symlink(Pid, Name, Target) -> + make_symlink(Pid, Name, Target, ?FILEOP_TIMEOUT). +make_symlink(Pid, Name, Target, FileOpTimeout) -> + call(Pid, {make_symlink,false, Name, Target}, FileOpTimeout). + +rename(Pid, FromFile, ToFile) -> + rename(Pid, FromFile, ToFile, ?FILEOP_TIMEOUT). +rename(Pid, FromFile, ToFile, FileOpTimeout) -> + call(Pid, {rename,false,FromFile, ToFile}, FileOpTimeout). + +delete(Pid, Name) -> + delete(Pid, Name, ?FILEOP_TIMEOUT). +delete(Pid, Name, FileOpTimeout) -> + call(Pid, {delete,false,Name}, FileOpTimeout). + +make_dir(Pid, Name) -> + make_dir(Pid, Name, ?FILEOP_TIMEOUT). +make_dir(Pid, Name, FileOpTimeout) -> + call(Pid, {make_dir,false,Name}, FileOpTimeout). + +del_dir(Pid, Name) -> + del_dir(Pid, Name, ?FILEOP_TIMEOUT). +del_dir(Pid, Name, FileOpTimeout) -> + call(Pid, {del_dir,false,Name}, FileOpTimeout). + +%% TODO : send_window and recv_window - Really needed? Not documented! +%% internal use maybe should be handled in other way! +send_window(Pid) -> + send_window(Pid, ?FILEOP_TIMEOUT). +send_window(Pid, FileOpTimeout) -> + call(Pid, send_window, FileOpTimeout). + +recv_window(Pid) -> + recv_window(Pid, ?FILEOP_TIMEOUT). +recv_window(Pid, FileOpTimeout) -> + call(Pid, recv_window, FileOpTimeout). + + +list_dir(Pid, Name) -> + list_dir(Pid, Name, ?FILEOP_TIMEOUT). + +list_dir(Pid, Name, FileOpTimeout) -> + case opendir(Pid, Name, FileOpTimeout) of + {ok,Handle} -> + Res = do_list_dir(Pid, Handle, FileOpTimeout, []), + close(Pid, Handle, FileOpTimeout), + case Res of + {ok, List} -> + NList = lists:foldl(fun({Nm, _Info},Acc) -> + [Nm|Acc] end, + [], List), + {ok,NList}; + Error -> Error + end; + Error -> + Error + end. + +do_list_dir(Pid, Handle, FileOpTimeout, Acc) -> + case readdir(Pid, Handle, FileOpTimeout) of + {ok, []} -> + {ok, Acc}; + {ok, Names} -> + do_list_dir(Pid, Handle, FileOpTimeout, Acc ++ Names); + eof -> + {ok, Acc}; + Error -> + Error + end. + + +read_file(Pid, Name) -> + read_file(Pid, Name, ?FILEOP_TIMEOUT). + +read_file(Pid, Name, FileOpTimeout) -> + case open(Pid, Name, [read, binary], FileOpTimeout) of + {ok, Handle} -> + {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout), + Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []), + close(Pid, Handle), + Res; + Error -> + Error + end. + +read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, Acc) -> + case read(Pid, Handle, PacketSz, FileOpTimeout) of + {ok, Data} -> + read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, [Data|Acc]); + eof -> + {ok, list_to_binary(lists:reverse(Acc))}; + Error -> + Error + end. + +write_file(Pid, Name, List) -> + write_file(Pid, Name, List, ?FILEOP_TIMEOUT). + +write_file(Pid, Name, List, FileOpTimeout) when is_list(List) -> + write_file(Pid, Name, list_to_binary(List), FileOpTimeout); +write_file(Pid, Name, Bin, FileOpTimeout) -> + case open(Pid, Name, [write, binary], FileOpTimeout) of + {ok, Handle} -> + {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout), + Res = write_file_loop(Pid, Handle, 0, Bin, size(Bin), Packet, + FileOpTimeout), + close(Pid, Handle, FileOpTimeout), + Res; + Error -> + Error + end. + +write_file_loop(_Pid, _Handle, _Pos, _Bin, 0, _PacketSz,_FileOpTimeout) -> + ok; +write_file_loop(Pid, Handle, Pos, Bin, Remain, PacketSz, FileOpTimeout) -> + if Remain >= PacketSz -> + <<_:Pos/binary, Data:PacketSz/binary, _/binary>> = Bin, + case write(Pid, Handle, Data, FileOpTimeout) of + ok -> + write_file_loop(Pid, Handle, + Pos+PacketSz, Bin, Remain-PacketSz, + PacketSz, FileOpTimeout); + Error -> + Error + end; + true -> + <<_:Pos/binary, Data/binary>> = Bin, + write(Pid, Handle, Data, FileOpTimeout) + end. + +%%==================================================================== +%% SSh channel callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} +%% +%% Description: +%%-------------------------------------------------------------------- +init([Cm, ChannelId, Timeout]) -> + erlang:monitor(process, Cm), + case ssh_connection:subsystem(Cm, ChannelId, "sftp", Timeout) of + success -> + Xf = #ssh_xfer{cm = Cm, + channel = ChannelId}, + {ok, #state{xf = Xf, + req_id = 0, + rep_buf = <<>>, + inf = new_inf()}}; + failure -> + {stop, {error, "server failed to start sftp subsystem"}}; + Error -> + {stop, Error} + end. + +%%-------------------------------------------------------------------- +%% Function: handle_call/3 +%% Description: Handling call messages +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%-------------------------------------------------------------------- +handle_call({{timeout, infinity}, wait_for_version_negotiation}, From, + #state{xf = #ssh_xfer{vsn = undefined} = Xf} = State) -> + {noreply, State#state{xf = Xf#ssh_xfer{vsn = From}}}; + +handle_call({{timeout, Timeout}, wait_for_version_negotiation}, From, + #state{xf = #ssh_xfer{vsn = undefined} = Xf} = State) -> + timer:send_after(Timeout, {timeout, undefined, From}), + {noreply, State#state{xf = Xf#ssh_xfer{vsn = From}}}; + +handle_call({_, wait_for_version_negotiation}, _, State) -> + {reply, ok, State}; + +handle_call({{timeout, infinity}, Msg}, From, State) -> + do_handle_call(Msg, From, State); +handle_call({{timeout, Timeout}, Msg}, From, #state{req_id = Id} = State) -> + timer:send_after(Timeout, {timeout, Id, From}), + do_handle_call(Msg, From, State). + +do_handle_call({open, Async,FileName,Mode}, From, #state{xf = XF} = State) -> + {Access,Flags,Attrs} = open_mode(XF#ssh_xfer.vsn, Mode), + ReqID = State#state.req_id, + ssh_xfer:open(XF, ReqID, FileName, Access, Flags, Attrs), + case Async of + true -> + {reply, {async,ReqID}, + update_request_info(ReqID, State, + fun({ok,Handle},State1) -> + open2(ReqID,FileName,Handle,Mode,Async, + From,State1); + (Rep,State1) -> + async_reply(ReqID, Rep, From, State1) + end)}; + false -> + {noreply, + update_request_info(ReqID, State, + fun({ok,Handle},State1) -> + open2(ReqID,FileName,Handle,Mode,Async, + From,State1); + (Rep,State1) -> + sync_reply(Rep, From, State1) + end)} + end; + +do_handle_call({opendir,Async,Path}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:opendir(?XF(State), ReqID, Path), + make_reply(ReqID, Async, From, State); + +do_handle_call({readdir,Async,Handle}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:readdir(?XF(State), ReqID, Handle), + make_reply(ReqID, Async, From, State); + +do_handle_call({close,_Async,Handle}, From, State) -> + %% wait until all operations on handle are done + case get_size(Handle, State) of + undefined -> + ReqID = State#state.req_id, + ssh_xfer:close(?XF(State), ReqID, Handle), + make_reply_post(ReqID, false, From, State, + fun(Rep, State1) -> + {Rep, erase_handle(Handle, State1)} + end); + _ -> + case lseek_position(Handle, cur, State) of + {ok,_} -> + ReqID = State#state.req_id, + ssh_xfer:close(?XF(State), ReqID, Handle), + make_reply_post(ReqID, false, From, State, + fun(Rep, State1) -> + {Rep, erase_handle(Handle, State1)} + end); + Error -> + {reply, Error, State} + end + end; + +do_handle_call({pread,Async,Handle,At,Length}, From, State) -> + case lseek_position(Handle, At, State) of + {ok,Offset} -> + ReqID = State#state.req_id, + ssh_xfer:read(?XF(State),ReqID,Handle,Offset,Length), + %% To get multiple async read to work we must update the offset + %% before the operation begins + State1 = update_offset(Handle, Offset+Length, State), + make_reply_post(ReqID,Async,From,State1, + fun({ok,Data}, State2) -> + case get_mode(Handle, State2) of + binary -> {{ok,Data}, State2}; + text -> + {{ok,binary_to_list(Data)}, State2} + end; + (Rep, State2) -> + {Rep, State2} + end); + Error -> + {reply, Error, State} + end; + +do_handle_call({read,Async,Handle,Length}, From, State) -> + case lseek_position(Handle, cur, State) of + {ok,Offset} -> + ReqID = State#state.req_id, + ssh_xfer:read(?XF(State),ReqID,Handle,Offset,Length), + %% To get multiple async read to work we must update the offset + %% before the operation begins + State1 = update_offset(Handle, Offset+Length, State), + make_reply_post(ReqID,Async,From,State1, + fun({ok,Data}, State2) -> + case get_mode(Handle, State2) of + binary -> {{ok,Data}, State2}; + text -> + {{ok,binary_to_list(Data)}, State2} + end; + (Rep, State2) -> {Rep, State2} + end); + Error -> + {reply, Error, State} + end; + +do_handle_call({pwrite,Async,Handle,At,Data0}, From, State) -> + case lseek_position(Handle, At, State) of + {ok,Offset} -> + Data = if + is_binary(Data0) -> + Data0; + is_list(Data0) -> + list_to_binary(Data0) + end, + ReqID = State#state.req_id, + Size = size(Data), + ssh_xfer:write(?XF(State),ReqID,Handle,Offset,Data), + State1 = update_size(Handle, Offset+Size, State), + make_reply(ReqID, Async, From, State1); + Error -> + {reply, Error, State} + end; + +do_handle_call({write,Async,Handle,Data0}, From, State) -> + case lseek_position(Handle, cur, State) of + {ok,Offset} -> + Data = if + is_binary(Data0) -> + Data0; + is_list(Data0) -> + list_to_binary(Data0) + end, + ReqID = State#state.req_id, + Size = size(Data), + ssh_xfer:write(?XF(State),ReqID,Handle,Offset,Data), + State1 = update_offset(Handle, Offset+Size, State), + make_reply(ReqID, Async, From, State1); + Error -> + {reply, Error, State} + end; + +do_handle_call({position,Handle,At}, _From, State) -> + %% We could make this auto sync when all request to Handle is done? + case lseek_position(Handle, At, State) of + {ok,Offset} -> + {reply, {ok, Offset}, update_offset(Handle, Offset, State)}; + Error -> + {reply, Error, State} + end; + +do_handle_call({rename,Async,FromFile,ToFile}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:rename(?XF(State),ReqID,FromFile,ToFile,[overwrite]), + make_reply(ReqID, Async, From, State); + +do_handle_call({delete,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:remove(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({make_dir,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:mkdir(?XF(State), ReqID, Name, + #ssh_xfer_attr{ type = directory }), + make_reply(ReqID, Async, From, State); + +do_handle_call({del_dir,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:rmdir(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({real_path,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:realpath(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_file_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:stat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({get_file_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:fstat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_link_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:lstat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_link,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:readlink(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({make_symlink, Async, Path, TargetPath}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:symlink(?XF(State), ReqID, Path, TargetPath), + make_reply(ReqID, Async, From, State); + +do_handle_call({write_file_info,Async,Name,Info}, From, State) -> + ReqID = State#state.req_id, + A = info_to_attr(Info), + ssh_xfer:setstat(?XF(State), ReqID, Name, A), + make_reply(ReqID, Async, From, State); + +%% TODO: Do we really want this format? Function send_window +%% is not documented and seems to be used only inernaly! +%% It is backwards compatible for now. +do_handle_call(send_window, _From, State) -> + XF = State#state.xf, + [{send_window,{{win_size, Size0},{packet_size, Size1}}}] = + ssh:channel_info(XF#ssh_xfer.cm, XF#ssh_xfer.channel, [send_window]), + {reply, {ok, {Size0, Size1}}, State}; + +%% TODO: Do we really want this format? Function recv_window +%% is not documented and seems to be used only inernaly! +%% It is backwards compatible for now. +do_handle_call(recv_window, _From, State) -> + XF = State#state.xf, + [{recv_window,{{win_size, Size0},{packet_size, Size1}}}] = + ssh:channel_info(XF#ssh_xfer.cm, XF#ssh_xfer.channel, [recv_window]), + {reply, {ok, {Size0, Size1}}, State}; + +%% Backwards compatible +do_handle_call(stop, _From, State) -> + {stop, shutdown, ok, State}; + +do_handle_call(Call, _From, State) -> + {reply, {error, bad_call, Call, State}, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages +%%-------------------------------------------------------------------- +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, 0, Data}}, #state{rep_buf = Data0} = + State0) -> + State = handle_reply(State0, <<Data0/binary,Data/binary>>), + {ok, State}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, 1, Data}}, State) -> + error_logger:format("ssh: STDERR: ~s\n", [binary_to_list(Data)]), + {ok, State}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, {eof, _ChannelId}}, State) -> + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, + State0) -> + State = reply_all(State0, {error, Error}), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State0) -> + State = reply_all(State0, {error, {exit_status, Status}}), + {stop, ChannelId, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages +%%-------------------------------------------------------------------- +handle_msg({ssh_channel_up, _, _}, #state{xf = Xf} = State) -> + ssh_xfer:protocol_version_request(Xf), + {ok, State}; + +%% Version negotiation timed out +handle_msg({timeout, undefined, From}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + ssh_channel:reply(From, {error, timeout}), + {stop, ChannelId, State}; + +handle_msg({timeout, Id, From}, #state{req_list = ReqList0} = State) -> + case lists:keysearch(Id, 1, ReqList0) of + false -> + {ok, State}; + _ -> + ReqList = lists:keydelete(Id, 1, ReqList0), + ssh_channel:reply(From, {error, timeout}), + {ok, State#state{req_list = ReqList}} + end; + +%% Connection manager goes down +handle_msg({'DOWN', _Ref, _Type, _Process, _}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + {stop, ChannelId, State}; + +%% Stopped by user +handle_msg({'EXIT', _, ssh_sftp_stop_channel}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + {stop, ChannelId, State}; + +handle_msg(_, State) -> + {ok, State}. +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: Called when the channel process is terminated +%%-------------------------------------------------------------------- +%% Backwards compatible +terminate(shutdown, #state{xf = #ssh_xfer{cm = Cm}} = State) -> + reply_all(State, {error, closed}), + ssh:close(Cm); + +terminate(_Reason, State) -> + reply_all(State, {error, closed}). + +%%==================================================================== +%% Internal functions +%%==================================================================== +call(Pid, Msg, TimeOut) -> + ssh_channel:call(Pid, {{timeout, TimeOut}, Msg}, infinity). + +handle_reply(State, <<?UINT32(Len),Reply:Len/binary,Rest/binary>>) -> + do_handle_reply(State, Reply, Rest); +handle_reply(State, Data) -> + State#state{rep_buf = Data}. + +do_handle_reply(#state{xf = Xf} = State, + <<?SSH_FXP_VERSION, ?UINT32(Version), BinExt/binary>>, Rest) -> + Ext = ssh_xfer:decode_ext(BinExt), + case Xf#ssh_xfer.vsn of + undefined -> + ok; + From -> + ssh_channel:reply(From, ok) + end, + State#state{xf = Xf#ssh_xfer{vsn = Version, ext = Ext}, rep_buf = Rest}; + +do_handle_reply(State0, Data, Rest) -> + case catch ssh_xfer:xf_reply(?XF(State0), Data) of + {'EXIT', _Reason} -> + handle_reply(State0, Rest); + XfReply -> + State = handle_req_reply(State0, XfReply), + handle_reply(State, Rest) + end. + +handle_req_reply(State0, {_, ReqID, _} = XfReply) -> + case lists:keysearch(ReqID, 1, State0#state.req_list) of + false -> + State0; + {value,{_,Fun}} -> + List = lists:keydelete(ReqID, 1, State0#state.req_list), + State1 = State0#state { req_list = List }, + case catch Fun(xreply(XfReply),State1) of + {'EXIT', _} -> + State1; + State -> + State + end + end. + +xreply({handle,_,H}) -> {ok, H}; +xreply({data,_,Data}) -> {ok, Data}; +xreply({name,_,Names}) -> {ok, Names}; +xreply({attrs, _, A}) -> {ok, attr_to_info(A)}; +xreply({extended_reply,_,X}) -> {ok, X}; +xreply({status,_,{ok, _Err, _Lang, _Rep}}) -> ok; +xreply({status,_,{eof, _Err, _Lang, _Rep}}) -> eof; +xreply({status,_,{Stat, _Err, _Lang, _Rep}}) -> {error, Stat}; +xreply({Code, _, Reply}) -> {Code, Reply}. + +update_request_info(ReqID, State, Fun) -> + List = [{ReqID,Fun} | State#state.req_list], + ID = (State#state.req_id + 1) band 16#ffffffff, + State#state { req_list = List, req_id = ID }. + +async_reply(ReqID, Reply, _From={To,_}, State) -> + To ! {async_reply, ReqID, Reply}, + State. + +sync_reply(Reply, From, State) -> + catch (ssh_channel:reply(From, Reply)), + State. + +open2(OrigReqID,FileName,Handle,Mode,Async,From,State) -> + I0 = State#state.inf, + FileMode = case lists:member(binary, Mode) orelse lists:member(raw, Mode) of + true -> binary; + false -> text + end, + I1 = add_new_handle(Handle, FileMode, I0), + State0 = State#state{inf = I1}, + ReqID = State0#state.req_id, + ssh_xfer:stat(State0#state.xf, ReqID, FileName, [size]), + case Async of + true -> + update_request_info(ReqID, State0, + fun({ok,FI},State1) -> + Size = FI#file_info.size, + State2 = if is_integer(Size) -> + put_size(Handle, Size, State1); + true -> + State1 + end, + async_reply(OrigReqID, {ok,Handle}, From, State2); + (_, State1) -> + async_reply(OrigReqID, {ok,Handle}, From, State1) + end); + false -> + update_request_info(ReqID, State0, + fun({ok,FI},State1) -> + Size = FI#file_info.size, + State2 = if is_integer(Size) -> + put_size(Handle, Size, State1); + true -> + State1 + end, + sync_reply({ok,Handle}, From, State2); + (_, State1) -> + sync_reply({ok,Handle}, From, State1) + end) + end. + +reply_all(State, Reply) -> + List = State#state.req_list, + lists:foreach(fun({_ReqID,Fun}) -> + catch Fun(Reply,State) + end, List), + State#state {req_list = []}. + +make_reply(ReqID, true, From, State) -> + {reply, {async, ReqID}, + update_request_info(ReqID, State, + fun(Reply,State1) -> + async_reply(ReqID,Reply,From,State1) + end)}; + +make_reply(ReqID, false, From, State) -> + {noreply, + update_request_info(ReqID, State, + fun(Reply,State1) -> + sync_reply(Reply, From, State1) + end)}. + +make_reply_post(ReqID, true, From, State, PostFun) -> + {reply, {async, ReqID}, + update_request_info(ReqID, State, + fun(Reply,State1) -> + case catch PostFun(Reply, State1) of + {'EXIT',_} -> + async_reply(ReqID,Reply, From, State1); + {Reply1, State2} -> + async_reply(ReqID,Reply1, From, State2) + end + end)}; + +make_reply_post(ReqID, false, From, State, PostFun) -> + {noreply, + update_request_info(ReqID, State, + fun(Reply,State1) -> + case catch PostFun(Reply, State1) of + {'EXIT',_} -> + sync_reply(Reply, From, State1); + {Reply1, State2} -> + sync_reply(Reply1, From, State2) + end + end)}. + +%% convert: file_info -> ssh_xfer_attr +info_to_attr(I) when is_record(I, file_info) -> + #ssh_xfer_attr { permissions = I#file_info.mode, + size = I#file_info.size, + type = I#file_info.type, + owner = I#file_info.uid, + group = I#file_info.gid, + atime = datetime_to_unix(I#file_info.atime), + mtime = datetime_to_unix(I#file_info.mtime), + createtime = datetime_to_unix(I#file_info.ctime)}. + +%% convert: ssh_xfer_attr -> file_info +attr_to_info(A) when is_record(A, ssh_xfer_attr) -> + #file_info{ + size = A#ssh_xfer_attr.size, + type = A#ssh_xfer_attr.type, + access = read_write, %% FIXME: read/write/read_write/none + atime = unix_to_datetime(A#ssh_xfer_attr.atime), + mtime = unix_to_datetime(A#ssh_xfer_attr.mtime), + ctime = unix_to_datetime(A#ssh_xfer_attr.createtime), + mode = A#ssh_xfer_attr.permissions, + links = 1, + major_device = 0, + minor_device = 0, + inode = 0, + uid = A#ssh_xfer_attr.owner, + gid = A#ssh_xfer_attr.group}. + + +%% Added workaround for sftp timestam problem. (Timestamps should be +%% in UTC but they where not) . The workaround uses a deprecated +%% function i calandar. This will work as expected most of the time +%% but has problems for the same reason as +%% calendar:local_time_to_universal_time/1. We consider it better that +%% the timestamps work as expected most of the time instead of none of +%% the time. Hopfully the file-api will be updated so that we can +%% solve this problem in a better way in the future. + +unix_to_datetime(undefined) -> + undefined; +unix_to_datetime(UTCSecs) -> + UTCDateTime = + calendar:gregorian_seconds_to_datetime(UTCSecs + 62167219200), + erlang:universaltime_to_localtime(UTCDateTime). + +datetime_to_unix(undefined) -> + undefined; +datetime_to_unix(LocalDateTime) -> + UTCDateTime = erlang:localtime_to_universaltime(LocalDateTime), + calendar:datetime_to_gregorian_seconds(UTCDateTime) - 62167219200. + + +open_mode(Vsn,Modes) when Vsn >= 5 -> + open_mode5(Modes); +open_mode(_Vsn, Modes) -> + open_mode3(Modes). + +open_mode5(Modes) -> + A = #ssh_xfer_attr{type = regular}, + {Fl, Ac} = case {lists:member(write, Modes), + lists:member(read, Modes), + lists:member(append, Modes)} of + {_, _, true} -> + {[append_data], + [read_attributes, + append_data, write_attributes]}; + {true, false, false} -> + {[create_truncate], + [write_data, write_attributes]}; + {true, true, _} -> + {[open_or_create], + [read_data, read_attributes, + write_data, write_attributes]}; + {false, true, _} -> + {[open_existing], + [read_data, read_attributes]} + end, + {Ac, Fl, A}. + +open_mode3(Modes) -> + A = #ssh_xfer_attr{type = regular}, + Fl = case {lists:member(write, Modes), + lists:member(read, Modes), + lists:member(append, Modes)} of + {_, _, true} -> + [append]; + {true, false, false} -> + [write, creat, trunc]; + {true, true, _} -> + [read, write]; + {false, true, _} -> + [read] + end, + {[], Fl, A}. + +%% accessors for inf dict +new_inf() -> dict:new(). + +add_new_handle(Handle, FileMode, Inf) -> + dict:store(Handle, #fileinf{offset=0, size=0, mode=FileMode}, Inf). + +update_size(Handle, NewSize, State) -> + OldSize = get_size(Handle, State), + if NewSize > OldSize -> + put_size(Handle, NewSize, State); + true -> + State + end. + +%% set_offset(Handle, NewOffset) -> +%% put({offset,Handle}, NewOffset). + +update_offset(Handle, NewOffset, State0) -> + State1 = put_offset(Handle, NewOffset, State0), + update_size(Handle, NewOffset, State1). + +%% access size and offset for handle +put_size(Handle, Size, State) -> + Inf0 = State#state.inf, + case dict:find(Handle, Inf0) of + {ok, FI} -> + State#state{inf=dict:store(Handle, FI#fileinf{size=Size}, Inf0)}; + _ -> + State#state{inf=dict:store(Handle, #fileinf{size=Size,offset=0}, + Inf0)} + end. + +put_offset(Handle, Offset, State) -> + Inf0 = State#state.inf, + case dict:find(Handle, Inf0) of + {ok, FI} -> + State#state{inf=dict:store(Handle, FI#fileinf{offset=Offset}, + Inf0)}; + _ -> + State#state{inf=dict:store(Handle, #fileinf{size=Offset, + offset=Offset}, Inf0)} + end. + +get_size(Handle, State) -> + case dict:find(Handle, State#state.inf) of + {ok, FI} -> + FI#fileinf.size; + _ -> + undefined + end. + +%% get_offset(Handle, State) -> +%% {ok, FI} = dict:find(Handle, State#state.inf), +%% FI#fileinf.offset. + +get_mode(Handle, State) -> + case dict:find(Handle, State#state.inf) of + {ok, FI} -> + FI#fileinf.mode; + _ -> + undefined + end. + +erase_handle(Handle, State) -> + FI = dict:erase(Handle, State#state.inf), + State#state{inf = FI}. + +%% +%% Caluclate a integer offset +%% +lseek_position(Handle, Pos, State) -> + case dict:find(Handle, State#state.inf) of + {ok, #fileinf{offset=O, size=S}} -> + lseek_pos(Pos, O, S); + _ -> + {error, einval} + end. + +lseek_pos(_Pos, undefined, _) -> + {error, einval}; +lseek_pos(Pos, _CurOffset, _CurSize) + when is_integer(Pos) andalso 0 =< Pos andalso Pos < ?SSH_FILEXFER_LARGEFILESIZE -> + {ok,Pos}; +lseek_pos(bof, _CurOffset, _CurSize) -> + {ok,0}; +lseek_pos(cur, CurOffset, _CurSize) -> + {ok,CurOffset}; +lseek_pos(eof, _CurOffset, CurSize) -> + {ok,CurSize}; +lseek_pos({bof, Offset}, _CurOffset, _CurSize) + when is_integer(Offset) andalso 0 =< Offset andalso Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + {ok, Offset}; +lseek_pos({cur, Offset}, CurOffset, _CurSize) + when is_integer(Offset) andalso -(?SSH_FILEXFER_LARGEFILESIZE) =< Offset andalso + Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + NewOffset = CurOffset + Offset, + if NewOffset < 0 -> + {ok, 0}; + true -> + {ok, NewOffset} + end; +lseek_pos({eof, Offset}, _CurOffset, CurSize) + when is_integer(Offset) andalso -(?SSH_FILEXFER_LARGEFILESIZE) =< Offset andalso + Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + NewOffset = CurSize + Offset, + if NewOffset < 0 -> + {ok, 0}; + true -> + {ok, NewOffset} + end; +lseek_pos(_, _, _) -> + {error, einval}. + + +%%%%%% Deprecated %%%% +connect(Cm) when is_pid(Cm) -> + connect(Cm, []); +connect(Host) when is_list(Host) -> + connect(Host, []). +connect(Cm, Opts) when is_pid(Cm) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:attach(Cm, []) of + {ok, ChannelId, Cm} -> + ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, ChannelId, + Timeout]); + Error -> + Error + end; +connect(Host, Opts) -> + connect(Host, 22, Opts). +connect(Host, Port, Opts) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:connect(Host, Port, proplists:delete(timeout, Opts)) of + {ok, ChannelId, Cm} -> + ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, + ChannelId, Timeout]); + Error -> + Error + end. + + +stop(Pid) -> + call(Pid, stop, infinity). + |