diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backup.rs | 44 | ||||
-rw-r--r-- | src/config.rs | 33 | ||||
-rw-r--r-- | src/error.rs | 19 | ||||
-rw-r--r-- | src/main.rs | 32 | ||||
-rw-r--r-- | src/packages.rs | 16 | ||||
-rw-r--r-- | src/packages/pacman.rs | 46 | ||||
-rw-r--r-- | src/pathinfo.rs | 246 | ||||
-rw-r--r-- | src/storage.rs | 8 |
8 files changed, 444 insertions, 0 deletions
diff --git a/src/backup.rs b/src/backup.rs new file mode 100644 index 0000000..4e74c97 --- /dev/null +++ b/src/backup.rs | |||
@@ -0,0 +1,44 @@ | |||
1 | use std::time::{SystemTime, UNIX_EPOCH}; | ||
2 | |||
3 | use serde::{Deserialize, Serialize}; | ||
4 | use uuid::Uuid; | ||
5 | |||
6 | use crate::{config::Config, pathinfo::PathInfo, packages::Package, error::Result}; | ||
7 | |||
8 | pub type BackupId = String; | ||
9 | |||
10 | #[derive(Debug, Serialize, Deserialize)] | ||
11 | pub struct Backup { | ||
12 | id: String, | ||
13 | timestamp: u64, | ||
14 | packages: Vec<Package>, | ||
15 | files: Vec<PathInfo>, | ||
16 | } | ||
17 | |||
18 | impl Backup { | ||
19 | pub fn create(config: &Config, packages: Vec<Package>) -> Result<Self> { | ||
20 | let mut files: Vec<PathInfo> = Vec::new(); | ||
21 | for dir in &config.directories { | ||
22 | files.push(PathInfo::from_path(config, dir)?); | ||
23 | } | ||
24 | Ok(Self { | ||
25 | // UUID not really needed, maybe a shorter hash | ||
26 | id: Uuid::new_v4().to_string(), | ||
27 | timestamp: SystemTime::now() | ||
28 | .duration_since(UNIX_EPOCH) | ||
29 | .unwrap() | ||
30 | .as_secs(), | ||
31 | packages, | ||
32 | files, | ||
33 | }) | ||
34 | } | ||
35 | |||
36 | |||
37 | } | ||
38 | |||
39 | struct BackupLocation { | ||
40 | id: BackupId, | ||
41 | rel_location: String, | ||
42 | } | ||
43 | |||
44 | type BackupList = Vec<BackupLocation>; | ||
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..625118a --- /dev/null +++ b/src/config.rs | |||
@@ -0,0 +1,33 @@ | |||
1 | use config::{File, Map}; | ||
2 | use serde::{Deserialize, Serialize}; | ||
3 | |||
4 | #[derive(Debug, Serialize, Deserialize)] | ||
5 | #[serde(default)] | ||
6 | pub struct Config { | ||
7 | pub root: String, | ||
8 | pub user: Vec<String>, | ||
9 | pub directories: Vec<String>, | ||
10 | pub custom_directories: Map<String, String> | ||
11 | } | ||
12 | |||
13 | impl Default for Config { | ||
14 | fn default() -> Self { | ||
15 | Self { | ||
16 | root: "/mnt/backup".to_string(), | ||
17 | user: vec![], | ||
18 | directories: vec![], | ||
19 | custom_directories: Map::new(), | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | |||
24 | impl Config { | ||
25 | pub fn load() -> Result<Self, config::ConfigError> { | ||
26 | let config = config::Config::builder() | ||
27 | .add_source(File::with_name("config.toml").required(false)) | ||
28 | .add_source(config::Environment::with_prefix("FXBAUP").separator("_")) | ||
29 | .build()?; | ||
30 | |||
31 | config.try_deserialize() | ||
32 | } | ||
33 | } | ||
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..77eab69 --- /dev/null +++ b/src/error.rs | |||
@@ -0,0 +1,19 @@ | |||
1 | pub type Result<T> = std::result::Result<T, Error>; | ||
2 | |||
3 | #[derive(Debug, PartialEq, Eq, thiserror::Error)] | ||
4 | pub enum Error { | ||
5 | #[error("unknown custom directory '{0}'")] | ||
6 | CustomDirectory(String), | ||
7 | |||
8 | #[error("invalid directory index '{0}'")] | ||
9 | InvalidIndex(String), | ||
10 | |||
11 | #[error("no directory index given")] | ||
12 | NoIndex, | ||
13 | |||
14 | #[error("invalid directory '{0}'")] | ||
15 | InvalidDirectory(String), | ||
16 | |||
17 | #[error("Only exactly one user allowed in config")] | ||
18 | MultiUser, | ||
19 | } | ||
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1fdcebf --- /dev/null +++ b/src/main.rs | |||
@@ -0,0 +1,32 @@ | |||
1 | use backup::Backup; | ||
2 | use config::Config; | ||
3 | use packages::{pacman::Pacman, PackageManager}; | ||
4 | use storage::save_index; | ||
5 | |||
6 | mod backup; | ||
7 | mod config; | ||
8 | mod error; | ||
9 | mod pathinfo; | ||
10 | mod packages; | ||
11 | mod storage; | ||
12 | |||
13 | fn main() -> anyhow::Result<()> { | ||
14 | let mut cfg = Config::load()?; | ||
15 | cfg.user.push("fx".to_string()); | ||
16 | cfg.directories.push("~/.config/nvim".to_string()); | ||
17 | cfg.directories.push("~/.config/hypr".to_string()); | ||
18 | let toml = toml::to_string(&cfg)?; | ||
19 | println!("{toml}"); | ||
20 | |||
21 | let pacman = Pacman; | ||
22 | let pkgs = pacman.get_installed(); | ||
23 | |||
24 | let backup = Backup::create(&cfg, pkgs)?; | ||
25 | // println!("{backup:#?}"); | ||
26 | |||
27 | save_index(backup); | ||
28 | |||
29 | // let fi = FileInfo::new("~/.config/nvim", &cfg)?; | ||
30 | // println!("{:?}", fi.get_absolute_path()); | ||
31 | Ok(()) | ||
32 | } | ||
diff --git a/src/packages.rs b/src/packages.rs new file mode 100644 index 0000000..9f765d6 --- /dev/null +++ b/src/packages.rs | |||
@@ -0,0 +1,16 @@ | |||
1 | use serde::{Deserialize, Serialize}; | ||
2 | |||
3 | pub mod pacman; | ||
4 | |||
5 | #[derive(Debug, Serialize, Deserialize)] | ||
6 | pub struct Package { | ||
7 | pub id: String, | ||
8 | pub version: String, | ||
9 | pub explicit: bool | ||
10 | } | ||
11 | |||
12 | pub trait PackageManager { | ||
13 | fn get_installed(&self) -> Vec<Package>; | ||
14 | |||
15 | fn install(&self, pkgs: Vec<Package>); | ||
16 | } | ||
diff --git a/src/packages/pacman.rs b/src/packages/pacman.rs new file mode 100644 index 0000000..0a9e1ff --- /dev/null +++ b/src/packages/pacman.rs | |||
@@ -0,0 +1,46 @@ | |||
1 | use std::process::Command; | ||
2 | |||
3 | use crate::packages::Package; | ||
4 | |||
5 | use super::PackageManager; | ||
6 | |||
7 | pub struct Pacman; | ||
8 | |||
9 | impl PackageManager for Pacman { | ||
10 | fn get_installed(&self) -> Vec<super::Package> { | ||
11 | let pm_pkgs = Command::new("pacman").args(["-Q"]).output().unwrap(); | ||
12 | let pm_e_pkgs = Command::new("pacman") | ||
13 | .args(["-Q", "--explicit"]) | ||
14 | .output() | ||
15 | .unwrap(); | ||
16 | |||
17 | let pm_pkgs_out = String::from_utf8(pm_pkgs.stdout).unwrap(); | ||
18 | let pm_e_pkgs_out = String::from_utf8(pm_e_pkgs.stdout).unwrap(); | ||
19 | |||
20 | let mut pkgs: Vec<Package> = Vec::new(); | ||
21 | let pacman_pkgs: Vec<&str> = pm_pkgs_out.split('\n').collect(); | ||
22 | for pkg in pacman_pkgs { | ||
23 | if pkg.is_empty() { | ||
24 | continue; | ||
25 | }; | ||
26 | let split: Vec<&str> = pkg.split_whitespace().collect(); | ||
27 | if split.len() != 2 { | ||
28 | panic!("Unknown Pacman Output"); | ||
29 | }; | ||
30 | |||
31 | let explicit = pm_e_pkgs_out.contains(pkg); | ||
32 | |||
33 | pkgs.push(Package { | ||
34 | id: split[0].to_string(), | ||
35 | version: split[1].to_string(), | ||
36 | explicit | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | pkgs | ||
41 | } | ||
42 | |||
43 | fn install(&self, pkgs: Vec<Package>) { | ||
44 | todo!(); | ||
45 | } | ||
46 | } | ||
diff --git a/src/pathinfo.rs b/src/pathinfo.rs new file mode 100644 index 0000000..b0c3be4 --- /dev/null +++ b/src/pathinfo.rs | |||
@@ -0,0 +1,246 @@ | |||
1 | use std::{ | ||
2 | fmt::Display, | ||
3 | path::PathBuf, | ||
4 | }; | ||
5 | |||
6 | use serde::{Deserialize, Serialize}; | ||
7 | |||
8 | use crate::{ | ||
9 | backup::BackupId, | ||
10 | config::Config, | ||
11 | error::{Error, Result}, | ||
12 | }; | ||
13 | |||
14 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
15 | pub struct PathInfo { | ||
16 | pub modified: bool, | ||
17 | pub is_file: bool, | ||
18 | rel_location: String, | ||
19 | location_root: LocationRoot, | ||
20 | last_modified: BackupId, | ||
21 | children: Vec<PathInfo> | ||
22 | } | ||
23 | |||
24 | impl PathInfo { | ||
25 | pub fn from_path(config: &Config, path: &str) -> Result<Self> { | ||
26 | let locations = Self::parse_location(path, config)?; | ||
27 | |||
28 | Ok(Self::handle_dir(config, &locations.0, &locations.1)?) | ||
29 | } | ||
30 | |||
31 | fn handle_dir( | ||
32 | config: &Config, | ||
33 | rel_location: &str, | ||
34 | location_root: &LocationRoot, | ||
35 | ) -> Result<Self> { | ||
36 | println!("Handling {rel_location}"); | ||
37 | let path = Self::get_abs_path(&location_root.to_string(), rel_location); | ||
38 | Ok(if path.is_dir() { | ||
39 | let mut modified = false; | ||
40 | let mut children: Vec<PathInfo> = Vec::new(); | ||
41 | |||
42 | let paths = std::fs::read_dir(path).unwrap(); | ||
43 | for path in paths { | ||
44 | let pathstr = path.unwrap().path().to_string_lossy().to_string(); | ||
45 | let root = format!("{}/", location_root.to_string()); | ||
46 | let Some(rl) = pathstr.split_once(&root) else { | ||
47 | panic!("HUH"); | ||
48 | }; | ||
49 | let handle = Self::handle_dir(config, rl.1, location_root)?; | ||
50 | if handle.modified { | ||
51 | modified = true; | ||
52 | }; | ||
53 | children.push(handle); | ||
54 | } | ||
55 | Self { | ||
56 | modified, | ||
57 | is_file: false, | ||
58 | rel_location: rel_location.to_string(), | ||
59 | location_root: location_root.clone(), | ||
60 | last_modified: "".to_string(), | ||
61 | children | ||
62 | } | ||
63 | } else { | ||
64 | Self::from_file(rel_location, location_root.clone())? | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | fn from_file(rel_location: &str, location_root: LocationRoot) -> Result<Self> { | ||
69 | println!("From file {rel_location}"); | ||
70 | |||
71 | let modified = false; | ||
72 | |||
73 | Ok(Self { | ||
74 | rel_location: rel_location.to_string(), | ||
75 | location_root, | ||
76 | modified, | ||
77 | last_modified: "".to_string(), | ||
78 | is_file: true, | ||
79 | children: Vec::new() | ||
80 | }) | ||
81 | } | ||
82 | |||
83 | pub fn get_absolute_path(&self) -> PathBuf { | ||
84 | Self::get_abs_path(&self.location_root.to_string(), &self.rel_location) | ||
85 | } | ||
86 | |||
87 | fn get_abs_path(location_root: &str, rel_location: &str) -> PathBuf { | ||
88 | let path = format!("{}/{}", location_root, rel_location); | ||
89 | PathBuf::from(path) | ||
90 | } | ||
91 | |||
92 | fn parse_location(value: &str, config: &Config) -> Result<(String, LocationRoot)> { | ||
93 | let Some(split) = value.split_once('/') else { | ||
94 | return Err(Error::InvalidDirectory(value.to_string())); | ||
95 | }; | ||
96 | if split.0.starts_with('~') { | ||
97 | if config.user.len() != 1 { | ||
98 | return Err(Error::MultiUser); | ||
99 | } | ||
100 | return Ok(( | ||
101 | split.1.to_string(), | ||
102 | LocationRoot::User(config.user[0].clone()), | ||
103 | )); | ||
104 | }; | ||
105 | Ok(( | ||
106 | split.1.to_string(), | ||
107 | LocationRoot::from_op_str(split.0, config)?, | ||
108 | )) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] | ||
113 | pub enum LocationRoot { | ||
114 | User(String), | ||
115 | Custom(String), | ||
116 | SystemSettings, | ||
117 | Root, | ||
118 | } | ||
119 | |||
120 | impl Display for LocationRoot { | ||
121 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
122 | match self { | ||
123 | LocationRoot::User(user) => write!(f, "/home/{user}"), | ||
124 | LocationRoot::Custom(loc) => write!(f, "{loc}"), | ||
125 | LocationRoot::SystemSettings => write!(f, "/etc"), | ||
126 | LocationRoot::Root => write!(f, "/"), | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | |||
131 | impl LocationRoot { | ||
132 | fn from_op_str(value: &str, config: &Config) -> Result<Self> { | ||
133 | let split_str = value.split_once(':'); | ||
134 | let Some(split_op) = split_str else { | ||
135 | return Err(Error::NoIndex); | ||
136 | }; | ||
137 | match split_op.0 { | ||
138 | "u" => Ok(Self::User(split_op.1.to_string())), | ||
139 | "s" => Ok(Self::SystemSettings), | ||
140 | "r" => Ok(Self::Root), | ||
141 | "c" => Ok(Self::Custom( | ||
142 | config | ||
143 | .custom_directories | ||
144 | .get(split_op.1) | ||
145 | .ok_or_else(|| Error::CustomDirectory(split_op.1.to_string()))? | ||
146 | .to_string(), | ||
147 | )), | ||
148 | _ => Err(Error::InvalidIndex(split_op.0.to_string())), | ||
149 | } | ||
150 | } | ||
151 | } | ||
152 | |||
153 | #[cfg(test)] | ||
154 | mod tests { | ||
155 | use crate::{ | ||
156 | config::Config, | ||
157 | error::{Error, Result}, | ||
158 | pathinfo::PathInfo, | ||
159 | }; | ||
160 | |||
161 | use super::LocationRoot; | ||
162 | |||
163 | #[test] | ||
164 | fn from_op_str() -> Result<()> { | ||
165 | let mut config = Config::default(); | ||
166 | config | ||
167 | .custom_directories | ||
168 | .insert("test".to_string(), "/usr/local/test".to_string()); | ||
169 | |||
170 | let mut values: Vec<(&str, Result<LocationRoot>)> = Vec::new(); | ||
171 | values.push(("u:test", Ok(LocationRoot::User("test".to_string())))); | ||
172 | values.push(("s:", Ok(LocationRoot::SystemSettings))); | ||
173 | values.push(("r:", Ok(LocationRoot::Root))); | ||
174 | values.push(( | ||
175 | "c:test", | ||
176 | Ok(LocationRoot::Custom("/usr/local/test".to_string())), | ||
177 | )); | ||
178 | values.push(("c:rest", Err(Error::CustomDirectory("rest".to_string())))); | ||
179 | values.push(("t:test/", Err(Error::InvalidIndex("t".to_string())))); | ||
180 | values.push(( | ||
181 | "test:test/usr", | ||
182 | Err(Error::InvalidIndex("test".to_string())), | ||
183 | )); | ||
184 | values.push(("/usr/local/test", Err(Error::NoIndex))); | ||
185 | values.push(("c/usr/local/test", Err(Error::NoIndex))); | ||
186 | |||
187 | for value in values { | ||
188 | print!("Testing {value:?}"); | ||
189 | assert_eq!(LocationRoot::from_op_str(value.0, &config), value.1); | ||
190 | println!("\rTesting {value:?} ✓"); | ||
191 | } | ||
192 | |||
193 | Ok(()) | ||
194 | } | ||
195 | |||
196 | #[test] | ||
197 | fn parse_location() -> Result<()> { | ||
198 | let mut config = Config::default(); | ||
199 | config.user.push("test".to_string()); | ||
200 | config | ||
201 | .custom_directories | ||
202 | .insert("test".to_string(), "/usr/local/test".to_string()); | ||
203 | |||
204 | let mut values: Vec<(&str, Result<(String, LocationRoot)>)> = Vec::new(); | ||
205 | values.push(( | ||
206 | "~/.config/nvim", | ||
207 | Ok(( | ||
208 | ".config/nvim".to_string(), | ||
209 | LocationRoot::User("test".to_string()), | ||
210 | )), | ||
211 | )); | ||
212 | values.push(( | ||
213 | "u:test/.config/nvim", | ||
214 | Ok(( | ||
215 | ".config/nvim".to_string(), | ||
216 | LocationRoot::User("test".to_string()), | ||
217 | )), | ||
218 | )); | ||
219 | values.push(( | ||
220 | "r:/.config/nvim", | ||
221 | Ok((".config/nvim".to_string(), LocationRoot::Root)), | ||
222 | )); | ||
223 | values.push(( | ||
224 | "r:/.config/nvim", | ||
225 | Ok((".config/nvim".to_string(), LocationRoot::Root)), | ||
226 | )); | ||
227 | values.push(( | ||
228 | "s:/.config/nvim", | ||
229 | Ok((".config/nvim".to_string(), LocationRoot::SystemSettings)), | ||
230 | )); | ||
231 | values.push(( | ||
232 | "c:test/.config/nvim", | ||
233 | Ok(( | ||
234 | ".config/nvim".to_string(), | ||
235 | LocationRoot::Custom("/usr/local/test".to_string()), | ||
236 | )), | ||
237 | )); | ||
238 | |||
239 | for value in values { | ||
240 | print!("Testing {value:?}"); | ||
241 | assert_eq!(PathInfo::parse_location(&value.0, &config), value.1); | ||
242 | println!("\rTesting {value:?} ✓"); | ||
243 | } | ||
244 | Ok(()) | ||
245 | } | ||
246 | } | ||
diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..b9e8de9 --- /dev/null +++ b/src/storage.rs | |||
@@ -0,0 +1,8 @@ | |||
1 | use std::{fs::File, io::Write}; | ||
2 | |||
3 | use crate::backup::Backup; | ||
4 | |||
5 | pub fn save_index(backup: Backup) { | ||
6 | let mut f = File::create("./index.json").unwrap(); | ||
7 | f.write_all(&serde_json::to_vec(&backup).unwrap()).unwrap(); | ||
8 | } | ||