1use log::{debug, info};
5use serde::{Deserialize, Serialize};
6use std::future::Future;
7
8use super::FileClient;
9use crate::errors::{NetDiskError, NetDiskResult};
10
11pub(crate) trait FileQueryExt {
13 fn list_directory(
30 &self,
31 dir: &str,
32 ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
33
34 fn list_directory_with_options(
39 &self,
40 dir: &str,
41 options: ListOptions,
42 ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
43
44 fn list_all(
77 &self,
78 path: &str,
79 start: i32,
80 limit: i32,
81 ) -> impl Future<Output = NetDiskResult<ListAllResult>> + Send;
82
83 fn list_all_with_options(
88 &self,
89 path: &str,
90 options: ListAllOptions,
91 ) -> impl Future<Output = NetDiskResult<ListAllResult>> + Send;
92
93 fn get_file_info(&self, path: &str) -> impl Future<Output = NetDiskResult<FileInfo>> + Send;
110
111 fn get_file_meta(&self, fs_id: u64) -> impl Future<Output = NetDiskResult<FileMeta>> + Send;
133
134 fn search_files(
151 &self,
152 key: &str,
153 dir: &str,
154 ) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
155
156 fn search_files_with_options(
161 &self,
162 key: &str,
163 options: SearchOptions,
164 ) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
165
166 fn semantic_search(
183 &self,
184 query: &str,
185 ) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
186
187 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(¶ms))
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(¶ms))
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 let normalized_path = if path.is_empty() { "/" } else { path };
358
359 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 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 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(¶ms))
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(¶ms))
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#[derive(Debug, Deserialize, Serialize)]
588pub struct ListOptions {
589 pub order: String,
591 pub desc: i32,
593 pub start: i32,
595 pub limit: i32,
597 pub web: i32,
599 pub folder: i32,
601 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 pub fn new() -> Self {
622 Self::default()
623 }
624
625 pub fn order(mut self, order: &str) -> Self {
627 self.order = order.to_string();
628 self
629 }
630
631 pub fn desc(mut self, desc: bool) -> Self {
633 self.desc = if desc { 1 } else { 0 };
634 self
635 }
636
637 pub fn start(mut self, start: i32) -> Self {
639 self.start = start;
640 self
641 }
642
643 pub fn limit(mut self, limit: i32) -> Self {
645 self.limit = limit;
646 self
647 }
648
649 pub fn web(mut self, web: bool) -> Self {
651 self.web = if web { 1 } else { 0 };
652 self
653 }
654
655 pub fn folder(mut self, folder: bool) -> Self {
657 self.folder = if folder { 1 } else { 0 };
658 self
659 }
660
661 pub fn showempty(mut self, showempty: bool) -> Self {
663 self.showempty = if showempty { 1 } else { 0 };
664 self
665 }
666}
667
668#[derive(Debug, Deserialize, Serialize, Clone)]
670pub struct ListAllResult {
671 pub list: Vec<FileInfo>,
673 pub has_more: bool,
675 pub cursor: Option<u64>,
677}
678
679#[derive(Debug, Deserialize, Serialize, Clone)]
681pub struct ListAllOptions {
682 pub recursion: i32,
684 pub order: String,
686 pub desc: i32,
688 pub start: i32,
690 pub limit: i32,
692 pub ctime: Option<u64>,
694 pub mtime: Option<u64>,
696 pub web: i32,
698 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 pub fn new() -> Self {
721 Self::default()
722 }
723
724 pub fn recursion(mut self, recursion: bool) -> Self {
726 self.recursion = if recursion { 1 } else { 0 };
727 self
728 }
729
730 pub fn order(mut self, order: &str) -> Self {
732 self.order = order.to_string();
733 self
734 }
735
736 pub fn desc(mut self, desc: bool) -> Self {
738 self.desc = if desc { 1 } else { 0 };
739 self
740 }
741
742 pub fn start(mut self, start: i32) -> Self {
744 self.start = start;
745 self
746 }
747
748 pub fn limit(mut self, limit: i32) -> Self {
750 self.limit = limit.min(1000);
751 self
752 }
753
754 pub fn ctime(mut self, ctime: u64) -> Self {
756 self.ctime = Some(ctime);
757 self
758 }
759
760 pub fn mtime(mut self, mtime: u64) -> Self {
762 self.mtime = Some(mtime);
763 self
764 }
765
766 pub fn web(mut self, web: bool) -> Self {
768 self.web = if web { 1 } else { 0 };
769 self
770 }
771
772 pub fn device_id(mut self, device_id: &str) -> Self {
774 self.device_id = device_id.to_string();
775 self
776 }
777}
778
779#[derive(Debug, Deserialize, Serialize, Clone)]
781pub struct FileInfo {
782 pub fs_id: Option<u64>,
784 pub path: String,
786 pub size: Option<u64>,
788 pub ctime: Option<u64>,
790 pub mtime: Option<u64>,
792 pub isdir: Option<i32>,
794 pub name: String,
796 pub md5: Option<String>,
798 pub category: Option<u32>,
800 pub oper_id: Option<u64>,
802 pub owner_id: Option<u64>,
804 pub owner_type: Option<u32>,
806 pub server_atime: Option<u64>,
808 pub server_ctime: Option<u64>,
810 pub server_mtime: Option<u64>,
812}
813
814#[derive(Debug, Deserialize, Serialize, Clone)]
816pub struct FileMeta {
817 pub fs_id: Option<u64>,
819 pub path: String,
821 pub size: Option<u64>,
823 pub name: String,
825 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#[derive(Debug, Deserialize, Serialize, Clone, Default)]
920pub struct SearchOptions {
921 pub dir: String,
923 pub category: Option<u32>,
925 pub recursion: bool,
927 pub web: bool,
929 pub device_id: String,
931}
932
933impl SearchOptions {
934 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 pub fn category(mut self, category: u32) -> Self {
947 self.category = Some(category);
948 self
949 }
950
951 pub fn recursion(mut self, recursion: bool) -> Self {
953 self.recursion = recursion;
954 self
955 }
956
957 pub fn web(mut self, web: bool) -> Self {
959 self.web = web;
960 self
961 }
962
963 pub fn device_id(mut self, device_id: &str) -> Self {
965 self.device_id = device_id.to_string();
966 self
967 }
968}
969
970#[derive(Debug, Deserialize, Serialize, Clone, Default)]
972pub struct SemanticSearchOptions {
973 pub dirs: Vec<String>,
975 pub categories: Vec<u32>,
977 pub num: i32,
979 pub stream: i32,
981 pub search_type: i32,
983 pub sources: Vec<i32>,
985}
986
987impl SemanticSearchOptions {
988 pub fn new() -> Self {
990 Self::default()
991 }
992
993 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 pub fn categories(mut self, categories: Vec<u32>) -> Self {
1001 self.categories = categories;
1002 self
1003 }
1004
1005 pub fn num(mut self, num: i32) -> Self {
1007 self.num = num;
1008 self
1009 }
1010
1011 pub fn stream(mut self, stream: i32) -> Self {
1013 self.stream = stream;
1014 self
1015 }
1016
1017 pub fn search_type(mut self, search_type: i32) -> Self {
1019 self.search_type = search_type;
1020 self
1021 }
1022
1023 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}