1use super::*;
2use std::fs;
3
4impl BondManager {
6 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 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 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 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 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 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 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 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 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}