refactor: remove unused modules and clean up imports for improved maintainability
ZhenYi committed
Modified lib/auth/src/api/convert.rs lib/auth/src/api/convert.rs +0 −4 View lib/auth/src/api/convert.rs
@@ -34,10 +34,6 @@
3434 .map(|naive| timestamp_from_datetime(Utc.from_utc_datetime(&naive)))
3535}
3636
37pub fn naive_date(value: &Timestamp) -> Option<NaiveDate> {
38 datetime(value).map(|value| value.date_naive())
39}
40
4137pub fn datetime_opt(value: Option<DateTime<Utc>>) -> Option<Timestamp> {
4238 value.map(timestamp_from_datetime)
4339}
Modified lib/auth/src/api/error.rs lib/auth/src/api/error.rs +0 −14 View lib/auth/src/api/error.rs
@@ -6,20 +6,6 @@
66
77use crate::errors::AuthError;
88
9/// 基础设施故障:调用方必须按可重试错误处理,不能当业务结论报告。
10pub fn is_infrastructure(error: &AuthError) -> bool {
11 matches!(
12 error,
13 AuthError::Database(_)
14 | AuthError::Cache(_)
15 | AuthError::Session(_)
16 | AuthError::Email(_)
17 | AuthError::Serialization(_)
18 | AuthError::Unavailable(_)
19 | AuthError::Internal(_)
20 )
21}
22
239pub fn code_of(error: &AuthError) -> Code {
2410 match error {
2511 AuthError::Expired => Code::ExpiredChallenge,
Modified lib/auth/src/api/store.rs lib/auth/src/api/store.rs +2 −90 View lib/auth/src/api/store.rs
@@ -1,13 +1,12 @@
11use crate::models::users::users;
22use crate::protos::auth::common::v1::{
3 AuthMethod, MfaStatus, MfaType, Principal, SshKeyAlgorithm, TokenScope, UserPublic, UserRef,
4 UserStatus, user_ref,
3 AuthMethod, MfaStatus, MfaType, Principal, SshKeyAlgorithm, UserPublic, UserRef, UserStatus,
4 user_ref,
55};
66use chrono::{DateTime, Utc};
77use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
88use uuid::Uuid;
99
10use crate::api::context::RequestMeta;
1110use crate::api::convert;
1211use crate::errors::{AuthError, AuthResult};
1312use crate::share::user as user_store;
@@ -97,72 +96,6 @@
9796 }
9897}
9998
100pub fn scope_of_name(name: &str) -> Option<TokenScope> {
101 let trimmed = name.trim();
102 ALLOWED_APP_SCOPES
103 .iter()
104 .position(|scope| *scope == trimmed)
105 .and_then(scope_at)
106}
107
108fn scope_at(index: usize) -> Option<TokenScope> {
109 Some(match index {
110 0 => TokenScope::UserName,
111 1 => TokenScope::SshKey,
112 2 => TokenScope::AccessKey,
113 3 => TokenScope::Assets,
114 4 => TokenScope::Email,
115 5 => TokenScope::Profile,
116 6 => TokenScope::Theme,
117 7 => TokenScope::Guild,
118 8 => TokenScope::Event,
119 9 => TokenScope::Avatar,
120 10 => TokenScope::RepoRead,
121 11 => TokenScope::RepoWrite,
122 _ => return None,
123 })
124}
125
126pub fn scope_name(scope: TokenScope) -> Option<&'static str> {
127 let index = (scope as usize).checked_sub(1)?;
128 ALLOWED_APP_SCOPES.get(index).copied()
129}
130
131pub fn scopes_from_names(names: &[String]) -> Vec<TokenScope> {
132 names
133 .iter()
134 .filter_map(|name| scope_of_name(name))
135 .collect()
136}
137
138pub fn scope_codes(scopes: &[TokenScope]) -> Vec<i32> {
139 scopes.iter().map(|scope| *scope as i32).collect()
140}
141
142/// 未知 scope 让要求的权限集合变大:丢弃未知项会把 `required` 收缩成更松的集合,
143/// 空集合更会让 [`scopes_cover`] 恒真,所以保留原值(零值)并让覆盖判定失败。
144pub fn required_scope_codes(names: &[String]) -> Vec<i32> {
145 // 只保留真正的令牌 scope:`openid` 这类 OIDC 标准字段在 auth service 里没有对应的
146 // TokenScope,若映射成 0 会让 `scope::valid` 判定整组失效(0 非法),
147 // 结果是持有 "openid profile email event" 的令牌反而被 SCOPE_INSUFFICIENT 拒掉。
148 names
149 .iter()
150 .filter_map(|name| scope_of_name(name).map(|scope| scope as i32))
151 .collect()
152}
153
154/// 每个要求项都必须被授予,且任何一项为零值(未指定/未知)都视为不满足。
155pub fn scopes_cover(granted: &[i32], required: &[i32]) -> bool {
156 scope_valid(granted)
157 && required.iter().all(|required| {
158 *required > 0 && scope_valid(&[*required]) && granted.contains(required)
159 })
160}
161
162fn scope_valid(scopes: &[i32]) -> bool {
163 scopes.iter().all(|scope| *scope > 0)
164}
165
16699pub fn user_public(user: &users::Model, email: String, avatar_url: Option<String>) -> UserPublic {
167100 UserPublic {
168101 user_id: user.id,
@@ -180,11 +113,6 @@
180113 }
181114}
182115
183pub fn email_visible(_meta: &RequestMeta, _user_id: i64) -> bool {
184 // RPC 面由共享密钥信任,邮箱由调用方自行脱敏。
185 true
186}
187
188116pub fn user_status_of(user: &users::Model, active_mfa_factors: u32) -> UserStatus {
189117 UserStatus {
190118 exists: true,
@@ -257,10 +185,6 @@
257185 algorithm as i32
258186}
259187
260pub fn sha256_digest(value: &str) -> String {
261 crate::share::crypto::sha256_hex(value.as_bytes())
262}
263
264188pub fn new_id() -> Uuid {
265189 Uuid::now_v7()
266190}
@@ -283,18 +207,6 @@
283207
284208pub fn session_index_key(cache: &crate::cache::Cache, user_id: i64) -> String {
285209 cache.key(&["user:session", &user_id.to_string()])
286}
287
288pub async fn index_session_key(
289 cache: &crate::cache::Cache,
290 user_id: i64,
291 session_key: &str,
292 ttl: std::time::Duration,
293) -> AuthResult<()> {
294 let index_key = session_index_key(cache, user_id);
295 cache.sadd(&index_key, session_key).await?;
296 cache.expire(&index_key, ttl).await?;
297 Ok(())
298210}
299211
300212pub async fn session_index(cache: &crate::cache::Cache, user_id: i64) -> AuthResult<Vec<String>> {
Modified lib/auth/src/services/authority.rs lib/auth/src/services/authority.rs +1 −20 View lib/auth/src/services/authority.rs
@@ -1,6 +1,6 @@
11use crate::models::users::{user_sessions, users};
22use crate::session::Session;
3use chrono::{DateTime, Duration, Utc};
3use chrono::{Duration, Utc};
44use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set};
55use uuid::Uuid;
66
@@ -120,25 +120,6 @@
120120 .check_session(user_id, version, &id, request_id)
121121 .await?;
122122 Ok(user_id)
123 }
124
125 /// 会话建立时间(`user_sessions.created_at`),供 `max_age` 与 id_token 的 `auth_time` 使用。
126 #[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
127 pub async fn session_created_at(
128 &self,
129 session: &Session,
130 request_id: Option<&str>,
131 ) -> AuthResult<Option<DateTime<Utc>>> {
132 let Some(id) = session.get::<String>("auth_session_id")? else {
133 return Ok(None);
134 };
135 let Ok(id) = Uuid::parse_str(&id) else {
136 return Ok(None);
137 };
138 let row = user_sessions::Entity::find_by_id(id)
139 .one(self.db().read())
140 .await?;
141 Ok(row.map(|row| row.created_at))
142123 }
143124
144125 pub async fn user(&self, user_id: i64) -> AuthResult<users::Model> {
Modified lib/auth/src/services/notifications.rs lib/auth/src/services/notifications.rs +1 −24 View lib/auth/src/services/notifications.rs
@@ -1,9 +1,6 @@
11use models::users::user_notifications;
22use models::users::user_notifications::NotificationType;
3use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter,
5 QueryOrder, QuerySelect, Set,
6};
3use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
74use serde::{Deserialize, Serialize};
85use serde_json::Value as JsonValue;
96use time::OffsetDateTime;
@@ -71,26 +68,6 @@
7168 .count(self.db().read())
7269 .await?;
7370 Ok(count)
74 }
75
76 pub async fn mark_notification_read(
77 &self,
78 uid: i64,
79 id: Uuid,
80 ) -> Result<(), AuthBusinessError> {
81 let db = self.db();
82 let Some(row) = user_notifications::Entity::find()
83 .filter(user_notifications::Column::Id.eq(id))
84 .filter(user_notifications::Column::UserId.eq(uid))
85 .one(db.write())
86 .await?
87 else {
88 return Err(AuthBusinessError::NotFound);
89 };
90 let mut active = row.into_active_model();
91 active.read_at = Set(Some(OffsetDateTime::now_utc()));
92 active.update(db.write()).await?;
93 Ok(())
9471 }
9572
9673 pub async fn mark_notifications_read(
Modified lib/auth/src/services/service.rs lib/auth/src/services/service.rs +0 −22 View lib/auth/src/services/service.rs
@@ -12,8 +12,6 @@
1212use crate::errors::{AuthError, AuthResult};
1313use crate::events::{EventHandler, Labels, label};
1414
15const EVENT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(5);
16
1715async fn actor_details(
1816 conn: &impl sea_orm::ConnectionTrait,
1917 labels: &Labels,
@@ -190,26 +188,6 @@
190188 )
191189 .await?;
192190 Ok(())
193 }
194
195 pub async fn verify_event_forwarding(&self) -> bool {
196 let Some(events) = self.events() else {
197 return false;
198 };
199 match tokio::time::timeout(EVENT_PUBLISH_TIMEOUT, events.health()).await {
200 Ok(Ok(())) => true,
201 Ok(Err(error)) => {
202 tracing::error!(%error, "rocketmq event forwarding is unavailable");
203 false
204 }
205 Err(_) => {
206 tracing::error!(
207 timeout_ms = EVENT_PUBLISH_TIMEOUT.as_millis() as u64,
208 "rocketmq event forwarding timed out"
209 );
210 false
211 }
212 }
213191 }
214192
215193 pub async fn publish_mail(
Modified lib/auth/src/services/suspensions.rs lib/auth/src/services/suspensions.rs +1 −44 View lib/auth/src/services/suspensions.rs
@@ -1,11 +1,8 @@
1use models::users::user_suspensions;
2use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter, QueryOrder};
31use serde::{Deserialize, Serialize};
42use time::OffsetDateTime;
53use uuid::Uuid;
64
75use crate::AuthService;
8use crate::errors::AuthBusinessError;
96
107#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
118pub struct SuspensionDto {
@@ -16,44 +13,4 @@
1613 pub ends_at: Option<OffsetDateTime>,
1714}
1815
19impl AuthService {
20 pub async fn active_suspension(
21 &self,
22 uid: i64,
23 ) -> Result<Option<SuspensionDto>, AuthBusinessError> {
24 let now = OffsetDateTime::now_utc();
25 let Some(row) = user_suspensions::Entity::find()
26 .filter(user_suspensions::Column::UserId.eq(uid))
27 .filter(user_suspensions::Column::RevokedAt.is_null())
28 .filter(user_suspensions::Column::StartsAt.lte(now))
29 .filter(
30 Condition::any()
31 .add(user_suspensions::Column::EndsAt.is_null())
32 .add(user_suspensions::Column::EndsAt.gt(now)),
33 )
34 .order_by_desc(user_suspensions::Column::StartsAt)
35 .one(self.db().read())
36 .await?
37 else {
38 return Ok(None);
39 };
40 Ok(Some(SuspensionDto {
41 id: row.id,
42 suspension_type: match row.suspension_type {
43 user_suspensions::SuspensionType::Temporary => "temporary",
44 user_suspensions::SuspensionType::Permanent => "permanent",
45 }
46 .to_string(),
47 reason: row.reason,
48 starts_at: row.starts_at,
49 ends_at: row.ends_at,
50 }))
51 }
52
53 pub async fn assert_not_suspended(&self, uid: i64) -> Result<(), AuthBusinessError> {
54 if self.active_suspension(uid).await?.is_some() {
55 return Err(AuthBusinessError::Forbidden);
56 }
57 Ok(())
58 }
59}
16impl AuthService {}
Modified lib/auth/src/share/account/validate.rs lib/auth/src/share/account/validate.rs +0 −11 View lib/auth/src/share/account/validate.rs
@@ -44,17 +44,6 @@
4444 Ok(name.to_string())
4545}
4646
47pub fn derive_username(email: &str, explicit: Option<&str>) -> Result<String, AuthBusinessError> {
48 match explicit {
49 Some(name) => validate_username(name),
50 None => {
51 let local_part = email.split('@').next().unwrap_or_default();
52 let base = local_part.split('+').next().unwrap_or_default();
53 validate_username(base)
54 }
55 }
56}
57
5847pub fn validate_age(date_of_birth: Option<Date>, min_age: u8) -> Result<(), AuthBusinessError> {
5948 let Some(dob) = date_of_birth else {
6049 return Err(AuthBusinessError::BadRequest(
Modified lib/auth/src/share/session/cookie.rs lib/auth/src/share/session/cookie.rs +2 −18 View lib/auth/src/share/session/cookie.rs
@@ -1,24 +1,8 @@
1use actix_web::http::header::{HeaderMap, HeaderValue, SET_COOKIE};
2use export::http::{Redirect, Response};
3
4pub fn read_cookie_header<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
5 let value = headers
6 .get(actix_web::http::header::COOKIE)?
7 .to_str()
8 .ok()?;
9 session::read_cookie(value, name)
10}
1use actix_web::http::header::{HeaderValue, SET_COOKIE};
2use export::http::Response;
113
124pub fn append_cookie(response: &mut Response, cookie: &str) {
135 if let Ok(value) = HeaderValue::from_str(cookie) {
146 response.headers_mut().append(SET_COOKIE, value);
157 }
16}
17
18pub fn redirect_with_cookies(location: &str, cookies: &[String]) -> Response {
19 let mut response = Redirect::to(location).into_response();
20 for cookie in cookies {
21 append_cookie(&mut response, cookie);
22 }
23 response
248}
Modified lib/auth/src/share/crypto.rs lib/auth/src/share/crypto.rs +0 −8 View lib/auth/src/share/crypto.rs
@@ -31,10 +31,6 @@
3131 Alphanumeric.sample_string(&mut rand::rng(), len)
3232}
3333
34pub fn random_hex(len: usize) -> String {
35 to_hex(&random_bytes(len))
36}
37
3834pub fn random_b64url(len: usize) -> String {
3935 URL_SAFE_NO_PAD.encode(random_bytes(len))
4036}
@@ -61,10 +57,6 @@
6157
6258pub fn sha256_hex(bytes: &[u8]) -> String {
6359 to_hex(&sha256(bytes))
64}
65
66pub fn sha256_b64url(bytes: &[u8]) -> String {
67 URL_SAFE_NO_PAD.encode(sha256(bytes))
6860}
6961
7062/// OpenSSH fingerprints use standard Base64 without padding.
Modified lib/auth/src/share/validate.rs lib/auth/src/share/validate.rs +0 −10 View lib/auth/src/share/validate.rs
@@ -99,16 +99,6 @@
9999 Ok(name.to_string())
100100}
101101
102pub fn derive_username(email: &str, explicit: Option<&str>) -> AuthResult<String> {
103 match explicit {
104 Some(name) => validate_username(name),
105 None => {
106 let local = email.split('@').next().unwrap_or_default();
107 validate_username(local.split('+').next().unwrap_or_default())
108 }
109 }
110}
111
112102pub fn validate_display_name(name: &str) -> AuthResult<String> {
113103 let name = name.trim();
114104 if name.is_empty() || name.chars().count() > MAX_DISPLAY_NAME_LEN {
Modified lib/cache/src/error.rs lib/cache/src/error.rs +0 −8 View lib/cache/src/error.rs
@@ -67,14 +67,6 @@
6767 matches!(self, Self::Config(_))
6868 }
6969
70 pub fn is_backend(&self) -> bool {
71 matches!(self, Self::Backend(_))
72 }
73
74 pub fn is_unsupported(&self) -> bool {
75 matches!(self, Self::Unsupported { .. })
76 }
77
7870 pub fn is_closed(&self) -> bool {
7971 match self {
8072 Self::Backend(error) => error.is_connection_dropped(),
Modified lib/cache/src/hash.rs lib/cache/src/hash.rs +1 −71 View lib/cache/src/hash.rs
@@ -1,11 +1,8 @@
11use std::collections::HashMap;
22
3use redis::{AsyncCommands, Cmd, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
3use redis::{AsyncCommands, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
64
75use crate::error::{CacheError, CacheResult};
8use crate::json::{decode, encode};
96use crate::kv::Cache;
107
118impl Cache {
@@ -18,21 +15,6 @@
1815 conn.hget(key, field).await.map_err(CacheError::from)
1916 }
2017
21 pub async fn hget_bytes(&self, key: &str, field: &str) -> CacheResult<Option<Vec<u8>>> {
22 self.hget::<Vec<u8>>(key, field).await
23 }
24
25 pub async fn hget_json<T: DeserializeOwned>(
26 &self,
27 key: &str,
28 field: &str,
29 ) -> CacheResult<Option<T>> {
30 match self.hget_bytes(key, field).await? {
31 Some(bytes) => Ok(Some(decode(key, &bytes)?)),
32 None => Ok(None),
33 }
34 }
35
3618 pub async fn hmget<T: FromRedisValue + Send, F: ToRedisArgs + Send + Sync>(
3719 &self,
3820 key: &str,
@@ -52,26 +34,6 @@
5234 conn.hset(key, field, value).await.map_err(CacheError::from)
5335 }
5436
55 pub async fn hset_json<F: ToSingleRedisArg + Send + Sync, V: Serialize>(
56 &self,
57 key: &str,
58 field: F,
59 value: &V,
60 ) -> CacheResult<usize> {
61 self.hset(key, field, encode(key, value)?).await
62 }
63
64 pub async fn hset_nx<V: ToRedisArgs + Send + Sync>(
65 &self,
66 key: &str,
67 field: &str,
68 value: V,
69 ) -> CacheResult<bool> {
70 let mut cmd = Cmd::new();
71 cmd.arg("HSETNX").arg(key).arg(field).arg(value);
72 self.exec(&cmd).await
73 }
74
7537 pub async fn hset_multiple<
7638 F: ToSingleRedisArg + Send + Sync,
7739 V: ToSingleRedisArg + Send + Sync,
@@ -86,44 +48,12 @@
8648 .map_err(CacheError::from)
8749 }
8850
89 pub async fn hset_multiple_json<F: ToRedisArgs + Send + Sync, V: Serialize>(
90 &self,
91 key: &str,
92 items: &[(F, V)],
93 ) -> CacheResult<()> {
94 let mut encoded = Vec::with_capacity(items.len());
95 for (field, value) in items {
96 encoded.push((field, encode(key, value)?));
97 }
98 let mut conn = self.conn().clone();
99 conn.hset_multiple(key, &encoded)
100 .await
101 .map_err(CacheError::from)
102 }
103
10451 pub async fn hgetall<T: FromRedisValue + Send>(
10552 &self,
10653 key: &str,
10754 ) -> CacheResult<HashMap<String, T>> {
10855 let mut conn = self.conn().clone();
10956 conn.hgetall(key).await.map_err(CacheError::from)
110 }
111
112 pub async fn hgetall_bytes(&self, key: &str) -> CacheResult<HashMap<String, Vec<u8>>> {
113 self.hgetall::<Vec<u8>>(key).await
114 }
115
116 pub async fn hgetall_json<T: DeserializeOwned>(
117 &self,
118 key: &str,
119 ) -> CacheResult<HashMap<String, T>> {
120 let raw = self.hgetall_bytes(key).await?;
121 let mut decoded = HashMap::with_capacity(raw.len());
122 for (field, bytes) in raw {
123 let value = decode(&field, &bytes)?;
124 decoded.insert(field, value);
125 }
126 Ok(decoded)
12757 }
12858
12959 pub async fn hdel<F: ToRedisArgs + Send + Sync>(
Modified lib/cache/src/json.rs lib/cache/src/json.rs +0 −68 View lib/cache/src/json.rs
@@ -1,4 +1,3 @@
1use std::future::Future;
21use std::time::Duration;
32
43use serde::Serialize;
@@ -15,17 +14,6 @@
1514 }
1615 }
1716
18 pub async fn get_del_json<T: DeserializeOwned>(&self, key: &str) -> CacheResult<Option<T>> {
19 match self.get_del::<Vec<u8>>(key).await? {
20 Some(bytes) => Ok(Some(decode(key, &bytes)?)),
21 None => Ok(None),
22 }
23 }
24
25 pub async fn set_json<T: Serialize>(&self, key: &str, value: &T) -> CacheResult<()> {
26 self.set(key, encode(key, value)?).await
27 }
28
2917 pub async fn set_json_ex<T: Serialize>(
3018 &self,
3119 key: &str,
@@ -33,62 +21,6 @@
3321 ttl: Duration,
3422 ) -> CacheResult<()> {
3523 self.set_ex(key, encode(key, value)?, ttl).await
36 }
37
38 pub async fn set_json_default<T: Serialize>(&self, key: &str, value: &T) -> CacheResult<()> {
39 self.set_default(key, encode(key, value)?).await
40 }
41
42 pub async fn set_json_nx<T: Serialize>(&self, key: &str, value: &T) -> CacheResult<bool> {
43 self.set_nx(key, encode(key, value)?).await
44 }
45
46 pub async fn set_json_nx_ex<T: Serialize>(
47 &self,
48 key: &str,
49 value: &T,
50 ttl: Duration,
51 ) -> CacheResult<bool> {
52 self.set_nx_ex(key, encode(key, value)?, ttl).await
53 }
54
55 pub async fn get_or_set_json<T, F, Fut>(
56 &self,
57 key: &str,
58 ttl: Option<Duration>,
59 fill: F,
60 ) -> CacheResult<T>
61 where
62 T: Serialize + DeserializeOwned,
63 F: FnOnce() -> Fut,
64 Fut: Future<Output = CacheResult<T>>,
65 {
66 if let Some(value) = self.get_json::<T>(key).await? {
67 return Ok(value);
68 }
69 let value = fill().await?;
70 match ttl {
71 Some(ttl) => self.set_json_ex(key, &value, ttl).await?,
72 None => self.set_json(key, &value).await?,
73 }
74 Ok(value)
75 }
76
77 pub async fn mget_json<T: DeserializeOwned>(
78 &self,
79 keys: Vec<String>,
80 ) -> CacheResult<Vec<Option<T>>> {
81 let raw: Vec<Option<Vec<u8>>> = self.mget(keys.clone()).await?;
82 raw.iter()
83 .enumerate()
84 .map(|(index, item)| match item {
85 Some(bytes) => {
86 let key = keys.get(index).map(String::as_str).unwrap_or_default();
87 Ok(Some(decode(key, bytes)?))
88 }
89 None => Ok(None),
90 })
91 .collect()
9224 }
9325}
9426
Modified lib/cache/src/keys.rs lib/cache/src/keys.rs +0 −39 View lib/cache/src/keys.rs
@@ -96,10 +96,6 @@
9696 join(self.key_prefix(), segments)
9797 }
9898
99 pub fn key_tagged(&self, tag: &str, segments: &[&str]) -> String {
100 join_tagged(self.key_prefix(), tag, segments)
101 }
102
10399 pub fn pattern(&self, segments: &[&str]) -> String {
104100 let mut pattern = self.key(segments);
105101 pattern.push('*');
@@ -116,41 +112,6 @@
116112 keys.push(item.map_err(CacheError::from)?);
117113 }
118114 Ok(keys)
119 }
120
121 pub async fn scan_prefix(&self, prefix: &str) -> CacheResult<Vec<String>> {
122 let pattern = format!("{prefix}*");
123 self.scan(&pattern).await
124 }
125
126 pub async fn count_prefix(&self, prefix: &str) -> CacheResult<usize> {
127 Ok(self.scan_prefix(prefix).await?.len())
128 }
129
130 pub async fn exists_prefix(&self, prefix: &str) -> CacheResult<bool> {
131 self.ensure_scan_supported()?;
132 let mut conn = self.acquire().await?;
133 let pattern = format!("{prefix}*");
134 let mut iter: AsyncIter<'_, String> =
135 conn.scan_match(pattern).await.map_err(CacheError::from)?;
136 // SCAN 迭代的错误不能被当成「存在」:否则缓存故障会静默变成 true。
137 match iter.next_item().await {
138 Some(Ok(_)) => Ok(true),
139 Some(Err(error)) => Err(CacheError::from(error)),
140 None => Ok(false),
141 }
142 }
143
144 pub async fn delete_prefix(&self, prefix: &str) -> CacheResult<usize> {
145 let keys = self.scan_prefix(prefix).await?;
146 if keys.is_empty() {
147 return Ok(0);
148 }
149 let mut removed = 0usize;
150 for chunk in keys.chunks(DEFAULT_BATCH) {
151 removed += self.delete_many(chunk.to_vec()).await?;
152 }
153 Ok(removed)
154115 }
155116
156117 fn ensure_scan_supported(&self) -> CacheResult<()> {
Modified lib/cache/src/kv.rs lib/cache/src/kv.rs +0 −103 View lib/cache/src/kv.rs
@@ -23,24 +23,6 @@
2323 })
2424 }
2525
26 pub fn from_conn(conn: CacheConn) -> Self {
27 Self {
28 conn,
29 prefix: String::new(),
30 default_ttl: None,
31 }
32 }
33
34 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
35 self.prefix = prefix.into();
36 self
37 }
38
39 pub fn with_default_ttl(mut self, ttl: Duration) -> Self {
40 self.default_ttl = Some(ttl);
41 self
42 }
43
4426 pub fn conn(&self) -> &CacheConn {
4527 &self.conn
4628 }
@@ -99,12 +81,6 @@
9981 self.get::<Vec<u8>>(key).await
10082 }
10183
102 pub async fn get_del<T: FromRedisValue + Send>(&self, key: &str) -> CacheResult<Option<T>> {
103 let mut cmd = Cmd::new();
104 cmd.arg("GETDEL").arg(key);
105 self.exec(&cmd).await
106 }
107
10884 pub async fn set<V: ToRedisArgs + Send + Sync>(&self, key: &str, value: V) -> CacheResult<()> {
10985 self.set_with(key, value, None, false, false).await
11086 }
@@ -127,14 +103,6 @@
127103 .await
128104 }
129105
130 pub async fn set_nx<V: ToRedisArgs + Send + Sync>(
131 &self,
132 key: &str,
133 value: V,
134 ) -> CacheResult<bool> {
135 self.set_flag(key, value, None, true, false).await
136 }
137
138106 pub async fn set_nx_ex<V: ToRedisArgs + Send + Sync>(
139107 &self,
140108 key: &str,
@@ -144,23 +112,6 @@
144112 self.set_flag(key, value, Some(ttl), true, false).await
145113 }
146114
147 pub async fn set_xx<V: ToRedisArgs + Send + Sync>(
148 &self,
149 key: &str,
150 value: V,
151 ) -> CacheResult<bool> {
152 self.set_flag(key, value, None, false, true).await
153 }
154
155 pub async fn set_xx_ex<V: ToRedisArgs + Send + Sync>(
156 &self,
157 key: &str,
158 value: V,
159 ttl: Duration,
160 ) -> CacheResult<bool> {
161 self.set_flag(key, value, Some(ttl), false, true).await
162 }
163
164115 async fn set_with<V: ToRedisArgs + Send + Sync>(
165116 &self,
166117 key: &str,
@@ -189,28 +140,6 @@
189140 self.exec(&cmd).await
190141 }
191142
192 pub async fn get_or_set<V, F, Fut>(
193 &self,
194 key: &str,
195 ttl: Option<Duration>,
196 fill: F,
197 ) -> CacheResult<Option<V>>
198 where
199 V: ToRedisArgs + FromRedisValue + Send + Sync,
200 F: FnOnce() -> Fut,
201 Fut: std::future::Future<Output = CacheResult<V>>,
202 {
203 if let Some(value) = self.get::<V>(key).await? {
204 return Ok(Some(value));
205 }
206 let value = fill().await?;
207 match ttl {
208 Some(ttl) => self.set_ex(key, &value, ttl).await?,
209 None => self.set(key, &value).await?,
210 }
211 Ok(Some(value))
212 }
213
214143 pub async fn append<V: ToSingleRedisArg + Send + Sync>(
215144 &self,
216145 key: &str,
@@ -223,13 +152,6 @@
223152 pub async fn strlen(&self, key: &str) -> CacheResult<usize> {
224153 let mut conn = self.conn.clone();
225154 conn.strlen(key).await.map_err(CacheError::from)
226 }
227
228 pub async fn get_range(&self, key: &str, start: isize, stop: isize) -> CacheResult<Vec<u8>> {
229 let mut conn = self.conn.clone();
230 conn.getrange(key, start, stop)
231 .await
232 .map_err(CacheError::from)
233155 }
234156
235157 pub async fn incr(&self, key: &str) -> CacheResult<i64> {
@@ -255,11 +177,6 @@
255177 conn.exists(key).await.map_err(CacheError::from)
256178 }
257179
258 pub async fn exists_many<K: ToRedisArgs + Send + Sync>(&self, keys: K) -> CacheResult<i64> {
259 let mut conn = self.conn.clone();
260 conn.exists(keys).await.map_err(CacheError::from)
261 }
262
263180 pub async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<bool> {
264181 let mut conn = self.conn.clone();
265182 conn.pexpire(key, millis(ttl))
@@ -267,12 +184,6 @@
267184 .map_err(CacheError::from)
268185 }
269186
270 pub async fn expire_at(&self, key: &str, unix_millis: i64) -> CacheResult<bool> {
271 let mut cmd = Cmd::new();
272 cmd.arg("PEXPIREAT").arg(key).arg(unix_millis);
273 self.exec(&cmd).await
274 }
275
276187 pub async fn persist(&self, key: &str) -> CacheResult<bool> {
277188 let mut conn = self.conn.clone();
278189 conn.persist(key).await.map_err(CacheError::from)
@@ -284,12 +195,6 @@
284195 Ok(duration_from_seconds(seconds))
285196 }
286197
287 pub async fn p_ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
288 let mut conn = self.conn.clone();
289 let millis: i64 = conn.pttl(key).await.map_err(CacheError::from)?;
290 Ok(duration_from_millis(millis))
291 }
292
293198 pub async fn delete(&self, key: &str) -> CacheResult<bool> {
294199 let mut conn = self.conn.clone();
295200 let removed: usize = conn.del(key).await.map_err(CacheError::from)?;
@@ -365,13 +270,5 @@
365270 None
366271 } else {
367272 Some(Duration::from_secs(seconds as u64))
368 }
369}
370
371fn duration_from_millis(millis: i64) -> Option<Duration> {
372 if millis < 0 {
373 None
374 } else {
375 Some(Duration::from_millis(millis as u64))
376273 }
377274}
Modified lib/cache/src/list.rs lib/cache/src/list.rs +1 −72 View lib/cache/src/list.rs
@@ -1,6 +1,4 @@
1use std::num::NonZeroUsize;
2
3use redis::{AsyncCommands, Cmd, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
1use redis::{AsyncCommands, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
42
53use crate::error::{CacheError, CacheResult};
64use crate::kv::Cache;
@@ -15,32 +13,6 @@
1513 conn.lpush(key, value).await.map_err(CacheError::from)
1614 }
1715
18 pub async fn lpush_many<V: ToRedisArgs + Send + Sync>(
19 &self,
20 key: &str,
21 values: Vec<V>,
22 ) -> CacheResult<usize> {
23 let mut conn = self.conn().clone();
24 conn.lpush(key, values).await.map_err(CacheError::from)
25 }
26
27 pub async fn lpush_capped<V: ToRedisArgs + Send + Sync>(
28 &self,
29 key: &str,
30 value: V,
31 limit: usize,
32 ) -> CacheResult<usize> {
33 let length = self.lpush(key, value).await?;
34 if limit == 0 {
35 self.delete(key).await?;
36 return Ok(0);
37 }
38 if length > limit {
39 self.ltrim(key, 0, limit as isize - 1).await?;
40 }
41 Ok(length.min(limit))
42 }
43
4416 pub async fn rpush<V: ToRedisArgs + Send + Sync>(
4517 &self,
4618 key: &str,
@@ -50,43 +22,16 @@
5022 conn.rpush(key, value).await.map_err(CacheError::from)
5123 }
5224
53 pub async fn rpush_many<V: ToRedisArgs + Send + Sync>(
54 &self,
55 key: &str,
56 values: Vec<V>,
57 ) -> CacheResult<usize> {
58 let mut conn = self.conn().clone();
59 conn.rpush(key, values).await.map_err(CacheError::from)
60 }
61
6225 pub async fn lpop<T: FromRedisValue + Send>(&self, key: &str) -> CacheResult<Option<T>> {
6326 let mut conn = self.conn().clone();
6427 conn.lpop(key, None).await.map_err(CacheError::from)
6528 }
6629
67 pub async fn lpop_many<T: FromRedisValue + Send>(
68 &self,
69 key: &str,
70 count: NonZeroUsize,
71 ) -> CacheResult<Vec<T>> {
72 let mut conn = self.conn().clone();
73 conn.lpop(key, Some(count)).await.map_err(CacheError::from)
74 }
75
7630 pub async fn rpop<T: FromRedisValue + Send>(&self, key: &str) -> CacheResult<Option<T>> {
7731 let mut conn = self.conn().clone();
7832 conn.rpop(key, None).await.map_err(CacheError::from)
7933 }
8034
81 pub async fn rpop_many<T: FromRedisValue + Send>(
82 &self,
83 key: &str,
84 count: NonZeroUsize,
85 ) -> CacheResult<Vec<T>> {
86 let mut conn = self.conn().clone();
87 conn.rpop(key, Some(count)).await.map_err(CacheError::from)
88 }
89
9035 pub async fn llen(&self, key: &str) -> CacheResult<usize> {
9136 let mut conn = self.conn().clone();
9237 conn.llen(key).await.map_err(CacheError::from)
@@ -136,21 +81,5 @@
13681 ) -> CacheResult<usize> {
13782 let mut conn = self.conn().clone();
13883 conn.lrem(key, count, value).await.map_err(CacheError::from)
139 }
140
141 pub async fn linsert<V: ToSingleRedisArg + Send + Sync>(
142 &self,
143 key: &str,
144 before: bool,
145 pivot: V,
146 value: V,
147 ) -> CacheResult<i64> {
148 let mut cmd = Cmd::new();
149 cmd.arg("LINSERT")
150 .arg(key)
151 .arg(if before { "BEFORE" } else { "AFTER" })
152 .arg(pivot)
153 .arg(value);
154 self.exec(&cmd).await
15584 }
15685}
Modified lib/cache/src/lock.rs lib/cache/src/lock.rs +0 −58 View lib/cache/src/lock.rs
@@ -1,4 +1,3 @@
1use std::future::Future;
21use std::time::Duration;
32
43use uuid::Uuid;
@@ -64,39 +63,6 @@
6463 tokio::time::sleep((deadline - now).min(RETRY_INTERVAL)).await;
6564 }
6665 }
67
68 pub async fn with_lock<T, F, Fut>(
69 &self,
70 key: &str,
71 ttl: Duration,
72 wait: Option<Duration>,
73 operation: F,
74 ) -> CacheResult<T>
75 where
76 F: FnOnce() -> Fut,
77 Fut: Future<Output = CacheResult<T>>,
78 {
79 let mut lock = self.lock(key, ttl, wait).await?;
80 let result = operation().await;
81 let _ = lock.release().await;
82 result
83 }
84
85 pub async fn try_lock_many(
86 &self,
87 keys: &[String],
88 ttl: Duration,
89 ) -> CacheResult<Option<CacheLocks>> {
90 if keys.is_empty() {
91 return Err(CacheError::lock("", "the key list must not be empty"));
92 }
93 let token = new_token();
94 if self.scripts().lock_many(keys, &token, ttl).await? {
95 Ok(Some(CacheLocks::new(self.clone(), keys, token, ttl)))
96 } else {
97 Ok(None)
98 }
99 }
10066}
10167
10268impl CacheLock {
@@ -120,16 +86,6 @@
12086
12187 pub fn ttl(&self) -> Duration {
12288 self.ttl
123 }
124
125 pub fn is_released(&self) -> bool {
126 self.released
127 }
128
129 pub async fn is_held(&self) -> CacheResult<bool> {
130 Ok(
131 matches!(self.cache.get_bytes(&self.key).await?, Some(value) if value == self.token.as_bytes()),
132 )
13389 }
13490
13591 pub async fn refresh(&self) -> CacheResult<bool> {
@@ -160,16 +116,6 @@
160116}
161117
162118impl CacheLocks {
163 pub(crate) fn new(cache: Cache, keys: &[String], token: String, ttl: Duration) -> Self {
164 Self {
165 cache,
166 keys: keys.to_vec(),
167 token,
168 ttl,
169 released: false,
170 }
171 }
172
173119 pub fn keys(&self) -> &[String] {
174120 &self.keys
175121 }
@@ -180,10 +126,6 @@
180126
181127 pub fn ttl(&self) -> Duration {
182128 self.ttl
183 }
184
185 pub fn is_released(&self) -> bool {
186 self.released
187129 }
188130
189131 pub async fn release(&mut self) -> CacheResult<usize> {
Modified lib/cache/src/pipeline.rs lib/cache/src/pipeline.rs +1 −47 View lib/cache/src/pipeline.rs
@@ -1,6 +1,6 @@
11use std::time::Duration;
22
3use redis::{Cmd, FromRedisValue, Pipeline, ToRedisArgs, Value, cmd};
3use redis::{Cmd, Pipeline, ToRedisArgs, Value, cmd};
44use serde::Serialize;
55
66use crate::error::{CacheError, CacheResult};
@@ -72,10 +72,6 @@
7272 self.add(cmd("SET").arg(key).arg(value).arg("PX").arg(millis(ttl)))
7373 }
7474
75 pub fn set_nx(&mut self, key: &str, value: &[u8]) -> &mut Self {
76 self.add(cmd("SET").arg(key).arg(value).arg("NX"))
77 }
78
7975 pub fn set_nx_ex(&mut self, key: &str, value: &[u8], ttl: Duration) -> &mut Self {
8076 self.add(
8177 cmd("SET")
@@ -160,10 +156,6 @@
160156 self.add(cmd("HLEN").arg(key))
161157 }
162158
163 pub fn hincr_by(&mut self, key: &str, field: &str, delta: i64) -> &mut Self {
164 self.add(cmd("HINCRBY").arg(key).arg(field).arg(delta))
165 }
166
167159 pub fn hkeys(&mut self, key: &str) -> &mut Self {
168160 self.add(cmd("HKEYS").arg(key))
169161 }
@@ -254,15 +246,6 @@
254246 self.add(cmd("ZRANGE").arg(key).arg(start).arg(stop))
255247 }
256248
257 pub fn zremrangebyscore(&mut self, key: &str, min: f64, max: f64) -> &mut Self {
258 self.add(cmd("ZREMRANGEBYSCORE").arg(key).arg(min).arg(max))
259 }
260
261 pub fn set_json<T: Serialize>(&mut self, key: &str, value: &T) -> CacheResult<&mut Self> {
262 let encoded = json::encode(key, value)?;
263 Ok(self.set(key, &encoded))
264 }
265
266249 pub fn set_json_ex<T: Serialize>(
267250 &mut self,
268251 key: &str,
@@ -273,16 +256,6 @@
273256 Ok(self.set_ex(key, &encoded, ttl))
274257 }
275258
276 pub fn hset_json<T: Serialize>(
277 &mut self,
278 key: &str,
279 field: &str,
280 value: &T,
281 ) -> CacheResult<&mut Self> {
282 let encoded = json::encode(key, value)?;
283 Ok(self.hset(key, field, &encoded))
284 }
285
286259 pub async fn execute(&self) -> CacheResult<Vec<Value>> {
287260 if self.pipeline.is_empty() {
288261 return Ok(Vec::new());
@@ -290,25 +263,6 @@
290263 let mut conn = self.cache.conn().clone();
291264 self.pipeline
292265 .query_async(&mut conn)
293 .await
294 .map_err(CacheError::from)
295 }
296
297 pub async fn execute_typed<T: FromRedisValue + Send>(&self) -> CacheResult<T> {
298 let mut conn = self.cache.conn().clone();
299 self.pipeline
300 .query_async(&mut conn)
301 .await
302 .map_err(CacheError::from)
303 }
304
305 pub async fn execute_unit(&self) -> CacheResult<()> {
306 if self.pipeline.is_empty() {
307 return Ok(());
308 }
309 let mut conn = self.cache.conn().clone();
310 self.pipeline
311 .exec_async(&mut conn)
312266 .await
313267 .map_err(CacheError::from)
314268 }
Modified lib/cache/src/scripts.rs lib/cache/src/scripts.rs +0 −157 View lib/cache/src/scripts.rs
@@ -21,19 +21,6 @@
2121return 0
2222"#;
2323
24const COMPARE_AND_SET_CODE: &str = r#"
25local current = redis.call('GET', KEYS[1])
26local matches = current == ARGV[1]
27if ARGV[4] == '1' and current == false then
28 matches = true
29end
30if not matches then
31 return 0
32end
33redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
34return 1
35"#;
36
3724const INCR_EX_CODE: &str = r#"
3825local value = redis.call('INCR', KEYS[1])
3926if value == 1 then
@@ -52,31 +39,6 @@
5239return 0
5340"#;
5441
55const GETSET_EX_CODE: &str = r#"
56local previous = redis.call('GET', KEYS[1])
57redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
58return previous
59"#;
60
61const LPUSH_CAPPED_CODE: &str = r#"
62local length = redis.call('LPUSH', KEYS[1], ARGV[1])
63local limit = tonumber(ARGV[2])
64if limit > 0 and length > limit then
65 redis.call('LTRIM', KEYS[1], 0, limit - 1)
66 return limit
67end
68return length
69"#;
70
71const HSET_EX_CODE: &str = r#"
72redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
73local ttl = tonumber(ARGV[3])
74if ttl > 0 then
75 redis.call('PEXPIRE', KEYS[1], ttl)
76end
77return 1
78"#;
79
8042const LOCK_MANY_CODE: &str = r#"
8143for index = 1, #KEYS do
8244 if redis.call('SET', KEYS[index], ARGV[1], 'PX', ARGV[2], 'NX') == false then
@@ -89,33 +51,11 @@
8951return 1
9052"#;
9153
92const RATE_LIMIT_CODE: &str = r#"
93-- 时间取自 Redis 服务器(TIME),避免多实例应用时钟漂移破坏窗口。
94local t = redis.call('TIME')
95local now = t[1] * 1000 + math.floor(t[2] / 1000)
96local window = tonumber(ARGV[1])
97local limit = tonumber(ARGV[2])
98redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)
99local count = redis.call('ZCARD', KEYS[1])
100if count >= limit then
101 return {0, count}
102end
103redis.call('ZADD', KEYS[1], now, ARGV[3])
104redis.call('PEXPIRE', KEYS[1], window)
105return {1, count + 1}
106"#;
107
10854static RELEASE: LazyLock<Script> = LazyLock::new(|| Script::new(RELEASE_CODE));
10955static REFRESH: LazyLock<Script> = LazyLock::new(|| Script::new(REFRESH_CODE));
110static COMPARE_AND_SET: LazyLock<Script> = LazyLock::new(|| Script::new(COMPARE_AND_SET_CODE));
11156static INCR_EX: LazyLock<Script> = LazyLock::new(|| Script::new(INCR_EX_CODE));
11257static CONSUME_MATCHING: LazyLock<Script> = LazyLock::new(|| Script::new(CONSUME_MATCHING_CODE));
113static GETSET_EX: LazyLock<Script> = LazyLock::new(|| Script::new(GETSET_EX_CODE));
114static LPUSH_CAPPED: LazyLock<Script> = LazyLock::new(|| Script::new(LPUSH_CAPPED_CODE));
115static HSET_EX: LazyLock<Script> = LazyLock::new(|| Script::new(HSET_EX_CODE));
11658static LOCK_MANY: LazyLock<Script> = LazyLock::new(|| Script::new(LOCK_MANY_CODE));
117static RATE_LIMIT: LazyLock<Script> = LazyLock::new(|| Script::new(RATE_LIMIT_CODE));
118
11959#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12060pub struct RateLimit {
12161 pub allowed: bool,
@@ -153,16 +93,6 @@
15393 .map_err(CacheError::from)
15494 }
15595
156 pub async fn compare_and_delete(&self, key: &str, expected: &[u8]) -> CacheResult<bool> {
157 let mut conn = self.cache.conn().clone();
158 RELEASE
159 .key(key)
160 .arg(expected)
161 .invoke_async(&mut conn)
162 .await
163 .map_err(CacheError::from)
164 }
165
16696 pub async fn refresh_lock(&self, key: &str, token: &str, ttl: Duration) -> CacheResult<bool> {
16797 let mut conn = self.cache.conn().clone();
16898 REFRESH
@@ -174,26 +104,6 @@
174104 .map_err(CacheError::from)
175105 }
176106
177 pub async fn compare_and_set(
178 &self,
179 key: &str,
180 expected: &[u8],
181 value: &[u8],
182 ttl: Duration,
183 allow_missing: bool,
184 ) -> CacheResult<bool> {
185 let mut conn = self.cache.conn().clone();
186 COMPARE_AND_SET
187 .key(key)
188 .arg(expected)
189 .arg(value)
190 .arg(millis(ttl))
191 .arg(if allow_missing { "1" } else { "0" })
192 .invoke_async(&mut conn)
193 .await
194 .map_err(CacheError::from)
195 }
196
197107 pub async fn incr_ex(&self, key: &str, ttl: Duration) -> CacheResult<i64> {
198108 let mut conn = self.cache.conn().clone();
199109 INCR_EX
@@ -216,51 +126,6 @@
216126 .map_err(CacheError::from)
217127 }
218128
219 pub async fn getset_ex(
220 &self,
221 key: &str,
222 value: &[u8],
223 ttl: Duration,
224 ) -> CacheResult<Option<Vec<u8>>> {
225 let mut conn = self.cache.conn().clone();
226 GETSET_EX
227 .key(key)
228 .arg(value)
229 .arg(millis(ttl))
230 .invoke_async(&mut conn)
231 .await
232 .map_err(CacheError::from)
233 }
234
235 pub async fn lpush_capped(&self, key: &str, value: &[u8], limit: usize) -> CacheResult<i64> {
236 let mut conn = self.cache.conn().clone();
237 LPUSH_CAPPED
238 .key(key)
239 .arg(value)
240 .arg(limit)
241 .invoke_async(&mut conn)
242 .await
243 .map_err(CacheError::from)
244 }
245
246 pub async fn hset_ex(
247 &self,
248 key: &str,
249 field: &str,
250 value: &[u8],
251 ttl: Option<Duration>,
252 ) -> CacheResult<bool> {
253 let mut conn = self.cache.conn().clone();
254 HSET_EX
255 .key(key)
256 .arg(field)
257 .arg(value)
258 .arg(ttl.map(millis).unwrap_or_default())
259 .invoke_async(&mut conn)
260 .await
261 .map_err(CacheError::from)
262 }
263
264129 pub async fn lock_many(
265130 &self,
266131 keys: &[String],
@@ -283,27 +148,5 @@
283148 .invoke_async(&mut conn)
284149 .await
285150 .map_err(CacheError::from)
286 }
287
288 pub async fn rate_limit(
289 &self,
290 key: &str,
291 limit: usize,
292 window: Duration,
293 member: &str,
294 ) -> CacheResult<RateLimit> {
295 let mut conn = self.cache.conn().clone();
296 let reply: Vec<i64> = RATE_LIMIT
297 .key(key)
298 .arg(millis(window))
299 .arg(limit)
300 .arg(member)
301 .invoke_async(&mut conn)
302 .await
303 .map_err(CacheError::from)?;
304 Ok(RateLimit {
305 allowed: reply.first().copied().unwrap_or_default() == 1,
306 count: reply.get(1).copied().unwrap_or_default(),
307 })
308151 }
309152}
Modified lib/cache/src/set.rs lib/cache/src/set.rs +1 −31 View lib/cache/src/set.rs
@@ -1,4 +1,4 @@
1use redis::{AsyncCommands, Cmd, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
1use redis::{AsyncCommands, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
22
33use crate::error::{CacheError, CacheResult};
44use crate::kv::Cache;
@@ -13,15 +13,6 @@
1313 conn.sadd(key, member).await.map_err(CacheError::from)
1414 }
1515
16 pub async fn sadd_many<V: ToRedisArgs + Send + Sync>(
17 &self,
18 key: &str,
19 members: Vec<V>,
20 ) -> CacheResult<usize> {
21 let mut conn = self.conn().clone();
22 conn.sadd(key, members).await.map_err(CacheError::from)
23 }
24
2516 pub async fn srem<V: ToRedisArgs + Send + Sync>(
2617 &self,
2718 key: &str,
@@ -31,17 +22,6 @@
3122 conn.srem(key, members).await.map_err(CacheError::from)
3223 }
3324
34 pub async fn smove<V: ToRedisArgs + Send + Sync>(
35 &self,
36 source: &str,
37 destination: &str,
38 member: V,
39 ) -> CacheResult<bool> {
40 let mut cmd = Cmd::new();
41 cmd.arg("SMOVE").arg(source).arg(destination).arg(member);
42 self.exec(&cmd).await
43 }
44
4525 pub async fn sismember<M: ToSingleRedisArg + Send + Sync>(
4626 &self,
4727 key: &str,
@@ -88,15 +68,5 @@
8868 pub async fn spop<T: FromRedisValue + Send>(&self, key: &str) -> CacheResult<Option<T>> {
8969 let mut conn = self.conn().clone();
9070 conn.spop(key).await.map_err(CacheError::from)
91 }
92
93 pub async fn spop_many<T: FromRedisValue + Send>(
94 &self,
95 key: &str,
96 count: usize,
97 ) -> CacheResult<Vec<T>> {
98 let mut cmd = Cmd::new();
99 cmd.arg("SPOP").arg(key).arg(count);
100 self.exec(&cmd).await
10171 }
10272}
Modified lib/cache/src/zset.rs lib/cache/src/zset.rs +1 −39 View lib/cache/src/zset.rs
@@ -1,4 +1,4 @@
1use redis::{AsyncCommands, Cmd, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
1use redis::{AsyncCommands, FromRedisValue, ToRedisArgs, ToSingleRedisArg};
22
33use crate::error::{CacheError, CacheResult};
44use crate::kv::Cache;
@@ -16,30 +16,6 @@
1616 .map_err(CacheError::from)
1717 }
1818
19 pub async fn zadd_many<M: ToRedisArgs + Send + Sync>(
20 &self,
21 key: &str,
22 items: &[(f64, M)],
23 ) -> CacheResult<usize> {
24 let mut cmd = Cmd::new();
25 cmd.arg("ZADD").arg(key);
26 for (score, member) in items {
27 cmd.arg(*score).arg(member);
28 }
29 self.exec(&cmd).await
30 }
31
32 pub async fn zadd_nx<M: ToRedisArgs + Send + Sync>(
33 &self,
34 key: &str,
35 member: M,
36 score: f64,
37 ) -> CacheResult<usize> {
38 let mut cmd = Cmd::new();
39 cmd.arg("ZADD").arg(key).arg("NX").arg(score).arg(member);
40 self.exec(&cmd).await
41 }
42
4319 pub async fn zrem<M: ToRedisArgs + Send + Sync>(
4420 &self,
4521 key: &str,
@@ -181,19 +157,5 @@
181157 ) -> CacheResult<Option<usize>> {
182158 let mut conn = self.conn().clone();
183159 conn.zrevrank(key, member).await.map_err(CacheError::from)
184 }
185
186 pub async fn zremrangebyscore<
187 M: ToSingleRedisArg + Send + Sync,
188 MM: ToSingleRedisArg + Send + Sync,
189 >(
190 &self,
191 key: &str,
192 min: M,
193 max: MM,
194 ) -> CacheResult<usize> {
195 let mut cmd = Cmd::new();
196 cmd.arg("ZREMRANGEBYSCORE").arg(key).arg(min).arg(max);
197 self.exec(&cmd).await
198160 }
199161}
Modified lib/chat/src/share/lookup.rs lib/chat/src/share/lookup.rs +2 −6 View lib/chat/src/share/lookup.rs
@@ -1,4 +1,4 @@
1use models::room::rooms::{self, RoomKind};
1use models::room::rooms::{self};
22use models::room::{room_bans, room_members};
33use models::space::{space_members, spaces};
44use sea_orm::EntityTrait;
@@ -14,11 +14,7 @@
1414 pub is_dm: bool,
1515}
1616
17impl RoomContext {
18 pub fn is_thread_room(&self) -> bool {
19 matches!(self.room.kind, RoomKind::Thread)
20 }
21}
17impl RoomContext {}
2218
2319/// Route slug (`lower_name`) of the space owning `space_id`, or `None` for a
2420/// DM. Notification payloads carry it so the inbox can deep-link straight to
Modified lib/config/src/app/app_config.rs lib/config/src/app/app_config.rs +0 −7 View lib/config/src/app/app_config.rs
@@ -41,13 +41,6 @@
4141 .filter(|key| key.enabled && key.private_key.is_some())
4242 }
4343
44 pub fn audience_or_id(&self) -> &str {
45 match &self.audience {
46 Some(audience) => audience.as_str(),
47 None => self.id.as_str(),
48 }
49 }
50
5144 pub fn validate(&self) -> Result<(), String> {
5245 let id = self.id.trim();
5346 if id.is_empty() {
Modified lib/config/src/auth.rs lib/config/src/auth.rs +0 −24 View lib/config/src/auth.rs
@@ -119,30 +119,6 @@
119119 }
120120 Ok(())
121121 }
122
123 pub fn validate_auth(&self) -> Result<(), String> {
124 self.validate_common()?;
125 let Some(key) = &self.totp_encryption_key else {
126 return Err("auth.totp_encryption_key is required".to_string());
127 };
128 if key.trim().starts_with("REPLACE_WITH_") {
129 return Err(
130 "auth.totp_encryption_key is still set to the placeholder from \
131 config.yaml.example"
132 .to_string(),
133 );
134 }
135 let length = base64::engine::general_purpose::STANDARD
136 .decode(key.trim())
137 .map(|bytes| bytes.len())
138 .unwrap_or(0);
139 if length < TOTP_KEY_MIN_BYTES {
140 return Err(format!(
141 "auth.totp_encryption_key must be base64 of at least {TOTP_KEY_MIN_BYTES} bytes"
142 ));
143 }
144 Ok(())
145 }
146122}
147123
148124impl crate::loader::Section for AuthConfig {
Modified lib/config/src/database.rs lib/config/src/database.rs +0 −12 View lib/config/src/database.rs
@@ -237,18 +237,6 @@
237237 replica.apply_to(&mut opts)?;
238238 Ok(opts)
239239 }
240
241 pub fn connect_options_for(&self, role: DatabaseRole) -> Result<ConnectOptions, String> {
242 match role {
243 DatabaseRole::Primary => self.primary_connect_options(),
244 DatabaseRole::Replica => {
245 if self.replicas.is_empty() {
246 return self.primary_connect_options();
247 }
248 self.replica_connect_options(0)
249 }
250 }
251 }
252240}
253241
254242fn url_encode(value: &str) -> String {
Modified lib/config/src/email.rs lib/config/src/email.rs +0 −4 View lib/config/src/email.rs
@@ -68,10 +68,6 @@
6868 Ok(())
6969 }
7070
71 pub fn enqueue_timeout(&self) -> std::time::Duration {
72 std::time::Duration::from_millis(self.enqueue_timeout_ms)
73 }
74
7571 pub fn retry_delay(&self) -> std::time::Duration {
7672 std::time::Duration::from_millis(self.retry_delay_ms)
7773 }
Modified lib/config/src/loader.rs lib/config/src/loader.rs +0 −9 View lib/config/src/loader.rs
@@ -114,15 +114,6 @@
114114 self.get(T::KEY)
115115 }
116116
117 pub fn section_or_default<T: Section + Default + DeserializeOwned>(
118 &self,
119 ) -> Result<T, ConfigLoadError> {
120 if !self.has(T::KEY) {
121 return Ok(T::default());
122 }
123 self.get(T::KEY)
124 }
125
126117 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, ConfigLoadError> {
127118 self.figment
128119 .extract_inner(key)
Modified lib/crypt/src/key.rs lib/crypt/src/key.rs +0 −7 View lib/crypt/src/key.rs
@@ -117,13 +117,6 @@
117117 })
118118 }
119119
120 pub fn from_key_pair(pair: &RsaKeyPair) -> Self {
121 Self {
122 key_id: pair.key_id().to_owned(),
123 public: pair.public(),
124 }
125 }
126
127120 pub fn key_id(&self) -> &str {
128121 &self.key_id
129122 }
Modified lib/db/src/error.rs lib/db/src/error.rs +0 −12 View lib/db/src/error.rs
@@ -21,14 +21,6 @@
2121 }
2222 }
2323
24 pub fn is_connectivity(&self) -> bool {
25 match self {
26 Self::Config(_) => false,
27 Self::Database(error) => is_connect_error(error),
28 Self::Connect { .. } => true,
29 }
30 }
31
3224 pub fn label(&self) -> Option<&str> {
3325 match self {
3426 Self::Connect { label, .. } => Some(label),
@@ -55,10 +47,6 @@
5547 Self::Connect { source, .. } => Some(source),
5648 }
5749 }
58}
59
60fn is_connect_error(error: &DbErr) -> bool {
61 matches!(error, DbErr::ConnectionAcquire(_) | DbErr::Conn(_))
6250}
6351
6452impl From<DbErr> for DbError {
Modified lib/deletion/src/lib.rs lib/deletion/src/lib.rs +0 −101 View lib/deletion/src/lib.rs
@@ -218,109 +218,8 @@
218218 .map(status_of_account))
219219}
220220
221pub async fn request_space_deletion(
222 state: &AppState,
223 actor: i64,
224 space_id: Uuid,
225 reason: &str,
226) -> Result<SpaceDeletionStatus, DeletionError> {
227 let db = state.database.write();
228 let space = spaces::Entity::find_by_id(space_id)
229 .one(db)
230 .await?
231 .ok_or_else(|| DeletionError::NotFound("space not found".into()))?;
232 if space.status != SpaceStatus::Archived {
233 let mut active: spaces::ActiveModel = space.into();
234 active.status = Set(SpaceStatus::Archived);
235 active.archived_at = Set(Some(sea_orm::prelude::DateTimeUtc::from(
236 std::time::SystemTime::now(),
237 )));
238 active.suspended_reason = Set(Some(reason.to_string()));
239 active.update(db).await?;
240 }
241
242 let now = now();
243 let purge_after = grace_until(state.config.space.deletion_grace_days);
244 let row = match space_deletion::Entity::find()
245 .filter(space_deletion::Column::SpaceId.eq(space_id))
246 .one(db)
247 .await?
248 {
249 Some(existing) => {
250 let mut active: space_deletion::ActiveModel = existing.into();
251 active.requested_by = Set(actor);
252 active.requested_at = Set(now);
253 active.purge_after = Set(purge_after);
254 active.status = Set("pending".into());
255 active.available_at = Set(purge_after);
256 active.last_error = Set(None);
257 active.attempts = Set(0);
258 active.updated_at = Set(now);
259 active.update(db).await?
260 }
261 None => {
262 space_deletion::ActiveModel {
263 id: Set(Uuid::now_v7()),
264 space_id: Set(space_id),
265 requested_by: Set(actor),
266 requested_at: Set(now),
267 purge_after: Set(purge_after),
268 status: Set("pending".into()),
269 last_error: Set(None),
270 attempts: Set(0),
271 available_at: Set(purge_after),
272 created_at: Set(now),
273 updated_at: Set(now),
274 }
275 .insert(db)
276 .await?
277 }
278 };
279 Ok(status_of_space(&row))
280}
281
282pub async fn cancel_space_deletion(
283 state: &AppState,
284 _actor: i64,
285 space_id: Uuid,
286) -> Result<(), DeletionError> {
287 let db = state.database.write();
288 if let Some(space) = spaces::Entity::find_by_id(space_id).one(db).await? {
289 let mut active: spaces::ActiveModel = space.into();
290 active.status = Set(SpaceStatus::Active);
291 active.archived_at = Set(None);
292 active.update(db).await?;
293 }
294 space_deletion::Entity::delete_many()
295 .filter(space_deletion::Column::SpaceId.eq(space_id))
296 .exec(db)
297 .await?;
298 Ok(())
299}
300
301pub async fn space_deletion_status(
302 state: &AppState,
303 space_id: Uuid,
304) -> Result<Option<SpaceDeletionStatus>, DeletionError> {
305 Ok(space_deletion::Entity::find()
306 .filter(space_deletion::Column::SpaceId.eq(space_id))
307 .one(state.database.read())
308 .await?
309 .as_ref()
310 .map(status_of_space))
311}
312
313221fn status_of_account(row: &account_deletion::Model) -> AccountDeletionStatus {
314222 AccountDeletionStatus {
315 status: row.status.clone(),
316 requested_at: row.requested_at,
317 purge_after: row.purge_after,
318 last_error: row.last_error.clone(),
319 }
320}
321
322fn status_of_space(row: &space_deletion::Model) -> SpaceDeletionStatus {
323 SpaceDeletionStatus {
324223 status: row.status.clone(),
325224 requested_at: row.requested_at,
326225 purge_after: row.purge_after,
Modified lib/envelope/src/cors.rs lib/envelope/src/cors.rs +0 −8 View lib/envelope/src/cors.rs
@@ -84,14 +84,6 @@
8484 }
8585}
8686
87/// 静态 CORS 白名单判定:envelope 中间件与 OAuth 端点级 CORS 共用同一口径。
88pub fn web_allows(web_cfg: &WebConfig, origin: &str) -> bool {
89 web_cfg
90 .allow_origins()
91 .iter()
92 .any(|allowed| allowed.eq_ignore_ascii_case(origin))
93}
94
9587fn web_allows_value(allow_any: &bool, allow_origins: &[String], origin: &str) -> bool {
9688 *allow_any
9789 || allow_origins
Modified lib/envelope/src/response.rs lib/envelope/src/response.rs +0 −25 View lib/envelope/src/response.rs
@@ -70,31 +70,6 @@
7070 }
7171 }
7272
73 pub fn err_with_detail(
74 code: i32,
75 message: impl Into<String>,
76 detail: impl Into<String>,
77 ) -> Self {
78 Self {
79 code,
80 message: message.into(),
81 data: None,
82 error: Some(detail.into()),
83 error_code: None,
84 request_id: String::new(),
85 }
86 }
87
88 pub fn with_code(mut self, code: i32) -> Self {
89 self.code = code;
90 self
91 }
92
93 pub fn with_message(mut self, message: impl Into<String>) -> Self {
94 self.message = message.into();
95 self
96 }
97
9873 pub fn with_error(mut self, error: impl Into<String>) -> Self {
9974 self.error = Some(error.into());
10075 self
Modified lib/events/src/outbox.rs lib/events/src/outbox.rs +1 −37 View lib/events/src/outbox.rs
@@ -1,7 +1,7 @@
11use std::sync::LazyLock;
22use std::time::{Duration, Instant};
33
4use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use chrono::{DateTime, Utc};
55use futures_util::future;
66use models::event_outbox;
77use opentelemetry::metrics::Gauge;
@@ -312,42 +312,6 @@
312312 event_outbox::Entity::update_many()
313313 .set(event_outbox::ActiveModel {
314314 available_at: Set(Utc::now()),
315 lease_id: Set(None),
316 leased_until: Set(None),
317 ..Default::default()
318 })
319 .filter(event_outbox::Column::Id.is_in(ids.to_vec()))
320 .filter(event_outbox::Column::PublishedAt.is_null())
321 .exec(db)
322 .await?;
323 Ok(())
324}
325
326pub async fn park(db: &DatabaseConnection, ids: &[Uuid], grace_secs: i64) -> Result<(), DbErr> {
327 if ids.is_empty() {
328 return Ok(());
329 }
330 event_outbox::Entity::update_many()
331 .set(event_outbox::ActiveModel {
332 available_at: Set(Utc::now() + ChronoDuration::seconds(grace_secs.max(0))),
333 lease_id: Set(None),
334 leased_until: Set(None),
335 ..Default::default()
336 })
337 .filter(event_outbox::Column::Id.is_in(ids.to_vec()))
338 .filter(event_outbox::Column::PublishedAt.is_null())
339 .exec(db)
340 .await?;
341 Ok(())
342}
343
344pub async fn discard(db: &DatabaseConnection, ids: &[Uuid]) -> Result<(), DbErr> {
345 if ids.is_empty() {
346 return Ok(());
347 }
348 event_outbox::Entity::update_many()
349 .set(event_outbox::ActiveModel {
350 published_at: Set(Some(Utc::now())),
351315 lease_id: Set(None),
352316 leased_until: Set(None),
353317 ..Default::default()
Modified lib/export/src/database.rs lib/export/src/database.rs +0 −7 View lib/export/src/database.rs
@@ -127,13 +127,6 @@
127127 self.pick_replica().unwrap_or(&self.primary)
128128 }
129129
130 pub fn for_role(&self, role: DatabaseRole) -> &DatabaseConnection {
131 match role {
132 DatabaseRole::Primary => self.write(),
133 DatabaseRole::Replica => self.read(),
134 }
135 }
136
137130 pub fn route_for(&self, sql: &str) -> &DatabaseConnection {
138131 if db::requires_primary(sql) {
139132 self.write()
Modified lib/export/src/redis.rs lib/export/src/redis.rs +0 −132 View lib/export/src/redis.rs
@@ -398,81 +398,6 @@
398398 self.incr_window(key, window_secs).await <= max
399399 }
400400
401 /// Opens a multiplexed connection to every master node of the cluster.
402 ///
403 /// Masters are discovered at call time via `CLUSTER SLOTS`, so this keeps
404 /// working after topology changes. Returns an empty list for standalone
405 /// connections.
406 pub async fn cluster_master_connections(
407 &self,
408 ) -> Result<Vec<MultiplexedConnection>, ExportError> {
409 let mut conn = self.connection_clone();
410 let RedisConnection::Cluster(cluster) = &mut conn else {
411 return Ok(Vec::new());
412 };
413
414 let slots: Value = redis::cmd("CLUSTER")
415 .arg("SLOTS")
416 .query_async(cluster)
417 .await
418 .map_err(ExportError::from)?;
419
420 let node_info = self.node_info.clone().ok_or_else(|| {
421 ExportError::InvalidConfig("cluster connection info is unavailable".into())
422 })?;
423
424 let mut seen = std::collections::HashSet::new();
425 let mut connections = Vec::new();
426
427 for (host, port) in cluster_master_addrs(&slots) {
428 if !seen.insert((host.clone(), port)) {
429 continue;
430 }
431
432 let addr = match node_info.addr() {
433 ConnectionAddr::Tcp(_, _) => ConnectionAddr::Tcp(host.clone(), port),
434 ConnectionAddr::TcpTls {
435 insecure,
436 tls_params,
437 ..
438 } => ConnectionAddr::TcpTls {
439 host: host.clone(),
440 port,
441 insecure: *insecure,
442 tls_params: tls_params.clone(),
443 },
444 ConnectionAddr::Unix(_) => {
445 return Err(ExportError::InvalidConfig(
446 "unix sockets are not supported for cluster connections".into(),
447 ));
448 }
449 _ => {
450 return Err(ExportError::InvalidConfig(
451 "unsupported connection address type for cluster connections".into(),
452 ));
453 }
454 };
455
456 let mut info = node_info.clone();
457 info = info.set_addr(addr);
458
459 let client = redis::Client::open(info).map_err(ExportError::from)?;
460 // BRPOP 之类的阻塞命令会持有连接直到超时;必须关闭响应超时,
461 // 否则默认超时会让阻塞读被误判为 timeout 错误。
462 let conn_config = redis::AsyncConnectionConfig::new()
463 .set_response_timeout(None)
464 .set_connection_timeout(Some(self.config.connection_timeout()));
465 let connection = client
466 .get_multiplexed_async_connection_with_config(&conn_config)
467 .await
468 .map_err(ExportError::from)?;
469
470 connections.push(connection);
471 }
472
473 Ok(connections)
474 }
475
476401 /// Returns `(slot_start, slot_end, master_index)` triples where `master_index`
477402 /// matches the position of the owning connection in [`cluster_master_connections`].
478403 ///
@@ -530,32 +455,6 @@
530455 None
531456}
532457
533fn cluster_master_addrs(slots: &Value) -> Vec<(String, u16)> {
534 let Value::Array(groups) = slots else {
535 return Vec::new();
536 };
537
538 let mut masters = Vec::new();
539 for group in groups {
540 let Value::Array(fields) = group else {
541 continue;
542 };
543
544 let Some(Value::Array(nodes)) = fields.get(2) else {
545 continue;
546 };
547
548 let (Some(Value::BulkString(host)), Some(Value::Int(port))) = (nodes.first(), nodes.get(1))
549 else {
550 continue;
551 };
552
553 masters.push((String::from_utf8_lossy(host).into_owned(), *port as u16));
554 }
555
556 masters
557}
558
559458/// Builds the connection info of the first configured node, used as a template
560459/// (scheme, auth, tls settings) for connections to other cluster nodes.
561460fn connection_info(urls: &str, config: &RedisConfig) -> Result<ConnectionInfo, ExportError> {
@@ -749,37 +648,6 @@
749648pub async fn exists(redis: &AppRedis, key: &str) -> Result<bool, redis::RedisError> {
750649 let mut conn = redis.connection_clone();
751650 dispatch!(conn, |c| redis::cmd("EXISTS").arg(key).query_async(c).await)
752}
753
754pub async fn getdel_string(
755 redis: &AppRedis,
756 key: &str,
757) -> Result<Option<String>, redis::RedisError> {
758 let mut conn = redis.connection_clone();
759 dispatch!(conn, |c| redis::cmd("GETDEL").arg(key).query_async(c).await)
760}
761
762const INCR_EXPIRE_SCRIPT: &str = r#"local c = redis.call('INCR', KEYS[1])
763if c == 1 then
764 redis.call('EXPIRE', KEYS[1], ARGV[1])
765end
766return c"#;
767
768pub async fn incr_expire(
769 redis: &AppRedis,
770 key: &str,
771 ttl_secs: u64,
772) -> Result<i64, redis::RedisError> {
773 let mut conn = redis.connection_clone();
774 dispatch!(conn, |c| {
775 redis::cmd("EVAL")
776 .arg(INCR_EXPIRE_SCRIPT)
777 .arg(1)
778 .arg(key)
779 .arg(ttl_secs)
780 .query_async(c)
781 .await
782 })
783651}
784652
785653/// Extracts `(start, end, master_index)` for every slot range.
Modified lib/git/src/context/cache.rs lib/git/src/context/cache.rs +0 −18 View lib/git/src/context/cache.rs
@@ -1,21 +1,6 @@
11use crate::context::facade::GitContext;
22
33impl GitContext {
4 pub async fn cache_insert<V>(
5 &self,
6 key: &str,
7 value: &V,
8 ttl: Option<std::time::Duration>,
9 ) -> Result<(), crate::share::errors::GitError>
10 where
11 V: serde::Serialize,
12 {
13 match ttl {
14 Some(ttl) => self.cache.set_json_ex(key, value, ttl).await?,
15 None => self.cache.set_json(key, value).await?,
16 }
17 Ok(())
18 }
194 pub async fn cache_get<V>(&self, key: &str) -> Result<Option<V>, crate::share::errors::GitError>
205 where
216 V: serde::de::DeserializeOwned,
@@ -25,8 +10,5 @@
2510 pub async fn cache_remove(&self, key: &str) -> Result<(), crate::share::errors::GitError> {
2611 let _ = self.cache.delete(key).await?;
2712 Ok(())
28 }
29 pub async fn cache_exists(&self, key: &str) -> Result<bool, crate::share::errors::GitError> {
30 Ok(self.cache.exists(key).await?)
3113 }
3214}
Modified lib/git/src/context/facade.rs lib/git/src/context/facade.rs +0 −9 View lib/git/src/context/facade.rs
@@ -5,7 +5,6 @@
55use export::Database;
66use models::repo::repos;
77use proto::kenshell::common::v1::{GitActor, RepositoryRef, WriteOptions};
8use uuid::Uuid;
98
109#[derive(Clone)]
1110pub struct GitContext {
@@ -45,17 +44,9 @@
4544 }),
4645 require_all_replicas: false,
4746 }
48 }
49
50 pub fn repo_dir(&self) -> PathBuf {
51 repo_dir_for(&self.repo_root, self.model.id)
5247 }
5348
5449 pub fn repo_uninitialized(&self) -> bool {
5550 self.model.nums_commits == 0
5651 }
57}
58
59pub fn repo_dir_for(repo_root: &std::path::Path, repo_id: Uuid) -> PathBuf {
60 repo_root.join(repo_id.to_string())
6152}
Modified lib/git/src/handler/html/error.rs lib/git/src/handler/html/error.rs +0 −4 View lib/git/src/handler/html/error.rs
@@ -38,7 +38,3 @@
3838 }
3939 resp
4040}
41
42pub fn err_archived() -> String {
43 String::new()
44}
Modified lib/git/src/handler/html/mod.rs lib/git/src/handler/html/mod.rs +0 −7 View lib/git/src/handler/html/mod.rs
@@ -241,13 +241,6 @@
241241 Some(p.trim().to_string())
242242}
243243
244pub fn validate_cursor(c: &str) -> Option<String> {
245 if c.len() != 40 || !c.chars().all(|x| x.is_ascii_hexdigit()) {
246 return None;
247 }
248 Some(c.to_ascii_lowercase())
249}
250
251244pub fn validate_revision(rev: &str) -> Option<String> {
252245 let r = rev.trim();
253246 if r.is_empty() || r.len() > MAX_REVISION_LEN {
Modified lib/git/src/lfs/error.rs lib/git/src/lfs/error.rs +0 −12 View lib/git/src/lfs/error.rs
@@ -1,13 +1 @@
11pub use crate::share::errors::GitError;
2
3pub fn http_status(err: &GitError) -> u16 {
4 match err {
5 GitError::NotFound => 404,
6 GitError::Unauthorized => 401,
7 GitError::Forbidden => 403,
8 GitError::Conflict(_) => 409,
9 GitError::InvalidArgument(_) => 400,
10 GitError::Unimplemented(_) => 501,
11 GitError::Cache(_) | GitError::Git(_) | GitError::Internal(_) => 500,
12 }
13}
Modified lib/git/src/rpcgit/diff/types.rs lib/git/src/rpcgit/diff/types.rs +1 −13 View lib/git/src/rpcgit/diff/types.rs
@@ -34,19 +34,7 @@
3434 Copied,
3535}
3636
37impl DiffStatus {
38 pub fn from_char(ch: char) -> Option<Self> {
39 match ch {
40 'A' => Some(Self::Added),
41 'D' => Some(Self::Deleted),
42 'M' => Some(Self::Modified),
43 'T' => Some(Self::TypeChanged),
44 'R' => Some(Self::Renamed),
45 'C' => Some(Self::Copied),
46 _ => None,
47 }
48 }
49}
37impl DiffStatus {}
5038
5139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5240pub struct ChangedPath {
Modified lib/git/src/rpcgit/api.rs lib/git/src/rpcgit/api.rs +1 −105 View lib/git/src/rpcgit/api.rs
@@ -29,8 +29,7 @@
2929use crate::rpcgit::error::BabyError;
3030use crate::rpcgit::oid::ObjectId;
3131use crate::rpcgit::refs::types::{ListRefsOptions, RefInfo, RefType};
32use crate::rpcgit::remote::types::{RemoteOption, UpdateRemoteMirrorRequest};
33use crate::rpcgit::setup::types::{FastImportCommit, ObjectFormat};
32use crate::rpcgit::setup::types::ObjectFormat;
3433use crate::rpcgit::tags::types::{
3534 CreateTagOptions, ListTagsOptions, TagInfo, TagType, TagVerifyResult,
3635};
@@ -699,21 +698,6 @@
699698 Ok(refs.into_iter().filter(|r| r.sha == target).collect())
700699 }
701700
702 pub async fn ref_exists(&self, name: &str) -> Result<bool, BabyError> {
703 let mut client = self.refs_client();
704 let response = client
705 .get_ref(pb::GetRefRequest {
706 repository: self.repository_ref_some(),
707 name: name.to_string(),
708 })
709 .await;
710 match response {
711 Ok(_) => Ok(true),
712 Err(status) if status.code() == tonic::Code::NotFound => Ok(false),
713 Err(status) => Err(BabyError::from(status)),
714 }
715 }
716
717701 pub async fn update_ref(
718702 &self,
719703 name: &str,
@@ -845,14 +829,6 @@
845829 oid_opt("0000000000000000000000000000000000000000").expect("zero")
846830 }),
847831 })
848 }
849
850 pub async fn branch_exists(&self, name: &str) -> Result<bool, BabyError> {
851 match self.find_branch(name).await {
852 Ok(_) => Ok(true),
853 Err(BabyError::BranchNotFound(_)) => Ok(false),
854 Err(error) => Err(error),
855 }
856832 }
857833
858834 pub async fn create_branch(
@@ -952,14 +928,6 @@
952928 .ok_or_else(|| BabyError::TagNotFound(name.to_string()))
953929 }
954930
955 pub async fn tag_exists(&self, name: &str) -> Result<bool, BabyError> {
956 match self.find_tag(name).await {
957 Ok(_) => Ok(true),
958 Err(BabyError::TagNotFound(_)) => Ok(false),
959 Err(error) => Err(error),
960 }
961 }
962
963931 pub async fn create_lightweight_tag(&self, opts: CreateTagOptions) -> Result<(), BabyError> {
964932 self.create_tag(opts, false).await
965933 }
@@ -1257,22 +1225,6 @@
12571225 });
12581226 }
12591227 Ok(out)
1260 }
1261
1262 pub async fn diverging_commits(
1263 &self,
1264 base: &str,
1265 target: &str,
1266 ) -> Result<crate::rpcgit::compare::types::DivergeObject, BabyError> {
1267 let ahead = self
1268 .commit_ids_between_reverse(base, target, None, None)
1269 .await?
1270 .len() as u32;
1271 let behind = self
1272 .commit_ids_between_reverse(target, base, None, None)
1273 .await?
1274 .len() as u32;
1275 Ok(crate::rpcgit::compare::types::DivergeObject { ahead, behind })
12761228 }
12771229
12781230 pub async fn commit_ids_between_reverse(
@@ -1330,26 +1282,6 @@
13301282 .collect())
13311283 }
13321284
1333 pub async fn add_remote(
1334 &self,
1335 name: &str,
1336 url: &str,
1337 _option: RemoteOption,
1338 ) -> Result<(), BabyError> {
1339 let mut client = self.context().ks.remote();
1340 client
1341 .upsert_remote(pb::UpsertRemoteRequest {
1342 repository: self.repository_ref_some(),
1343 name: name.to_string(),
1344 url: url.to_string(),
1345 actor: Some(self.context().actor()),
1346 options: Some(self.context().write_options()),
1347 })
1348 .await
1349 .map_err(BabyError::from)?;
1350 Ok(())
1351 }
1352
13531285 pub async fn remove_remote(&self, name: &str) -> Result<(), BabyError> {
13541286 let mut client = self.context().ks.remote();
13551287 client
@@ -1364,15 +1296,6 @@
13641296 Ok(())
13651297 }
13661298
1367 pub async fn get_remote_address(&self, name: &str) -> Result<Option<String>, BabyError> {
1368 Ok(self
1369 .list_remotes()
1370 .await?
1371 .into_iter()
1372 .find(|(remote, _)| remote == name)
1373 .map(|(_, url)| url))
1374 }
1375
13761299 pub async fn create_archive(&self, req: CreateArchiveRequest) -> Result<(), BabyError> {
13771300 let mut client = self.context().ks.archive();
13781301 client
@@ -1470,18 +1393,6 @@
14701393 Err(unsupported("merge tree"))
14711394 }
14721395
1473 pub async fn fast_import(
1474 &self,
1475 _path: &Path,
1476 _commits: Vec<FastImportCommit>,
1477 ) -> Result<(), BabyError> {
1478 Err(unsupported("fast-import"))
1479 }
1480
1481 pub async fn config_set(&self, _key: &str, _value: &str) -> Result<(), BabyError> {
1482 Ok(())
1483 }
1484
14851396 pub async fn verify_tag_signature(&self, _name: &str) -> Result<TagVerifyResult, BabyError> {
14861397 Err(unsupported("tag signature verification"))
14871398 }
@@ -1495,21 +1406,6 @@
14951406
14961407 pub async fn prune(&self) -> Result<(), BabyError> {
14971408 Err(unsupported("prune"))
1498 }
1499
1500 pub async fn update_remote_mirror(
1501 &self,
1502 _req: UpdateRemoteMirrorRequest,
1503 ) -> Result<crate::rpcgit::remote::types::UpdateRemoteMirrorResult, BabyError> {
1504 Err(unsupported("remote mirror update"))
1505 }
1506
1507 pub async fn find_remote_repository(&self, _url: &str) -> Result<bool, BabyError> {
1508 Ok(true)
1509 }
1510
1511 pub async fn config_get(&self, _key: &str) -> Result<Option<String>, BabyError> {
1512 Ok(None)
15131409 }
15141410}
15151411
Modified lib/git/src/rpcgit/oid.rs lib/git/src/rpcgit/oid.rs +0 −4 View lib/git/src/rpcgit/oid.rs
@@ -31,10 +31,6 @@
3131 Ok(Self(text.to_ascii_lowercase().into_boxed_str()))
3232 }
3333
34 pub fn from_bytes_or_panic(bytes: &[u8]) -> Self {
35 Self::from_hex(bytes).expect("invalid object id")
36 }
37
3834 pub fn as_bytes(&self) -> &[u8] {
3935 self.0.as_bytes()
4036 }
Modified lib/git/src/service/cache.rs lib/git/src/service/cache.rs +0 −55 View lib/git/src/service/cache.rs
@@ -41,58 +41,3 @@
4141 let _ = cache.set_json_ex(&sanitized, &val, ttl).await;
4242 Ok(val)
4343}
44
45fn oid_key(kind: &str, oid: &str) -> String {
46 sanitize_key(&format!("object.{kind}.{oid}"))
47}
48
49fn rev_key(module: &str, func: &str, key_part: &str, tip: &str) -> String {
50 sanitize_key(&format!("{module}.{func}.{key_part}:{tip}"))
51}
52
53pub async fn cache_oid_get<V>(cache: &Cache, kind: &str, oid: &str) -> Result<Option<V>, GitError>
54where
55 V: DeserializeOwned,
56{
57 Ok(cache.get_json(&oid_key(kind, oid)).await?)
58}
59
60pub async fn cache_oid_put<V>(cache: &Cache, kind: &str, oid: &str, v: &V) -> Result<(), GitError>
61where
62 V: Serialize,
63{
64 cache.set_json(&oid_key(kind, oid), v).await?;
65 Ok(())
66}
67
68pub async fn cache_rev_get<V>(
69 cache: &Cache,
70 module: &str,
71 func: &str,
72 key_part: &str,
73 tip: &str,
74) -> Result<Option<V>, GitError>
75where
76 V: DeserializeOwned,
77{
78 Ok(cache
79 .get_json(&rev_key(module, func, key_part, tip))
80 .await?)
81}
82
83pub async fn cache_rev_put<V>(
84 cache: &Cache,
85 module: &str,
86 func: &str,
87 key_part: &str,
88 tip: &str,
89 v: &V,
90) -> Result<(), GitError>
91where
92 V: Serialize,
93{
94 cache
95 .set_json_ex(&rev_key(module, func, key_part, tip), v, REV_TTL)
96 .await?;
97 Ok(())
98}
Modified lib/git/src/service/clone.rs lib/git/src/service/clone.rs +0 −14 View lib/git/src/service/clone.rs
@@ -1,5 +1,3 @@
1use crate::share::errors::GitError;
2
31#[derive(Debug, Clone)]
42pub struct GitCredential {
53 pub username: String,
@@ -14,16 +12,4 @@
1412 secret: secret.into(),
1513 }
1614 }
17}
18
19pub async fn mirror_fetch(
20 _label: &str,
21 _dir: &std::path::Path,
22 _url: &str,
23 _credential: Option<&GitCredential>,
24 _timeout_secs: u64,
25) -> Result<(), GitError> {
26 Err(GitError::Unimplemented(
27 "mirror fetch is served by KenShell RemoteService",
28 ))
2915}
Modified lib/git/src/service/conflicts.rs lib/git/src/service/conflicts.rs +1 −48 View lib/git/src/service/conflicts.rs
@@ -1,4 +1,4 @@
1use crate::rpcgit::conflict::types::{ConflictFile, ListConflictOptions};
1use crate::rpcgit::conflict::types::ListConflictOptions;
22use serde::{Deserialize, Serialize};
33
44use crate::context::facade::GitContext;
@@ -81,35 +81,6 @@
8181 .await
8282 }
8383
84 pub async fn conflict_conflicted_files(
85 &self,
86 ctx: &GitContext,
87 base: String,
88 head: String,
89 ) -> Result<Vec<String>, GitError> {
90 let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?;
91 let key = format!(
92 "repo.{}.conflict_files.{}.{}",
93 ctx.model.id, our_oid, their_oid
94 );
95 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
96 let baby = self.baby(&ctx);
97 let files = baby
98 .list_conflict_files(
99 &our_oid,
100 &their_oid,
101 &ListConflictOptions {
102 allow_tree_conflicts: false,
103 skip_content: false,
104 },
105 )
106 .await
107 .map_err(map_baby)?;
108 Ok(conflict_paths(&files))
109 })
110 .await
111 }
112
11384 pub async fn conflict_detail(
11485 &self,
11586 ctx: &GitContext,
@@ -189,22 +160,4 @@
189160 })
190161 .await
191162 }
192}
193
194fn conflict_paths(files: &[ConflictFile]) -> Vec<String> {
195 files
196 .iter()
197 .filter_map(|f| {
198 f.header
199 .our_path
200 .clone()
201 .map(|p| p.to_string_lossy().into_owned())
202 .or_else(|| {
203 f.header
204 .their_path
205 .clone()
206 .map(|p| p.to_string_lossy().into_owned())
207 })
208 })
209 .collect()
210163}
Modified lib/git/src/service/hooks.rs lib/git/src/service/hooks.rs +0 −4 View lib/git/src/service/hooks.rs
@@ -59,10 +59,6 @@
5959 })
6060 }
6161
62 pub fn hook_supported(&self, kind: HookEventKind) -> bool {
63 matches!(kind, HookEventKind::PreReceive | HookEventKind::PostReceive)
64 }
65
6662 pub async fn hook_execute_script(
6763 &self,
6864 _ctx: &GitContext,
Modified lib/git/src/service/lfs.rs lib/git/src/service/lfs.rs +0 −67 View lib/git/src/service/lfs.rs
@@ -5,51 +5,6 @@
55use crate::share::errors::GitError;
66
77impl GitService {
8 pub async fn lfs_batch(
9 &self,
10 _ctx: &GitContext,
11 _req: LfsBatchRequest,
12 ) -> Result<LfsBatchResponse, GitError> {
13 Err(GitError::Unimplemented(
14 "LFS object transfer is served by KenShell",
15 ))
16 }
17
18 #[allow(clippy::too_many_arguments)]
19 pub async fn lfs_upload(
20 &self,
21 _space_name: String,
22 _repo_name: String,
23 _caller: i64,
24 _oid: String,
25 _size: i64,
26 _content_type: Option<String>,
27 _body: Vec<u8>,
28 ) -> Result<bool, GitError> {
29 Err(GitError::Unimplemented(
30 "LFS object upload is served by KenShell",
31 ))
32 }
33
34 #[allow(clippy::too_many_arguments)]
35 pub async fn lfs_upload_stream<R>(
36 &self,
37 _space_name: String,
38 _repo_name: String,
39 _caller: i64,
40 _oid: String,
41 _size: i64,
42 _content_type: Option<String>,
43 _reader: R,
44 ) -> Result<bool, GitError>
45 where
46 R: tokio::io::AsyncRead + Unpin + Send,
47 {
48 Err(GitError::Unimplemented(
49 "LFS object upload is served by KenShell",
50 ))
51 }
52
538 pub async fn lfs_download_to<W>(
549 &self,
5510 _space_name: String,
@@ -60,28 +15,6 @@
6015 where
6116 W: tokio::io::AsyncWrite + Send + Unpin,
6217 {
63 Err(GitError::Unimplemented(
64 "LFS object download is served by KenShell",
65 ))
66 }
67
68 pub async fn lfs_verify(
69 &self,
70 _space_name: String,
71 _repo_name: String,
72 _req: LfsVerifyRequest,
73 ) -> Result<(), GitError> {
74 Err(GitError::Unimplemented(
75 "LFS object verification is served by KenShell",
76 ))
77 }
78
79 pub async fn lfs_download(
80 &self,
81 _space_name: String,
82 _repo_name: String,
83 _oid: String,
84 ) -> Result<Vec<u8>, GitError> {
8518 Err(GitError::Unimplemented(
8619 "LFS object download is served by KenShell",
8720 ))
Modified lib/git/src/service/licenses.rs lib/git/src/service/licenses.rs +0 −7 View lib/git/src/service/licenses.rs
@@ -109,11 +109,4 @@
109109 };
110110 Ok(saved.into())
111111 }
112
113 pub async fn license_exists(&self, name: &str) -> Result<bool, GitError> {
114 let row = repo_license::Entity::find_by_id(name.to_string())
115 .one(self.db_read())
116 .await?;
117 Ok(row.is_some())
118 }
119112}
Modified lib/git/src/service/logs.rs lib/git/src/service/logs.rs +0 −48 View lib/git/src/service/logs.rs
@@ -81,52 +81,4 @@
8181 })
8282 .await
8383 }
84
85 pub async fn log_commits_between(
86 &self,
87 ctx: &GitContext,
88 after: String,
89 before: String,
90 limit: u64,
91 ) -> Result<LogListDto, GitError> {
92 let limit = limit.clamp(1, MAX_LOG_LIMIT);
93 let baby = self.baby(&ctx);
94 let after_oid = baby.rev_parse(&after).await.map_err(map_baby)?;
95 let before_oid = baby.rev_parse(&before).await.map_err(map_baby)?;
96
97 let key = format!(
98 "repo.{}.log_between.{}.{}:{}",
99 ctx.model.id, after_oid, before_oid, limit
100 );
101 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
102 let commits = baby
103 .list_commits(ListCommitsOptions {
104 revision: format!("{after_oid}..{before_oid}"),
105 max_count: Some(limit as u32),
106 ..Default::default()
107 })
108 .await
109 .map_err(map_baby)?;
110 Ok(LogListDto {
111 commits,
112 cursor: None,
113 has_more: false,
114 })
115 })
116 .await
117 }
118
119 pub async fn log_last_commit(
120 &self,
121 ctx: &GitContext,
122 branch: String,
123 ) -> Result<CommitInfo, GitError> {
124 let baby = self.baby(&ctx);
125 let rev_oid = baby.rev_parse(&branch).await.map_err(map_baby)?;
126 let key = format!("repo.{}.commit.{}", ctx.model.id, rev_oid);
127 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
128 baby.get_commit(&branch).await.map_err(map_baby)
129 })
130 .await
131 }
13284}
Modified lib/git/src/service/merge.rs lib/git/src/service/merge.rs +0 −62 View lib/git/src/service/merge.rs
@@ -2,7 +2,6 @@
22use models::pull_request::pull_requests::MergeMethod;
33use serde::{Deserialize, Serialize};
44use time::OffsetDateTime;
5use uuid::Uuid;
65
76use crate::context::facade::GitContext;
87use crate::service::GitService;
@@ -111,30 +110,6 @@
111110 .await
112111 }
113112
114 pub async fn recent_commits(
115 &self,
116 ctx: &GitContext,
117 revision: Option<&str>,
118 limit: u32,
119 skip: u32,
120 ) -> Result<Vec<MergeCommitDto>, GitError> {
121 let reference = match revision.map(str::trim).filter(|value| !value.is_empty()) {
122 Some(value) => value.to_string(),
123 None => format!("refs/heads/{}", ctx.model.default_branch),
124 };
125 let commits = self
126 .baby(&ctx)
127 .list_commits(crate::rpcgit::commit::types::ListCommitsOptions {
128 revision: reference,
129 max_count: Some(limit.min(MAX_LISTED_COMMITS).saturating_add(skip)),
130 skip: Some(skip),
131 ..Default::default()
132 })
133 .await
134 .map_err(map_baby)?;
135 Ok(commits.into_iter().map(commit_dto).collect())
136 }
137
138113 pub async fn commits_between(
139114 &self,
140115 ctx: &GitContext,
@@ -161,42 +136,5 @@
161136 ));
162137 }
163138 Ok(out)
164 }
165
166 pub async fn tag_target(&self, repo_id: Uuid, tag_name: &str) -> Option<String> {
167 use models::git::git_tags;
168 use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
169 git_tags::Entity::find()
170 .filter(git_tags::Column::RepoId.eq(repo_id))
171 .filter(git_tags::Column::Name.eq(tag_name))
172 .one(self.db_read())
173 .await
174 .ok()
175 .flatten()
176 .map(|row| row.commit_sha)
177 }
178
179 pub async fn default_branch_base(
180 &self,
181 ctx: &GitContext,
182 depth: u32,
183 ) -> Result<Option<String>, GitError> {
184 let baby = self.baby(&ctx);
185 let head = baby
186 .rev_parse(&format!("refs/heads/{}", ctx.model.default_branch))
187 .await
188 .map_err(map_baby)?;
189 let mut cursor = head;
190 for _ in 0..depth {
191 let info = match baby.get_commit(&hex(&cursor)).await {
192 Ok(info) => info,
193 Err(_) => return Ok(None),
194 };
195 match info.parents.first() {
196 Some(parent) => cursor = parent.clone(),
197 None => return Ok(None),
198 }
199 }
200 Ok(Some(hex(&cursor)))
201139 }
202140}
Modified lib/git/src/service/refs.rs lib/git/src/service/refs.rs +1 −13 View lib/git/src/service/refs.rs
@@ -1,4 +1,4 @@
1use crate::rpcgit::refs::types::{ListRefsOptions, RefInfo, RefType};
1use crate::rpcgit::refs::types::{ListRefsOptions, RefInfo};
22use serde::Serialize;
33
44use crate::context::facade::GitContext;
@@ -169,18 +169,6 @@
169169 .await
170170 .map_err(map_baby)?;
171171 Ok(refs.into_iter().any(|r| r.name == refname))
172 }
173
174 pub async fn refs_classify(&self, name: &str) -> RefType {
175 if name.starts_with("refs/heads/") {
176 RefType::Branch
177 } else if name.starts_with("refs/tags/") {
178 RefType::Tag
179 } else if name.starts_with("refs/remotes/") {
180 RefType::Remote
181 } else {
182 RefType::Other
183 }
184172 }
185173
186174 async fn ref_protected(
Modified lib/git/src/service/trees.rs lib/git/src/service/trees.rs +0 −20 View lib/git/src/service/trees.rs
@@ -168,26 +168,6 @@
168168 .await
169169 }
170170
171 pub async fn tree_entries_with_latest_html(
172 &self,
173 ctx: &std::sync::Arc<crate::context::facade::GitContext>,
174 revision: &str,
175 ) -> Result<Vec<crate::rpcgit::tree::types::TreeEntryWithLatest>, GitError> {
176 validate::validate_revision(revision)?;
177 if ctx.repo_uninitialized() {
178 return Ok(Vec::new());
179 }
180 let baby = self.baby(ctx);
181 let rev_oid = baby.rev_parse(revision).await.map_err(map_baby)?;
182 let key = format!("repo.{}.tree_entries_latest_html.{}", ctx.model.id, rev_oid);
183 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
184 baby.tree_entries_with_latest(revision, None)
185 .await
186 .map_err(map_baby)
187 })
188 .await
189 }
190
191171 pub async fn tree_with_commits_html(
192172 &self,
193173 ctx: &std::sync::Arc<crate::context::facade::GitContext>,
Modified lib/git/src/service/watches.rs lib/git/src/service/watches.rs +0 −30 View lib/git/src/service/watches.rs
@@ -95,36 +95,6 @@
9595 })
9696 }
9797
98 pub async fn unwatch(&self, ctx: &GitContext, user_id: i64) -> Result<(), GitError> {
99 let db = self.db_write();
100 let repo_id = ctx.model.id;
101 let now = OffsetDateTime::now_utc();
102 TransactionTrait::transaction(db, |txn| {
103 Box::pin(async move {
104 repo_watch::Entity::delete_many()
105 .filter(repo_watch::Column::UserId.eq(user_id))
106 .filter(repo_watch::Column::RepoId.eq(repo_id))
107 .exec(txn)
108 .await?;
109 Ok::<(), sea_orm::DbErr>(())
110 })
111 })
112 .await?;
113 let after = repo_watch::Entity::find()
114 .filter(repo_watch::Column::RepoId.eq(repo_id))
115 .all(db)
116 .await?
117 .len() as i32;
118 let am = repos::ActiveModel {
119 id: Unchanged(repo_id),
120 nums_watch: Set(after),
121 updated_at: Set(now),
122 ..Default::default()
123 };
124 am.update(db).await?;
125 Ok(())
126 }
127
12898 pub async fn get_watch_mode(
12999 &self,
130100 ctx: &GitContext,
Modified lib/git/src/share/policy.rs lib/git/src/share/policy.rs +1 −31 View lib/git/src/share/policy.rs
@@ -1,7 +1,6 @@
11use globset::{Glob, GlobMatcher};
22use models::repo::{
33 repo_branch_protection_policy, repo_branch_protection_whitelists, repo_tag_protection_policy,
4 repo_tag_protection_whitelists,
54};
65use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
76use uuid::Uuid;
@@ -70,18 +69,7 @@
7069 pub approval_allowed: Vec<i64>,
7170}
7271
73impl WhitelistSet {
74 pub fn push_actor_allowed(&self, subject: WhitelistSubject) -> bool {
75 match subject {
76 WhitelistSubject::User(uid) => self.push_allowed.contains(&uid),
77 WhitelistSubject::Group(_) => false,
78 }
79 }
80
81 pub fn tag_modifier_allowed(&self, uid: i64) -> bool {
82 self.merge_allowed.contains(&uid) || self.approval_allowed.contains(&uid)
83 }
84}
72impl WhitelistSet {}
8573
8674pub async fn load_branch_whitelists(
8775 db: &sea_orm::DatabaseConnection,
@@ -106,24 +94,6 @@
10694 out.approval_allowed.push(uid)
10795 }
10896 }
109 }
110 Ok(out)
111}
112
113pub async fn load_tag_whitelists(
114 db: &sea_orm::DatabaseConnection,
115 policy_ids: &[Uuid],
116) -> Result<WhitelistSet, GitError> {
117 if policy_ids.is_empty() {
118 return Ok(WhitelistSet::default());
119 }
120 let rows = repo_tag_protection_whitelists::Entity::find()
121 .filter(repo_tag_protection_whitelists::Column::PolicyId.is_in(policy_ids.iter().copied()))
122 .all(db)
123 .await?;
124 let mut out = WhitelistSet::default();
125 for row in rows {
126 out.merge_allowed.push(row.user_id);
12797 }
12898 Ok(out)
12999}
Modified lib/i18n/src/catalog.rs lib/i18n/src/catalog.rs +0 −238 View lib/i18n/src/catalog.rs
@@ -23,172 +23,6 @@
2323 }
2424}
2525
26/// The plain-text body of a verification mail.
27pub fn verify_text(locale: UiLocale, code: &str) -> String {
28 match locale {
29 UiLocale::ZhCn => format!("你的验证码是:{code}"),
30 UiLocale::En => format!("Your verification code is: {code}"),
31 }
32}
33
34/// Verification mail: body copy around the code.
35pub fn verify_body(locale: UiLocale, minutes: u64) -> String {
36 match locale {
37 UiLocale::ZhCn => format!("输入下列验证码以继续,{minutes} 分钟内有效。"),
38 UiLocale::En => {
39 format!("Enter the following code to continue. It expires in {minutes} minutes.")
40 }
41 }
42}
43
44pub fn verify_verify_button(locale: UiLocale) -> &'static str {
45 match locale {
46 UiLocale::ZhCn => "验证邮箱",
47 UiLocale::En => "Verify email",
48 }
49}
50
51pub fn link_fallback(locale: UiLocale) -> &'static str {
52 match locale {
53 UiLocale::ZhCn => "如果按钮无法点击,请把下面的链接复制到浏览器打开:",
54 UiLocale::En => "If the button doesn't work, copy this link into your browser:",
55 }
56}
57
58pub fn ignore_if_unrequested(locale: UiLocale) -> &'static str {
59 match locale {
60 UiLocale::ZhCn => "如果这不是你本人的操作,可以忽略这封邮件。",
61 UiLocale::En => "If you didn't request this, you can safely ignore this email.",
62 }
63}
64
65/// Password-reset mail.
66pub fn reset_subject(locale: UiLocale) -> &'static str {
67 match locale {
68 UiLocale::ZhCn => "重置你的密码",
69 UiLocale::En => "Reset your password",
70 }
71}
72
73pub fn reset_heading(locale: UiLocale) -> &'static str {
74 match locale {
75 UiLocale::ZhCn => "重置密码",
76 UiLocale::En => "Reset your password",
77 }
78}
79
80pub fn reset_body(locale: UiLocale) -> &'static str {
81 match locale {
82 UiLocale::ZhCn => "点击下方按钮重置密码,链接 30 分钟内有效。",
83 UiLocale::En => {
84 "Click the button below to reset your password. This link expires in 30 minutes."
85 }
86 }
87}
88
89pub fn reset_button(locale: UiLocale) -> &'static str {
90 match locale {
91 UiLocale::ZhCn => "重置密码",
92 UiLocale::En => "Reset password",
93 }
94}
95
96pub fn reset_text(locale: UiLocale, token: &str) -> String {
97 match locale {
98 UiLocale::ZhCn => format!("请在有效期内使用以下重置令牌:{token}"),
99 UiLocale::En => format!("Use this reset token before it expires: {token}"),
100 }
101}
102
103pub fn reset_ignore(locale: UiLocale) -> &'static str {
104 match locale {
105 UiLocale::ZhCn => "如果你没有申请重置密码,请忽略这封邮件。",
106 UiLocale::En => "If you didn't request a password reset, ignore this email.",
107 }
108}
109
110/// Email-change confirmation mail.
111pub fn email_change_subject(locale: UiLocale) -> &'static str {
112 match locale {
113 UiLocale::ZhCn => "确认你的新邮箱",
114 UiLocale::En => "Confirm your new email",
115 }
116}
117
118pub fn email_change_heading(locale: UiLocale) -> &'static str {
119 match locale {
120 UiLocale::ZhCn => "确认新邮箱",
121 UiLocale::En => "Confirm your new email",
122 }
123}
124
125pub fn email_change_body(locale: UiLocale, new_email: &str) -> String {
126 match locale {
127 UiLocale::ZhCn => format!("你申请把邮箱改为 {new_email},点击下方按钮完成确认。"),
128 UiLocale::En => format!(
129 "You requested to change your email to {new_email}. Click the button below to confirm."
130 ),
131 }
132}
133
134pub fn email_change_button(locale: UiLocale) -> &'static str {
135 match locale {
136 UiLocale::ZhCn => "确认更换邮箱",
137 UiLocale::En => "Confirm email change",
138 }
139}
140
141pub fn email_change_or_code(locale: UiLocale) -> &'static str {
142 match locale {
143 UiLocale::ZhCn => "或输入以下验证码:",
144 UiLocale::En => "Or enter this code:",
145 }
146}
147
148pub fn email_change_not_requested(locale: UiLocale) -> &'static str {
149 match locale {
150 UiLocale::ZhCn => "不是你本人操作?",
151 UiLocale::En => "Didn't request this?",
152 }
153}
154
155pub fn email_change_cancel(locale: UiLocale) -> &'static str {
156 match locale {
157 UiLocale::ZhCn => "撤销本次修改",
158 UiLocale::En => "Cancel the change",
159 }
160}
161
162pub fn email_change_text(locale: UiLocale, code: &str) -> String {
163 match locale {
164 UiLocale::ZhCn => format!("你的邮箱更换验证码是:{code}"),
165 UiLocale::En => format!("Your email change confirmation code is: {code}"),
166 }
167}
168
169/// Signup / add-email text mail.
170pub fn add_email_subject(locale: UiLocale) -> &'static str {
171 match locale {
172 UiLocale::ZhCn => "验证你的邮箱",
173 UiLocale::En => "Verify your email",
174 }
175}
176
177pub fn add_email_text(locale: UiLocale, code: &str) -> String {
178 match locale {
179 UiLocale::ZhCn => format!("你的邮箱验证码是:{code}"),
180 UiLocale::En => format!("Your email verification code is: {code}"),
181 }
182}
183
184/// Notification text.
185pub fn notification_mention_title(locale: UiLocale, room: &str) -> String {
186 match locale {
187 UiLocale::ZhCn => format!("你在 #{room} 中被提及"),
188 UiLocale::En => format!("You were mentioned in #{room}"),
189 }
190}
191
19226/// 私信与群组私信:OS 推送标题只说明"是谁给你发了消息",作者名由推送
19327/// worker 拼在前面;这两类不写站内收件箱,未读由聊天自己的未读计数承担。
19428pub fn notification_direct_message_title(locale: UiLocale) -> &'static str {
@@ -211,27 +45,6 @@
21145 }
21246}
21347
214pub fn notification_invite_title(locale: UiLocale, space: &str) -> String {
215 match locale {
216 UiLocale::ZhCn => format!("你被邀请加入 {space}"),
217 UiLocale::En => format!("You were invited to join {space}"),
218 }
219}
220
221pub fn notification_issue_assigned_title(locale: UiLocale, number: i32) -> String {
222 match locale {
223 UiLocale::ZhCn => format!("工单 #{number} 已指派给你"),
224 UiLocale::En => format!("Issue #{number} was assigned to you"),
225 }
226}
227
228pub fn notification_issue_comment_title(locale: UiLocale, number: i32) -> String {
229 match locale {
230 UiLocale::ZhCn => format!("工单 #{number} 有新评论"),
231 UiLocale::En => format!("New comment on issue #{number}"),
232 }
233}
234
23548/// Mail/notification subjects that mirror the issue and mention notices.
23649pub fn mention_email_subject(locale: UiLocale, room: &str) -> String {
23750 match locale {
@@ -254,13 +67,6 @@
25467 }
25568}
25669
257pub fn space_invite_subject(locale: UiLocale, space: &str) -> String {
258 match locale {
259 UiLocale::ZhCn => format!("邀请你加入 {space}"),
260 UiLocale::En => format!("You're invited to join {space}"),
261 }
262}
263
26470/// HTML result pages served directly by the backend.
26571pub fn email_verified_title(locale: UiLocale) -> &'static str {
26672 match locale {
@@ -394,49 +200,5 @@
394200 match locale {
395201 UiLocale::ZhCn => format!("工单 #{number} 指派给你:{title}"),
396202 UiLocale::En => format!("You were assigned to issue #{number}: {title}"),
397 }
398}
399
400/// Notification wording for the same three issue events, without the title
401/// (the inbox already renders it from `data`).
402pub fn notification_issue_mention_title(locale: UiLocale, number: i32) -> String {
403 match locale {
404 UiLocale::ZhCn => format!("你在工单 #{number} 中被提及"),
405 UiLocale::En => format!("You were mentioned in issue #{number}"),
406 }
407}
408
409/// Space invite mail.
410pub fn space_invite_body(locale: UiLocale, space: &str) -> String {
411 match locale {
412 UiLocale::ZhCn => format!("你被邀请加入 {space}。"),
413 UiLocale::En => format!("You have been invited to join {space}."),
414 }
415}
416
417pub fn space_invite_accept(locale: UiLocale) -> &'static str {
418 match locale {
419 UiLocale::ZhCn => "接受邀请",
420 UiLocale::En => "Accept invite",
421 }
422}
423
424pub fn space_invite_text(locale: UiLocale, space: &str, url: &str) -> String {
425 match locale {
426 UiLocale::ZhCn => format!(
427 "你被邀请加入 {space}\n\n{}:{url}\n",
428 space_invite_accept(locale)
429 ),
430 UiLocale::En => format!(
431 "You have been invited to join {space}\n\n{}: {url}\n",
432 space_invite_accept(locale)
433 ),
434 }
435}
436
437pub fn space_invite_or_copy(locale: UiLocale, url: &str) -> String {
438 match locale {
439 UiLocale::ZhCn => format!("或复制此链接:{url}"),
440 UiLocale::En => format!("Or copy this link: {url}"),
441203 }
442204}
Modified lib/i18n/src/lib.rs lib/i18n/src/lib.rs +0 −9 View lib/i18n/src/lib.rs
@@ -15,12 +15,3 @@
1515
1616pub use catalog::*;
1717pub use locale::{UiLocale, negotiate_accept_language, resolve_user_locale, resolve_user_locales};
18
19/// Normalize a stored user preference to a locale this build can render.
20///
21/// `users.language` is a free-form preference, so values such as `ja` or `auto`
22/// are normal: they are not errors, they simply do not select a UI catalogue and
23/// therefore lose their turn in the chain.
24pub fn from_user_preference(value: Option<&str>) -> Option<UiLocale> {
25 UiLocale::parse(value?)
26}
Modified lib/issues/src/services/issues.rs lib/issues/src/services/issues.rs +0 −9 View lib/issues/src/services/issues.rs
@@ -206,15 +206,6 @@
206206 ("urgent", IssuePriority::Urgent),
207207];
208208
209/// Inverse of {@link PRIORITY_KEYS}: the query string a caller should send.
210pub fn priority_query_key(priority: IssuePriority) -> &'static str {
211 PRIORITY_KEYS
212 .iter()
213 .find(|(_, value)| *value == priority)
214 .map(|(key, _)| *key)
215 .unwrap_or("none")
216}
217
218209#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
219210pub struct IssuePath {
220211 pub space_name: String,
Modified lib/issues/src/services/watches.rs lib/issues/src/services/watches.rs +0 −29 View lib/issues/src/services/watches.rs
@@ -34,35 +34,6 @@
3434}
3535
3636impl IssueService {
37 pub async fn get_watch(
38 &self,
39 caller: i64,
40 space_name: &str,
41 board_id: Uuid,
42 issue_id: Uuid,
43 ) -> Result<WatchDto, IssueError> {
44 lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
45
46 let w = issue_watch::Entity::find()
47 .filter(issue_watch::Column::IssueId.eq(Some(issue_id)))
48 .filter(issue_watch::Column::UserId.eq(caller))
49 .one(self.db().read())
50 .await?
51 .ok_or(IssueError::NotFound)?;
52
53 let user_map = self.load_user_briefs(&[w.user_id]).await?;
54 let user = user_map.get(&w.user_id).cloned();
55
56 Ok(WatchDto {
57 id: w.id,
58 issue_id: w.issue_id,
59 user,
60 mode: w.mode,
61 created_at: w.created_at,
62 updated_at: w.updated_at,
63 })
64 }
65
6637 #[tracing::instrument(name = "issues.upsert_watch", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))]
6738 pub async fn upsert_watch(
6839 &self,
Modified lib/models/src/space/mod.rs lib/models/src/space/mod.rs +0 −10 View lib/models/src/space/mod.rs
@@ -185,16 +185,6 @@
185185}
186186
187187impl AllowJoinLevel {
188 pub fn as_i16(self) -> i16 {
189 match self {
190 Self::None => 0,
191 Self::NeedMailActive => 1,
192 Self::NeedMailActiveAndLastOneMid => 2,
193 Self::NeedMailActiveAndLastOneHours => 3,
194 Self::NeedMailActiveAndPhoneActive => 4,
195 }
196 }
197
198188 pub fn from_i16(value: i16) -> Self {
199189 match value {
200190 1 => Self::NeedMailActive,
Modified lib/oltp/src/config.rs lib/oltp/src/config.rs +0 −57 View lib/oltp/src/config.rs
@@ -264,65 +264,8 @@
264264 config
265265 }
266266
267 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
268 self.service_version = Some(version.into());
269 self
270 }
271
272 pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
273 self.environment = Some(environment.into());
274 self
275 }
276
277 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
278 self.attributes.push((key.into(), value.into()));
279 self
280 }
281
282 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
283 self.headers.insert(key.into(), value.into());
284 self
285 }
286
287267 pub fn with_timeout(mut self, timeout: Duration) -> Self {
288268 self.timeout = timeout;
289 self
290 }
291
292 pub fn with_sample_ratio(mut self, ratio: f64) -> Self {
293 self.sample_ratio = ratio;
294 self
295 }
296
297 pub fn with_log_filter(mut self, filter: impl Into<String>) -> Self {
298 self.log_filter = filter.into();
299 self
300 }
301
302 pub fn with_fallback_log_filter(mut self, filter: impl Into<String>) -> Self {
303 if self.log_filter == DEFAULT_LOG_FILTER {
304 self.log_filter = filter.into();
305 }
306 self
307 }
308
309 pub fn with_stdout(mut self, stdout: bool) -> Self {
310 self.stdout = stdout;
311 self
312 }
313
314 pub fn with_traces(mut self, traces: SignalConfig) -> Self {
315 self.traces = traces;
316 self
317 }
318
319 pub fn with_logs(mut self, logs: SignalConfig) -> Self {
320 self.logs = logs;
321 self
322 }
323
324 pub fn with_metrics(mut self, metrics: MetricsConfig) -> Self {
325 self.metrics = metrics;
326269 self
327270 }
328271
Modified lib/oltp/src/metrics.rs lib/oltp/src/metrics.rs +0 −14 View lib/oltp/src/metrics.rs
@@ -157,13 +157,6 @@
157157 .build()
158158});
159159
160static GIT_OPS: LazyLock<Counter<u64>> = LazyLock::new(|| {
161 app_meter()
162 .u64_counter("git.ops")
163 .with_description("Git protocol operations by operation and outcome")
164 .build()
165});
166
167160static PUSH_DELIVERIES: LazyLock<Counter<u64>> = LazyLock::new(|| {
168161 app_meter()
169162 .u64_counter("push.deliveries")
@@ -194,13 +187,6 @@
194187
195188pub fn auth_login(outcome: &'static str) {
196189 AUTH_LOGINS.add(1, &[KeyValue::new("outcome", outcome)]);
197}
198
199pub fn git_op(op: &'static str, outcome: &'static str) {
200 GIT_OPS.add(
201 1,
202 &[KeyValue::new("op", op), KeyValue::new("outcome", outcome)],
203 );
204190}
205191
206192pub fn push_delivery(outcome: &'static str) {
Modified lib/proto/src/error.rs lib/proto/src/error.rs +0 −22 View lib/proto/src/error.rs
@@ -35,30 +35,8 @@
3535 self.code() == Some(Code::NotFound)
3636 }
3737
38 pub fn is_permission_denied(&self) -> bool {
39 self.code() == Some(Code::PermissionDenied)
40 }
41
42 pub fn is_invalid_argument(&self) -> bool {
43 self.code() == Some(Code::InvalidArgument)
44 }
45
4638 pub fn is_unauthenticated(&self) -> bool {
4739 self.code() == Some(Code::Unauthenticated)
48 }
49
50 pub fn is_conflict(&self) -> bool {
51 matches!(
52 self.code(),
53 Some(Code::Aborted) | Some(Code::AlreadyExists) | Some(Code::FailedPrecondition)
54 )
55 }
56
57 pub fn is_unavailable(&self) -> bool {
58 matches!(
59 self.code(),
60 Some(Code::Unavailable) | Some(Code::DeadlineExceeded)
61 )
6240 }
6341
6442 pub fn message(&self) -> String {
Modified lib/queue/src/envelope.rs lib/queue/src/envelope.rs +0 −10 View lib/queue/src/envelope.rs
@@ -143,16 +143,6 @@
143143 self
144144 }
145145
146 pub fn with_label(mut self, key: impl Into<String>, value: impl std::fmt::Display) -> Self {
147 self.labels.insert(key, value);
148 self
149 }
150
151 pub fn with_attempt(mut self, attempt: u32) -> Self {
152 self.attempt = attempt;
153 self
154 }
155
156146 pub fn message_id(&self) -> String {
157147 if self.delivery == DeliveryClass::TaskCommand {
158148 return self
Modified lib/queue/src/error.rs lib/queue/src/error.rs +0 −4 View lib/queue/src/error.rs
@@ -40,10 +40,6 @@
4040 matches!(self, Self::Config(_))
4141 }
4242
43 pub fn is_connect(&self) -> bool {
44 matches!(self, Self::Connect(_))
45 }
46
4743 pub fn is_retryable(&self) -> bool {
4844 matches!(self, Self::Connect(_) | Self::Publish(_))
4945 }
Modified lib/session/src/error.rs lib/session/src/error.rs +0 −4 View lib/session/src/error.rs
@@ -49,10 +49,6 @@
4949 Self::UpdateConflict { key: key.into() }
5050 }
5151
52 pub fn is_purged(&self) -> bool {
53 matches!(self, Self::Purged)
54 }
55
5652 pub fn is_retryable(&self) -> bool {
5753 matches!(self, Self::Store(source) if source.is_retryable())
5854 }
Modified lib/session/src/state.rs lib/session/src/state.rs +0 −46 View lib/session/src/state.rs
@@ -109,20 +109,6 @@
109109 self.commit_update(key, Some(&previous), value)
110110 }
111111
112 pub fn update_or_set<T, F>(&self, key: &str, default: T, update: F) -> Result<(), SessionError>
113 where
114 T: DeserializeOwned + Serialize,
115 F: FnOnce(T) -> T,
116 {
117 let previous = self.current_value(key)?;
118 let value = match previous.as_deref() {
119 Some(value) => update(deserialize_value(key, value)?),
120 None => default,
121 };
122 let value = serialize_value(key, &value)?;
123 self.commit_update(key, previous.as_deref(), value)
124 }
125
126112 pub fn remove(&self, key: &str) -> Option<String> {
127113 let mut inner = self.inner.borrow_mut();
128114 let value = inner.state.remove(key);
@@ -164,10 +150,6 @@
164150
165151 pub fn uid(&self) -> Option<i64> {
166152 self.user_id().ok().flatten()
167 }
168
169 pub fn is_authenticated(&self) -> bool {
170 self.uid().is_some()
171153 }
172154
173155 pub fn session_id(&self) -> Option<String> {
@@ -185,30 +167,10 @@
185167 self.set(SESSION_USER_ID, user_id)
186168 }
187169
188 pub fn set_auth_session_id(&self, session_id: impl Into<String>) -> Result<(), SessionError> {
189 self.set(SESSION_AUTH_SESSION_ID, session_id.into())
190 }
191
192 pub fn set_mfa_verified(&self, verified: bool) -> Result<(), SessionError> {
193 self.set(SESSION_MFA_VERIFIED, verified)
194 }
195
196 pub fn clear_user_id(&self) {
197 self.remove(SESSION_USER_ID);
198 }
199
200170 pub fn client_id(&self) -> Result<Option<String>, SessionError> {
201171 self.get(SESSION_CLIENT_ID)
202172 }
203173
204 pub fn set_client_id(&self, client_id: impl Into<String>) -> Result<(), SessionError> {
205 self.set(SESSION_CLIENT_ID, client_id.into())
206 }
207
208 pub fn clear_client_id(&self) {
209 self.remove(SESSION_CLIENT_ID);
210 }
211
212174 pub fn client_ip(&self) -> Result<Option<String>, SessionError> {
213175 self.get(SESSION_CLIENT_IP)
214176 }
@@ -217,20 +179,12 @@
217179 self.set(SESSION_CLIENT_IP, client_ip.into())
218180 }
219181
220 pub fn clear_client_ip(&self) {
221 self.remove(SESSION_CLIENT_IP);
222 }
223
224182 pub fn user_agent(&self) -> Result<Option<String>, SessionError> {
225183 self.get(SESSION_USER_AGENT)
226184 }
227185
228186 pub fn set_user_agent(&self, user_agent: impl Into<String>) -> Result<(), SessionError> {
229187 self.set(SESSION_USER_AGENT, user_agent.into())
230 }
231
232 pub fn clear_user_agent(&self) {
233 self.remove(SESSION_USER_AGENT);
234188 }
235189
236190 pub fn security_version(&self) -> Result<Option<i32>, SessionError> {
Modified lib/social/src/parse/mod.rs lib/social/src/parse/mod.rs +0 −30 View lib/social/src/parse/mod.rs
@@ -76,16 +76,6 @@
7676 }
7777 }
7878
79 /// 同 [`EmbedRule::frame`],但指定宽高比。
80 pub const fn frame_with(src: &'static str, aspect: Aspect) -> Self {
81 Self {
82 pattern: None,
83 src,
84 aspect,
85 height: None,
86 }
87 }
88
8979 /// 指定正则与宽高比。
9080 pub const fn capture(pattern: &'static str, src: &'static str, aspect: Aspect) -> Self {
9181 Self {
@@ -100,26 +90,6 @@
10090 pub const fn fixed_height(mut self, height: u32) -> Self {
10191 self.height = Some(height);
10292 self
103 }
104
105 /// 高宽比用 `aspect`,同时钉一个固定像素高。
106 pub const fn frame_fixed(src: &'static str, height: u32) -> Self {
107 Self {
108 pattern: None,
109 src,
110 aspect: Aspect::Auto,
111 height: Some(height),
112 }
113 }
114
115 /// 正则 + 固定像素高。
116 pub const fn capture_fixed(pattern: &'static str, src: &'static str, height: u32) -> Self {
117 Self {
118 pattern: Some(pattern),
119 src,
120 aspect: Aspect::Auto,
121 height: Some(height),
122 }
12393 }
12494
12595 /// 16:9 的正则规则。
Modified lib/space/src/services/dto.rs lib/space/src/services/dto.rs +0 −15 View lib/space/src/services/dto.rs
@@ -6,21 +6,6 @@
66
77pub const RESERVED_SPACE_NAMES: [&str; 1] = ["explore"];
88
9fn default_true() -> bool {
10 true
11}
12
13fn is_reserved_lower_name(name: &str) -> bool {
14 RESERVED_SPACE_NAMES.contains(&name)
15}
16
17fn is_valid_lower_name(name: &str) -> bool {
18 !name.is_empty()
19 && name
20 .chars()
21 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
22}
23
249pub use export::users::UserDto;
2510pub use export::{resolve_space_image_url, resolve_user_avatar_url};
2611
Modified lib/space/src/services/join.rs lib/space/src/services/join.rs +3 −100 View lib/space/src/services/join.rs
@@ -1,20 +1,18 @@
11use chrono::Utc;
2use sea_orm::sea_query::Expr;
32use sea_orm::{
43 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set, TransactionTrait,
4 QuerySelect, Set,
65};
76use uuid::Uuid;
87
98use crate::errors::{SpaceError, SpaceResult};
109use crate::services::service::SpaceService;
1110use crate::services::types::{
12 AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER,
11 AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER,
1312};
1413use crate::services::{member, service};
1514use models::space::{
16 SpaceJoinMethod, SpaceJoinRequestState, SpaceMemberRole, space_join_requests, space_join_rules,
17 space_members, space_roles,
15 SpaceJoinRequestState, SpaceMemberRole, space_join_requests, space_join_rules, space_roles,
1816};
1917use models::users::users;
2018
@@ -377,100 +375,5 @@
377375 request_id,
378376 )
379377 .await
380 }
381
382 #[tracing::instrument(skip_all, fields(organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
383 pub async fn ensure_bot_member(
384 &self,
385 organization_id: Uuid,
386 user_id: i64,
387 role_keys: Vec<String>,
388 role_ids: Vec<String>,
389 request_id: Option<&str>,
390 ) -> SpaceResult<(crate::services::types::MemberSummary, bool)> {
391 let txn = self.db_conn().write().begin().await?;
392 // 机器人成员的角色上限:OWNER 只能由人类走成员面转移,不能通过内部接口直接塞入。
393 let user = models::users::users::Entity::find_by_id(user_id)
394 .filter(models::users::users::Column::DeletedAt.is_null())
395 .one(&txn)
396 .await?
397 .ok_or(SpaceError::UserNotFound)?;
398 if !user.bot {
399 return Err(SpaceError::Forbidden);
400 }
401 let (row, created) = member::upsert_join(
402 &txn,
403 organization_id,
404 user_id,
405 SpaceMemberRole::Member,
406 SpaceJoinMethod::Direct,
407 None,
408 None,
409 )
410 .await?;
411 let mut roles = Vec::new();
412 for key in role_keys {
413 if let Some(role) = space_roles::Entity::find()
414 .filter(space_roles::Column::OrganizationId.eq(organization_id))
415 .filter(space_roles::Column::Key.eq(key))
416 .one(&txn)
417 .await?
418 {
419 if role.key == SYSTEM_ROLE_OWNER {
420 return Err(SpaceError::Forbidden);
421 }
422 roles.push(role);
423 }
424 }
425 for id in role_ids {
426 let Ok(id) = Uuid::parse_str(id.trim()) else {
427 continue;
428 };
429 if let Some(role) = space_roles::Entity::find_by_id(id)
430 .filter(space_roles::Column::OrganizationId.eq(organization_id))
431 .one(&txn)
432 .await?
433 {
434 if role.key == SYSTEM_ROLE_OWNER {
435 return Err(SpaceError::Forbidden);
436 }
437 roles.push(role);
438 }
439 }
440 for role in roles {
441 let existing = models::space::space_member_roles::Entity::find()
442 .filter(models::space::space_member_roles::Column::MemberId.eq(row.id))
443 .filter(models::space::space_member_roles::Column::RoleId.eq(role.id))
444 .one(&txn)
445 .await?;
446 if existing.is_none() {
447 models::space::space_member_roles::ActiveModel {
448 member_id: Set(row.id),
449 role_id: Set(role.id),
450 ..Default::default()
451 }
452 .insert(&txn)
453 .await?;
454 }
455 }
456 service::bump_version(&txn, organization_id).await?;
457 txn.commit().await?;
458 let summary = self.member_summary(&row).await?;
459 Ok((summary, created))
460 }
461
462 #[tracing::instrument(skip_all, fields(organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
463 pub async fn touch_member_activity(
464 &self,
465 organization_id: Uuid,
466 user_id: i64,
467 request_id: Option<&str>,
468 ) {
469 let _ = space_members::Entity::update_many()
470 .col_expr(space_members::Column::LastActiveAt, Expr::value(Utc::now()))
471 .filter(space_members::Column::OrganizationId.eq(organization_id))
472 .filter(space_members::Column::UserId.eq(user_id))
473 .exec(self.db_conn().write())
474 .await;
475378 }
476379}
Modified lib/space/src/services/service.rs lib/space/src/services/service.rs +0 −19 View lib/space/src/services/service.rs
@@ -7,8 +7,6 @@
77use db::DbConn;
88use events::{EventHandler, Labels, label};
99use export::{AppState, Database};
10use models::room;
11use models::room::room_members;
1210use models::users::users;
1311use sea_orm::sea_query::{Expr, ExprTrait};
1412use sea_orm::{
@@ -143,23 +141,6 @@
143141 Some(uid) => i18n::resolve_user_locale(self.db().read(), uid, fallback).await,
144142 None => fallback,
145143 }
146 }
147
148 pub(crate) async fn send_email(
149 &self,
150 to: &str,
151 subject: &str,
152 text: &str,
153 html: &str,
154 ) -> Result<(), SpaceError> {
155 let Some(client) = self.state.mail.as_ref() else {
156 tracing::warn!(to, "email rpc disabled; skipping space mail");
157 return Ok(());
158 };
159 client
160 .send_mail(to, subject, text, html, None)
161 .await
162 .map_err(|error| SpaceError::Internal(format!("email send failed: {error}")))
163144 }
164145}
165146
Modified lib/space/src/services/types.rs lib/space/src/services/types.rs +0 −5 View lib/space/src/services/types.rs
@@ -98,11 +98,6 @@
9898 PERMISSIONS.contains(&permission)
9999}
100100
101pub fn normalize_permission(permission: &str) -> Option<&'static str> {
102 let trimmed = permission.trim();
103 PERMISSIONS.iter().copied().find(|known| *known == trimmed)
104}
105
106101pub fn is_admin_permission(permission: &str) -> bool {
107102 ADMIN_PERMISSIONS.contains(&permission)
108103}
Modified lib/space/src/share/lookup.rs lib/space/src/share/lookup.rs +0 −13 View lib/space/src/share/lookup.rs
@@ -1,5 +1,4 @@
11use models::space::spaces;
2use uuid::Uuid;
32
43use crate::authority;
54use crate::errors::SpaceError;
@@ -27,16 +26,4 @@
2726 space_name: &str,
2827) -> Result<spaces::Model, SpaceError> {
2928 find_active_space_by_name(&service.state, caller, space_name).await
30}
31
32pub async fn lookup_space_by_id(
33 service: &SpaceService,
34 caller: i64,
35 space_id: Uuid,
36) -> Result<spaces::Model, SpaceError> {
37 let space = authority::space_by_id(&service.state, caller, space_id).await?;
38 if !authority::space_is_active(&space) {
39 return Err(SpaceError::NotFound);
40 }
41 Ok(space)
4229}
Modified lib/space/src/authority.rs lib/space/src/authority.rs +2 −101 View lib/space/src/authority.rs
@@ -2,16 +2,15 @@
22
33use export::AppState;
44use models::space::space_extras::{self, Entity as SpaceExtras};
5use models::space::space_member_extras::{self, Entity as MemberExtras};
65use models::space::space_members::{self, Entity as Members};
76use models::space::spaces::{self, Entity as Spaces};
87use models::space::{AllowJoinLevel, SpaceJoinPolicy, SpaceStatus};
9use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect, Set};
8use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set};
109use time::OffsetDateTime;
1110use uuid::Uuid;
1211
1312use crate::errors::SpaceError;
14use crate::services::dto::{MemberDto, SpaceDto, UserDto, resolve_space_image_url};
13use crate::services::dto::{SpaceDto, UserDto, resolve_space_image_url};
1514use crate::services::service::SpaceService;
1615
1716pub const RPC_KEY: &str = "kenoauth is not configured";
@@ -73,36 +72,6 @@
7372 space.status == SpaceStatus::Active && space.archived_at.is_none()
7473}
7574
76pub fn member_dto(
77 record: &space_members::Model,
78 extras: Option<&space_member_extras::Model>,
79 users: &HashMap<i64, UserDto>,
80) -> MemberDto {
81 MemberDto {
82 space_id: record.organization_id,
83 user: users.get(&record.user_id).cloned(),
84 alias_name: record.alias_name.clone(),
85 alias_avatar_url: record.alias_avatar_url.clone(),
86 status: record.status(),
87 last_active_at: to_time_opt(record.last_active_at),
88 left_at: to_time_opt(record.left_at),
89 invited_at: extras.and_then(|row| row.invited_at),
90 invited_by: record.invited_by.and_then(|uid| users.get(&uid).cloned()),
91 joined_at: to_time(record.joined_at),
92 created_at: extras
93 .map(|row| row.created_at)
94 .unwrap_or_else(|| to_time(record.joined_at)),
95 updated_at: extras.map(|row| row.updated_at),
96 message_notifications: record.message_notifications,
97 muted: record.muted,
98 mute_until: to_time_opt(record.mute_until),
99 mobile_push: record.mobile_push,
100 suppress_everyone: record.suppress_everyone,
101 suppress_roles: record.suppress_roles,
102 hide_muted_channels: record.hide_muted_channels,
103 }
104}
105
10675pub async fn extras(
10776 state: &AppState,
10877 space_id: Uuid,
@@ -112,16 +81,6 @@
11281 .await?)
11382}
11483
115pub async fn member_extras(
116 state: &AppState,
117 space_id: Uuid,
118 user_id: i64,
119) -> Result<Option<space_member_extras::Model>, SpaceError> {
120 Ok(MemberExtras::find_by_id((space_id, user_id))
121 .one(state.database.read())
122 .await?)
123}
124
12584pub async fn space_avatar(
12685 state: &AppState,
12786 space: &spaces::Model,
@@ -384,62 +343,4 @@
384343 }
385344 };
386345 Ok(row)
387}
388
389pub async fn upsert_member_extras(
390 state: &AppState,
391 space_id: Uuid,
392 user_id: i64,
393 invited_at: Option<OffsetDateTime>,
394) -> Result<(), SpaceError> {
395 let now = OffsetDateTime::now_utc();
396 let existing = member_extras(state, space_id, user_id).await?;
397 match existing {
398 Some(row) => {
399 let mut active: space_member_extras::ActiveModel = row.into();
400 if invited_at.is_some() {
401 active.invited_at = Set(invited_at);
402 }
403 active.updated_at = Set(now);
404 active.update(state.database.write()).await?;
405 }
406 None => {
407 space_member_extras::ActiveModel {
408 space_id: Set(space_id),
409 user_id: Set(user_id),
410 invited_at: Set(invited_at),
411 created_at: Set(now),
412 updated_at: Set(now),
413 }
414 .insert(state.database.write())
415 .await?;
416 }
417 }
418 Ok(())
419}
420
421pub async fn remove_extras(state: &AppState, space_id: Uuid) -> Result<(), SpaceError> {
422 SpaceExtras::delete_by_id(space_id)
423 .exec(state.database.write())
424 .await?;
425 MemberExtras::delete_many()
426 .filter(space_member_extras::Column::SpaceId.eq(space_id))
427 .exec(state.database.write())
428 .await?;
429 Ok(())
430}
431
432pub async fn remove_member_extras(
433 state: &AppState,
434 space_id: Uuid,
435 user_id: i64,
436) -> Result<(), SpaceError> {
437 MemberExtras::delete_by_id((space_id, user_id))
438 .exec(state.database.write())
439 .await?;
440 Ok(())
441}
442
443pub fn db_error(error: DbErr) -> SpaceError {
444 SpaceError::from(error)
445346}
Modified lib/storage/src/s3.rs lib/storage/src/s3.rs +2 −94 View lib/storage/src/s3.rs
@@ -1,7 +1,4 @@
1use std::collections::HashMap;
2use std::io;
31use std::pin::Pin;
4use std::task::{Context as TaskContext, Poll};
52
63use config::s3::S3Config;
74use futures::StreamExt as _;
@@ -11,7 +8,7 @@
118use s3::creds::Credentials;
129use s3::region::Region;
1310use s3::serde_types::{DateTime as S3DateTime, Object as S3Object};
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
11use tokio::io::{AsyncRead, AsyncReadExt};
1512
1613use crate::error::StorageError;
1714
@@ -220,28 +217,6 @@
220217 Ok(response.as_slice().to_vec())
221218 }
222219
223 /// Stream an object into `writer` without buffering it in memory; the
224 /// writer is expected to be Send + Unpin so HTTP body writers can be fed
225 /// directly. Returns the number of bytes written.
226 pub async fn get_to_writer<W: AsyncWrite + Send + Unpin>(
227 &self,
228 key: &str,
229 writer: &mut W,
230 ) -> Result<u64, StorageError> {
231 let mut counting = CountingWriter {
232 inner: writer,
233 count: 0,
234 };
235 let status = self.bucket.get_object_to_writer(key, &mut counting).await?;
236 if !(200..300).contains(&status) {
237 return Err(StorageError::UnexpectedStatus {
238 operation: "get_object",
239 status,
240 });
241 }
242 Ok(counting.count)
243 }
244
245220 /// Stream an object out as a byte stream (ready for an HTTP response
246221 /// body). The returned stream yields `Result<Bytes, StorageError>`.
247222 pub async fn get_stream(
@@ -301,10 +276,6 @@
301276 )))
302277 }
303278
304 pub async fn bucket_exists(&self) -> Result<bool, StorageError> {
305 Ok(self.bucket.exists().await?)
306 }
307
308279 pub async fn list_page(
309280 &self,
310281 prefix: &str,
@@ -366,18 +337,6 @@
366337 Ok(self.presign_bucket.presign_get(key, expiry_secs, None)?)
367338 }
368339
369 pub fn presign_get_with_queries(
370 &self,
371 key: &str,
372 expiry_secs: u32,
373 custom_queries: HashMap<String, String>,
374 ) -> Result<String, StorageError> {
375 check_expiry(expiry_secs)?;
376 Ok(self
377 .presign_bucket
378 .presign_get(key, expiry_secs, Some(custom_queries))?)
379 }
380
381340 pub fn presign_put(&self, key: &str, expiry_secs: u32) -> Result<String, StorageError> {
382341 check_expiry(expiry_secs)?;
383342 Ok(self.presign_bucket.presign_put(key, expiry_secs, None)?)
@@ -473,48 +432,13 @@
473432async fn abort_upload(bucket: &Bucket, path: &str, upload_id: &str) {
474433 if let Err(error) = bucket.abort_upload(path, upload_id).await {
475434 tracing::warn!(%error, "failed to abort multipart upload");
476 }
477}
478
479struct CountingWriter<'a, W> {
480 inner: &'a mut W,
481 count: u64,
482}
483
484impl<W: AsyncWrite + Unpin> AsyncWrite for CountingWriter<'_, W> {
485 fn poll_write(
486 mut self: Pin<&mut Self>,
487 cx: &mut TaskContext<'_>,
488 buf: &[u8],
489 ) -> Poll<io::Result<usize>> {
490 let this = &mut *self;
491 match Pin::new(&mut *this.inner).poll_write(cx, buf) {
492 Poll::Ready(Ok(n)) => {
493 this.count += n as u64;
494 Poll::Ready(Ok(n))
495 }
496 Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
497 Poll::Pending => Poll::Pending,
498 }
499 }
500
501 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
502 Pin::new(&mut *self.inner).poll_flush(cx)
503 }
504
505 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
506 Pin::new(&mut *self.inner).poll_shutdown(cx)
507 }
508
509 fn is_write_vectored(&self) -> bool {
510 self.inner.is_write_vectored()
511435 }
512436}
513437
514438#[cfg(test)]
515439mod tests {
516440 use super::*;
517 use tokio::io::AsyncWriteExt;
441 use std::io;
518442
519443 #[tokio::test]
520444 async fn read_chunk_respects_chunk_size() {
@@ -531,21 +455,5 @@
531455 let mut cursor = io::Cursor::new(&data);
532456 let first = read_chunk(&mut cursor).await.expect("read");
533457 assert_eq!(first.len(), CHUNK_SIZE - 1);
534 }
535
536 #[tokio::test]
537 async fn counting_writer_tallies_bytes() {
538 let mut buf = Vec::new();
539 {
540 let mut cw = CountingWriter {
541 inner: &mut buf,
542 count: 0,
543 };
544 cw.write_all(b"hello ").await.expect("write");
545 cw.write_all(b"world").await.expect("write");
546 cw.flush().await.expect("flush");
547 assert_eq!(cw.count, 11);
548 }
549 assert_eq!(buf, b"hello world");
550458 }
551459}