Filesystem & Uploads
This chapter describes how Stylus SFTP Server manages user home directories, controls access, handles the upload lifecycle, enforces disk quotas, and integrates with downstream systems.
Home Directories
Every user receives an isolated home directory. The root path is defined by
the <home-root> element in
filesystem-config.xml:
<home-root>${STYLUS_SFTPSERVER_HOME}/homes/${username}</home-root>
When a user connects, the server resolves the template and places them
in the resulting directory. The default path places each user under
homes/<username> within the installation root.
Path Macros
The following macros are resolved at runtime:
| Macro | Resolves To |
|---|---|
${STYLUS_SFTPSERVER_HOME} |
Server installation root directory |
${STYLUS_SFTPSERVER_DATA} |
Writable data directory (conf, db, homes, logs) |
${username} |
Authenticated SFTP username for the current session |
${env.VAR} |
Value of the environment variable VAR |
Relative paths in <home-root> are resolved against
STYLUS_SFTPSERVER_HOME.
Directory Creation
Home directories are auto-created on first login. If the
directory does not exist when the user authenticates, the server creates it
automatically. You can also pre-create directories via the admin command
CREATE_USER_HOME.
Users with read-write access can create subdirectories freely within their home directory.
Virtual Folders
Beyond the per-user home directory, Stylus SFTP Server can surface
virtual folders — logical paths (like /releases/)
mapped to physical directories on disk and granted to groups. Users see the
granted folders as siblings of their home, respecting per-grant permissions
(READ / WRITE / READ_WRITE). Which
channels surface them is controlled per-channel by mount-routing switches
in sftp-server.xml and admin-console.xml.
See the Organizations & Groups chapter for the full identity-model narrative and the corresponding admin workflow. Two rules worth flagging at the filesystem level:
- Single-segment paths only. A virtual folder path must
be either the root (
/) or one segment like/shared/. Nested paths (/a/b/c/) are rejected. Missing leading slashes are auto-prepended (shared→/shared). $shared$is reserved as a folder path segment. It cannot be used as either a logical or physical name and is rejected at input time.- MISSING behavior. If the physical directory backing a
virtual folder disappears (deleted out-of-band, volume unmounted), the
folder reconciler marks it
MISSING. Operations against a MISSING folder fail with a clear "folder is currently unavailable (contact admin)" error rather than an empty-folder facade — users know something is wrong, and the admin sees the same MISSING indicator in the Folder Health view.
Access Control
The <default-access> element in
filesystem-config.xml sets the baseline access level for all
users:
<default-access>read-write</default-access>
| Value | Effect |
|---|---|
read-write |
Users can upload, download, create subdirectories, and delete files |
read-only |
Users can only download files; uploads are rejected |
Per-User Overrides
The default access level can be overridden on a per-user basis through two mechanisms:
- JDBC provider: The
read_onlycolumn in thesftp_userstable. Set totrueto make a specific user read-only regardless of the default. - LDAP provider: AD/LDAP group membership determines access.
The server reads the user's
memberOfattribute and matches against the configured<access-groups>and<read-only-groups>lists.
LDAP Access Control Groups
Configure group mappings in the <ldap> block of
conf/sftp-server.xml:
| Configuration | Effect |
|---|---|
<access-groups> |
User can connect; home directory is read-write |
<read-only-groups> |
User can connect; home directory is read-only (download only) |
If a user matches both access and read-only groups, read-only
wins — the most restrictive policy takes precedence. If no
configured groups match (or no groups are configured), the server falls back
to the <default-access> setting. Group matching is
case-insensitive on the CN value.
Read-Only Rejection
When a read-only user attempts to upload a file, the server rejects
the operation with SSH_FX_PERMISSION_DENIED and records an
UPLOAD_REJECTED audit event.
Upload Lifecycle
Every upload goes through a controlled staging process that guarantees downstream consumers never see incomplete files:
- Staging write — When a client begins an upload,
the server writes the data to a staging file in a centralized
.staging/directory under the data root — never in the user's home directory. The staging file is namedfilename.UUID.tmp. The UUID ensures that two concurrent uploads of the same filename receive distinct staging files. - Isolated from users — Because staging files are in a separate directory, they never appear in any protocol's directory listing (SFTP, FTP, WebDAV, or the Web Portal). User home directories contain only user files.
- Atomic rename — On successful channel close,
the staging file is renamed to its final name using
ATOMIC_MOVE. This is a single filesystem operation — the file either appears in its entirety or not at all. - Duplicate handling — If a file with the target
name already exists, the server applies the configured
<on-duplicate>strategy (see below). - Abort cleanup — If the upload is aborted (client disconnects, error occurs), the staging file is deleted.
.staging/ directory and the final target directory
should reside on the same filesystem volume for
ATOMIC_MOVE to work. If they are on different volumes,
the server falls back to a non-atomic move and logs a warning.
Staging Guarantees
The UUID-based staging file naming means that even if two clients upload a file with the same name at the same time, each upload gets its own independent staging file. There is no contention or data corruption. The atomic rename at completion ensures that partial or corrupted files never appear in the user's directory — only fully transferred files are visible.
Rename Pattern
The <rename-pattern> element controls the final filename
after a successful upload:
<rename-pattern>${basename}</rename-pattern>
Available macros:
| Macro | Description | Example |
|---|---|---|
${username} |
Authenticated username | jdoe |
${basename} |
Original filename as sent by the client | report.csv |
${filename} |
Filename without extension | report |
${ext} |
Extension including the dot | .csv |
For example, to prefix every uploaded file with the username:
<rename-pattern>${username}_${basename}</rename-pattern>
Duplicate Handling
The <on-duplicate> element controls what happens when
the target filename already exists in the user's home directory:
<on-duplicate>timestamp-suffix</on-duplicate>
| Option | Behavior |
|---|---|
timestamp-suffix |
Appends _yyyyMMddHHmmss before the file extension.
For example, report.csv becomes
report_20260303172633.csv. If two files arrive
within the same second, a nanosecond fallback ensures unique
names. |
overwrite |
Silently replaces the existing file with the new upload. |
reject |
Closes the SFTP channel with an error. The upload is not committed. |
Staging Cleanup
On server startup, the entire .staging/ directory is wiped.
Everything in it is transient — if the server is starting, no
transfers are in progress. The same cleanup runs on orderly shutdown.
There is no background cleanup worker or age-based scanning. If the server crashes, leftover staging files are cleaned up automatically on the next boot.
Volume Guard
The volume guard is a server-level disk protection mechanism that prevents the storage volume from filling up. Unlike per-user quotas, the volume guard monitors the actual free space on the filesystem volume and rejects uploads when free space falls below a configurable threshold.
Configure the volume guard in filesystem-config.xml:
<volume-guard>
<min-free-bytes>1073741824</min-free-bytes> <!-- 1 GiB -->
<check-interval-bytes>1073741824</check-interval-bytes> <!-- 1 GiB -->
</volume-guard>
| Element | Default | Description |
|---|---|---|
min-free-bytes |
1073741824 (1 GiB) | Minimum free space to maintain on the volume. Uploads are
rejected when usable space falls below this threshold.
Set to 0 to disable. |
check-interval-bytes |
1073741824 (1 GiB) | How often to re-check free space during large uploads, measured in bytes written. Minimum value: 536870912 (512 MB). |
How It Works
The volume guard checks free space at two points during an upload:
- Upload start — if the volume is already below the threshold, the upload is rejected immediately.
- During write — for large uploads, free space is
re-checked periodically (every
check-interval-bytesbytes written) to catch uploads that consume the remaining space.
Each check is a single OS system call (statfs on Linux,
GetDiskFreeSpaceEx on Windows) with negligible overhead.
No filesystem walks, no shared state, and no locks are involved.
filesystem-config.xml are detected automatically and take
effect within a few seconds, with no server restart required.
Disk Quotas
Disk quotas prevent any single user from consuming excessive storage.
Configure quotas in filesystem-config.xml:
<quota>
<default-limit-bytes>1073741824</default-limit-bytes>
<enforcement>hard</enforcement>
</quota>
The default quota is 1 GiB per user.
Per-User Override
Individual users can have custom quota limits stored in the
sftp_quotas database table. Use the admin CLI commands
to manage per-user quotas:
| Command | Description |
|---|---|
set-quota <username> <bytes> |
Set a custom quota limit for a user |
get-quota <username> |
Display the effective quota for a user |
clear-quota <username> |
Remove the per-user override (reverts to default) |
Enforcement Modes
| Mode | Behavior |
|---|---|
hard |
When the quota is exceeded, the write is blocked. The staging
file is discarded (not committed as a truncated file). An
UPLOAD_REJECTED audit event is recorded, and the
client receives SSH_FX_FAILURE. |
soft |
A warning is logged but the upload proceeds to completion. |
How Usage Is Calculated
Disk usage is calculated by walking all files in the user's home directory tree. Lookups are cached on a per-user basis with a baseline TTL of 300 seconds to avoid excessive filesystem scans.
Partial Upload Handling
If a quota is exceeded mid-transfer (some data was written successfully before the limit was hit), the staging file is discarded. It is never committed as a truncated file. This prevents downstream systems from processing incomplete data.
Downstream Integration
Stylus SFTP Server acts as a file-exchange hub. External systems pick up transferred files directly from user home directories on the shared filesystem.
.staging/ directory
and never appear in user home directories. Once a file appears in a
user's directory, it is fully written and safe to process.
The atomic rename guarantee means that once a file appears with its final name, it is fully written and closed. There is no window where a downstream process could read a partially written file.
Configuration Reference
All filesystem settings are in conf/filesystem-config.xml.
A complete example:
<filesystem xmlns="http://www.xmlpipelineserver.com/sftp/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.xmlpipelineserver.com/sftp/
../docs/xsd/filesystem-config.xsd">
<home-root>${STYLUS_SFTPSERVER_DATA}/homes/${username}</home-root>
<default-access>read-write</default-access>
<rename-pattern>${basename}</rename-pattern>
<on-duplicate>timestamp-suffix</on-duplicate>
<orphan-max-age>PT2H</orphan-max-age>
<quota>
<default-limit-bytes>1073741824</default-limit-bytes>
<enforcement>hard</enforcement>
</quota>
<volume-guard>
<min-free-bytes>1073741824</min-free-bytes>
<check-interval-bytes>1073741824</check-interval-bytes>
</volume-guard>
</filesystem>