ExFTP.StorageConnector behaviour (ExFTP v1.2.0)
View SourceA behaviour defining a Storage Connector.
Storage Connectors are used by the FTP interface to interact with a particular type of storage.
Custom Connectors
To create a custom storage connector, implement all callbacks in this behaviour:
# SPDX-License-Identifier: Apache-2.0
defmodule MyStorageConnector do
@moduledoc false
@behaviour ExFTP.StorageConnector
alias ExFTP.StorageConnector
@impl StorageConnector
@spec get_working_directory(connector_state :: StorageConnector.connector_state()) ::
String.t()
def get_working_directory(%{current_working_directory: cwd} = _connector_state) do
# returns the current directory, for most cases this is just a pass through
# however, you might want to modify what the current directory is
# based on some state
end
@impl StorageConnector
@spec directory_exists?(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: boolean
def directory_exists?(path, _connector_state) do
# Given a path, does this directory exist in storage?
end
@impl StorageConnector
@spec make_directory(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: {:ok, StorageConnector.connector_state()} | {:error, term()}
def make_directory(path, connector_state) do
# Given a path, make a directory
# For S3-like connectors, a "directory" doesn't really exist
# so those connectors typically keep track of virtual directories
# that we're created by user during the session
# if they're unused, they aren't persisted.
end
@impl StorageConnector
@spec delete_directory(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: {:ok, StorageConnector.connector_state()} | {:error, term()}
def delete_directory(path, connector_state) do
# Give a path, delete the directory
end
@impl StorageConnector
@spec delete_file(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: {:ok, StorageConnector.connector_state()} | {:error, term()}
def delete_file(path, connector_state) do
# Give a path, delete the file
end
@impl StorageConnector
@spec get_directory_contents(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) ::
{:ok, [StorageConnector.content_info()]} | {:error, term()}
def get_directory_contents(path, connector_state) do
# returns a list of content_infos
# the model for them was inspired by File.lstat()
# Have a look at StorageConnector.content_info type
end
@impl StorageConnector
@spec get_content_info(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) ::
{:ok, StorageConnector.content_info()} | {:error, term()}
def get_content_info(path, _connector_state) do
# given a path, return information on the file/directory there
# Have a look at StorageConnector.content_info type
end
@impl StorageConnector
@spec get_content(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: {:ok, any()} | {:error, term()}
def get_content(path, _connector_state) do
# Return a {:ok, stream} of path
end
@impl StorageConnector
@spec create_write_func(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state(),
opts :: list()
) :: function()
def create_write_func(path, connector_state, opts \\ []) do
# Return a function that will write `stream` to your storage at path
# e.g
# fn stream ->
# fs = File.stream!(path)
#
# try do
# _ =
# stream
# |> chunk_stream(opts)
# |> Enum.into(fs)
#
# {:ok, connector_state}
# rescue
# _ ->
# {:error, "Failed to transfer"}
# end
#end
end
endUser Scopes
All connector_state maps contain the authenticator_state map, which contains username and any metadata added by the Authenticator
@impl StorageConnector
@spec get_content(
path :: StorageConnector.path(),
connector_state :: StorageConnector.connector_state()
) :: {:ok, any()} | {:error, term()}
def get_content(path, connector_state) do
# Access the authenticated user's information
# username = connector_state.authenticator_state.username
# Scope the path to the user's directory
# scoped_path = Path.join(["/users", username, path])
end
# ... implement other callbacks similarly
end๐ See Also
๐ Resources
Summary
Types
State held onto by the server and modified by the StorageConnector.
Information about a given file, directory, or symlink
A string representing a file path (e.g "/path/to/file.txt" or "/path/to/dir/")
A Port representing a socket to communicate with an FTP client.
Callbacks
Create a function/1 that writes a stream to storage
Deletes a given directory
Deletes a given file
Whether a given path is an existing directory
Returns a stream to read the raw bytes of an object specified by a given path
Returns content_info/0 representing a single object in a given directory
Returns a list of content_info/0 representing each object in a given directory
Returns the current working directory
Creates a directory, given a path
Types
@type connector_state() :: %{}
State held onto by the server and modified by the StorageConnector.
It may be used by the storage connector to keep stateful values.
โ ๏ธ Reminders
Special Keys
current_working_directory:String.t/0always present. Represents the "directory" we're operating from.authenticator_state:ExFTP.Authenticator.authenticator_state()always present when callbacks are invoked. Contains the authenticated user's information (including username) and any custom data stored by the authenticator. Storage connector callbacks are only invoked after successful authentication.
๐ Resources
@type content_info() :: %{ file_name: String.t(), modified_datetime: DateTime.t(), size: integer(), access: :read | :write | :read_write | :none, type: :directory | :symlink | :file }
Information about a given file, directory, or symlink
๐ท๏ธ Keys
- filename ::
String.t/0e.g "my_file.txt", "my_dir/" or "my_sym_link -> target/" - modified_datetime ::
DateTime.t/0 - size ::
integere.g 1000 access ::
:read | :write | :read_write | :nonetype ::
:directory | :symlink | :file
๐ See Also
๐ Resources
@type path() :: String.t()
A string representing a file path (e.g "/path/to/file.txt" or "/path/to/dir/")
โ ๏ธ Reminders
Relative paths
The Server will ensure all paths sent to the connector are absolute paths,
so you don't need to worry about handling relative paths, or .. notations
๐ Resources
@type socket() :: port()
A Port representing a socket to communicate with an FTP client.
โ ๏ธ Reminders
Sockets are everywhere
This socket represents the TCP connection between the FTP Server and the client (often through port 21)
While related, this socket is not a PASV socket, which is a negotiated, temporary socket for sending or receiving data.
๐ Resources
Callbacks
@callback create_write_func(path(), connector_state(), opts :: list()) :: function()
Create a function/1 that writes a stream to storage
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0 - opts :: list of options
๐ป Examples
@impl StorageConnector
def create_write_func(path, connector_state, opts \ []) do
fn stream ->
fs = File.stream!(path)
try do
_ =
stream
|> chunk_stream(opts)
|> Enum.into(fs)
{:ok, connector_state}
rescue
_ ->
{:error, "Failed to transfer"}
end
end
end
@callback delete_directory(path(), connector_state()) :: {:ok, connector_state()} | {:error, term()}
Deletes a given directory
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, connector_state}โ On Failure
{:error, err}๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> connector_state = %{current_working_directory: "/"}
iex> dir_to_make = File.cwd!() |> Path.join("new_dir")
iex> {:ok, connector_state} = FileConnector.make_directory(dir_to_make, connector_state)
iex> dir_to_rm = dir_to_make
iex> {:ok, connector_state} = FileConnector.delete_directory(dir_to_rm, connector_state)
iex> FileConnector.directory_exists?(dir_to_rm, connector_state)
false๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback delete_file(path(), connector_state()) :: {:ok, connector_state()} | {:error, term()}
Deletes a given file
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, connector_state}โ On Failure
{:error, err}๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback directory_exists?(path(), connector_state()) :: boolean()
Whether a given path is an existing directory
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
`true` or `false`๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> FileConnector.directory_exists?("/tmp", %{current_working_directory: "/"})
true
iex> FileConnector.directory_exists?("/does-not-exist", %{current_working_directory: "/"})
false๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback get_content(path(), connector_state()) :: {:ok, any()} | {:error, term()}
Returns a stream to read the raw bytes of an object specified by a given path
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, data}โ On Failure
{:error, err}๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> connector_state = %{current_working_directory: "/"}
iex> file_to_get_content = File.cwd!() |> File.ls!() |> Enum.filter(&String.contains?(&1,".")) |> hd()
iex> path = Path.join(File.cwd!(), file_to_get_content)
iex> {:ok, _data} = FileConnector.get_content(path, connector_state)๐ Resources
- ๐ RFC 959 (page-30)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback get_content_info(path(), connector_state()) :: {:ok, content_info()} | {:error, term()}
Returns content_info/0 representing a single object in a given directory
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, %{...}}โ On Failure
{:error, err}๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> connector_state = %{current_working_directory: "/"}
iex> file_to_get_info = File.cwd!() |> File.ls!() |> hd()
iex> path = Path.join(File.cwd!(), file_to_get_info)
iex> {:ok, content_info} = FileConnector.get_content_info(path, connector_state)๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback get_directory_contents(path(), connector_state()) :: {:ok, [content_info()]} | {:error, term()}
Returns a list of content_info/0 representing each object in a given directory
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, [%{...}, ...]}โ On Failure
{:error, err}๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> connector_state = %{current_working_directory: "/"}
iex> dir = File.cwd!()
iex> {:ok, _content_infos} = FileConnector.get_directory_contents(dir, connector_state)๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback get_working_directory(connector_state()) :: String.t()
Returns the current working directory
๐ท๏ธ Params
- connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
(The working directory)๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> FileConnector.get_working_directory(%{current_working_directory: "/"})
"/"โ ๏ธ Reminders
Doesn't the connector_state already have it?
Most Storage Connectors will just return what's already in the connector_state. However, this method is implemented just in case a Connector has a different way of determining the current working directory.
๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)
@callback make_directory(path(), connector_state()) :: {:ok, connector_state()} | {:error, term()}
Creates a directory, given a path
๐ท๏ธ Params
- path ::
path/0 - connector_state ::
connector_state/0
โคต๏ธ Returns
โ On Success
{:ok, connector_state}โ On Failure
{:error, err}๐ป Examples
iex> alias ExFTP.Storage.FileConnector
iex> connector_state = %{current_working_directory: "/"}
iex> dir_to_make = File.cwd!() |> Path.join("new_dir")
iex> {:ok, connector_state} = FileConnector.make_directory(dir_to_make, connector_state)
iex> FileConnector.directory_exists?(dir_to_make, connector_state)
true
iex> {:ok, _connector_state} = FileConnector.delete_directory(dir_to_make, connector_state)
iex> FileConnector.directory_exists?(dir_to_rm, connector_state)
false๐ Resources
- ๐ RFC 959 (page-32)
- ๐ RFC 3659
- ๐ฌ Contact the maintainer (he's happy to help!)