Skip to main content

bonds_core/manager/
lifecycle.rs

1use super::*;
2use std::fs;
3
4/// Create/update/delete and metadata mutation flows.
5impl BondManager {
6    /// Create a symlink bond and persist it (no metadata).
7    pub fn create_bond<P: AsRef<Path>, Q: AsRef<Path>>(
8        &self,
9        source: P,
10        target: Q,
11        name: Option<String>,
12    ) -> Result<Bond, BondError> {
13        self.create_bond_internal(source, target, name, None)
14    }
15
16    /// Create a symlink bond with metadata and persist it.
17    pub fn create_bond_with_metadata<P: AsRef<Path>, Q: AsRef<Path>>(
18        &self,
19        source: P,
20        target: Q,
21        name: Option<String>,
22        metadata: Option<HashMap<String, String>>,
23    ) -> Result<Bond, BondError> {
24        self.create_bond_internal(source, target, name, metadata)
25    }
26
27    /// Shared create implementation used by both create paths.
28    fn create_bond_internal<P: AsRef<Path>, Q: AsRef<Path>>(
29        &self,
30        source: P,
31        target: Q,
32        name: Option<String>,
33        metadata: Option<HashMap<String, String>>,
34    ) -> Result<Bond, BondError> {
35        let src = source.as_ref().to_path_buf();
36        let tgt = target.as_ref().to_path_buf();
37
38        // Name uniqueness check.
39        if let Some(ref n) = name {
40            let mut stmt = self
41                .conn
42                .prepare("SELECT COUNT(*) FROM bonds WHERE name = ?1")?;
43            let count: i64 = stmt.query_row(params![n], |row| row.get(0))?;
44            if count > 0 {
45                return Err(BondError::AlreadyExists);
46            }
47        }
48
49        if !src.exists() {
50            return Err(BondError::InvalidPath(format!(
51                "source does not exist: {:?}",
52                src
53            )));
54        }
55
56        if tgt.exists() {
57            // Permit replacing an empty directory.
58            let is_empty_dir = tgt.is_dir()
59                && std::fs::read_dir(&tgt)
60                    .map(|mut d| d.next().is_none())
61                    .unwrap_or(false);
62
63            if !is_empty_dir {
64                return Err(BondError::TargetExists(format!("{}", tgt.display())));
65            }
66
67            std::fs::remove_dir(&tgt)?;
68        }
69
70        if let Some(parent) = tgt.parent() {
71            fs::create_dir_all(parent)?;
72        }
73
74        #[cfg(unix)]
75        std::os::unix::fs::symlink(&src, &tgt)?;
76        #[cfg(windows)]
77        {
78            if src.is_dir() {
79                std::os::windows::fs::symlink_dir(&src, &tgt)?;
80            } else {
81                std::os::windows::fs::symlink_file(&src, &tgt)?;
82            }
83        }
84
85        let mut bond = Bond::new(src.clone(), tgt.clone(), name);
86        bond.metadata = metadata;
87
88        let metadata_json: Option<String> =
89            bond.metadata().map(serde_json::to_string).transpose()?;
90
91        self.conn.execute(
92            "INSERT INTO bonds (id, name, source, target, created_at, metadata) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
93            params![
94                bond.id(),
95                bond.name(),
96                bond.source().to_string_lossy().to_string(),
97                bond.target().to_string_lossy().to_string(),
98                bond.created_at_rfc3339(),
99                metadata_json
100            ],
101        )?;
102
103        self.emit_event(BondEventPayload::Created { bond: bond.clone() });
104        Ok(bond)
105    }
106
107    /// Update source and/or target and/or name.
108    pub fn update_bond(
109        &self,
110        id: &str,
111        new_source: Option<PathBuf>,
112        new_target: Option<PathBuf>,
113        new_name: Option<String>,
114    ) -> Result<Bond, BondError> {
115        let mut bond = self.get_bond(id)?;
116        let before = bond.clone();
117
118        let source = match new_source {
119            Some(s) => {
120                if !s.exists() {
121                    return Err(BondError::InvalidPath(format!(
122                        "source does not exist: {:?}",
123                        s
124                    )));
125                }
126                s
127            }
128            None => bond.source.clone(),
129        };
130
131        let target = new_target.unwrap_or_else(|| bond.target.clone());
132
133        // Nothing changed.
134        if source == bond.source && target == bond.target && new_name.is_none() {
135            return Ok(bond);
136        }
137
138        if bond.target.exists() || bond.target.symlink_metadata().is_ok() {
139            fs::remove_file(&bond.target)?;
140        }
141
142        if target != bond.target && target.exists() {
143            return Err(BondError::AlreadyExists);
144        }
145
146        if let Some(parent) = target.parent() {
147            fs::create_dir_all(parent)?;
148        }
149
150        #[cfg(unix)]
151        std::os::unix::fs::symlink(&source, &target)?;
152        #[cfg(windows)]
153        {
154            if source.is_dir() {
155                std::os::windows::fs::symlink_dir(&source, &target)?;
156            } else {
157                std::os::windows::fs::symlink_file(&source, &target)?;
158            }
159        }
160
161        self.conn.execute(
162            "UPDATE bonds SET source = ?1, target = ?2, name = ?3 WHERE id = ?4",
163            params![
164                source.to_string_lossy().to_string(),
165                target.to_string_lossy().to_string(),
166                new_name.as_ref().or(bond.name.as_ref()),
167                bond.id,
168            ],
169        )?;
170
171        bond.source = source;
172        bond.target = target;
173        if new_name.is_some() {
174            bond.name = new_name;
175        }
176
177        self.emit_event(BondEventPayload::Updated {
178            before,
179            after: bond.clone(),
180        });
181        Ok(bond)
182    }
183
184    /// Replace full metadata map. Use None to clear.
185    pub fn update_bond_metadata(
186        &self,
187        identifier: &str,
188        metadata: Option<HashMap<String, String>>,
189    ) -> Result<Bond, BondError> {
190        let mut bond = self.get_bond(identifier)?;
191        let before = bond.clone();
192
193        let metadata_json: Option<String> =
194            metadata.as_ref().map(serde_json::to_string).transpose()?;
195
196        self.conn.execute(
197            "UPDATE bonds SET metadata = ?1 WHERE id = ?2",
198            params![metadata_json, bond.id()],
199        )?;
200
201        bond.metadata = metadata;
202
203        self.emit_event(BondEventPayload::MetadataUpdated {
204            before,
205            after: bond.clone(),
206        });
207        Ok(bond)
208    }
209
210    /// Delete bond and optionally remove non-symlink target.
211    pub fn delete_bond(&self, id: &str, remove_target: bool) -> Result<Bond, BondError> {
212        let bond = self.get_bond(id)?;
213
214        if bond.target.exists() {
215            let meta = fs::symlink_metadata(&bond.target)?;
216            if meta.file_type().is_symlink() {
217                fs::remove_file(&bond.target)?;
218            } else if remove_target {
219                if bond.target.is_dir() {
220                    fs::remove_dir_all(&bond.target)?;
221                } else {
222                    fs::remove_file(&bond.target)?;
223                }
224            } else {
225                return Err(BondError::InvalidPath(format!(
226                    "target exists and is not a symlink: {:?}",
227                    bond.target
228                )));
229            }
230        }
231
232        self.conn
233            .execute("DELETE FROM bonds WHERE id = ?1", params![bond.id])?;
234
235        self.emit_event(BondEventPayload::Deleted { bond: bond.clone() });
236        Ok(bond)
237    }
238}