Behaviour for SFTP storage backends.
See the HexDocs extras Backends and Custom Backends for package-level
guidance before implementing this behaviour directly.
Implement this behaviour to create custom storage backends for the SFTP server. Built-in backends include:
Sftpd.Backends.S3- Amazon S3 or compatible object storage
Module-Based Backends
Implement the Sftpd.Backend behaviour:
defmodule MyApp.CustomBackend do
@behaviour Sftpd.Backend
@impl true
def init(opts) do
{:ok, %{root: opts[:root] || "/"}}
end
@impl true
def list_dir(path, state) do
{:ok, [~c".", ~c"..", ~c"file.txt"]}
end
# ... implement other callbacks
endThen use it:
Sftpd.start_server(
backend: MyApp.CustomBackend,
backend_opts: [root: "/data"],
...
)Process-Based Backends
For stateful backends, you can use a GenServer process instead:
Sftpd.start_server(
backend: {:genserver, MyApp.BackendServer},
...
)By default, the process receives the legacy message shapes:
def handle_call({:list_dir, path}, _from, state)
def handle_call({:file_info, path}, _from, state)
def handle_call({:make_dir, path}, _from, state)
def handle_call({:del_dir, path}, _from, state)
def handle_call({:delete, path}, _from, state)
def handle_call({:rename, src, dst}, _from, state)
def handle_call({:read_file, path}, _from, state)
def handle_call({:write_file, path, content}, _from, state)Opt in to authenticated session context with {:genserver, server, session: true}. Session-aware processes must handle:
def handle_call({:list_dir, path, session}, _from, state)
def handle_call({:file_info, path, session}, _from, state)
def handle_call({:make_dir, path, session}, _from, state)
def handle_call({:del_dir, path, session}, _from, state)
def handle_call({:delete, path, session}, _from, state)
def handle_call({:rename, src, dst, session}, _from, state)
def handle_call({:read_file, path, session}, _from, state)
def handle_call({:write_file, path, content, session}, _from, state)Each should reply with the same format as the behaviour callbacks.
Optional Streaming Callbacks
Module backends can implement optional streaming callbacks for more efficient large-file transfers:
read_file_range/4begin_write/2write_chunk/4finish_write/2abort_write/2
When these callbacks are present, Sftpd.IODevice avoids buffering entire
files in memory for reads and most writes.
Close Semantics
Erlang's stock :ssh_sftpd server ignores the return value of
file_handler.close/2 and always reports close success to the client. Write
failures can therefore only be surfaced reliably during active writes, not on
close/final multipart completion.
Summary
Types
Erlang file_info tuple
SFTP path as charlist
Authenticated SSH session context returned by Sftpd.Auth callbacks
Backend state, returned from init/1 and threaded through all calls
Opaque backend-managed write handle used by optional streaming callbacks
Callbacks
Abort a streaming write operation.
Begin a streaming write operation.
Delete an empty directory.
Delete a file.
Get file or directory information.
Finalize a streaming write operation.
Initialize the backend with the given options.
List the contents of a directory.
Create a directory.
Read the entire contents of a file.
Read a byte range from a file.
Rename/move a file or directory.
Append a chunk to a streaming write operation at the given offset.
Write content to a file, creating or overwriting it.
Functions
Build a file_info tuple for a directory.
Build a file_info tuple for a regular file.
Normalize an SFTP path to a string without leading slash.
Return true if the path refers to the root directory.
Return true when a module backend implements the given optional callback.
Types
@type file_info() :: {:file_info, non_neg_integer(), :regular | :directory, :read | :write | :read_write, tuple(), tuple(), tuple(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
Erlang file_info tuple
@type genserver_backend() :: {:genserver, GenServer.server()} | {:genserver, GenServer.server(), keyword()}
@type path() :: charlist()
SFTP path as charlist
@type session() :: map()
Authenticated SSH session context returned by Sftpd.Auth callbacks
@type state() :: term()
Backend state, returned from init/1 and threaded through all calls
@type writer_handle() :: term()
Opaque backend-managed write handle used by optional streaming callbacks
Callbacks
@callback abort_write(writer_handle(), state()) :: :ok
Abort a streaming write operation.
@callback abort_write(writer_handle(), session(), state()) :: :ok
@callback begin_write(path(), state()) :: {:ok, writer_handle()} | {:error, atom()}
Begin a streaming write operation.
The returned writer handle is passed back to subsequent streaming write callbacks.
@callback begin_write(path(), session(), state()) :: {:ok, writer_handle()} | {:error, atom()}
Delete an empty directory.
Delete a file.
Get file or directory information.
Returns an Erlang file_info tuple. Use Sftpd.Backend.file_info/3 or
Sftpd.Backend.directory_info/0 helpers to construct these.
@callback finish_write(writer_handle(), state()) :: :ok | {:error, atom()}
Finalize a streaming write operation.
@callback finish_write(writer_handle(), session(), state()) :: :ok | {:error, atom()}
Initialize the backend with the given options.
Called once when the SFTP session starts. Returns the initial state that will be passed to all subsequent callbacks.
List the contents of a directory.
Returns a list of filenames as charlists. Must include . and .. entries.
Create a directory.
Read the entire contents of a file.
For large files, consider implementing streaming in your backend
and using the read_file_range/4 optional callback.
@callback read_file_range( path(), offset :: non_neg_integer(), len :: pos_integer(), state() ) :: {:ok, binary()} | :eof | {:error, atom()}
Read a byte range from a file.
This is an optional callback used to avoid buffering entire files in memory.
Return :eof when the requested offset is at or past the end of the file.
@callback read_file_range( path(), offset :: non_neg_integer(), len :: pos_integer(), session(), state() ) :: {:ok, binary()} | :eof | {:error, atom()}
Rename/move a file or directory.
@callback write_chunk(writer_handle(), offset :: non_neg_integer(), iodata(), state()) :: {:ok, writer_handle()} | {:error, atom()}
Append a chunk to a streaming write operation at the given offset.
@callback write_chunk( writer_handle(), offset :: non_neg_integer(), iodata(), session(), state() ) :: {:ok, writer_handle()} | {:error, atom()}
Write content to a file, creating or overwriting it.
Functions
@spec directory_info() :: file_info()
Build a file_info tuple for a directory.
Returns a directory with mode 16877 (0o40755 = directory with rwxr-xr-x permissions).
@spec file_info(non_neg_integer(), :calendar.datetime(), :read | :write | :read_write) :: file_info()
Build a file_info tuple for a regular file.
Parameters
size- File size in bytesmtime- Modification time as Erlang datetime tuple{{Y,M,D},{H,M,S}}access- Access mode, one of:read,:write,:read_write
File Info Tuple Structure
The tuple matches Erlang's #file_info{} record:
{:file_info,
size, # File size in bytes
type, # :regular | :directory | :symlink | etc.
access, # :read | :write | :read_write | :none
atime, # Last access time {{Y,M,D},{H,M,S}}
mtime, # Last modification time
ctime, # Creation/change time
mode, # Unix permission bits (33188 = 0o100644 = regular file, rw-r--r--)
links, # Number of hard links
major_device, # Major device number (0 for regular files)
minor_device, # Minor device number (0 for regular files)
inode, # Inode number (random for virtual filesystems)
uid, # Owner user ID
gid} # Owner group IDExamples
iex> {:file_info, 12, :regular, :read_write, {{2024, 1, 1}, {0, 0, 0}}, _, _, _, _, _, _, _, _, _} =
...> Sftpd.Backend.file_info(12, {{2024, 1, 1}, {0, 0, 0}})
Normalize an SFTP path to a string without leading slash.
Useful for backends that use string keys (like S3).
Examples
iex> Sftpd.Backend.normalize_path(~c"/folder/file.txt")
"folder/file.txt"
iex> Sftpd.Backend.normalize_path("already/normalized")
"already/normalized"
Return true if the path refers to the root directory.
Handles all common root path representations used by SFTP clients.
Examples
iex> Sftpd.Backend.root_path?(~c"/")
true
iex> Sftpd.Backend.root_path?(~c"/nested")
false
@spec supports_callback?(module() | genserver_backend(), atom(), arity()) :: boolean()
Return true when a module backend implements the given optional callback.
Process-based backends use the legacy callback contract only.