upstream-rs 2.6.0

Fetch package updates directly from the source.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process;

use anyhow::{Context, Result, anyhow};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::utils::filesystem::atomic_ops::write_atomic;

pub const MANIFEST_FILE_NAME: &str = "migration.json";
pub const MANIFEST_STORAGE_VERSION: u32 = 1;
pub const CURRENT_LAYOUT_VERSION: u32 = 3;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformInfo {
    pub os: String,
    pub arch: String,
    pub family: String,
}

impl PlatformInfo {
    pub fn current() -> Self {
        Self {
            os: std::env::consts::OS.to_string(),
            arch: std::env::consts::ARCH.to_string(),
            family: std::env::consts::FAMILY.to_string(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MigrationManifest {
    pub manifest_version: u32,
    pub layout_version: u32,
    pub root_id: String,
    pub created_by: String,
    pub created_at: DateTime<Utc>,
    pub last_migrated_by: String,
    pub last_migrated_at: DateTime<Utc>,
    pub previous_layout_version: Option<u32>,
    pub platform: PlatformInfo,
}

impl MigrationManifest {
    pub fn current() -> Self {
        let now = Utc::now();
        Self {
            manifest_version: MANIFEST_STORAGE_VERSION,
            layout_version: CURRENT_LAYOUT_VERSION,
            root_id: generate_root_id(now),
            created_by: current_upstream_version(),
            created_at: now,
            last_migrated_by: current_upstream_version(),
            last_migrated_at: now,
            previous_layout_version: None,
            platform: PlatformInfo::current(),
        }
    }

    pub fn record_migration(&mut self, layout_version: u32) {
        self.record_migration_from(None, layout_version);
    }

    pub fn record_migration_from(
        &mut self,
        previous_layout_version: Option<u32>,
        layout_version: u32,
    ) {
        let previous = previous_layout_version.unwrap_or(self.layout_version);
        self.layout_version = layout_version;
        self.last_migrated_by = current_upstream_version();
        self.last_migrated_at = Utc::now();
        self.previous_layout_version = (previous != layout_version).then_some(previous);
    }
}

#[derive(Debug)]
pub struct ManifestStorage {
    manifest_file: PathBuf,
    manifest: Option<MigrationManifest>,
}

impl ManifestStorage {
    pub fn new(migration_file: &Path) -> Result<Self> {
        let mut storage = Self {
            manifest_file: migration_file.to_path_buf(),
            manifest: None,
        };
        storage.load()?;
        Ok(storage)
    }

    pub fn path_for_root(upstream_root: &Path) -> PathBuf {
        upstream_root.join(MANIFEST_FILE_NAME)
    }

    pub fn load(&mut self) -> Result<()> {
        if !self.manifest_file.exists() {
            self.manifest = None;
            return Ok(());
        }

        let json = fs::read_to_string(&self.manifest_file).with_context(|| {
            format!(
                "Failed to read migration manifest '{}'",
                self.manifest_file.display()
            )
        })?;
        if json.trim().is_empty() {
            self.manifest = None;
            return Ok(());
        }

        let manifest: MigrationManifest = serde_json::from_str(&json).with_context(|| {
            format!(
                "Failed to parse migration manifest '{}'",
                self.manifest_file.display()
            )
        })?;
        if manifest.manifest_version != MANIFEST_STORAGE_VERSION {
            return Err(anyhow!(
                "Unsupported migration manifest version {} in '{}'. Expected version {}.",
                manifest.manifest_version,
                self.manifest_file.display(),
                MANIFEST_STORAGE_VERSION
            ));
        }

        self.manifest = Some(manifest);
        Ok(())
    }

    pub fn manifest(&self) -> Option<&MigrationManifest> {
        self.manifest.as_ref()
    }

    pub fn save_manifest(&mut self, manifest: MigrationManifest) -> Result<()> {
        if manifest.manifest_version != MANIFEST_STORAGE_VERSION {
            return Err(anyhow!(
                "Cannot save unsupported migration manifest version {}. Expected version {}.",
                manifest.manifest_version,
                MANIFEST_STORAGE_VERSION
            ));
        }

        let json = serde_json::to_string_pretty(&manifest)
            .context("Failed to serialize migration manifest")?;
        write_atomic(&self.manifest_file, json.as_bytes()).with_context(|| {
            format!(
                "Failed to write migration manifest to '{}'",
                self.manifest_file.display()
            )
        })?;
        self.manifest = Some(manifest);
        Ok(())
    }

    pub fn ensure_current(&mut self) -> Result<()> {
        if self.manifest.is_none() {
            self.save_manifest(MigrationManifest::current())?;
        }
        Ok(())
    }

    pub fn record_migration(&mut self, layout_version: u32) -> Result<()> {
        self.record_migration_from(None, layout_version)
    }

    pub fn record_migration_from(
        &mut self,
        previous_layout_version: Option<u32>,
        layout_version: u32,
    ) -> Result<()> {
        let mut manifest = self
            .manifest
            .clone()
            .unwrap_or_else(MigrationManifest::current);
        manifest.record_migration_from(previous_layout_version, layout_version);
        self.save_manifest(manifest)
    }
}

fn current_upstream_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

fn generate_root_id(now: DateTime<Utc>) -> String {
    format!("upstream-{}-{}", now.timestamp_micros(), process::id())
}

#[cfg(test)]
mod tests {
    use super::{
        CURRENT_LAYOUT_VERSION, MANIFEST_STORAGE_VERSION, ManifestStorage, MigrationManifest,
        PlatformInfo,
    };
    use chrono::Utc;
    use std::path::{Path, PathBuf};
    use std::time::{SystemTime, UNIX_EPOCH};
    use std::{fs, io};

    fn temp_manifest_file(name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        std::env::temp_dir()
            .join(format!("upstream-migration-manifest-test-{name}-{nanos}"))
            .join("migration.json")
    }

    fn cleanup(path: &Path) -> io::Result<()> {
        if let Some(parent) = path.parent() {
            fs::remove_dir_all(parent)?;
        }
        Ok(())
    }

    fn manifest(layout_version: u32) -> MigrationManifest {
        let now = Utc::now();
        MigrationManifest {
            manifest_version: MANIFEST_STORAGE_VERSION,
            layout_version,
            root_id: "root".to_string(),
            created_by: "test".to_string(),
            created_at: now,
            last_migrated_by: "test".to_string(),
            last_migrated_at: now,
            previous_layout_version: None,
            platform: PlatformInfo {
                os: "linux".to_string(),
                arch: "x86_64".to_string(),
                family: "unix".to_string(),
            },
        }
    }

    #[test]
    fn new_starts_empty_when_manifest_is_missing() {
        let path = temp_manifest_file("missing");
        let storage = ManifestStorage::new(&path).expect("create storage");
        assert!(storage.manifest().is_none());
    }

    #[test]
    fn ensure_current_writes_migration_manifest() {
        let path = temp_manifest_file("ensure-current");
        let mut storage = ManifestStorage::new(&path).expect("create storage");
        storage.ensure_current().expect("ensure current");

        let reloaded = ManifestStorage::new(&path).expect("reload storage");
        let manifest = reloaded.manifest().expect("manifest exists");
        assert_eq!(manifest.manifest_version, MANIFEST_STORAGE_VERSION);
        assert_eq!(manifest.layout_version, CURRENT_LAYOUT_VERSION);
        assert!(!manifest.root_id.is_empty());
        assert_eq!(manifest.platform.os, std::env::consts::OS);
        cleanup(&path).expect("cleanup");
    }

    #[test]
    fn record_migration_preserves_creation_fields_and_tracks_previous_layout() {
        let path = temp_manifest_file("record");
        let mut storage = ManifestStorage::new(&path).expect("create storage");
        storage
            .save_manifest(manifest(1))
            .expect("save old manifest");
        let created_at = storage.manifest().expect("manifest").created_at;

        storage
            .record_migration(CURRENT_LAYOUT_VERSION)
            .expect("record migration");

        let manifest = storage.manifest().expect("manifest");
        assert_eq!(manifest.layout_version, CURRENT_LAYOUT_VERSION);
        assert_eq!(manifest.previous_layout_version, Some(1));
        assert_eq!(manifest.created_at, created_at);
        assert_eq!(manifest.root_id, "root");
        cleanup(&path).expect("cleanup");
    }

    #[test]
    fn rejects_unsupported_manifest_version() {
        let path = temp_manifest_file("bad-version");
        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
        fs::write(
            &path,
            r#"{"manifest_version":2,"layout_version":1,"root_id":"root","created_by":"test","created_at":"2026-01-01T00:00:00Z","last_migrated_by":"test","last_migrated_at":"2026-01-01T00:00:00Z","previous_layout_version":null,"platform":{"os":"linux","arch":"x86_64","family":"unix"}}"#,
        )
        .expect("write manifest");

        let err = ManifestStorage::new(&path).expect_err("unsupported version");
        assert!(
            err.to_string()
                .contains("Unsupported migration manifest version")
        );
        cleanup(&path).expect("cleanup");
    }
}