Skip to main content

yggdrasil/api/settings/
trash.rs

1// 与 posts 模块一致:Leptos `#[server]` 宏触发 deprecated/unit/too_many_arguments
2// 提示,按项目惯例放行(限流/运行器等配置项天然参数多)。
3#![allow(clippy::unused_unit, deprecated, clippy::too_many_arguments)]
4
5use crate::api::error::ApiError;
6use leptos::prelude::*;
7
8#[cfg(feature = "server")]
9use crate::api::auth::get_current_admin_user;
10#[cfg(feature = "server")]
11use crate::api::error::AppError;
12#[cfg(feature = "server")]
13use crate::db::pool::get_conn;
14use crate::models::settings::TrashSettings;
15
16/// Web 与 MCP 共用的回收站配置读取;缺失或非法值回退默认值。
17#[cfg(feature = "server")]
18pub(crate) async fn load_trash_settings(
19    client: &tokio_postgres::Client,
20) -> Result<TrashSettings, AppError> {
21    let enabled: bool = client
22        .query_opt(
23            "SELECT value FROM settings WHERE key = 'trash_auto_purge_enabled'",
24            &[],
25        )
26        .await
27        .map_err(AppError::query)?
28        .and_then(|r| r.get::<_, String>("value").parse().ok())
29        .unwrap_or(crate::models::settings::DEFAULT_AUTO_PURGE_ENABLED);
30
31    let days: i32 = client
32        .query_opt(
33            "SELECT value FROM settings WHERE key = 'trash_retention_days'",
34            &[],
35        )
36        .await
37        .map_err(AppError::query)?
38        .and_then(|r| r.get::<_, String>("value").parse().ok())
39        .unwrap_or(crate::models::settings::DEFAULT_RETENTION_DAYS);
40
41    Ok(TrashSettings {
42        auto_purge_enabled: enabled,
43        retention_days: TrashSettings::clamp_retention(days),
44    })
45}
46
47/// Web 与 MCP 共用的配置写入;保留天数统一钳制后写入并返回。
48#[cfg(feature = "server")]
49pub(crate) async fn save_trash_settings(
50    client: &tokio_postgres::Client,
51    auto_purge_enabled: bool,
52    retention_days: i32,
53) -> Result<TrashSettings, AppError> {
54    let retention_days = TrashSettings::clamp_retention(retention_days);
55
56    super::upsert_settings(
57        client,
58        &[
59            ("trash_auto_purge_enabled", auto_purge_enabled.to_string()),
60            ("trash_retention_days", retention_days.to_string()),
61        ],
62    )
63    .await?;
64
65    Ok(TrashSettings {
66        auto_purge_enabled,
67        retention_days,
68    })
69}
70
71/// 读取回收站配置。
72///
73/// settings 表缺失键时回退到默认值,保证向后兼容。
74#[server(GetTrashSettings, endpoint = "get_trash_settings", input = leptos::server_fn::codec::Json)]
75pub async fn get_trash_settings() -> Result<TrashSettings, ApiError> {
76    let _user = get_current_admin_user().await?;
77
78    #[cfg(feature = "server")]
79    {
80        let client = get_conn().await.map_err(AppError::db_conn)?;
81        Ok(load_trash_settings(&client).await?)
82    }
83
84    #[cfg(not(feature = "server"))]
85    {
86        Ok(TrashSettings::default())
87    }
88}
89
90/// 更新回收站配置。
91///
92/// retention_days 会被 clamp 到合法范围后写入。
93#[server(UpdateTrashSettings, endpoint = "update_trash_settings", input = leptos::server_fn::codec::Json)]
94pub async fn update_trash_settings(
95    auto_purge_enabled: bool,
96    retention_days: i32,
97) -> Result<TrashSettings, ApiError> {
98    let _user = get_current_admin_user().await?;
99
100    #[cfg(feature = "server")]
101    {
102        let client = get_conn().await.map_err(AppError::db_conn)?;
103        let settings = save_trash_settings(&client, auto_purge_enabled, retention_days).await?;
104
105        tracing::info!(
106            "Trash settings updated: auto_purge={}, retention_days={}",
107            settings.auto_purge_enabled,
108            settings.retention_days
109        );
110
111        Ok(settings)
112    }
113
114    #[cfg(not(feature = "server"))]
115    {
116        Ok(TrashSettings {
117            auto_purge_enabled,
118            retention_days,
119        })
120    }
121}
122
123#[cfg(all(test, feature = "server"))]
124mod tests {
125    use super::*;
126
127    #[tokio::test]
128    #[ignore = "requires DATABASE_URL; uses only a connection-local temporary table"]
129    async fn trash_settings_database_roundtrip() {
130        let url =
131            std::env::var("DATABASE_URL").expect("set DATABASE_URL to run this database test");
132        let (client, connection) = tokio_postgres::connect(&url, tokio_postgres::NoTls)
133            .await
134            .unwrap();
135        let connection = tokio::spawn(connection);
136        client
137            .batch_execute(
138                "CREATE TEMP TABLE settings (
139                    key TEXT PRIMARY KEY,
140                    value TEXT NOT NULL,
141                    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
142                )",
143            )
144            .await
145            .unwrap();
146
147        assert_eq!(
148            load_trash_settings(&client).await.unwrap(),
149            TrashSettings::default()
150        );
151        client
152            .batch_execute(
153                "INSERT INTO settings (key, value) VALUES
154                    ('trash_auto_purge_enabled', 'invalid'),
155                    ('trash_retention_days', 'invalid')",
156            )
157            .await
158            .unwrap();
159        assert_eq!(
160            load_trash_settings(&client).await.unwrap(),
161            TrashSettings::default()
162        );
163
164        for (enabled, input_days, expected_days) in [(true, -5, 1), (false, 366, 365), (true, 7, 7)]
165        {
166            let expected = TrashSettings {
167                auto_purge_enabled: enabled,
168                retention_days: expected_days,
169            };
170            assert_eq!(
171                save_trash_settings(&client, enabled, input_days)
172                    .await
173                    .unwrap(),
174                expected
175            );
176            assert_eq!(load_trash_settings(&client).await.unwrap(), expected);
177            let stored: String = client
178                .query_one(
179                    "SELECT value FROM settings WHERE key = 'trash_retention_days'",
180                    &[],
181                )
182                .await
183                .unwrap()
184                .get(0);
185            assert_eq!(stored, expected_days.to_string());
186        }
187
188        for (stored, expected_days) in [("-5", 1), ("366", 365)] {
189            client
190                .execute(
191                    "UPDATE settings SET value = $1 WHERE key = 'trash_retention_days'",
192                    &[&stored],
193                )
194                .await
195                .unwrap();
196            assert_eq!(
197                load_trash_settings(&client).await.unwrap().retention_days,
198                expected_days
199            );
200        }
201
202        client
203            .batch_execute("ALTER TABLE pg_temp.settings RENAME COLUMN value TO invalid_value")
204            .await
205            .unwrap();
206        assert!(matches!(
207            load_trash_settings(&client).await,
208            Err(AppError::Query(_))
209        ));
210        assert!(matches!(
211            save_trash_settings(&client, true, 30).await,
212            Err(AppError::Query(_))
213        ));
214        drop(client);
215        connection.await.unwrap().unwrap();
216    }
217}