Skip to main content

baidu_netdisk_sdk/file/
query.rs

1//! File query module
2//!
3//! Provides file and folder query functionality (list, get info, get metadata)
4use log::{debug, info};
5use serde::{Deserialize, Serialize};
6use std::future::Future;
7
8use super::FileClient;
9use crate::errors::{NetDiskError, NetDiskResult};
10
11/// Extension trait for file query operations
12pub(crate) trait FileQueryExt {
13    /// List directory contents with default options
14    ///
15    /// # Examples
16    ///
17    /// ```
18    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
19    ///
20    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21    /// let client = BaiduNetDiskClient::builder().build()?;
22    /// client.load_token_from_env()?;
23    ///
24    /// let files = client.file().list_directory("/").await?;
25    /// println!("Found {} items", files.len());
26    /// # Ok(())
27    /// # }
28    /// ```
29    fn list_directory(
30        &self,
31        dir: &str,
32    ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
33
34    /// List directory contents with custom options
35    ///
36    /// This is a lower-level method for advanced use cases.
37    /// Most users should use `list_directory()` instead.
38    fn list_directory_with_options(
39        &self,
40        dir: &str,
41        options: ListOptions,
42    ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
43
44    /// List all files recursively with pagination
45    ///
46    /// This method defaults to recursive mode (recursion=1) and provides simple pagination.
47    /// For advanced options, use `list_all_with_options()` instead.
48    ///
49    /// # Arguments
50    ///
51    /// * `path` - Directory path to list
52    /// * `start` - Start offset for pagination. For first page use 0, for subsequent pages use cursor from previous response
53    /// * `limit` - Number of items per page (default: 1000, max: 1000)
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
59    ///
60    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
61    /// let client = BaiduNetDiskClient::builder().build()?;
62    /// client.load_token_from_env()?;
63    ///
64    /// // First page
65    /// let result = client.file().list_all("/", 0, 100).await?;
66    /// println!("Found {} items", result.list.len());
67    ///
68    /// // Next page using cursor as start
69    /// if result.has_more && result.cursor.is_some() {
70    ///     let next_result = client.file().list_all("/", result.cursor.unwrap() as i32, 100).await?;
71    ///     println!("Next page: {} items", next_result.list.len());
72    /// }
73    /// # Ok(())
74    /// # }
75    /// ```
76    fn list_all(
77        &self,
78        path: &str,
79        start: i32,
80        limit: i32,
81    ) -> impl Future<Output = NetDiskResult<ListAllResult>> + Send;
82
83    /// List all files recursively with custom options
84    ///
85    /// This is a lower-level method for advanced use cases.
86    /// Most users should use `list_all()` instead.
87    fn list_all_with_options(
88        &self,
89        path: &str,
90        options: ListAllOptions,
91    ) -> impl Future<Output = NetDiskResult<ListAllResult>> + Send;
92
93    /// Get file or folder information by path
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
99    ///
100    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
101    /// let client = BaiduNetDiskClient::builder().build()?;
102    /// client.load_token_from_env()?;
103    ///
104    /// let file_info = client.file().get_file_info("/myfile.txt").await?;
105    /// println!("File size: {:?} bytes", file_info.size);
106    /// # Ok(())
107    /// # }
108    /// ```
109    fn get_file_info(&self, path: &str) -> impl Future<Output = NetDiskResult<FileInfo>> + Send;
110
111    /// Get file metadata (including download link) by fs_id
112    ///
113    /// This method is specifically used for getting download links.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
119    ///
120    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
121    /// let client = BaiduNetDiskClient::builder().build()?;
122    /// client.load_token_from_env()?;
123    ///
124    /// let fs_id = 123456;
125    /// let meta = client.file().get_file_meta(fs_id).await?;
126    /// if let Some(dlink) = meta.dlink {
127    ///     println!("Download link: {}", dlink);
128    /// }
129    /// # Ok(())
130    /// # }
131    /// ```
132    fn get_file_meta(&self, fs_id: u64) -> impl Future<Output = NetDiskResult<FileMeta>> + Send;
133
134    /// Search files by keyword
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
140    ///
141    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
142    /// let client = BaiduNetDiskClient::builder().build()?;
143    /// client.load_token_from_env()?;
144    ///
145    /// let (files, has_more) = client.file().search_files("document", "/").await?;
146    /// println!("Found {} items", files.len());
147    /// # Ok(())
148    /// # }
149    /// ```
150    fn search_files(
151        &self,
152        key: &str,
153        dir: &str,
154    ) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
155
156    /// Search files by keyword with custom options
157    ///
158    /// This is a lower-level method for advanced use cases.
159    /// Most users should use `search_files()` instead.
160    fn search_files_with_options(
161        &self,
162        key: &str,
163        options: SearchOptions,
164    ) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
165
166    /// Semantic search for files (AI-powered search)
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
172    ///
173    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
174    /// let client = BaiduNetDiskClient::builder().build()?;
175    /// client.load_token_from_env()?;
176    ///
177    /// let files = client.file().semantic_search("photos from 2024").await?;
178    /// println!("Found {} items", files.len());
179    /// # Ok(())
180    /// # }
181    /// ```
182    fn semantic_search(
183        &self,
184        query: &str,
185    ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
186
187    /// Semantic search with custom options
188    ///
189    /// This is a lower-level method for advanced use cases.
190    /// Most users should use `semantic_search()` instead.
191    fn semantic_search_with_options(
192        &self,
193        query: &str,
194        options: SemanticSearchOptions,
195    ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
196}
197
198impl FileQueryExt for FileClient {
199    async fn list_directory(&self, dir: &str) -> NetDiskResult<Vec<FileInfo>> {
200        self.list_directory_with_options(dir, ListOptions::default())
201            .await
202    }
203
204    async fn list_directory_with_options(
205        &self,
206        dir: &str,
207        options: ListOptions,
208    ) -> NetDiskResult<Vec<FileInfo>> {
209        let token = self.token_getter.get_token().await?;
210        let params = [
211            ("method", "list"),
212            ("dir", dir),
213            ("order", &options.order),
214            ("desc", &options.desc.to_string()),
215            ("start", &options.start.to_string()),
216            ("limit", &options.limit.to_string()),
217            ("web", &options.web.to_string()),
218            ("folder", &options.folder.to_string()),
219            ("showempty", &options.showempty.to_string()),
220            ("access_token", &token.access_token),
221        ];
222
223        debug!("Listing directory: {} with options: {:?}", dir, options);
224
225        let response: ListResponse = self
226            .http_client()
227            .get("/rest/2.0/xpan/file", Some(&params))
228            .await?;
229
230        if response.errno != 0 {
231            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
232            return Err(NetDiskError::api_error(response.errno, errmsg));
233        }
234
235        let list = response.list.unwrap_or_default();
236        info!("Listed {} items in directory: {}", list.len(), dir);
237
238        Ok(list
239            .into_iter()
240            .map(|item| FileInfo {
241                fs_id: item.fs_id,
242                path: item.path,
243                size: item.size,
244                ctime: item.ctime,
245                mtime: item.mtime,
246                isdir: item.isdir,
247                name: item.server_filename,
248                md5: item.md5,
249                category: item.category,
250                oper_id: item.oper_id,
251                owner_id: item.owner_id,
252                owner_type: item.owner_type,
253                server_atime: item.server_atime,
254                server_ctime: item.server_ctime,
255                server_mtime: item.server_mtime,
256            })
257            .collect())
258    }
259
260    async fn list_all(&self, path: &str, start: i32, limit: i32) -> NetDiskResult<ListAllResult> {
261        let options = ListAllOptions::new()
262            .recursion(true)
263            .start(start)
264            .limit(limit);
265        self.list_all_with_options(path, options).await
266    }
267
268    async fn list_all_with_options(
269        &self,
270        path: &str,
271        options: ListAllOptions,
272    ) -> NetDiskResult<ListAllResult> {
273        let token = self.token_getter.get_token().await?;
274        let recursion_str = options.recursion.to_string();
275        let desc_str = options.desc.to_string();
276        let start_str = options.start.to_string();
277        let limit_str = options.limit.to_string();
278        let web_str = options.web.to_string();
279        let ctime_str = options.ctime.map(|c| c.to_string()).unwrap_or_default();
280        let mtime_str = options.mtime.map(|m| m.to_string()).unwrap_or_default();
281
282        let mut params: Vec<(&str, &str)> = vec![
283            ("method", "listall"),
284            ("access_token", &token.access_token),
285            ("path", path),
286            ("recursion", &recursion_str),
287            ("order", &options.order),
288            ("desc", &desc_str),
289            ("start", &start_str),
290            ("limit", &limit_str),
291            ("web", &web_str),
292        ];
293
294        if !ctime_str.is_empty() {
295            params.push(("ctime", &ctime_str));
296        }
297
298        if !mtime_str.is_empty() {
299            params.push(("mtime", &mtime_str));
300        }
301
302        if !options.device_id.is_empty() {
303            params.push(("device_id", &options.device_id));
304        }
305
306        debug!(
307            "Listing all files recursively: {} with options: {:?}",
308            path, options
309        );
310
311        let response: ListAllResponse = self
312            .http_client()
313            .get("/rest/2.0/xpan/multimedia", Some(&params))
314            .await?;
315
316        debug!("ListAllResponse: {:?}", response);
317
318        if response.errno != 0 {
319            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
320            return Err(NetDiskError::api_error(response.errno, errmsg));
321        }
322
323        let list = response.list.unwrap_or_default();
324
325        let file_info_list = list
326            .into_iter()
327            .map(|item| FileInfo {
328                fs_id: item.fs_id,
329                path: item.path,
330                size: item.size,
331                ctime: item.local_ctime,
332                mtime: item.local_mtime,
333                isdir: item.isdir,
334                name: item.server_filename,
335                md5: item.md5,
336                category: item.category,
337                oper_id: None,
338                owner_id: None,
339                owner_type: None,
340                server_atime: None,
341                server_ctime: item.server_ctime,
342                server_mtime: item.server_mtime,
343            })
344            .collect();
345
346        Ok(ListAllResult {
347            list: file_info_list,
348            has_more: response.has_more.unwrap_or(0) == 1,
349            cursor: response.cursor,
350        })
351    }
352
353    async fn get_file_info(&self, path: &str) -> NetDiskResult<FileInfo> {
354        debug!("Getting file info for: {}", path);
355
356        // Normalize path
357        let normalized_path = if path.is_empty() { "/" } else { path };
358
359        // Get parent directory
360        let parent_path = if normalized_path == "/" {
361            "/".to_string()
362        } else {
363            let parent_parts: Vec<&str> = normalized_path.split('/').collect();
364            if parent_parts.len() <= 2 {
365                "/".to_string()
366            } else {
367                let parent = parent_parts[..parent_parts.len() - 1].join("/");
368                if parent.is_empty() {
369                    "/".to_string()
370                } else {
371                    parent
372                }
373            }
374        };
375
376        let folder_name = normalized_path
377            .rsplit('/')
378            .next()
379            .unwrap_or(normalized_path);
380
381        // Use list_directory_with_options to get the parent directory listing
382        let files = self
383            .list_directory_with_options(&parent_path, ListOptions::default())
384            .await?;
385
386        for item in files {
387            if item.path == normalized_path || item.name == folder_name {
388                return Ok(item);
389            }
390        }
391
392        Err(NetDiskError::api_error(-6, "File or folder not found"))
393    }
394
395    /// Get file metadata for download
396    ///
397    /// This method is specifically used for getting download links.
398    /// According to Baidu NetDisk Open Platform documentation:
399    /// <https://pan.baidu.com/union/doc/Fksg0sbcm>
400    ///
401    /// Note: The API endpoint is /rest/2.0/xpan/multimedia (not /rest/2.0/xpan/file)
402    async fn get_file_meta(&self, fs_id: u64) -> NetDiskResult<FileMeta> {
403        let token = self.token_getter.get_token().await?;
404        let fsids = serde_json::to_string(&[fs_id]).map_err(|e| NetDiskError::Unknown {
405            message: format!("Failed to serialize fsids: {}", e),
406        })?;
407
408        let params = [
409            ("method", "filemetas"),
410            ("access_token", &token.access_token),
411            ("fsids", &fsids),
412            ("dlink", "1"),
413        ];
414
415        debug!("Getting file metadata for download, fs_id: {}", fs_id);
416
417        let response: FileMetaResponse = self
418            .http_client()
419            .get("/rest/2.0/xpan/multimedia", Some(&params))
420            .await?;
421
422        if response.errno != 0 {
423            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
424            return Err(NetDiskError::api_error(response.errno, errmsg));
425        }
426
427        let list = match response.list {
428            Some(list) if !list.is_empty() => list,
429            _ => {
430                return Err(NetDiskError::api_error(
431                    -1,
432                    "File not found or no download link available",
433                ))
434            }
435        };
436
437        let file_info = &list[0];
438
439        Ok(FileMeta {
440            fs_id: Some(file_info.fs_id),
441            path: file_info.path.clone(),
442            size: file_info.size,
443            name: file_info.server_filename.clone(),
444            dlink: file_info.dlink.clone(),
445        })
446    }
447
448    async fn search_files(&self, key: &str, dir: &str) -> NetDiskResult<(Vec<FileInfo>, bool)> {
449        self.search_files_with_options(key, SearchOptions::new(dir))
450            .await
451    }
452
453    async fn search_files_with_options(
454        &self,
455        key: &str,
456        options: SearchOptions,
457    ) -> NetDiskResult<(Vec<FileInfo>, bool)> {
458        let token = self.token_getter.get_token().await?;
459        let category_str = options.category.map(|c| c.to_string());
460        let mut params = vec![
461            ("method", "search"),
462            ("access_token", &token.access_token),
463            ("key", key),
464            ("dir", &options.dir),
465            ("num", "500"),
466        ];
467
468        if let Some(ref category) = category_str {
469            params.push(("category", category));
470        }
471        if options.recursion {
472            params.push(("recursion", "1"));
473        }
474        if options.web {
475            params.push(("web", "1"));
476        }
477        if !options.device_id.is_empty() {
478            params.push(("device_id", &options.device_id));
479        }
480
481        debug!("Searching files with key: {}, options: {:?}", key, options);
482
483        let response: SearchResponse = self
484            .http_client()
485            .get("/rest/2.0/xpan/file", Some(&params))
486            .await?;
487
488        if response.errno != 0 {
489            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
490            return Err(NetDiskError::api_error(response.errno, errmsg));
491        }
492
493        let list = response.list.unwrap_or_default();
494        let has_more = response.has_more == 1;
495
496        let file_info_list = list
497            .into_iter()
498            .map(|item| FileInfo {
499                fs_id: item.fs_id,
500                path: item.path,
501                size: item.size,
502                ctime: item.local_ctime,
503                mtime: item.local_mtime,
504                isdir: item.isdir,
505                name: item.server_filename,
506                md5: item.md5,
507                category: item.category,
508                oper_id: None,
509                owner_id: None,
510                owner_type: None,
511                server_atime: None,
512                server_ctime: item.server_ctime,
513                server_mtime: item.server_mtime,
514            })
515            .collect();
516
517        Ok((file_info_list, has_more))
518    }
519
520    async fn semantic_search(&self, query: &str) -> NetDiskResult<Vec<FileInfo>> {
521        self.semantic_search_with_options(query, SemanticSearchOptions::default())
522            .await
523    }
524
525    async fn semantic_search_with_options(
526        &self,
527        query: &str,
528        options: SemanticSearchOptions,
529    ) -> NetDiskResult<Vec<FileInfo>> {
530        let token = self.token_getter.get_token().await?;
531        let num_str = options.num.to_string();
532        let stream_str = options.stream.to_string();
533        let search_type_str = options.search_type.to_string();
534
535        let url = format!("https://pan.baidu.com/xpan/unisearch?access_token={}&scene=mcpserver&query={}&num={}&stream={}&search_type={}",
536            urlencoding::encode(&token.access_token),
537            urlencoding::encode(query),
538            num_str,
539            stream_str,
540            search_type_str
541        );
542
543        debug!(
544            "Semantic search with query: {}, options: {:?}",
545            query, options
546        );
547
548        let response: SemanticSearchResponse = self
549            .http_client()
550            .post_json(&url, &serde_json::json!({}))
551            .await?;
552
553        if response.error_no != 0 {
554            let errmsg = response.error_msg.as_deref().unwrap_or("Unknown error");
555            return Err(NetDiskError::api_error(response.error_no, errmsg));
556        }
557
558        let mut file_info_list = Vec::new();
559
560        for data_item in response.data.unwrap_or_default() {
561            for item in data_item.list.unwrap_or_default() {
562                file_info_list.push(FileInfo {
563                    fs_id: Some(item.fsid),
564                    path: item.path,
565                    size: item.size,
566                    ctime: item.local_createtime,
567                    mtime: item.local_mtime,
568                    isdir: item.isdir,
569                    name: item.filename,
570                    md5: item.md5,
571                    category: Some(item.category as u32),
572                    oper_id: None,
573                    owner_id: None,
574                    owner_type: None,
575                    server_atime: None,
576                    server_ctime: item.server_ctime,
577                    server_mtime: item.server_mtime,
578                });
579            }
580        }
581
582        Ok(file_info_list)
583    }
584}
585
586/// Options for listing directory contents
587#[derive(Debug, Deserialize, Serialize)]
588pub struct ListOptions {
589    /// Sort order field (default: "name")
590    pub order: String,
591    /// Sort order descending (default: true)
592    pub desc: i32,
593    /// Start offset (default: 0)
594    pub start: i32,
595    /// Limit per page (default: 100)
596    pub limit: i32,
597    /// Web mode flag (default: true)
598    pub web: i32,
599    /// Show only folders (default: false)
600    pub folder: i32,
601    /// Show empty folders (default: false)
602    pub showempty: i32,
603}
604
605impl Default for ListOptions {
606    fn default() -> Self {
607        ListOptions {
608            order: "name".to_string(),
609            desc: 1,
610            start: 0,
611            limit: 100,
612            web: 1,
613            folder: 0,
614            showempty: 0,
615        }
616    }
617}
618
619impl ListOptions {
620    /// Create new ListOptions with defaults
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Set sort order field
626    pub fn order(mut self, order: &str) -> Self {
627        self.order = order.to_string();
628        self
629    }
630
631    /// Set sort order (descending if true)
632    pub fn desc(mut self, desc: bool) -> Self {
633        self.desc = if desc { 1 } else { 0 };
634        self
635    }
636
637    /// Set start offset
638    pub fn start(mut self, start: i32) -> Self {
639        self.start = start;
640        self
641    }
642
643    /// Set limit per page
644    pub fn limit(mut self, limit: i32) -> Self {
645        self.limit = limit;
646        self
647    }
648
649    /// Set web mode flag
650    pub fn web(mut self, web: bool) -> Self {
651        self.web = if web { 1 } else { 0 };
652        self
653    }
654
655    /// Set show only folders flag
656    pub fn folder(mut self, folder: bool) -> Self {
657        self.folder = if folder { 1 } else { 0 };
658        self
659    }
660
661    /// Set show empty folders flag
662    pub fn showempty(mut self, showempty: bool) -> Self {
663        self.showempty = if showempty { 1 } else { 0 };
664        self
665    }
666}
667
668/// Result of list_all operation
669#[derive(Debug, Deserialize, Serialize, Clone)]
670pub struct ListAllResult {
671    /// File list
672    pub list: Vec<FileInfo>,
673    /// Whether there are more pages
674    pub has_more: bool,
675    /// Cursor for next page query (when has_more is true)
676    pub cursor: Option<u64>,
677}
678
679/// Options for listing all files recursively
680#[derive(Debug, Deserialize, Serialize, Clone)]
681pub struct ListAllOptions {
682    /// Recursive listing (default: false)
683    pub recursion: i32,
684    /// Sort order field (default: "name")
685    pub order: String,
686    /// Sort order descending (default: false)
687    pub desc: i32,
688    /// Start offset (default: 0)
689    pub start: i32,
690    /// Limit per page (default: 1000)
691    pub limit: i32,
692    /// Filter by creation time
693    pub ctime: Option<u64>,
694    /// Filter by modification time
695    pub mtime: Option<u64>,
696    /// Web mode flag (default: false)
697    pub web: i32,
698    /// Device ID
699    pub device_id: String,
700}
701
702impl Default for ListAllOptions {
703    fn default() -> Self {
704        ListAllOptions {
705            recursion: 0,
706            order: "name".to_string(),
707            desc: 0,
708            start: 0,
709            limit: 1000,
710            ctime: None,
711            mtime: None,
712            web: 0,
713            device_id: String::new(),
714        }
715    }
716}
717
718impl ListAllOptions {
719    /// Create new ListAllOptions with defaults
720    pub fn new() -> Self {
721        Self::default()
722    }
723
724    /// Set recursive mode
725    pub fn recursion(mut self, recursion: bool) -> Self {
726        self.recursion = if recursion { 1 } else { 0 };
727        self
728    }
729
730    /// Set sort order field
731    pub fn order(mut self, order: &str) -> Self {
732        self.order = order.to_string();
733        self
734    }
735
736    /// Set sort order (descending if true)
737    pub fn desc(mut self, desc: bool) -> Self {
738        self.desc = if desc { 1 } else { 0 };
739        self
740    }
741
742    /// Set start offset
743    pub fn start(mut self, start: i32) -> Self {
744        self.start = start;
745        self
746    }
747
748    /// Set limit per page (max 1000)
749    pub fn limit(mut self, limit: i32) -> Self {
750        self.limit = limit.min(1000);
751        self
752    }
753
754    /// Set creation time filter
755    pub fn ctime(mut self, ctime: u64) -> Self {
756        self.ctime = Some(ctime);
757        self
758    }
759
760    /// Set modification time filter
761    pub fn mtime(mut self, mtime: u64) -> Self {
762        self.mtime = Some(mtime);
763        self
764    }
765
766    /// Set web mode flag
767    pub fn web(mut self, web: bool) -> Self {
768        self.web = if web { 1 } else { 0 };
769        self
770    }
771
772    /// Set device ID
773    pub fn device_id(mut self, device_id: &str) -> Self {
774        self.device_id = device_id.to_string();
775        self
776    }
777}
778
779/// File or folder information
780#[derive(Debug, Deserialize, Serialize, Clone)]
781pub struct FileInfo {
782    /// File server ID
783    pub fs_id: Option<u64>,
784    /// File full path
785    pub path: String,
786    /// File size in bytes
787    pub size: Option<u64>,
788    /// Creation time (timestamp)
789    pub ctime: Option<u64>,
790    /// Modification time (timestamp)
791    pub mtime: Option<u64>,
792    /// Is directory (1=directory, 0=file)
793    pub isdir: Option<i32>,
794    /// File name
795    pub name: String,
796    /// MD5 hash
797    pub md5: Option<String>,
798    /// File category
799    pub category: Option<u32>,
800    /// Operator ID
801    pub oper_id: Option<u64>,
802    /// Owner ID
803    pub owner_id: Option<u64>,
804    /// Owner type
805    pub owner_type: Option<u32>,
806    /// Server access time
807    pub server_atime: Option<u64>,
808    /// Server creation time
809    pub server_ctime: Option<u64>,
810    /// Server modification time
811    pub server_mtime: Option<u64>,
812}
813
814/// File metadata containing download link
815#[derive(Debug, Deserialize, Serialize, Clone)]
816pub struct FileMeta {
817    /// File server ID
818    pub fs_id: Option<u64>,
819    /// File path
820    pub path: String,
821    /// File size in bytes
822    pub size: Option<u64>,
823    /// File name
824    pub name: String,
825    /// Download link (dlink)
826    pub dlink: Option<String>,
827}
828
829#[derive(Debug, Deserialize)]
830struct ListResponse {
831    errno: i32,
832    errmsg: Option<String>,
833    list: Option<Vec<ListItem>>,
834}
835
836#[derive(Debug, Deserialize)]
837struct ListItem {
838    fs_id: Option<u64>,
839    path: String,
840    size: Option<u64>,
841    ctime: Option<u64>,
842    mtime: Option<u64>,
843    isdir: Option<i32>,
844    server_filename: String,
845    md5: Option<String>,
846    category: Option<u32>,
847    oper_id: Option<u64>,
848    owner_id: Option<u64>,
849    owner_type: Option<u32>,
850    server_atime: Option<u64>,
851    server_ctime: Option<u64>,
852    server_mtime: Option<u64>,
853}
854
855#[derive(Debug, Deserialize)]
856#[allow(dead_code)]
857struct ListAllResponse {
858    errno: i32,
859    errmsg: Option<String>,
860    cursor: Option<u64>,
861    has_more: Option<i32>,
862    list: Option<Vec<ListAllItem>>,
863}
864
865#[derive(Debug, Deserialize)]
866#[allow(dead_code)]
867struct ListAllItem {
868    category: Option<u32>,
869    #[serde(rename = "fs_id")]
870    fs_id: Option<u64>,
871    isdir: Option<i32>,
872    local_ctime: Option<u64>,
873    local_mtime: Option<u64>,
874    md5: Option<String>,
875    path: String,
876    server_filename: String,
877    server_ctime: Option<u64>,
878    server_mtime: Option<u64>,
879    size: Option<u64>,
880    thumbs: Option<Thumbs>,
881}
882
883#[derive(Debug, Deserialize)]
884#[allow(dead_code)]
885struct Thumbs {
886    url1: Option<String>,
887    url2: Option<String>,
888    url3: Option<String>,
889    icon: Option<String>,
890}
891
892#[derive(Debug, Deserialize)]
893struct FileMetaResponse {
894    errno: i32,
895    errmsg: Option<String>,
896    list: Option<Vec<FileMetaItem>>,
897}
898
899#[derive(Debug, Deserialize)]
900#[allow(dead_code)]
901struct FileMetaItem {
902    category: Option<u32>,
903    dlink: Option<String>,
904    #[serde(rename = "filename")]
905    server_filename: String,
906    fs_id: u64,
907    isdir: Option<u32>,
908    local_ctime: Option<u64>,
909    local_mtime: Option<u64>,
910    md5: Option<String>,
911    oper_id: Option<u64>,
912    path: String,
913    server_ctime: Option<u64>,
914    server_mtime: Option<u64>,
915    size: Option<u64>,
916}
917
918/// Options for keyword search
919#[derive(Debug, Deserialize, Serialize, Clone, Default)]
920pub struct SearchOptions {
921    /// Directory to search in
922    pub dir: String,
923    /// File category filter
924    pub category: Option<u32>,
925    /// Recursive search flag
926    pub recursion: bool,
927    /// Web mode flag
928    pub web: bool,
929    /// Device ID
930    pub device_id: String,
931}
932
933impl SearchOptions {
934    /// Create new SearchOptions with directory
935    pub fn new(dir: &str) -> Self {
936        Self {
937            dir: dir.to_string(),
938            category: None,
939            recursion: false,
940            web: false,
941            device_id: "".to_string(),
942        }
943    }
944
945    /// Set file category filter
946    pub fn category(mut self, category: u32) -> Self {
947        self.category = Some(category);
948        self
949    }
950
951    /// Set recursive search flag
952    pub fn recursion(mut self, recursion: bool) -> Self {
953        self.recursion = recursion;
954        self
955    }
956
957    /// Set web mode flag
958    pub fn web(mut self, web: bool) -> Self {
959        self.web = web;
960        self
961    }
962
963    /// Set device ID
964    pub fn device_id(mut self, device_id: &str) -> Self {
965        self.device_id = device_id.to_string();
966        self
967    }
968}
969
970/// Options for semantic search
971#[derive(Debug, Deserialize, Serialize, Clone, Default)]
972pub struct SemanticSearchOptions {
973    /// Directories to search in
974    pub dirs: Vec<String>,
975    /// File categories to search
976    pub categories: Vec<u32>,
977    /// Max number of results to return (default: 50)
978    pub num: i32,
979    /// Stream flag (default: 1)
980    pub stream: i32,
981    /// Search type (default: 0)
982    pub search_type: i32,
983    /// Sources to search from
984    pub sources: Vec<i32>,
985}
986
987impl SemanticSearchOptions {
988    /// Create new SemanticSearchOptions with defaults
989    pub fn new() -> Self {
990        Self::default()
991    }
992
993    /// Set directories to search in
994    pub fn dirs(mut self, dirs: Vec<&str>) -> Self {
995        self.dirs = dirs.iter().map(|s| s.to_string()).collect();
996        self
997    }
998
999    /// Set file categories to search
1000    pub fn categories(mut self, categories: Vec<u32>) -> Self {
1001        self.categories = categories;
1002        self
1003    }
1004
1005    /// Set max number of results
1006    pub fn num(mut self, num: i32) -> Self {
1007        self.num = num;
1008        self
1009    }
1010
1011    /// Set stream flag
1012    pub fn stream(mut self, stream: i32) -> Self {
1013        self.stream = stream;
1014        self
1015    }
1016
1017    /// Set search type
1018    pub fn search_type(mut self, search_type: i32) -> Self {
1019        self.search_type = search_type;
1020        self
1021    }
1022
1023    /// Set sources to search from
1024    pub fn sources(mut self, sources: Vec<i32>) -> Self {
1025        self.sources = sources;
1026        self
1027    }
1028}
1029
1030#[derive(Debug, Deserialize)]
1031struct SearchResponse {
1032    errno: i32,
1033    errmsg: Option<String>,
1034    list: Option<Vec<SearchItem>>,
1035    has_more: i32,
1036}
1037
1038#[derive(Debug, Deserialize)]
1039#[allow(dead_code)]
1040struct SearchItem {
1041    category: Option<u32>,
1042    #[serde(rename = "fs_id")]
1043    fs_id: Option<u64>,
1044    isdir: Option<i32>,
1045    local_ctime: Option<u64>,
1046    local_mtime: Option<u64>,
1047    server_ctime: Option<u64>,
1048    server_mtime: Option<u64>,
1049    md5: Option<String>,
1050    path: String,
1051    server_filename: String,
1052    size: Option<u64>,
1053    thumbs: Option<Thumbs>,
1054}
1055
1056#[derive(Debug, Deserialize)]
1057#[allow(dead_code)]
1058struct SemanticSearchResponse {
1059    #[serde(rename = "error_no")]
1060    error_no: i32,
1061    #[serde(rename = "error_msg")]
1062    error_msg: Option<String>,
1063    data: Option<Vec<SemanticSearchDataItem>>,
1064    #[serde(rename = "is_end")]
1065    is_end: bool,
1066}
1067
1068#[derive(Debug, Deserialize)]
1069#[allow(dead_code)]
1070struct SemanticSearchDataItem {
1071    list: Option<Vec<SemanticSearchItem>>,
1072    source: i32,
1073}
1074
1075#[derive(Debug, Deserialize)]
1076struct SemanticSearchItem {
1077    category: i32,
1078    filename: String,
1079    fsid: u64,
1080    isdir: Option<i32>,
1081    #[serde(rename = "local_createtime")]
1082    local_createtime: Option<u64>,
1083    #[serde(rename = "local_mtime")]
1084    local_mtime: Option<u64>,
1085    md5: Option<String>,
1086    path: String,
1087    #[serde(rename = "server_ctime")]
1088    server_ctime: Option<u64>,
1089    #[serde(rename = "server_mtime")]
1090    server_mtime: Option<u64>,
1091    size: Option<u64>,
1092}