self_update_extras/
throttle.rs1use self_update::Status;
4use self_update::errors::{Error, Result};
5use self_update::update::{Release, ReleaseUpdate, UpdateStatus};
6use std::time::Duration;
7
8const DEFAULT_THROTTLE_WINDOW: Duration = Duration::from_secs(15 * 60);
10
11#[derive(Default)]
17pub struct UpdateBuilder {
18 release_update: Option<Box<dyn ReleaseUpdate>>,
19 throttle_window: Option<Duration>,
20}
21
22impl UpdateBuilder {
23 pub fn new() -> Self {
25 Default::default()
26 }
27
28 pub fn release_update(&mut self, release_update: Box<dyn ReleaseUpdate>) -> &mut Self {
30 self.release_update = Some(release_update);
31 self
32 }
33
34 pub fn throttle_window(&mut self, window: Duration) -> &mut Self {
36 self.throttle_window = Some(window);
37 self
38 }
39
40 pub fn build(&mut self) -> Result<Box<dyn ReleaseUpdate>> {
45 let inner = self
46 .release_update
47 .take()
48 .ok_or_else(|| Error::Config("`release_update` required".to_owned()))?;
49
50 Ok(Box::new(Update {
51 inner,
52 throttle_window: self.throttle_window.unwrap_or(DEFAULT_THROTTLE_WINDOW),
53 }))
54 }
55}
56
57pub struct Update {
69 inner: Box<dyn ReleaseUpdate>,
70 throttle_window: Duration,
71}
72
73impl Update {
74 pub fn configure() -> UpdateBuilder {
76 UpdateBuilder::new()
77 }
78
79 fn should_check(&self) -> bool {
81 let path = self.throttle_path();
82 match std::fs::metadata(&path) {
83 Ok(meta) => {
84 if let Ok(mtime) = meta.modified()
85 && let Ok(elapsed) = std::time::SystemTime::now().duration_since(mtime)
86 {
87 return elapsed > self.throttle_window;
88 }
89 true
90 }
91 Err(_) => true,
92 }
93 }
94
95 fn throttle_path(&self) -> std::path::PathBuf {
97 std::env::temp_dir().join(format!("{}.throttle", &self.inner.bin_name()))
98 }
99
100 fn touch_throttle(&self) {
102 let _ = std::fs::OpenOptions::new()
103 .create(true)
104 .truncate(true)
105 .write(true)
106 .open(self.throttle_path());
107 }
108}
109
110impl ReleaseUpdate for Update {
111 fn get_latest_release(&self) -> Result<Release> {
112 self.inner.get_latest_release()
113 }
114
115 fn get_latest_releases(&self, current_version: &str) -> Result<Vec<Release>> {
116 self.inner.get_latest_releases(current_version)
117 }
118
119 fn get_release_version(&self, ver: &str) -> Result<Release> {
120 self.inner.get_release_version(ver)
121 }
122
123 fn current_version(&self) -> String {
124 self.inner.current_version()
125 }
126
127 fn target(&self) -> String {
128 self.inner.target()
129 }
130
131 fn target_version(&self) -> Option<String> {
132 self.inner.target_version()
133 }
134
135 fn bin_name(&self) -> String {
136 self.inner.bin_name()
137 }
138
139 fn bin_install_path(&self) -> std::path::PathBuf {
140 self.inner.bin_install_path()
141 }
142
143 fn bin_path_in_archive(&self) -> String {
144 self.inner.bin_path_in_archive()
145 }
146
147 fn show_download_progress(&self) -> bool {
148 self.inner.show_download_progress()
149 }
150
151 fn show_output(&self) -> bool {
152 self.inner.show_output()
153 }
154
155 fn no_confirm(&self) -> bool {
156 self.inner.no_confirm()
157 }
158
159 fn progress_template(&self) -> String {
160 self.inner.progress_template()
161 }
162
163 fn progress_chars(&self) -> String {
164 self.inner.progress_chars()
165 }
166
167 fn auth_token(&self) -> Option<String> {
168 self.inner.auth_token()
169 }
170
171 fn update(&self) -> Result<Status> {
172 if !self.should_check() {
173 return Ok(Status::UpToDate(self.current_version()));
174 }
175
176 let result = self.inner.update();
177 self.touch_throttle();
178 result
179 }
180
181 fn update_extended(&self) -> Result<UpdateStatus> {
182 if !self.should_check() {
183 return Ok(UpdateStatus::UpToDate);
184 }
185
186 let result = self.inner.update_extended();
187 self.touch_throttle();
188 result
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::test_support::MockRelease;
196
197 fn remove(path: &std::path::Path) {
198 let _ = std::fs::remove_file(path);
199 }
200
201 fn concrete(mock: MockRelease, window: Duration) -> Update {
205 Update {
206 inner: Box::new(mock),
207 throttle_window: window,
208 }
209 }
210
211 #[test]
215 fn should_check_is_true_without_a_throttle_file() {
216 let updater = concrete(
217 MockRelease::new("mock-throttle-missing"),
218 DEFAULT_THROTTLE_WINDOW,
219 );
220 remove(&updater.throttle_path());
221
222 assert!(updater.should_check());
223 }
224
225 #[test]
226 fn should_check_is_false_with_a_recent_throttle_file() {
227 let updater = concrete(
228 MockRelease::new("mock-throttle-recent"),
229 DEFAULT_THROTTLE_WINDOW,
230 );
231 let path = updater.throttle_path();
232 remove(&path);
233
234 updater.touch_throttle();
235
236 assert!(!updater.should_check());
237 remove(&path);
238 }
239
240 #[test]
241 fn should_check_is_true_with_an_expired_throttle_file() {
242 let updater = concrete(
243 MockRelease::new("mock-throttle-expired"),
244 Duration::from_secs(60),
245 );
246 let path = updater.throttle_path();
247 std::fs::write(&path, "").unwrap();
248
249 let expired = std::time::SystemTime::now()
250 .duration_since(std::time::UNIX_EPOCH)
251 .unwrap()
252 - Duration::from_secs(120);
253 let expired = filetime::FileTime::from_unix_time(expired.as_secs() as i64, 0);
254 filetime::set_file_mtime(&path, expired).unwrap();
255
256 assert!(updater.should_check());
257 remove(&path);
258 }
259
260 #[test]
261 fn touch_throttle_creates_the_throttle_file() {
262 let updater = concrete(
263 MockRelease::new("mock-throttle-touch"),
264 DEFAULT_THROTTLE_WINDOW,
265 );
266 let path = updater.throttle_path();
267 remove(&path);
268
269 updater.touch_throttle();
270
271 assert!(path.exists());
272 remove(&path);
273 }
274
275 #[test]
276 fn update_runs_inner_and_touches_throttle_when_allowed() {
277 let mock = MockRelease::new("mock-throttle-run");
278 let calls = mock.call_counter();
279 let updater = concrete(mock, DEFAULT_THROTTLE_WINDOW);
280 let path = updater.throttle_path();
281 remove(&path);
282
283 let status = updater.update().unwrap();
284
285 assert!(matches!(status, Status::UpToDate(_)));
286 assert_eq!(calls.get(), 1);
287 assert!(
288 path.exists(),
289 "throttle file should be touched after a check"
290 );
291 remove(&path);
292 }
293
294 #[test]
295 fn update_skips_inner_when_throttled() {
296 let mock = MockRelease::new("mock-throttle-skip");
297 let calls = mock.call_counter();
298 let updater = concrete(mock, DEFAULT_THROTTLE_WINDOW);
299 let path = updater.throttle_path();
300 remove(&path);
301 updater.touch_throttle();
302
303 let status = updater.update().unwrap();
304
305 assert!(matches!(status, Status::UpToDate(_)));
306 assert_eq!(calls.get(), 0);
307 remove(&path);
308 }
309
310 #[test]
311 fn update_extended_runs_inner_when_allowed() {
312 let mock = MockRelease::new("mock-throttle-run-ext");
313 let calls = mock.call_counter();
314 let updater = concrete(mock, DEFAULT_THROTTLE_WINDOW);
315 let path = updater.throttle_path();
316 remove(&path);
317
318 let status = updater.update_extended().unwrap();
319
320 assert!(matches!(status, UpdateStatus::UpToDate));
321 assert_eq!(calls.get(), 1);
322 assert!(path.exists());
323 remove(&path);
324 }
325
326 #[test]
327 fn update_extended_skips_inner_when_throttled() {
328 let mock = MockRelease::new("mock-throttle-skip-ext");
329 let calls = mock.call_counter();
330 let updater = concrete(mock, DEFAULT_THROTTLE_WINDOW);
331 let path = updater.throttle_path();
332 remove(&path);
333 updater.touch_throttle();
334
335 let status = updater.update_extended().unwrap();
336
337 assert!(matches!(status, UpdateStatus::UpToDate));
338 assert_eq!(calls.get(), 0);
339 remove(&path);
340 }
341
342 #[test]
343 fn build_requires_a_release_update() {
344 assert!(Update::configure().build().is_err());
345 }
346
347 #[test]
348 fn builder_wraps_the_release_update() {
349 let mock = MockRelease::new("mock-throttle-builder");
350 let calls = mock.call_counter();
351 let path = std::env::temp_dir().join("mock-throttle-builder.throttle");
352 remove(&path);
353
354 let updater = Update::configure()
355 .release_update(Box::new(mock))
356 .throttle_window(Duration::from_secs(60))
357 .build()
358 .unwrap();
359
360 updater.update().unwrap();
361
362 assert_eq!(calls.get(), 1);
363 remove(&path);
364 }
365}