siftp

Storage

A user's virtual_root picks the backend. Every protocol works the same on both.

virtual_rootBackend
/srv/siftp/alice (or empty, or relative)Local directory
s3://bucket/prefixS3-compatible object storage

Local directories

S3

[s3]
endpoint = "https://s3.eu-central-1.amazonaws.com"
region = "eu-central-1"
access_key_id = "AKIA..."
secret_access_key = "${ENV:SIFTP_S3_SECRET}"
path_style = false            # true for MinIO, VersityGW, SeaweedFS, Ceph
spool_dir = "/var/lib/siftp/spool"

[[users]]
name = "alice"
password_hash = "$argon2id$..."
virtual_root = "s3://company-files/users/alice"
permissions = "rwdnml"

Set [server].root = "s3://bucket/base" to put every default user root in S3. Tested against AWS-style SigV4 servers VersityGW and SeaweedFS.

OperationOn S3
File /a/b.txtObject prefix/a/b.txt
DirectoryA key prefix; mkdir writes an empty dir/ marker
DownloadRanged GETs, 8 MiB at a time
UploadStaged in spool_dir, published when the transfer ends (multipart above part_size_bytes)
Failed uploadNothing is published; the previous object stays
RenameCopy, then delete (directories up to 10 000 objects)
Append, REST resumeThe existing object is downloaded into the spool first
SymlinksNot supported (invalid_command)
Times and modesServer-assigned Last-Modified; MFMT/SETSTAT times are accepted and ignored
Free spaceReported as 1 PiB; quotas still apply

The spool needs room for the largest concurrent uploads. Requests are presigned (SigV4) and sent over HTTPS with rustls.

Tiered storage

With [tier].remote set, every file under [server].root is also copied to S3, and the local disk keeps only what is in use. Linux and other Unix hosts; the filesystem needs user extended attributes and sparse files (ext4, XFS, Btrfs).

[s3]
endpoint = "https://s3.us-east-2.amazonaws.com"
region = "us-east-2"
path_style = false
credentials = "instance"          # the EC2 instance role

[tier]
remote = "s3://example-siftp-tier/ftp"
storage_class = "INTELLIGENT_TIERING"
evict_after_days = 30
high_watermark_percent = 80
low_watermark_percent = 70
EventWhat happens
A file has not changed for upload_after_secsUploaded under a random key in the chosen storage class
It changes laterUploaded again under a new key
Nobody reads or writes it for evict_after_daysThe local copy becomes a placeholder: same name, size, time, and mode, no data on disk
The disk passes high_watermark_percentLeast recently used copies become placeholders until usage is below low_watermark_percent
A client reads a placeholder (or appends, resumes, or truncates it to a new size)The file is downloaded first, checked, and put back in place
A client overwrites a placeholderThe new upload replaces it; nothing is downloaded
Rename, delete, MFMT, SETSTAT times and modesWork on placeholders without downloading

Listings, sizes, times, and quotas are the same whether a file is local or a placeholder. The first read of a placeholder waits for the whole file to download. Storage classes are limited to those that serve reads immediately: STANDARD, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, GLACIER_IR.

Each file's state is an extended attribute (user.siftp.tier). Copy files in or out with siftp (FTP, SFTP) rather than on the host: a placeholder copied with cp is an empty sparse file. A placeholder changed on the host is quarantined (never uploaded, refused on read, counted in tier_quarantined). Setup, bucket policy, and restore: Operations.