complete local markdown resource lifecycle contract

This commit is contained in:
lix-2026
2026-05-24 03:22:03 +08:00
parent 6df74fb4e0
commit 36eac664f2
8 changed files with 269 additions and 33 deletions
@@ -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>,
+4
View File
@@ -271,6 +271,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/local-folder/files/open",
get(local_folder_source::open_local_file),
)
.route(
"/api/local-folder/files/stat",
get(local_folder_source::stat_local_file),
)
.route(
"/api/local-folder/resource/read",
get(local_folder_source::read_local_resource),