# ol.clave.storage.file

Filesystem-based storage implementation.

Provides a [`FileStorage`](#filestorage) record that implements [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage)
and [`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker) using the local filesystem.
The presence of a lock file for a given name indicates a lock is held.

## Key-to-Path Mapping

Storage keys map directly to filesystem paths relative to a root directory.
Key normalization converts backslashes to forward slashes and strips leading
and trailing slashes.
Path traversal attempts (keys containing `..` that escape the root) throw
`IllegalArgumentException`.

## Atomic Writes

All writes use atomic move semantics: data is written to a temporary file
and renamed into place.
This ensures readers never see partial writes.
On filesystems that do not support atomic moves, a best-effort rename is used.

## Locking

Advisory locking is implemented with lock files in a `locks/` subdirectory.
Locks are created atomically by relying on the filesystem to enforce
exclusive file creation.

Processes that terminate abnormally will not have a chance to clean up their
lock files.
To handle this, while a lock is actively held, a background virtual thread
periodically updates a timestamp in the lock file (every 5 seconds).
If another process tries to acquire the lock but fails, it checks whether
the timestamp is still fresh.
If so, it waits by polling (every 1 second).
Otherwise, the stale lock file is deleted, effectively forcing an unlock.

While lock acquisition is atomic, unlocking is not perfectly atomic.
Filesystems offer atomic file creation but not necessarily atomic deletion.
It is theoretically possible for two processes to discover the same stale
lock and both attempt to delete it.
If one process deletes the lock file and creates a new one before the other
calls delete, the new lock may be deleted by mistake.
This means mutual exclusion is not guaranteed to be perfectly enforced in
the presence of stale locks.
However, these cases are rare, and we prefer the simpler solution over
alternatives that risk infinite loops.

## Filesystem Considerations

This implementation is designed for local filesystems and relies on specific
filesystem semantics:

* Exclusive file creation via `O_CREAT | O_EXCL`.
  Lock acquisition depends on the filesystem atomically failing when creating
  a file that already exists.
* Durable writes via `fsync`.
  Lock file timestamps must survive crashes to enable stale lock detection.

Network filesystems (NFS, CIFS/SMB, AWS EFS) may not reliably support these
semantics.
In particular, some network filesystems do not honor `O_EXCL` across nodes or
do not guarantee that data is persisted after `fsync`, which can leave lock
files empty or corrupt after a crash or network interruption.

## Permissions

On POSIX systems, files are created with `rw-------` and directories with
`rwx------`.
On non-POSIX systems (Windows), default permissions apply.

## Usage

```clojure
(require '[ol.clave.storage.file :as fs]
         '[ol.clave.storage :as s])

;; Use platform-appropriate directories for certificate storage
(def storage (fs/file-storage {:root (fs/data-dir "myapp")}))

;; Or specify a custom path
(def storage (fs/file-storage {:root "/var/lib/myapp"}))

;; Store and retrieve data
(s/store-string storage nil "certs/example.com/cert.pem" cert)
(s/load-string storage nil "certs/example.com/cert.pem")
```

## Related Namespaces

* [`ol.clave.storage`](api/ol-clave-storage.adoc) - Storage protocol and utilities
* [`ol.clave.lease`](api/ol-clave-lease.adoc) - Cooperative cancellation

## ->LockMeta

```clojure
(->LockMeta created-ms updated-ms)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)

---

## LockMeta

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)

---

## map->LockMeta

```clojure
(map->LockMeta m)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)

---

## ->FileStorage

```clojure
(->FileStorage root)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)

---

## FileStorage

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)

---

## map->FileStorage

```clojure
(map->FileStorage m)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)

---

## home-dir

```clojure
(home-dir)
```

Returns the user’s home directory.

Uses the `user.home` system property, which the JVM resolves appropriately
for each platform.
Returns `nil` if the home directory cannot be determined.

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L442-L449)

---

## state-dir

```clojure
(state-dir)
(state-dir app-name)
```

Returns the directory for persistent application state.

With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).

Checks environment variables in order:
1. `$STATE_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_STATE_HOME` - XDG base directory spec / systemd user units
3. Platform default

When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.

Platform defaults when no environment variable is set:

|     |     |
| --- | --- |
| platform | path |
| Linux | `$HOME/.local/state` |
| macOS | `$HOME/Library/Application Support` |
| Windows | `%LOCALAPPDATA%` |

Returns `nil` if a suitable directory cannot be determined.

```clojure
(state-dir)           ; => "/home/user/.local/state"
(state-dir "myapp")  ; => "/home/user/.local/state/myapp"

;; Under systemd system unit with StateDirectory=myapp:
(state-dir)           ; => "/var/lib/myapp"
(state-dir "myapp")  ; => "/var/lib/myapp" (no double append)

(file-storage {:root (state-dir "myapp")})
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L451-L500)

---

## config-dir

```clojure
(config-dir)
(config-dir app-name)
```

Returns the directory for application configuration files.

With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).

Checks environment variables in order:
1. `$CONFIGURATION_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_CONFIG_HOME` - XDG base directory spec / systemd user units
3. Platform default

When systemd sets `$CONFIGURATION_DIRECTORY`, it may contain multiple
colon-separated paths if the unit configures multiple directories; this
function returns the first path.

Platform defaults when no environment variable is set:

|     |     |
| --- | --- |
| platform | path |
| Linux | `$HOME/.config` |
| macOS | `$HOME/Library/Preferences` |
| Windows | `%APPDATA%` |

Returns `nil` if a suitable directory cannot be determined.

```clojure
(config-dir)           ; => "/home/user/.config"
(config-dir "myapp")  ; => "/home/user/.config/myapp"

;; Under systemd system unit with ConfigurationDirectory=myapp:
(config-dir)           ; => "/etc/myapp"
(config-dir "myapp")  ; => "/etc/myapp" (no double append)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L502-L549)

---

## cache-dir

```clojure
(cache-dir)
(cache-dir app-name)
```

Returns the directory for application cache files.

With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).

Checks environment variables in order:
1. `$CACHE_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_CACHE_HOME` - XDG base directory spec / systemd user units
3. Platform default

When systemd sets `$CACHE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.

Platform defaults when no environment variable is set:

|     |     |
| --- | --- |
| platform | path |
| Linux | `$HOME/.cache` |
| macOS | `$HOME/Library/Caches` |
| Windows | `%LOCALAPPDATA%` |

Returns `nil` if a suitable directory cannot be determined.

```clojure
(cache-dir)           ; => "/home/user/.cache"
(cache-dir "myapp")  ; => "/home/user/.cache/myapp"

;; Under systemd system unit with CacheDirectory=myapp:
(cache-dir)           ; => "/var/cache/myapp"
(cache-dir "myapp")  ; => "/var/cache/myapp" (no double append)
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L551-L598)

---

## data-dir

```clojure
(data-dir)
(data-dir app-name)
```

Returns the directory for persistent application data.

This is the recommended directory for ACME certificates and keys because they
are valuable cryptographic material with rate limits on reissuance.

With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).

Checks environment variables in order:
1. `$STATE_DIRECTORY` - set by systemd (`StateDirectory=` maps to `/var/lib/` which is semantically correct)
2. `$XDG_DATA_HOME` - XDG base directory spec
3. Platform default

When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.

Platform defaults when no environment variable is set:

|     |     |
| --- | --- |
| platform | path |
| Linux | `$HOME/.local/share` |
| macOS | `$HOME/Library/Application Support` |
| Windows | `%LOCALAPPDATA%` |

Returns `nil` if a suitable directory cannot be determined.

```clojure
(data-dir)           ; => "/home/user/.local/share"
(data-dir "myapp")  ; => "/home/user/.local/share/myapp"

;; Under systemd system unit with StateDirectory=myapp:
(data-dir)           ; => "/var/lib/myapp"
(data-dir "myapp")  ; => "/var/lib/myapp" (no double append)

(file-storage {:root (data-dir "myapp")})
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L600-L652)

---

## file-storage

```clojure
(file-storage)
(file-storage opts)
```

Creates a [`FileStorage`](#filestorage).

With no arguments, storage defaults to the `"ol.clave"` subdirectory
inside [`data-dir`](#data-dir).

With one argument, pass an opts map.

Options:

| key     | description
|---------|-------------
| `:root` | Required root directory as a string or `java.nio.file.Path`

The root directory is created if it does not exist.

Returns a record implementing [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage) and
[`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker).

Example:

```clojure
(file-storage {:root "/var/lib/myapp"})
```

[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L654-L692)
