open_lark/service/im/v1/file/
mod.rs

1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::core::{
6    api_req::ApiRequest,
7    api_resp::{ApiResponseTrait, BaseResponse, ResponseFormat},
8    config::Config,
9    constants::AccessTokenType,
10    endpoints::EndpointBuilder,
11    error::LarkAPIError,
12    http::Transport,
13    req_option::RequestOption,
14    standard_response::StandardResponse,
15    trait_system::executable_builder::ExecutableBuilder,
16    validation::{validate_file_name, validate_upload_file, ValidateBuilder, ValidationResult},
17    SDKResult,
18};
19use crate::impl_full_service;
20use async_trait::async_trait;
21
22/// 文件服务
23pub struct FileService {
24    pub config: Config,
25}
26
27/// 上传文件响应
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CreateFileResponse {
30    /// 文件的key
31    pub file_key: String,
32}
33
34impl ApiResponseTrait for CreateFileResponse {
35    fn data_format() -> ResponseFormat {
36        ResponseFormat::Data
37    }
38}
39
40/// 下载文件响应
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct GetFileResponse {
43    /// 文件数据
44    pub data: Vec<u8>,
45}
46
47impl ApiResponseTrait for GetFileResponse {
48    fn data_format() -> ResponseFormat {
49        ResponseFormat::Data
50    }
51}
52
53impl FileService {
54    pub fn new(config: Config) -> Self {
55        Self { config }
56    }
57
58    /// 上传文件
59    pub async fn create(
60        &self,
61        file_type: &str,
62        file_name: &str,
63        file_data: Vec<u8>,
64        option: Option<RequestOption>,
65    ) -> SDKResult<CreateFileResponse> {
66        let mut query_params = HashMap::new();
67        query_params.insert("file_type", file_type.to_string());
68        query_params.insert("file_name", file_name.to_string());
69
70        let api_req = ApiRequest {
71            http_method: Method::POST,
72            api_path: crate::core::endpoints::im::IM_V1_FILES.to_string(),
73            supported_access_token_types: vec![AccessTokenType::Tenant, AccessTokenType::User],
74            query_params,
75            body: file_data,
76            ..Default::default()
77        };
78
79        let api_resp: BaseResponse<CreateFileResponse> =
80            Transport::request(api_req, &self.config, option).await?;
81        api_resp.into_result()
82    }
83
84    /// 下载文件
85    pub async fn get(
86        &self,
87        file_key: &str,
88        option: Option<RequestOption>,
89    ) -> SDKResult<GetFileResponse> {
90        let api_req = ApiRequest {
91            http_method: Method::GET,
92            api_path: EndpointBuilder::replace_param(
93                crate::core::endpoints::im::IM_V1_DOWNLOAD_FILE,
94                "file_key",
95                file_key,
96            ),
97            supported_access_token_types: vec![AccessTokenType::Tenant, AccessTokenType::User],
98            ..Default::default()
99        };
100
101        let api_resp: BaseResponse<GetFileResponse> =
102            Transport::request(api_req, &self.config, option).await?;
103        api_resp.into_result()
104    }
105
106    /// 创建文件上传Builder (推荐)
107    pub fn upload_builder(&self) -> FileUploadBuilder {
108        FileUploadBuilder::new()
109    }
110
111    /// 创建文件下载Builder (推荐)
112    pub fn download_builder(&self) -> FileDownloadBuilder {
113        FileDownloadBuilder::new()
114    }
115}
116
117// 接入统一 Service 抽象(IM v1 - FileService)
118impl_full_service!(FileService, "im.file", "v1");
119
120/// 文件上传请求结构
121#[derive(Debug, Clone, Default)]
122pub struct FileUploadRequest {
123    /// 文件类型
124    pub file_type: String,
125    /// 文件名
126    pub file_name: String,
127    /// 文件数据
128    pub file_data: Vec<u8>,
129}
130
131/// 文件上传Builder
132#[derive(Default)]
133pub struct FileUploadBuilder {
134    request: FileUploadRequest,
135}
136
137impl FileUploadBuilder {
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// 设置文件类型
143    pub fn file_type(mut self, file_type: impl ToString) -> Self {
144        self.request.file_type = file_type.to_string();
145        self
146    }
147
148    /// 设置文件名
149    pub fn file_name(mut self, file_name: impl ToString) -> Self {
150        self.request.file_name = file_name.to_string();
151        self
152    }
153
154    /// 设置文件数据
155    pub fn file_data(mut self, file_data: Vec<u8>) -> Self {
156        self.request.file_data = file_data;
157        self
158    }
159
160    /// 构建文件上传请求
161    pub fn build(self) -> SDKResult<FileUploadRequest> {
162        // 验证文件类型
163        if self.request.file_type.is_empty() {
164            return Err(LarkAPIError::illegal_param(
165                "file_type is required".to_string(),
166            ));
167        }
168
169        // 验证文件名
170        let (cleaned_name, name_result) = validate_file_name(&self.request.file_name);
171        if !name_result.is_valid() {
172            return Err(LarkAPIError::illegal_param(format!(
173                "Invalid file_name: {}",
174                name_result.error().unwrap_or("unknown error")
175            )));
176        }
177
178        // 验证文件数据
179        if self.request.file_data.is_empty() {
180            return Err(LarkAPIError::illegal_param(
181                "file_data cannot be empty".to_string(),
182            ));
183        }
184
185        // 验证上传文件(IM上传有更小的限制)
186        let upload_result = validate_upload_file(&self.request.file_data, &cleaned_name, true);
187        if !upload_result.is_valid() {
188            return Err(LarkAPIError::illegal_param(format!(
189                "File validation failed: {}",
190                upload_result.error().unwrap_or("unknown error")
191            )));
192        }
193
194        Ok(FileUploadRequest {
195            file_type: self.request.file_type,
196            file_name: cleaned_name,
197            file_data: self.request.file_data,
198        })
199    }
200
201    /// 构建文件上传请求(无验证,用于向后兼容)
202    pub fn build_unvalidated(self) -> FileUploadRequest {
203        self.request
204    }
205}
206
207impl ValidateBuilder for FileUploadBuilder {
208    fn validate(&self) -> ValidationResult {
209        // 验证文件类型
210        if self.request.file_type.is_empty() {
211            return ValidationResult::Invalid("file_type is required".to_string());
212        }
213
214        // 验证文件名
215        let (_, name_result) = validate_file_name(&self.request.file_name);
216        if !name_result.is_valid() {
217            return name_result;
218        }
219
220        // 验证文件数据
221        if self.request.file_data.is_empty() {
222            return ValidationResult::Invalid("file_data cannot be empty".to_string());
223        }
224
225        // 验证上传文件
226        validate_upload_file(&self.request.file_data, &self.request.file_name, true)
227    }
228}
229
230#[async_trait]
231impl ExecutableBuilder<FileService, FileUploadRequest, CreateFileResponse> for FileUploadBuilder {
232    fn build(self) -> FileUploadRequest {
233        // Legacy build method - create request without validation for backward compatibility
234        self.build_unvalidated()
235    }
236
237    async fn execute(self, service: &FileService) -> SDKResult<CreateFileResponse> {
238        let request = self.build_unvalidated();
239        service
240            .create(
241                &request.file_type,
242                &request.file_name,
243                request.file_data,
244                None,
245            )
246            .await
247    }
248
249    async fn execute_with_options(
250        self,
251        service: &FileService,
252        option: RequestOption,
253    ) -> SDKResult<CreateFileResponse> {
254        let request = self.build_unvalidated();
255        service
256            .create(
257                &request.file_type,
258                &request.file_name,
259                request.file_data,
260                Some(option),
261            )
262            .await
263    }
264}
265
266/// 文件下载Builder
267#[derive(Default)]
268pub struct FileDownloadBuilder {
269    file_key: Option<String>,
270}
271
272impl FileDownloadBuilder {
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// 设置文件key
278    pub fn file_key(mut self, file_key: impl ToString) -> Self {
279        self.file_key = Some(file_key.to_string());
280        self
281    }
282
283    pub fn build(self) -> String {
284        self.file_key.unwrap_or_default()
285    }
286}
287
288#[async_trait]
289impl ExecutableBuilder<FileService, String, GetFileResponse> for FileDownloadBuilder {
290    fn build(self) -> String {
291        self.build()
292    }
293
294    async fn execute(self, service: &FileService) -> SDKResult<GetFileResponse> {
295        let file_key = self.build();
296        service.get(&file_key, None).await
297    }
298
299    async fn execute_with_options(
300        self,
301        service: &FileService,
302        option: RequestOption,
303    ) -> SDKResult<GetFileResponse> {
304        let file_key = self.build();
305        service.get(&file_key, Some(option)).await
306    }
307}
308
309#[cfg(test)]
310#[allow(unused_variables, unused_unsafe)]
311mod tests {
312    use super::*;
313    use crate::core::config::Config;
314
315    fn create_test_config() -> Config {
316        Config::default()
317    }
318
319    #[test]
320    fn test_file_service_creation() {
321        let config = create_test_config();
322        let service = FileService::new(config.clone());
323
324        assert_eq!(service.config.app_id, config.app_id);
325        assert_eq!(service.config.app_secret, config.app_secret);
326    }
327
328    #[test]
329    fn test_file_service_with_custom_config() {
330        let config = Config::builder()
331            .app_id("file_app")
332            .app_secret("file_secret")
333            .req_timeout(std::time::Duration::from_millis(15000))
334            .base_url("https://file.api.com")
335            .build();
336
337        let service = FileService::new(config.clone());
338
339        assert_eq!(service.config.app_id, "file_app");
340        assert_eq!(service.config.app_secret, "file_secret");
341        assert_eq!(service.config.base_url, "https://file.api.com");
342        assert_eq!(
343            service.config.req_timeout,
344            Some(std::time::Duration::from_millis(15000))
345        );
346    }
347
348    #[test]
349    fn test_file_service_config_independence() {
350        let config1 = Config::builder()
351            .app_id("file1")
352            .app_secret("secret1")
353            .build();
354        let config2 = Config::builder()
355            .app_id("file2")
356            .app_secret("secret2")
357            .build();
358
359        let service1 = FileService::new(config1);
360        let service2 = FileService::new(config2);
361
362        assert_eq!(service1.config.app_id, "file1");
363        assert_eq!(service2.config.app_id, "file2");
364        assert_ne!(service1.config.app_id, service2.config.app_id);
365    }
366
367    #[test]
368    fn test_file_service_memory_layout() {
369        let config = create_test_config();
370        let service = FileService::new(config);
371
372        let service_ptr = std::ptr::addr_of!(service) as *const u8;
373        let config_ptr = std::ptr::addr_of!(service.config) as *const u8;
374
375        assert!(
376            !service_ptr.is_null(),
377            "Service should have valid memory address"
378        );
379        assert!(
380            !config_ptr.is_null(),
381            "Config should have valid memory address"
382        );
383    }
384
385    #[test]
386    fn test_file_service_with_different_configurations() {
387        let test_configs = vec![
388            Config::builder()
389                .app_id("file_basic")
390                .app_secret("basic_secret")
391                .build(),
392            Config::builder()
393                .app_id("file_timeout")
394                .app_secret("timeout_secret")
395                .req_timeout(std::time::Duration::from_millis(12000))
396                .build(),
397            Config::builder()
398                .app_id("file_custom")
399                .app_secret("custom_secret")
400                .base_url("https://custom.file.com")
401                .build(),
402            Config::builder()
403                .app_id("file_full")
404                .app_secret("full_secret")
405                .req_timeout(std::time::Duration::from_millis(20000))
406                .base_url("https://full.file.com")
407                .enable_token_cache(false)
408                .build(),
409        ];
410
411        for config in test_configs {
412            let service = FileService::new(config.clone());
413
414            assert_eq!(service.config.app_id, config.app_id);
415            assert_eq!(service.config.app_secret, config.app_secret);
416            assert_eq!(service.config.base_url, config.base_url);
417            assert_eq!(service.config.req_timeout, config.req_timeout);
418        }
419    }
420
421    #[test]
422    fn test_file_service_multiple_instances() {
423        let config = create_test_config();
424        let service1 = FileService::new(config.clone());
425        let service2 = FileService::new(config.clone());
426
427        assert_eq!(service1.config.app_id, service2.config.app_id);
428        assert_eq!(service1.config.app_secret, service2.config.app_secret);
429
430        let ptr1 = std::ptr::addr_of!(service1) as *const u8;
431        let ptr2 = std::ptr::addr_of!(service2) as *const u8;
432        assert_ne!(ptr1, ptr2, "Services should be independent instances");
433    }
434
435    #[test]
436    fn test_file_service_config_cloning() {
437        let original_config = create_test_config();
438        let cloned_config = original_config.clone();
439
440        let service = FileService::new(cloned_config);
441
442        assert_eq!(service.config.app_id, original_config.app_id);
443        assert_eq!(service.config.app_secret, original_config.app_secret);
444    }
445
446    #[test]
447    fn test_file_service_with_empty_config() {
448        let config = Config::default();
449        let service = FileService::new(config);
450
451        assert_eq!(service.config.app_id, "");
452        assert_eq!(service.config.app_secret, "");
453    }
454
455    #[test]
456    fn test_file_service_with_unicode_config() {
457        let config = Config::builder()
458            .app_id("文件应用")
459            .app_secret("文件密钥")
460            .base_url("https://文件.com")
461            .build();
462        let service = FileService::new(config);
463
464        assert_eq!(service.config.app_id, "文件应用");
465        assert_eq!(service.config.app_secret, "文件密钥");
466        assert_eq!(service.config.base_url, "https://文件.com");
467    }
468
469    #[test]
470    fn test_file_service_with_extreme_timeout() {
471        let config = Config::builder()
472            .app_id("file_extreme")
473            .app_secret("extreme_secret")
474            .req_timeout(std::time::Duration::from_secs(7200))
475            .build();
476        let service = FileService::new(config);
477
478        assert_eq!(
479            service.config.req_timeout,
480            Some(std::time::Duration::from_secs(7200))
481        );
482    }
483
484    #[test]
485    fn test_file_service_builder_methods() {
486        let config = create_test_config();
487        let service = FileService::new(config);
488
489        let upload_builder = service.upload_builder();
490        let download_builder = service.download_builder();
491
492        // Builders should be created successfully
493        let upload_ptr = std::ptr::addr_of!(upload_builder) as *const u8;
494        let download_ptr = std::ptr::addr_of!(download_builder) as *const u8;
495        assert!(!upload_ptr.is_null());
496        assert!(!download_ptr.is_null());
497    }
498
499    #[test]
500    fn test_file_upload_builder_basic() {
501        let builder = FileUploadBuilder::new()
502            .file_type("image")
503            .file_name("test.jpg")
504            .file_data(vec![1, 2, 3, 4]);
505
506        let request = builder.build_unvalidated();
507        assert_eq!(request.file_type, "image");
508        assert_eq!(request.file_name, "test.jpg");
509        assert_eq!(request.file_data, vec![1, 2, 3, 4]);
510    }
511
512    #[test]
513    fn test_file_upload_builder_chaining() {
514        let request = FileUploadBuilder::new()
515            .file_type("document")
516            .file_name("document.pdf")
517            .file_data(vec![0xFF, 0xFE, 0xFD])
518            .build_unvalidated();
519
520        assert_eq!(request.file_type, "document");
521        assert_eq!(request.file_name, "document.pdf");
522        assert_eq!(request.file_data, vec![0xFF, 0xFE, 0xFD]);
523    }
524
525    #[test]
526    fn test_file_download_builder_basic() {
527        let builder = FileDownloadBuilder::new().file_key("test_key_123");
528
529        let file_key = builder.build();
530        assert_eq!(file_key, "test_key_123");
531    }
532
533    #[test]
534    fn test_file_download_builder_empty() {
535        let builder = FileDownloadBuilder::new();
536        let file_key = builder.build();
537        assert_eq!(file_key, "");
538    }
539
540    #[test]
541    fn test_file_upload_request_default() {
542        let request = FileUploadRequest::default();
543        assert_eq!(request.file_type, "");
544        assert_eq!(request.file_name, "");
545        assert_eq!(request.file_data, Vec::<u8>::new());
546    }
547
548    #[test]
549    fn test_create_file_response_format() {
550        assert_eq!(CreateFileResponse::data_format(), ResponseFormat::Data);
551    }
552
553    #[test]
554    fn test_get_file_response_format() {
555        assert_eq!(GetFileResponse::data_format(), ResponseFormat::Data);
556    }
557}