Skip to content

feat: enforce server-side upload limits (5 GiB/upload, 5 GiB rolling/email)#103

Merged
rubenhensen merged 1 commit intomainfrom
feat/server-side-upload-limits
Apr 23, 2026
Merged

feat: enforce server-side upload limits (5 GiB/upload, 5 GiB rolling/email)#103
rubenhensen merged 1 commit intomainfrom
feat/server-side-upload-limits

Conversation

@dobby-coder
Copy link
Copy Markdown
Contributor

@dobby-coder dobby-coder Bot commented Apr 21, 2026

Summary

Adds server-side enforcement of upload quotas described in #100:

  1. 5 GiB per upload — any chunk that would push the current upload past 5 GiB is rejected with 413.
  2. 5 GiB rolling per sender email over 14 days — checked at finalize time, once the sender email is derived from the PostGuard attributes; the request is rejected with 413 and both the in-memory state and the on-disk file are cleaned up.
  3. GET /usage?email=... — returns used_bytes, limit_bytes, window_days, per_upload_limit_bytes, and resets_at so the frontend can warn users proactively.

Closes #100.

Why at finalize and not at init?

The FileState.sender is only populated at finalize time by reading the pbdf.sidn-pbdf.email.email attribute out of the encrypted blob. Checking earlier would require trusting a client-supplied header, which can be bypassed — the whole point of this change is that bypass is what we want to prevent. The per-upload 5 GiB check runs on every chunk, so a malicious client still can't silently burn storage; they just waste their own bandwidth up to the limit.

413 body shape

Both rejection paths return the same JSON shape:

{
  "error": "human-readable explanation",
  "limit": "per_upload" | "rolling_window",
  "used_bytes": 123,
  "limit_bytes": 5368709120
}

Usage endpoint shape

{
  "email": "alice@example.com",
  "used_bytes": 2147483648,
  "limit_bytes": 5368709120,
  "window_days": 14,
  "per_upload_limit_bytes": 5368709120,
  "resets_at": "2026-05-05T12:00:00Z"
}

resets_at is the moment the oldest recorded upload falls out of the rolling window (a partial quota reset), or null if the sender has no recorded uploads.

Units

Values are GiB (binary), i.e. 5·1024³. Documented in api-description.yaml and in the 413 response body.

Tests

Unit tests for the usage tracker in src/store.rs:

  • empty state returns zero
  • records inside the window sum correctly
  • records outside the window are pruned
  • pruning respects the exact window boundary
  • different emails are isolated

Run with cargo test. All pass.

Full end-to-end testing (init → chunked upload → finalize) requires a running PKG and mailcrab, which isn't available in this dev environment. The handler logic is exercised at compile time; edge cases covered by the store unit tests.

Known follow-ups (out of scope)

Test plan

  • cargo check clean
  • cargo test — 5/5 new tests pass
  • cargo clippy --all-targets — no new warnings (pre-existing email.rs:225 warning untouched)
  • Full upload flow against a live PKG + mailcrab stack (not available in this dev env; to be verified on staging before merge)

Refs encryption4all/postguard-website#85, encryption4all/postguard-website#87.

…/email)

Adds server-side enforcement for the upload quotas described in cryptify#100.

- Rejects any chunk that would push the current upload past 5 GiB, returning
  413 with a JSON body identifying the per_upload limit, used_bytes, and
  limit_bytes.
- At finalize time, once the sender email is known from the postguard
  attributes, checks the sender's usage in the last 14 days. If the upload
  would push the sender past 15 GiB, returns 413 with limit="rolling_window"
  and cleans up both the FileState and the on-disk file.
- Records successful finalizations against the sender email so the rolling
  window is actually tracked.
- Exposes GET /usage?email=... so the frontend can show warnings before
  hitting the limit. Response includes used_bytes, limit_bytes, window_days,
  per_upload_limit_bytes, and resets_at (RFC-3339, when the oldest recorded
  upload falls out of the window).

Also documents the new endpoint and 413 responses in api-description.yaml.

Refs #100

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rubenhensen rubenhensen changed the title feat: enforce server-side upload limits (5 GiB/upload, 15 GiB rolling/email) feat: enforce server-side upload limits (5 GiB/upload, 5 GiB rolling/email) Apr 23, 2026
@rubenhensen rubenhensen marked this pull request as ready for review April 23, 2026 12:26
@rubenhensen rubenhensen merged commit 70a085e into main Apr 23, 2026
5 checks passed
@rubenhensen rubenhensen deleted the feat/server-side-upload-limits branch April 23, 2026 12:28
@dobby-coder
Copy link
Copy Markdown
Contributor Author

dobby-coder Bot commented Apr 23, 2026

Dobby sees the approval! 🧦 Dobby is so happy rubenhensen approved the upload-limits PR! Dobby notes it is already merged — Dobby's work here is done, Dobby will go rest his ears now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce server-side upload limits: 5 GB rolling per email (14 days)

1 participant