summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/backup.rs44
-rw-r--r--src/config.rs33
-rw-r--r--src/error.rs19
-rw-r--r--src/main.rs32
-rw-r--r--src/packages.rs16
-rw-r--r--src/packages/pacman.rs46
-rw-r--r--src/pathinfo.rs246
-rw-r--r--src/storage.rs8
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 @@
1use std::time::{SystemTime, UNIX_EPOCH};
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::{config::Config, pathinfo::PathInfo, packages::Package, error::Result};
7
8pub type BackupId = String;
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct Backup {
12 id: String,
13 timestamp: u64,
14 packages: Vec<Package>,
15 files: Vec<PathInfo>,
16}
17
18impl 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
39struct BackupLocation {
40 id: BackupId,
41 rel_location: String,
42}
43
44type 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 @@
1use config::{File, Map};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize)]
5#[serde(default)]
6pub 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
13impl 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
24impl 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 @@
1pub type Result<T> = std::result::Result<T, Error>;
2
3#[derive(Debug, PartialEq, Eq, thiserror::Error)]
4pub 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 @@
1use backup::Backup;
2use config::Config;
3use packages::{pacman::Pacman, PackageManager};
4use storage::save_index;
5
6mod backup;
7mod config;
8mod error;
9mod pathinfo;
10mod packages;
11mod storage;
12
13fn 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 @@
1use serde::{Deserialize, Serialize};
2
3pub mod pacman;
4
5#[derive(Debug, Serialize, Deserialize)]
6pub struct Package {
7 pub id: String,
8 pub version: String,
9 pub explicit: bool
10}
11
12pub 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 @@
1use std::process::Command;
2
3use crate::packages::Package;
4
5use super::PackageManager;
6
7pub struct Pacman;
8
9impl 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 @@
1use std::{
2 fmt::Display,
3 path::PathBuf,
4};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 backup::BackupId,
10 config::Config,
11 error::{Error, Result},
12};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub 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
24impl 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)]
113pub enum LocationRoot {
114 User(String),
115 Custom(String),
116 SystemSettings,
117 Root,
118}
119
120impl 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
131impl 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)]
154mod 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 @@
1use std::{fs::File, io::Write};
2
3use crate::backup::Backup;
4
5pub 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}