Content Retention
Content retention lets an administrator set a per-virtual folder policy that automatically deletes files older than a chosen age. It is designed for the common regulatory pattern — transaction files purged after 90 days, log archives after 30, receipts kept for seven years. Retention is off by default; existing folders behave exactly as before the feature was introduced until an operator enables it explicitly.
Retention is available on every edition. It is a compliance feature, not a paid tier.
The four dimensions
A retention policy is defined by four values plus a master toggle. All five settings live on the virtual folder itself and are edited from the Virtual Folders tab on the Web Admin Console, the Swing Admin Console, or the CLI.
| Setting | Values | What it means |
|---|---|---|
retention_enabled | on / off | Master toggle. When off, the other four values are preserved but ignored, so a temporary pause does not lose the operator's settings. |
retention_days | integer, minimum 3 | Files older than this many days are staged for deletion. |
retention_anchor | CREATED or MODIFIED | Which timestamp determines "old". CREATED uses the file's creation time; MODIFIED uses the last-modified time. |
retention_scope | RECURSIVE or ROOT_ONLY | Whether the whole tree under the folder is swept, or only the files directly under the folder's real path. |
retention_pattern | Java regex, optional | If set, only paths matching the pattern are candidates. The match is against the file's path relative to the folder root, with forward slashes on every OS. |
Regex examples
.*\.log$ — only .log files
archive/.* — everything under archive/
(?i).*/invoice-.*\.pdf — invoices (case-insensitive)
.*\.tmp$ — cleanup for stray .tmp files
The pattern is a full-anchor match (Matcher.matches()) so
you do not need a leading ^ or trailing $.
Blank pattern matches every file.
The 24-hour grace bin
Deletion is a two-step operation. When a file becomes
eligible, the retention worker moves it into a hidden
.retention/ staging area sitting on the same volume as the
folder's real path. The file stays there for 24 hours
before it is finally deleted from disk. During that grace window an
administrator can:
- Restore the file back to its original location (atomic rename, zero data loss).
- Delete Now to reap the file immediately without waiting for the grace window to expire.
The grace bin is what makes retention safe. A typo in the policy — wrong day count, too broad a regex — does not delete anything for a full day. The bin also cannot be bypassed: even a force-delete goes through the same audit trail.
The staging area is a hidden .retention/ directory under
the data folder (${STYLUS_SFTPSERVER_DATA}/.retention/).
Each staged file mirrors its original physical path
with the drive letter URL-encoded (F: becomes
F%3A) and a millisecond-precision UTC timestamp appended
to the leaf. Example:
Original: F:\FTPRoots\uploads\acme\foo.pdf
Staged: <data>\.retention\F%3A\FTPRoots\uploads\acme\foo.pdf.2026-07-22-14-30-15-123
Mirroring the physical layout means an operator browsing the staging
area on disk can always tell which file came from where without a DB
lookup. The timestamp suffix guarantees uniqueness even if a file with
the same path is re-uploaded and re-retained later. The staging root
uses the same drive-letter-encoding convention as the upload
.staging/ directory.
End-user visibility in the File Portal
Retention is configured by administrators, but the users whose files it governs can see it too. The Web File Transfer Portal file grid includes an Expires column: for every file subject to a retention policy it shows the date the file is scheduled to be deleted — the file's created or last-modified time (per the policy's anchor) plus the retention period. The value is shown in the user's local time zone with a relative hint and a colour cue (red within 24 hours, amber within three days). Files not subject to any policy, and all folders, show a dash.
The Expires date always reflects the retention rule that actually applies to the file — the same basis the worker uses to decide what to delete — so it is shown correctly even when a user reaches the file by browsing a shared folder that sits above the folder carrying the policy. This gives end users a chance to download or refresh a file before it ages out.
What the worker skips
The retention worker never touches:
- Anything inside the
.retention/or.staging/directories — these are the worker's own grace bin and the upload lifecycle's in-flight area, both of which live outside the VD tree. - Symbolic links — the walker does not follow them.
- Files it cannot read (permission denied, in use). A warning is logged and the file is retried on the next tick.
Files whose name ends in .tmp are not
skipped — they are subject to the same policy as any other
file. If you specifically want to target .tmp files for
retention (e.g. to clean up abandoned third-party staging leftovers),
set the file-path pattern to .*\.tmp$.
Editing a policy from the Web Admin Console
- Open the Virtual Folders tab.
- Select the folder in the top grid.
- Click Edit on the folder toolbar. The Content retention section is at the bottom of the dialog.
- Tick Enable content retention to unlock the four gated fields.
- Set the values and click Save.
The Grace bin (retention pending) tab, below the folders grid, shows the current staging area for the selected folder. Each row lists the original relative path, the file size, when it was staged, when it will be deleted, and the reason the worker picked it. Use the Restore or Delete Now buttons to act on a row.
Editing a policy from the Swing Admin Console
Same shape as the Web version. Select the folder in the left list, scroll to the Content retention section on the right, tick the master checkbox, adjust the values, and click Save folder. The grace bin is a sibling tab of Groups granted access under the folder detail pane.
Editing a policy from the CLI
admin folder-retention /releases/ --enable --days 30 --anchor MODIFIED --scope RECURSIVE
admin folder-retention /releases/ --enable --days 90 --pattern ".*\.log$"
admin folder-retention /releases/ --disable
When --disable is used the days / anchor / scope /
pattern values are preserved, so re-enabling later picks up
right where it left off. That is by design.
Grace bin actions:
admin folder-retention-pending /releases/
admin folder-retention-restore /releases/ 42
admin folder-retention-delete /releases/ 42
The integer argument in the last two commands is the
retention_id shown in the pending list.
The Content Retention admin page
Both admin consoles carry a dedicated Content Retention page (between Virtual Folders and Audit). It gives an at-a-glance view of the worker's health, the current schedule, and everything in the grace bin across every VD.
Summary
The summary block at the top of the page shows:
- Stage / real path — the physical location under the data folder where staged files live.
- Sweep interval — how often the worker ticks (default 1 hour).
- Grace window — how long a staged file waits before hard delete (24 hours).
- Last run — timestamp of the most recent scheduled tick.
- Next run — when the next scheduled tick will fire.
- Pending in grace bin — total items in the staging area, plus a highlighted count of items whose grace window has already closed and will be reaped on the next tick.
Recent runs
A history table lists the last 50 worker ticks with started/ended timestamps, duration, and the number of files staged, reaped, and warnings per tick. The Origin column tags each row as Scheduled or On demand so a Run-Now action is distinguishable from a normal timer tick.
Run Now
The Run Now button next to Next run fires
one tick immediately. It does not shift the scheduled cadence
— the next scheduled tick still fires at its original time. The
row it writes to the run history is marked ON_DEMAND so
the summary card's Last-run / Next-run values keep reflecting the
timer's real cadence.
Run Now is useful for testing a freshly-changed policy (no need to wait up to an hour to see whether a file gets staged), for clearing a backlog that has piled up during off-hours, or for QA workflows on non-production installs.
Global Grace Bin
The Grace Bin tab is a global view of every staged file across every VD, sorted so the most-urgent rows appear first. Columns: Folder, Path, Size, Staged at, Delete after, Countdown, and Reason.
The Countdown column is the key operator-facing signal. It is colored:
- Red bold — will be reaped on the next scheduled tick (grace window has already closed).
- Amber — less than 6 hours remaining.
- Grey — everything else.
A Show filter above the table restricts the list to items that will be reaped within a chosen horizon (1 h / 6 h / 24 h / All). Server-side paging keeps payloads bounded regardless of backlog size; the default page holds 500 rows.
The row toolbar carries the same Restore and Delete Now actions as the per-VD grace bin: Restore atomically moves the file back to its original location, Delete Now force-reaps before the grace window ends. Both actions are audited.
CLI equivalents
admin retention-status # summary + last 20 runs
admin retention-run # Run Now — one ON_DEMAND tick
admin retention-pending-all # global Grace Bin, terminal-formatted
admin retention-pending-all --reaps-in 6 # filter: reaps in the next 6 h
admin retention-pending-all --limit 200 # up to 200 rows
Audit
Four event types are emitted:
| Event | When | Target |
|---|---|---|
FOLDER_RETENTION_UPDATED | Policy changed | Folder path |
FILE_STAGED_FOR_RETENTION | Worker moved a file into the grace bin | Original absolute path |
FILE_RESTORED_FROM_RETENTION | Admin restored a staged file | Restored path |
FILE_DELETED_BY_RETENTION | File removed from disk (grace window expired or admin force-delete) | Staged path |
Every event records the actor (the RETENTION_WORKER for automatic actions, the administrator's username for manual ones). See the Audit & Logging chapter for the full list of audit event types.
Scheduling
The retention worker runs every hour by default. Setting a policy or changing one takes effect on the next tick; there is no need to restart the server. If the grace bin has entries whose 24-hour window has expired, they are reaped on the same tick.
The interval can be shortened for testing with the JVM system
property stylus.sftpserver.retention-worker.disabled=true
(disables the worker entirely). No knob is exposed for a shorter
interval in production; the once-per-hour cadence is fine even for
folders with millions of files because the walker skips the whole
tree if the policy is off.
Edge cases
| Scenario | Behaviour |
|---|---|
| Invalid regex saved when disabled | Rejected. The regex is compiled even in the off state so a broken pattern cannot lie in wait. |
retention_days below 3 | Rejected. Enforced by the server and the UI. The floor guards against typos like 0 or 1 deleting an entire folder immediately. |
| Grace bin is on a different volume than the folder | The worker requires the .retention/ directory to share a filesystem with the folder's real path so the move can be atomic. If that is not possible (rare on Windows with symlinked drives), the worker logs a warning and skips the file until the layout is fixed. |
| Folder deleted while items are in the grace bin | The delete is refused until the pending items are restored or reaped. The admin console surfaces this as an error. |
| Two admins edit the same policy at once | Last write wins. No lock is taken; the whole edit is a single row update. |
See also
- Organizations, Groups, and Virtual Folders — virtual folders are the object retention attaches to.
- Filesystem & Uploads — upload lifecycle and the
.staging/directory. The retention worker uses the same "hidden sibling directory" pattern. - Audit & Logging — where retention events show up.