# `Sftpd.Backend`
[🔗](https://github.com/elixir-ssh/sftpd/blob/v0.1.1/lib/sftpd/backend.ex#L1)

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
    end

Then 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/4`
- `begin_write/2`
- `write_chunk/4`
- `finish_write/2`
- `abort_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.

# `file_info`

```elixir
@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

# `genserver_backend`

```elixir
@type genserver_backend() ::
  {:genserver, GenServer.server()} | {:genserver, GenServer.server(), keyword()}
```

# `path`

```elixir
@type path() :: charlist()
```

SFTP path as charlist

# `session`

```elixir
@type session() :: map()
```

Authenticated SSH session context returned by `Sftpd.Auth` callbacks

# `state`

```elixir
@type state() :: term()
```

Backend state, returned from init/1 and threaded through all calls

# `writer_handle`

```elixir
@type writer_handle() :: term()
```

Opaque backend-managed write handle used by optional streaming callbacks

# `abort_write`
*optional* 

```elixir
@callback abort_write(writer_handle(), state()) :: :ok
```

Abort a streaming write operation.

# `abort_write`
*optional* 

```elixir
@callback abort_write(writer_handle(), session(), state()) :: :ok
```

# `begin_write`
*optional* 

```elixir
@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.

# `begin_write`
*optional* 

```elixir
@callback begin_write(path(), session(), state()) ::
  {:ok, writer_handle()} | {:error, atom()}
```

# `del_dir`

```elixir
@callback del_dir(path(), state()) :: :ok | {:error, atom()}
```

Delete an empty directory.

# `del_dir`
*optional* 

```elixir
@callback del_dir(path(), session(), state()) :: :ok | {:error, atom()}
```

# `delete`

```elixir
@callback delete(path(), state()) :: :ok | {:error, atom()}
```

Delete a file.

# `delete`
*optional* 

```elixir
@callback delete(path(), session(), state()) :: :ok | {:error, atom()}
```

# `file_info`

```elixir
@callback file_info(path(), state()) :: {:ok, file_info()} | {:error, atom()}
```

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.

# `file_info`
*optional* 

```elixir
@callback file_info(path(), session(), state()) :: {:ok, file_info()} | {:error, atom()}
```

# `finish_write`
*optional* 

```elixir
@callback finish_write(writer_handle(), state()) :: :ok | {:error, atom()}
```

Finalize a streaming write operation.

# `finish_write`
*optional* 

```elixir
@callback finish_write(writer_handle(), session(), state()) :: :ok | {:error, atom()}
```

# `init`

```elixir
@callback init(opts :: keyword()) :: {:ok, state()} | {:error, term()}
```

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_dir`

```elixir
@callback list_dir(path(), state()) :: {:ok, [charlist()]} | {:error, atom()}
```

List the contents of a directory.

Returns a list of filenames as charlists. Must include `.` and `..` entries.

# `list_dir`
*optional* 

```elixir
@callback list_dir(path(), session(), state()) :: {:ok, [charlist()]} | {:error, atom()}
```

# `make_dir`

```elixir
@callback make_dir(path(), state()) :: :ok | {:error, atom()}
```

Create a directory.

# `make_dir`
*optional* 

```elixir
@callback make_dir(path(), session(), state()) :: :ok | {:error, atom()}
```

# `read_file`

```elixir
@callback read_file(path(), state()) :: {:ok, binary()} | {:error, atom()}
```

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.

# `read_file`
*optional* 

```elixir
@callback read_file(path(), session(), state()) :: {:ok, binary()} | {:error, atom()}
```

# `read_file_range`
*optional* 

```elixir
@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.

# `read_file_range`
*optional* 

```elixir
@callback read_file_range(
  path(),
  offset :: non_neg_integer(),
  len :: pos_integer(),
  session(),
  state()
) :: {:ok, binary()} | :eof | {:error, atom()}
```

# `rename`

```elixir
@callback rename(src :: path(), dst :: path(), state()) :: :ok | {:error, atom()}
```

Rename/move a file or directory.

# `rename`
*optional* 

```elixir
@callback rename(src :: path(), dst :: path(), session(), state()) ::
  :ok | {:error, atom()}
```

# `write_chunk`
*optional* 

```elixir
@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.

# `write_chunk`
*optional* 

```elixir
@callback write_chunk(
  writer_handle(),
  offset :: non_neg_integer(),
  iodata(),
  session(),
  state()
) :: {:ok, writer_handle()} | {:error, atom()}
```

# `write_file`

```elixir
@callback write_file(path(), content :: binary(), state()) :: :ok | {:error, atom()}
```

Write content to a file, creating or overwriting it.

# `write_file`
*optional* 

```elixir
@callback write_file(path(), content :: binary(), session(), state()) ::
  :ok | {:error, atom()}
```

# `directory_info`

```elixir
@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).

# `file_info`

```elixir
@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 bytes
- `mtime` - 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 ID

## Examples

    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_path`

```elixir
@spec normalize_path(path() | String.t()) :: String.t()
```

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"

# `root_path?`

```elixir
@spec root_path?(path()) :: boolean()
```

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

# `supports_callback?`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
