File Storage Backend¶
den/storage/file is the reference Storage backend. It writes attachment bytes to the local filesystem under a configurable root directory, addressed by content hash.
Construction¶
- First argument: filesystem root where bytes are stored.
- Second argument: HTTP URL prefix for
Storage.URL().
Importing the package for its side effect also registers the file:// scheme with storage.OpenURL, so configuration-driven setups can use either form interchangeably:
import (
"github.com/oliverandrich/den/storage"
_ "github.com/oliverandrich/den/storage/file"
)
fs, err := storage.OpenURL("file:///uploads?url_prefix=/media")
DSN syntax¶
The file:// DSN follows the same SQLAlchemy/JDBC convention as sqlite://:
| DSN | Path handed to the filesystem |
|---|---|
file:///data/media |
data/media (relative, 3 slashes) |
file:////var/media |
/var/media (absolute, 4 slashes) |
One leading slash is stripped on parse so standard URL libraries (Go net/url, Python urllib.parse) can tokenise the DSN with the authority component staying empty. Direct construction via file.New(...) takes the filesystem path literally — no stripping.
The HTTP URL prefix is set via the url_prefix query parameter:
| DSN | Storage.URL() prefix |
|---|---|
file:///uploads?url_prefix=/media |
/media |
file:///uploads |
empty (URLs return as relative paths under the root) |
url_prefix is consumed by the storage registry and never reaches the file backend's parser, so other backends (S3) can honor or ignore the same query param uniformly.
Object layout¶
The generated path is YYYY/MM/<first-16-of-sha256><ext> — grouped by month, content-addressed, dedup-on-write. Two uploads of the same bytes in the same month resolve to the same path; the second Store returns the existing path instead of duplicating the file on disk.
Security-relevant behaviour¶
- Path traversal is refused —
OpenandDeleteuseos.Root(Go 1.24+). AStoragePaththat escapes the root via..or symlinks cannot read anything outside the configured directory. - Empty uploads are rejected —
Storeon a zero-byte reader returnsstorage.ErrEmptyContent. - Delete is idempotent — a missing path returns success, simplifying cleanup orchestration against the document lifecycle.
- Atomic dedup —
Storeusesos.Linkto install the temp upload at the final path;fs.ErrExistis treated as a successful dedup hit, which closes the read-then-rename TOCTOU window.
URL-prefix accessor¶
The filesystem backend exposes its HTTP prefix via URLPrefix() string:
HTTP-layer packages (burrow/uploader, custom handlers) use this to mount a serving handler on the same route that URL produces, without having the prefix configured twice. Remote backends (S3, GCS) intentionally do NOT implement this method — the absent URLPrefix() is the signal that the Storage is responsible for serving and the HTTP package should skip local routing.