This document identifies code quality improvements and refactoring opportunities in the Nameback codebase.
Problem:
- Single file contains all platform-specific installation logic
- Windows, macOS, Linux code all interleaved
- Hard to navigate and maintain
- Violates Single Responsibility Principle
Solution: Split into module structure
Proposed Structure:
nameback-core/src/deps/
├── mod.rs # Public API, shared types
├── common.rs # is_command_available, check_dependencies
├── bundled.rs # install_from_bundled logic
├── windows/
│ ├── mod.rs # Windows entry point
│ ├── scoop.rs # Scoop installation
│ ├── chocolatey.rs # Chocolatey installation
│ ├── dns_fallback.rs # DNS switching logic
│ └── msi_progress.rs # MSI progress reporting
├── macos/
│ ├── mod.rs # macOS entry point
│ ├── homebrew.rs # Homebrew installation
│ ├── macports.rs # MacPorts fallback
│ └── dns_fallback.rs # DNS switching logic
└── linux/
├── mod.rs # Linux entry point
├── apt.rs # apt/dpkg
├── dnf.rs # dnf/rpm
├── pacman.rs # Arch Linux
└── dns_fallback.rs # DNS switching logic
Benefits:
- Each file < 300 lines
- Clear separation by platform
- Easier to test individual components
- Can add new package managers easily
- Better for team collaboration
Estimated Effort: 4-6 hours
Problem: The exiftool, tesseract, ffmpeg, and imagemagick installation sections have near-identical code:
// This pattern repeats 4 times with minor variations:
if has_error {
msi_progress::report_action_data("Scoop failed, trying Chocolatey fallback...");
// Check if Chocolatey is installed
let choco_check = Command::new("powershell")...
// Install Chocolatey if needed
// Try installing via Chocolatey
// Try bundled installer fallback
// Show error message
}Solution: Extract to shared functions
// In deps/windows/common.rs
fn install_with_scoop_or_fallback(
package_name: &str,
scoop_name: &str,
choco_name: &str,
display_name: &str,
) -> Result<(), String> {
// Layer 1: Try Scoop
if try_scoop_install(scoop_name).is_ok() {
return Ok(());
}
// Layer 2: DNS fallback + retry Scoop
if try_with_dns_fallback(|| try_scoop_install(scoop_name)).is_ok() {
return Ok(());
}
// Layer 3: Try Chocolatey
if try_chocolatey_install(choco_name).is_ok() {
return Ok(());
}
// Layer 4: Bundled installer
if install_from_bundled(package_name, "windows").is_ok() {
return Ok(());
}
// Layer 5: Error
Err(format_installation_error(display_name))
}Usage:
install_with_scoop_or_fallback("exiftool", "exiftool", "exiftool", "ExifTool")?;
install_with_scoop_or_fallback("tesseract", "tesseract", "tesseract", "Tesseract")?;
install_with_scoop_or_fallback("ffmpeg", "ffmpeg", "ffmpeg", "FFmpeg")?;Benefits:
- Reduces 1,200+ lines to ~300 lines
- Single place to fix bugs
- Consistent behavior across all dependencies
- Easier to add Layer 6, 7, etc.
Estimated Effort: 3-4 hours
Problem: Clippy reports multiple warnings:
// deps.rs:192 - unused because not all code paths use it yet
warning: unused variable: `install_from_bundled`
// deps_check.rs:64
warning: variable does not need to be mutable
let mut needs_imagemagick = false;
// lib.rs:112
warning: value assigned to `analyses` is never read
let mut analyses = Vec::new();
// lib.rs:392
warning: method `analyze_file` is never used
fn analyze_file(...) { }
// extractor.rs:157
warning: field `creator` is never read
struct ExiftoolOutput {
creator: Option<String>, // Never used
}Solution: Clean up dead code
# Auto-fix most issues
cargo clippy --fix --lib -p nameback-core
# Manually review and remove:
# - analyze_file method (dead code)
# - creator field (unused)
# - analyses variable assignmentBenefits:
- Cleaner codebase
- No misleading code
- Better compile times
- Clearer intent
Estimated Effort: 30 minutes
Problem: Mixing error types and string formatting
// Current
return Err(format!("Failed to download: {}", e));
return Err("Failed to run extraction command".to_string());
// Mixing Result<(), String> with anyhow::ResultSolution: Use proper error types
// Create custom error type
#[derive(Debug, thiserror::Error)]
pub enum DepsError {
#[error("Package manager failed: {0}")]
PackageManagerFailed(String),
#[error("Download failed: {0}")]
DownloadFailed(#[from] reqwest::Error),
#[error("Extraction failed: {0}")]
ExtractionFailed(String),
#[error("All installation methods failed")]
AllMethodsFailed {
scoop: Option<String>,
chocolatey: Option<String>,
bundled: Option<String>,
},
}
// Then use
pub fn run_installer() -> Result<(), DepsError> { ... }Benefits:
- Structured error information
- Better error messages
- Type-safe error handling
- Can add context easily
Estimated Effort: 2-3 hours
Problem: DNS fallback appears 3 times (once per platform) with similar but slightly different code.
Solution: Extract to shared trait
// In deps/common.rs
pub trait DnsFallback {
fn save_dns_settings(&self) -> Result<String, String>;
fn apply_public_dns(&self) -> Result<(), String>;
fn restore_dns_settings(&self, original: &str) -> Result<(), String>;
}
// Then implement per platform
impl DnsFallback for WindowsDns { ... }
impl DnsFallback for MacOsDns { ... }
impl DnsFallback for LinuxDns { ... }
// Generic retry with DNS fallback
pub fn retry_with_dns_fallback<F, T>(
operation: F,
dns: &dyn DnsFallback,
) -> Result<T, String>
where
F: Fn() -> Result<T, String>,
{
match operation() {
Ok(result) => Ok(result),
Err(e) if is_dns_error(&e) => {
let original = dns.save_dns_settings()?;
dns.apply_public_dns()?;
let result = operation();
dns.restore_dns_settings(&original)?;
result
}
Err(e) => Err(e),
}
}Benefits:
- Single implementation of retry logic
- Platform-specific DNS handling
- Testable in isolation
- Guaranteed cleanup
Estimated Effort: 2 hours
Problem: The install_from_bundled closure has nested #[cfg] blocks making it hard to read.
Solution: Extract platform-specific installers
// In deps/bundled.rs
pub fn install_from_bundled(dep_name: &str, platform: &str) -> Result<(), String> {
let installer_path = download_from_github(dep_name, platform)?;
#[cfg(target_os = "windows")]
return windows::install_bundled(&installer_path, dep_name);
#[cfg(target_os = "macos")]
return macos::install_bundled(&installer_path, dep_name);
#[cfg(target_os = "linux")]
return linux::install_bundled(&installer_path, dep_name);
}
// Then in deps/windows/bundled.rs
pub fn install_bundled(installer_path: &Path, dep_name: &str) -> Result<(), String> {
let extract_dir = extract_archive(installer_path)?;
match dep_name {
"tesseract" => install_tesseract_setup(&extract_dir),
_ => install_portable(&extract_dir, dep_name),
}
}Benefits:
- Clear separation per platform
- Each platform file < 100 lines
- Easier to add new dependencies
- Can test each platform independently
Estimated Effort: 1-2 hours
Problem: Progress reporting mixed throughout installation logic
msi_progress::report_action_data("Installing exiftool...");
report_progress("ExifTool installed", 25);
println!("Installing exiftool via Scoop...");Solution: Centralized progress reporting
pub struct ProgressReporter {
callback: Option<ProgressCallback>,
msi_enabled: bool,
}
impl ProgressReporter {
pub fn report(&self, message: &str, percentage: u8) {
if self.msi_enabled {
msi_progress::report_action_data(message);
}
if let Some(ref cb) = self.callback {
cb(message, percentage);
} else {
println!("[{}%] {}", percentage, message);
}
}
}Benefits:
- Single source of truth
- Consistent formatting
- Easier to add new output methods
- Testable
Estimated Effort: 1 hour
Problem: The bundled installer fallback has no tests
Solution: Add integration tests
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
#[test]
fn test_download_from_github_success() {
let mut server = Server::new();
let mock = server.mock("GET", "/releases/download/v0.7.0/deps-exiftool-windows.zip")
.with_status(200)
.with_body(b"fake zip content")
.create();
let result = download_from_github("exiftool", "windows");
assert!(result.is_ok());
mock.assert();
}
#[test]
fn test_download_from_github_not_found() {
let mut server = Server::new();
let mock = server.mock("GET", "/releases/download/v0.7.0/deps-fake-windows.zip")
.with_status(404)
.create();
let result = download_from_github("fake", "windows");
assert!(result.is_err());
}
}Benefits:
- Confidence in bundled fallback
- Catch regressions early
- Document expected behavior
Estimated Effort: 2 hours
Problem: 13 clippy warnings for simple improvements
// Using contains() instead of iter().any()
warning: using `contains()` instead of `iter().any()` is more efficient
// Stripping prefix manually
warning: stripping a prefix manually
// Complex map_or
warning: this `map_or` can be simplified
// Confusing method name
warning: method `default` can be confused for the standard trait methodSolution: Auto-fix most
cargo clippy --fix --lib -p nameback-coreBenefits:
- More idiomatic Rust
- Better performance
- Clearer code
Estimated Effort: 15 minutes (automated)
Problem: URLs and version numbers scattered throughout
// In multiple places:
"https://github.com/h4x0r/nameback/releases/download/v{}/{}",
"https://exiftool.org/exiftool-{}.zip"
"https://community.chocolatey.org/install.ps1"Solution: Centralize constants
// deps/constants.rs
pub mod urls {
pub const GITHUB_RELEASES: &str = "https://github.com/h4x0r/nameback/releases/download";
pub const EXIFTOOL_DOWNLOAD: &str = "https://exiftool.org";
pub const CHOCOLATEY_INSTALL: &str = "https://community.chocolatey.org/install.ps1";
}
pub mod versions {
pub const EXIFTOOL: &str = "12.70";
pub const TESSERACT: &str = "5.3.3";
pub const FFMPEG: &str = "6.1";
}Benefits:
- Single place to update URLs
- Easy to see what external dependencies exist
- Can validate URLs in tests
Estimated Effort: 30 minutes
| Priority | Refactoring | Impact | Effort | LOC Reduction |
|---|---|---|---|---|
| 1 | Split deps.rs into modules | High | 4-6h | N/A (reorganize) |
| 1 | Extract duplicate installation logic | High | 3-4h | -900 lines |
| 1 | Remove dead code | Medium | 30min | -50 lines |
| 2 | Better error handling | Medium | 2-3h | ~same |
| 2 | DNS fallback trait | Medium | 2h | -200 lines |
| 2 | Extract bundled platform logic | Medium | 1-2h | ~same |
| 3 | Centralized progress reporting | Low | 1h | -100 lines |
| 3 | Add tests for bundled fallback | Low | 2h | +200 lines |
| 3 | Apply clippy fixes | Low | 15min | -10 lines |
| 3 | Centralize constants | Low | 30min | ~same |
Total Estimated Effort: 16-21 hours Total LOC Reduction: ~1,260 lines Final deps.rs Size: ~600 lines (down from 1,861)
- ✅ Run
cargo clippy --fix(15 min) - ✅ Remove dead code (30 min)
- ✅ Centralize constants (30 min)
- ✅ Centralized progress reporting (1 hour)
- ✅ Extract duplicate installation logic (3-4 hours)
- ✅ DNS fallback trait (2 hours)
- ✅ Better error handling (2-3 hours)
- ✅ Extract bundled platform logic (1-2 hours)
- ✅ Split deps.rs into modules (4-6 hours)
- ✅ Add tests for bundled fallback (2 hours)
After Full Refactoring:
- ✅ deps.rs: 1,861 lines → ~600 lines (68% reduction)
- ✅ Much easier to add new package managers
- ✅ Clear separation of concerns
- ✅ Platform-specific code isolated
- ✅ Consistent error handling
- ✅ Comprehensive test coverage
- ✅ Easier for new contributors
- ✅ Less code duplication (DRY principle)
Arguments FOR:
- Code is fresh in mind
- Before next feature additions
- Before it gets worse
- Easier to maintain going forward
Arguments AGAINST:
- Current code works
- No immediate bugs
- Time investment needed
- Risk of introducing bugs
Recommendation: Do Phase 1 (Quick Wins) immediately - low risk, high value. Schedule Phase 2-3 for next sprint/version.