From e2cb36d2a87fc44448d3bfec851b92368303fd20 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Sat, 21 Mar 2026 14:03:58 -0300 Subject: [PATCH] feat(server): add file_size to API ItemInfo response --- CHANGELOG.md | 1 + src/modes/server/api/item.rs | 45 +++++++++++++++++++++++------------- src/modes/server/common.rs | 33 +++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3250ab..3a193fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Database index on `items(ts)` column for faster ORDER BY sorting +- Server API `ItemInfo` now includes `file_size` — actual filesystem-reported size of the item data file ### Changed diff --git a/src/modes/server/api/item.rs b/src/modes/server/api/item.rs index 20794dc..7cb9607 100644 --- a/src/modes/server/api/item.rs +++ b/src/modes/server/api/item.rs @@ -178,6 +178,7 @@ pub async fn handle_list_items( let item_infos: Vec = items_with_meta .into_iter() .filter_map(|iwm| ItemInfo::try_from(iwm).ok()) + .map(|info| info.with_file_size(&state.data_dir)) .collect(); ResponseBuilder::json(ApiResponse::ok(item_infos)) @@ -339,6 +340,7 @@ pub async fn handle_post_item( let db = state.db.clone(); let item_service = state.item_service.clone(); let settings = state.settings.clone(); + let data_dir = state.data_dir.clone(); // Parse tags from query parameter let tags: Vec = params @@ -472,10 +474,12 @@ pub async fn handle_post_item( return Err(StatusCode::PAYLOAD_TOO_LARGE); } - let item_info = ItemInfo::try_from(item_with_meta).map_err(|e| { - warn!("Item conversion failed: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let item_info = ItemInfo::try_from(item_with_meta) + .map(|info| info.with_file_size(&data_dir)) + .map_err(|e| { + warn!("Item conversion failed: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; Ok(Json(ApiResponse::ok(item_info))) } @@ -1092,6 +1096,7 @@ pub async fn handle_delete_item( compression: deleted_item.compression, tags: vec![], metadata: HashMap::new(), + file_size: None, }; Ok(Json(ApiResponse::ok(item_info))) @@ -1124,6 +1129,7 @@ pub async fn handle_get_item_info( let db = state.db.clone(); let item_service = state.item_service.clone(); + let data_dir = state.data_dir.clone(); let item_with_meta = task::spawn_blocking(move || { let conn = db.blocking_lock(); @@ -1136,10 +1142,12 @@ pub async fn handle_get_item_info( })? .map_err(handle_item_error)?; - let item_info = ItemInfo::try_from(item_with_meta).map_err(|e| { - warn!("Item conversion failed: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let item_info = ItemInfo::try_from(item_with_meta) + .map(|info| info.with_file_size(&data_dir)) + .map_err(|e| { + warn!("Item conversion failed: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; Ok(Json(ApiResponse::ok(item_info))) } @@ -1352,6 +1360,7 @@ pub async fn handle_update_item( let db = state.db.clone(); let item_service = state.item_service.clone(); let settings = state.settings.clone(); + let data_dir = state.data_dir.clone(); let size_param = params.uncompressed_size; let item_info = task::spawn_blocking(move || { @@ -1369,10 +1378,12 @@ pub async fn handle_update_item( return Err(StatusCode::INTERNAL_SERVER_ERROR); } match item_service.get_item(&conn, item_id) { - Ok(iwm) => ItemInfo::try_from(iwm).map_err(|e| { - warn!("Item conversion failed: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - }), + Ok(iwm) => ItemInfo::try_from(iwm) + .map(|info| info.with_file_size(&data_dir)) + .map_err(|e| { + warn!("Item conversion failed: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + }), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } @@ -1392,10 +1403,12 @@ pub async fn handle_update_item( ); match result { - Ok(item_with_meta) => ItemInfo::try_from(item_with_meta).map_err(|e| { - warn!("Item conversion failed: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - }), + Ok(item_with_meta) => ItemInfo::try_from(item_with_meta) + .map(|info| info.with_file_size(&data_dir)) + .map_err(|e| { + warn!("Item conversion failed: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + }), Err(CoreError::ItemNotFound(_)) => Err(StatusCode::NOT_FOUND), Err(e) => { warn!("Failed to update item {item_id}: {e}"); diff --git a/src/modes/server/common.rs b/src/modes/server/common.rs index b45c3dc..a072624 100644 --- a/src/modes/server/common.rs +++ b/src/modes/server/common.rs @@ -366,10 +366,13 @@ pub struct StatusInfoResponse { /// let item_info = ItemInfo { /// id: 42, /// ts: "2023-12-01T15:30:45Z".to_string(), -/// size: Some(1024), +/// uncompressed_size: Some(1024), +/// compressed_size: Some(512), +/// closed: true, /// compression: "gzip".to_string(), /// tags: vec!["important".to_string()], /// metadata: HashMap::from([("mime_type".to_string(), "text/plain".to_string())]), +/// file_size: Some(512), /// }; /// ``` #[derive(Serialize, Deserialize, ToSchema)] @@ -413,6 +416,33 @@ pub struct ItemInfo { /// Key-value pairs containing additional metadata about the item. #[schema(example = json!({"mime_type": "text/plain", "mime_encoding": "utf-8", "line_count": "42"}))] pub metadata: HashMap, + /// Actual file size in bytes. + /// + /// The filesystem-reported size of the item's data file. This may differ from + /// `compressed_size` if the file was written and the database hasn't been updated. + /// None if the file cannot be read (e.g., file not found, permission denied). + #[schema(example = 512)] + pub file_size: Option, +} + +impl ItemInfo { + /// Enriches this `ItemInfo` with the actual filesystem-reported size. + /// + /// Reads the size of the item's data file from disk and sets `file_size`. + /// If the file cannot be read, `file_size` is left as None. + /// + /// # Arguments + /// + /// * `data_dir` - The data directory path containing item files. + /// + /// # Returns + /// + /// A new `ItemInfo` with `file_size` populated from the filesystem. + pub fn with_file_size(mut self, data_dir: &std::path::Path) -> Self { + let item_path = data_dir.join(self.id.to_string()); + self.file_size = std::fs::metadata(&item_path).map(|m| m.len() as i64).ok(); + self + } } impl TryFrom for ItemInfo { @@ -433,6 +463,7 @@ impl TryFrom for ItemInfo { compression: item_with_meta.item.compression, tags, metadata, + file_size: None, }) } }