[![CI](https://harton.dev/james/tahr/actions/workflows/ci.yml/badge.svg)](https://harton.dev/james/tahr/actions)
[![Licence](https://img.shields.io/badge/licence-Apache--2.0-blue.svg)](LICENSE)

# Tahr

A standalone Elixir library for building NFSv3 file servers natively
on the BEAM — no kernel NFS server, no NIFs, no out-of-tree Rust. If
you can implement a behaviour, you can serve NFS from an Elixir
application.

The whole stack is here, from the wire up:

- `Tahr.XDR` — codec for every ONC RPC and NFSv3 data structure
- `Tahr.RPC` — the ONC RPC framework (record marking, programs,
  TCP transport)
- `Tahr.Mount` — the MOUNT protocol, against a
  `Tahr.Mount.Backend` behaviour
- `Tahr.NFSv3` — the NFSv3 procedures, against a
  `Tahr.NFSv3.Backend` behaviour

Implement the two backend behaviours over your storage and any NFS
client built into Linux, macOS, or Windows can mount it.

The library follows the same split-of-concerns pattern as
[`firkin`](https://hex.pm/packages/firkin) (S3) and
[`davy`](https://hex.pm/packages/davy) (WebDAV): protocol library
here, storage-specific backend elsewhere.

## Installation

Add `tahr` to your dependencies:

```elixir
def deps do
  [{:tahr, "~> 0.1.0"}]
end
```

## Quick start

Implement the two backend behaviours over your storage:

- `Tahr.Mount.Backend` — export resolution (which clients may
  mount which paths)
- `Tahr.NFSv3.Backend` — filesystem operations (getattr, read,
  write, readdir, and friends)

Then wire the protocol handlers and the RPC listener into your
supervision tree:

```elixir
programs = %{
  100_005 => %{3 => Tahr.Mount.Handler.with_backend(MyApp.MountBackend)},
  100_003 => %{3 => Tahr.NFSv3.Handler.with_backend(MyApp.NFSBackend)}
}

children = [
  {Tahr.RPC.Server, port: 2049, programs: programs}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

The portmapper (program 100000) is always registered so that NFS
clients can discover the server, and the NFSv3 mapping is announced
automatically on whatever port is bound.

## Features

- **Pure BEAM, from the wire up** — XDR (RFC 4506), ONC RPC v2 with
  record marking and AUTH_SYS, MOUNT v3, and the full NFSv3 procedure
  set (RFC 1813)
- **Pluggable storage** — two behaviours separate the protocol from
  your storage layer
- **Streaming reads** — READ replies stream to the socket as iolists;
  files are never materialised into a single binary
- **One process per connection** — slow clients don't affect anyone
  else, and the listener drains in-flight RPCs on shutdown
- **Batteries included** — `Tahr.InMemory`, a read-write reference
  backend, and `mix tahr.serve` to mount it locally

## Try it locally

```bash
mix tahr.serve

# in another terminal
sudo mount -t nfs -o vers=3,tcp,port=2049,mountport=2049 \
  127.0.0.1:/export /mnt/nfs
ls -l /mnt/nfs
```

## Tutorials

Step-by-step guides to building a backend, included in the
generated docs (`mix docs`):

- [Serving a read-only filesystem](documentation/tutorials/read-only-server.md)
- [A read-write server](documentation/tutorials/read-write-server.md)

## Building and testing

```bash
mix deps.get
mix compile
mix test
```

## Licence

[Apache-2.0](LICENSE)
