complete local markdown resource lifecycle contract
This commit is contained in:
@@ -3175,6 +3175,96 @@ pub async fn open_local_file(
|
||||
Ok((StatusCode::OK, headers, bytes))
|
||||
}
|
||||
|
||||
pub async fn stat_local_file(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalFileOpenQuery>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let root_path = parse_file_root_uri(&query.root_uri)?;
|
||||
let canonical_root = root_path.canonicalize().map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_folder_unavailable",
|
||||
format!("无法访问本地文件夹: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let requested = Path::new(&query.path);
|
||||
if requested.is_absolute()
|
||||
|| requested
|
||||
.components()
|
||||
.any(|component| matches!(component, std::path::Component::ParentDir))
|
||||
{
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_file_stat_root_escape",
|
||||
"本地文件路径不能越过 root",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let target = canonical_root.join(requested);
|
||||
let metadata = match fs::symlink_metadata(&target) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
HeaderMap::new(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"result": {
|
||||
"rootUri": query.root_uri,
|
||||
"path": query.path,
|
||||
"exists": false,
|
||||
"sourceKind": "local_folder",
|
||||
}
|
||||
})),
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_file_stat_failed",
|
||||
format!("无法读取本地文件状态 {}: {error}", target.display()),
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
};
|
||||
let canonical_target = target.canonicalize().map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"local_file_stat_failed",
|
||||
format!("无法解析本地文件路径 {}: {error}", target.display()),
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
if !canonical_target.starts_with(&canonical_root) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_file_stat_root_escape",
|
||||
"本地文件路径不能越过 root",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let file_name = canonical_target
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("资源")
|
||||
.to_string();
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
HeaderMap::new(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"result": {
|
||||
"rootUri": query.root_uri,
|
||||
"path": query.path,
|
||||
"exists": true,
|
||||
"isDirectory": metadata.is_dir(),
|
||||
"fileName": file_name,
|
||||
"contentType": content_type_for_path(&canonical_target).to_str().unwrap_or("application/octet-stream"),
|
||||
"sourceKind": "local_folder",
|
||||
}
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn read_local_resource(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
|
||||
Reference in New Issue
Block a user