feat(server): add file_size to API ItemInfo response

This commit is contained in:
2026-03-21 14:03:58 -03:00
parent 0004324301
commit e2cb36d2a8
3 changed files with 62 additions and 17 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Database index on `items(ts)` column for faster ORDER BY sorting - 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 ### Changed

View File

@@ -178,6 +178,7 @@ pub async fn handle_list_items(
let item_infos: Vec<ItemInfo> = items_with_meta let item_infos: Vec<ItemInfo> = items_with_meta
.into_iter() .into_iter()
.filter_map(|iwm| ItemInfo::try_from(iwm).ok()) .filter_map(|iwm| ItemInfo::try_from(iwm).ok())
.map(|info| info.with_file_size(&state.data_dir))
.collect(); .collect();
ResponseBuilder::json(ApiResponse::ok(item_infos)) ResponseBuilder::json(ApiResponse::ok(item_infos))
@@ -339,6 +340,7 @@ pub async fn handle_post_item(
let db = state.db.clone(); let db = state.db.clone();
let item_service = state.item_service.clone(); let item_service = state.item_service.clone();
let settings = state.settings.clone(); let settings = state.settings.clone();
let data_dir = state.data_dir.clone();
// Parse tags from query parameter // Parse tags from query parameter
let tags: Vec<String> = params let tags: Vec<String> = params
@@ -472,7 +474,9 @@ pub async fn handle_post_item(
return Err(StatusCode::PAYLOAD_TOO_LARGE); return Err(StatusCode::PAYLOAD_TOO_LARGE);
} }
let item_info = ItemInfo::try_from(item_with_meta).map_err(|e| { 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}"); warn!("Item conversion failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
@@ -1092,6 +1096,7 @@ pub async fn handle_delete_item(
compression: deleted_item.compression, compression: deleted_item.compression,
tags: vec![], tags: vec![],
metadata: HashMap::new(), metadata: HashMap::new(),
file_size: None,
}; };
Ok(Json(ApiResponse::ok(item_info))) Ok(Json(ApiResponse::ok(item_info)))
@@ -1124,6 +1129,7 @@ pub async fn handle_get_item_info(
let db = state.db.clone(); let db = state.db.clone();
let item_service = state.item_service.clone(); let item_service = state.item_service.clone();
let data_dir = state.data_dir.clone();
let item_with_meta = task::spawn_blocking(move || { let item_with_meta = task::spawn_blocking(move || {
let conn = db.blocking_lock(); let conn = db.blocking_lock();
@@ -1136,7 +1142,9 @@ pub async fn handle_get_item_info(
})? })?
.map_err(handle_item_error)?; .map_err(handle_item_error)?;
let item_info = ItemInfo::try_from(item_with_meta).map_err(|e| { 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}"); warn!("Item conversion failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
@@ -1352,6 +1360,7 @@ pub async fn handle_update_item(
let db = state.db.clone(); let db = state.db.clone();
let item_service = state.item_service.clone(); let item_service = state.item_service.clone();
let settings = state.settings.clone(); let settings = state.settings.clone();
let data_dir = state.data_dir.clone();
let size_param = params.uncompressed_size; let size_param = params.uncompressed_size;
let item_info = task::spawn_blocking(move || { let item_info = task::spawn_blocking(move || {
@@ -1369,7 +1378,9 @@ pub async fn handle_update_item(
return Err(StatusCode::INTERNAL_SERVER_ERROR); return Err(StatusCode::INTERNAL_SERVER_ERROR);
} }
match item_service.get_item(&conn, item_id) { match item_service.get_item(&conn, item_id) {
Ok(iwm) => ItemInfo::try_from(iwm).map_err(|e| { Ok(iwm) => ItemInfo::try_from(iwm)
.map(|info| info.with_file_size(&data_dir))
.map_err(|e| {
warn!("Item conversion failed: {e}"); warn!("Item conversion failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
}), }),
@@ -1392,7 +1403,9 @@ pub async fn handle_update_item(
); );
match result { match result {
Ok(item_with_meta) => ItemInfo::try_from(item_with_meta).map_err(|e| { 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}"); warn!("Item conversion failed: {e}");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
}), }),

View File

@@ -366,10 +366,13 @@ pub struct StatusInfoResponse {
/// let item_info = ItemInfo { /// let item_info = ItemInfo {
/// id: 42, /// id: 42,
/// ts: "2023-12-01T15:30:45Z".to_string(), /// 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(), /// compression: "gzip".to_string(),
/// tags: vec!["important".to_string()], /// tags: vec!["important".to_string()],
/// metadata: HashMap::from([("mime_type".to_string(), "text/plain".to_string())]), /// metadata: HashMap::from([("mime_type".to_string(), "text/plain".to_string())]),
/// file_size: Some(512),
/// }; /// };
/// ``` /// ```
#[derive(Serialize, Deserialize, ToSchema)] #[derive(Serialize, Deserialize, ToSchema)]
@@ -413,6 +416,33 @@ pub struct ItemInfo {
/// Key-value pairs containing additional metadata about the item. /// Key-value pairs containing additional metadata about the item.
#[schema(example = json!({"mime_type": "text/plain", "mime_encoding": "utf-8", "line_count": "42"}))] #[schema(example = json!({"mime_type": "text/plain", "mime_encoding": "utf-8", "line_count": "42"}))]
pub metadata: HashMap<String, String>, pub metadata: HashMap<String, String>,
/// 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<i64>,
}
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<ItemWithMeta> for ItemInfo { impl TryFrom<ItemWithMeta> for ItemInfo {
@@ -433,6 +463,7 @@ impl TryFrom<ItemWithMeta> for ItemInfo {
compression: item_with_meta.item.compression, compression: item_with_meta.item.compression,
tags, tags,
metadata, metadata,
file_size: None,
}) })
} }
} }