feat(server): add file_size to API ItemInfo response
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +474,12 @@ 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)
|
||||||
warn!("Item conversion failed: {e}");
|
.map(|info| info.with_file_size(&data_dir))
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
.map_err(|e| {
|
||||||
})?;
|
warn!("Item conversion failed: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Json(ApiResponse::ok(item_info)))
|
Ok(Json(ApiResponse::ok(item_info)))
|
||||||
}
|
}
|
||||||
@@ -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,10 +1142,12 @@ 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)
|
||||||
warn!("Item conversion failed: {e}");
|
.map(|info| info.with_file_size(&data_dir))
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
.map_err(|e| {
|
||||||
})?;
|
warn!("Item conversion failed: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Json(ApiResponse::ok(item_info)))
|
Ok(Json(ApiResponse::ok(item_info)))
|
||||||
}
|
}
|
||||||
@@ -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,10 +1378,12 @@ 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)
|
||||||
warn!("Item conversion failed: {e}");
|
.map(|info| info.with_file_size(&data_dir))
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
.map_err(|e| {
|
||||||
}),
|
warn!("Item conversion failed: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}),
|
||||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1392,10 +1403,12 @@ 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)
|
||||||
warn!("Item conversion failed: {e}");
|
.map(|info| info.with_file_size(&data_dir))
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
.map_err(|e| {
|
||||||
}),
|
warn!("Item conversion failed: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}),
|
||||||
Err(CoreError::ItemNotFound(_)) => Err(StatusCode::NOT_FOUND),
|
Err(CoreError::ItemNotFound(_)) => Err(StatusCode::NOT_FOUND),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to update item {item_id}: {e}");
|
warn!("Failed to update item {item_id}: {e}");
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user