1- /// Query parameter parsing and list/unlinked operations.
2- ///
3- /// Handles GET requests with a query string, which are routing differently
4- /// from plain GET (redirect) requests.
5- ///
6- /// Supported operations:
7- /// - `?list[&start=X][&limit=N]` list active keys under prefix
8- /// - `?unlinked[&start=X][&limit=N]` list soft-deleted keys
9- /// - `?list-type=2&prefix=X` S3-style listing
1+ //! Query handling for list-style operations.
2+ //!
3+ //! This module processes GET requests that include a query string.
4+ //! Plain GET requests (without a query) are handled elsewhere and
5+ //! result in object redirects.
6+ //!
7+ //! Supported query forms:
8+ //!
9+ //! - `?list[&start=X][&limit=N]`
10+ //! Lists active (non-deleted) keys under the provided prefix.
11+ //!
12+ //! - `?unlinked[&start=X][&limit=N]`
13+ //! Lists soft-deleted keys under the provided prefix.
14+ //!
15+ //! - `?list-type=2&prefix=X`
16+ //! Provides an S3-compatible XML listing.
17+ //!
18+ //! Listing is prefix-based and backed by a metadata scan (`scan_prefix`).
19+ //! Results are filtered by deletion state at decode time.
20+ //!
21+ //! `start` acts as a cursor (lexicographic lower bound).
22+ //! `limit` bounds the number of returned keys. A hard cap of
23+ //! `MAX_KEYS` prevents unbounded responses.
24+
1025use std:: sync:: Arc ;
1126
1227use axum:: http:: StatusCode ;
@@ -17,30 +32,46 @@ use tracing::debug;
1732use minikv_core:: record:: Deleted ;
1833use minikv_core:: state:: AppState ;
1934
20- /// JSON response for list operations.
35+ /// JSON response body returned by `?list` and `?unlinked`.
36+ ///
37+ /// `next` is a continuation cursor. It is empty if no further
38+ /// results are available.
39+ ///
40+ /// `keys` contains UTF-8 representations of matching object keys.
2141#[ derive( Debug , Serialize ) ]
2242pub struct ListResponse {
2343 pub next : String ,
2444 pub keys : Vec < String > ,
2545}
2646
27- /// Query parameters common to list and unlinked operations.
47+ /// Optional query parameters used by `?list` and `?unlinked`.
48+ ///
49+ /// `start` is a lexicographic cursor. Keys strictly smaller than
50+ /// this value are skipped.
51+ ///
52+ /// `limit` bounds the number of keys returned. If omitted,
53+ /// all matching keys up to `MAX_KEYS` may be returned.
2854#[ allow( unused) ]
2955#[ derive( Debug , Deserialize ) ]
3056pub struct ListParams {
3157 pub start : Option < String > ,
3258 pub limit : Option < usize > ,
3359}
3460
35- /// Maximum number of keys returned in a single list response before a
36- /// 413 (Payload Too Large) is returned. Matches Go's hard limit of 1,000,000.
61+ /// Absolute upper bound on keys returned in a single response.
62+ ///
63+ /// If this limit is exceeded during iteration, the request
64+ /// fails with `413 Payload Too Large`.
3765const MAX_KEYS : usize = 1_000_000 ;
3866
39- /// Handle a GET request that has a non-empty query string.
67+ /// Entry point for GET requests with a non-empty query string.
4068///
41- /// Dispatches to:
42- /// - S3-style listing (`?list-type=2`)
43- /// - Our own `?list` / `?unlinked` operations
69+ /// Dispatches based on query parameters:
70+ ///
71+ /// - `?list-type=2` → S3-compatible XML listing
72+ /// - `?list` / `?unlinked` → JSON listing
73+ ///
74+ /// Any other query results in `403 Forbidden`.
4475pub async fn handle_query ( state : Arc < AppState > , key_prefix : & [ u8 ] , raw_query : & str ) -> Response {
4576 // S3-style listing: ?list-type=2&prefix=...
4677 if raw_query. contains ( "list-type=2" ) {
@@ -56,7 +87,15 @@ pub async fn handle_query(state: Arc<AppState>, key_prefix: &[u8], raw_query: &s
5687 }
5788}
5889
59- /// Handle `?list` and `?unlinked` queries.
90+ /// Handles `?list` and `?unlinked` queries.
91+ ///
92+ /// Performs a prefix scan over metadata, applies cursor and limit,
93+ /// filters by deletion state, and returns a JSON response.
94+ ///
95+ /// Returns:
96+ /// - `200 OK` with JSON body on success
97+ /// - `413 Payload Too Large` if `MAX_KEYS` is exceeded
98+ /// - `500 Internal Server Error` on metadata or encoding failure
6099async fn handle_list (
61100 state : Arc < AppState > ,
62101 key_prefix : & [ u8 ] ,
@@ -131,7 +170,13 @@ async fn handle_list(
131170 . into_response ( )
132171}
133172
134- /// Handle `?list-type=2` S3-style listing.
173+ /// Handles `?list-type=2` S3-style listing.
174+ ///
175+ /// Extends the provided prefix with the S3 `prefix` parameter,
176+ /// scans metadata, filters out deleted records, and returns
177+ /// a minimal XML response compatible with S3 clients.
178+ ///
179+ /// Only active (non-deleted) keys are included.
135180async fn handle_s3_list ( state : Arc < AppState > , key_prefix : & [ u8 ] , raw_query : & str ) -> Response {
136181 // Append the S3 `prefix` parameter to our key prefix.
137182 let s3_prefix = extract_param ( raw_query, "prefix" ) . unwrap_or_default ( ) ;
@@ -170,7 +215,12 @@ async fn handle_s3_list(state: Arc<AppState>, key_prefix: &[u8], raw_query: &str
170215 . into_response ( )
171216}
172217
173- /// Extract a named query parameter from a raw query string.
218+ /// Extracts a query parameter from a raw query string.
219+ ///
220+ /// The query string is not URL-decoded. This function performs
221+ /// a simple `key=value` match split by `&`.
222+ ///
223+ /// Returns `None` if the parameter is not present.
174224fn extract_param ( raw_query : & str , name : & str ) -> Option < String > {
175225 for part in raw_query. split ( '&' ) {
176226 if let Some ( ( k, v) ) = part. split_once ( '=' )
0 commit comments