skootrs_lib/service/
source.rs

1//
2// Copyright 2024 The Skootrs Authors.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![allow(clippy::module_name_repetitions)]
17
18use std::{fs, path::Path, process::Command};
19
20use sha2::Digest;
21use tracing::{debug, info};
22
23use skootrs_model::skootrs::{
24    InitializedRepo, InitializedSource, SkootError, SourceInitializeParams,
25};
26
27use super::repo::{LocalRepoService, RepoService};
28/// The `SourceService` trait provides an interface for and managing a project's source code.
29/// This code is usually something a local git repo. The service differs from the repo service
30/// in that it's focused on the files and not the repo itself.
31pub trait SourceService {
32    /// Initializes a source code directory for a project. This usually involves cloning a repo from
33    /// a repo service.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the source code directory can't be initialized.
38    fn initialize(
39        &self,
40        params: SourceInitializeParams,
41        initialized_repo: InitializedRepo,
42    ) -> Result<InitializedSource, SkootError>;
43
44    /// Commits changes to the repo and pushed them to the remote.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the changes can't be committed and pushed to the remote.
49    fn commit_and_push_changes(
50        &self,
51        source: InitializedSource,
52        message: String,
53    ) -> Result<(), SkootError>;
54
55    /// Writes a file to the source code directory.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the file can't be written to the source code directory.
60    fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(
61        &self,
62        source: InitializedSource,
63        path: P,
64        name: String,
65        contents: C,
66    ) -> Result<(), SkootError>;
67
68    /// Reads a file from the source code directory.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the file can't be read from the source code directory.
73    fn read_file<P: AsRef<Path>>(
74        &self,
75        source: &InitializedSource,
76        path: P,
77        name: String,
78    ) -> Result<String, SkootError>;
79
80    /// `hash_file` returns the SHA256 hash of a file.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the file can't be opened and hashed.
85    fn hash_file<P: AsRef<Path>>(
86        &self,
87        source: &InitializedSource,
88        path: P,
89        name: String,
90    ) -> Result<String, SkootError>;
91
92    /// Pulls updates from the remote repo.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the updates can't be pulled from the remote repo.
97    fn pull_updates(&self, source: InitializedSource) -> Result<(), SkootError>;
98}
99
100/// The `LocalSourceService` struct provides an implementation of the `SourceService` trait for initializing
101/// and managing a project's source files from the local machine.
102#[derive(Debug)]
103pub struct LocalSourceService {}
104
105impl SourceService for LocalSourceService {
106    /// Returns `Ok(())` if changes are committed and pushed back to the remote  if successful,
107    /// otherwise returns an error.
108    fn initialize(
109        &self,
110        params: SourceInitializeParams,
111        initialized_repo: InitializedRepo,
112    ) -> Result<InitializedSource, SkootError> {
113        let repo_service = LocalRepoService {};
114        repo_service.clone_local(initialized_repo, params.parent_path)
115    }
116
117    fn commit_and_push_changes(
118        &self,
119        source: InitializedSource,
120        message: String,
121    ) -> Result<(), SkootError> {
122        let _output = Command::new("git")
123            .arg("add")
124            .arg(".")
125            .current_dir(&source.path)
126            .output()?;
127
128        let _output = Command::new("git")
129            .arg("commit")
130            .arg("-m")
131            .arg(message)
132            .current_dir(&source.path)
133            .output()?;
134        info!("Committed changes for {}", source.path);
135
136        let _output = Command::new("git")
137            .arg("push")
138            .current_dir(&source.path)
139            .output()?;
140        info!("Pushed changes for {}", source.path);
141        Ok(())
142    }
143
144    /// Returns `Ok(())` if a file is successfully written to some path within the source directory. Otherwise,
145    /// it returns an error.
146    fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(
147        &self,
148        source: InitializedSource,
149        path: P,
150        name: String,
151        contents: C,
152    ) -> Result<(), SkootError> {
153        let full_path = Path::new(&source.path).join(&path);
154        // Ensure path exists
155        info!("Creating path {:?}", &full_path);
156        fs::create_dir_all(&full_path)?;
157        let complete_path = full_path.join(name);
158        fs::write(complete_path, contents)?;
159        debug!("{:?} file written", &full_path);
160        Ok(())
161    }
162
163    fn read_file<P: AsRef<Path>>(
164        &self,
165        source: &InitializedSource,
166        path: P,
167        name: String,
168    ) -> Result<String, SkootError> {
169        let full_path = Path::new(&source.path).join(&path).join(name);
170        let contents = fs::read_to_string(full_path)?;
171        Ok(contents)
172    }
173
174    fn hash_file<P: AsRef<Path>>(
175        &self,
176        source: &InitializedSource,
177        path: P,
178        name: String,
179    ) -> Result<String, SkootError> {
180        let full_path: std::path::PathBuf = Path::new(&source.path).join(&path).join(name);
181        debug!("Hashing file {:?}", &full_path);
182        let mut contents = fs::File::open(&full_path)?;
183        let mut hasher = sha2::Sha256::new();
184        std::io::copy(&mut contents, &mut hasher)?;
185        let hash = hasher.finalize();
186        //let hash = "test".to_string();
187
188        debug!("Hashed file {:?} with hash: {:x}", &full_path, &hash);
189        Ok(format!("{hash:x}"))
190    }
191
192    fn pull_updates(&self, source: InitializedSource) -> Result<(), SkootError> {
193        let _output = Command::new("git")
194            .arg("pull")
195            .current_dir(&source.path)
196            .output()?;
197        info!("Pulled updates for {}", source.path);
198        Ok(())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use skootrs_model::skootrs::{GithubUser, InitializedGithubRepo};
206    use std::path::PathBuf;
207    use tempdir::TempDir;
208
209    #[test]
210    fn test_initialize() {
211        let source_service = LocalSourceService {};
212        let temp_dir = TempDir::new("test").unwrap();
213        let parent_path = temp_dir.path().to_str().unwrap();
214        let params = SourceInitializeParams {
215            parent_path: parent_path.to_string(),
216        };
217        let initialized_repo = InitializedRepo::Github(InitializedGithubRepo {
218            name: "skootrs".to_string(),
219            organization: GithubUser::Organization("kusaridev".to_string()),
220        });
221        let result = source_service.initialize(params, initialized_repo);
222        assert!(result.is_ok());
223        let initialized_source = result.unwrap();
224        assert_eq!(
225            initialized_source.path,
226            format!("{}/{}", parent_path, "skootrs")
227        );
228    }
229
230    #[test]
231    fn test_write_file() {
232        let source_service = LocalSourceService {};
233        let temp_dir = TempDir::new("test").unwrap();
234        let initialized_source = InitializedSource {
235            path: temp_dir.path().to_str().unwrap().to_string(),
236        };
237        let path = "subdirectory";
238        let name = "file.txt".to_string();
239        let contents = "File contents".as_bytes();
240        let result = source_service.write_file(initialized_source, path, name.clone(), contents);
241        assert!(result.is_ok());
242        let file_path =
243            PathBuf::from(format!("{}/{}", temp_dir.path().to_str().unwrap(), path)).join(name);
244        assert!(file_path.exists());
245        let file_contents = fs::read_to_string(file_path).unwrap();
246        assert_eq!(file_contents, "File contents");
247    }
248
249    #[test]
250    fn test_read_file() {
251        let source_service = LocalSourceService {};
252        let temp_dir = TempDir::new("test").unwrap();
253        let initialized_source = InitializedSource {
254            path: temp_dir.path().to_str().unwrap().to_string(),
255        };
256        let path = "subdirectory";
257        let name = "file.txt".to_string();
258        let contents = "File contents".as_bytes();
259        let result =
260            source_service.write_file(initialized_source.clone(), path, name.clone(), contents);
261        assert!(result.is_ok());
262        let file_path = PathBuf::from(format!("{}/{}", temp_dir.path().to_str().unwrap(), path))
263            .join(name.clone());
264        assert!(file_path.exists());
265        let file_contents = source_service
266            .read_file(&initialized_source, path, name)
267            .unwrap();
268        assert_eq!(file_contents, "File contents");
269    }
270}