use openlark_core::{
SDKResult,
api::{ApiRequest, ApiResponseTrait, ResponseFormat},
config::Config,
http::Transport,
validate_required,
};
use serde::{Deserialize, Serialize};
use crate::common::{api_endpoints::DriveApi, api_utils::*};
#[derive(Debug, Clone, Serialize)]
pub struct CopyFileRequest {
#[serde(skip)]
config: Config,
pub file_token: String,
#[serde(skip)]
pub user_id_type: Option<String>,
pub name: String,
pub folder_token: String,
#[serde(rename = "type")]
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<Vec<Property>>,
}
impl CopyFileRequest {
pub fn new(
config: Config,
file_token: impl Into<String>,
name: impl Into<String>,
file_type: impl Into<String>,
folder_token: impl Into<String>,
) -> Self {
Self {
config,
file_token: file_token.into(),
user_id_type: None,
name: name.into(),
folder_token: folder_token.into(),
r#type: file_type.into(),
extra: None,
}
}
pub fn user_id_type(mut self, user_id_type: impl Into<String>) -> Self {
self.user_id_type = Some(user_id_type.into());
self
}
pub fn extra(mut self, extra: Vec<Property>) -> Self {
self.extra = Some(extra);
self
}
pub async fn execute(self) -> SDKResult<CopyFileResponse> {
self.execute_with_options(openlark_core::req_option::RequestOption::default())
.await
}
pub async fn execute_with_options(
self,
option: openlark_core::req_option::RequestOption,
) -> SDKResult<CopyFileResponse> {
validate_required!(self.file_token, "file_token 不能为空");
validate_required!(self.folder_token, "folder_token 不能为空");
if self.r#type.is_empty() {
return Err(openlark_core::error::validation_error(
"type",
"type 不能为空",
));
}
match self.r#type.as_str() {
"file" | "doc" | "sheet" | "bitable" | "docx" | "mindnote" | "slides" => {}
_ => {
return Err(openlark_core::error::validation_error(
"type",
"type 仅支持 file/doc/sheet/bitable/docx/mindnote/slides",
));
}
}
if let Some(user_id_type) = &self.user_id_type {
match user_id_type.as_str() {
"open_id" | "union_id" | "user_id" => {}
_ => {
return Err(openlark_core::error::validation_error(
"user_id_type",
"user_id_type 仅支持 open_id/union_id/user_id",
));
}
}
}
let name_len = self.name.len();
if name_len == 0 || name_len > 256 {
return Err(openlark_core::error::validation_error(
"name",
"name 长度必须在 1~256 字节之间",
));
}
if let Some(extra) = &self.extra {
for (idx, prop) in extra.iter().enumerate() {
if prop.key.trim().is_empty() {
return Err(openlark_core::error::validation_error(
&format!("extra[{idx}].key"),
"key 不能为空",
));
}
if prop.value.trim().is_empty() {
return Err(openlark_core::error::validation_error(
&format!("extra[{idx}].value"),
"value 不能为空",
));
}
}
}
let api_endpoint = DriveApi::CopyFile(self.file_token.clone());
let mut request = ApiRequest::<CopyFileResponse>::post(&api_endpoint.to_url());
if let Some(user_id_type) = &self.user_id_type {
request = request.query("user_id_type", user_id_type);
}
#[derive(Serialize)]
struct CopyFileBody {
name: String,
#[serde(rename = "type")]
r#type: String,
folder_token: String,
#[serde(skip_serializing_if = "Option::is_none")]
extra: Option<Vec<Property>>,
}
request = request.body(serialize_params(
&CopyFileBody {
name: self.name,
r#type: self.r#type,
folder_token: self.folder_token,
extra: self.extra,
},
"复制文件",
)?);
let response = Transport::request(request, &self.config, Some(option)).await?;
extract_response_data(response, "复制文件")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Property {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyFileResponse {
pub file: CopiedFile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyShortcutInfo {
pub target_type: String,
pub target_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopiedFile {
pub token: String,
pub name: String,
#[serde(rename = "type")]
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shortcut_info: Option<CopyShortcutInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_id: Option<String>,
}
impl ApiResponseTrait for CopyFileResponse {
fn data_format() -> ResponseFormat {
ResponseFormat::Data
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_copy_file_request_builder() {
let config = Config::default();
let request =
CopyFileRequest::new(config, "file_token", "Demo copy", "docx", "folder_token");
assert_eq!(request.file_token, "file_token");
assert_eq!(request.name, "Demo copy");
assert_eq!(request.r#type, "docx");
assert_eq!(request.folder_token, "folder_token");
assert!(request.extra.is_none());
}
#[test]
fn test_copy_file_request_with_extra() {
let config = Config::default();
let request =
CopyFileRequest::new(config, "file_token", "Demo copy", "docx", "folder_token").extra(
vec![Property {
key: "target_type".to_string(),
value: "docx".to_string(),
}],
);
assert_eq!(
request
.extra
.expect("extra should be set when .extra() is called")
.len(),
1
);
}
#[test]
fn test_copy_file_with_empty_file_token() {
let config = Config::default();
let request = CopyFileRequest::new(config, "", "name", "docx", "folder_token");
assert_eq!(request.file_token, "");
}
#[test]
fn test_copy_file_with_invalid_type() {
let config = Config::default();
let request = CopyFileRequest::new(config, "file_token", "name", "invalid", "folder_token");
assert_eq!(request.r#type, "invalid");
}
#[test]
fn test_copy_file_with_empty_name() {
let config = Config::default();
let request = CopyFileRequest::new(config, "file_token", "", "docx", "folder_token");
assert_eq!(request.name, "");
}
#[test]
fn test_copy_file_with_too_long_name() {
let config = Config::default();
let long_name = "a".repeat(257);
let request = CopyFileRequest::new(config, "file_token", long_name, "docx", "folder_token");
assert!(request.name.len() > 256);
}
#[test]
fn test_copy_file_with_max_length_name() {
let config = Config::default();
let max_name = "a".repeat(256);
let request = CopyFileRequest::new(
config,
"file_token",
max_name.clone(),
"docx",
"folder_token",
);
assert_eq!(request.name, max_name);
assert_eq!(request.name.len(), 256);
}
#[test]
fn test_copy_file_with_invalid_user_id_type() {
let config = Config::default();
let request = CopyFileRequest::new(config, "file_token", "name", "docx", "folder_token")
.user_id_type("invalid_type");
assert_eq!(request.user_id_type, Some("invalid_type".to_string()));
}
#[test]
fn test_copy_file_with_extra_empty_key() {
let config = Config::default();
let _request = CopyFileRequest::new(config, "file_token", "name", "docx", "folder_token")
.extra(vec![Property {
key: "".to_string(),
value: "value".to_string(),
}]);
}
#[test]
fn test_copied_file_structure() {
let file_data = CopiedFile {
token: "doxcnUkUOWtOelpFcha2Zabcef".to_string(),
name: "Demo copy".to_string(),
r#type: "docx".to_string(),
parent_token: Some("fldbcO1UuPz8VwnpPx5a92abcef".to_string()),
url: Some("https://feishu.cn/docx/doxcnUkUOWtOelpFcha2Zabcef".to_string()),
shortcut_info: None,
created_time: None,
modified_time: None,
owner_id: None,
};
assert_eq!(file_data.token, "doxcnUkUOWtOelpFcha2Zabcef");
assert_eq!(file_data.name, "Demo copy");
assert_eq!(file_data.r#type, "docx");
}
#[test]
fn test_response_trait() {
assert_eq!(CopyFileResponse::data_format(), ResponseFormat::Data);
}
}