Skip to content
Merged
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
6 changes: 1 addition & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,11 +537,7 @@ impl Cli {
let should_backup = if backup {
true
} else {
print!("Create backup before encryption? [y/N]: ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
response.trim().to_lowercase() == "y"
utils::prompt_yes_no("Create backup before encryption?", true)?
};

// Create backup if requested
Expand Down
77 changes: 62 additions & 15 deletions src/gpg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! GPG integration for encrypting/decrypting vault files.

use crate::error::{Result, VaultError};
use crate::utils;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
Expand Down Expand Up @@ -36,13 +37,28 @@ impl GpgOperations {
return Err(VaultError::VaultNotFound(vault_path.to_path_buf()));
}

// Create output path
// Create output path based on original file extension
let output_path = if armor {
vault_path.with_extension("md.asc")
vault_path.with_extension("toml.asc")
} else {
vault_path.with_extension("md.gpg")
vault_path.with_extension("toml.gpg")
};

// Check if output already exists and prompt for overwrite
if output_path.exists()
&& !utils::prompt_yes_no(
&format!(
"GPG encrypted file {} already exists. Overwrite?",
output_path.display()
),
true,
)?
{
return Err(VaultError::Cancelled);
}

// Remove this duplicate backup prompt - backup is already handled in save_vault

// Build GPG command
let mut cmd = Command::new("gpg");

Expand Down Expand Up @@ -88,19 +104,37 @@ impl GpgOperations {
let output = if let Some(path) = output_path {
path.to_path_buf()
} else {
// Remove .gpg or .asc extension
let stem = encrypted_path
.file_stem()
.ok_or_else(|| VaultError::Other("Invalid encrypted file name".to_string()))?;
encrypted_path.with_file_name(stem)
// Remove .gpg or .asc extension to get original filename
let file_name = encrypted_path
.file_name()
.ok_or_else(|| VaultError::Other("Invalid encrypted file name".to_string()))?
.to_string_lossy();

// Handle both vault.toml.gpg and vault.toml.asc formats
let output_name = if file_name.ends_with(".gpg") {
file_name.trim_end_matches(".gpg")
} else if file_name.ends_with(".asc") {
file_name.trim_end_matches(".asc")
} else {
return Err(VaultError::Other(
"Encrypted file must have .gpg or .asc extension".to_string(),
));
};

encrypted_path.with_file_name(output_name)
};

// Check if output already exists
if output.exists() {
return Err(VaultError::Other(format!(
"Output file already exists: {}. Please move or delete it first.",
output.display()
)));
// Check if output already exists and prompt for overwrite
if output.exists()
&& !utils::prompt_yes_no(
&format!(
"Output file {} already exists. Overwrite?",
output.display()
),
true,
)?
{
return Err(VaultError::Cancelled);
}

// Build GPG command
Expand Down Expand Up @@ -166,7 +200,20 @@ impl GpgOperations {

/// Create a backup of the vault file before GPG operations.
pub fn backup_vault(vault_path: &Path) -> Result<PathBuf> {
let backup_path = vault_path.with_extension("md.backup");
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let file_name = vault_path
.file_name()
.ok_or_else(|| VaultError::Other("Invalid file name".to_string()))?
.to_string_lossy();

// For GPG files, create backup as vault.toml.gpg.backup.timestamp
// For regular files, create backup as vault.toml.backup.timestamp
let backup_path = if file_name.ends_with(".gpg") || file_name.ends_with(".asc") {
vault_path.with_file_name(format!("{file_name}.backup.{timestamp}"))
} else {
// When encrypting a regular file, assume it will become .gpg
vault_path.with_file_name(format!("{file_name}.gpg.backup.{timestamp}"))
};

fs::copy(vault_path, &backup_path)
.map_err(|e| VaultError::Other(format!("Failed to create backup: {e}")))?;
Expand Down
14 changes: 2 additions & 12 deletions src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,20 +486,10 @@ impl InteractiveVault {
};

// Ask for ASCII armor
print!("\nCreate ASCII armored output (.asc)? [y/N]: ");
io::stdout().flush()?;

let mut armor_response = String::new();
io::stdin().read_line(&mut armor_response)?;
let armor = armor_response.trim().to_lowercase() == "y";
let armor = utils::prompt_yes_no("\nCreate ASCII armored output (.asc)?", true)?;

// Ask for backup
print!("\nCreate backup before encryption? [y/N]: ");
io::stdout().flush()?;

let mut backup_response = String::new();
io::stdin().read_line(&mut backup_response)?;
let create_backup = backup_response.trim().to_lowercase() == "y";
let create_backup = utils::prompt_yes_no("\nCreate backup before encryption?", true)?;

// Create backup if requested
if create_backup {
Expand Down
35 changes: 35 additions & 0 deletions src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,47 @@ impl VaultService {

/// Save a vault document to file.
pub fn save_vault(&self, doc: &VaultDocument, path: &Path) -> Result<()> {
// Check if vault already exists
if path.exists() {
// Prompt for backup
if utils::prompt_yes_no("Create backup before saving?", true)? {
self.create_backup(path)?;
}
}

// Format as TOML and save
let content = self.parser.format(doc);
std::fs::write(path, content).map_err(VaultError::Io)?;
Ok(())
}

/// Create a backup of the vault file
fn create_backup(&self, path: &Path) -> Result<()> {
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let file_name = path
.file_name()
.ok_or_else(|| VaultError::Other("Invalid file name".to_string()))?
.to_string_lossy();
let backup_path = path.with_file_name(format!("{file_name}.backup.{timestamp}"));

// Check if backup already exists (unlikely with timestamp, but just in case)
if backup_path.exists()
&& !utils::prompt_yes_no(
&format!(
"Backup {} already exists. Overwrite?",
backup_path.display()
),
true,
)?
{
return Err(VaultError::Cancelled);
}

std::fs::copy(path, &backup_path).map_err(VaultError::Io)?;
println!("Created backup: {}", backup_path.display());
Ok(())
}

/// Add a new entry to the vault.
pub fn add_entry(
&self,
Expand Down
19 changes: 19 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,25 @@ pub fn cleanup_old_temp_files() -> Result<()> {
Ok(())
}

/// Prompt user with a yes/no question, with default value
pub fn prompt_yes_no(prompt: &str, default: bool) -> Result<bool> {
use std::io::{self, Write};

let default_hint = if default { "Y/n" } else { "y/N" };
print!("{prompt} [{default_hint}]: ");
io::stdout().flush().map_err(VaultError::Io)?;

let mut input = String::new();
io::stdin().read_line(&mut input).map_err(VaultError::Io)?;

let input = input.trim().to_lowercase();
if input.is_empty() {
Ok(default)
} else {
Ok(input == "y" || input == "yes")
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down