Packages

Elixir bindings to macOS NetFS.framework. Mount and unmount SMB, NFS, AFP, and WebDAV network shares with Kerberos and DFS support.

Current section

Files

Jump to
ex_netfs lib ex_netfs.ex
Raw

lib/ex_netfs.ex

defmodule ExNetfs do
@moduledoc """
Elixir bindings to macOS NetFS.framework for mounting network file systems.
NetFS is the framework that Finder uses under the hood when you "Connect to Server".
It supports SMB, NFS, AFP, and WebDAV protocols and is Kerberos-aware — if a valid
TGT exists in the credential cache (e.g. from `ExKrb5.kinit/2`), it will use it
automatically without needing explicit username/password.
## Key Features
- **Kerberos integration**: Pass `nil` for user/password to use the Kerberos TGT
- **DFS support**: Automatically follows SMB DFS referrals
- **Mount options**: Control visibility, read-only, sub-mounts, soft-mount behavior
- **UI suppression**: Mount silently without system auth dialogs
## Quick Start
# Mount with Kerberos (after ExKrb5.kinit)
{:ok, [mountpoint]} = ExNetfs.mount("smb://fileserver.ad.example.com/share")
# Mount with explicit credentials
{:ok, [mountpoint]} = ExNetfs.mount("smb://server/share", user: "admin", password: "secret")
# Mount hidden (not visible in Finder sidebar)
{:ok, [mountpoint]} = ExNetfs.mount("smb://server/share", mount_options: [:no_browse])
# Mount at specific path
{:ok, [mountpoint]} = ExNetfs.mount("smb://server/share",
mountpath: "/Volumes/myshare",
mount_options: [:mount_at_dir])
# Unmount
:ok = ExNetfs.unmount("/Volumes/share")
# List network mounts
[{"//server/share", "/Volumes/share", "smbfs"}] = ExNetfs.list_mounts()
## Error Codes
Mount errors return `{:error, {reason, code}}` where reason is an atom:
| Atom | Code | Meaning |
|------|------|---------|
| `:not_found` | 2 | Mount point doesn't exist |
| `:permission_denied` | 13 | Access denied |
| `:already_mounted` | 16 | Resource busy |
| `:timed_out` | 60 | Connection timed out |
| `:connection_refused` | 61 | Server refused connection |
| `:host_down` | 64 | Host is down |
| `:host_unreachable` | 65 | No route to host |
| `:user_cancelled` | -128 | User cancelled auth dialog |
| `:no_shares_available` | -5998 | No shares on server |
| `:no_auth_mech` | -5997 | No supported auth mechanism |
| `:guest_not_supported` | -6004 | Server doesn't allow guests |
"""
@doc """
Mount a network file share.
## Parameters
- `url` - Share URL: `"smb://server/share"`, `"nfs://server/path"`, `"afp://server/vol"`
- `opts` - Keyword options:
- `:mountpath` - Specific mount point path (default: system picks under `/Volumes/`)
- `:user` - Username (default: nil = use Kerberos)
- `:password` - Password (default: nil = use Kerberos)
- `:open_options` - List of session options:
- `:no_ui` - Suppress authentication dialogs
- `:guest` - Login as guest
- `:allow_loopback` - Allow mounting from localhost
- `:mount_options` - List of mount options:
- `:no_browse` - Hide from Finder sidebar (MNT_DONTBROWSE)
- `:read_only` - Mount read-only (MNT_RDONLY)
- `:allow_sub_mounts` - Allow mounting subdirectories of the share
- `:soft_mount` - Soft failure semantics (default if not set)
- `:mount_at_dir` - Mount exactly at mountpath, not below it
## Returns
- `{:ok, mountpoints}` - List of mount point paths (usually one)
- `{:error, {reason, code}}` - Error with reason atom and numeric code
## Examples
# Kerberos mount (silent, no UI)
{:ok, paths} = ExNetfs.mount("smb://fs.ad.corp.com/home",
open_options: [:no_ui])
# Explicit credentials to specific path
{:ok, paths} = ExNetfs.mount("smb://nas/backup",
mountpath: "/Volumes/backup",
user: "admin",
password: "secret",
mount_options: [:mount_at_dir, :no_browse])
# NFS mount
{:ok, paths} = ExNetfs.mount("nfs://nfsserver/export/data")
"""
@spec mount(String.t(), keyword()) :: {:ok, [String.t()]} | {:error, {atom(), integer()}}
def mount(url, opts \\ []) do
mountpath = Keyword.get(opts, :mountpath)
user = Keyword.get(opts, :user)
password = Keyword.get(opts, :password)
open_opts = Keyword.get(opts, :open_options, [])
mount_opts = Keyword.get(opts, :mount_options, [])
ExNetfs.Native.mount(url, mountpath, user, password, open_opts, mount_opts)
end
@doc """
Unmount a network file share at the given mount point.
## Parameters
- `path` - Mount point path, e.g. `"/Volumes/share"`
- `opts` - Options:
- `:force` - Force unmount even if busy (default: false)
## Returns
- `:ok` - Successfully unmounted
- `{:error, {reason, code}}` - Error
## Examples
:ok = ExNetfs.unmount("/Volumes/share")
:ok = ExNetfs.unmount("/Volumes/stuck_share", force: true)
"""
@spec unmount(String.t(), keyword()) :: :ok | {:error, {atom(), integer()}}
def unmount(path, opts \\ []) do
force = Keyword.get(opts, :force, false)
ExNetfs.Native.unmount_volume(path, force)
end
@doc """
Check if a path is currently a mount point.
## Examples
true = ExNetfs.mounted?("/Volumes/share")
false = ExNetfs.mounted?("/tmp/not_a_mount")
"""
@spec mounted?(String.t()) :: boolean()
def mounted?(path) do
ExNetfs.Native.is_mounted(path)
end
@doc """
List all currently mounted network filesystems (SMB, NFS, AFP, WebDAV).
Returns a list of `{remote_path, mount_point, filesystem_type}` tuples.
## Examples
[
{"//fileserver/share", "/Volumes/share", "smbfs"},
{"nfsserver:/export", "/Volumes/export", "nfs"}
] = ExNetfs.list_mounts()
"""
@spec list_mounts() :: [{String.t(), String.t(), String.t()}]
def list_mounts do
ExNetfs.Native.list_network_mounts()
end
# ==================== Convenience Functions ====================
@doc """
Mount an SMB share using Kerberos, suppressing UI dialogs.
This is the common case for AD-joined Macs where `ExKrb5.kinit/2` has
already been called to populate the Kerberos credential cache.
## Examples
:ok = ExKrb5.kinit("user@AD.EXAMPLE.COM", password)
{:ok, [path]} = ExNetfs.mount_smb_kerberos("smb://fileserver.ad.example.com/home$")
"""
@spec mount_smb_kerberos(String.t(), keyword()) :: {:ok, [String.t()]} | {:error, {atom(), integer()}}
def mount_smb_kerberos(url, opts \\ []) do
mount(url, Keyword.merge([open_options: [:no_ui], mount_options: [:soft_mount]], opts))
end
@doc """
Mount a share hidden from Finder at a specific path.
Useful for programmatic mounts that shouldn't clutter the user's sidebar.
## Examples
{:ok, _} = ExNetfs.mount_hidden("smb://server/share", "/Volumes/.hidden_share")
"""
@spec mount_hidden(String.t(), String.t(), keyword()) :: {:ok, [String.t()]} | {:error, {atom(), integer()}}
def mount_hidden(url, mountpath, opts \\ []) do
mount(url, Keyword.merge([
mountpath: mountpath,
open_options: [:no_ui],
mount_options: [:no_browse, :mount_at_dir, :soft_mount]
], opts))
end
@doc """
Mount as guest (no authentication).
## Examples
{:ok, [path]} = ExNetfs.mount_guest("smb://publicserver/publicshare")
"""
@spec mount_guest(String.t(), keyword()) :: {:ok, [String.t()]} | {:error, {atom(), integer()}}
def mount_guest(url, opts \\ []) do
mount(url, Keyword.merge([open_options: [:guest, :no_ui]], opts))
end
@doc """
Force unmount a stuck share.
Equivalent to `unmount(path, force: true)`.
"""
@spec force_unmount(String.t()) :: :ok | {:error, {atom(), integer()}}
def force_unmount(path) do
unmount(path, force: true)
end
@doc """
Find the mount point for a given server/share, if currently mounted.
## Examples
{:ok, "/Volumes/share"} = ExNetfs.find_mount("fileserver", "share")
:not_found = ExNetfs.find_mount("offline-server", "share")
"""
@spec find_mount(String.t(), String.t()) :: {:ok, String.t()} | :not_found
def find_mount(server, share) do
server_lower = String.downcase(server)
share_lower = String.downcase(share)
case Enum.find(list_mounts(), fn {remote, _mount, _type} ->
remote_lower = String.downcase(remote)
String.contains?(remote_lower, server_lower) and String.contains?(remote_lower, share_lower)
end) do
{_remote, mount_point, _type} -> {:ok, mount_point}
nil -> :not_found
end
end
end