use log::{debug, info};
use serde::Deserialize;
use std::future::Future;
use super::FileClient;
use crate::auth::AccessToken;
use crate::errors::{NetDiskError, NetDiskResult};
pub(crate) trait FileQueryExt {
fn list_directory(
&self,
access_token: &AccessToken,
dir: &str,
) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
fn list_directory_with_options(
&self,
access_token: &AccessToken,
dir: &str,
options: ListOptions,
) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
fn list_all(
&self,
access_token: &AccessToken,
path: &str,
) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
fn list_all_with_options(
&self,
access_token: &AccessToken,
path: &str,
options: ListAllOptions,
) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
fn get_file_info(
&self,
access_token: &AccessToken,
path: &str,
) -> impl Future<Output = NetDiskResult<FileInfo>> + Send;
fn get_file_meta(
&self,
access_token: &AccessToken,
fs_id: u64,
) -> impl Future<Output = NetDiskResult<FileMeta>> + Send;
fn search_files(
&self,
access_token: &AccessToken,
key: &str,
dir: &str,
) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
fn search_files_with_options(
&self,
access_token: &AccessToken,
key: &str,
options: SearchOptions,
) -> impl Future<Output = NetDiskResult<(Vec<FileInfo>, bool)>> + Send;
fn semantic_search(
&self,
access_token: &AccessToken,
query: &str,
) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
fn semantic_search_with_options(
&self,
access_token: &AccessToken,
query: &str,
options: SemanticSearchOptions,
) -> impl Future<Output = NetDiskResult<Vec<FileInfo>>> + Send;
}
impl FileQueryExt for FileClient {
async fn list_directory(
&self,
access_token: &AccessToken,
dir: &str,
) -> NetDiskResult<Vec<FileInfo>> {
self.list_directory_with_options(access_token, dir, ListOptions::default())
.await
}
async fn list_directory_with_options(
&self,
access_token: &AccessToken,
dir: &str,
options: ListOptions,
) -> NetDiskResult<Vec<FileInfo>> {
let params = [
("method", "list"),
("dir", dir),
("order", &options.order),
("desc", &options.desc.to_string()),
("start", &options.start.to_string()),
("limit", &options.limit.to_string()),
("web", &options.web.to_string()),
("folder", &options.folder.to_string()),
("showempty", &options.showempty.to_string()),
("access_token", &access_token.access_token),
];
debug!("Listing directory: {} with options: {:?}", dir, options);
let response: ListResponse = self
.http_client()
.get("/rest/2.0/xpan/file", Some(¶ms))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
info!("Listed {} items in directory: {}", list.len(), dir);
Ok(list
.into_iter()
.map(|item| FileInfo {
fs_id: item.fs_id,
path: item.path,
size: item.size,
ctime: item.ctime,
mtime: item.mtime,
isdir: item.isdir,
name: item.server_filename,
md5: item.md5,
category: item.category,
oper_id: item.oper_id,
owner_id: item.owner_id,
owner_type: item.owner_type,
server_atime: item.server_atime,
server_ctime: item.server_ctime,
server_mtime: item.server_mtime,
})
.collect())
}
async fn list_all(
&self,
access_token: &AccessToken,
path: &str,
) -> NetDiskResult<(Vec<FileInfo>, bool)> {
self.list_all_with_options(access_token, path, ListAllOptions::new())
.await
}
async fn list_all_with_options(
&self,
access_token: &AccessToken,
path: &str,
options: ListAllOptions,
) -> NetDiskResult<(Vec<FileInfo>, bool)> {
let recursion_str = options.recursion.to_string();
let desc_str = options.desc.to_string();
let start_str = options.start.to_string();
let limit_str = options.limit.to_string();
let web_str = options.web.to_string();
let ctime_str = options.ctime.map(|c| c.to_string()).unwrap_or_default();
let mtime_str = options.mtime.map(|m| m.to_string()).unwrap_or_default();
let mut params: Vec<(&str, &str)> = vec![
("method", "listall"),
("access_token", &access_token.access_token),
("path", path),
("recursion", &recursion_str),
("order", &options.order),
("desc", &desc_str),
("start", &start_str),
("limit", &limit_str),
("web", &web_str),
];
if !ctime_str.is_empty() {
params.push(("ctime", &ctime_str));
}
if !mtime_str.is_empty() {
params.push(("mtime", &mtime_str));
}
if !options.device_id.is_empty() {
params.push(("device_id", &options.device_id));
}
debug!(
"Listing all files recursively: {} with options: {:?}",
path, options
);
let response: ListAllResponse = self
.http_client()
.get("/rest/2.0/xpan/multimedia", Some(¶ms))
.await?;
debug!("ListAllResponse: {:?}", response);
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
let has_more = response.has_more.unwrap_or(0) == 1;
let file_info_list = list
.into_iter()
.map(|item| FileInfo {
fs_id: item.fs_id,
path: item.path,
size: item.size,
ctime: item.local_ctime,
mtime: item.local_mtime,
isdir: item.isdir,
name: item.server_filename,
md5: item.md5,
category: item.category,
oper_id: None,
owner_id: None,
owner_type: None,
server_atime: None,
server_ctime: item.server_ctime,
server_mtime: item.server_mtime,
})
.collect();
Ok((file_info_list, has_more))
}
async fn get_file_info(
&self,
access_token: &AccessToken,
path: &str,
) -> NetDiskResult<FileInfo> {
debug!("Getting file info for: {}", path);
let normalized_path = if path.is_empty() { "/" } else { path };
let parent_path = if normalized_path == "/" {
"/".to_string()
} else {
let parent_parts: Vec<&str> = normalized_path.split('/').collect();
if parent_parts.len() <= 2 {
"/".to_string()
} else {
let parent = parent_parts[..parent_parts.len() - 1].join("/");
if parent.is_empty() {
"/".to_string()
} else {
parent
}
}
};
let folder_name = normalized_path
.rsplit('/')
.next()
.unwrap_or(normalized_path);
let files = self
.list_directory_with_options(access_token, &parent_path, ListOptions::default())
.await?;
for item in files {
if item.path == normalized_path || item.name == folder_name {
return Ok(item);
}
}
Err(NetDiskError::api_error(-6, "File or folder not found"))
}
async fn get_file_meta(
&self,
access_token: &AccessToken,
fs_id: u64,
) -> NetDiskResult<FileMeta> {
let fsids = serde_json::to_string(&[fs_id]).map_err(|e| NetDiskError::Unknown {
message: format!("Failed to serialize fsids: {}", e),
})?;
let params = [
("method", "filemetas"),
("access_token", &access_token.access_token),
("fsids", &fsids),
("dlink", "1"),
];
debug!("Getting file metadata for download, fs_id: {}", fs_id);
let response: FileMetaResponse = self
.http_client()
.get("/rest/2.0/xpan/multimedia", Some(¶ms))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = match response.list {
Some(list) if !list.is_empty() => list,
_ => {
return Err(NetDiskError::api_error(
-1,
"File not found or no download link available",
))
}
};
let file_info = &list[0];
Ok(FileMeta {
fs_id: Some(file_info.fs_id),
path: file_info.path.clone(),
size: file_info.size,
name: file_info.server_filename.clone(),
dlink: file_info.dlink.clone(),
})
}
async fn search_files(
&self,
access_token: &AccessToken,
key: &str,
dir: &str,
) -> NetDiskResult<(Vec<FileInfo>, bool)> {
self.search_files_with_options(access_token, key, SearchOptions::new(dir))
.await
}
async fn search_files_with_options(
&self,
access_token: &AccessToken,
key: &str,
options: SearchOptions,
) -> NetDiskResult<(Vec<FileInfo>, bool)> {
let category_str = options.category.map(|c| c.to_string());
let mut params = vec![
("method", "search"),
("access_token", &access_token.access_token),
("key", key),
("dir", &options.dir),
("num", "500"),
];
if let Some(ref category) = category_str {
params.push(("category", category));
}
if options.recursion {
params.push(("recursion", "1"));
}
if options.web {
params.push(("web", "1"));
}
if !options.device_id.is_empty() {
params.push(("device_id", &options.device_id));
}
debug!("Searching files with key: {}, options: {:?}", key, options);
let response: SearchResponse = self
.http_client()
.get("/rest/2.0/xpan/file", Some(¶ms))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
let has_more = response.has_more == 1;
let file_info_list = list
.into_iter()
.map(|item| FileInfo {
fs_id: item.fs_id,
path: item.path,
size: item.size,
ctime: item.local_ctime,
mtime: item.local_mtime,
isdir: item.isdir,
name: item.server_filename,
md5: item.md5,
category: item.category,
oper_id: None,
owner_id: None,
owner_type: None,
server_atime: None,
server_ctime: item.server_ctime,
server_mtime: item.server_mtime,
})
.collect();
Ok((file_info_list, has_more))
}
async fn semantic_search(
&self,
access_token: &AccessToken,
query: &str,
) -> NetDiskResult<Vec<FileInfo>> {
self.semantic_search_with_options(access_token, query, SemanticSearchOptions::default())
.await
}
async fn semantic_search_with_options(
&self,
access_token: &AccessToken,
query: &str,
options: SemanticSearchOptions,
) -> NetDiskResult<Vec<FileInfo>> {
let num_str = options.num.to_string();
let stream_str = options.stream.to_string();
let search_type_str = options.search_type.to_string();
let url = format!("https://pan.baidu.com/xpan/unisearch?access_token={}&scene=mcpserver&query={}&num={}&stream={}&search_type={}",
urlencoding::encode(&access_token.access_token),
urlencoding::encode(query),
num_str,
stream_str,
search_type_str
);
debug!(
"Semantic search with query: {}, options: {:?}",
query, options
);
let response: SemanticSearchResponse = self
.http_client()
.post_json(&url, &serde_json::json!({}))
.await?;
if response.error_no != 0 {
let errmsg = response.error_msg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.error_no, errmsg));
}
let mut file_info_list = Vec::new();
for data_item in response.data.unwrap_or_default() {
for item in data_item.list.unwrap_or_default() {
file_info_list.push(FileInfo {
fs_id: Some(item.fsid),
path: item.path,
size: item.size,
ctime: item.local_createtime,
mtime: item.local_mtime,
isdir: item.isdir,
name: item.filename,
md5: item.md5,
category: Some(item.category as u32),
oper_id: None,
owner_id: None,
owner_type: None,
server_atime: None,
server_ctime: item.server_ctime,
server_mtime: item.server_mtime,
});
}
}
Ok(file_info_list)
}
}
#[derive(Debug)]
pub struct ListOptions {
pub order: String,
pub desc: i32,
pub start: i32,
pub limit: i32,
pub web: i32,
pub folder: i32,
pub showempty: i32,
}
impl Default for ListOptions {
fn default() -> Self {
ListOptions {
order: "name".to_string(),
desc: 1,
start: 0,
limit: 100,
web: 1,
folder: 0,
showempty: 0,
}
}
}
impl ListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn order(mut self, order: &str) -> Self {
self.order = order.to_string();
self
}
pub fn desc(mut self, desc: bool) -> Self {
self.desc = if desc { 1 } else { 0 };
self
}
pub fn start(mut self, start: i32) -> Self {
self.start = start;
self
}
pub fn limit(mut self, limit: i32) -> Self {
self.limit = limit;
self
}
pub fn web(mut self, web: bool) -> Self {
self.web = if web { 1 } else { 0 };
self
}
pub fn folder(mut self, folder: bool) -> Self {
self.folder = if folder { 1 } else { 0 };
self
}
pub fn showempty(mut self, showempty: bool) -> Self {
self.showempty = if showempty { 1 } else { 0 };
self
}
}
#[derive(Debug, Clone)]
pub struct ListAllOptions {
pub recursion: i32,
pub order: String,
pub desc: i32,
pub start: i32,
pub limit: i32,
pub ctime: Option<u64>,
pub mtime: Option<u64>,
pub web: i32,
pub device_id: String,
}
impl Default for ListAllOptions {
fn default() -> Self {
ListAllOptions {
recursion: 0,
order: "name".to_string(),
desc: 0,
start: 0,
limit: 1000,
ctime: None,
mtime: None,
web: 0,
device_id: String::new(),
}
}
}
impl ListAllOptions {
pub fn new() -> Self {
Self::default()
}
pub fn recursion(mut self, recursion: bool) -> Self {
self.recursion = if recursion { 1 } else { 0 };
self
}
pub fn order(mut self, order: &str) -> Self {
self.order = order.to_string();
self
}
pub fn desc(mut self, desc: bool) -> Self {
self.desc = if desc { 1 } else { 0 };
self
}
pub fn start(mut self, start: i32) -> Self {
self.start = start;
self
}
pub fn limit(mut self, limit: i32) -> Self {
self.limit = limit.min(1000);
self
}
pub fn ctime(mut self, ctime: u64) -> Self {
self.ctime = Some(ctime);
self
}
pub fn mtime(mut self, mtime: u64) -> Self {
self.mtime = Some(mtime);
self
}
pub fn web(mut self, web: bool) -> Self {
self.web = if web { 1 } else { 0 };
self
}
pub fn device_id(mut self, device_id: &str) -> Self {
self.device_id = device_id.to_string();
self
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct FileInfo {
pub fs_id: Option<u64>,
pub path: String,
pub size: Option<u64>,
pub ctime: Option<u64>,
pub mtime: Option<u64>,
pub isdir: Option<i32>,
pub name: String,
pub md5: Option<String>,
pub category: Option<u32>,
pub oper_id: Option<u64>,
pub owner_id: Option<u64>,
pub owner_type: Option<u32>,
pub server_atime: Option<u64>,
pub server_ctime: Option<u64>,
pub server_mtime: Option<u64>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct FileMeta {
pub fs_id: Option<u64>,
pub path: String,
pub size: Option<u64>,
pub name: String,
pub dlink: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ListResponse {
errno: i32,
errmsg: Option<String>,
list: Option<Vec<ListItem>>,
}
#[derive(Debug, Deserialize)]
struct ListItem {
fs_id: Option<u64>,
path: String,
size: Option<u64>,
ctime: Option<u64>,
mtime: Option<u64>,
isdir: Option<i32>,
server_filename: String,
md5: Option<String>,
category: Option<u32>,
oper_id: Option<u64>,
owner_id: Option<u64>,
owner_type: Option<u32>,
server_atime: Option<u64>,
server_ctime: Option<u64>,
server_mtime: Option<u64>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ListAllResponse {
errno: i32,
errmsg: Option<String>,
cursor: Option<u64>,
has_more: Option<i32>,
list: Option<Vec<ListAllItem>>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ListAllItem {
category: Option<u32>,
#[serde(rename = "fs_id")]
fs_id: Option<u64>,
isdir: Option<i32>,
local_ctime: Option<u64>,
local_mtime: Option<u64>,
md5: Option<String>,
path: String,
server_filename: String,
server_ctime: Option<u64>,
server_mtime: Option<u64>,
size: Option<u64>,
thumbs: Option<Thumbs>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Thumbs {
url1: Option<String>,
url2: Option<String>,
url3: Option<String>,
icon: Option<String>,
}
#[derive(Debug, Deserialize)]
struct FileMetaResponse {
errno: i32,
errmsg: Option<String>,
list: Option<Vec<FileMetaItem>>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct FileMetaItem {
category: Option<u32>,
dlink: Option<String>,
#[serde(rename = "filename")]
server_filename: String,
fs_id: u64,
isdir: Option<u32>,
local_ctime: Option<u64>,
local_mtime: Option<u64>,
md5: Option<String>,
oper_id: Option<u64>,
path: String,
server_ctime: Option<u64>,
server_mtime: Option<u64>,
size: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct SearchOptions {
pub dir: String,
pub category: Option<u32>,
pub recursion: bool,
pub web: bool,
pub device_id: String,
}
impl SearchOptions {
pub fn new(dir: &str) -> Self {
Self {
dir: dir.to_string(),
category: None,
recursion: false,
web: false,
device_id: "".to_string(),
}
}
pub fn category(mut self, category: u32) -> Self {
self.category = Some(category);
self
}
pub fn recursion(mut self, recursion: bool) -> Self {
self.recursion = recursion;
self
}
pub fn web(mut self, web: bool) -> Self {
self.web = web;
self
}
pub fn device_id(mut self, device_id: &str) -> Self {
self.device_id = device_id.to_string();
self
}
}
#[derive(Debug, Clone, Default)]
pub struct SemanticSearchOptions {
pub dirs: Vec<String>,
pub categories: Vec<u32>,
pub num: i32,
pub stream: i32,
pub search_type: i32,
pub sources: Vec<i32>,
}
impl SemanticSearchOptions {
pub fn new() -> Self {
Self::default()
}
pub fn dirs(mut self, dirs: Vec<&str>) -> Self {
self.dirs = dirs.iter().map(|s| s.to_string()).collect();
self
}
pub fn categories(mut self, categories: Vec<u32>) -> Self {
self.categories = categories;
self
}
pub fn num(mut self, num: i32) -> Self {
self.num = num;
self
}
pub fn stream(mut self, stream: i32) -> Self {
self.stream = stream;
self
}
pub fn search_type(mut self, search_type: i32) -> Self {
self.search_type = search_type;
self
}
pub fn sources(mut self, sources: Vec<i32>) -> Self {
self.sources = sources;
self
}
}
#[derive(Debug, Deserialize)]
struct SearchResponse {
errno: i32,
errmsg: Option<String>,
list: Option<Vec<SearchItem>>,
has_more: i32,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SearchItem {
category: Option<u32>,
#[serde(rename = "fs_id")]
fs_id: Option<u64>,
isdir: Option<i32>,
local_ctime: Option<u64>,
local_mtime: Option<u64>,
server_ctime: Option<u64>,
server_mtime: Option<u64>,
md5: Option<String>,
path: String,
server_filename: String,
size: Option<u64>,
thumbs: Option<Thumbs>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SemanticSearchResponse {
#[serde(rename = "error_no")]
error_no: i32,
#[serde(rename = "error_msg")]
error_msg: Option<String>,
data: Option<Vec<SemanticSearchDataItem>>,
#[serde(rename = "is_end")]
is_end: bool,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SemanticSearchDataItem {
list: Option<Vec<SemanticSearchItem>>,
source: i32,
}
#[derive(Debug, Deserialize)]
struct SemanticSearchItem {
category: i32,
filename: String,
fsid: u64,
isdir: Option<i32>,
#[serde(rename = "local_createtime")]
local_createtime: Option<u64>,
#[serde(rename = "local_mtime")]
local_mtime: Option<u64>,
md5: Option<String>,
path: String,
#[serde(rename = "server_ctime")]
server_ctime: Option<u64>,
#[serde(rename = "server_mtime")]
server_mtime: Option<u64>,
size: Option<u64>,
}