1use 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 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 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 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 Command::new(&self.git_cmd)
194 .args(["switch", "-c", &self.config.branch])
195 .status()?;
196
197 Command::new(&self.git_cmd)
199 .args(["pull", "origin", &self.config.branch])
200 .status()?;
201
202 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 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 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 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 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 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 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 Command::new(&self.git_cmd)
293 .args(["switch", "-c", &self.config.branch])
294 .status()?;
295
296 Command::new(&self.git_cmd)
298 .args(["pull", "origin", &self.config.branch])
299 .status()?;
300
301 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 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 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}