ref: extract serve_file_variant for static file handlers - #919
Conversation
a54d510 to
a960b9d
Compare
Greptile SummaryThis PR extracts the repeated variant-generation + disk-serving + cache-header logic from
Confidence Score: 4/5Safe to merge; the refactor is mechanically correct and the two inline suggestions are polish items rather than blocking issues. The nexus-webapi/src/routes/static/serve_dir.rs — the two inline findings live here.
|
| Filename | Overview |
|---|---|
| nexus-webapi/src/routes/static/serve_dir.rs | Core change: adds the serve_file_variant helper that consolidates variant generation, disk serving, and cache/download headers. Minor design issue: the owner_id parameter is redundant with file.owner_id, and the filename in Content-Disposition is inserted as-is without RFC 6266 encoding. |
| nexus-webapi/src/routes/static/avatar.rs | Cleanly refactored to delegate to serve_file_variant; the switch from user_id to owner_id (extracted from the file URI) for the disk path is actually more correct for cross-user avatar scenarios. |
| nexus-webapi/src/routes/static/files.rs | Simplified to delegate to serve_file_variant; variant validation and ?dl= download logic are preserved correctly. |
| nexus-webapi/benches/avatar.rs | New benchmark for the avatar handler with warm and cold (variant regeneration) scenarios. Correctly acknowledges the OnceLock constraint. |
| nexus-webapi/Cargo.toml | Adds http-body-util as a dev-dependency for BodyExt in the benchmark, and registers the new avatar bench target. |
| nexus-common/src/models/user/mod.rs | Re-exports CACHE_USER_RECOMMENDED_KEY_PARTS alongside existing stream exports; unrelated to the main refactor. |
Sequence Diagram
sequenceDiagram
participant C as Client
participant AH as user_avatar_handler
participant FH as static_files_handler
participant SFV as serve_file_variant
participant Blob as Blob get_by_id
participant PSD as PubkyServeDir
C->>AH: GET /avatar/:user_id
AH->>AH: Lookup UserDetails and FileDetails
AH->>SFV: serve_file_variant(req, file, owner_id, Small, path, false)
SFV->>Blob: ensure/create variant on disk
Blob-->>SFV: content_type
SFV->>PSD: try_call(req, disk path, content_type, path)
PSD-->>SFV: Response
SFV->>SFV: set Cache-Control header
SFV-->>AH: Response
AH-->>C: 200 + avatar bytes
C->>FH: GET /static/files/:owner/:file/:variant
FH->>FH: Lookup FileDetails and validate variant
FH->>SFV: serve_file_variant(req, file, owner_id, variant, path, dl)
SFV->>Blob: ensure/create variant on disk
Blob-->>SFV: content_type
SFV->>PSD: try_call(req, disk path, content_type, path)
PSD-->>SFV: Response
SFV->>SFV: set Cache-Control header
SFV->>SFV: optionally set Content-Disposition attachment
SFV-->>FH: Response
FH-->>C: 200 + file bytes
Reviews (1): Last reviewed commit: "fix: serve avatar files from URI owner" | Re-trigger Greptile
| pub async fn serve_file_variant( | ||
| request: Request<Body>, | ||
| file: &FileDetails, | ||
| owner_id: &str, | ||
| variant: &FileVariant, | ||
| files_path: PathBuf, | ||
| download: bool, | ||
| ) -> Result<Response<ServeFileSystemResponseBody>> { | ||
| let content_type = Blob::get_by_id(file, variant, files_path.clone()).await?; | ||
|
|
||
| let disk_path = format!("/{owner_id}/{}/{variant}", file.id); |
There was a problem hiding this comment.
The
owner_id parameter is already available as file.owner_id, so callers must keep both in sync manually. Since Blob::get_by_id looks up the file with file.owner_id, using a separately-passed owner_id to build the disk path creates a potential divergence — if they ever differ (e.g. from a stale cache entry), the variant is written under file.owner_id but served from a different path. Removing the extra parameter eliminates that class of bug entirely.
| pub async fn serve_file_variant( | |
| request: Request<Body>, | |
| file: &FileDetails, | |
| owner_id: &str, | |
| variant: &FileVariant, | |
| files_path: PathBuf, | |
| download: bool, | |
| ) -> Result<Response<ServeFileSystemResponseBody>> { | |
| let content_type = Blob::get_by_id(file, variant, files_path.clone()).await?; | |
| let disk_path = format!("/{owner_id}/{}/{variant}", file.id); | |
| pub async fn serve_file_variant( | |
| request: Request<Body>, | |
| file: &FileDetails, | |
| variant: &FileVariant, | |
| files_path: PathBuf, | |
| download: bool, | |
| ) -> Result<Response<ServeFileSystemResponseBody>> { | |
| let content_type = Blob::get_by_id(file, variant, files_path.clone()).await?; | |
| let disk_path = format!("/{}/{}/{variant}", file.owner_id, file.id); |
| let filename = &file.name; | ||
| let content_disposition_header = format!("attachment; filename=\"{filename}\"") | ||
| .parse() | ||
| .inspect_err(|_| error!("Invalid content disposition header: {filename}"))?; | ||
| response | ||
| .headers_mut() | ||
| .insert("content-disposition", content_disposition_header); |
There was a problem hiding this comment.
Unescaped filename in
Content-Disposition header
The filename value is interpolated directly into the attachment; filename="..." string without any RFC 6266 escaping. A filename containing " or \ (e.g. my"file.txt) produces a structurally malformed header that clients will interpret incorrectly — the closing quote appears early and the rest of the name leaks into the directive. HeaderValue::from_str will accept this silently because those bytes are in the valid visible-ASCII range. The safe fix is to percent-encode the filename or use the filename* (RFC 5987) form: attachment; filename*=UTF-8''<percent-encoded>.
There was a problem hiding this comment.
ok300
left a comment
There was a problem hiding this comment.
Looks good, just a few NITs that would simplify the code but keep the logic unchanged.
| ) -> Result<Response<ServeFileSystemResponseBody>> { | ||
| let content_type = Blob::get_by_id(file, variant, files_path.clone()).await?; | ||
|
|
||
| let disk_path = format!("/{}/{}/{variant}", file.owner_id, file.id); |
There was a problem hiding this comment.
This fix (user_id → file.owner_id) is a behavior change.
The tests in tests/user/avatar.rs and the new benchmark both use the same-owner case (USER_PUBKY owns FILE_ID), so the exact scenario the fix repairs is untested.
NIT: would it make sense to add a test? For example, one where a user's image points to a file owned by a different PK would lock in the fix and prevent regression.
Closes #331
Summary
Extracts shared static file serving logic into
serve_file_variantand completes the remaining polish from the avatar endpoint work in #322.Checklist status
/avatarbenchmarkChanges
serve_file_varianthelper inserve_dir.rsfor variant generation, disk serving, and cache headersavatar.rsandfiles.rsto use the shared helperfiles.rskeeps variant validation and optional?dl=download headerPre-submission Checklist
Footnotes
Sample prompt: "Can this be simplified? Can the code, comments, or logic introduced by these changes be simplfied, clarified or otherwise made more terse, concise and understandable, without affecting functionality?" ↩
cargo bench -p nexus-webapi↩