Awlexus
Streaming xml file over ftp fails if I don't convert it into a list first
I’m trying to stream an xml file over ftp with Stream.resource/3 and SweetXml.stream_tags!/3, but I’m running into a strange issue where sweet_xml raises an error, before chunk arrives, unless I call Enum.to_list/1.
# Function definitions
def stream_file!(client) do
Stream.resource(fn -> start_file_stream(client) end, &next_file_chunk/1, &stop_file_stream/1)
end
defp start_file_stream(client) do
Logger.debug("Requesting to stream file")
with :ok <- :ftp.recv_chunk_start(client.conn, to_charlist(client.credentials.file_path)) do
client
end
end
defp next_file_chunk(%{conn: conn} = client) do
case :ftp.recv_chunk(conn) do
{:ok, binary} ->
Logger.debug("Stream file chunk #{byte_size(binary)} bytes)")
{[binary], client}
:ok ->
Logger.debug("Transfer completed successfully")
{:halt, client}
error ->
Logger.error(
credentials_id: client.credentials.id,
message: "Unable to stream file",
reason: error
)
{:halt, error}
end
end
defp next_file_chunk(error), do: {:halt, error}
defp stop_file_stream(_), do: []
# Code that is executed
# conn is the pid from :ftp.open/2
# Credentials holds infos about the file and the credentials for the ftp server
client = %{conn: conn, credentials: credentials}
client
|> stream_file!()
# For debugging
|> Stream.each(&IO.inspect(&1, label: :chunk))
# |> Enum.to_list()
|> SweetXml.stream_tags!(:Mitglied, discard: [:Mitglied])
|> Stream.map(fn {:Mitglied, elem} ->
# parse data here
end)
|> Stream.run()
Calling this code produces the following output. If I uncomment |> Enum.to_list() it will work fine and print all the chunks as they come. Any idea what might cause this?
[debug] Requesting to stream file
[error] 3917- fatal: :expected_element_start_tag
** (SweetXml.XmerlFatal) :expected_element_start_tag
(sweet_xml 0.7.3) lib/sweet_xml.ex:539: anonymous fn/1 in SweetXml.stream!/2
(elixir 1.14.4) lib/stream.ex:1619: Stream.do_resource/5
(elixir 1.14.4) lib/stream.ex:1813: Enumerable.Stream.do_each/4
(elixir 1.14.4) lib/stream.ex:689: Stream.run/1
For reference, I have a separate implementation for streaming files over sftp with sftp_client, which works perfectly. I’ve looked into their implementation, but couldn’t find any meaningful difference.
Marked As Solved
Awlexus
Then I’ll just take this advice and just admit defeat. Enum.to_list may stay here for now
Also Liked
D4no0
Yeah sftp erlang client works pretty much flawlessly, the standard is sound and almost all servers use pretty much the same version (v3 sftp, v2 ssh if I remember well), so I would recommend to use that instead of calling an external process, because there is always the risk in a long-running system that you will have some kind process leaks at some point.
Awlexus
The erlang ftp module does not allow you to set the chunksize (or at least I couldn’t find it in their docs), so I assume it’s sftp
dimitarvp
No clear-and-cut answer but I would very likely opt for having N background rclone processes waiting (a pool for Oban workers for example) and then enforce uniqueness via Oban’s mechanism so e.g. you can’t write to the same file from 2 or more workers. That’s trivially achievable.
The tricky part is of course if somebody wants the file upload to be synchronous but IIRC Oban offers that as well? (I think you can send a job and wait on it?)
BTW when it comes to sFTP in particular I think Erlang is slightly better equipped but don’t quote me on this, I only used a library for it once and it was a few years ago. I’d still try work with a normal library and only reach for an external tool if things don’t work stably – as it happened with OP.
But you also said you don’t want to tie your application’s logic to the transport protocol. Hm, in that case I’d probably choose ex_aws_s3 because you can still use it not only for Amazon S3 like Cloudflare’s B2 but also for self-hosted software like MinIO.







