diff --git a/src/endpoints.rs b/src/endpoints.rs index e2b5a9f..052daf5 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -91,9 +91,44 @@ impl Client { .await .map_err(Into::into) } + + pub async fn file_detail( + &self, + req: types::request::FileDetailRequest, + ) -> anyhow::Result { + let client = reqwest::Client::new(); + let request = client + .post("https://forest.sendy.jp/cloud/service/file/v1/file") + .bearer_auth(&self.token) + .json(&req); + + let response = request.send().await?; + response + .json::() + .await + .map_err(Into::into) + } + + pub async fn delete_file( + &self, + req: types::request::DeleteFileRequest, + ) -> anyhow::Result { + let client = reqwest::Client::new(); + let request = client + .delete("https://forest.sendy.jp/cloud/service/file/v3/files") + .bearer_auth(&self.token) + .json(&req); + + let response = request.send().await?; + response + .json::() + .await + .map_err(Into::into) + } } // https://www.rakuten-drive.com/api/account/refreshtoken POST RefreshTokenRequest RefreshTokenResponse +// https://forest.sendy.jp/cloud/service/file/v1/file POST FileDetailRequest FileDetailResponse // https://forest.sendy.jp/cloud/service/file/v1/files POST ListFilesRequest ListFilesResponse // https://forest.sendy.jp/cloud/service/file/v3/files DELETE DeleteFileRequest JobKeyResponse // https://forest.sendy.jp/cloud/service/file/v1/files/create POST CreateFolderRequest diff --git a/src/main.rs b/src/main.rs index c5b8307..edcf86e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use types::response::ListFilesResponseFile; mod endpoints; mod types; -const BEARER_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxNTQwYWM3MWJiOTJhYTA2OTNjODI3MTkwYWNhYmU1YjA1NWNiZWMiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoi5bm457-8IOW_l-adkSIsInBsYW4iOiJza2YiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc2VuZHktc2VydmljZSIsImF1ZCI6InNlbmR5LXNlcnZpY2UiLCJhdXRoX3RpbWUiOjE3MjEyMjYwMTUsInVzZXJfaWQiOiJHY2xUN0RybkxGaG83dm5JaXJVemp0TUxoUmsyIiwic3ViIjoiR2NsVDdEcm5MRmhvN3ZuSWlyVXpqdE1MaFJrMiIsImlhdCI6MTcyMTI1NTM1OCwiZXhwIjoxNzIxMjU4OTU4LCJlbWFpbCI6ImtvdXN1a2UxMTIzNjEyNEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsia291c3VrZTExMjM2MTI0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.uC-X4XCMTJ-Vv0bmm85cZy65LVdNxRKBlNXxsg8_QqyMV1rRzmpDMQpwWKk10OUDj6xovg1tfmlUW2syL0twANO8hKOSlI_wLZ1Rvvm0TF8EvDLvv8OGFc93nm3OIaSaiZj-xcORZzeJDVHsdraGoYDX3YbYPIJAhDaOsHX5_QbLwuxoz0dxd0fTAoDH7aEpDhcojjTmMImtbGqMzpvUpwNunJaJK2YZTYiHXZtcK7mr9cQLF5b3Exee--R5hGEU9E49jGtXKQNrP_6mkTXVivJh6TdKeFiMCrbc-6xZvuBnkEQ8g0GvU9cERhJTZ73U2jdHLzWbYitCh2nzkbQDNA"; +const BEARER_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxNTQwYWM3MWJiOTJhYTA2OTNjODI3MTkwYWNhYmU1YjA1NWNiZWMiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoi5bm457-8IOW_l-adkSIsInBsYW4iOiJza2YiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc2VuZHktc2VydmljZSIsImF1ZCI6InNlbmR5LXNlcnZpY2UiLCJhdXRoX3RpbWUiOjE3MjEyMjYwMTUsInVzZXJfaWQiOiJHY2xUN0RybkxGaG83dm5JaXJVemp0TUxoUmsyIiwic3ViIjoiR2NsVDdEcm5MRmhvN3ZuSWlyVXpqdE1MaFJrMiIsImlhdCI6MTcyMTI2MDk2NSwiZXhwIjoxNzIxMjY0NTY1LCJlbWFpbCI6ImtvdXN1a2UxMTIzNjEyNEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsia291c3VrZTExMjM2MTI0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.qgMyyPkP992xCtsTjr28PhzjiqaNBn1O1Z6HYEk3cjgPyVoPRiMV5KaDuWWveP0Z2x5_jvd5We0zsYzpxnOcf1dlf4VmAUqNGrVjCRqWUBuU008EkTAFlKBZAk4yYON0xddNcoUR6OrmYSLKMR5OOOV9FkTbFzd72rydQpcbiy9nmint_uXnN3z_9th0yf0J8oBd4_aXUzGUw2YOG8mBlgJfAoFdO5gMxJCWkC2V9r1TbxYWDMrFwtj7QMLVN4TOz2Bcy8erPiA_T46ap2gc9T0wTaPrE8h436FkTLdiaSJaYBLMEbS7dtXNLZ7SxaA4JOfeIgt2KoN5BrZg4qqt4Q"; const HOST_ID: &str = "GclT7DrnLFho7vnIirUzjtMLhRk2"; const CHUNK_SIZE: usize = 1024 * 1024 * 10; // 10MB const APP_VERSION: &str = "v21.11.10"; @@ -38,7 +38,6 @@ enum Commands { prefix: Option, }, Upload { - #[clap(short, long)] file: PathBuf, #[clap(short, long)] prefix: Option, @@ -46,13 +45,16 @@ enum Commands { recursive: bool, }, Download { - #[clap(short, long)] path: String, #[clap(long)] prefix: Option, }, Move {}, - Delete {}, + Delete { + path: String, + #[clap(long)] + recursive: bool, + }, MkDir {}, } @@ -68,7 +70,7 @@ async fn main() { match &args.command { Commands::List { prefix } => { - let res = list_files(prefix.clone()).await.unwrap(); + let res = list_files(Some(&prefix.clone().unwrap_or("".to_string()))).await.unwrap(); res.file.iter().for_each(|f| { let permission_string = if f.is_folder { "d" } else { "-" }; println!( @@ -243,7 +245,7 @@ async fn main() { let file_path = path.split('/').collect::>()[0..path.split('/').count() - 1].join("/"); - let list = list_files(Some(file_path.clone())).await.unwrap(); + let list = list_files(Some(&file_path)).await.unwrap(); let file = list .file @@ -286,8 +288,40 @@ async fn main() { Commands::Move {} => { println!("Move"); } - Commands::Delete {} => { - println!("Delete"); + Commands::Delete { path, recursive } => { + let client = endpoints::Client::new(BEARER_TOKEN.to_string(), HOST_ID.to_string()); + let file = file_detail(path).await.unwrap(); + if file.is_folder && !*recursive { + println!("Use --recursive option for folder delete"); + return; + } + let req = types::request::DeleteFileRequest { + file: vec![types::request::FileModifyRequestFile { + last_modified: file.last_modified, + path: file.path, + version_id: file.version_id, + size: file.size, + }], + host_id: client.host_id.clone(), + prefix: "".to_string(), + trash: true, + }; + let res = client.delete_file(req).await.unwrap(); + + loop { + let req = types::request::CheckActionRequest { + key: res.key.clone(), + }; + let res = client.check_action(req).await.unwrap(); + + if res.state == "complete" { + break; + } + + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + println!("Deleted"); } Commands::MkDir {} => { println!("MkDir"); @@ -440,14 +474,25 @@ async fn multipart_upload( Ok(()) } -async fn list_files(prefix: Option) -> anyhow::Result { +async fn file_detail(path: &str) -> anyhow::Result { + let client = endpoints::Client::new(BEARER_TOKEN.to_string(), HOST_ID.to_string()); + let req = types::request::FileDetailRequest { + host_id: client.host_id.clone(), + path: path.to_string(), + thumbnail_size: 130, + }; + let res = client.file_detail(req).await?; + Ok(res.file) +} + +async fn list_files(prefix: Option<&str>) -> anyhow::Result { let client = endpoints::Client::new(BEARER_TOKEN.to_string(), HOST_ID.to_string()); let pagination_size = 40; let mut files = Vec::::new(); let req = types::request::ListFilesRequest { from: 0, host_id: client.host_id.clone(), - path: prefix.clone().unwrap_or("".to_string()), + path: prefix.clone().unwrap_or("").to_string(), sort_type: "path".to_string(), reverse: false, thumbnail_size: 130, @@ -463,7 +508,7 @@ async fn list_files(prefix: Option) -> anyhow::Result, + pub file: Vec, pub host_id: String, pub name: String, pub path: String, @@ -37,7 +37,7 @@ pub struct RenameFileRequest { #[derive(Debug, Serialize, Deserialize)] -pub struct RenameFileRequestFile { +pub struct FileModifyRequestFile { pub last_modified: String, // 1970-01-20T22:07:12.804Z pub path: String, pub size: i64, @@ -53,7 +53,7 @@ pub struct CheckActionRequest { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct MoveFileRequest { - pub file: Vec, + pub file: Vec, pub host_id: String, pub prefix: String, pub path: String, @@ -96,7 +96,7 @@ pub struct CompleteUploadRequestFile { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct DeleteFileRequest { - pub file: Vec, + pub file: Vec, pub host_id: String, pub prefix: String, pub trash: bool, @@ -117,8 +117,10 @@ pub struct GetFileLinkRequestFile { pub size: i64, } -// #[derive(Debug, Serialize, Deserialize)] -// pub struct GetFileLinkRequest { -// pub host_id: String, -// pub path: String, -// } \ No newline at end of file +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct FileDetailRequest { + pub host_id: String, + pub path: String, + pub thumbnail_size: i64, +} \ No newline at end of file diff --git a/src/types/response.rs b/src/types/response.rs index fc6dca7..f4e06fe 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -108,4 +108,43 @@ pub struct GetFileLinkTokenResponse { #[serde(rename_all = "snake_case")] pub struct GetFileLinkResponse { pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct FileDetailResponse { + pub access_level: String, + pub file: FileDetailResponseFile, + pub owner: String, + pub prefix: String, + pub usage_size: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct FileDetailResponseFile { + pub access_level: String, + pub has_child_folder: bool, + + #[serde(rename = "HostID")] + pub host_id: String, + pub is_backed_up: bool, + pub is_folder: bool, + pub is_latest: bool, + pub is_share: String, + pub items_count: i64, + pub last_modified: String, // 2024-07-16T06:18:06.595Z + + #[serde(rename = "LastModifierID")] + pub last_modifier_id: String, + + #[serde(rename = "OwnerID")] + pub owner_id: String, // OwnerID + pub path: String, + pub size: i64, + pub thumbnail: String, + pub version: serde_json::Value, // returns null + + #[serde(rename = "VersionID")] + pub version_id: String, } \ No newline at end of file