kaizen/core/
legacy_import.rs1use anyhow::Result;
5use std::path::Path;
6
7const MARKER: &str = "LEGACY_IMPORTED.txt";
8
9pub enum ImportOutcome {
10 Skipped,
11 AlreadyImported,
12 Conflict,
13 Imported,
14}
15
16pub fn import_legacy(workspace: &Path, target: &Path) -> Result<ImportOutcome> {
18 let source = workspace.join(".kaizen");
19 if let Some(outcome) = skip_reason(&source, target)? {
20 return Ok(outcome);
21 }
22 validate_tree(&source)?;
23 std::fs::create_dir_all(target)?;
24 copy_entries(&source, target)?;
25 write_marker(&source, target)?;
26 Ok(ImportOutcome::Imported)
27}
28
29fn skip_reason(source: &Path, target: &Path) -> Result<Option<ImportOutcome>> {
30 if source_missing(source)? {
31 return Ok(Some(ImportOutcome::Skipped));
32 }
33 if target.join(MARKER).exists() {
34 return Ok(Some(ImportOutcome::AlreadyImported));
35 }
36 Ok(target_has_data(target)?.then_some(ImportOutcome::Conflict))
37}
38
39fn source_missing(source: &Path) -> Result<bool> {
40 match std::fs::symlink_metadata(source) {
41 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(true),
42 Err(error) => Err(error.into()),
43 Ok(_) => {
44 anyhow::ensure!(
45 validated_metadata(source)?.is_dir(),
46 "legacy root must be a directory"
47 );
48 Ok(false)
49 }
50 }
51}
52
53fn target_has_data(target: &Path) -> Result<bool> {
54 Ok(target.exists() && std::fs::read_dir(target)?.next().transpose()?.is_some())
55}
56
57fn copy_entries(source: &Path, target: &Path) -> Result<()> {
58 for entry in std::fs::read_dir(source)? {
59 let entry = entry?;
60 if entry.file_name() != "MIGRATED.txt" {
61 copy_recursive(&entry.path(), &target.join(entry.file_name()))?;
62 }
63 }
64 Ok(())
65}
66
67fn validate_tree(root: &Path) -> Result<()> {
68 anyhow::ensure!(
69 validated_metadata(root)?.is_dir(),
70 "legacy root must be a directory"
71 );
72 for entry in std::fs::read_dir(root)? {
73 validate_entry(&entry?.path())?;
74 }
75 Ok(())
76}
77
78fn validate_entry(path: &Path) -> Result<()> {
79 let metadata = validated_metadata(path)?;
80 if metadata.is_dir() {
81 validate_tree(path)?;
82 }
83 Ok(())
84}
85
86fn validated_metadata(path: &Path) -> Result<std::fs::Metadata> {
87 let metadata = std::fs::symlink_metadata(path)?;
88 anyhow::ensure!(
89 !metadata.file_type().is_symlink(),
90 "legacy import rejects symlink: {}",
91 path.display()
92 );
93 anyhow::ensure!(
94 metadata.is_file() || metadata.is_dir(),
95 "unsupported legacy entry: {}",
96 path.display()
97 );
98 Ok(metadata)
99}
100
101fn copy_recursive(source: &Path, target: &Path) -> Result<()> {
102 let metadata = validated_metadata(source)?;
103 if !metadata.is_dir() {
104 std::fs::copy(source, target)?;
105 return Ok(());
106 }
107 std::fs::create_dir_all(target)?;
108 copy_entries(source, target)
109}
110
111fn write_marker(source: &Path, target: &Path) -> Result<()> {
112 let message = format!(
113 "copied from: {}\nsource left unchanged; remove it manually after verification\n",
114 source.display()
115 );
116 std::fs::write(target.join(MARKER), message)?;
117 Ok(())
118}