fontpm_source_google_fonts/
lib.rs1mod github;
2mod data;
3
4use std::collections::HashMap;
5use std::fs::{create_dir_all, File};
6use std::path::{Path, PathBuf};
7use default_env::default_env;
8use fontpm_api::{FpmHost, Source, trace};
9use fontpm_api::async_trait::async_trait;
10use fontpm_api::host::EmptyFpmHost;
11use fontpm_api::source::{RefreshOutput};
12use fontpm_api::Error;
13use std::io::{Error as IOError, ErrorKind as IOErrorKind, Read, Write};
14use reqwest::{Client, ClientBuilder};
15use serde::{Serialize};
16use serde::de::DeserializeOwned;
17use sha2::{Sha256, Digest};
18use fontpm_api::font::{DefinedFontInstallSpec, DefinedFontVariantSpec, FontInstallSpec, FontDescription as FpmFontDescription};
19use fontpm_api::util::create_parent;
20use crate::data::Data;
21use crate::data::description::variant_to_string;
22use crate::github::GithubBranchData;
23
24pub struct GoogleFontsSource<'host> {
25 host: &'host dyn FpmHost,
26 client: Option<Client>
27}
28
29const COMMIT_DATA_URL: &str = default_env!("COMMIT", "https://api.github.com/repos/fontpm/data/branches/data");
31const FONT_INDEX_URL: &str = default_env!("FONT_INDEX_URL", "https://raw.githubusercontent.com/fontpm/data/data/google-fonts.json");
33
34const COMMIT_FILE: &str = "commit.sha";
35const DATA_FILE: &str = "data.json";
36
37impl<'host> GoogleFontsSource<'host> {
38 pub const ID: &'host str = "google-fonts";
39 pub const NAME: &'host str = "Google Fonts";
40
41 pub fn new() -> Self {
42 return GoogleFontsSource {
43 host: &EmptyFpmHost::EMPTY_HOST,
44 client: None
45 };
46 }
47
48 fn client(&self) -> &Client {
49 return self.client.as_ref().unwrap();
50 }
51
52 fn cache_dir(&self) -> PathBuf {
53 return self.host.cache_dir_for(Self::ID.into());
54 }
55 fn cache_file<P>(&self, s: P) -> PathBuf where P: AsRef<Path> {
56 let mut path = self.cache_dir();
57 path.push(s);
58 path
59 }
60
61 fn last_downloaded_commit(&self) -> Option<String> {
62 log::debug!("Reading last downloaded commit");
63 let mut path = self.cache_dir();
64 path.push(COMMIT_FILE);
65 let path_str = path.clone().into_os_string();
66 let path_str = path_str.to_string_lossy();
67
68 let path = path;
69
70 let mut file = match File::open(path) {
71 Ok(v) => v,
72 Err(e) => {
73 match e.kind() {
74 IOErrorKind::NotFound => return None,
75 _ => log::error!("Error whilst opening {}: {}", path_str, e)
77 }
78 return None
79 }
80 };
81
82 let mut data = String::new();
83 match file.read_to_string(&mut data) {
84 Ok(v) => log::trace!("Read {} bytes from {}", v, path_str),
85 Err(e) => {
86 log::error!("Error whilst reading data: {}", e);
87 return None
88 }
89 }
90
91 Some(data)
92 }
93
94 async fn latest_commit(&self) -> Result<String, Error> {
95 let response = self.client().get(COMMIT_DATA_URL).send().await?;
96 #[cfg(debug_assertions)]
97 let data = {
98 let text = response.text().await?;
99 serde_json::from_str::<GithubBranchData>(text.as_str())
100 .map_err(|v| Error::Deserialisation(v.to_string()))?
101 };
102 #[cfg(not(debug_assertions))]
103 let data: GithubBranchData = response.json().await?;
104
105 Ok(data.commit.sha)
106 }
107
108 async fn get_data(&self) -> Result<Data, reqwest::Error> {
109 let response = self.client().get(FONT_INDEX_URL).send().await?;
110 let data = response.json::<Data>().await?;
111 Ok(data)
112 }
113
114 fn cache_write_str<S, V>(&self, file: S, value: V) -> Result<(), Error> where S: AsRef<Path>, V: Into<String> {
115 let path = self.cache_file(file);
116 create_parent(&path)?;
117
118 let str: String = value.into();
119 let mut file = File::create(path)?;
120 file.write_all(str.as_bytes())?;
121
122 Ok(())
123 }
124 fn cache_write_serialise<P, T>(&self, file: P, value: &T) -> Result<(), Error> where P: AsRef<Path>, T: Serialize {
125 let path = self.cache_file(file);
126 create_parent(&path)?;
127
128 let file = File::create(path)?;
129 serde_json::ser::to_writer(file, value)
130 .map_err(|v| Error::Generic(format!("Error whilst serialising: {}", v)))?;
131
132 Ok(())
133 }
134 fn cache_read_deserialise<'de, T, P>(&self, file: P) -> Result<T, Error> where P: AsRef<Path>, T: DeserializeOwned {
135 let path = self.cache_file(file);
136 if !path.exists() {
137 return Err(Error::IO(IOErrorKind::NotFound.into()))
138 }
139
140 let file = File::open(path)?;
141 let result = serde_json::de::from_reader::<_, T>(file)
142 .map_err(|v| Error::Deserialisation(format!("{}", v)));
143
144 result
145 }
146
147 fn read_data(&self) -> Result<Data, Error> {
148 let data_file = self.cache_file(DATA_FILE);
149 if !data_file.exists() {
150 return Err(Error::IO(IOError::new(IOErrorKind::NotFound, "Data file does not exist")))
151 }
152
153 self.cache_read_deserialise(data_file)
154 }
155}
156
157#[async_trait]
158impl<'host> Source<'host> for GoogleFontsSource<'host> {
159 fn id(&self) -> &'host str {
160 return Self::ID;
161 }
162 fn name(&self) -> &'host str {
163 return Self::NAME;
164 }
165
166 fn set_host(&mut self, host: &'host dyn FpmHost) {
167 self.host = host;
168 self.client = Some(
169 ClientBuilder::new()
170 .user_agent(host.user_agent())
171 .build()
172 .expect("HTTP client required")
173 )
174 }
175
176 async fn refresh(&self, force: bool) -> Result<RefreshOutput, Error> {
177 let cache_file = self.cache_file(DATA_FILE);
178 let latest = self.latest_commit().await?;
179 trace!("[{}] Latest commit: {}", Self::ID, latest);
180
181 if !force {
182 let current = self.last_downloaded_commit();
183 trace!("[{}] Last downloaded commit: {}", Self::ID, current.clone().unwrap_or("<none>".into()));
184 if current != None && cache_file.exists() && current.unwrap() == latest {
185 return Ok(RefreshOutput::AlreadyUpToDate)
186 }
187 }
188
189 let index = self.get_data().await?;
190 self.cache_write_serialise(DATA_FILE, &index)?;
191 self.cache_write_str(COMMIT_FILE, latest)?;
192
193 Ok(RefreshOutput::Downloaded)
194 }
195
196 async fn resolve_font(&self, spec: &FontInstallSpec) -> Result<(DefinedFontInstallSpec, FpmFontDescription), Error> {
197 let data = self.read_data()?;
198
199 let family = data.get_family(&spec.id).ok_or(Error::NoSuchFamily(spec.id.clone()))?;
200
201 Ok((family.clone().try_into()?, family.into()))
202 }
203
204 async fn download_font(&self, font_id: &DefinedFontInstallSpec, dir: &PathBuf) -> Result<HashMap<DefinedFontVariantSpec, PathBuf>, Error> {
205 let data = self.read_data()?;
206
207 let font = if let Some(desc) = data.get_family(&font_id.id) {
208 desc
209 } else {
210 return Err(Error::Generic(format!("Font {} does not exist", font_id.id)))
211 };
212
213 let dir = dir.join(&font_id.id);
214 let mut paths = HashMap::new();
215
216 for variant in &font_id.styles {
217 let variant_name = variant_to_string(variant);
218 let remote_file = match font.files.get(variant_name.as_str()) {
219 Some(file) => file.clone(),
220 None => return Err(Error::Generic(format!("Could not get file for font variant {variant_name}")))
221 };
222
223 let extension = PathBuf::from(&remote_file).extension().map_or(String::new(), |v| ".".to_string() + v.to_str().unwrap());
224 let url_hash = Sha256::new()
225 .chain_update(&remote_file)
226 .finalize();
227 let url_hash = format!("{:x}", url_hash);
228 let path = dir.join(variant_name).join(format!("{}{}", url_hash, extension));
229
230 paths.insert(variant.clone(), path.clone());
231 if path.exists() {
232 continue;
233 }
234 path.parent().map(create_dir_all);
235
236 let remote_data = reqwest::get("https://".to_string() + remote_file.as_str()).await?.error_for_status()?;
237 let mut file = File::create(&path)?;
238 let remote_data = remote_data.bytes().await?;
239 file.write_all(remote_data.as_ref())?;
240 }
241
242 Ok(paths)
243 }
244}