bacup/remotes/
git.rs

1// Copyright 2022 Paolo Galeone <nessuno@nerdz.eu>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::config::{GitConfig, SshConfig};
16use crate::remotes::remote;
17use crate::remotes::ssh;
18
19use tokio::fs;
20use tokio::fs::File;
21use tokio::io::AsyncWriteExt;
22
23use std::io;
24
25use std::path::{Path, PathBuf};
26
27use std::fmt;
28use std::string::String;
29
30use which::which;
31
32use async_trait::async_trait;
33
34use scopeguard::defer;
35
36use std::process::Command;
37
38#[derive(Debug)]
39pub enum Error {
40    InvalidPrivateKey(String),
41    CommandNotFound(which::Error),
42    RuntimeError(io::Error),
43    DoesNotExist(PathBuf),
44}
45
46impl From<which::Error> for Error {
47    fn from(error: which::Error) -> Self {
48        Error::CommandNotFound(error)
49    }
50}
51
52impl From<io::Error> for Error {
53    fn from(error: io::Error) -> Self {
54        Error::RuntimeError(error)
55    }
56}
57
58impl From<ssh::Error> for Error {
59    fn from(error: ssh::Error) -> Self {
60        match error {
61            ssh::Error::CommandNotFound(e) => Error::CommandNotFound(e),
62            ssh::Error::InvalidPrivateKey(e) => Error::InvalidPrivateKey(e),
63            ssh::Error::RuntimeError(e) => Error::RuntimeError(e),
64        }
65    }
66}
67
68impl std::error::Error for Error {}
69
70impl fmt::Display for Error {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Error::CommandNotFound(ref error) => write!(f, "Command not found: {}", error),
74            Error::InvalidPrivateKey(ref msg) => write!(f, "Invalid private key: {}", msg),
75            Error::RuntimeError(ref error) => write!(f, "Error while reading/writing: {}", error),
76            Error::DoesNotExist(ref path) => write!(f, "Path {} does not exist", path.display()),
77        }
78    }
79}
80
81impl From<Error> for remote::Error {
82    fn from(error: Error) -> Self {
83        match error {
84            Error::CommandNotFound(error) => {
85                remote::Error::LocalError(std::io::Error::other(error))
86            }
87            Error::InvalidPrivateKey(msg) => remote::Error::LocalError(std::io::Error::other(msg)),
88            Error::RuntimeError(error) => remote::Error::LocalError(std::io::Error::other(error)),
89            Error::DoesNotExist(path) => {
90                remote::Error::LocalError(std::io::Error::other(path.to_str().unwrap()))
91            }
92        }
93    }
94}
95
96#[derive(Clone)]
97pub struct Git {
98    pub remote_name: String,
99    pub config: GitConfig,
100    pub git_cmd: PathBuf,
101}
102
103impl Git {
104    pub async fn new(config: GitConfig, remote_name: &str) -> Result<Git, Error> {
105        // Instantiate an ssh remote that will check for us the validity of
106        // all the ssh parameters
107        let ssh_config = SshConfig {
108            host: config.host.clone(),
109            port: config.port,
110            private_key: config.private_key.clone(),
111            username: config.username.clone(),
112        };
113        ssh::Ssh::new(ssh_config, remote_name).await?;
114
115        let git_cmd = which("git")?;
116        Ok(Git {
117            remote_name: String::from(remote_name),
118            config,
119            git_cmd,
120        })
121    }
122
123    fn clone_repository(&self) -> Result<PathBuf, Error> {
124        let dest = PathBuf::from(&self.config.repository.split('/').next_back().unwrap());
125        if dest.exists() {
126            let git_repo = dest.join(".git");
127            if git_repo.exists() && git_repo.is_dir() {
128                return Ok(dest);
129            }
130        }
131        let url = format!(
132            "ssh://{}@{}:{}/{}",
133            &self.config.username, &self.config.host, &self.config.port, &self.config.repository
134        );
135
136        let status = Command::new(&self.git_cmd)
137            .args(["clone", &url, "--depth", "1"])
138            .status()?;
139        if !status.success() {
140            return Err(Error::RuntimeError(io::Error::other(format!(
141                "Unable to execute {} clone {} --depth 1",
142                self.git_cmd.display(),
143                &url
144            ))));
145        }
146
147        let dest = PathBuf::from(&self.config.repository.split('/').next_back().unwrap());
148        if !dest.exists() {
149            return Err(Error::DoesNotExist(dest));
150        }
151        Ok(dest)
152    }
153}
154
155#[async_trait]
156impl remote::Remote for Git {
157    fn name(&self) -> String {
158        self.remote_name.clone()
159    }
160
161    async fn enumerate(&self, _remote_path: &Path) -> Result<Vec<String>, remote::Error> {
162        Err(remote::Error::LocalError(io::Error::other(
163            "enumerate is not possibile on Git remote!",
164        )))
165    }
166
167    async fn delete(&self, _remote_path: &Path) -> Result<(), remote::Error> {
168        Err(remote::Error::LocalError(io::Error::other(
169            "delete makes no sense on git remote. Change the repo, and upload the new repo status.",
170        )))
171    }
172
173    async fn upload_file(&self, path: &Path, remote_path: &Path) -> Result<(), remote::Error> {
174        let repo = self.clone_repository()?;
175
176        // cp file <repo_location>/[<subdir>]
177        let dest = repo.join(remote_path.strip_prefix("/").unwrap());
178        if !dest.exists() {
179            fs::create_dir_all(&dest).await.unwrap();
180        }
181        fs::copy(path, dest.join(path.file_name().unwrap())).await?;
182
183        // cd <repo path>
184        let cwd = std::env::current_dir()?;
185        defer! {
186            #[allow(unused_must_use)] {
187            std::env::set_current_dir(cwd);
188            }
189        }
190        std::env::set_current_dir(&dest)?;
191
192        // git switch -c branch (ignore failures - we might be in the branch already)
193        Command::new(&self.git_cmd)
194            .args(["switch", "-c", &self.config.branch])
195            .status()?;
196
197        // git pull origin branch (ignore failures)
198        Command::new(&self.git_cmd)
199            .args(["pull", "origin", &self.config.branch])
200            .status()?;
201
202        // git add . -A
203        let status = Command::new(&self.git_cmd)
204            .args(["add", ".", "-A"])
205            .status()?;
206        if !status.success() {
207            return Err(remote::Error::LocalError(io::Error::other(format!(
208                "Unable to execute git add . -A into {}",
209                dest.display()
210            ))));
211        }
212        // git commit -m '[bacup] snapshot'
213        let status = Command::new(&self.git_cmd)
214            .args(["commit", "-m", "[bacup] snapshot"])
215            .status()?;
216        if !status.success() {
217            return Err(remote::Error::LocalError(io::Error::other(format!(
218                "Unable to execute git commit -m [bacup] snapshot into {}",
219                dest.display()
220            ))));
221        }
222        // git push origin <branch>
223        let status = Command::new(&self.git_cmd)
224            .args(["push", "origin", &self.config.branch])
225            .status()?;
226        if !status.success() {
227            return Err(remote::Error::LocalError(io::Error::other(format!(
228                "Unable to execute git add . -A into {}",
229                dest.display()
230            ))));
231        }
232        Ok(())
233    }
234
235    async fn upload_file_compressed(
236        &self,
237        path: &Path,
238        remote_path: &Path,
239    ) -> Result<(), remote::Error> {
240        // Read and compress
241        let compressed_bytes = self.compress_file(path).await?;
242        let remote_path = self.remote_compressed_file_path(remote_path);
243
244        let mut buffer = File::create(&remote_path).await?;
245        buffer.write_all(&compressed_bytes).await?;
246
247        defer! {
248            #[allow(unused_must_use)]
249            {
250                fs::remove_file(&remote_path);
251            }
252        }
253        self.upload_file(&remote_path, &remote_path).await?;
254        Ok(())
255    }
256
257    async fn upload_folder(
258        &self,
259        paths: &[PathBuf],
260        remote_path: &Path,
261    ) -> Result<(), remote::Error> {
262        let repo = self.clone_repository()?;
263
264        // cp file <repo_location>/[<subdir>]
265        let dest = repo.join(remote_path.strip_prefix("/").unwrap());
266        if !dest.exists() {
267            fs::create_dir_all(&dest).await.unwrap();
268        }
269        let git_folder = std::path::Component::Normal(".git".as_ref());
270        for path in paths.iter() {
271            // Skip .git and content of this folder
272            if path.components().any(|x| x == git_folder) {
273                continue;
274            }
275            if path.is_dir() {
276                fs::create_dir_all(dest.join(path.file_name().unwrap())).await?;
277            } else {
278                fs::copy(path, dest.join(path.file_name().unwrap())).await?;
279            }
280        }
281
282        // cd <repo path>
283        let cwd = std::env::current_dir()?;
284        defer! {
285            #[allow(unused_must_use)] {
286            std::env::set_current_dir(cwd);
287            }
288        }
289        std::env::set_current_dir(&dest)?;
290
291        // git switch -c branch (ignore failures - we might be in the branch already)
292        Command::new(&self.git_cmd)
293            .args(["switch", "-c", &self.config.branch])
294            .status()?;
295
296        // git pull origin branch (ignore failures)
297        Command::new(&self.git_cmd)
298            .args(["pull", "origin", &self.config.branch])
299            .status()?;
300
301        // git add . -A
302        let status = Command::new(&self.git_cmd)
303            .args(["add", ".", "-A"])
304            .status()?;
305        if !status.success() {
306            return Err(remote::Error::LocalError(io::Error::other(format!(
307                "Unable to execute git add . -A into {}",
308                dest.display()
309            ))));
310        }
311        // git commit -m '[bacup] snapshot'
312        let status = Command::new(&self.git_cmd)
313            .args(["commit", "-m", "[bacup] snapshot"])
314            .status()?;
315        if !status.success() {
316            return Err(remote::Error::LocalError(io::Error::other(format!(
317                "Unable to execute git commit -m [bacup] snapshot into {}",
318                dest.display()
319            ))));
320        }
321        // git push origin <branch>
322        let status = Command::new(&self.git_cmd)
323            .args(["push", "origin", &self.config.branch])
324            .status()?;
325        if !status.success() {
326            return Err(remote::Error::LocalError(io::Error::other(format!(
327                "Unable to execute git add . -A into {}",
328                dest.display()
329            ))));
330        }
331        Ok(())
332    }
333
334    async fn upload_folder_compressed(
335        &self,
336        path: &Path,
337        remote_path: &Path,
338    ) -> Result<(), remote::Error> {
339        if !path.is_dir() {
340            return Err(remote::Error::NotADirectory);
341        }
342
343        let remote_path = self.remote_archive_path(remote_path);
344        let compressed_folder = self.compress_folder(path).await?;
345
346        self.upload_file(compressed_folder.path(), &remote_path)
347            .await
348    }
349}