# Backends

`Sftpd` separates the SFTP server runtime from storage through the
`Sftpd.Backend` behaviour. A backend is responsible for the filesystem-like
operations that SFTP clients expect: listing directories, reading files,
writing files, and reporting metadata.

## Choosing a Backend Style

You can plug in storage in two ways:

- module-based backends
- process-based backends

Module-based backends are the simplest fit for stateless adapters and are what
the built-in backends use. Process-based backends are useful when the storage
layer already has a long-lived process, mutable state, or its own lifecycle.

| Need | Use |
| --- | --- |
| Tests, demos, and local development | `Sftpd.Backends.Memory` |
| Amazon S3, MinIO, or another S3-compatible store | `Sftpd.Backends.S3` |
| A local disk folder | A custom folder backend |
| A shared process, cache, queue, or connection pool | `{:genserver, name_or_pid}` |
| Async ingestion after upload | Store synchronously in the backend, then enqueue a Broadway job |

## Built-In Backends

### `Sftpd.Backends.Memory`

The memory backend stores files in an `Agent` and is intended for:

- development
- tests
- backend experimentation

Properties:

- no external dependencies
- immediate startup
- supports the core `Sftpd.Backend` callbacks
- does not implement the optional streaming callbacks

Example:

```elixir
{:ok, ref} =
  Sftpd.start_server(
    port: 2222,
    backend: Sftpd.Backends.Memory,
    backend_opts: [],
    auth: {:passwords, [{"dev", "dev"}]},
    system_dir: "ssh_keys"
  )
```

See `Sftpd.Backends.Memory` for API details.

### `Sftpd.Backends.S3`

The S3 backend maps SFTP operations onto object storage and is intended for:

- Amazon S3
- MinIO
- S3-compatible providers

S3 support is optional. Applications that use this backend must also depend on:

- `:ex_aws`
- `:ex_aws_s3`
- `:hackney`
- `:sweet_xml`
- `:jason`
- `:configparser_ex`

If those dependencies are not available, `Sftpd.Backends.S3.init/1` returns
`{:error, :missing_s3_dependency}` instead of failing during compilation.

Properties:

- supports range reads through `read_file_range/4`
- supports multipart streaming writes through the optional streaming callbacks
- uses delimiter-based paginated listings for directory traversal
- models directories with `.keep` marker objects

Example:

```elixir
{:ok, ref} =
  Sftpd.start_server(
    port: 2222,
    backend: Sftpd.Backends.S3,
    backend_opts: [bucket: "my-bucket", prefix: "tenant-a/"],
    auth: {:passwords, [{"user", "pass"}]},
    system_dir: "ssh_keys"
  )
```

The S3 `:prefix` option can be a static string or `{:session, key}`. Session
prefixes read from the authenticated session map returned by `Sftpd.Auth`
callbacks, which is useful for tenant-scoped object keys:

```elixir
backend_opts: [bucket: "uploads", prefix: {:session, :sftp_prefix}]
```

See `Sftpd.Backends.S3` for configuration details and caveats. The Getting
Started guide has the same dependency list in the context of a full server
setup.

## Module-Based Backends

A module backend implements the `Sftpd.Backend` callbacks directly and returns
its own backend state from `init/1`.

Minimal shape:

```elixir
defmodule MyApp.CustomBackend do
  @behaviour Sftpd.Backend

  @impl true
  def init(opts) do
    {:ok, %{root: Keyword.fetch!(opts, :root)}}
  end

  @impl true
  def list_dir(_path, _state), do: {:ok, [~c".", ~c".."]}

  @impl true
  def file_info(_path, _state), do: {:error, :enoent}

  @impl true
  def make_dir(_path, _state), do: :ok

  @impl true
  def del_dir(_path, _state), do: :ok

  @impl true
  def delete(_path, _state), do: :ok

  @impl true
  def rename(_src, _dst, _state), do: :ok

  @impl true
  def read_file(_path, _state), do: {:error, :enoent}

  @impl true
  def write_file(_path, _content, _state), do: :ok
end
```

Use it with:

```elixir
Sftpd.start_server(
  backend: MyApp.CustomBackend,
  backend_opts: [root: "/data"],
  auth: {:passwords, [{"user", "pass"}]},
  system_dir: "ssh_keys"
)
```

## Process-Based Backends

You can also pass a running GenServer as `{:genserver, server}`. In that mode,
`Sftpd` skips `init/1` and forwards backend operations as legacy
`handle_call/3` messages so existing process backends keep working.

Calls follow this shape:

- `{:list_dir, path}`
- `{:file_info, path}`
- `{:make_dir, path}`
- `{:del_dir, path}`
- `{:delete, path}`
- `{:rename, src, dst}`
- `{:read_file, path}`
- `{:write_file, path, content}`

If a process backend needs authenticated session context, opt in with
`{:genserver, server, session: true}`. Session-aware calls follow this shape:

- `{:list_dir, path, session}`
- `{:file_info, path, session}`
- `{:make_dir, path, session}`
- `{:del_dir, path, session}`
- `{:delete, path, session}`
- `{:rename, src, dst, session}`
- `{:read_file, path, session}`
- `{:write_file, path, content, session}`

The reply format must match the `Sftpd.Backend` callback contracts.

## Streaming Support

For large files, module backends can optionally implement:

- `read_file_range/4`
- `begin_write/2`
- `write_chunk/4`
- `finish_write/2`
- `abort_write/2`

Session-aware variants are also supported by adding the authenticated session
map immediately before backend state, for example `list_dir(path, session,
state)` or `write_file(path, content, session, state)`. When both variants are
available, `Sftpd` calls the session-aware function.

When present:

- reads avoid preloading the entire file into memory
- sequential writes can stream directly to the backend
- large S3 uploads can use multipart upload instead of a full-buffer rewrite

If those callbacks are not implemented, `Sftpd` falls back to whole-file
buffering semantics using the required callbacks.

## Metadata and Directory Semantics

Backends are expected to expose filesystem-like results even when the
underlying storage is not a filesystem.

Important conventions:

- `list_dir/2` must include `.` and `..`
- `file_info/2` should distinguish `:regular` from `:directory`
- `root_path?/1` and `normalize_path/1` in `Sftpd.Backend` help normalize SFTP
  paths consistently
- `directory_info/0` and `file_info/3` build compatible Erlang-style metadata

## Error Mapping

Backend functions should return POSIX-style atoms such as:

- `:enoent`
- `:eacces`
- `:einval`
- `:eio`

That keeps behavior predictable across storage implementations and maps cleanly
onto what SFTP clients expect.

## Next Steps

- Read [Custom Backends](CUSTOM_BACKENDS.md) for implementation guidance
- See `Sftpd.Backend` for the authoritative callback contracts
- See [Telemetry](TELEMETRY.md) for the emitted telemetry events around backend operations
