Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ディレクトリを再帰的に移動する #42

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MachineId> = Lazy::new(|| MachineId::create_random());

#[tokio::main]
async fn main() {
let args: Args = Args::parse();
Expand Down Expand Up @@ -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;
}
}
Expand Down
31 changes: 28 additions & 3 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -227,7 +252,7 @@ pub struct Record {
pub last_update_by: Option<UserId>,
#[serde(rename = "lastModifyingMachineId", default)]
// Essential Toolsだと欠けている
pub last_update_machine: Option<String>,
pub last_update_machine: Option<MachineId>,
pub name: String,
pub record_type: RecordType,
#[serde(default)]
Expand All @@ -246,9 +271,9 @@ pub struct Record {
pub thumbnail_uri: Option<Url>,
#[serde(rename = "creationTime", default)]
// Essential Toolsだと欠けている
created_at: Option<DateTime<Utc>>,
pub created_at: Option<DateTime<Utc>>,
#[serde(rename = "lastModificationTime", deserialize_with = "fallback_to_utc")]
updated_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub random_order: i32,
pub visits: i32,
pub rating: f64,
Expand Down
234 changes: 187 additions & 47 deletions src/operation.rs
Original file line number Diff line number Diff line change
@@ -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, RecordType, UserId, UserLoginPostBody, UserLoginPostResponse};
use crate::model::{AuthorizationInfo, DirectoryMetadata, LoginResponse, Record, RecordId, RecordOwner, RecordType, SessionState, UserId, UserLoginPostBody, UserLoginPostResponse};

pub struct Operation;

Expand Down Expand Up @@ -127,7 +129,15 @@ impl Operation {
res
}

pub async fn move_record(owner_id: UserId, record_id: RecordId, to: Vec<String>, authorization_info: &Option<AuthorizationInfo>, keep_record_id: bool) {
#[async_recursion]
pub async fn move_record(
owner_id: UserId,
record_id: RecordId,
new_base_directory: Vec<String>,
authorization_info: &Option<AuthorizationInfo>,
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;

Expand All @@ -143,6 +153,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);
Expand All @@ -160,51 +232,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");
}
Expand Down Expand Up @@ -247,4 +274,117 @@ impl Operation {
}
}
}

pub async fn insert_record(owner_id: UserId, path: Vec<String>, mut record: Record, authorization_info: &Option<AuthorizationInfo>) {
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<String>, name: String, authorization_info: &Option<AuthorizationInfo>, 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<String> {
#[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::<PartialUserResponse>()
.await
.expect("Failed to deserialize response")
.user_name;

Some(v)
}
404 => {
error!("User not found");
None
}
other => {
warn!("Unhandled: {other}");
None
}
}
}
}