soma/repository/
backend.rs

1use std::fmt::{self, Display};
2use std::path::{Path, PathBuf};
3
4use fs_extra::dir;
5use git2::{BranchType, ObjectType, Repository as GitRepository, ResetType};
6use remove_dir_all::remove_dir_all;
7use serde::{Deserialize, Serialize};
8use url::Url;
9
10use crate::prelude::*;
11
12#[typetag::serde(tag = "type")]
13pub trait Backend: BackendClone + Display {
14    fn update_at_path(&self, local_path: &Path) -> SomaResult<()>;
15}
16
17pub trait BackendClone {
18    fn clone_box(&self) -> Box<dyn Backend>;
19}
20
21impl<T> BackendClone for T
22where
23    T: 'static + Backend + Clone,
24{
25    fn clone_box(&self) -> Box<dyn Backend> {
26        Box::new(self.clone())
27    }
28}
29
30impl Clone for Box<dyn Backend> {
31    fn clone(&self) -> Box<dyn Backend> {
32        self.clone_box()
33    }
34}
35
36pub trait BackendExt: Backend {
37    fn update_at(&self, local_path: impl AsRef<Path>) -> SomaResult<()> {
38        self.update_at_path(local_path.as_ref())
39    }
40}
41
42impl<T> BackendExt for T where T: ?Sized + Backend {}
43
44pub fn location_to_backend(repo_location: &str) -> SomaResult<(String, Box<dyn Backend>)> {
45    let path = Path::new(repo_location);
46    if path.is_dir() {
47        // local backend
48        Ok((
49            path.file_name()
50                .ok_or(SomaError::FileNameNotFound)?
51                .to_str()
52                .ok_or(SomaError::InvalidUnicode)?
53                .to_lowercase(),
54            Box::new(LocalBackend::new(path.canonicalize()?.to_owned())),
55        ))
56    } else {
57        // git backend
58        let parsed_url = Url::parse(repo_location).or(Err(SomaError::RepositoryNotFound))?;
59        let last_name = parsed_url
60            .path_segments()
61            .ok_or(SomaError::RepositoryNotFound)?
62            .last()
63            .ok_or(SomaError::FileNameNotFound)?;
64        let repo_name = if last_name.ends_with(".git") {
65            &last_name[..last_name.len() - 4]
66        } else {
67            &last_name
68        };
69        Ok((
70            repo_name.to_lowercase(),
71            Box::new(GitBackend::new(repo_location.to_owned())),
72        ))
73    }
74}
75
76#[derive(Clone, Deserialize, Serialize)]
77pub struct GitBackend {
78    url: String,
79}
80
81impl GitBackend {
82    pub fn new(url: String) -> Self {
83        GitBackend { url }
84    }
85}
86
87#[typetag::serde]
88impl Backend for GitBackend {
89    fn update_at_path(&self, local_path: &Path) -> SomaResult<()> {
90        let git_repo = GitRepository::open(local_path)
91            .or_else(|_| GitRepository::clone(&self.url, local_path))?;
92        git_repo
93            .find_remote("origin")?
94            .fetch(&["master"], None, None)?;
95
96        let origin_master = git_repo.find_branch("origin/master", BranchType::Remote)?;
97        let head_commit = origin_master.get().peel(ObjectType::Commit)?;
98        git_repo.reset(&head_commit, ResetType::Hard, None)?;
99
100        Ok(())
101    }
102}
103
104impl Display for GitBackend {
105    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106        write!(f, "Git: {}", &self.url)
107    }
108}
109
110// On Windows, this path corresponds to extended length path
111// which means we can only join backslash-delimited paths to it
112#[derive(Clone, Deserialize, Serialize)]
113pub struct LocalBackend {
114    origin: PathBuf,
115}
116
117impl LocalBackend {
118    pub fn new(origin: PathBuf) -> Self {
119        LocalBackend { origin }
120    }
121}
122
123#[typetag::serde]
124impl Backend for LocalBackend {
125    fn update_at_path(&self, local_path: &Path) -> SomaResult<()> {
126        if local_path.exists() {
127            remove_dir_all(local_path)?;
128        }
129
130        let mut copy_options = dir::CopyOptions::new();
131        copy_options.copy_inside = true;
132        dir::copy(&self.origin, local_path, &copy_options)?;
133
134        Ok(())
135    }
136}
137
138impl Display for LocalBackend {
139    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140        write!(f, "Local: {}", self.origin.to_string_lossy())
141    }
142}