docker-image-pusher 0.5.6

A memory-optimized Docker image transfer tool for handling large images efficiently
use crate::{PusherError, STATE_DIR};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
type Result<T> = std::result::Result<T, PusherError>;

const CREDENTIALS_FILE: &str = "credentials.json";
const HISTORY_FILE: &str = "push_history.json";
const MAX_HISTORY: usize = 5;

#[derive(Debug, Clone)]
pub struct StoredCredential {
    pub registry: String,
    pub username: String,
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct CredentialRecord {
    username: String,
    password: String,
}

#[derive(Debug, Serialize, Deserialize, Default)]
struct CredentialStore {
    entries: HashMap<String, CredentialRecord>,
}

#[derive(Debug, Serialize, Deserialize, Default)]
struct PushHistory {
    entries: Vec<String>,
}

pub async fn store_credentials(registry: &str, username: &str, password: &str) -> Result<()> {
    let mut store = load_credentials_store().await?;
    store.entries.insert(
        registry.to_string(),
        CredentialRecord {
            username: username.to_string(),
            password: password.to_string(),
        },
    );
    save_credentials_store(&store).await
}

pub async fn load_credentials(registry: &str) -> Result<Option<(String, String)>> {
    let store = load_credentials_store().await?;
    Ok(store
        .entries
        .get(registry)
        .map(|entry| (entry.username.clone(), entry.password.clone())))
}

pub async fn all_credentials() -> Result<Vec<StoredCredential>> {
    let store = load_credentials_store().await?;
    Ok(store
        .entries
        .iter()
        .map(|(registry, record)| StoredCredential {
            registry: registry.clone(),
            username: record.username.clone(),
            password: record.password.clone(),
        })
        .collect())
}

pub async fn record_push_target(target: &str) -> Result<()> {
    let mut history = load_history().await?;
    history.entries.retain(|entry| entry != target);
    history.entries.insert(0, target.to_string());
    if history.entries.len() > MAX_HISTORY {
        history.entries.truncate(MAX_HISTORY);
    }
    save_history(&history).await
}

pub async fn recent_targets() -> Result<Vec<String>> {
    let history = load_history().await?;
    Ok(history.entries)
}

async fn load_credentials_store() -> Result<CredentialStore> {
    load_json(CREDENTIALS_FILE).await
}

async fn save_credentials_store(store: &CredentialStore) -> Result<()> {
    save_json(CREDENTIALS_FILE, store).await
}

async fn load_history() -> Result<PushHistory> {
    load_json(HISTORY_FILE).await
}

async fn save_history(history: &PushHistory) -> Result<()> {
    save_json(HISTORY_FILE, history).await
}

async fn load_json<T>(file_name: &str) -> Result<T>
where
    T: Default + for<'de> Deserialize<'de>,
{
    let path = state_file_path(file_name).await?;
    match tokio::fs::read(&path).await {
        Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| {
            PusherError::CacheError(format!("Failed to deserialize {}: {}", path.display(), e))
        }),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(T::default()),
        Err(err) => Err(PusherError::CacheError(format!(
            "Failed to read {}: {}",
            path.display(),
            err
        ))),
    }
}

async fn save_json<T>(file_name: &str, value: &T) -> Result<()>
where
    T: Serialize,
{
    let path = state_file_path(file_name).await?;
    let data = serde_json::to_vec_pretty(value)
        .map_err(|e| PusherError::CacheError(format!("Failed to serialize state: {}", e)))?;
    tokio::fs::write(&path, data)
        .await
        .map_err(|e| PusherError::CacheError(format!("Failed to write {}: {}", path.display(), e)))
}

async fn state_file_path(file_name: &str) -> Result<PathBuf> {
    let state_dir = PathBuf::from(STATE_DIR);
    tokio::fs::create_dir_all(&state_dir).await.map_err(|e| {
        PusherError::CacheError(format!(
            "Failed to create state directory {}: {}",
            state_dir.display(),
            e
        ))
    })?;
    Ok(state_dir.join(file_name))
}