Backends
A "backend" in vpt-rs is a concrete implementation of the five core traits for a specific platform and volume management technology. This page explains how backends are organized, selected at compile time and runtime, and how to add a new one.
What is a backend?
A backend is a struct that implements all five traits: Backend,
SnapshotProvider, BackupExecutor, RestorePlanner, and MountManager.
Each backend wraps a native volume management tool and translates the
generic plan types into platform-specific commands.
| Backend | Native tool | Snapshot mechanism | Backup mechanism |
|---|---|---|---|
BtrfsBackend | btrfs CLI | btrfs subvolume snapshot | btrfs send (stream) |
LvmBackend | lvcreate/lvremove | lvcreate --snapshot | copy_blocks() (block copy) |
ZfsBackend | zfs CLI | zfs snapshot | zfs send (stream) |
WindowsBackend | VSS COM API | VSS snapshot | Block-level copy |
MacOsBackend | (stub) | Unsupported | Unsupported |
UnixBackend | (stub) | Unsupported | Unsupported |
CurrentBackend type alias
The platform module (src/platform/mod.rs) defines a CurrentBackend type
alias that resolves to exactly one backend per target OS. When you call
platform::current_backend(), it returns CurrentBackend::default():
// src/platform/mod.rs:53-55
pub fn current_backend() -> CurrentBackend {
CurrentBackend::default()
}
The type alias is selected at compile time:
// src/platform/mod.rs:14-25
#[cfg(target_os = "linux")]
pub use linux::LinuxBackend as CurrentBackend;
#[cfg(target_os = "macos")]
pub use macos::MacOsBackend as CurrentBackend;
#[cfg(all(
target_family = "unix",
not(target_os = "linux"),
not(target_os = "macos")
))]
pub use unix::UnixBackend as CurrentBackend;
#[cfg(target_os = "windows")]
pub use windows::WindowsBackend as CurrentBackend;
Usage:
let backend = vpt_rs::platform::current_backend();
println!("Running on: {}", backend.backend_name());
CurrentBackend is a concrete type, not a trait object -- zero dynamic
dispatch overhead. The compiler knows the exact type and can inline method
calls.
BackendDescriptor
A BackendDescriptor is a lightweight metadata struct for enumerating
available backends at runtime. It is defined in src/platform/mod.rs:45-51:
// src/platform/mod.rs:45-51
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackendDescriptor {
pub platform: &'static str,
pub provider_name: Option<&'static str>,
pub backend_name: &'static str,
pub capabilities: &'static [Capability],
}
The available_backend_descriptors() function returns all backends for the
current platform:
// src/platform/mod.rs:73-82
pub fn available_backend_descriptors() -> Vec<BackendDescriptor> {
#[cfg(target_os = "linux")]
{
linux::available_descriptors()
}
#[cfg(not(target_os = "linux"))]
{
vec![current_backend_descriptor()]
}
}
On Linux this returns three descriptors (btrfs, lvm, zfs). On other platforms it returns exactly one.
let descriptors = vpt_rs::platform::available_backend_descriptors();
for desc in &descriptors {
println!("[{}] {} (provider: {:?}, {} capabilities)",
desc.platform,
desc.backend_name,
desc.provider_name,
desc.capabilities.len()
);
}
// On Linux output:
// [linux] linux-btrfs (provider: Some("btrfs"), 4 capabilities)
// [linux] linux-lvm (provider: Some("lvm"), 4 capabilities)
// [linux] linux-zfs (provider: Some("zfs"), 5 capabilities)
LinuxBackend: the enum + delegate! pattern
Linux is unique because three snapshot technologies coexist. LinuxBackend
(src/platform/linux/mod.rs:34-39) is an enum that wraps all three:
// src/platform/linux/mod.rs:34-39
#[derive(Debug, Clone)]
pub enum LinuxBackend {
Btrfs(BtrfsBackend),
Lvm(LvmBackend),
Zfs(ZfsBackend),
}
The default is Btrfs (src/platform/linux/mod.rs:21):
// src/platform/linux/mod.rs:21
pub const DEFAULT_PROVIDER: &str = "btrfs";
Every trait method delegates to the inner variant using the delegate! macro
(src/platform/linux/mod.rs:24-32):
// src/platform/linux/mod.rs:24-32
macro_rules! delegate {
($self:expr, $method:ident $(, $arg:expr)*) => {
match $self {
Self::Btrfs(inner) => inner.$method($($arg),*),
Self::Lvm(inner) => inner.$method($($arg),*),
Self::Zfs(inner) => inner.$method($($arg),*),
}
};
}
This macro is used in every trait implementation. For example:
// src/platform/linux/mod.rs:95-107
impl SnapshotProvider for LinuxBackend {
fn create_snapshot(&self, request: &SnapshotRequest) -> Result<SnapshotInfo> {
delegate!(self, create_snapshot, request)
}
fn delete_snapshot(&self, snapshot: &SnapshotHandle) -> Result<()> {
delegate!(self, delete_snapshot, snapshot)
}
fn list_snapshots(&self, source: &VolumeRef) -> Result<Vec<SnapshotInfo>> {
delegate!(self, list_snapshots, source)
}
}
Selecting a provider at runtime
You can explicitly select a provider by name:
// src/platform/linux/mod.rs:42-51
let backend = LinuxBackend::named("zfs")?; // returns ZfsBackend
The named() method returns Err(Error::InvalidArgument) for unknown
provider names. Valid names are "btrfs", "lvm", and "zfs".
You can also get all available backends:
// src/platform/linux/mod.rs:53-59
let backends = LinuxBackend::available();
for backend in &backends {
println!("{}", backend.provider_name());
}
LinuxBackend::default() uses DEFAULT_PROVIDER which is "btrfs". If you
need a different default, use LinuxBackend::named() explicitly.
WindowsBackend with feature gates
The Windows backend uses Cargo feature flags to control VSS support. The
relevant code in src/platform/mod.rs:
// src/platform/mod.rs:26-27
#[cfg(all(target_os = "windows", feature = "windows-vss"))]
pub use windows::vss::VssSnapshotProvider;
With features = ["windows-vss"] enabled, snapshot and backup operations
delegate to the real VSS provider. Without the feature, they return
Error::UnsupportedOperation.
The windows-vss feature requires the Windows SDK and COM interop. It is only
available on Windows build targets. The VSS provider coordinates with
application writers to create application-consistent snapshots -- the only
backend that supports Capability::ApplicationConsistentSnapshot.
StubBackend for macOS/Unix
StubBackend (src/platform/mod.rs:88-170) implements all five traits but
returns Error::UnsupportedOperation for every operational method. macOS and
generic Unix backends wrap it as their inner implementation.
Here is the full implementation:
// src/platform/mod.rs:88-120
#[derive(Debug, Clone, Default)]
pub struct StubBackend {
backend_name: &'static str,
capabilities: &'static [Capability],
}
impl StubBackend {
pub const fn new(backend_name: &'static str, capabilities: &'static [Capability]) -> Self {
Self {
backend_name,
capabilities,
}
}
pub fn descriptor(&self) -> BackendDescriptor {
BackendDescriptor {
platform: current_platform(),
provider_name: None,
backend_name: self.backend_name,
capabilities: self.capabilities,
}
}
}
impl Backend for StubBackend {
fn backend_name(&self) -> &'static str {
self.backend_name
}
fn capabilities(&self) -> &'static [Capability] {
self.capabilities
}
}
Every operational method follows the same pattern:
// src/platform/mod.rs:122-140
impl SnapshotProvider for StubBackend {
fn create_snapshot(&self, _request: &SnapshotRequest) -> Result<SnapshotInfo> {
let error = unsupported("create_snapshot", self.backend_name);
error!(backend = self.backend_name, error = %error, "create_snapshot failed");
Err(error)
}
fn delete_snapshot(&self, _snapshot: &SnapshotHandle) -> Result<()> {
let error = unsupported("delete_snapshot", self.backend_name);
error!(backend = self.backend_name, error = %error, "delete_snapshot failed");
Err(error)
}
fn list_snapshots(&self, _source: &VolumeRef) -> Result<Vec<SnapshotInfo>> {
let error = unsupported("list_snapshots", self.backend_name);
error!(backend = self.backend_name, error = %error, "list_snapshots failed");
Err(error)
}
}
The unsupported() helper function (src/platform/mod.rs:84-86) creates a
structured Error::UnsupportedOperation with the operation name and backend
name. This means even stub errors carry useful context for debugging.
Backend selection flowchart
LinuxBackend dispatch diagram
When a method is called on LinuxBackend, the delegate! macro dispatches to
the correct inner backend:
How to add a new backend
Here is a step-by-step guide for adding a new backend (e.g. FreeBSD ZFS):
Step 1: Create the backend file
Create src/platform/freebsd.rs:
// src/platform/freebsd.rs
use super::StubBackend;
use crate::types::Capability;
const CAPABILITIES: &[Capability] = &[
Capability::CrashConsistentSnapshot,
Capability::BlockLevelBackup,
Capability::BlockLevelRestore,
];
#[derive(Debug, Clone)]
pub struct FreeBSDBackend(StubBackend);
impl FreeBSDBackend {
pub fn new() -> Self {
Self(StubBackend::new("freebsd-zfs", CAPABILITIES))
}
}
Step 2: Implement the Backend trait
use crate::backend::Backend;
impl Backend for FreeBSDBackend {
fn backend_name(&self) -> &'static str {
self.0.backend_name()
}
fn capabilities(&self) -> &'static [Capability] {
self.0.capabilities()
}
}
Step 3: Implement operational traits (start with stubs)
use crate::snapshot::SnapshotProvider;
use crate::error::{Error, Result};
use crate::types::*;
impl SnapshotProvider for FreeBSDBackend {
fn create_snapshot(&self, _request: &SnapshotRequest) -> Result<SnapshotInfo> {
Err(Error::UnsupportedOperation {
operation: "create_snapshot",
backend: self.backend_name(),
})
}
fn delete_snapshot(&self, _snapshot: &SnapshotHandle) -> Result<()> {
Err(Error::UnsupportedOperation {
operation: "delete_snapshot",
backend: self.backend_name(),
})
}
fn list_snapshots(&self, _source: &VolumeRef) -> Result<Vec<SnapshotInfo>> {
Err(Error::UnsupportedOperation {
operation: "list_snapshots",
backend: self.backend_name(),
})
}
}
// Implement BackupExecutor, RestorePlanner, MountManager similarly...
Step 4: Register in the platform module
// src/platform/mod.rs -- add these lines:
#[cfg(target_os = "freebsd")]
mod freebsd;
#[cfg(target_os = "freebsd")]
pub use freebsd::FreeBSDBackend as CurrentBackend;
Step 5: Add real implementations incrementally
Replace the stub implementations one method at a time. Each backend file is
self-contained -- you define internal plan types, implement plan_* methods
for validation, and run_* methods for execution.
Start with a stub and add real logic incrementally. The capability system lets callers discover what works at runtime, so a stubbed backend is still useful. Declare only the capabilities that actually work.
When adding a new backend, write tests for the plan_* methods first. They
do not require root privileges or real volumes. The test pattern used by
BtrfsBackend, LvmBackend, and ZfsBackend creates temporary directories
and validates the plan structs without executing any commands.
Next steps
- Capabilities -- what each backend supports
- Plans -- how plans work with backends
- Error Handling -- how backend errors are structured
- Traits -- the traits that backends implement