diff --git a/src/api/client.rs b/src/api/client.rs index bf27c2c..add7ed9 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -159,48 +159,4 @@ impl ApiClient { self.handle_response(response) } - - pub fn post_file( - &self, - path: &str, - file_path: &std::path::Path, - ) -> Result { - use reqwest::blocking::multipart::{Form, Part}; - use std::fs::File; - use std::io::Read; - - let url = format!("{}{}", self.base_url, path); - - let mut file = File::open(file_path) - .map_err(|e| ApiError::Other(format!("Failed to open file: {}", e)))?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer) - .map_err(|e| ApiError::Other(format!("Failed to read file: {}", e)))?; - - let file_name = file_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("file.sql") - .to_string(); - - let part = Part::bytes(buffer) - .file_name(file_name) - .mime_str("application/octet-stream") - .map_err(|e| ApiError::Other(format!("Failed to set mime type: {}", e)))?; - - let form = Form::new().part("file", part); - - let mut headers = self.headers()?; - headers.remove(CONTENT_TYPE); - - let response = self - .client - .post(&url) - .headers(headers) - .multipart(form) - .send() - .map_err(ApiError::NetworkError)?; - - self.handle_response(response) - } } diff --git a/src/cli.rs b/src/cli.rs index 3bc3e9d..0325b5d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,4 @@ use clap::{Parser, Subcommand}; -use std::path::PathBuf; #[derive(Parser)] #[command(name = "vector")] @@ -395,30 +394,6 @@ pub enum EnvSecretCommands { #[derive(Subcommand)] pub enum EnvDbCommands { - /// Import a SQL file directly (files under 50MB) - Import { - /// Environment ID - env_id: String, - /// Path to SQL file - file: PathBuf, - /// Drop all existing tables before import - #[arg(long)] - drop_tables: bool, - /// Disable foreign key checks during import - #[arg(long)] - disable_foreign_keys: bool, - /// Search string for search-and-replace during import - #[arg(long)] - search_replace_from: Option, - /// Replace string for search-and-replace during import - #[arg(long)] - search_replace_to: Option, - }, - /// Manage import sessions for large files - ImportSession { - #[command(subcommand)] - command: EnvDbImportSessionCommands, - }, /// Promote dev database to this environment Promote { /// Environment ID @@ -437,46 +412,26 @@ pub enum EnvDbCommands { /// Promote ID promote_id: String, }, + /// Manage domain changes + DomainChange { + #[command(subcommand)] + command: EnvDomainChangeCommands, + }, } #[derive(Subcommand)] -pub enum EnvDbImportSessionCommands { - /// Create an import session +pub enum EnvDomainChangeCommands { + /// Create a domain change Create { /// Environment ID env_id: String, - /// Filename - #[arg(long)] - filename: Option, - /// Content length in bytes - #[arg(long)] - content_length: Option, - /// Drop all existing tables before import - #[arg(long)] - drop_tables: bool, - /// Disable foreign key checks during import - #[arg(long)] - disable_foreign_keys: bool, - /// Search string for search-and-replace during import - #[arg(long)] - search_replace_from: Option, - /// Replace string for search-and-replace during import - #[arg(long)] - search_replace_to: Option, - }, - /// Run an import session - Run { - /// Environment ID - env_id: String, - /// Import ID - import_id: String, }, - /// Check import session status + /// Check domain change status Status { /// Environment ID env_id: String, - /// Import ID - import_id: String, + /// Domain change ID + domain_change_id: String, }, } @@ -538,26 +493,7 @@ pub enum SslCommands { #[derive(Subcommand)] pub enum DbCommands { - /// Import a SQL file directly (files under 50MB) - Import { - /// Site ID - site_id: String, - /// Path to SQL file - file: PathBuf, - /// Drop all existing tables before import - #[arg(long)] - drop_tables: bool, - /// Disable foreign key checks during import - #[arg(long)] - disable_foreign_keys: bool, - /// Search string for search-and-replace during import - #[arg(long)] - search_replace_from: Option, - /// Replace string for search-and-replace during import - #[arg(long)] - search_replace_to: Option, - }, - /// Manage import sessions for large files + /// Manage archive import sessions ImportSession { #[command(subcommand)] command: DbImportSessionCommands, @@ -571,7 +507,7 @@ pub enum DbCommands { #[derive(Subcommand)] pub enum DbImportSessionCommands { - /// Create an import session + /// Create an archive import session Create { /// Site ID site_id: String, @@ -594,14 +530,14 @@ pub enum DbImportSessionCommands { #[arg(long)] search_replace_to: Option, }, - /// Run an import session + /// Run an archive import session Run { /// Site ID site_id: String, /// Import ID import_id: String, }, - /// Check import session status + /// Check archive import session status Status { /// Site ID site_id: String, @@ -1063,6 +999,18 @@ pub enum RestoreCommands { /// Restore scope (full, database, files) #[arg(long, default_value = "full")] scope: String, + /// Drop all existing tables before restore + #[arg(long)] + drop_tables: bool, + /// Disable foreign key checks during restore + #[arg(long)] + disable_foreign_keys: bool, + /// Search string for search-and-replace during restore + #[arg(long)] + search_replace_from: Option, + /// Replace string for search-and-replace during restore + #[arg(long)] + search_replace_to: Option, }, } diff --git a/src/commands/db.rs b/src/commands/db.rs index b97d735..0663eeb 100644 --- a/src/commands/db.rs +++ b/src/commands/db.rs @@ -1,6 +1,5 @@ use serde::Serialize; use serde_json::Value; -use std::path::Path; use crate::api::{ApiClient, ApiError}; use crate::output::{OutputFormat, format_option, print_json, print_key_value, print_message}; @@ -39,71 +38,6 @@ struct CreateExportRequest { format: Option, } -#[allow(clippy::too_many_arguments)] -pub fn import_direct( - client: &ApiClient, - site_id: &str, - file_path: &Path, - drop_tables: bool, - disable_foreign_keys: bool, - search_replace_from: Option, - search_replace_to: Option, - format: OutputFormat, -) -> Result<(), ApiError> { - // Check file size - direct import only supports files under 50MB - let metadata = std::fs::metadata(file_path) - .map_err(|e| ApiError::Other(format!("Failed to read file: {}", e)))?; - - if metadata.len() > 50 * 1024 * 1024 { - return Err(ApiError::Other( - "File too large for direct import. Use 'import-session' for files over 50MB." - .to_string(), - )); - } - - let mut path = format!("/api/v1/vector/sites/{}/db/import", site_id); - let mut params = vec![]; - if drop_tables { - params.push("drop_tables=true".to_string()); - } - if disable_foreign_keys { - params.push("disable_foreign_keys=true".to_string()); - } - if let Some(ref from) = search_replace_from { - params.push(format!("search_replace_from={}", from)); - } - if let Some(ref to) = search_replace_to { - params.push(format!("search_replace_to={}", to)); - } - if !params.is_empty() { - path = format!("{}?{}", path, params.join("&")); - } - - let response: Value = client.post_file(&path, file_path)?; - - if format == OutputFormat::Json { - print_json(&response); - return Ok(()); - } - - let data = &response["data"]; - if data["success"].as_bool().unwrap_or(false) { - print_message(&format!( - "Database imported successfully ({}ms).", - data["duration_ms"].as_u64().unwrap_or(0) - )); - } else { - return Err(ApiError::Other( - data["error"] - .as_str() - .unwrap_or("Import failed") - .to_string(), - )); - } - - Ok(()) -} - #[allow(clippy::too_many_arguments)] pub fn import_session_create( client: &ApiClient, @@ -138,10 +72,8 @@ pub fn import_session_create( options, }; - let response: Value = client.post( - &format!("/api/v1/vector/sites/{}/db/imports", site_id), - &body, - )?; + let response: Value = + client.post(&format!("/api/v1/vector/sites/{}/imports", site_id), &body)?; if format == OutputFormat::Json { print_json(&response); @@ -179,7 +111,7 @@ pub fn import_session_run( format: OutputFormat, ) -> Result<(), ApiError> { let response: Value = client.post_empty(&format!( - "/api/v1/vector/sites/{}/db/imports/{}/run", + "/api/v1/vector/sites/{}/imports/{}/run", site_id, import_id ))?; @@ -205,7 +137,7 @@ pub fn import_session_status( format: OutputFormat, ) -> Result<(), ApiError> { let response: Value = client.get(&format!( - "/api/v1/vector/sites/{}/db/imports/{}", + "/api/v1/vector/sites/{}/imports/{}", site_id, import_id ))?; diff --git a/src/commands/env.rs b/src/commands/env.rs index 3d12771..fd9b54c 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -1,6 +1,5 @@ use serde::Serialize; use serde_json::Value; -use std::path::Path; use crate::api::{ApiClient, ApiError}; use crate::output::{ @@ -434,34 +433,6 @@ pub fn secret_delete( // Environment DB commands -#[derive(Debug, Serialize)] -struct EnvImportOptions { - #[serde(skip_serializing_if = "std::ops::Not::not")] - drop_tables: bool, - #[serde(skip_serializing_if = "std::ops::Not::not")] - disable_foreign_keys: bool, - #[serde(skip_serializing_if = "Option::is_none")] - search_replace: Option, -} - -#[derive(Debug, Serialize)] -struct EnvSearchReplace { - from: String, - to: String, -} - -#[derive(Debug, Serialize)] -struct EnvCreateImportSessionRequest { - #[serde(skip_serializing_if = "Option::is_none")] - filename: Option, - #[serde(skip_serializing_if = "Option::is_none")] - content_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] - content_md5: Option, - #[serde(skip_serializing_if = "Option::is_none")] - options: Option, -} - #[derive(Debug, Serialize)] struct PromoteRequest { #[serde(skip_serializing_if = "std::ops::Not::not")] @@ -470,106 +441,20 @@ struct PromoteRequest { disable_foreign_keys: bool, } -#[allow(clippy::too_many_arguments)] -pub fn db_import( - client: &ApiClient, - env_id: &str, - file_path: &Path, - drop_tables: bool, - disable_foreign_keys: bool, - search_replace_from: Option, - search_replace_to: Option, - format: OutputFormat, -) -> Result<(), ApiError> { - let metadata = std::fs::metadata(file_path) - .map_err(|e| ApiError::Other(format!("Failed to read file: {}", e)))?; - - if metadata.len() > 50 * 1024 * 1024 { - return Err(ApiError::Other( - "File too large for direct import. Use 'env db import-session' for files over 50MB." - .to_string(), - )); - } - - let mut path = format!("/api/v1/vector/environments/{}/db/import", env_id); - let mut params = vec![]; - if drop_tables { - params.push("drop_tables=true".to_string()); - } - if disable_foreign_keys { - params.push("disable_foreign_keys=true".to_string()); - } - if let Some(ref from) = search_replace_from { - params.push(format!("search_replace_from={}", from)); - } - if let Some(ref to) = search_replace_to { - params.push(format!("search_replace_to={}", to)); - } - if !params.is_empty() { - path = format!("{}?{}", path, params.join("&")); - } - - let response: Value = client.post_file(&path, file_path)?; - - if format == OutputFormat::Json { - print_json(&response); - return Ok(()); - } - - let data = &response["data"]; - if data["success"].as_bool().unwrap_or(false) { - print_message(&format!( - "Database imported successfully ({}ms).", - data["duration_ms"].as_u64().unwrap_or(0) - )); - } else { - return Err(ApiError::Other( - data["error"] - .as_str() - .unwrap_or("Import failed") - .to_string(), - )); - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -pub fn db_import_session_create( +pub fn db_promote( client: &ApiClient, env_id: &str, - filename: Option, - content_length: Option, drop_tables: bool, disable_foreign_keys: bool, - search_replace_from: Option, - search_replace_to: Option, format: OutputFormat, ) -> Result<(), ApiError> { - let search_replace = match (search_replace_from, search_replace_to) { - (Some(from), Some(to)) => Some(EnvSearchReplace { from, to }), - _ => None, - }; - - let options = if drop_tables || disable_foreign_keys || search_replace.is_some() { - Some(EnvImportOptions { - drop_tables, - disable_foreign_keys, - search_replace, - }) - } else { - None - }; - - let body = EnvCreateImportSessionRequest { - filename, - content_length, - content_md5: None, - options, + let body = PromoteRequest { + drop_tables, + disable_foreign_keys, }; let response: Value = client.post( - &format!("/api/v1/vector/environments/{}/db/imports", env_id), + &format!("/api/v1/vector/environments/{}/db/promote", env_id), &body, )?; @@ -578,65 +463,25 @@ pub fn db_import_session_create( return Ok(()); } - let data = &response["data"]; - print_key_value(vec![ - ("Import ID", data["id"].as_str().unwrap_or("-").to_string()), - ("Status", data["status"].as_str().unwrap_or("-").to_string()), - ( - "Upload URL", - format_option(&data["upload_url"].as_str().map(String::from)), - ), - ( - "Expires", - format_option(&data["upload_expires_at"].as_str().map(String::from)), - ), - ]); - - print_message("\nUpload your SQL file to the URL above, then run:"); - print_message(&format!( - " vector env db import-session run {} {}", - env_id, - data["id"].as_str().unwrap_or("IMPORT_ID") - )); - - Ok(()) -} - -pub fn db_import_session_run( - client: &ApiClient, - env_id: &str, - import_id: &str, - format: OutputFormat, -) -> Result<(), ApiError> { - let response: Value = client.post_empty(&format!( - "/api/v1/vector/environments/{}/db/imports/{}/run", - env_id, import_id - ))?; - - if format == OutputFormat::Json { - print_json(&response); - return Ok(()); - } - let data = &response["data"]; print_message(&format!( - "Import started: {} ({})", - import_id, + "Promote started: {} ({})", + data["id"].as_str().unwrap_or("-"), data["status"].as_str().unwrap_or("-") )); Ok(()) } -pub fn db_import_session_status( +pub fn db_promote_status( client: &ApiClient, env_id: &str, - import_id: &str, + promote_id: &str, format: OutputFormat, ) -> Result<(), ApiError> { let response: Value = client.get(&format!( - "/api/v1/vector/environments/{}/db/imports/{}", - env_id, import_id + "/api/v1/vector/environments/{}/db/promotes/{}", + env_id, promote_id ))?; if format == OutputFormat::Json { @@ -646,12 +491,8 @@ pub fn db_import_session_status( let data = &response["data"]; print_key_value(vec![ - ("Import ID", data["id"].as_str().unwrap_or("-").to_string()), + ("Promote ID", data["id"].as_str().unwrap_or("-").to_string()), ("Status", data["status"].as_str().unwrap_or("-").to_string()), - ( - "Filename", - format_option(&data["filename"].as_str().map(String::from)), - ), ( "Duration (ms)", format_option(&data["duration_ms"].as_u64().map(|v| v.to_string())), @@ -673,22 +514,17 @@ pub fn db_import_session_status( Ok(()) } -pub fn db_promote( +// Domain change commands + +pub fn domain_change_create( client: &ApiClient, env_id: &str, - drop_tables: bool, - disable_foreign_keys: bool, format: OutputFormat, ) -> Result<(), ApiError> { - let body = PromoteRequest { - drop_tables, - disable_foreign_keys, - }; - - let response: Value = client.post( - &format!("/api/v1/vector/environments/{}/db/promote", env_id), - &body, - )?; + let response: Value = client.post_empty(&format!( + "/api/v1/vector/environments/{}/domain-change", + env_id + ))?; if format == OutputFormat::Json { print_json(&response); @@ -697,7 +533,7 @@ pub fn db_promote( let data = &response["data"]; print_message(&format!( - "Promote started: {} ({})", + "Domain change started: {} ({})", data["id"].as_str().unwrap_or("-"), data["status"].as_str().unwrap_or("-") )); @@ -705,15 +541,15 @@ pub fn db_promote( Ok(()) } -pub fn db_promote_status( +pub fn domain_change_status( client: &ApiClient, env_id: &str, - promote_id: &str, + domain_change_id: &str, format: OutputFormat, ) -> Result<(), ApiError> { let response: Value = client.get(&format!( - "/api/v1/vector/environments/{}/db/promotes/{}", - env_id, promote_id + "/api/v1/vector/environments/{}/domain-changes/{}", + env_id, domain_change_id ))?; if format == OutputFormat::Json { @@ -723,12 +559,11 @@ pub fn db_promote_status( let data = &response["data"]; print_key_value(vec![ - ("Promote ID", data["id"].as_str().unwrap_or("-").to_string()), - ("Status", data["status"].as_str().unwrap_or("-").to_string()), ( - "Duration (ms)", - format_option(&data["duration_ms"].as_u64().map(|v| v.to_string())), + "Domain Change ID", + data["id"].as_str().unwrap_or("-").to_string(), ), + ("Status", data["status"].as_str().unwrap_or("-").to_string()), ( "Error", format_option(&data["error_message"].as_str().map(String::from)), diff --git a/src/commands/restore.rs b/src/commands/restore.rs index e7b97f0..8d88f91 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -25,6 +25,24 @@ pub struct ListRestoresQuery { struct CreateRestoreRequest { backup_id: String, scope: String, + #[serde(skip_serializing_if = "Option::is_none")] + options: Option, +} + +#[derive(Debug, Serialize)] +struct RestoreOptions { + #[serde(skip_serializing_if = "std::ops::Not::not")] + drop_tables: bool, + #[serde(skip_serializing_if = "std::ops::Not::not")] + disable_foreign_keys: bool, + #[serde(skip_serializing_if = "Option::is_none")] + search_replace: Option, +} + +#[derive(Debug, Serialize)] +struct RestoreSearchReplace { + from: String, + to: String, } pub fn list( @@ -148,15 +166,36 @@ pub fn show(client: &ApiClient, restore_id: &str, format: OutputFormat) -> Resul Ok(()) } +#[allow(clippy::too_many_arguments)] pub fn create( client: &ApiClient, backup_id: &str, scope: &str, + drop_tables: bool, + disable_foreign_keys: bool, + search_replace_from: Option, + search_replace_to: Option, format: OutputFormat, ) -> Result<(), ApiError> { + let search_replace = match (search_replace_from, search_replace_to) { + (Some(from), Some(to)) => Some(RestoreSearchReplace { from, to }), + _ => None, + }; + + let options = if drop_tables || disable_foreign_keys || search_replace.is_some() { + Some(RestoreOptions { + drop_tables, + disable_foreign_keys, + search_replace, + }) + } else { + None + }; + let body = CreateRestoreRequest { backup_id: backup_id.to_string(), scope: scope.to_string(), + options, }; let response: Value = client.post("/api/v1/vector/restores", &body)?; diff --git a/src/main.rs b/src/main.rs index d910d05..f09ce30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use cli::{ AccountApiKeyCommands, AccountCommands, AccountSecretCommands, AccountSshKeyCommands, AuthCommands, BackupCommands, BackupDownloadCommands, Cli, Commands, DbCommands, DbExportCommands, DbImportSessionCommands, DeployCommands, EnvCommands, EnvDbCommands, - EnvDbImportSessionCommands, EnvSecretCommands, EventCommands, McpCommands, RestoreCommands, + EnvDomainChangeCommands, EnvSecretCommands, EventCommands, McpCommands, RestoreCommands, SiteCommands, SiteSshKeyCommands, SslCommands, WafAllowedReferrerCommands, WafBlockedIpCommands, WafBlockedReferrerCommands, WafCommands, WafRateLimitCommands, WebhookCommands, @@ -231,26 +231,6 @@ fn run_env_db( format: OutputFormat, ) -> Result<(), ApiError> { match command { - EnvDbCommands::Import { - env_id, - file, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - } => env::db_import( - client, - &env_id, - &file, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - format, - ), - EnvDbCommands::ImportSession { command } => { - run_env_db_import_session(client, command, format) - } EnvDbCommands::Promote { env_id, drop_tables, @@ -259,40 +239,23 @@ fn run_env_db( EnvDbCommands::PromoteStatus { env_id, promote_id } => { env::db_promote_status(client, &env_id, &promote_id, format) } + EnvDbCommands::DomainChange { command } => run_env_domain_change(client, command, format), } } -fn run_env_db_import_session( +fn run_env_domain_change( client: &ApiClient, - command: EnvDbImportSessionCommands, + command: EnvDomainChangeCommands, format: OutputFormat, ) -> Result<(), ApiError> { match command { - EnvDbImportSessionCommands::Create { - env_id, - filename, - content_length, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - } => env::db_import_session_create( - client, - &env_id, - filename, - content_length, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - format, - ), - EnvDbImportSessionCommands::Run { env_id, import_id } => { - env::db_import_session_run(client, &env_id, &import_id, format) - } - EnvDbImportSessionCommands::Status { env_id, import_id } => { - env::db_import_session_status(client, &env_id, &import_id, format) + EnvDomainChangeCommands::Create { env_id } => { + env::domain_change_create(client, &env_id, format) } + EnvDomainChangeCommands::Status { + env_id, + domain_change_id, + } => env::domain_change_status(client, &env_id, &domain_change_id, format), } } @@ -331,23 +294,6 @@ fn run_db(command: DbCommands, format: OutputFormat) -> Result<(), ApiError> { let client = get_client()?; match command { - DbCommands::Import { - site_id, - file, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - } => db::import_direct( - &client, - &site_id, - &file, - drop_tables, - disable_foreign_keys, - search_replace_from, - search_replace_to, - format, - ), DbCommands::ImportSession { command } => run_db_import_session(&client, command, format), DbCommands::Export { command } => run_db_export(&client, command, format), } @@ -694,9 +640,23 @@ fn run_restore(command: RestoreCommands, format: OutputFormat) -> Result<(), Api format, ), RestoreCommands::Show { restore_id } => restore::show(&client, &restore_id, format), - RestoreCommands::Create { backup_id, scope } => { - restore::create(&client, &backup_id, &scope, format) - } + RestoreCommands::Create { + backup_id, + scope, + drop_tables, + disable_foreign_keys, + search_replace_from, + search_replace_to, + } => restore::create( + &client, + &backup_id, + &scope, + drop_tables, + disable_foreign_keys, + search_replace_from, + search_replace_to, + format, + ), } }