Skip to main content

u_sdk/esa/
site_management.rs

1use super::utils::{de_option_empty_string_as_none, parse_json_response};
2use super::{Client, Error, OPENAPI_STYLE, OPENAPI_VERSION};
3use bon::Builder;
4use serde::{Deserialize, Serialize, Serializer};
5use std::collections::HashMap;
6use time::OffsetDateTime;
7use u_sdk_common::helper::into_header_map;
8use u_sdk_common::open_api_sign::{SignParams, get_openapi_request_header};
9
10//region ListSites request
11/// [ListSites](https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites)
12#[serde_with::skip_serializing_none]
13#[derive(Builder, Serialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct ListSites<'a> {
16    #[builder(start_fn)]
17    #[serde(skip_serializing)]
18    pub(crate) client: &'a Client,
19
20    /// 标签过滤规则。 (Key, Value)
21    #[builder(field)]
22    #[serde(
23        skip_serializing_if = "Vec::is_empty",
24        serialize_with = "serialize_tag_filter_as_json_string"
25    )]
26    tag_filter: Vec<(Option<&'a str>, Option<&'a str>)>,
27
28    /// 站点名称。用于查询的过滤条件。
29    site_name: Option<&'a str>,
30
31    /// 站点名称的搜索匹配模式。默认为精确匹配。
32    site_search_type: Option<SiteSearchType>,
33
34    /// 页码。默认值:1。
35    page_number: Option<i32>,
36
37    /// 分页大小。默认值:500。
38    page_size: Option<i32>,
39
40    /// 资源组 ID。用于查询的过滤条件。
41    resource_group_id: Option<&'a str>,
42
43    /// 站点状态。用于查询的过滤条件。
44    status: Option<&'a str>,
45
46    /// 仅企业版,传 true 时代表仅查询企业版的站点。
47    only_enterprise: Option<bool>,
48
49    /// 套餐订阅类型。
50    plan_subscribe_type: Option<PlanSubscribeType>,
51
52    /// 加速区域。
53    coverage: Option<Coverage>,
54
55    /// 接入类型。
56    access_type: Option<AccessType>,
57
58    /// 排序字段,默认按照创建时间排序。
59    order_by: Option<OrderBy>,
60}
61
62impl<'a, S: list_sites_builder::State> ListSitesBuilder<'a, S> {
63    /// 添加标签过滤规则
64    pub fn tag_filter(mut self, item: (Option<&'a str>, Option<&'a str>)) -> Self {
65        self.tag_filter.push(item);
66        self
67    }
68
69    /// 批量添加标签过滤规则
70    pub fn tag_filters(
71        mut self,
72        items: impl IntoIterator<Item = (Option<&'a str>, Option<&'a str>)>,
73    ) -> Self {
74        self.tag_filter.extend(items);
75        self
76    }
77}
78
79#[serde_with::skip_serializing_none]
80#[derive(Serialize)]
81#[serde(rename_all = "PascalCase")]
82struct TagFilterItem<'a> {
83    key: Option<&'a str>,
84    value: Option<&'a str>,
85}
86
87pub fn serialize_tag_filter_as_json_string<'a, S>(
88    tag_filter: &Vec<(Option<&'a str>, Option<&'a str>)>,
89    serializer: S,
90) -> Result<S::Ok, S::Error>
91where
92    S: Serializer,
93{
94    // 过滤掉 (None, None),避免出现 "{}"
95    let items: Vec<TagFilterItem<'a>> = tag_filter
96        .iter()
97        .filter(|(k, v)| k.is_some() || v.is_some())
98        .map(|(k, v)| TagFilterItem { key: *k, value: *v })
99        .collect();
100
101    // 生成 JSON 字符串
102    let json = serde_json::to_string(&items).map_err(serde::ser::Error::custom)?;
103
104    // 按“字符串”序列化出去
105    serializer.serialize_str(&json)
106}
107
108/// 站点名称搜索匹配模式:prefix/suffix/exact/fuzzy
109#[derive(Serialize, Clone)]
110#[serde(rename_all = "lowercase")]
111pub enum SiteSearchType {
112    Prefix,
113    Suffix,
114    Exact,
115    Fuzzy,
116}
117
118/// 套餐订阅类型:basicplan/standardplan/advancedplan/enterpriseplan
119#[derive(Serialize, Clone)]
120pub enum PlanSubscribeType {
121    #[serde(rename = "basicplan")]
122    Basic,
123    #[serde(rename = "standardplan")]
124    Standard,
125    #[serde(rename = "advancedplan")]
126    Advanced,
127    #[serde(rename = "enterpriseplan")]
128    Enterprise,
129}
130
131/// 加速区域:domestic/global/overseas
132#[derive(Serialize, Clone, Debug, Deserialize)]
133#[serde(rename_all = "lowercase")]
134pub enum Coverage {
135    Domestic,
136    Global,
137    Overseas,
138}
139
140/// 接入类型:NS/CNAME(注意:API 值大小写敏感时建议按原样)
141#[derive(Serialize, Clone, Debug, Deserialize)]
142#[serde(rename_all = "UPPERCASE")]
143pub enum AccessType {
144    NS,
145    CName,
146}
147
148/// 排序字段:gmtCreate/visitTime
149#[derive(Serialize, Clone)]
150#[serde(rename_all = "camelCase")]
151pub enum OrderBy {
152    GmtCreate,
153    VisitTime,
154}
155//endregion
156
157//region ListSites response
158#[derive(Debug, Clone, Deserialize)]
159#[serde(rename_all = "PascalCase")]
160pub struct ListSitesResponse {
161    /// 请求 ID
162    pub request_id: String,
163
164    /// 返回数据的页码
165    pub page_number: i32,
166
167    /// 每页显示的站点个数
168    pub page_size: i32,
169
170    /// 总站点数量
171    pub total_count: i32,
172
173    /// 查询到的站点信息列表
174    pub sites: Vec<SiteInfo>,
175}
176
177#[derive(Debug, Clone, Deserialize)]
178#[serde(rename_all = "PascalCase")]
179pub struct SiteInfo {
180    /// 站点接入类型
181    pub access_type: AccessType,
182
183    /// 站点的 CNAME 后缀
184    pub cname_zone: String,
185
186    /// 站点加速区域
187    pub coverage: Coverage,
188
189    /// 站点创建时间(ISO8601, UTC)
190    #[serde(with = "time::serde::iso8601")]
191    pub create_time: OffsetDateTime,
192
193    /// 站点更新时间(ISO8601, UTC)
194    #[serde(with = "time::serde::iso8601")]
195    pub update_time: OffsetDateTime,
196
197    /// 站点绑定的套餐实例 ID
198    pub instance_id: String,
199
200    /// 站点分配的 NS 列表,逗号分隔
201    pub name_server_list: String,
202
203    /// 套餐名称
204    pub plan_name: String,
205
206    /// 套餐规格名称
207    pub plan_spec_name: Option<String>,
208
209    /// 资源组 ID
210    pub resource_group_id: String,
211
212    /// 站点 ID
213    pub site_id: i64,
214
215    /// 站点名称
216    pub site_name: String,
217
218    /// 站点状态
219    pub status: SiteStatus,
220
221    /// 站点标签(Key-Value 形式)
222    pub tags: Option<HashMap<String, String>>,
223
224    /// 站点归属校验码(CNAME 接入时使用)
225    pub verify_code: String,
226
227    /// 站点访问时间(ISO8601, UTC)
228    #[serde(with = "time::serde::iso8601")]
229    pub visit_time: OffsetDateTime,
230
231    /// 站点停用原因
232    #[serde(deserialize_with = "de_option_empty_string_as_none")]
233    pub offline_reason: Option<OfflineReason>,
234}
235
236#[derive(Debug, Clone, Deserialize)]
237#[serde(rename_all = "lowercase")]
238pub enum SiteStatus {
239    Pending,
240    Active,
241    Offline,
242    Moved,
243}
244
245#[derive(Debug, Clone, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum OfflineReason {
248    ExpirationArrears,
249    InternallyDisabled,
250    MissingIcp,
251    ContentViolation,
252    ProactivelyDisabled,
253}
254
255//endregion
256
257impl Client {
258    pub fn list_sites(&self) -> ListSitesBuilder<'_> {
259        ListSites::builder(self)
260    }
261}
262
263impl ListSites<'_> {
264    pub async fn send(&self) -> Result<ListSitesResponse, Error> {
265        let client = self.client;
266        let creds = client.credentials_provider.load().await?;
267
268        let sign_params = SignParams {
269            req_method: "GET",
270            host: &client.host,
271            query_map: self,
272            x_acs_action: "ListSites",
273            x_acs_version: OPENAPI_VERSION,
274            x_acs_security_token: creds.sts_security_token.as_deref(),
275            request_body: None,
276            style: &OPENAPI_STYLE,
277        };
278
279        let (common_headers, url_) =
280            get_openapi_request_header(&creds.access_key_secret, &creds.access_key_id, sign_params)
281                .map_err(|e| {
282                    Error::Common(format!("failed to get openapi request header: {}", e))
283                })?;
284        let header_map = into_header_map(common_headers);
285
286        let resp = client
287            .http_client
288            .get(url_)
289            .headers(header_map)
290            .send()
291            .await?;
292
293        let data = parse_json_response(resp).await?;
294        Ok(data)
295    }
296}