Skip to main content

ralph/migration/file_migrations/
rename.rs

1//! Purpose: Apply generic file rename and rollback migrations.
2//!
3//! Responsibilities:
4//! - Copy files from old path to new path safely.
5//! - Create destination parents as needed.
6//! - Optionally keep backups and update config references.
7//! - Roll back rename migrations when the original backup still exists.
8//!
9//! Scope:
10//! - Generic file move/rename behavior only; JSON-to-JSONC wrappers and config
11//!   reference update internals live in sibling modules.
12//!
13//! Usage:
14//! - Used by `MigrationType::FileRename` and by JSON-to-JSONC wrapper helpers.
15//!
16//! Invariants/Assumptions:
17//! - Source must exist before migration.
18//! - Destination must not already exist unless source and destination are identical.
19//! - Keeping backups defaults to true.
20
21use anyhow::{Context, Result};
22use std::fs;
23
24use super::super::MigrationContext;
25use super::config_refs::update_config_file_references;
26
27/// Options for file migration.
28#[derive(Debug, Clone)]
29pub struct FileMigrationOptions {
30    /// Whether to keep the original file as a backup.
31    pub keep_backup: bool,
32    /// Whether to update config file references.
33    pub update_config: bool,
34}
35
36impl Default for FileMigrationOptions {
37    fn default() -> Self {
38        Self {
39            keep_backup: true,
40            update_config: true,
41        }
42    }
43}
44
45/// Apply a file rename migration.
46/// Copies content from old_path to new_path and optionally updates config.
47pub fn apply_file_rename(ctx: &MigrationContext, old_path: &str, new_path: &str) -> Result<()> {
48    let opts = FileMigrationOptions::default();
49    apply_file_rename_with_options(ctx, old_path, new_path, &opts)
50}
51
52/// Apply a file rename migration with custom options.
53pub fn apply_file_rename_with_options(
54    ctx: &MigrationContext,
55    old_path: &str,
56    new_path: &str,
57    opts: &FileMigrationOptions,
58) -> Result<()> {
59    let old_full_path = ctx.resolve_path(old_path);
60    let new_full_path = ctx.resolve_path(new_path);
61
62    if !old_full_path.exists() {
63        anyhow::bail!("Source file does not exist: {}", old_full_path.display());
64    }
65
66    if new_full_path.exists() && old_full_path != new_full_path {
67        anyhow::bail!(
68            "Destination file already exists: {}",
69            new_full_path.display()
70        );
71    }
72
73    if let Some(parent) = new_full_path.parent() {
74        fs::create_dir_all(parent)
75            .with_context(|| format!("create parent directory {}", parent.display()))?;
76    }
77
78    fs::copy(&old_full_path, &new_full_path).with_context(|| {
79        format!(
80            "copy {} to {}",
81            old_full_path.display(),
82            new_full_path.display()
83        )
84    })?;
85
86    log::info!(
87        "Migrated file from {} to {}",
88        old_full_path.display(),
89        new_full_path.display()
90    );
91
92    if opts.update_config {
93        update_config_file_references(ctx, old_path, new_path)
94            .context("update config file references")?;
95    }
96
97    if !opts.keep_backup {
98        fs::remove_file(&old_full_path)
99            .with_context(|| format!("remove original file {}", old_full_path.display()))?;
100        log::debug!("Removed original file {}", old_full_path.display());
101    } else {
102        log::debug!("Kept original file {} as backup", old_full_path.display());
103    }
104
105    Ok(())
106}
107
108/// Rollback a file migration by restoring from backup.
109/// This removes the new file and restores the original.
110pub fn rollback_file_migration(
111    ctx: &MigrationContext,
112    old_path: &str,
113    new_path: &str,
114) -> Result<()> {
115    let old_full_path = ctx.resolve_path(old_path);
116    let new_full_path = ctx.resolve_path(new_path);
117
118    if !old_full_path.exists() {
119        anyhow::bail!(
120            "Cannot rollback: original file {} does not exist",
121            old_full_path.display()
122        );
123    }
124
125    if new_full_path.exists() {
126        fs::remove_file(&new_full_path)
127            .with_context(|| format!("remove migrated file {}", new_full_path.display()))?;
128    }
129
130    log::info!(
131        "Rolled back file migration: restored {}, removed {}",
132        old_full_path.display(),
133        new_full_path.display()
134    );
135
136    Ok(())
137}