Skip to content

Commit 88c5423

Browse files
committed
Prototype new upload implementation using SFTP
1 parent 9051dca commit 88c5423

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

examples/upload.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{:ok, conn} = SSHKit.connect("127.0.0.1", port: 2222, user: "deploy", password: "deploy", silently_accept_hosts: true)
2+
3+
source = "test/fixtures"
4+
target = "/tmp/fixtures"
5+
6+
:ok = SSHKit.upload(conn, source, target, recursive: true)
7+
8+
:ok = SSHKit.close(conn)

lib/sshkit.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule SSHKit do
2222

2323
alias SSHKit.Context
2424
alias SSHKit.Host
25+
alias SSHKit.Upload
2526

2627
@doc """
2728
TODO
@@ -245,8 +246,12 @@ defmodule SSHKit do
245246
|> SSHKit.upload("local.txt", as: "remote.txt")
246247
```
247248
"""
248-
def upload(context, source, options \\ []) do
249-
# TODO
249+
def upload(conn, source, target, options \\ []) do
250+
upload = Upload.init(source, target, options)
251+
252+
with {:ok, upload} <- Upload.start(upload, conn) do
253+
Upload.loop(upload)
254+
end
250255
end
251256

252257
@doc ~S"""
@@ -280,7 +285,7 @@ defmodule SSHKit do
280285
|> SSHKit.download("remote.txt", as: "local.txt")
281286
```
282287
"""
283-
def download(context, source, options \\ []) do
288+
def download(conn, source, options \\ []) do
284289
# TODO
285290
end
286291
end

lib/sshkit/upload.ex

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
defmodule SSHKit.Upload do
2+
@moduledoc """
3+
TODO
4+
"""
5+
6+
defstruct [:source, :target, :options, :cwd, :stack, :channel]
7+
8+
def init(source, target, options \\ []) do
9+
%__MODULE__{source: Path.expand(source), target: target, options: options}
10+
end
11+
12+
def start(%__MODULE__{} = upload, connection) do
13+
with {:ok, upload} <- prepare(upload) do
14+
{:ok, channel} = :ssh_sftp.start_channel(connection.ref) # accepts options like timeout… http://erlang.org/doc/man/ssh_sftp.html#start_channel-1
15+
{:ok, %{upload | channel: channel}}
16+
end
17+
end
18+
19+
defp prepare(%__MODULE__{source: source, options: options} = upload) do
20+
# TODO: Support globs, https://hexdocs.pm/elixir/Path.html#wildcard/2
21+
if !Keyword.get(options, :recursive, false) && File.dir?(source) do
22+
{:error, "Option :recursive not specified, but local file is a directory (#{source})"} # TODO: Better error
23+
else
24+
{:ok, %{upload | cwd: Path.dirname(source), stack: [[Path.basename(source)]]}}
25+
end
26+
end
27+
28+
def stop(%__MODULE__{channel: nil} = upload), do: {:ok, upload}
29+
def stop(%__MODULE__{channel: channel} = upload) do
30+
with :ok <- :ssh_sftp.stop_channel(channel) do
31+
{:ok, %{upload | channel: nil}}
32+
end
33+
end
34+
35+
# TODO: Handle unstarted uploads w/o channel, cwd, stack… and provide helpful error?
36+
37+
def continue(%__MODULE__{stack: []} = upload) do
38+
{:ok, upload}
39+
end
40+
41+
def continue(%__MODULE__{stack: [[] | paths]} = upload) do
42+
{:ok, %{upload | cwd: Path.dirname(upload.cwd), stack: paths}}
43+
end
44+
45+
def continue(%__MODULE__{stack: [[name | rest] | paths]} = upload) do
46+
path = Path.join(upload.cwd, name)
47+
relpath = Path.relative_to(path, Path.expand(upload.source))
48+
relpath = if relpath == path, do: ".", else: relpath
49+
50+
remote =
51+
upload.target
52+
|> Path.join(relpath)
53+
|> Path.expand()
54+
55+
with {:ok, stat} <- File.stat(path, time: :posix) do
56+
# TODO: Set timestamps… if :preserve option is true, http://erlang.org/doc/man/ssh_sftp.html#write_file_info-3
57+
58+
channel = upload.channel
59+
60+
case stat.type do
61+
:directory ->
62+
# TODO: Timeouts
63+
:ok = :ssh_sftp.make_dir(channel, remote)
64+
{:ok, names} = File.ls(path)
65+
{:ok, %{upload | cwd: path, stack: [names | [rest | paths]]}}
66+
67+
:regular ->
68+
# TODO: Timeouts
69+
{:ok, handle} = :ssh_sftp.open(channel, remote, [:write, :binary])
70+
71+
path
72+
|> File.stream!([], 16_384)
73+
|> Stream.each(fn data -> :ok = :ssh_sftp.write(channel, handle, data) end)
74+
|> Stream.run()
75+
76+
:ok = :ssh_sftp.close(channel, handle)
77+
{:ok, %{upload | stack: [rest | paths]}}
78+
79+
:symlink ->
80+
nil
81+
82+
_ ->
83+
{:error, {:unkown_file_type, path}}
84+
end
85+
end
86+
end
87+
88+
def loop(%__MODULE__{stack: []}) do
89+
:ok
90+
end
91+
92+
def loop(%__MODULE__{} = upload) do
93+
case continue(upload) do
94+
{:ok, upload} ->
95+
loop(upload)
96+
97+
error ->
98+
error
99+
end
100+
end
101+
102+
def done?(%__MODULE__{stack: []}), do: true
103+
def done?(%__MODULE__{}), do: false
104+
end

0 commit comments

Comments
 (0)