From ad14569ccc6579e3b7948475ebb8ce0fdbb3d847 Mon Sep 17 00:00:00 2001 From: Kisaragi Marine Date: Mon, 1 Aug 2022 02:14:23 +0900 Subject: [PATCH] feat: recursive move (stub) --- src/main.rs | 11 ++- src/model.rs | 31 ++++++- src/operation.rs | 234 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 224 insertions(+), 52 deletions(-) diff --git a/src/main.rs b/src/main.rs index 454fe3a..846402f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,17 @@ use std::io::stdin; use std::process::exit; use clap::Parser; use log::{debug, error, info, warn}; +use once_cell::sync::Lazy; use crate::cli::{Args, LogLevel, ToolSubCommand}; -use crate::model::{AuthorizationInfo, LoginInfo, SessionToken}; +use crate::model::{AuthorizationInfo, LoginInfo, MachineId, SessionState, SessionToken}; use crate::operation::Operation; mod operation; mod model; mod cli; +static MACHINE_ID_IN_THIS_SESSION: Lazy = Lazy::new(|| MachineId::create_random()); + #[tokio::main] async fn main() { let args: Args = Args::parse(); @@ -84,7 +87,11 @@ async fn main() { record_id.clone(), to.clone(), &authorization_info, - args.keep_record_id + args.keep_record_id, + SessionState { + machine_id: MACHINE_ID_IN_THIS_SESSION.clone(), + user_name: Operation::lookup_user_name(owner_id).await.expect("The user id does not exist"), + }, ).await; } } diff --git a/src/model.rs b/src/model.rs index b6835f9..1348fa4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -27,6 +27,12 @@ impl FromStr for UserId { /// This is thin pointer to the actual Record. It is unique, and has one-by-one relation with Record. pub struct RecordId(pub String); +impl RecordId { + pub fn make_random() -> Self { + Self(Uuid::new_v4().to_string().to_lowercase()) + } +} + #[derive(Display, Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] pub struct GroupId(String); @@ -163,6 +169,25 @@ impl AuthorizationInfo { } } +#[derive(Debug, Clone)] +pub struct SessionState { + pub machine_id: MachineId, + pub user_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachineId(String); + +impl MachineId { + pub fn create_random() -> Self { + let random_uuid = Uuid::new_v4().to_string(); + let random_uuid = random_uuid.as_bytes(); + let nonce = base64::encode_config(random_uuid, base64::URL_SAFE_NO_PAD).to_lowercase(); + + Self(nonce) + } +} + #[derive(Debug, Clone)] pub struct LoginResponse { pub using_token: AuthorizationInfo, @@ -227,7 +252,7 @@ pub struct Record { pub last_update_by: Option, #[serde(rename = "lastModifyingMachineId", default)] // Essential Toolsだと欠けている - pub last_update_machine: Option, + pub last_update_machine: Option, pub name: String, pub record_type: RecordType, #[serde(default)] @@ -246,9 +271,9 @@ pub struct Record { pub thumbnail_uri: Option, #[serde(rename = "creationTime", default)] // Essential Toolsだと欠けている - created_at: Option>, + pub created_at: Option>, #[serde(rename = "lastModificationTime", deserialize_with = "fallback_to_utc")] - updated_at: DateTime, + pub updated_at: DateTime, pub random_order: i32, pub visits: i32, pub rating: f64, diff --git a/src/operation.rs b/src/operation.rs index 64175c8..72fdb68 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,9 +1,11 @@ use reqwest::header::AUTHORIZATION; use log::{debug, error, info, warn}; use async_recursion::async_recursion; +use chrono::Utc; +use reqwest::Client; use uuid::Uuid; use crate::LoginInfo; -use crate::model::{AuthorizationInfo, DirectoryMetadata, LoginResponse, Record, RecordId, UserId, UserLoginPostBody, UserLoginPostResponse}; +use crate::model::{AuthorizationInfo, DirectoryMetadata, LoginResponse, Record, RecordId, RecordOwner, RecordType, SessionState, UserId, UserLoginPostBody, UserLoginPostResponse}; pub struct Operation; @@ -127,7 +129,15 @@ impl Operation { res } - pub async fn move_record(owner_id: UserId, record_id: RecordId, to: Vec, authorization_info: &Option, keep_record_id: bool) { + #[async_recursion] + pub async fn move_record( + owner_id: UserId, + record_id: RecordId, + new_base_directory: Vec, + authorization_info: &Option, + keep_record_id: bool, + session_state: SessionState + ) { let client = reqwest::Client::new(); let find = Self::get_record(owner_id.clone(), record_id.clone(), authorization_info).await; @@ -136,6 +146,68 @@ impl Operation { let from = (&found_record.path).clone(); + // region insert + { + debug!("insert!"); + if found_record.record_type == RecordType::Directory { + Self::create_directory(owner_id.clone(), new_base_directory.clone(), found_record.name.clone(), authorization_info, session_state.clone()).await; + let dig_into_dir = { + let mut d = new_base_directory.clone(); + d.push(found_record.name.clone()); + d + }; + let child_items = Self::get_directory_items(owner_id.clone(), from.split('\\').map(|a| a.to_string()).collect(), authorization_info).await; + for child in child_items { + // FIXME: infinite recursion + Self::move_record( + owner_id.clone(), + child.id, + dig_into_dir.clone(), + authorization_info, + keep_record_id, + session_state.clone() + ).await; + } + } else { + let record_id = { + // GUIDは小文字が「推奨」されているため念の為小文字にしておく + let record_id = RecordId(format!("R-{}", Uuid::new_v4().to_string().to_lowercase())); + debug!("new record id: {record_id}", record_id = &record_id); + record_id + }; + + let endpoint = format!("{BASE_POINT}/users/{owner_id}/records/{record_id}", owner_id = &owner_id, record_id = &record_id); + debug!("endpoint: {endpoint}", endpoint = &endpoint); + let mut req = client.put(endpoint); + + if let Some(authorization_info) = authorization_info { + debug!("auth set"); + req = req.header(reqwest::header::AUTHORIZATION, authorization_info.as_authorization_header_value()); + } + + let mut record = found_record.clone(); + record.path = new_base_directory.join("\\"); + record.id = record_id.clone(); + + debug!("requesting..."); + let res = req + .json(&record) + .send() + .await + .unwrap(); + if res.status().is_success() { + info!("Success! {record_id} for {owner_id} was moved from {from} to {to}.", to = new_base_directory.join("\\"), record_id = &record_id); + } else if res.status().is_client_error() { + error!("Client error ({status}): this is fatal bug. Please report this to bug tracker.", status = res.status()); + } else if res.status().is_server_error() { + error!("Server error ({status}): Please try again in later.", status = res.status()); + } else { + warn!("Unhandled status code: {status}", status = res.status()) + } + debug!("Response: {res:?}", res = &res); + } + } + // endregion // region delete old record { let endpoint = format!("{BASE_POINT}/users/{owner_id}/records/{record_id}", owner_id = &owner_id); @@ -153,51 +225,6 @@ impl Operation { debug!("deleted: {deleted:?}"); } // endregion - // region insert - { - debug!("insert!"); - let record_id = if keep_record_id { - debug!("record id unchanged"); - record_id - } else { - // GUIDは小文字が「推奨」されているため念の為小文字にしておく - let record_id = RecordId(format!("R-{}", Uuid::new_v4().to_string().to_lowercase())); - debug!("new record id: {record_id}", record_id = &record_id); - record_id - }; - - let endpoint = format!("{BASE_POINT}/users/{owner_id}/records/{record_id}", owner_id = &owner_id, record_id = &record_id); - debug!("endpoint: {endpoint}", endpoint = &endpoint); - let mut req = client.put(endpoint); - - if let Some(authorization_info) = authorization_info { - debug!("auth set"); - req = req.header(reqwest::header::AUTHORIZATION, authorization_info.as_authorization_header_value()); - } - - let mut record = found_record.clone(); - record.path = to.join("\\"); - record.id = record_id.clone(); - - debug!("requesting..."); - let res = req - .json(&record) - .send() - .await - .unwrap(); - if res.status().is_success() { - info!("Success! {record_id} for {owner_id} was moved from {from} to {to}.", to = to.join("\\"), record_id = &record_id); - } else if res.status().is_client_error() { - error!("Client error ({status}): this is fatal bug. Please report this to bug tracker.", status = res.status()); - // TODO: rollback - } else if res.status().is_server_error() { - error!("Server error ({status}): Please try again in later.", status = res.status()); - } else { - warn!("Unhandled status code: {status}", status = res.status()) - } - debug!("Response: {res:?}", res = &res); - } - // endregion } else { warn!("not found"); } @@ -240,4 +267,117 @@ impl Operation { } } } + + pub async fn insert_record(owner_id: UserId, path: Vec, mut record: Record, authorization_info: &Option) { + debug!("Preparing insert record for {owner_id}, to {path}. Content: {record:?}", owner_id = &owner_id, path = &path.join("\\")); + let new_record_id = { + // GUIDは小文字が「推奨」されているため念の為小文字にしておく + let record_id = RecordId(format!("R-{}", Uuid::new_v4().to_string().to_lowercase())); + debug!("new record id: {record_id}", record_id = &record_id); + record_id + }; + + let endpoint = format!("{BASE_POINT}/users/{owner_id}/records/{record_id}", owner_id = &owner_id, record_id = &new_record_id); + record.id = new_record_id.clone(); + + debug!("endpoint: {endpoint}", endpoint = &endpoint); + let client = Client::new(); + let mut req = client.put(endpoint); + + if let Some(authorization_info) = authorization_info { + debug!("auth set"); + req = req.header(reqwest::header::AUTHORIZATION, authorization_info.as_authorization_header_value()); + } + + debug!("requesting..."); + let res = req + .json(&record) + .send() + .await + .unwrap(); + if res.status().is_success() { + info!("Success! Created record with {new_record_id} for {owner_id}. Content: {record:?}"); + } else if res.status().is_client_error() { + error!("Client error ({status}): this is fatal bug. Please report this to bug tracker.", status = res.status()); + } else if res.status().is_server_error() { + error!("Server error ({status}): Please try again in later.", status = res.status()); + } else { + warn!("Unhandled status code: {status}", status = res.status()) + } + debug!("Response: {res:?}", res = &res); + } + + pub async fn create_directory(owner_id: UserId, base_dir: Vec, name: String, authorization_info: &Option, session: SessionState) -> RecordId { + let created_date = Utc::now(); + let id = RecordId::make_random(); + Self::insert_record(owner_id.clone(), { + let mut d = base_dir.clone(); + d.push(name.clone()); + d + }, Record { + id: id.clone(), + asset_uri: None, + global_version: 0, + local_version: 0, + last_update_by: Some(owner_id.clone()), + last_update_machine: Some(session.machine_id), + name, + record_type: RecordType::Directory, + owner_name: Some(session.user_name), + tags: vec![], + path: base_dir.join("\\"), + is_public: false, + is_for_patrons: false, + is_listed: false, + is_deleted: false, + thumbnail_uri: None, + created_at: Some(created_date), + updated_at: created_date, + random_order: 0, + visits: 0, + rating: 0.0, + owner_id: Some(RecordOwner::User(owner_id)), + submissions: vec![] + }, authorization_info).await; + + id + } + + pub async fn lookup_user_name(id: UserId) -> Option { + #[derive(serde::Deserialize)] + struct PartialUserResponse { + #[serde(rename = "username")] + user_name: String + } + + // see: https://github.com/PolyLogiX-Studio/NeosVR-API/issues/6 + let endpoint = format!("{BASE_POINT}/users/{id}?byUsername=false"); + let client = Client::new(); + let res = client.get(endpoint) + .send() + .await + .expect("HTTP connection issue"); + + let status_code = res.status(); + + match status_code.as_u16() { + 200 => { + let v = res + .json::() + .await + .expect("Failed to deserialize response") + .user_name; + + Some(v) + } + 404 => { + error!("User not found"); + None + } + other => { + warn!("Unhandled: {other}"); + None + } + } + } }