diff --git a/Cargo.lock b/Cargo.lock index dc9210d..cdbc1b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,7 +954,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pmv-cli" -version = "1.8.0" +version = "2.0.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index d45a5bc..0d6a5a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ description = "Command line interface client for PersonalMediaVault" edition = "2021" license = "MIT" name = "pmv-cli" -version = "1.8.0" +version = "2.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/LICENSE b/LICENSE index c25a36d..9d04c6b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 Agustin San Roman +Copyright (c) 2023-2025 Agustin San Roman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/MANUAL.md b/MANUAL.md index 5958350..bd39e55 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -26,6 +26,7 @@ pmv-cli [OPTIONS] | [invites](#command-invites) | Manages invites | | [batch](#command-batch) | Applies a batch operation to a list of media assets | | [get-server-information](#command-get-server-information) | Gets server information, like the version it is using | +| [get-disk-usage](#command-get-disk-usage) | Gets server disk usage | **Options:** @@ -54,6 +55,7 @@ pmv-cli login [OPTIONS] | `-U, --username ` | Vault username. You can also specify the credentials in the URL | | `-D, --duration ` | Session duration. Can be: day, week, month or year | | `-I, --invite-code ` | Invite code. Setting this option will ignore the credentials and use the code | +| `-T, --tfa-code ` | Two factor authentication code | | `-h, --help` | Print help | ## Command: logout @@ -89,7 +91,12 @@ pmv-cli account | [context](#command-account-context) | Prints account context to the standard output | | [change-username](#command-account-change-username) | Changes username (only for root account) | | [change-password](#command-account-change-password) | Changes account password | -| [list](#command-account-list) | List accounts | +| [get-security-settings](#command-account-get-security-settings) | Gets account security settings | +| [set-auth-confirmation](#command-account-set-auth-confirmation) | Sets auth confirmation options | +| [get-totp-settings](#command-account-get-totp-settings) | Gets TOTP settings in order to enable two factor authentication | +| [enable-tfa](#command-account-enable-tfa) | Enables two factor authentication | +| [disable-tfa](#command-account-disable-tfa) | Disables two factor authentication | +| [list](#command-account-list) | Lists accounts | | [create](#command-account-create) | Creates new account | | [update](#command-account-update) | Updates an account | | [delete](#command-account-delete) | Deletes an existing account | @@ -154,9 +161,112 @@ pmv-cli account change-password | --- | --- | | `-h, --help` | Print help | +### Command: account get-security-settings + +Gets account security settings + +**Usage:** + +``` +pmv-cli account get-security-settings +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: account set-auth-confirmation + +Sets auth confirmation options + +**Usage:** + +``` +pmv-cli account set-auth-confirmation [OPTIONS] +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Set to 'true' to enable auth confirmation, Set it to 'false' to disable it | + +**Options:** + +| Option | Description | +| --- | --- | +| `--prefer-password` | | +| `Prefer using the account password instead of two factor authentication` | | +| `--period-seconds ` | | +| `Period (seconds) to remember the last auth confirmation` | | +| `-h, --help` | | +| `Print help` | | + +### Command: account get-totp-settings + +Gets TOTP settings in order to enable two factor authentication + +**Usage:** + +``` +pmv-cli account get-totp-settings [OPTIONS] +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `--issuer ` | TOTP issuer (to be added th the URL) | +| `--account ` | TOTP account (to be added th the URL) | +| `--algorithm ` | Hashing algorithm (sha-1, sha-256 or sha-512) | +| `--period ` | TOTP period (30s, 60s or 120s) | +| `--allow-skew` | Allows clock skew of 1 period | +| `-h, --help` | Print help | + +### Command: account enable-tfa + +Enables two factor authentication + +**Usage:** + +``` +pmv-cli account enable-tfa +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Two factor authentication method (from the settings command result) | +| `` | Two factor authentication secret | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: account disable-tfa + +Disables two factor authentication + +**Usage:** + +``` +pmv-cli account disable-tfa +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + ### Command: account list -List accounts +Lists accounts **Usage:** @@ -1467,6 +1577,8 @@ pmv-cli config | [set-max-tasks](#command-config-set-max-tasks) | Sets max tasks in parallel | | [set-encoding-threads](#command-config-set-encoding-threads) | Sets number of encoding threads to use | | [set-video-previews-interval](#command-config-set-video-previews-interval) | Sets the video previews interval in seconds | +| [set-max-invites](#command-config-set-max-invites) | Sets the max number of invited sessions by user | +| [set-preserve-originals](#command-config-set-preserve-originals) | Sets the option to preserve original files, before encoding, as an attachment | | [set-css](#command-config-set-css) | Sets custom CSS for the vault | | [clear-css](#command-config-clear-css) | Clears custom CSS for the vault | | [add-video-resolution](#command-config-add-video-resolution) | Adds video resolution | @@ -1600,6 +1712,50 @@ pmv-cli config set-video-previews-interval | --- | --- | | `-h, --help` | Print help | +### Command: config set-max-invites + +Sets the max number of invited sessions by user + +**Usage:** + +``` +pmv-cli config set-max-invites +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Max number of invited sessions by user | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: config set-preserve-originals + +Sets the option to preserve original files, before encoding, as an attachment + +**Usage:** + +``` +pmv-cli config set-preserve-originals [PRESERVE_ORIGINALS] +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `[PRESERVE_ORIGINALS]` | Preserve original media, before encoding, as an attachment? | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + ### Command: config set-css Sets custom CSS for the vault @@ -2094,3 +2250,19 @@ pmv-cli get-server-information | Option | Description | | --- | --- | | `-h, --help` | Print help | + +## Command: get-disk-usage + +Gets server disk usage + +**Usage:** + +``` +pmv-cli get-disk-usage +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | diff --git a/build-production.bat b/build-production.bat index 60bf17d..7e98106 100644 --- a/build-production.bat +++ b/build-production.bat @@ -1,3 +1,5 @@ @echo off call cargo build --release + +call cp -f target/release/pmv-cli.exe pmv-cli.exe diff --git a/src/api/about.rs b/src/api/about.rs index c3bc0d7..a681d45 100644 --- a/src/api/about.rs +++ b/src/api/about.rs @@ -1,7 +1,7 @@ // About API use crate::{ - models::ServerInformation, + models::{ServerDiskUsage, ServerInformation}, tools::{do_get_request, RequestError, VaultURI}, }; @@ -22,3 +22,21 @@ pub async fn api_call_about( Ok(parsed_body.unwrap()) } + +pub async fn api_call_disk_usage( + url: &VaultURI, + debug: bool, +) -> Result { + let body_str = do_get_request(url, "/api/about/disk_usage".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} diff --git a/src/api/account.rs b/src/api/account.rs index e7c1d12..20dd8b0 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -2,7 +2,7 @@ use crate::{ models::{ - AccountContext, AccountCreateBody, AccountDeleteBody, AccountListItem, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody + AccountContext, AccountCreateBody, AccountDeleteBody, AccountListItem, AccountSecuritySettings, AccountSetSecuritySettingsBody, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody, TfaDisableBody, TimeOtpEnableBody, TimeOtpOptions, TimeOtpSettings }, tools::{do_get_request, do_post_request, RequestError, VaultURI}, }; @@ -119,3 +119,109 @@ pub async fn api_call_delete_account( Ok(()) } + + +pub async fn api_call_get_security_settings( + url: &VaultURI, + debug: bool, +) -> Result { + let body_str = do_get_request(url, "/api/account/security".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + +pub async fn api_call_set_security_settings( + url: &VaultURI, + req_body: AccountSetSecuritySettingsBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_get_totp_settings( + url: &VaultURI, + options: TimeOtpOptions, + debug: bool, +) -> Result { + let mut url_path = "/api/account/security/tfa/totp".to_string(); + + url_path.push_str(&("?algorithm=".to_owned() + &urlencoding::encode(&options.algorithm.to_string()))); + url_path.push_str(&("&period=".to_owned() + &urlencoding::encode(&options.period.to_string()))); + + if let Some(issuer) = options.issuer { + url_path.push_str(&("&issuer=".to_owned() + &urlencoding::encode(&issuer))); + } + + if let Some(account) = options.account { + url_path.push_str(&("&account=".to_owned() + &urlencoding::encode(&account))); + } + + if options.skew { + url_path.push_str("&skew=allow"); + } else { + url_path.push_str("&skew=disallow"); + } + + let body_str = do_get_request(url, "/api/account/security".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + + +pub async fn api_call_enable_totp( + url: &VaultURI, + req_body: TimeOtpEnableBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security/tfa/totp".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_disable_tfa( + url: &VaultURI, + req_body: TfaDisableBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security/tfa/disable".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/api/media.rs b/src/api/media.rs index d476729..cf5609f 100644 --- a/src/api/media.rs +++ b/src/api/media.rs @@ -7,8 +7,7 @@ use crate::{ ImageNote, MediaAssetSizeStats, MediaAttachment, MediaAudioTrack, MediaMetadata, MediaRenameAttachmentBody, MediaRenameSubtitleOrAudioBody, MediaResolution, MediaSubtitle, MediaTimeSlice, MediaUpdateDescriptionBody, MediaUpdateExtendedDescriptionBody, MediaUpdateExtraBody, MediaUpdateThumbnailResponse, MediaUpdateTitleBody, MediaUploadResponse, TaskEncodeResolution }, tools::{ - do_get_request, do_multipart_upload_request, do_post_request, ProgressReceiver, - RequestError, VaultURI, + do_get_request, do_multipart_upload_request, do_multipart_upload_request_with_confirmation, do_post_request, ProgressReceiver, RequestError, VaultURI }, }; @@ -270,7 +269,7 @@ pub async fn api_call_media_replace( debug: bool, progress_receiver: Arc>, ) -> Result<(), RequestError> { - do_multipart_upload_request( + do_multipart_upload_request_with_confirmation( url, format!("/api/media/{media}/replace"), "file".to_string(), diff --git a/src/commands/account.rs b/src/commands/account.rs index 3152635..dc1954a 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -7,15 +7,18 @@ use clap::Subcommand; use crate::{ api::{ api_call_change_password, api_call_change_username, api_call_context, - api_call_create_account, api_call_delete_account, api_call_list_accounts, - api_call_update_account, + api_call_create_account, api_call_delete_account, api_call_disable_tfa, + api_call_enable_totp, api_call_get_security_settings, api_call_get_totp_settings, + api_call_list_accounts, api_call_set_security_settings, api_call_update_account, }, models::{ - AccountCreateBody, AccountDeleteBody, AccountUpdateBody, ChangePasswordBody, - ChangeUsernameBody, + AccountCreateBody, AccountDeleteBody, AccountSetSecuritySettingsBody, AccountUpdateBody, + ChangePasswordBody, ChangeUsernameBody, TfaDisableBody, TimeOtpAlgorithm, + TimeOtpEnableBody, TimeOtpOptions, TimeOtpPeriod, }, tools::{ - ask_user, ask_user_password, ensure_login, parse_vault_uri, print_table, to_csv_string, + ask_user, ask_user_password, ensure_login, parse_vault_uri, print_table, + request_auth_confirmation_password, request_auth_confirmation_tfa, to_csv_string, }, }; @@ -35,7 +38,59 @@ pub enum AccountCommand { /// Changes account password ChangePassword, - /// List accounts + /// Gets account security settings + GetSecuritySettings, + + /// Sets auth confirmation options + SetAuthConfirmation { + /// Set to 'true' to enable auth confirmation, Set it to 'false' to disable it + auth_confirmation: String, + + /// Prefer using the account password instead of two factor authentication + #[arg(long)] + prefer_password: bool, + + /// Period (seconds) to remember the last auth confirmation + #[arg(long)] + period_seconds: Option, + }, + + /// Gets TOTP settings in order to enable two factor authentication + GetTotpSettings { + /// TOTP issuer (to be added th the URL) + #[arg(long)] + issuer: Option, + + /// TOTP account (to be added th the URL) + #[arg(long)] + account: Option, + + /// Hashing algorithm (sha-1, sha-256 or sha-512) + #[arg(long)] + algorithm: Option, + + /// TOTP period (30s, 60s or 120s) + #[arg(long)] + period: Option, + + /// Allows clock skew of 1 period + #[arg(long)] + allow_skew: bool, + }, + + /// Enables two factor authentication + EnableTfa { + /// Two factor authentication method (from the settings command result) + method: String, + + /// Two factor authentication secret + secret: String, + }, + + /// Disables two factor authentication + DisableTfa, + + /// Lists accounts #[clap(alias("ls"))] List { /// CSV format @@ -104,6 +159,38 @@ pub async fn run_account_cmd(global_opts: CommandGlobalOptions, cmd: AccountComm AccountCommand::Delete { username } => { run_cmd_delete_account(global_opts, username).await; } + AccountCommand::GetSecuritySettings => { + run_cmd_get_account_security(global_opts).await; + } + AccountCommand::SetAuthConfirmation { + auth_confirmation, + prefer_password, + period_seconds, + } => { + run_cmd_set_account_security( + global_opts, + auth_confirmation, + prefer_password, + period_seconds, + ) + .await; + } + AccountCommand::GetTotpSettings { + issuer, + account, + algorithm, + period, + allow_skew, + } => { + run_cmd_get_totp_settings(global_opts, issuer, account, algorithm, period, allow_skew) + .await; + } + AccountCommand::EnableTfa { method, secret } => { + run_cmd_enable_tfa(global_opts, method, secret).await; + } + AccountCommand::DisableTfa => { + run_cmd_disable_tfa(global_opts).await; + } } } @@ -777,3 +864,441 @@ pub async fn run_cmd_delete_account(global_opts: CommandGlobalOptions, username: } } } + +pub async fn run_cmd_get_account_security(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_security_settings(&vault_url, global_opts.debug).await; + + match api_res { + Ok(res) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + if res.auth_confirmation { + println!("Auth confirmation: Enabled"); + + if res.auth_confirmation_method == "pw" { + println!("Auth confirmation preferred method: Account password"); + } else { + println!("Auth confirmation preferred method: Two factor authentication"); + } + + println!( + "Auth confirmation period: {} seconds", + res.auth_confirmation_period_seconds + ); + } else { + println!("Auth confirmation: Disabled"); + } + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_set_account_security( + global_opts: CommandGlobalOptions, + auth_confirmation: String, + prefer_password: bool, + period_seconds: Option, +) { + let auth_confirmation_bool = match auth_confirmation.to_lowercase().as_str() { + "0" | "false" | "no" => false, + "1" | "true" | "yes" => true, + _ => { + eprintln!("Invalid argument: Set it to TRUE or FALSE"); + process::exit(1); + } + }; + + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_set_security_settings( + &vault_url, + AccountSetSecuritySettingsBody { + auth_confirmation: auth_confirmation_bool, + auth_confirmation_method: if prefer_password { + "pw".to_string() + } else { + "tfa".to_string() + }, + auth_confirmation_period_seconds: period_seconds.unwrap_or(120), + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully updated account security settings"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_get_totp_settings( + global_opts: CommandGlobalOptions, + issuer: Option, + account: Option, + algorithm: Option, + period: Option, + allow_skew: bool, +) { + let hash_algorithm = match algorithm { + Some(a) => match TimeOtpAlgorithm::parse(&a) { + Ok(p) => p, + Err(_) => { + eprintln!("Invalid hash algorithm. Valid ones are: sha-1, sha-256, sha-512"); + process::exit(1); + } + }, + None => TimeOtpAlgorithm::Sha1, + }; + + let time_period = match period { + Some(a) => match TimeOtpPeriod::parse(&a) { + Ok(p) => p, + Err(_) => { + eprintln!("Invalid period. Valid ones are: 30s, 60s, 120s"); + process::exit(1); + } + }, + None => TimeOtpPeriod::P30, + }; + + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_totp_settings( + &vault_url, + TimeOtpOptions { + issuer, + account, + algorithm: hash_algorithm, + period: time_period, + skew: allow_skew, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(res) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + println!("Method: {}", res.method); + println!("Secret: {}", res.secret); + println!("URL: {}", res.url); + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_enable_tfa(global_opts: CommandGlobalOptions, method: String, secret: String) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Ask for confirmation + + let confirmation_pw = request_auth_confirmation_password().await; + let confirmation_tfa = request_auth_confirmation_tfa().await; + + // Call API + + let api_res = api_call_enable_totp( + &vault_url, + TimeOtpEnableBody { + method, + secret, + password: confirmation_pw, + code: confirmation_tfa, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully enabled two factor authentication"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_disable_tfa(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Ask for confirmation + + let confirmation_tfa = request_auth_confirmation_tfa().await; + + // Call API + + let api_res = api_call_disable_tfa( + &vault_url, + TfaDisableBody { + code: confirmation_tfa, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully disabled two factor authentication"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} diff --git a/src/commands/config.rs b/src/commands/config.rs index 435701b..93fe2e8 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -46,6 +46,18 @@ pub enum ConfigCommand { interval_seconds: i32, }, + /// Sets the max number of invited sessions by user + SetMaxInvites { + /// Max number of invited sessions by user + invite_limit: i32, + }, + + /// Sets the option to preserve original files, before encoding, as an attachment + SetPreserveOriginals { + /// Preserve original media, before encoding, as an attachment? + preserve_originals: bool, + }, + /// Sets custom CSS for the vault SetCSS { /// Path to the css file to use @@ -100,6 +112,12 @@ pub async fn run_config_cmd(global_opts: CommandGlobalOptions, cmd: ConfigComman ConfigCommand::SetVideoPreviewsInterval { interval_seconds } => { run_cmd_config_set_video_previews_interval(global_opts, interval_seconds).await; } + ConfigCommand::SetMaxInvites { invite_limit } => { + run_cmd_config_set_invite_limit(global_opts, invite_limit).await; + } + ConfigCommand::SetPreserveOriginals { preserve_originals } => { + run_cmd_config_set_preserve_originals(global_opts, preserve_originals).await; + } ConfigCommand::SetCSS { file_path } => { run_cmd_config_set_css(global_opts, file_path).await; } @@ -185,9 +203,24 @@ pub async fn run_cmd_config_get(global_opts: CommandGlobalOptions) { res_video_previews_interval = 3; } + let mut invite_limit = config.invite_limit; + + if invite_limit == 0 { + invite_limit = 10; + } + println!("Max tasks in parallel: {res_max_tasks}"); println!("Number of encoding threads: {res_encoding_threads}"); println!("Video previews interval: {res_video_previews_interval} seconds"); + println!("Max number of invited sessions by user: {invite_limit}"); + println!( + "Preserve original files, before encoding, as an attachment?: {}", + if config.preserve_originals { + "Yes" + } else { + "No" + } + ); if !config.resolutions.is_empty() { let list: Vec = config @@ -354,8 +387,7 @@ pub async fn run_cmd_config_set_title(global_opts: CommandGlobalOptions, title: // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -436,8 +468,7 @@ pub async fn run_cmd_config_set_max_tasks(global_opts: CommandGlobalOptions, max // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -521,8 +552,7 @@ pub async fn run_cmd_config_set_encoding_threads( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -606,8 +636,7 @@ pub async fn run_cmd_config_set_video_previews_interval( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -637,6 +666,175 @@ pub async fn run_cmd_config_set_video_previews_interval( } } +pub async fn run_cmd_config_set_invite_limit(global_opts: CommandGlobalOptions, invite_limit: i32) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Get config + + let api_res_get_conf = api_call_get_config(&vault_url, global_opts.debug).await; + + let current_config: VaultConfig = match api_res_get_conf { + Ok(config) => config, + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + }; + + // Changes + + let mut new_config = current_config.clone(); + + new_config.invite_limit = invite_limit; + + // Set config + + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; + + match api_res_set_conf { + Ok(_) => { + eprintln!( + "Successfully changed max number of invited sessions by user: {invite_limit}" + ); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_config_set_preserve_originals( + global_opts: CommandGlobalOptions, + preserve_originals: bool, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Get config + + let api_res_get_conf = api_call_get_config(&vault_url, global_opts.debug).await; + + let current_config: VaultConfig = match api_res_get_conf { + Ok(config) => config, + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + }; + + // Changes + + let mut new_config = current_config.clone(); + + new_config.preserve_originals = preserve_originals; + + // Set config + + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; + + match api_res_set_conf { + Ok(_) => { + eprintln!( + "Successfully changed the option to preserve original files: {preserve_originals}" + ); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + pub async fn run_cmd_config_set_css(global_opts: CommandGlobalOptions, file_path: String) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -720,8 +918,7 @@ pub async fn run_cmd_config_set_css(global_opts: CommandGlobalOptions, file_path // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -823,8 +1020,7 @@ pub async fn run_cmd_config_clear_css(global_opts: CommandGlobalOptions) { // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -953,8 +1149,7 @@ pub async fn run_cmd_config_add_video_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1084,8 +1279,7 @@ pub async fn run_cmd_config_remove_video_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1215,8 +1409,7 @@ pub async fn run_cmd_config_add_image_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1348,8 +1541,7 @@ pub async fn run_cmd_config_remove_image_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { diff --git a/src/commands/disk_usage.rs b/src/commands/disk_usage.rs new file mode 100644 index 0000000..e0f3beb --- /dev/null +++ b/src/commands/disk_usage.rs @@ -0,0 +1,82 @@ +// Server info command + +use crate::{ + api::api_call_disk_usage, + commands::logout::do_logout, + tools::{ensure_login, parse_vault_uri, render_size_bytes}, +}; + +use std::process; + +use super::{get_vault_url, print_request_error, CommandGlobalOptions}; + +pub async fn run_cmd_disk_usage(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_disk_usage(&vault_url, global_opts.debug).await; + + match api_res { + Ok(disk_usage) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + println!("Disk usage: {}", disk_usage.usage.round()); + println!("Available: {}", render_size_bytes(disk_usage.available)); + println!("Free: {}", render_size_bytes(disk_usage.free)); + println!("Total size: {}", render_size_bytes(disk_usage.total)); + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} diff --git a/src/commands/login.rs b/src/commands/login.rs index 66d8d80..b51cfc6 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -18,6 +18,7 @@ pub async fn run_cmd_login( username: Option, duration: Option, invite_code: Option, + tfa_code: Option, ) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -134,7 +135,7 @@ pub async fn run_cmd_login( } None => { let login_result = - ensure_login_ext(&vault_url, &username, &duration, global_opts.debug).await; + ensure_login_ext(&vault_url, &username, &None, &tfa_code, &duration, global_opts.debug, false).await; if login_result.is_err() { process::exit(1); diff --git a/src/commands/media_download.rs b/src/commands/media_download.rs index 1caf293..9f062c8 100644 --- a/src/commands/media_download.rs +++ b/src/commands/media_download.rs @@ -740,7 +740,7 @@ pub async fn download_media_asset( if path_parts.is_empty() { out_file = "download".to_string(); } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); out_file = last_part .split('?') .next() @@ -757,7 +757,7 @@ pub async fn download_media_asset( if path_parts.is_empty() { out_file = "download".to_string(); } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); out_file = last_part .split('?') .next() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0a76c29..39e3873 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -21,6 +21,9 @@ use batch_operation::*; mod config; use config::*; +mod disk_usage; +pub use disk_usage::*; + mod invites; use invites::*; @@ -40,12 +43,12 @@ mod media_export; mod media_extended_description; mod media_image_notes; mod media_import; +mod media_replace; mod media_resolutions; mod media_subtitles; mod media_thumbnail; mod media_time_slices; mod media_upload; -mod media_replace; mod random; use random::*; @@ -91,6 +94,10 @@ pub enum Commands { /// Invite code. Setting this option will ignore the credentials and use the code. #[arg(short = 'I', long)] invite_code: Option, + + /// Two factor authentication code + #[arg(short = 'T', long)] + tfa_code: Option, }, /// Closes the active session, given a session URL @@ -277,12 +284,21 @@ pub enum Commands { /// Gets server information, like the version it is using. #[clap(alias("server-info"))] GetServerInformation, + + /// Gets server disk usage. + #[clap(alias("disk-usage"))] + GetDiskUsage, } pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { match cmd { - Commands::Login { username, duration, invite_code } => { - run_cmd_login(global_opts, username, duration, invite_code).await; + Commands::Login { + username, + duration, + invite_code, + tfa_code, + } => { + run_cmd_login(global_opts, username, duration, invite_code, tfa_code).await; } Commands::Logout => { run_cmd_logout(global_opts).await; @@ -378,10 +394,13 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { } Commands::Invites { invites_cmd } => { run_invites_cmd(global_opts, invites_cmd).await; - }, + } Commands::GetServerInformation => { run_cmd_server_info(global_opts).await; - }, + } + Commands::GetDiskUsage => { + run_cmd_disk_usage(global_opts).await; + } } } diff --git a/src/models/about.rs b/src/models/about.rs index 4c46251..e303610 100644 --- a/src/models/about.rs +++ b/src/models/about.rs @@ -13,3 +13,19 @@ pub struct ServerInformation { #[serde(rename = "ffmpeg_version")] pub ffmpeg_version: String, } + + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerDiskUsage { + #[serde(rename = "usage")] + pub usage: f32, + + #[serde(rename = "available")] + pub available: u64, + + #[serde(rename = "free")] + pub free: u64, + + #[serde(rename = "total")] + pub total: u64, +} diff --git a/src/models/account.rs b/src/models/account.rs index fcc34b3..05fd42f 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -1,80 +1,216 @@ // Models for account API +use std::fmt::Display; + use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct AccountContext { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "title")] - pub title: Option, + #[serde(rename = "title")] + pub title: Option, - #[serde(rename = "css")] - pub css: Option, + #[serde(rename = "css")] + pub css: Option, - #[serde(rename = "root")] - pub root: bool, + #[serde(rename = "root")] + pub root: bool, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct ChangeUsernameBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, } #[derive(Debug, Serialize, Deserialize)] pub struct ChangePasswordBody { - #[serde(rename = "old_password")] - pub old_password: String, + #[serde(rename = "old_password")] + pub old_password: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, } #[derive(Debug, Serialize, Deserialize)] pub struct AccountListItem { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } - #[derive(Debug, Serialize, Deserialize)] pub struct AccountCreateBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct AccountUpdateBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "newUsername")] - pub new_username: Option, + #[serde(rename = "newUsername")] + pub new_username: Option, - #[serde(rename = "write")] - pub write: Option, + #[serde(rename = "write")] + pub write: Option, } - #[derive(Debug, Serialize, Deserialize)] pub struct AccountDeleteBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountSecuritySettings { + #[serde(rename = "tfa")] + pub tfa: bool, + + #[serde(rename = "tfaMethod")] + pub tfa_method: String, + + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, + + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, + + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountSetSecuritySettingsBody { + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, + + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, + + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, +} + +#[derive(Debug, Copy, Clone)] +pub enum TimeOtpAlgorithm { + Sha1, + Sha256, + Sha512, +} + +impl TimeOtpAlgorithm { + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "sha1" | "sha-1" => Ok(TimeOtpAlgorithm::Sha1), + "sha256" | "sha-256" => Ok(TimeOtpAlgorithm::Sha256), + "sha512" | "sha-512" => Ok(TimeOtpAlgorithm::Sha512), + _ => Err(()), + } + } +} + +impl Display for TimeOtpAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + TimeOtpAlgorithm::Sha1 => "sha1".to_string(), + TimeOtpAlgorithm::Sha256 => "sha256".to_string(), + TimeOtpAlgorithm::Sha512 => "sha512".to_string(), + }) + } +} + +#[derive(Debug, Copy, Clone)] +pub enum TimeOtpPeriod { + P30, + P60, + P120, +} + +impl TimeOtpPeriod { + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "30s" | "30" => Ok(TimeOtpPeriod::P30), + "1m" | "60s" | "60" => Ok(TimeOtpPeriod::P60), + "2m" | "120s" | "120" => Ok(TimeOtpPeriod::P120), + _ => Err(()), + } + } +} + +impl Display for TimeOtpPeriod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + TimeOtpPeriod::P30 => "30".to_string(), + TimeOtpPeriod::P60 => "60".to_string(), + TimeOtpPeriod::P120 => "120".to_string(), + } + ) + } +} + +#[derive(Debug)] +pub struct TimeOtpOptions { + pub issuer: Option, + + pub account: Option, + + pub algorithm: TimeOtpAlgorithm, + + pub period: TimeOtpPeriod, + + pub skew: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TimeOtpSettings { + #[serde(rename = "secret")] + pub secret: String, + + #[serde(rename = "method")] + pub method: String, + + #[serde(rename = "url")] + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TimeOtpEnableBody { + #[serde(rename = "secret")] + pub secret: String, + + #[serde(rename = "method")] + pub method: String, + + #[serde(rename = "password")] + pub password: String, + + #[serde(rename = "code")] + pub code: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TfaDisableBody { + #[serde(rename = "code")] + pub code: String, } diff --git a/src/models/auth.rs b/src/models/auth.rs index df9b8b3..b94c95f 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -12,6 +12,9 @@ pub struct Credentials { #[serde(rename = "duration")] pub duration: Option, + + #[serde(rename = "tfaCode")] + pub tfa_code: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/models/config.rs b/src/models/config.rs index bf2e61b..9ade03a 100644 --- a/src/models/config.rs +++ b/src/models/config.rs @@ -40,9 +40,15 @@ pub struct VaultConfig { #[serde(rename = "video_previews_interval")] pub video_previews_interval: Option, + #[serde(rename = "invite_limit")] + pub invite_limit: i32, + + #[serde(rename = "preserve_originals", default)] + pub preserve_originals: bool, + #[serde(rename = "resolutions")] pub resolutions: Vec, - + #[serde(rename = "image_resolutions")] pub image_resolutions: Vec, } diff --git a/src/tools/ensure_login.rs b/src/tools/ensure_login.rs index 91b0740..1f2f31b 100644 --- a/src/tools/ensure_login.rs +++ b/src/tools/ensure_login.rs @@ -12,8 +12,11 @@ use super::VaultURI; pub async fn ensure_login_ext( url: &VaultURI, given_username: &Option, + given_password: &Option, + given_tfa_code: &Option, duration: &Option, debug: bool, + required_tfa: bool, ) -> Result { match url.clone() { VaultURI::LoginURI { @@ -34,14 +37,35 @@ pub async fn ensure_login_ext( } }; + let tfa_code_m = match given_tfa_code { + Some(code) => Some(code.clone()), + None => { + if required_tfa { + let code = ask_user("Two factor authentication code: ") + .await + .unwrap_or("".to_string()); + Some(code) + } else { + None + } + } + }; + let mut password_m = password.clone(); if password_m.is_empty() { - // Ask password - eprintln!("Input password for vault: {base_url}"); - password_m = ask_user_password("Password: ") - .await - .unwrap_or("".to_string()); + match given_password { + Some(p) => { + password_m = p.clone(); + } + None => { + // Ask password + eprintln!("Input password for vault: {base_url}"); + password_m = ask_user_password("Password: ") + .await + .unwrap_or("".to_string()); + } + } } // Login @@ -49,17 +73,45 @@ pub async fn ensure_login_ext( let login_res = api_call_login( url, Credentials { - username: username_m, - password: password_m, + username: username_m.clone(), + password: password_m.clone(), duration: duration.clone(), + tfa_code: tfa_code_m, }, debug, ) .await; if login_res.is_err() { - print_request_error(login_res.err().unwrap()); - return Err(()); + let error = login_res.err().unwrap(); + + match error.clone() { + super::RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 && code == "TFA_REQUIRED" { + return Box::pin(ensure_login_ext( + url, + &Some(username_m), + &Some(password_m), + &None, + duration, + debug, + true, + )) + .await; + } else { + print_request_error(error); + return Err(()); + } + } + _ => { + print_request_error(error); + return Err(()); + } + } } let session_id = login_res.unwrap().session_id; @@ -87,5 +139,5 @@ pub async fn ensure_login( given_username: &Option, debug: bool, ) -> Result { - ensure_login_ext(url, given_username, &None, debug).await + ensure_login_ext(url, given_username, &None, &None, &None, debug, false).await } diff --git a/src/tools/request.rs b/src/tools/request.rs index e9a7e44..44798dd 100644 --- a/src/tools/request.rs +++ b/src/tools/request.rs @@ -1,12 +1,16 @@ // HTTP requests +use crate::tools::{ask_user, ask_user_password}; + use super::super::models::*; use super::vault_uri::VaultURI; pub const SESSION_HEADER_NAME: &str = "x-session-token"; +pub const AUTH_CONFIRMATION_PASSWORD_HEADER_NAME: &str = "x-auth-confirmation-pw"; +pub const AUTH_CONFIRMATION_TFA_HEADER_NAME: &str = "x-auth-confirmation-tfa"; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RequestError { StatusCode(reqwest::StatusCode), Api { @@ -50,7 +54,9 @@ pub fn get_session_from_uri(uri: VaultURI) -> Option { } } -pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { +pub async fn send_request( + request_builder: reqwest::RequestBuilder, +) -> Result { let response_result = request_builder.send().await; match response_result { @@ -65,8 +71,9 @@ pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { if res_status != reqwest::StatusCode::OK { if !res_body.is_empty() { - let parsed_body: Result = serde_json::from_str(&res_body); - + let parsed_body: Result = + serde_json::from_str(&res_body); + match parsed_body { Ok(r) => { return Err(RequestError::Api { @@ -80,20 +87,16 @@ pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { - Err(RequestError::NetworkError(err.to_string())) - }, + } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } - }, - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - }, + } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } @@ -125,11 +128,79 @@ pub async fn do_get_request( send_request(request_builder).await } +pub async fn request_auth_confirmation_password() -> String { + eprintln!("Input your account password to confirm the operation"); + ask_user_password("Password: ") + .await + .unwrap_or("".to_string()) +} + +pub async fn request_auth_confirmation_tfa() -> String { + eprintln!("Input your one-time two factor authentication code to confirm the operation"); + ask_user("Two factor authentication code: ") + .await + .unwrap_or("".to_string()) +} + pub async fn do_post_request( uri: &VaultURI, path: String, body: String, debug: bool, +) -> Result { + let final_uri = resolve_vault_api_uri(uri.clone(), path.clone()); + + if debug { + eprintln!("\rDEBUG: POST {final_uri}"); + } + + let client = reqwest::Client::new(); + + let mut request_builder = client + .post(final_uri) + .header("Content-Type", "application/json") + .body(body.clone()); + + let session = get_session_from_uri(uri.clone()); + + if let Some(s) = session { + request_builder = request_builder.header(SESSION_HEADER_NAME, s); + } + + match send_request(request_builder).await { + Ok(r) => Ok(r), + Err(err) => match err.clone() { + RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 { + if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { + let confirmation_tfa = request_auth_confirmation_tfa().await; + do_post_request_with_confirmation(uri, path, body, debug, None, Some(confirmation_tfa)).await + } else if code == "AUTH_CONFIRMATION_REQUIRED_PW" { + let confirmation_pw = request_auth_confirmation_password().await; + do_post_request_with_confirmation(uri, path, body, debug, Some(confirmation_pw), None).await + } else { + Err(err) + } + } else { + Err(err) + } + } + _ => Err(err), + }, + } +} + +pub async fn do_post_request_with_confirmation( + uri: &VaultURI, + path: String, + body: String, + debug: bool, + confirmation_password: Option, + confirmation_tfa: Option, ) -> Result { let final_uri = resolve_vault_api_uri(uri.clone(), path); @@ -139,7 +210,8 @@ pub async fn do_post_request( let client = reqwest::Client::new(); - let mut request_builder = client.post(final_uri) + let mut request_builder = client + .post(final_uri) .header("Content-Type", "application/json") .body(body); @@ -149,10 +221,17 @@ pub async fn do_post_request( request_builder = request_builder.header(SESSION_HEADER_NAME, s); } + if let Some(cp) = confirmation_password { + request_builder = request_builder.header(AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, cp); + } + + if let Some(ct) = confirmation_tfa { + request_builder = request_builder.header(AUTH_CONFIRMATION_TFA_HEADER_NAME, ct); + } + send_request(request_builder).await } - pub async fn do_delete_request( uri: &VaultURI, path: String, diff --git a/src/tools/request_upload.rs b/src/tools/request_upload.rs index 9494f64..45e282e 100644 --- a/src/tools/request_upload.rs +++ b/src/tools/request_upload.rs @@ -6,6 +6,11 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use tokio_sync_read_stream::SyncReadStream; +use crate::tools::{ + request_auth_confirmation_password, request_auth_confirmation_tfa, + AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, AUTH_CONFIRMATION_TFA_HEADER_NAME, +}; + use super::{super::models::*, ProgressReceiver}; use super::{get_session_from_uri, resolve_vault_api_uri, RequestError, SESSION_HEADER_NAME}; @@ -110,6 +115,79 @@ impl std::io::Read for UploadProgressReporterSync { } } +pub async fn do_multipart_upload_request_with_confirmation( + uri: &VaultURI, + path: String, + field: String, + file_path: String, + debug: bool, + progress_receiver: Arc>, +) -> Result { + let res = do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + None, + None, + progress_receiver.clone(), + ) + .await; + + match res { + Ok(r) => Ok(r), + Err(err) => match err.clone() { + RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 { + if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { + let confirmation_tfa = request_auth_confirmation_tfa().await; + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + None, + Some(confirmation_tfa), + progress_receiver.clone(), + ) + .await + } else if code == "AUTH_CONFIRMATION_REQUIRED_PW" { + let confirmation_pw = request_auth_confirmation_password().await; + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + Some(confirmation_pw), + None, + progress_receiver.clone(), + ) + .await + } else { + Err(err) + } + } else { + Err(err) + } + } + _ => Err(err), + }, + } +} + pub async fn do_multipart_upload_request( uri: &VaultURI, path: String, @@ -118,23 +196,52 @@ pub async fn do_multipart_upload_request( debug: bool, progress_receiver: Arc>, ) -> Result { - let final_uri = resolve_vault_api_uri(uri.clone(), path); + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path, + field, + file_path, + debug, + }, + None, + None, + progress_receiver, + ) + .await +} - if debug { - eprintln!("\rDEBUG: UPLOAD {file_path} -> {final_uri}"); +pub struct MultipartUploadRequestOptions { + pub path: String, + pub field: String, + pub file_path: String, + pub debug: bool, +} + +pub async fn do_multipart_upload_request_internal( + uri: &VaultURI, + options: MultipartUploadRequestOptions, + confirmation_password: Option, + confirmation_tfa: Option, + progress_receiver: Arc>, +) -> Result { + let final_uri = resolve_vault_api_uri(uri.clone(), options.path); + + if options.debug { + eprintln!("\rDEBUG: UPLOAD {} -> {}", options.file_path, final_uri); } let client = reqwest::Client::new(); // Load file - let file_path_o = Path::new(&file_path); + let file_path_o = Path::new(&options.file_path); let file_name: String = match file_path_o.file_name() { Some(n) => n.to_str().unwrap_or("").to_string(), None => "".to_string(), }; - let file_res = File::open(&file_path); + let file_res = File::open(&options.file_path); let file_len: u64; let mut reporter: UploadProgressReporterSync; @@ -166,9 +273,10 @@ pub async fn do_multipart_upload_request( let stream: SyncReadStream = reporter.clone().into(); - let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name); + let file_part = + reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name); - let form = reqwest::multipart::Form::new().part(field, file_part); + let form = reqwest::multipart::Form::new().part(options.field, file_part); let mut request_builder = client.post(final_uri).multipart(form); @@ -178,6 +286,14 @@ pub async fn do_multipart_upload_request( request_builder = request_builder.header(SESSION_HEADER_NAME, s); } + if let Some(cp) = confirmation_password { + request_builder = request_builder.header(AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, cp); + } + + if let Some(ct) = confirmation_tfa { + request_builder = request_builder.header(AUTH_CONFIRMATION_TFA_HEADER_NAME, ct); + } + // Send request let response_result = request_builder.send().await; @@ -225,9 +341,7 @@ pub async fn do_multipart_upload_request( Ok(res_body) } - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } @@ -303,8 +417,6 @@ pub async fn do_multipart_upload_request_memory( Ok(res_body) } - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } diff --git a/src/tools/size_render.rs b/src/tools/size_render.rs index a01db95..7dd954e 100644 --- a/src/tools/size_render.rs +++ b/src/tools/size_render.rs @@ -1,14 +1,22 @@ // Size rendering +const KB: u64 = 1024; +const MB: u64 = KB * KB; +const GB: u64 = MB * KB; +const TB: u64 = GB * KB; + pub fn render_size_bytes(bytes: u64) -> String { - if bytes > 1024 * 1024 * 1024 { - let v = bytes as f64 / (1024 * 1024 * 1024) as f64; + if bytes > TB { + let v = bytes as f64 / TB as f64; + format!("{v:.2} TB") + } else if bytes > GB { + let v = bytes as f64 / GB as f64; format!("{v:.2} GB") - } else if bytes > 1024 * 1024 { - let v = bytes as f64 / (1024 * 1024) as f64; + } else if bytes > MB { + let v = bytes as f64 / MB as f64; format!("{v:.2} MB") - } else if bytes > 1024 { - let v = bytes as f64 / 1024_f64; + } else if bytes > KB { + let v = bytes as f64 / KB as f64; format!("{v:.2} KB") } else { format!("{bytes} Bytes") diff --git a/src/tools/vault_uri.rs b/src/tools/vault_uri.rs index 7ab2c45..a44916d 100644 --- a/src/tools/vault_uri.rs +++ b/src/tools/vault_uri.rs @@ -185,7 +185,7 @@ pub fn get_extension_from_url(download_path: &str, default_ext: &str) -> String if path_parts.is_empty() { default_ext.to_string() } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); let file_name = last_part .split('?') .next() @@ -193,7 +193,7 @@ pub fn get_extension_from_url(download_path: &str, default_ext: &str) -> String .to_string(); file_name .split('.') - .last() + .next_back() .unwrap_or(default_ext) .to_string() } diff --git a/ws.code-workspace b/ws.code-workspace index 0576264..7d1f4c2 100644 --- a/ws.code-workspace +++ b/ws.code-workspace @@ -13,5 +13,8 @@ ".\\Cargo.toml" ], "rust-analyzer.showUnlinkedFileNotification": false, + "cSpell.words": [ + "totp" + ], } } \ No newline at end of file