1use super::*;
4use crate::json::Json;
5
6#[derive(Debug, Clone)]
8pub struct Relink {
9 pub id: u64,
11 pub owner: UserRef,
13 pub from: EntityRef,
15 pub to: EntityRef,
17 pub at: u64,
19 pub undone_at: Option<u64>,
21 pub(super) origin_id: u64,
24 pub(super) before: UserData,
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct RelinkSummary {
30 pub annotations: usize,
32 pub plays: usize,
34 pub counts: usize,
36 pub relations: usize,
38 pub playlists: usize,
40 pub scrobbles: usize,
42}
43
44fn snapshot(data: &UserData, owner: &str, target: &EntityRef) -> UserData {
45 UserData {
46 annotations: data
47 .annotations
48 .iter()
49 .filter(|a| a.owner == owner && a.target == *target)
50 .cloned()
51 .collect(),
52 plays: data
53 .plays
54 .iter()
55 .filter(|p| p.owner == owner && p.track == *target)
56 .cloned()
57 .collect(),
58 counts: data
59 .counts
60 .iter()
61 .filter(|c| c.owner == owner && c.track == *target)
62 .cloned()
63 .collect(),
64 playlists: data
65 .playlists
66 .iter()
67 .filter(|p| p.owner == owner && p.tracks.contains(target))
68 .cloned()
69 .collect(),
70 scrobbles: data
71 .scrobbles
72 .iter()
73 .filter(|p| p.owner == owner && p.track == *target)
74 .cloned()
75 .collect(),
76 relation_annotations: data
77 .relation_annotations
78 .iter()
79 .filter(|a| {
80 a.owner == owner && (a.relation.source == *target || a.relation.target == *target)
81 })
82 .cloned()
83 .collect(),
84 ..Default::default()
85 }
86}
87
88fn summary(data: &UserData) -> RelinkSummary {
89 RelinkSummary {
90 annotations: data.annotations.len(),
91 plays: data.plays.len(),
92 counts: data.counts.len(),
93 relations: data.relation_annotations.len(),
94 playlists: data.playlists.len(),
95 scrobbles: data.scrobbles.len(),
96 }
97}
98
99fn has_records(data: &UserData) -> bool {
100 summary(data) != RelinkSummary::default()
101}
102
103fn rewrite(data: &mut UserData, owner: &str, from: &EntityRef, to: &EntityRef) {
104 for target in data
105 .annotations
106 .iter_mut()
107 .filter(|a| a.owner == owner)
108 .map(|a| &mut a.target)
109 .chain(
110 data.plays
111 .iter_mut()
112 .filter(|p| p.owner == owner)
113 .map(|p| &mut p.track),
114 )
115 .chain(
116 data.counts
117 .iter_mut()
118 .filter(|c| c.owner == owner)
119 .map(|c| &mut c.track),
120 )
121 .chain(
122 data.relation_annotations
123 .iter_mut()
124 .filter(|a| a.owner == owner)
125 .flat_map(|a| [&mut a.relation.source, &mut a.relation.target]),
126 )
127 .chain(
128 data.scrobbles
129 .iter_mut()
130 .filter(|p| p.owner == owner)
131 .map(|p| &mut p.track),
132 )
133 .chain(
134 data.playlists
135 .iter_mut()
136 .filter(|p| p.owner == owner)
137 .flat_map(|p| &mut p.tracks),
138 )
139 {
140 if target == from {
141 *target = to.clone();
142 }
143 }
144}
145
146impl UserData {
147 pub fn preview_relink(
153 &self,
154 owner: &str,
155 from: &EntityRef,
156 to: &EntityRef,
157 catalog: &Catalog,
158 ) -> Result<RelinkSummary, String> {
159 if from.kind != to.kind {
160 return Err("reattachment requires two references of the same kind".into());
161 }
162 if from.resolve(catalog).is_some() {
163 return Err(
164 "the source still exists; only waiting references can be reattached".into(),
165 );
166 }
167 if to.resolve(catalog).is_none() {
168 return Err("the destination is not in the catalog".into());
169 }
170 let records = snapshot(self, owner, from);
171 if !has_records(&records) {
172 return Err("no personal record for this owner uses the source reference".into());
173 }
174 let destination_id = to.resolve(catalog);
175 let conflicting = self
176 .annotations
177 .iter()
178 .filter(|a| a.owner == owner)
179 .map(|a| &a.target)
180 .chain(
181 self.plays
182 .iter()
183 .filter(|p| p.owner == owner)
184 .map(|p| &p.track),
185 )
186 .chain(
187 self.counts
188 .iter()
189 .filter(|c| c.owner == owner)
190 .map(|c| &c.track),
191 )
192 .chain(
193 self.relation_annotations
194 .iter()
195 .filter(|a| a.owner == owner)
196 .flat_map(|a| [&a.relation.source, &a.relation.target]),
197 )
198 .chain(
199 self.scrobbles
200 .iter()
201 .filter(|p| p.owner == owner)
202 .map(|p| &p.track),
203 )
204 .chain(
205 self.playlists
206 .iter()
207 .filter(|p| p.owner == owner)
208 .flat_map(|p| &p.tracks),
209 )
210 .any(|reference| {
211 reference.kind == to.kind && reference.resolve(catalog) == destination_id
212 });
213 if conflicting {
214 return Err("the destination already has personal data; reattachment would merge or overwrite it".into());
215 }
216 Ok(summary(&records))
217 }
218
219 pub fn relink(
221 &mut self,
222 owner: &str,
223 from: &EntityRef,
224 to: &EntityRef,
225 catalog: &Catalog,
226 now: u64,
227 ) -> Result<u64, String> {
228 self.preview_relink(owner, from, to, catalog)?;
229 let destination = to
230 .resolve(catalog)
231 .and_then(|id| EntityRef::of(catalog, to.kind, id))
232 .ok_or("the destination cannot be named stably")?;
233 if destination != *to {
234 self.preview_relink(owner, from, &destination, catalog)?;
235 }
236 let id = self
237 .relinks
238 .iter()
239 .map(|row| row.id)
240 .max()
241 .unwrap_or(0)
242 .checked_add(1)
243 .ok_or("reattachment identifiers are exhausted")?;
244 let before = snapshot(self, owner, from);
245 rewrite(self, owner, from, &destination);
246 self.relinks.push(Relink {
247 id,
248 owner: owner.into(),
249 from: from.clone(),
250 to: destination,
251 at: now,
252 undone_at: None,
253 origin_id: id,
254 before,
255 });
256 reconcile(self, catalog);
257 Ok(id)
258 }
259
260 pub fn preview_undo_relink(&self, owner: &str, id: u64) -> Result<RelinkSummary, String> {
265 let event = self
266 .relinks
267 .iter()
268 .find(|row| row.id == id && row.owner == owner)
269 .ok_or("no reattachment with that ID belongs to this owner")?;
270 if event.undone_at.is_some() {
271 return Err("this reattachment has already been undone".into());
272 }
273 if has_records(&snapshot(self, owner, &event.from)) {
274 return Err(
275 "the original reference now has personal data; undo would overwrite it".into(),
276 );
277 }
278 let mut expected = event.before.clone();
279 rewrite(&mut expected, owner, &event.from, &event.to);
280 let current = snapshot(self, owner, &event.to);
281 if current.annotations != expected.annotations
282 || current.plays != expected.plays
283 || current.counts != expected.counts
284 || current.relation_annotations != expected.relation_annotations
285 || current.playlists != expected.playlists
286 || current.scrobbles != expected.scrobbles
287 {
288 return Err(
289 "the reattached records have changed; undo would discard newer personal data"
290 .into(),
291 );
292 }
293 Ok(summary(&expected))
294 }
295
296 pub fn undo_relink(&mut self, owner: &str, id: u64, now: u64) -> Result<RelinkSummary, String> {
298 let result = self.preview_undo_relink(owner, id)?;
299 let event = self
300 .relinks
301 .iter()
302 .find(|row| row.id == id && row.owner == owner)
303 .ok_or("reattachment is missing")?;
304 let (from, to) = (event.from.clone(), event.to.clone());
305 rewrite(self, owner, &to, &from);
306 if let Some(event) = self
307 .relinks
308 .iter_mut()
309 .find(|row| row.id == id && row.owner == owner)
310 {
311 event.undone_at = Some(now);
312 }
313 Ok(result)
314 }
315}
316
317impl Relink {
318 pub(super) fn to_json(&self) -> Json {
319 let mut row = Json::obj();
320 row.set("id", self.id.into());
321 row.set("origin_id", self.origin_id.into());
322 row.set("owner", self.owner.as_str().into());
323 row.set("from", self.from.to_token().into());
324 row.set("to", self.to.to_token().into());
325 row.set("at", self.at.into());
326 if let Some(at) = self.undone_at {
327 row.set("undone_at", at.into());
328 }
329 row.set("before", super::to_json(&self.before));
330 row
331 }
332
333 pub(super) fn from_json(row: &Json) -> Option<Self> {
334 let before = row.get("before")?;
335 if !before
337 .get("relinks")
338 .and_then(Json::as_arr)
339 .unwrap_or(&[])
340 .is_empty()
341 {
342 return None;
343 }
344 let id = row.field_u64("id").filter(|id| *id > 0)?;
345 let event = Self {
346 id,
347 origin_id: row
348 .field_u64("origin_id")
349 .filter(|id| *id > 0)
350 .unwrap_or(id),
351 owner: row.field_str("owner")?,
352 from: EntityRef::parse_token(&row.field_str("from")?)?,
353 to: EntityRef::parse_token(&row.field_str("to")?)?,
354 at: row.field_u64("at")?,
355 undone_at: row.field_u64("undone_at"),
356 before: super::from_json(before).ok()?,
357 };
358 if event.from.kind != event.to.kind || !has_records(&event.before) {
359 return None;
360 }
361 let selected = snapshot(&event.before, &event.owner, &event.from);
362 if selected.annotations != event.before.annotations
363 || selected.plays != event.before.plays
364 || selected.counts != event.before.counts
365 || selected.relation_annotations != event.before.relation_annotations
366 || selected.playlists != event.before.playlists
367 || selected.scrobbles != event.before.scrobbles
368 {
369 return None;
370 }
371 Some(event)
372 }
373}
374
375#[cfg(test)]
376#[path = "user_relink_tests.rs"]
377mod tests;