feat(blog): 接入 posts 接口(CRUD + CORS PUT)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-27 16:48:26 +08:00
parent 9d113b72f8
commit edb975d818
2 changed files with 94 additions and 1 deletions

View File

@ -16,6 +16,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer};
use crate::folders::{FolderRegistry, WallState, QUOTA_BYTES, MAX_ITEMS};
use crate::photos::{SaveError, MAX_BYTES, MAX_PHOTOS};
use crate::posts::{Post, PostDraft, PostError, MAX_POSTS};
use crate::routes::extract::AuthUser;
use crate::{
@ -35,7 +36,7 @@ pub fn router(state: AppState) -> Router {
"http://localhost:5174".parse().unwrap(),
"http://127.0.0.1:5174".parse().unwrap(),
]))
.allow_methods([Method::GET, Method::POST, Method::DELETE])
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
// 照片接口接收 base64 上传,需放宽 body 上限到 16MB其余接口保持
@ -56,6 +57,11 @@ pub fn router(state: AppState) -> Router {
let state_routes = Router::new()
.route("/api/web/folder/{name}/state", get(get_folder_state).put(save_folder_state));
// 博客文章路由(正文是文本,默认 2MB body 上限足够)。
let post_routes = Router::new()
.route("/api/web/posts", get(list_posts).post(create_post))
.route("/api/web/posts/{id}", get(get_post).put(update_post).delete(delete_post));
Router::new()
.route("/api/web/stats", get(stats))
.route("/api/web/gate", post(gate))
@ -64,6 +70,7 @@ pub fn router(state: AppState) -> Router {
.merge(photo_routes)
.merge(folder_routes)
.merge(state_routes)
.merge(post_routes)
.layer(cors)
.with_state(state)
}
@ -465,3 +472,85 @@ async fn save_folder_state(
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
/* ════════════════════════════════════════════════════════════════
AuthUser
*/
#[derive(Serialize)]
struct PostListResponse {
items: Vec<Post>,
max: usize,
}
#[derive(Deserialize)]
struct PostInput {
title: String,
tag: String,
body: String,
}
/// PostError → HTTP 状态码(与 photos 同构)。
fn post_status(e: PostError) -> StatusCode {
match e {
PostError::Full => StatusCode::CONFLICT,
PostError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
PostError::BadInput | PostError::BadUser => StatusCode::BAD_REQUEST,
PostError::NotFound => StatusCode::NOT_FOUND,
PostError::Io => StatusCode::INTERNAL_SERVER_ERROR,
}
}
async fn list_posts(AuthUser(user): AuthUser, State(state): State<AppState>) -> Json<PostListResponse> {
Json(PostListResponse {
items: state.posts.list(&user),
max: MAX_POSTS,
})
}
async fn create_post(
AuthUser(user): AuthUser,
State(state): State<AppState>,
Json(req): Json<PostInput>,
) -> Result<(StatusCode, Json<Post>), StatusCode> {
let draft = PostDraft { title: req.title, tag: req.tag, body: req.body };
state
.posts
.create(&user, draft)
.map(|p| (StatusCode::CREATED, Json(p)))
.map_err(post_status)
}
async fn get_post(
AuthUser(user): AuthUser,
State(state): State<AppState>,
AxPath(id): AxPath<String>,
) -> Result<Json<Post>, StatusCode> {
state.posts.read(&user, &id).map(Json).ok_or(StatusCode::NOT_FOUND)
}
async fn update_post(
AuthUser(user): AuthUser,
State(state): State<AppState>,
AxPath(id): AxPath<String>,
Json(req): Json<PostInput>,
) -> Result<StatusCode, StatusCode> {
let draft = PostDraft { title: req.title, tag: req.tag, body: req.body };
state
.posts
.update(&user, &id, draft)
.map(|_| StatusCode::NO_CONTENT)
.map_err(post_status)
}
async fn delete_post(
AuthUser(user): AuthUser,
State(state): State<AppState>,
AxPath(id): AxPath<String>,
) -> StatusCode {
if state.posts.delete(&user, &id) {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}

View File

@ -11,6 +11,7 @@ use crate::auth::{self, DevGate, UserStore, WallUserStore};
use crate::folders::{FolderRegistry, FolderStore};
use crate::monitor::Stats;
use crate::photos::PhotoStore;
use crate::posts::PostStore;
use crate::weather::WeatherService;
#[derive(Clone)]
@ -31,6 +32,8 @@ pub struct AppState {
pub folders: FolderRegistry,
/// 特殊文件夹图片仓库(按文件夹隔离的磁盘目录)。
pub folder_photos: FolderStore,
/// 个人博客文章仓库(按用户隔离的磁盘目录)。
pub posts: PostStore,
}
impl AppState {
@ -47,6 +50,7 @@ impl AppState {
photos: PhotoStore::from_env(),
folders: FolderRegistry::from_env(),
folder_photos: FolderStore::from_env(),
posts: PostStore::from_env(),
}
}
}