1use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::bucket::BucketName;
11
12#[derive(Debug, Clone, Serialize)]
13#[serde(rename = "VersioningConfiguration")]
14struct VersioningConfiguration {
15 #[serde(rename = "Status")]
16 status: String,
17}
18
19#[derive(Debug, Clone, Deserialize)]
20#[serde(rename = "VersioningConfiguration")]
21struct VersioningConfigurationResponse {
22 #[serde(rename = "Status")]
23 status: String,
24}
25
26pub struct PutBucketVersioningBuilder {
27 client: Arc<OSSClientInner>,
28 bucket: BucketName,
29 status: String,
30}
31
32impl PutBucketVersioningBuilder {
33 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, status: String) -> Self {
34 Self {
35 client,
36 bucket,
37 status,
38 }
39 }
40
41 pub async fn send(self) -> Result<PutBucketVersioningOutput> {
42 let endpoint = self.client.endpoint.clone();
43 let uri = format!("https://{}.{}?versioning", self.bucket.as_str(), endpoint);
44
45 let query_params: Vec<(String, String)> = vec![("versioning".into(), String::new())];
46
47 let config = VersioningConfiguration {
48 status: self.status,
49 };
50 let body_xml = crate::util::xml::to_xml(&config)?;
51
52 let request = HttpRequest::builder()
53 .method(http::Method::PUT)
54 .uri(&uri)
55 .body(bytes::Bytes::from(body_xml))
56 .build();
57
58 let response = self
59 .client
60 .send_signed(request, Some(&self.bucket), query_params)
61 .await
62 .map_err(|e| OssError {
63 kind: OssErrorKind::TransportError,
64 context: Box::new(ErrorContext {
65 operation: Some("PutBucketVersioning".into()),
66 bucket: Some(self.bucket.to_string()),
67 endpoint: Some(endpoint),
68 ..Default::default()
69 }),
70 source: Some(Box::new(e)),
71 })?;
72
73 if response.status().is_success() {
74 Ok(PutBucketVersioningOutput {
75 request_id: response
76 .headers
77 .get("x-oss-request-id")
78 .and_then(|v| v.to_str().ok())
79 .unwrap_or("")
80 .to_string(),
81 })
82 } else {
83 Err(OssError {
84 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
85 status_code: response.status().as_u16(),
86 code: String::new(),
87 message: String::new(),
88 request_id: String::new(),
89 host_id: String::new(),
90 resource: Some(self.bucket.to_string()),
91 string_to_sign: None,
92 })),
93 context: Box::new(ErrorContext {
94 operation: Some("PutBucketVersioning".into()),
95 bucket: Some(self.bucket.to_string()),
96 ..Default::default()
97 }),
98 source: None,
99 })
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
105pub struct PutBucketVersioningOutput {
106 pub request_id: String,
107}
108
109pub struct GetBucketVersioningBuilder {
110 client: Arc<OSSClientInner>,
111 bucket: BucketName,
112}
113
114impl GetBucketVersioningBuilder {
115 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
116 Self { client, bucket }
117 }
118
119 pub async fn send(self) -> Result<GetBucketVersioningOutput> {
120 let endpoint = self.client.endpoint.clone();
121 let uri = format!("https://{}.{}?versioning", self.bucket.as_str(), endpoint);
122
123 let query_params: Vec<(String, String)> = vec![("versioning".into(), String::new())];
124
125 let request = HttpRequest::builder()
126 .method(http::Method::GET)
127 .uri(&uri)
128 .build();
129
130 let response = self
131 .client
132 .send_signed(request, Some(&self.bucket), query_params)
133 .await
134 .map_err(|e| OssError {
135 kind: OssErrorKind::TransportError,
136 context: Box::new(ErrorContext {
137 operation: Some("GetBucketVersioning".into()),
138 bucket: Some(self.bucket.to_string()),
139 endpoint: Some(endpoint),
140 ..Default::default()
141 }),
142 source: Some(Box::new(e)),
143 })?;
144
145 if response.is_success() {
146 let body_str = response.body_as_str().unwrap_or("");
147 let config: VersioningConfigurationResponse = crate::util::xml::from_xml(body_str)
148 .map_err(|e| OssError {
149 kind: OssErrorKind::DeserializationError,
150 context: Box::new(ErrorContext {
151 operation: Some("GetBucketVersioning: parse XML".into()),
152 bucket: Some(self.bucket.to_string()),
153 ..Default::default()
154 }),
155 source: Some(Box::new(e)),
156 })?;
157
158 Ok(GetBucketVersioningOutput {
159 status: config.status,
160 })
161 } else {
162 Err(OssError {
163 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
164 status_code: response.status().as_u16(),
165 code: String::new(),
166 message: String::new(),
167 request_id: String::new(),
168 host_id: String::new(),
169 resource: Some(self.bucket.to_string()),
170 string_to_sign: None,
171 })),
172 context: Box::new(ErrorContext {
173 operation: Some("GetBucketVersioning".into()),
174 bucket: Some(self.bucket.to_string()),
175 ..Default::default()
176 }),
177 source: None,
178 })
179 }
180 }
181}
182
183#[derive(Debug, Clone)]
184pub struct GetBucketVersioningOutput {
185 pub status: String,
186}
187
188impl BucketOperations {
189 pub fn put_versioning(&self, status: impl Into<String>) -> PutBucketVersioningBuilder {
190 PutBucketVersioningBuilder::new(
191 self.client_inner().clone(),
192 self.bucket_name().clone(),
193 status.into(),
194 )
195 }
196
197 pub fn get_versioning(&self) -> GetBucketVersioningBuilder {
198 GetBucketVersioningBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use std::str::FromStr;
205 use std::sync::Mutex;
206
207 use crate::client::OSSClientInner;
208 use crate::config::credentials::Credentials;
209 use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
210 use crate::types::region::Region;
211
212 use super::*;
213
214 struct RecordingHttpClient {
215 requests: Arc<Mutex<Vec<HttpRequest>>>,
216 status_code: http::StatusCode,
217 response_body: bytes::Bytes,
218 }
219
220 #[async_trait::async_trait]
221 impl HttpClient for RecordingHttpClient {
222 async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
223 self.requests.lock().unwrap().push(request);
224 let mut headers = http::HeaderMap::new();
225 headers.insert(
226 "x-oss-request-id",
227 http::HeaderValue::from_static("rid-versioning"),
228 );
229 Ok(HttpResponse {
230 status: self.status_code,
231 headers,
232 body: self.response_body.clone(),
233 })
234 }
235 }
236
237 fn create_test_inner_with_body(
238 status: http::StatusCode,
239 body: bytes::Bytes,
240 ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
241 let requests = Arc::new(Mutex::new(Vec::new()));
242 let http = Arc::new(RecordingHttpClient {
243 requests: requests.clone(),
244 status_code: status,
245 response_body: body,
246 });
247 let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
248 Credentials::builder()
249 .access_key_id("test-ak")
250 .access_key_secret("test-sk")
251 .build()
252 .unwrap(),
253 ));
254 let inner = Arc::new(OSSClientInner {
255 http,
256 credentials,
257 signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
258 region: Region::CnHangzhou,
259 endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
260 });
261 (inner, requests)
262 }
263
264 #[test]
265 fn put_versioning_xml_enabled() {
266 let config = VersioningConfiguration {
267 status: "Enabled".into(),
268 };
269 let xml = crate::util::xml::to_xml(&config).unwrap();
270 assert!(xml.contains("<Status>Enabled</Status>"));
271 }
272
273 #[test]
274 fn put_versioning_xml_suspended() {
275 let config = VersioningConfiguration {
276 status: "Suspended".into(),
277 };
278 let xml = crate::util::xml::to_xml(&config).unwrap();
279 assert!(xml.contains("<Status>Suspended</Status>"));
280 }
281
282 #[tokio::test]
283 async fn put_versioning_sends_request() {
284 let (inner, requests) =
285 create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::new());
286 let builder = PutBucketVersioningBuilder::new(
287 inner,
288 BucketName::new("test-bucket").unwrap(),
289 "Enabled".into(),
290 );
291 builder.send().await.unwrap();
292 let captured = requests.lock().unwrap();
293 assert_eq!(captured[0].method, http::Method::PUT);
294 assert!(captured[0].uri.contains("?versioning"));
295 }
296
297 #[tokio::test]
298 async fn get_versioning_parses_response() {
299 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
300<VersioningConfiguration>
301 <Status>Enabled</Status>
302</VersioningConfiguration>"#;
303 let (inner, _) = create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::from(xml));
304 let builder =
305 GetBucketVersioningBuilder::new(inner, BucketName::new("test-bucket").unwrap());
306 let output = builder.send().await.unwrap();
307 assert_eq!(output.status, "Enabled");
308 }
309
310 #[tokio::test]
311 #[ignore = "requires valid OSS credentials"]
312 async fn e2e_bucket_versioning() {
313 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
314 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
315 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
316 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
317
318 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
319 endpoint: format!("oss-{}.aliyuncs.com", region_str),
320 region_id: region_str.clone(),
321 });
322
323 let client = crate::client::OSSClient::builder()
324 .region(region)
325 .credentials(ak, sk)
326 .build()
327 .unwrap();
328
329 let bucket = client.bucket(&bucket_str).unwrap();
330 bucket.put_versioning("Enabled").send().await.unwrap();
331 let output = bucket.get_versioning().send().await.unwrap();
332 assert!(!output.status.is_empty());
333 eprintln!("GetBucketVersioning: status={}", output.status);
334 }
335}