cargo_backup/
lib.rs

1use misc::{pretty_print_packages, Errors};
2use owo_colors::OwoColorize;
3use semver::Version;
4use serde::{Deserialize, Serialize};
5use std::{collections::HashMap, path::PathBuf, vec};
6
7mod misc;
8pub mod remote;
9mod url;
10
11#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
12pub struct Package {
13    pub name: String,
14    pub features: Vec<String>,
15    pub all_features: bool,
16    pub no_default_features: bool,
17    pub version: Version,
18}
19
20#[derive(Serialize, Deserialize, Debug)]
21struct Crates {
22    installs: HashMap<String, Install>,
23}
24
25#[derive(Serialize, Deserialize, Debug)]
26struct Install {
27    #[serde(default)]
28    pub features: Vec<String>,
29    pub no_default_features: bool,
30    pub all_features: bool,
31}
32
33/// Returns the path to the .crates2.json file.
34fn get_crates_path() -> PathBuf {
35    #[cfg(test)]
36    {
37        use std::env;
38        env::current_dir().unwrap().join("tests/.crates2.json")
39    }
40
41    #[cfg(not(test))]
42    {
43        let path = dirs::home_dir().unwrap().join(".cargo/.crates2.json");
44        assert!(path.exists());
45        path
46    }
47}
48
49/// Gets the currently installed packages from the .crates2.json file.
50///
51/// # Examples
52/// ```no_run
53/// use cargo_backup::get_packages;
54///
55/// let packages = get_packages();
56/// ```
57///
58/// # Panics
59/// * If the .crates2.json file is not valid JSON.
60/// * If the .crates2.json file cannot be read.
61pub fn get_packages() -> Vec<Package> {
62    let path = get_crates_path();
63    let crates: Crates = serde_json::from_str(
64        &std::fs::read_to_string(path).unwrap_or_else(|_| panic!("{}", Errors::ReadFile)),
65    )
66    .unwrap_or_else(|_| panic!("{}", Errors::JsonParse));
67
68    let mut packages = vec![];
69
70    for (id, install) in crates.installs {
71        let (name, version, skip) = slice_info(&id);
72
73        if skip {
74            continue;
75        }
76
77        packages.push(Package {
78            name: name.to_string(),
79            features: install.features,
80            all_features: install.all_features,
81            no_default_features: install.no_default_features,
82            version,
83        });
84    }
85
86    packages
87}
88
89pub fn install_packages(
90    packages: &[Package],
91    skip_install: bool,
92    skip_update: bool,
93    skip_remove: bool,
94) {
95    let installed_packages = get_packages();
96
97    let mut to_update: Vec<Package> = vec![];
98    let mut to_install: Vec<Package> = vec![];
99    let mut to_remove: Vec<Package> = vec![];
100
101    if !skip_install {
102        for package in packages {
103            if !installed_packages.iter().any(|p| p.name == package.name) {
104                to_install.push(package.clone());
105            }
106        }
107    }
108
109    if !skip_update {
110        for package in &installed_packages {
111            if let Some(p) = packages.iter().find(|np| np.name == package.name) {
112                if p.version > package.version {
113                    to_update.push(p.clone());
114                }
115            }
116        }
117    }
118
119    if !skip_remove {
120        for package in &installed_packages {
121            if !packages.iter().any(|np| np.name == package.name) {
122                to_remove.push(package.clone());
123            }
124        }
125    }
126
127    pretty_print_packages(&to_install.clone(), &to_update.clone(), &to_remove.clone());
128
129    // Skip the Installation process if it is a test
130    #[cfg(not(test))]
131    {
132        use crate::misc::{execute_cmd, CommandType};
133        use dialoguer::Confirm;
134
135        if Confirm::new().with_prompt("Proceed?").interact().unwrap() {
136            // TODO: Install
137
138            for package in to_install {
139                execute_cmd(&package, CommandType::Install);
140            }
141
142            for package in to_update {
143                execute_cmd(&package, CommandType::Install);
144            }
145
146            for package in to_remove {
147                execute_cmd(&package, CommandType::Remove);
148            }
149        }
150    }
151}
152
153/// Gets the Package name and Version and from the string.
154/// The bool will be true if the Package is a local package and it should be skipped.
155///
156/// # Examples
157/// ```no_run
158/// let (name, version, skip) = slice_info("foo 0.1.0 (path+file:///home/user/foo)");
159/// ```
160fn slice_info(package_str: &str) -> (String, Version, bool) {
161    let splits: Vec<&str> = package_str.splitn(3, ' ').collect();
162    let name = splits[0].to_string();
163    let version = Version::parse(splits[1]).unwrap();
164    let local_package = splits[2].contains("path+file://") || splits[2].contains("git+https://");
165    if local_package {
166        println!(
167            "{} {}",
168            name.blue(),
169            "Package ignored because it is either a local or git Package".red()
170        );
171    }
172    (name, version, local_package)
173}
174
175#[test]
176fn test_slice_info() {
177    use std::str::FromStr;
178
179    let (name, version, skip) = slice_info("foo 0.1.0 (path+file:///home/user/foo)");
180    assert_eq!(name, "foo");
181    assert_eq!(version, Version::from_str("0.1.0").unwrap());
182    assert!(skip);
183
184    let (name, version, skip) = slice_info("foo 0.1.0 (registry+https://example.com/foo)");
185    assert_eq!(name, "foo");
186    assert_eq!(version, Version::from_str("0.1.0").unwrap());
187    assert!(!skip);
188
189    let (name, version, skip) = slice_info("foo 0.1.0 (git+https://github.com/foo/bar#hash)");
190    assert_eq!(name, "foo");
191    assert_eq!(version, Version::from_str("0.1.0").unwrap());
192    assert!(skip);
193}
194
195#[test]
196fn test_get_packages() {
197    let packages = get_packages();
198    assert_eq!(packages.len(), 3);
199}
200
201#[test]
202fn test_install_packages() {
203    let fake_packages: Vec<Package> = vec![
204        Package {
205            all_features: true,
206            features: vec![],
207            name: "foo".to_string(),
208            no_default_features: false,
209            version: Version::parse("0.1.0").unwrap(),
210        },
211        Package {
212            name: "package".to_string(),
213            version: Version::parse("0.5.3").unwrap(),
214            all_features: false,
215            no_default_features: false,
216            features: vec!["feature1".to_string(), "feature2".to_string()],
217        },
218    ];
219
220    install_packages(&fake_packages, false, false, false);
221}