From 676d717d9a4a9786e8e477911fcccd689e7db3ca Mon Sep 17 00:00:00 2001 From: Rodolfo Silva Date: Thu, 18 Dec 2025 17:10:36 -0300 Subject: [PATCH 1/4] feat: add transfer_complete callback to storage connectors --- lib/ex_ftp/storage/common.ex | 1 + lib/ex_ftp/storage/file_connector.ex | 7 +++++++ lib/ex_ftp/storage/s3_connector.ex | 9 ++++++++- lib/ex_ftp/storage_connector.ex | 23 +++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/ex_ftp/storage/common.ex b/lib/ex_ftp/storage/common.ex index acdeae3..307a2cb 100644 --- a/lib/ex_ftp/storage/common.ex +++ b/lib/ex_ftp/storage/common.ex @@ -398,6 +398,7 @@ defmodule ExFTP.Storage.Common do {:ok, stream} -> PassiveSocket.write(pasv, stream, close_after_write: true) send_resp(@closing_connection_success, "Transfer complete.", socket) + connector.transfer_complete(w_path, connector_state) _ -> send_resp(@action_aborted, "File not found.", socket) diff --git a/lib/ex_ftp/storage/file_connector.ex b/lib/ex_ftp/storage/file_connector.ex index 63735fa..bc1419d 100644 --- a/lib/ex_ftp/storage/file_connector.ex +++ b/lib/ex_ftp/storage/file_connector.ex @@ -404,6 +404,13 @@ defmodule ExFTP.Storage.FileConnector do end end + @impl StorageConnector + @spec transfer_complete( + path :: ExFTP.StorageConnector.path(), + connector_state :: ExFTP.StorageConnector.connector_state() + ) :: any() + def transfer_complete(_path, _connector_state), do: :ok + defp rmrf_dir("/"), do: {:error, "Not something to delete"} defp rmrf_dir(dir) do diff --git a/lib/ex_ftp/storage/s3_connector.ex b/lib/ex_ftp/storage/s3_connector.ex index 874b9bf..b78d39c 100644 --- a/lib/ex_ftp/storage/s3_connector.ex +++ b/lib/ex_ftp/storage/s3_connector.ex @@ -287,7 +287,7 @@ defmodule ExFTP.Storage.S3Connector do * **path** :: `t:ExFTP.StorageConnector.path/0` * **connector_state** :: `t:ExFTP.StorageConnector.connector_state/0` - #{ExFTP.Doc.returns(success: "{:ok, data}", failure: "{:error, err}")} + #{ExFTP.Doc.returns(success: "{:ok, data}", failure: "{:error, err}")} #{ExFTP.Doc.related(["`c:ExFTP.StorageConnector.get_content/2`"])} @@ -392,6 +392,13 @@ defmodule ExFTP.Storage.S3Connector do end end + @impl StorageConnector + @spec transfer_complete( + path :: ExFTP.StorageConnector.path(), + connector_state :: ExFTP.StorageConnector.connector_state() + ) :: any() + def transfer_complete(_path, _connector_state), do: :ok + defp clean_path(path) do path |> Path.join("") diff --git a/lib/ex_ftp/storage_connector.ex b/lib/ex_ftp/storage_connector.ex index b8905da..2a2dc5c 100644 --- a/lib/ex_ftp/storage_connector.ex +++ b/lib/ex_ftp/storage_connector.ex @@ -464,4 +464,27 @@ defmodule ExFTP.StorageConnector do """ @callback create_write_func(path, connector_state, opts :: list()) :: function() + + @doc """ + Called when a transfer is complete + + + ### 🏷️ Params + * **path** :: `t:path/0` + * **connector_state** :: `t:connector_state/0` + + #{ExFTP.Doc.returns(success: "data")} + + ### 💻 Examples + + iex> alias ExFTP.Storage.FileConnector + iex> connector_state = %{current_working_directory: "/"} + iex> dir = File.cwd!() + iex> FileConnector.transfer_complete(dir, connector_state) + + #{ExFTP.Doc.resources("page-30")} + + + """ + @callback transfer_complete(path, connector_state) :: any() end From 6a84bbfa77329998084156684e3422435f265ac0 Mon Sep 17 00:00:00 2001 From: Rodolfo Silva Date: Fri, 19 Dec 2025 06:37:51 -0300 Subject: [PATCH 2/4] feat: add mimic for testing and update transfer_complete signature in storage connectors --- lib/ex_ftp/storage/common.ex | 3 ++- lib/ex_ftp/storage/file_connector.ex | 3 ++- lib/ex_ftp/storage/s3_connector.ex | 3 ++- lib/ex_ftp/storage_connector.ex | 19 +++++++++++++++++-- mix.exs | 1 + mix.lock | 2 ++ test/ex_ftp/storage/file_connector_test.exs | 12 ++++++++++++ test/test_helper.exs | 3 +++ 8 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/ex_ftp/storage/common.ex b/lib/ex_ftp/storage/common.ex index 307a2cb..1f8c3b3 100644 --- a/lib/ex_ftp/storage/common.ex +++ b/lib/ex_ftp/storage/common.ex @@ -398,7 +398,7 @@ defmodule ExFTP.Storage.Common do {:ok, stream} -> PassiveSocket.write(pasv, stream, close_after_write: true) send_resp(@closing_connection_success, "Transfer complete.", socket) - connector.transfer_complete(w_path, connector_state) + connector.transfer_complete(:retrieve, w_path, connector_state) _ -> send_resp(@action_aborted, "File not found.", socket) @@ -509,6 +509,7 @@ defmodule ExFTP.Storage.Common do ) ExFTP.Common.send_resp(@closing_connection_success, "Transfer Complete.", socket) + connector.transfer_complete(:store, w_path, connector_state) connector_state end diff --git a/lib/ex_ftp/storage/file_connector.ex b/lib/ex_ftp/storage/file_connector.ex index bc1419d..3cd6a86 100644 --- a/lib/ex_ftp/storage/file_connector.ex +++ b/lib/ex_ftp/storage/file_connector.ex @@ -406,10 +406,11 @@ defmodule ExFTP.Storage.FileConnector do @impl StorageConnector @spec transfer_complete( + type :: ExFTP.StorageConnector.transfer_type(), path :: ExFTP.StorageConnector.path(), connector_state :: ExFTP.StorageConnector.connector_state() ) :: any() - def transfer_complete(_path, _connector_state), do: :ok + def transfer_complete(_type, _path, _connector_state), do: :ok defp rmrf_dir("/"), do: {:error, "Not something to delete"} diff --git a/lib/ex_ftp/storage/s3_connector.ex b/lib/ex_ftp/storage/s3_connector.ex index b78d39c..e10e88a 100644 --- a/lib/ex_ftp/storage/s3_connector.ex +++ b/lib/ex_ftp/storage/s3_connector.ex @@ -394,10 +394,11 @@ defmodule ExFTP.Storage.S3Connector do @impl StorageConnector @spec transfer_complete( + type :: ExFTP.StorageConnector.transfer_type(), path :: ExFTP.StorageConnector.path(), connector_state :: ExFTP.StorageConnector.connector_state() ) :: any() - def transfer_complete(_path, _connector_state), do: :ok + def transfer_complete(_type, _path, _connector_state), do: :ok defp clean_path(path) do path diff --git a/lib/ex_ftp/storage_connector.ex b/lib/ex_ftp/storage_connector.ex index 2a2dc5c..7906481 100644 --- a/lib/ex_ftp/storage_connector.ex +++ b/lib/ex_ftp/storage_connector.ex @@ -357,6 +357,20 @@ defmodule ExFTP.StorageConnector do type: :directory | :symlink | :file } + @typedoc """ + The type of transfer being completed + + + ### 🏷️ Values + * **:retrieve** :: Retrieval of a file from storage + * **:store** :: Storage of a file to storage + + #{ExFTP.Doc.resources()} + + + """ + @type transfer_type :: :retrieve | :store + @doc """ Returns a list of `t:content_info/0` representing each object in a given directory @@ -470,6 +484,7 @@ defmodule ExFTP.StorageConnector do ### 🏷️ Params + * **type** :: `t:transfer_type/0` * **path** :: `t:path/0` * **connector_state** :: `t:connector_state/0` @@ -480,11 +495,11 @@ defmodule ExFTP.StorageConnector do iex> alias ExFTP.Storage.FileConnector iex> connector_state = %{current_working_directory: "/"} iex> dir = File.cwd!() - iex> FileConnector.transfer_complete(dir, connector_state) + iex> FileConnector.transfer_complete(:retrieve, dir, connector_state) #{ExFTP.Doc.resources("page-30")} """ - @callback transfer_complete(path, connector_state) :: any() + @callback transfer_complete(type :: transfer_type(), path :: path(), connector_state :: connector_state()) :: any() end diff --git a/mix.exs b/mix.exs index f489639..1af7d22 100644 --- a/mix.exs +++ b/mix.exs @@ -125,6 +125,7 @@ defmodule ExFTP.MixProject do {:excoveralls, "~> 0.18", only: [:test]}, {:ex_machina, "~> 2.8.0", only: :test}, {:faker, "~> 0.18.0", only: :test}, + {:mimic, "~> 2.0", only: :test}, {:req, "~> 0.5"}, {:junit_formatter, "~> 3.1", only: [:test]}, {:ex_aws, "~> 2.0"}, diff --git a/mix.lock b/mix.lock index 3053627..6ed762f 100644 --- a/mix.lock +++ b/mix.lock @@ -19,6 +19,7 @@ "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "ham": {:hex, :ham, "0.3.2", "02ae195f49970ef667faf9d01bc454fb80909a83d6c775bcac724ca567aeb7b3", [:mix], [], "hexpm", "b71cc684c0e5a3d32b5f94b186770551509e93a9ae44ca1c1a313700f2f6a69a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -30,6 +31,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mimic": {:hex, :mimic, "2.2.0", "32a0ac9d3e98ac1edbceb770e7c524331fbfc43ca341cf2fe087a508e57e015c", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "c9766036a11f024fe922a435f851d3e3a7b1da65125b98fb5e36ed792891c45c"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, diff --git a/test/ex_ftp/storage/file_connector_test.exs b/test/ex_ftp/storage/file_connector_test.exs index dce5884..4d6e707 100644 --- a/test/ex_ftp/storage/file_connector_test.exs +++ b/test/ex_ftp/storage/file_connector_test.exs @@ -2,6 +2,7 @@ defmodule ExFTP.Storage.FileConnectorTest do @moduledoc false use ExUnit.Case + use Mimic import ExFTP.StorageTester import ExFTP.TestHelper @@ -14,6 +15,8 @@ defmodule ExFTP.Storage.FileConnectorTest do doctest Common doctest FileConnector + setup :set_mimic_global + setup do Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) Application.put_env(:ex_ftp, :authenticator_config, %{}) @@ -154,10 +157,15 @@ defmodule ExFTP.Storage.FileConnectorTest do # CWD w_dir w_dir = File.cwd!() + FileConnector + |> stub(:transfer_complete, fn _type, _path, _connector_state -> :stub end) + paths_to_download = w_dir |> File.ls!() |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) test_retr(state, w_dir, paths_to_download) + + assert length(calls(&FileConnector.transfer_complete/3)) > 1 end test "SIZE", state do @@ -171,6 +179,9 @@ defmodule ExFTP.Storage.FileConnectorTest do w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) on_exit(fn -> File.rm_rf!(w_dir) end) + FileConnector + |> stub(:transfer_complete, fn _type, _path, _connector_state -> :stub end) + files_to_store = File.cwd!() |> File.ls!() @@ -179,5 +190,6 @@ defmodule ExFTP.Storage.FileConnectorTest do refute Enum.empty?(files_to_store) test_stor(state, w_dir, files_to_store) + assert length(calls(&FileConnector.transfer_complete/3)) > 1 end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 690ad1d..23a1b7b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,6 @@ +Mimic.copy(ExFTP.Storage.FileConnector) +Mimic.copy(ExFTP.Storage.S3Connector) + ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) ExUnit.start() From 00d01cba25046af004d19dfb35aef4de6a4dd4b6 Mon Sep 17 00:00:00 2001 From: Cam Cook Date: Sat, 27 Dec 2025 11:12:37 -0500 Subject: [PATCH 3/4] feat: can include on_transfer_complete in storage_config --- README.md | 48 +++++- lib/ex_ftp/storage/common.ex | 16 +- lib/ex_ftp/storage/file_connector.ex | 8 - lib/ex_ftp/storage/s3_connector.ex | 8 - lib/ex_ftp/storage_connector.ex | 38 ----- lib/ex_ftp/worker.ex | 13 +- mix.exs | 1 - mix.lock | 12 +- test/ex_ftp/storage/file_connector_test.exs | 12 -- .../ex_ftp/storage/transfer_complete_test.exs | 150 ++++++++++++++++++ test/test_helper.exs | 3 - 11 files changed, 224 insertions(+), 85 deletions(-) create mode 100644 test/ex_ftp/storage/transfer_complete_test.exs diff --git a/README.md b/README.md index 6a1e4ac..bdf8e01 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,15 @@ config :ex_ftp, }, # See "Choose a Storage Connector" storage_connector: ExFTP.Storage.FileConnector, - storage_config: %{} + storage_config: %{ + # Optional: Handler for file transfer completion notifications + # If not configured, logs "Transfer complete: " by default + # Called as: module.function(type, path, connector_state) + # - type: :retrieve (download) or :store (upload) + # - path: the file path + # - connector_state: contains current_working_directory and authenticator_state + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} + } ``` @@ -446,7 +454,33 @@ This is the out-of-the-box behavior you'd expect from any FTP server. config :ex_ftp, #.... storage_connector: ExFTP.Storage.FileConnector, - storage_config: %{} + storage_config: %{ + # Optional: Handler for transfer completion notifications + # Default: Logs "Transfer complete: " + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} + } +``` + +**Example Handler:** + +```elixir +defmodule MyApp.TransferHandler do + require Logger + + def handle_complete(type, path, connector_state) do + # type is :retrieve or :store + # path is the file path + # connector_state contains authenticator_state with username and other metadata + username = connector_state.authenticator_state.username + Logger.info("User #{username} #{type}: #{path}") + + # Trigger custom workflows + case type do + :store -> MyApp.notify_upload(username, path) + :retrieve -> MyApp.track_download(username, path) + end + end +end ``` [^ top](#top) @@ -471,7 +505,9 @@ config :ex_ftp, storage_connector: ExFTP.Storage.S3Connector, storage_config: %{ # the `/` path of the FTP server will point to s3://{my-storage-bucket}/ - storage_bucket: "my-storage-bucket" + storage_bucket: "my-storage-bucket", + # Optional: Handler for transfer completion notifications + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} } ``` @@ -513,7 +549,9 @@ config :ex_ftp, storage_connector: ExFTP.Storage.S3Connector, storage_config: %{ # the `/` path of the FTP server will point to s3://{my-storage-bucket}/ - storage_bucket: "my-storage-bucket" + storage_bucket: "my-storage-bucket", + # Optional: Handler for transfer completion notifications + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} } ``` @@ -635,7 +673,7 @@ defmodule MyStorageConnector do ) :: function() def create_write_func(path, connector_state, opts \\ []) do # Return a function that will write `stream` to your storage at path - # e.g + # e.g # fn stream -> # fs = File.stream!(path) # diff --git a/lib/ex_ftp/storage/common.ex b/lib/ex_ftp/storage/common.ex index 1f8c3b3..246d717 100644 --- a/lib/ex_ftp/storage/common.ex +++ b/lib/ex_ftp/storage/common.ex @@ -16,6 +16,8 @@ defmodule ExFTP.Storage.Common do alias ExFTP.PassiveSocket + require Logger + @directory_action_ok 257 @directory_action_not_taken 521 @file_action_ok 250 @@ -398,7 +400,7 @@ defmodule ExFTP.Storage.Common do {:ok, stream} -> PassiveSocket.write(pasv, stream, close_after_write: true) send_resp(@closing_connection_success, "Transfer complete.", socket) - connector.transfer_complete(:retrieve, w_path, connector_state) + notify_transfer_complete(:retrieve, w_path, connector_state) _ -> send_resp(@action_aborted, "File not found.", socket) @@ -509,7 +511,7 @@ defmodule ExFTP.Storage.Common do ) ExFTP.Common.send_resp(@closing_connection_success, "Transfer Complete.", socket) - connector.transfer_complete(:store, w_path, connector_state) + notify_transfer_complete(:store, w_path, connector_state) connector_state end @@ -695,4 +697,14 @@ defmodule ExFTP.Storage.Common do {key, val} end) end + + defp notify_transfer_complete(type, path, connector_state) do + case connector_state[:on_transfer_complete] do + {module, function} when is_atom(module) and is_atom(function) -> + apply(module, function, [type, path, connector_state]) + + _ -> + Logger.info("Transfer complete: #{type} #{path}") + end + end end diff --git a/lib/ex_ftp/storage/file_connector.ex b/lib/ex_ftp/storage/file_connector.ex index 3cd6a86..63735fa 100644 --- a/lib/ex_ftp/storage/file_connector.ex +++ b/lib/ex_ftp/storage/file_connector.ex @@ -404,14 +404,6 @@ defmodule ExFTP.Storage.FileConnector do end end - @impl StorageConnector - @spec transfer_complete( - type :: ExFTP.StorageConnector.transfer_type(), - path :: ExFTP.StorageConnector.path(), - connector_state :: ExFTP.StorageConnector.connector_state() - ) :: any() - def transfer_complete(_type, _path, _connector_state), do: :ok - defp rmrf_dir("/"), do: {:error, "Not something to delete"} defp rmrf_dir(dir) do diff --git a/lib/ex_ftp/storage/s3_connector.ex b/lib/ex_ftp/storage/s3_connector.ex index e10e88a..dd335ff 100644 --- a/lib/ex_ftp/storage/s3_connector.ex +++ b/lib/ex_ftp/storage/s3_connector.ex @@ -392,14 +392,6 @@ defmodule ExFTP.Storage.S3Connector do end end - @impl StorageConnector - @spec transfer_complete( - type :: ExFTP.StorageConnector.transfer_type(), - path :: ExFTP.StorageConnector.path(), - connector_state :: ExFTP.StorageConnector.connector_state() - ) :: any() - def transfer_complete(_type, _path, _connector_state), do: :ok - defp clean_path(path) do path |> Path.join("") diff --git a/lib/ex_ftp/storage_connector.ex b/lib/ex_ftp/storage_connector.ex index 7906481..b8905da 100644 --- a/lib/ex_ftp/storage_connector.ex +++ b/lib/ex_ftp/storage_connector.ex @@ -357,20 +357,6 @@ defmodule ExFTP.StorageConnector do type: :directory | :symlink | :file } - @typedoc """ - The type of transfer being completed - - - ### 🏷️ Values - * **:retrieve** :: Retrieval of a file from storage - * **:store** :: Storage of a file to storage - - #{ExFTP.Doc.resources()} - - - """ - @type transfer_type :: :retrieve | :store - @doc """ Returns a list of `t:content_info/0` representing each object in a given directory @@ -478,28 +464,4 @@ defmodule ExFTP.StorageConnector do """ @callback create_write_func(path, connector_state, opts :: list()) :: function() - - @doc """ - Called when a transfer is complete - - - ### 🏷️ Params - * **type** :: `t:transfer_type/0` - * **path** :: `t:path/0` - * **connector_state** :: `t:connector_state/0` - - #{ExFTP.Doc.returns(success: "data")} - - ### 💻 Examples - - iex> alias ExFTP.Storage.FileConnector - iex> connector_state = %{current_working_directory: "/"} - iex> dir = File.cwd!() - iex> FileConnector.transfer_complete(:retrieve, dir, connector_state) - - #{ExFTP.Doc.resources("page-30")} - - - """ - @callback transfer_complete(type :: transfer_type(), path :: path(), connector_state :: connector_state()) :: any() end diff --git a/lib/ex_ftp/worker.ex b/lib/ex_ftp/worker.ex index ebcaf57..7c8fc9c 100644 --- a/lib/ex_ftp/worker.ex +++ b/lib/ex_ftp/worker.ex @@ -35,6 +35,8 @@ defmodule ExFTP.Worker do connector = env[:storage_connector] || FileConnector authenticator = env[:authenticator] || PassthroughAuth server_name = env[:server_name] || :ExFTP + storage_config = env[:storage_config] || %{} + on_transfer_complete = storage_config[:on_transfer_complete] {:ok, host} = ftp_addr @@ -49,13 +51,22 @@ defmodule ExFTP.Worker do send_resp(220, "Hello from #{server_name}.", socket) + connector_state = %{current_working_directory: "/"} + + connector_state = + if on_transfer_complete do + Map.put(connector_state, :on_transfer_complete, on_transfer_complete) + else + connector_state + end + %Worker{ socket: socket, host: host, pasv_socket: nil, type: :ascii, storage_connector: connector, - connector_state: %{current_working_directory: "/"}, + connector_state: connector_state, authenticator: authenticator, authenticator_state: %{} } diff --git a/mix.exs b/mix.exs index 1af7d22..f489639 100644 --- a/mix.exs +++ b/mix.exs @@ -125,7 +125,6 @@ defmodule ExFTP.MixProject do {:excoveralls, "~> 0.18", only: [:test]}, {:ex_machina, "~> 2.8.0", only: :test}, {:faker, "~> 0.18.0", only: :test}, - {:mimic, "~> 2.0", only: :test}, {:req, "~> 0.5"}, {:junit_formatter, "~> 3.1", only: [:test]}, {:ex_aws, "~> 2.0"}, diff --git a/mix.lock b/mix.lock index 6ed762f..b8b700e 100644 --- a/mix.lock +++ b/mix.lock @@ -3,14 +3,14 @@ "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "configparser_ex": {:hex, :configparser_ex, "4.0.0", "17e2b831cfa33a08c56effc610339b2986f0d82a9caa0ed18880a07658292ab6", [:mix], [], "hexpm", "02e6d1a559361a063cba7b75bc3eb2d6ad7e62730c551cc4703541fd11e65e5b"}, - "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "ex_aws": {:hex, :ex_aws, "2.5.9", "8e2455172f0e5cbe2f56dd68de514f0dae6bb26d6b6e2f435a06434cf9dbb412", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbdb6ffb0e6c6368de05ed8641fe1376298ba23354674428e5b153a541f23359"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, - "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_license": {:hex, :ex_license, "0.1.1", "bbdaba704f861894da3ae80e4399984379f19de1cca4ecf7d799616c832b8821", [:mix], [], "hexpm", "dd95aaaba0c9c6f0e9be2db3e9fac0bf69784557e04f700f94e90629f5e24e1d"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, @@ -19,7 +19,6 @@ "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, - "ham": {:hex, :ham, "0.3.2", "02ae195f49970ef667faf9d01bc454fb80909a83d6c775bcac724ca567aeb7b3", [:mix], [], "hexpm", "b71cc684c0e5a3d32b5f94b186770551509e93a9ae44ca1c1a313700f2f6a69a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -31,7 +30,6 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, - "mimic": {:hex, :mimic, "2.2.0", "32a0ac9d3e98ac1edbceb770e7c524331fbfc43ca341cf2fe087a508e57e015c", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "c9766036a11f024fe922a435f851d3e3a7b1da65125b98fb5e36ed792891c45c"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, @@ -40,12 +38,12 @@ "poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"}, "proper_case": {:hex, :proper_case, "1.3.1", "5f51cabd2d422a45f374c6061b7379191d585b5154456b371432d0fa7cb1ffda", [:mix], [], "hexpm", "6cc715550cc1895e61608060bbe67aef0d7c9cf55d7ddb013c6d7073036811dd"}, "quokka": {:hex, :quokka, "2.11.2", "2856118154425f18547720d997199be54febed771a740ba3c988a17762328287", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "8208f5d814007cb35a2eb278462464d083fca8c463f62517ab94eef982f181cc"}, - "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, } diff --git a/test/ex_ftp/storage/file_connector_test.exs b/test/ex_ftp/storage/file_connector_test.exs index 4d6e707..dce5884 100644 --- a/test/ex_ftp/storage/file_connector_test.exs +++ b/test/ex_ftp/storage/file_connector_test.exs @@ -2,7 +2,6 @@ defmodule ExFTP.Storage.FileConnectorTest do @moduledoc false use ExUnit.Case - use Mimic import ExFTP.StorageTester import ExFTP.TestHelper @@ -15,8 +14,6 @@ defmodule ExFTP.Storage.FileConnectorTest do doctest Common doctest FileConnector - setup :set_mimic_global - setup do Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) Application.put_env(:ex_ftp, :authenticator_config, %{}) @@ -157,15 +154,10 @@ defmodule ExFTP.Storage.FileConnectorTest do # CWD w_dir w_dir = File.cwd!() - FileConnector - |> stub(:transfer_complete, fn _type, _path, _connector_state -> :stub end) - paths_to_download = w_dir |> File.ls!() |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) test_retr(state, w_dir, paths_to_download) - - assert length(calls(&FileConnector.transfer_complete/3)) > 1 end test "SIZE", state do @@ -179,9 +171,6 @@ defmodule ExFTP.Storage.FileConnectorTest do w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) on_exit(fn -> File.rm_rf!(w_dir) end) - FileConnector - |> stub(:transfer_complete, fn _type, _path, _connector_state -> :stub end) - files_to_store = File.cwd!() |> File.ls!() @@ -190,6 +179,5 @@ defmodule ExFTP.Storage.FileConnectorTest do refute Enum.empty?(files_to_store) test_stor(state, w_dir, files_to_store) - assert length(calls(&FileConnector.transfer_complete/3)) > 1 end end diff --git a/test/ex_ftp/storage/transfer_complete_test.exs b/test/ex_ftp/storage/transfer_complete_test.exs new file mode 100644 index 0000000..b4fb189 --- /dev/null +++ b/test/ex_ftp/storage/transfer_complete_test.exs @@ -0,0 +1,150 @@ +defmodule ExFTP.Storage.TransferCompleteTest do + @moduledoc false + + use ExUnit.Case + + import ExFTP.StorageTester + import ExFTP.TestHelper + import ExUnit.CaptureLog + + alias ExFTP.Auth.PassthroughAuth + alias ExFTP.Storage.FileConnector + + defmodule TestHandler do + require Logger + + def handle_complete(type, path, _connector_state) do + Logger.info("Custom handler called: #{type} #{path}") + :ok + end + end + + describe "transfer completion with custom handler" do + setup do + Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) + Application.put_env(:ex_ftp, :authenticator_config, %{}) + Application.put_env(:ex_ftp, :storage_connector, FileConnector) + + Application.put_env(:ex_ftp, :storage_config, %{ + on_transfer_complete: {TestHandler, :handle_complete} + }) + + socket = get_socket() + username = Faker.Internet.user_name() + password = Faker.Internet.slug() + + socket + |> send_and_expect("USER", [username], 331, "User name okay, need password") + |> send_and_expect("PASS", [password], 230, "Welcome.") + + on_exit(fn -> + Application.put_env(:ex_ftp, :storage_config, %{}) + end) + + %{socket: socket, username: username} + end + + test "calls custom handler on STOR (upload)", state do + w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) + on_exit(fn -> File.rm_rf!(w_dir) end) + + files_to_store = + File.cwd!() + |> File.ls!() + |> Enum.filter(fn file -> File.cwd!() |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(files_to_store) + + log = + capture_log(fn -> + test_stor(state, w_dir, files_to_store) + end) + + # Verify custom handler was called (not default) + assert log =~ "Custom handler called: store" + refute log =~ "Transfer complete: store" + end + + test "calls custom handler on RETR (download)", state do + w_dir = File.cwd!() + + paths_to_download = + w_dir + |> File.ls!() + |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(paths_to_download) + + log = + capture_log(fn -> + test_retr(state, w_dir, paths_to_download) + end) + + # Verify custom handler was called (not default) + assert log =~ "Custom handler called: retrieve" + refute log =~ "Transfer complete: retrieve" + end + end + + describe "transfer completion with default behavior" do + setup do + Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) + Application.put_env(:ex_ftp, :authenticator_config, %{}) + Application.put_env(:ex_ftp, :storage_connector, FileConnector) + Application.put_env(:ex_ftp, :storage_config, %{}) + + socket = get_socket() + username = Faker.Internet.user_name() + password = Faker.Internet.slug() + + socket + |> send_and_expect("USER", [username], 331, "User name okay, need password") + |> send_and_expect("PASS", [password], 230, "Welcome.") + + %{socket: socket} + end + + test "logs default message on RETR when no handler configured", state do + w_dir = File.cwd!() + + paths_to_download = + w_dir + |> File.ls!() + |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(paths_to_download) + + log = + capture_log(fn -> + test_retr(state, w_dir, paths_to_download) + end) + + # Verify default log message appears + assert log =~ "Transfer complete: retrieve" + end + + test "logs default message on STOR when no handler configured", state do + w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) + on_exit(fn -> File.rm_rf!(w_dir) end) + + files_to_store = + File.cwd!() + |> File.ls!() + |> Enum.filter(fn file -> File.cwd!() |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(files_to_store) + + log = + capture_log(fn -> + test_stor(state, w_dir, files_to_store) + end) + + # Verify default log message appears + assert log =~ "Transfer complete: store" + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 23a1b7b..690ad1d 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,3 @@ -Mimic.copy(ExFTP.Storage.FileConnector) -Mimic.copy(ExFTP.Storage.S3Connector) - ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) ExUnit.start() From 14d290abe197366e4951a712cc63830a45175258 Mon Sep 17 00:00:00 2001 From: Cam Cook Date: Sat, 27 Dec 2025 11:19:48 -0500 Subject: [PATCH 4/4] build: v1.3.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index f489639..e4b09f3 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule ExFTP.MixProject do alias ExFTP.Storage.S3ConnectorConfig @source_url "https://github.com/camatcode/ex_ftp" - @version "1.2.0" + @version "1.3.0" def project do [