1use std::{collections::HashMap, thread, time::Duration};
2
3use super::{get_config, save_config, ProviderConfig, RemoteProvider};
4use crate::{url::UrlBuilder, Package};
5use owo_colors::OwoColorize;
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9pub struct Github {
10 keyring: keyring::Entry,
11 config: Config,
12}
13
14#[derive(Serialize, Deserialize, Default, Clone)]
15pub struct Config {
16 gist_id: Option<String>,
17}
18
19impl ProviderConfig for Config {
20 fn get_name() -> String {
21 String::from("github")
22 }
23}
24
25impl Github {
26 fn get_auth(&self) -> Option<String> {
27 if let Ok(content) = self.keyring.get_password() {
28 return Some(content);
29 }
30 None
31 }
32}
33
34impl RemoteProvider for Github {
35 fn get_keyring() -> keyring::Entry {
36 keyring::Entry::new("cargo-backup", "github").expect("Could not open keyring")
37 }
38
39 fn new() -> Self {
40 Self {
41 keyring: Self::get_keyring(),
42 config: get_config(),
43 }
44 }
45
46 fn pull(&self) -> Result<Vec<crate::Package>, Box<dyn std::error::Error>> {
47 let auth = self
48 .get_auth()
49 .unwrap_or_else(|| panic!("Please login first with \"cargo sync login\""));
50
51 let gist_id: String = self
52 .config
53 .gist_id
54 .as_ref()
55 .unwrap_or_else(|| panic!("Gist Id not set"))
56 .to_owned();
57
58 let response: Gist = ureq::get(
59 &UrlBuilder::new(&format!("https://api.github.com/gists/{}", gist_id)).build(),
60 )
61 .set("Accept", "application/json")
62 .set("Authorization", &format!("token {}", auth))
63 .set("Content-Type", "application/json")
64 .set(
65 "User-Agent",
66 &format!("CargoBackup/{}", env!("CARGO_PKG_VERSION")),
67 )
68 .set("Authorization", &format!("token {}", auth))
69 .call()?
70 .into_json()?;
71
72 let mut packages: Vec<Package> = Vec::new();
73 for file in response.files.values() {
74 if file.filename == "backup.json" {
75 packages = serde_json::from_str(file.content.as_ref().unwrap())?;
76 }
77 }
78 Ok(packages)
79 }
80
81 fn push(&self, backup: &[crate::Package]) -> Result<(), Box<dyn std::error::Error>> {
82 let auth = self
83 .get_auth()
84 .unwrap_or_else(|| panic!("Please login first with \"cargo sync login\""));
85
86 let gist_id = self.config.gist_id.as_ref();
87
88 let request = match gist_id {
89 Some(id) => ureq::patch(&format!("https://api.github.com/gists/{}", id)),
90 None => ureq::post("https://api.github.com/gists"),
91 };
92
93 let result = request
94 .set("Accept", "application/json")
95 .set("Authorization", &format!("token {}", auth))
96 .set("Content-Type", "application/json")
97 .set(
98 "User-Agent",
99 &format!("CargoBackup/{}", env!("CARGO_PKG_VERSION")),
100 )
101 .send_json(json!({
102 "description": "Cargo Package Backup (Created by cargo-backup https://github.com/Kiramily/cargo-backup)",
103 "public": false,
104 "files": {
105 "backup.json": {
106 "content": serde_json::to_string(&backup).unwrap()
107 }
108 }
109 }))?;
110
111 if result.status() == 200 {
112 println!("Successfully pushed backup");
113 } else {
114 println!("Failed to push backup");
115 }
116 Ok(())
117 }
118
119 fn login(&self, relogin: bool) -> Result<(), Box<dyn std::error::Error>> {
120 if relogin {
121 let _ = self.keyring.delete_password();
122 }
123
124 if self.keyring.get_password().is_ok() {
125 return Ok(());
126 }
127
128 let device_login: Code = ureq::post(
129 &UrlBuilder::new("https://github.com/login/device/code")
130 .add_param("client_id", "65102f4f3d896bfc9c1a")
131 .add_param("scope", "gist")
132 .build(),
133 )
134 .set("Accept", "application/json")
135 .call()?
136 .into_json()?;
137
138 println!("Open the following URL in your browser and enter the code.");
139 println!("{}", device_login.verification_uri.green());
140 println!("{}", device_login.user_code.blue().bold());
141
142 let mut has_token = false;
143
144 while !has_token {
145 thread::sleep(Duration::from_secs(device_login.interval));
146 let poll_request = ureq::post(
147 &UrlBuilder::new("https://github.com/login/oauth/access_token")
148 .add_param("client_id", "65102f4f3d896bfc9c1a")
149 .add_param("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
150 .add_param("device_code", &device_login.device_code)
151 .build(),
152 )
153 .set("Accept", "application/json")
154 .call()?;
155
156 let poll_request: LoginStatus = poll_request.into_json()?;
157
158 match poll_request.error {
159 LoginError::None => {
160 self.keyring
161 .set_password(&poll_request.access_token.unwrap())
162 .unwrap();
163 has_token = true;
164 println!("Successfull Login");
165 }
166 LoginError::AccessDenied => panic!("Access Denied"),
167 LoginError::IncorrectClientCredentials => panic!("Incorrect Client Credentials"),
168 LoginError::ExpiredToken => panic!("Token expired"),
169 LoginError::IncorrectDeviceCode => panic!("Incorrect Device code"),
170 LoginError::SlowDown => todo!("Handle Slow down request"),
171 LoginError::AuthorizationPending => {}
172 LoginError::UnsupportedGrantType => panic!("Unsupported grant type"),
173 }
174 }
175
176 Ok(())
177 }
178
179 fn set_id(&self, id: String) -> Result<(), Box<dyn std::error::Error>> {
180 let mut config: Config = self.config.to_owned();
181 config.gist_id = Some(id);
182 save_config(config);
183 Ok(())
184 }
185}
186
187#[derive(Deserialize, Debug)]
188pub struct Code {
189 pub device_code: String,
190 pub user_code: String,
191 pub verification_uri: String,
192 pub expires_in: u32,
193 pub interval: u64,
194}
195
196#[derive(Deserialize)]
197struct LoginStatus {
198 #[serde(default)]
199 error: LoginError,
200 access_token: Option<String>,
201}
202
203#[derive(Debug, Deserialize, Default)]
204#[serde(rename_all = "snake_case")]
205pub enum LoginError {
206 AuthorizationPending,
207 SlowDown,
208 ExpiredToken,
209 UnsupportedGrantType,
210 IncorrectClientCredentials,
211 IncorrectDeviceCode,
212 AccessDenied,
213 #[default]
214 None,
215}
216
217#[derive(Serialize, Deserialize, Debug)]
218pub struct Gist {
219 #[serde(rename = "id", skip_serializing)]
220 pub id: String,
221
222 #[serde(rename = "files", default)]
223 pub files: HashMap<String, File>,
224
225 #[serde(rename = "public", default)]
226 pub public: bool,
227}
228
229#[derive(Serialize, Deserialize, Debug)]
230pub struct File {
231 #[serde(rename = "filename")]
232 pub filename: String,
233
234 #[serde(rename = "raw_url", skip_serializing)]
235 pub raw_url: Option<String>,
236
237 pub content: Option<String>,
238}