anni_repo/
manager.rs

1use crate::prelude::*;
2use anni_common::fs;
3use indexmap::IndexSet;
4use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use uuid::Uuid;
8
9/// A simple repository visitor. Can perform simple operations on the repository.
10pub struct RepositoryManager {
11    root: PathBuf,
12    repo: Repository,
13}
14
15impl RepositoryManager {
16    pub fn new<P>(root: P) -> RepoResult<Self>
17    where
18        P: AsRef<Path>,
19    {
20        let repo = root.as_ref().join("repo.toml");
21
22        #[cfg(feature = "git")]
23        crate::utils::git::setup_git2_internal();
24
25        Ok(Self {
26            root: root.as_ref().to_owned(),
27            repo: Repository::from_str(&fs::read_to_string(repo)?)?,
28        })
29    }
30
31    #[cfg(feature = "git")]
32    pub fn clone<P>(url: &str, root: P) -> RepoResult<Self>
33    where
34        P: AsRef<Path>,
35    {
36        crate::utils::git::setup_git2_internal();
37        git2::Repository::clone(url, root.as_ref())?;
38        Self::new(root.as_ref())
39    }
40
41    #[cfg(feature = "git")]
42    pub fn pull<P>(root: P, branch: &str) -> RepoResult<Self>
43    where
44        P: AsRef<Path>,
45    {
46        crate::utils::git::setup_git2_internal();
47        crate::utils::git::pull(root.as_ref(), branch)?;
48        Self::new(root.as_ref())
49    }
50
51    pub fn name(&self) -> &str {
52        self.repo.name()
53    }
54
55    pub fn edition(&self) -> &str {
56        self.repo.edition()
57    }
58
59    // Get all album roots.
60    fn album_roots(&self) -> Vec<PathBuf> {
61        self.repo
62            .albums()
63            .iter()
64            .map(|album| self.root.join(album))
65            .collect()
66    }
67
68    fn default_album_root(&self) -> PathBuf {
69        self.root.join(
70            self.repo
71                .albums()
72                .get(0)
73                .map_or_else(|| "album", String::as_str),
74        )
75    }
76
77    /// Get all album paths.
78    /// TODO: use iterator
79    pub fn all_album_paths(&self) -> RepoResult<Vec<PathBuf>> {
80        let mut paths = Vec::new();
81        for root in self.album_roots() {
82            let files = fs::read_dir(root)?;
83            for file in files {
84                let file = file?;
85                let path = file.path();
86                if path.is_file() {
87                    if let Some(ext) = path.extension() {
88                        if ext == "toml" {
89                            paths.push(path);
90                        }
91                    }
92                } else if path.is_dir() {
93                    let mut index = 0;
94                    let catalog = file.file_name();
95                    loop {
96                        let path = path.join(&catalog).with_extension(format!("{index}.toml"));
97                        if path.exists() {
98                            paths.push(path);
99                            index += 1;
100                        } else {
101                            break;
102                        }
103                    }
104                }
105            }
106        }
107        Ok(paths)
108    }
109
110    /// Get album paths with given catalog.
111    pub fn album_paths(&self, catalog: &str) -> RepoResult<Vec<PathBuf>> {
112        let mut paths = Vec::new();
113        for root in self.album_roots() {
114            let file = root.join(format!("{catalog}.toml"));
115            if file.exists() {
116                // toml exists
117                paths.push(file);
118            } else {
119                let folder = root.join(catalog);
120                if folder.exists() {
121                    // folder /{catalog} exists
122                    for file in fs::read_dir(folder)? {
123                        let dir = file?;
124                        if dir.path().extension() == Some("toml".as_ref()) {
125                            paths.push(dir.path());
126                        }
127                    }
128                }
129            }
130        }
131        Ok(paths)
132    }
133
134    /// Load album with given path.
135    fn load_album<P>(&self, path: P) -> RepoResult<Album>
136    where
137        P: AsRef<Path>,
138    {
139        let input = fs::read_to_string(path.as_ref())?;
140        Album::from_str(&input)
141    }
142
143    /// Load album(s) with given catalog.
144    pub fn load_albums(&self, catalog: &str) -> RepoResult<Vec<Album>> {
145        Ok(self
146            .album_paths(catalog)?
147            .into_iter()
148            .filter_map(|path| {
149                let album = self.load_album(&path);
150                match album {
151                    Ok(album) => Some(album),
152                    Err(err) => {
153                        log::error!("Failed to load album in {path:?}: {err}",);
154                        None
155                    }
156                }
157            })
158            .collect())
159    }
160
161    /// Add new album to the repository.
162    pub fn add_album(&self, mut album: Album, allow_duplicate: bool) -> RepoResult<()> {
163        let catalog = album.catalog();
164        let folder = self.default_album_root().join(catalog);
165        let file = folder.with_extension("toml");
166
167        if folder.exists() {
168            // multiple albums with the same catalog exists
169            let count = fs::PathWalker::new(&folder, false, false, Default::default())
170                .filter(|p|
171                    // p.extension is toml
172                    p.extension() == Some("toml".as_ref()))
173                .count();
174            let new_file_name = format!("{catalog}.{count}.toml");
175            fs::write(folder.join(new_file_name), album.format_to_string())?;
176        } else if file.exists() {
177            // album with the same catalog exists
178            if !allow_duplicate {
179                return Err(Error::RepoAlbumExists(catalog.to_string()));
180            }
181            // make sure the folder exists
182            fs::create_dir_all(&folder)?;
183            // move the old toml file to folder
184            fs::rename(file, folder.join(format!("{catalog}.0.toml")))?;
185            // write new toml file
186            fs::write(
187                folder.join(format!("{catalog}.1.toml")),
188                album.format_to_string(),
189            )?;
190        } else {
191            // no catalog with given catalog exists
192            fs::write(&file, album.format_to_string())?;
193        }
194        Ok(())
195    }
196
197    pub fn into_owned_manager(self) -> RepoResult<OwnedRepositoryManager> {
198        OwnedRepositoryManager::new(self)
199    }
200
201    pub fn root(&self) -> &Path {
202        self.root.as_path()
203    }
204}
205
206/// A repository manager which own full copy of a repo.
207///
208/// This is helpful when you need to perform a full-repo operation,
209/// such as ring check on tags, full-repo validation, etc.
210pub struct OwnedRepositoryManager {
211    pub repo: RepositoryManager,
212
213    /// All available tags.
214    tags: HashMap<String, HashMap<TagType, Tag>>,
215    /// Parent to child tag relation
216    tags_relation: HashMap<TagRef<'static>, IndexSet<TagRef<'static>>>,
217    /// Tag -> File
218    tag_path: HashMap<TagRef<'static>, PathBuf>,
219
220    album_tags: HashMap<TagRef<'static>, Vec<Uuid>>,
221    /// AlbumID -> Album
222    albums: HashMap<Uuid, Album>,
223    /// AlbumID -> Album Path
224    album_path: HashMap<Uuid, PathBuf>,
225}
226
227impl OwnedRepositoryManager {
228    pub fn new(repo: RepositoryManager) -> RepoResult<Self> {
229        let mut repo = Self {
230            repo,
231            tags: Default::default(),
232            tags_relation: Default::default(),
233            tag_path: Default::default(),
234            album_tags: Default::default(),
235            albums: Default::default(),
236            album_path: Default::default(),
237        };
238
239        // create lock file so that other anni repository managers can not visit the repo
240        let lock_file = repo.lock_file();
241        if lock_file.exists() {
242            return Err(Error::RepoInUse);
243        }
244
245        fs::write(lock_file, "")?;
246        repo.load_tags()?;
247        repo.load_albums()?;
248
249        Ok(repo)
250    }
251
252    fn lock_file(&self) -> PathBuf {
253        self.repo.root().join(".repo_lock")
254    }
255
256    pub fn album(&self, album_id: &Uuid) -> Option<&Album> {
257        self.albums.get(album_id)
258    }
259
260    pub fn album_path(&self, album_id: &Uuid) -> Option<&Path> {
261        self.album_path.get(album_id).map(|p| p.as_path())
262    }
263
264    pub fn albums(&self) -> &HashMap<Uuid, Album> {
265        &self.albums
266    }
267
268    pub fn albums_iter(&self) -> impl Iterator<Item = &Album> {
269        self.albums.values()
270    }
271
272    pub fn tag(&self, tag: &TagRef<'_>) -> Option<&Tag> {
273        self.tags
274            .get(tag.name())
275            .and_then(|tags| tags.get(tag.tag_type()))
276    }
277
278    pub fn tags_iter(&self) -> impl Iterator<Item = &Tag> {
279        self.tags.values().flat_map(|m| m.values())
280    }
281
282    pub fn tag_path<'a>(&'a self, tag: &'a TagRef<'_>) -> Option<&'a PathBuf> {
283        self.tag_path.get(tag)
284    }
285
286    pub fn child_tags<'me, 'tag>(&'me self, tag: &TagRef<'tag>) -> IndexSet<&'me TagRef<'tag>>
287    where
288        'tag: 'me,
289    {
290        self.tags_relation
291            .get(tag)
292            .map_or(IndexSet::new(), |children| children.iter().collect())
293    }
294
295    pub fn albums_tagged_by<'me, 'tag>(&'me self, tag: &'me TagRef<'tag>) -> Option<&'me Vec<Uuid>>
296    where
297        'tag: 'me,
298    {
299        self.album_tags.get(tag)
300    }
301
302    fn add_tag(&mut self, tag: Tag, tag_relative_path: PathBuf) -> Result<(), Error> {
303        // fully duplicated tags are not allowed
304        if let Some(tag) = self.tag(tag.as_ref()) {
305            return Err(Error::RepoTagDuplicated(tag.get_owned_ref()));
306        }
307
308        let map = if let Some(map) = self.tags.get_mut(tag.name()) {
309            map
310        } else {
311            let map = HashMap::with_capacity(1);
312            self.tags.insert(tag.name().to_string(), map);
313            self.tags.get_mut(tag.name()).unwrap()
314        };
315        let tag_ref = tag.get_owned_ref();
316        map.insert(tag_ref.tag_type().clone(), tag);
317        self.tag_path.insert(tag_ref, tag_relative_path);
318        Ok(())
319    }
320
321    fn add_tag_relation(&mut self, parent: TagRef<'static>, child: TagRef<'static>) {
322        if let Some(children) = self.tags_relation.get_mut(&parent) {
323            children.insert(child);
324        } else {
325            let mut set = IndexSet::new();
326            set.insert(child);
327            self.tags_relation.insert(parent, set);
328        }
329    }
330
331    /// Load tags into self.tags.
332    fn load_tags(&mut self) -> RepoResult<()> {
333        // filter out toml files
334        let tags_path =
335            fs::PathWalker::new(self.repo.root.join("tag"), true, false, Default::default())
336                .filter(|p| p.extension().map(|e| e == "toml").unwrap_or(false));
337
338        // clear tags
339        self.tags.clear();
340        self.tags_relation.clear();
341
342        // iterate over tag files
343        for tag_file in tags_path {
344            let text = fs::read_to_string(&tag_file)?;
345            let tags = toml::from_str::<Tags>(&text)
346                .map_err(|e| Error::TomlParseError {
347                    target: "Tags",
348                    input: text,
349                    err: e,
350                })?
351                .into_inner();
352            let relative_path = pathdiff::diff_paths(&tag_file, &self.repo.root).unwrap();
353
354            for tag in tags {
355                for parent in tag.parents() {
356                    self.add_tag_relation(parent.0.clone(), tag.get_owned_ref());
357                }
358
359                // add children to set
360                for child in tag.simple_children() {
361                    let parent = tag.get_owned_ref();
362                    let full = child.clone().into_full(vec![parent.into()]);
363                    self.add_tag(full, relative_path.clone())?;
364                    self.add_tag_relation(tag.get_owned_ref(), child.clone());
365                }
366
367                self.add_tag(tag, relative_path.clone())?;
368            }
369        }
370
371        // check tag relationship
372        let all_tags: HashSet<_> = self.tags_iter().map(Tag::as_ref).collect();
373        let mut rel_tags: HashSet<_> = self.tags_relation.keys().collect();
374        let rel_children: HashSet<_> = self.tags_relation.values().flatten().collect();
375        rel_tags.extend(rel_children);
376        if !rel_tags.is_subset(&all_tags) {
377            return Err(Error::RepoTagsUndefined(
378                rel_tags.difference(&all_tags).cloned().cloned().collect(),
379            ));
380        }
381
382        Ok(())
383    }
384
385    fn load_albums(&mut self) -> RepoResult<()> {
386        self.album_tags.clear();
387
388        let mut problems = vec![];
389        for path in self.repo.all_album_paths()? {
390            let mut album = self.repo.load_album(&path)?;
391            album.resolve_tags(&self.tags)?;
392
393            let album_id = album.album_id();
394            let catalog = album.catalog();
395            let tags = album.tags();
396            if tags.is_empty() {
397                // this album has no tag
398                log::warn!(
399                    "No tag found in album {}, catalog = {}",
400                    album.album_id(),
401                    catalog,
402                );
403            } else {
404                for tag_ref in tags {
405                    if let None = self.tag(tag_ref) {
406                        log::error!(
407                            "Orphan tag {tag_ref} found in album {album_id}, catalog = {catalog}"
408                        );
409                        problems.push(Error::RepoTagsUndefined(vec![tag_ref.clone()]));
410                    }
411
412                    if !self.album_tags.contains_key(tag_ref) {
413                        self.album_tags.insert(tag_ref.clone(), vec![]);
414                    }
415                    self.album_tags.get_mut(tag_ref).unwrap().push(album_id);
416                }
417            }
418            if let Some(album_with_same_id) = self.albums.insert(album_id, album) {
419                log::error!(
420                    "Duplicated album id detected: {}",
421                    album_with_same_id.album_id()
422                );
423                problems.push(Error::RepoDuplicatedAlbumId(album_id.to_string()));
424            }
425            self.album_path.insert(
426                album_id,
427                pathdiff::diff_paths(&path, &self.repo.root).unwrap(),
428            );
429        }
430
431        if problems.is_empty() {
432            Ok(())
433        } else {
434            Err(Error::MultipleErrors(problems))
435        }
436    }
437
438    pub fn check_tags_loop<'me, 'tag>(&'me self) -> Option<Vec<&'me TagRef<'tag>>>
439    where
440        'me: 'tag,
441    {
442        fn dfs<'tag, 'func>(
443            tag: &'tag TagRef<'tag>,
444            tags_relation: &'tag HashMap<TagRef<'static>, IndexSet<TagRef<'static>>>,
445            current: &'func mut HashMap<&'tag TagRef<'tag>, bool>,
446            visited: &'func mut HashMap<&'tag TagRef<'tag>, bool>,
447            mut path: Vec<&'tag TagRef<'tag>>,
448        ) -> (bool, Vec<&'tag TagRef<'tag>>) {
449            visited.insert(tag, true);
450            current.insert(tag, true);
451            path.push(tag);
452
453            if let Some(children) = tags_relation.get(tag) {
454                for child in children {
455                    if let Some(true) = current.get(child) {
456                        path.push(child);
457                        return (true, path);
458                    }
459                    // if !visited[child]
460                    if !visited.get(child).map_or(false, |x| *x) {
461                        let (loop_detected, loop_path) =
462                            dfs(child, tags_relation, current, visited, path);
463                        if loop_detected {
464                            return (true, loop_path);
465                        } else {
466                            path = loop_path;
467                        }
468                    }
469                }
470            }
471
472            current.insert(tag, false);
473            path.pop();
474            (false, path)
475        }
476
477        let mut visited: HashMap<&TagRef, bool> = Default::default();
478        let mut current: HashMap<&TagRef, bool> = Default::default();
479        let tags: Vec<_> = self.tags_iter().map(|t| t.as_ref()).collect();
480        for tag in tags.into_iter() {
481            // if !visited[tag]
482            if !visited.get(&tag).map_or(false, |x| *x) {
483                let (loop_detected, path) = dfs(
484                    tag,
485                    &self.tags_relation,
486                    &mut current,
487                    &mut visited,
488                    Default::default(),
489                );
490                if loop_detected {
491                    return Some(path);
492                }
493            }
494        }
495
496        None
497    }
498
499    #[cfg(feature = "db-write")]
500    pub fn to_database<P>(&self, database_path: P) -> RepoResult<()>
501    where
502        P: AsRef<Path>,
503    {
504        use std::time::{SystemTime, UNIX_EPOCH};
505
506        // remove database first
507        let _ = std::fs::remove_file(database_path.as_ref());
508
509        let db = crate::db::RepoDatabaseWrite::create(database_path.as_ref())?;
510        // TODO: get url / ref from repo
511        db.write_info(self.repo.name(), self.repo.edition(), "", "")?;
512
513        // Write all tags
514        let tags = self.tags_iter();
515        db.add_tags(tags)?;
516
517        // Write all albums
518        for album in self.albums_iter() {
519            db.add_album(album)?;
520        }
521
522        // Create Index
523        db.create_index()?;
524
525        // Creation time
526        fs::write(
527            database_path.as_ref().with_file_name("repo.json"),
528            format!(
529                "{{\"last_modified\": {}}}",
530                SystemTime::now()
531                    .duration_since(UNIX_EPOCH)
532                    .unwrap()
533                    .as_secs()
534            ),
535        )?;
536        Ok(())
537    }
538
539    #[cfg(feature = "search")]
540    pub fn build_search_index<P>(&self, path: P)
541    where
542        P: AsRef<Path>,
543    {
544        use crate::search::RepositorySearchManager;
545
546        let searcher = RepositorySearchManager::create(path).unwrap();
547        let mut index_writer = searcher.index.writer(100_000_000).unwrap();
548
549        for album in self.albums_iter() {
550            for (disc_id_v, disc) in album.iter().enumerate() {
551                let disc_id_v = disc_id_v + 1;
552                for (track_id_v, track) in disc.iter().enumerate() {
553                    let track_id_v = track_id_v + 1;
554                    index_writer
555                        .add_document(searcher.build_document(
556                            track.title(),
557                            track.artist(),
558                            &album.album_id,
559                            disc_id_v as i64,
560                            track_id_v as i64,
561                        ))
562                        .unwrap();
563                }
564            }
565        }
566        index_writer.commit().unwrap();
567    }
568}
569
570impl Drop for OwnedRepositoryManager {
571    fn drop(&mut self) {
572        let lock_file = self.lock_file();
573        // it should exist. If it does not exist, then something wrong happened
574        // TODO: add detection for this case
575        if lock_file.exists() {
576            let _ = std::fs::remove_file(lock_file);
577        }
578    }
579}