Skip to content

Core Concepts

PolicyFS presents a single FUSE mountpoint over multiple physical storage paths. Routing rules control which storage handles reads and writes for each virtual path. An optional SQLite index lets metadata operations skip spinning disks entirely.

Storage paths

A storage path is a physical directory root. Each storage path has:

Field Description
id Stable identifier used in config, logs, and the index database.
path Absolute filesystem path (e.g. /mnt/hdd1/media).
indexed When true, metadata is served from the SQLite index instead of the physical disk. Mutations are deferred and applied later by pfs prune.
min_free_gb Minimum free space (GiB) required before PolicyFS will write new files to this storage.

Storage groups

A storage group is a named list of storage path IDs. Groups simplify routing rules and mover configuration — you can reference ssds instead of listing ssd1, ssd2 everywhere.

storage_groups:
  ssds: [ssd1, ssd2]
  hdds: [hdd1, hdd2, hdd3]

Groups are expanded when routing rules and mover jobs are evaluated.

Routing rules

Routing rules map virtual paths to storage targets. Rules are evaluated top-to-bottom, first match wins.

A rule defines:

Field Description
match Glob pattern matched against the virtual path. ** matches any depth.
read_targets Storage IDs or group names used for reads and directory listings.
write_targets Storage IDs or group names used for writes (create, link, rename).
write_policy How to pick one write target from the candidates — see below.
path_preserving When true, prefer write targets where the parent directory already exists.

Shorthand: targets sets both read_targets and write_targets at once.

Catch-all rule required

The last routing rule must be the catch-all pattern **. PolicyFS rejects configs without it.

Write policies

When a write operation needs a single target, the write policy selects one from the resolved candidates (after min_free_gb filtering):

Policy Behavior
first_found Use the first eligible target in list order. This is the default.
most_free Use the target with the most free space.
least_free Use the target with the least free space.

Path-preserving writes

When path_preserving: true, PolicyFS prefers targets where the parent directory of the new file already exists on disk. If no target has the parent directory, all candidates remain eligible and the write policy decides.

This keeps related files together — for example, all files in library/movies/MovieA/ will land on the same disk.

Directory listings

Unlike reads and writes (first match wins), directory listings consider all routing rules whose pattern could match descendants of the listed directory. The result is the union of entries across all matching storage targets, deduplicated by name.

Indexed storage and deferred mutations

Storage paths with indexed: true are backed by a per-mount SQLite database.

Reads: Metadata operations (lookup, getattr, readdir) are served from the index, so the physical disk does not need to spin up.

Writes: Mutations on indexed paths are recorded in an event log (events.ndjson) instead of being applied immediately. The supported deferred event types are:

  • DELETE — unlink or rmdir
  • RENAME — file or directory rename
  • SETATTR — permission, ownership, or timestamp changes

These deferred operations are applied to the physical disk later by pfs prune.

Locks

PolicyFS uses file locks to prevent conflicts:

Lock Location Purpose
daemon.lock /run/pfs/<mount>/locks/ Ensures only one FUSE daemon runs per mount.
job.lock /run/pfs/<mount>/locks/ Ensures only one maintenance job (index, move, prune, maint) runs at a time per mount.

If a lock is already held, the command exits with code 75 (busy).

State and runtime directories

Directory Default Contents
Config /etc/pfs/pfs.yaml Main configuration file.
State /var/lib/pfs/<mount>/ Persistent data: SQLite index database, event logs.
Runtime /run/pfs/<mount>/ Ephemeral data: lock files, daemon control socket.