11use  std:: collections:: { BTreeMap ,  HashMap } ; 
22use  std:: ffi:: OsString ; 
33use  std:: fs:: { self ,  File } ; 
4- use  std:: io:: { self ,  BufWriter ,  Cursor ,  Write } ; 
4+ use  std:: io:: { self ,  BufWriter ,  Cursor ,  Read ,   Write } ; 
55use  std:: path:: { Path ,  PathBuf } ; 
6- use  std:: time:: Duration ; 
6+ use  std:: time:: { Duration ,   SystemTime } ; 
77
88use  once_cell:: unsync:: OnceCell ; 
99use  ureq:: tls:: { RootCerts ,  TlsConfig } ; 
1010use  yansi:: Paint ; 
1111use  zip:: ZipArchive ; 
1212
13- use  crate :: config:: Config ; 
13+ use  crate :: config:: CacheConfig ; 
1414use  crate :: error:: { Error ,  Result } ; 
1515use  crate :: util:: { self ,  info_end,  info_start,  infoln,  warnln,  Dedup } ; 
1616
@@ -19,18 +19,43 @@ const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO_PKG_VE
1919
2020type  PagesArchive  = ZipArchive < Cursor < Vec < u8 > > > ; 
2121
22- pub  struct  Cache < ' a >  { 
23-     dir :  & ' a  Path , 
22+ pub  struct  Cache  { 
2423    platforms :  OnceCell < Vec < OsString > > , 
25-     age :  OnceCell < Duration > , 
24+     cfg :  CacheConfig , 
25+     update_cache_on_drop :  bool , 
26+ } 
27+ impl  Drop  for  Cache  { 
28+     fn  drop ( & mut  self )  { 
29+         if  self . update_cache_on_drop  { 
30+             let  _ = ( || -> Result < ( ) >  { 
31+                 // if cache is no longer stale 
32+                 if  !self . is_stale ( ) ? { 
33+                     return  Ok ( ( ) ) ; 
34+                 } 
35+                 infoln ! ( "cache is stale, updating..." ) ; 
36+                 if  let  Err ( e)  = self 
37+                     . update ( ) 
38+                     . map_err ( |e| e. describe ( Error :: DESC_AUTO_UPDATE_ERR ) ) 
39+                 { 
40+                     // use this only for printing 
41+                     e. exit_code ( ) ; 
42+                 } 
43+                 Ok ( ( ) ) 
44+             } ) ( ) ; 
45+         } 
46+     } 
2647} 
2748
28- impl < ' a >  Cache < ' a >  { 
29-     pub  fn  new ( dir :  & ' a  Path )  -> Self  { 
49+ impl  Cache  { 
50+     pub  fn  new ( mut  cfg :  CacheConfig )  -> Self  { 
51+         // Sort to always download archives in alphabetical order. 
52+         cfg. languages . sort_unstable ( ) ; 
53+         // The user can put duplicates in the config file. 
54+         cfg. languages . dedup ( ) ; 
3055        Self  { 
31-             dir, 
3256            platforms :  OnceCell :: new ( ) , 
33-             age :  OnceCell :: new ( ) , 
57+             cfg, 
58+             update_cache_on_drop :  false , 
3459        } 
3560    } 
3661
@@ -41,7 +66,7 @@ impl<'a> Cache<'a> {
4166
4267    /// Return `true` if the specified subdirectory exists in the cache. 
4368pub  fn  subdir_exists ( & self ,  sd :  & str )  -> bool  { 
44-         self . dir . join ( sd) . is_dir ( ) 
69+         self . cfg . dir . join ( sd) . is_dir ( ) 
4570    } 
4671
4772    /// Send a GET request with the provided agent and return the response body. 
@@ -76,11 +101,7 @@ impl<'a> Cache<'a> {
76101    } 
77102
78103    /// Download tldr pages archives for directories that are out of date and update the checksum file. 
79- fn  download_and_verify ( 
80-         & self , 
81-         mirror :  & str , 
82-         languages :  & [ String ] , 
83-     )  -> Result < BTreeMap < String ,  PagesArchive > >  { 
104+ fn  download_and_verify ( & self )  -> Result < BTreeMap < String ,  PagesArchive > >  { 
84105        let  agent = ureq:: Agent :: config_builder ( ) 
85106            . user_agent ( USER_AGENT ) 
86107            . timeout_global ( Some ( Duration :: from_secs ( 30 ) ) ) 
@@ -92,17 +113,25 @@ impl<'a> Cache<'a> {
92113            . build ( ) 
93114            . into ( ) ; 
94115
95-         let  sums = Self :: get_asset ( & agent,  & format ! ( "{mirror }/tldr.sha256sums" ) ) ?; 
116+         let  sums = Self :: get_asset ( & agent,  & format ! ( "{}/tldr.sha256sums" ,   self . cfg . mirror ) ) ?; 
96117        let  sums_str = String :: from_utf8_lossy ( & sums) ; 
97118        let  sum_map = Self :: parse_sumfile ( & sums_str) ?; 
98119
99-         let  old_sumfile_path = self . dir . join ( "tldr.sha256sums" ) ; 
100-         let  old_sums = fs:: read_to_string ( & old_sumfile_path) . unwrap_or_default ( ) ; 
120+         let  old_sumfile_path = self . cfg . dir . join ( "tldr.sha256sums" ) ; 
121+         let  mut  old_sumfile = fs:: File :: open ( & old_sumfile_path) ; 
122+         let  old_sums = old_sumfile
123+             . as_mut ( ) 
124+             . map ( |f| { 
125+                 let  mut  s = String :: new ( ) ; 
126+                 let  _ = f. read_to_string ( & mut  s) ; 
127+                 s
128+             } ) 
129+             . unwrap_or_default ( ) ; 
101130        let  old_sum_map = Self :: parse_sumfile ( & old_sums) . unwrap_or_default ( ) ; 
102131
103132        let  mut  langdir_archive_map = BTreeMap :: new ( ) ; 
104133
105-         for  lang in  languages { 
134+         for  lang in  & self . cfg . languages  { 
106135            let  lang = & * * lang; 
107136            let  Some ( sum)  = sum_map. get ( lang)  else  { 
108137                // Skip nonexistent languages. 
@@ -112,10 +141,17 @@ impl<'a> Cache<'a> {
112141            let  lang_dir = format ! ( "pages.{lang}" ) ; 
113142            if  Some ( sum)  == old_sum_map. get ( lang)  && self . subdir_exists ( & lang_dir)  { 
114143                infoln ! ( "'pages.{lang}' is up to date" ) ; 
144+                 // update modified to current timestamp to refresh cache age 
145+                 if  let  Ok ( ref  mut  file)  = old_sumfile { 
146+                     file. set_modified ( SystemTime :: now ( ) ) ?; 
147+                 } 
115148                continue ; 
116149            } 
117150
118-             let  archive = Self :: get_asset ( & agent,  & format ! ( "{mirror}/tldr-pages.{lang}.zip" ) ) ?; 
151+             let  archive = Self :: get_asset ( 
152+                 & agent, 
153+                 & format ! ( "{}/tldr-pages.{}.zip" ,  self . cfg. mirror,  lang) , 
154+             ) ?; 
119155            info_start ! ( "validating sha256sums... " ) ; 
120156            let  actual_sum = util:: sha256_hexdigest ( & archive) ; 
121157
@@ -133,7 +169,7 @@ impl<'a> Cache<'a> {
133169            langdir_archive_map. insert ( lang_dir,  ZipArchive :: new ( Cursor :: new ( archive) ) ?) ; 
134170        } 
135171
136-         fs:: create_dir_all ( self . dir ) ?; 
172+         fs:: create_dir_all ( & self . cfg . dir ) ?; 
137173        File :: create ( & old_sumfile_path) ?. write_all ( & sums) ?; 
138174
139175        Ok ( langdir_archive_map) 
@@ -199,7 +235,7 @@ impl<'a> Cache<'a> {
199235                continue ; 
200236            } 
201237
202-             let  path = self . dir . join ( lang_dir) . join ( & fname) ; 
238+             let  path = self . cfg . dir . join ( lang_dir) . join ( & fname) ; 
203239
204240            if  zipfile. is_dir ( )  { 
205241                fs:: create_dir_all ( & path) ?; 
@@ -226,13 +262,8 @@ impl<'a> Cache<'a> {
226262    } 
227263
228264    /// Delete the old cache and replace it with a fresh copy. 
229- pub  fn  update ( & self ,  mirror :  & str ,  languages :  & mut  Vec < String > )  -> Result < ( ) >  { 
230-         // Sort to always download archives in alphabetical order. 
231-         languages. sort_unstable ( ) ; 
232-         // The user can put duplicates in the config file. 
233-         languages. dedup ( ) ; 
234- 
235-         let  archives = self . download_and_verify ( mirror,  languages) ?; 
265+ pub  fn  update ( & self )  -> Result < ( ) >  { 
266+         let  archives = self . download_and_verify ( ) ?; 
236267
237268        if  archives. is_empty ( )  { 
238269            infoln ! ( 
@@ -249,7 +280,7 @@ impl<'a> Cache<'a> {
249280            #[ allow( clippy:: cast_possible_truncation,  clippy:: cast_possible_wrap) ]  
250281            let  n_existing = self . list_all_vec ( & lang_dir) . map ( |v| v. len ( ) ) . unwrap_or ( 0 )  as  i32 ; 
251282
252-             let  lang_dir_full = self . dir . join ( & lang_dir) ; 
283+             let  lang_dir_full = self . cfg . dir . join ( & lang_dir) ; 
253284            if  lang_dir_full. is_dir ( )  { 
254285                fs:: remove_dir_all ( & lang_dir_full) ?; 
255286            } 
@@ -277,15 +308,15 @@ impl<'a> Cache<'a> {
277308
278309    /// Delete the cache directory. 
279310pub  fn  clean ( & self )  -> Result < ( ) >  { 
280-         if  !self . dir . is_dir ( )  { 
311+         if  !self . cfg . dir . is_dir ( )  { 
281312            infoln ! ( "cache does not exist, not cleaning." ) ; 
282-             fs:: create_dir_all ( self . dir ) ?; 
313+             fs:: create_dir_all ( & self . cfg . dir ) ?; 
283314            return  Ok ( ( ) ) ; 
284315        } 
285316
286317        infoln ! ( "cleaning the cache directory..." ) ; 
287-         fs:: remove_dir_all ( self . dir ) ?; 
288-         fs:: create_dir_all ( self . dir ) ?; 
318+         fs:: remove_dir_all ( & self . cfg . dir ) ?; 
319+         fs:: create_dir_all ( & self . cfg . dir ) ?; 
289320
290321        Ok ( ( ) ) 
291322    } 
@@ -296,7 +327,7 @@ impl<'a> Cache<'a> {
296327            . get_or_try_init ( || { 
297328                let  mut  result = vec ! [ ] ; 
298329
299-                 for  entry in  fs:: read_dir ( self . dir . join ( ENGLISH_DIR ) ) ? { 
330+                 for  entry in  fs:: read_dir ( self . cfg . dir . join ( ENGLISH_DIR ) ) ? { 
300331                    let  entry = entry?; 
301332                    let  path = entry. path ( ) ; 
302333                    let  platform = path. file_name ( ) . unwrap ( ) ; 
@@ -339,7 +370,7 @@ impl<'a> Cache<'a> {
339370        P :  AsRef < Path > , 
340371    { 
341372        for  lang_dir in  lang_dirs { 
342-             let  path = self . dir . join ( lang_dir) . join ( & platform) . join ( fname) ; 
373+             let  path = self . cfg . dir . join ( lang_dir) . join ( & platform) . join ( fname) ; 
343374
344375            if  path. is_file ( )  { 
345376                return  Some ( path) ; 
@@ -350,11 +381,38 @@ impl<'a> Cache<'a> {
350381    } 
351382
352383    /// Find all pages with the given name. 
353- pub  fn  find ( & self ,  name :  & str ,  languages :  & [ String ] ,  platform :  & str )  -> Result < Vec < PathBuf > >  { 
384+ pub  fn  find ( 
385+         & self , 
386+         name :  & str , 
387+         languages_override :  Option < & [ String ] > , 
388+         platform :  & str , 
389+     )  -> Result < Vec < PathBuf > >  { 
390+         let  res = self . find_paths ( name,  languages_override,  platform) ; 
391+         // if no paths were found and we are using optimistic cache, let's run update and search 
392+         // again 
393+         if  res. as_ref ( ) . is_ok_and ( Vec :: is_empty) 
394+             && self . is_stale ( ) . unwrap_or_default ( ) 
395+             && self . cfg . optimistic_cache 
396+         { 
397+             warnln ! ( "Page not found, updating cache" ) ; 
398+             self . update ( ) ?; 
399+             self . find_paths ( name,  languages_override,  platform) 
400+         }  else  { 
401+             res
402+         } 
403+     } 
404+ 
405+     fn  find_paths ( 
406+         & self , 
407+         name :  & str , 
408+         languages_override :  Option < & [ String ] > , 
409+         platform :  & str , 
410+     )  -> Result < Vec < PathBuf > >  { 
354411        // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-resolution 
355412
356413        let  platforms = self . get_platforms_and_check ( platform) ?; 
357414        let  file = format ! ( "{name}.md" ) ; 
415+         let  languages = languages_override. unwrap_or ( & self . cfg . languages ) ; 
358416
359417        let  mut  result = vec ! [ ] ; 
360418        let  mut  lang_dirs:  Vec < String >  = languages. iter ( ) . map ( |x| format ! ( "pages.{x}" ) ) . collect ( ) ; 
@@ -411,7 +469,7 @@ impl<'a> Cache<'a> {
411469        P :  AsRef < Path > , 
412470        Q :  AsRef < Path > , 
413471    { 
414-         match  fs:: read_dir ( self . dir . join ( lang_dir. as_ref ( ) ) . join ( platform) )  { 
472+         match  fs:: read_dir ( self . cfg . dir . join ( lang_dir. as_ref ( ) ) . join ( platform) )  { 
415473            Ok ( entries)  => { 
416474                let  entries = entries. map ( |res| res. map ( |ent| ent. file_name ( ) ) ) ; 
417475                Ok ( entries. collect :: < io:: Result < Vec < OsString > > > ( ) ?) 
@@ -493,7 +551,7 @@ impl<'a> Cache<'a> {
493551
494552    /// List languages (used in shell completions). 
495553pub  fn  list_languages ( & self )  -> Result < ( ) >  { 
496-         let  languages = fs:: read_dir ( self . dir ) ?
554+         let  languages = fs:: read_dir ( & self . cfg . dir ) ?
497555            . filter ( |res| res. is_ok ( )  && res. as_ref ( ) . unwrap ( ) . path ( ) . is_dir ( ) ) 
498556            . map ( |res| res. unwrap ( ) . file_name ( ) ) ; 
499557        let  mut  stdout = io:: stdout ( ) . lock ( ) ; 
@@ -509,11 +567,11 @@ impl<'a> Cache<'a> {
509567    } 
510568
511569    /// Show cache information. 
512- pub  fn  info ( & self ,   cfg :   & Config )  -> Result < ( ) >  { 
570+ pub  fn  info ( & self )  -> Result < ( ) >  { 
513571        let  mut  n_map = BTreeMap :: new ( ) ; 
514572        let  mut  n_total = 0 ; 
515573
516-         for  lang_dir in  fs:: read_dir ( self . dir ) ? { 
574+         for  lang_dir in  fs:: read_dir ( & self . cfg . dir ) ? { 
517575            let  lang_dir = lang_dir?; 
518576            if  !lang_dir. path ( ) . is_dir ( )  { 
519577                continue ; 
@@ -534,12 +592,12 @@ impl<'a> Cache<'a> {
534592        writeln ! ( 
535593            stdout, 
536594            "Cache: {} (last update: {} ago)" , 
537-             self . dir. display( ) . red( ) , 
595+             self . cfg . dir. display( ) . red( ) , 
538596            util:: duration_fmt( age) . green( ) . bold( ) 
539597        ) ?; 
540598
541-         if  cfg . cache . auto_update  { 
542-             let  max_age = cfg . cache_max_age ( ) . as_secs ( ) ; 
599+         if  self . cfg . auto_update  { 
600+             let  max_age = self . max_age ( ) . as_secs ( ) ; 
543601            if  max_age > age { 
544602                let  age_diff = max_age - age; 
545603
@@ -568,27 +626,62 @@ impl<'a> Cache<'a> {
568626
569627        Ok ( ( ) ) 
570628    } 
629+     /// Checks cache status and downloads it stale or empty. 
630+ pub  fn  load ( & mut  self ,  is_offline :  bool )  -> Result < ( ) >  { 
631+         if  !self . subdir_exists ( ENGLISH_DIR )  { 
632+             if  is_offline { 
633+                 return  Err ( Error :: offline_no_cache ( ) ) ; 
634+             } 
635+             infoln ! ( "cache is empty, downloading..." ) ; 
636+             self . update ( ) ?; 
637+         }  else  if  self . cfg . auto_update  && self . age ( ) ? > self . max_age ( )  { 
638+             let  age = util:: duration_fmt ( self . age ( ) ?. as_secs ( ) ) ; 
639+             let  age = age. green ( ) . bold ( ) ; 
640+ 
641+             if  is_offline { 
642+                 warnln ! ( 
643+                 "cache is stale (last update: {age} ago). Run tldr without --offline to update." 
644+             ) ; 
645+             }  else  if  self . cfg . optimistic_cache  { 
646+                 self . update_cache_on_drop  = true ; 
647+                 // For optimistic cache, we'll notify the user but defer the update until after displaying the page 
648+                 warnln ! ( 
649+                 "cache is stale (last update: {age} ago), will defer update after cache lookup. Run without --optimistic-cache to update before lookup" 
650+             ) ; 
651+             }  else  { 
652+                 infoln ! ( "cache is stale (last update: {age} ago), updating..." ) ; 
653+                 self . update ( ) 
654+                     . map_err ( |e| e. describe ( Error :: DESC_AUTO_UPDATE_ERR ) ) ?; 
655+             } 
656+         } 
657+         Ok ( ( ) ) 
658+     } 
659+ 
660+     pub  fn  is_stale ( & self )  -> Result < bool >  { 
661+         self . age ( ) . map ( |age| age > self . max_age ( ) ) 
662+     } 
571663
572664    /// Get the age of the cache. 
573- pub  fn  age ( & self )  -> Result < Duration >  { 
574-         self . age 
575-             . get_or_try_init ( || { 
576-                 let  sumfile = self . dir . join ( "tldr.sha256sums" ) ; 
577-                 let  metadata = if  sumfile. is_file ( )  { 
578-                     fs:: metadata ( & sumfile) 
579-                 }  else  { 
580-                     // The sumfile is not available, fall back to the base directory. 
581-                     fs:: metadata ( self . dir ) 
582-                 } ?; 
665+ fn  age ( & self )  -> Result < Duration >  { 
666+         let  sumfile = self . cfg . dir . join ( "tldr.sha256sums" ) ; 
667+         let  metadata = if  sumfile. is_file ( )  { 
668+             fs:: metadata ( & sumfile) 
669+         }  else  { 
670+             // The sumfile is not available, fall back to the base directory. 
671+             fs:: metadata ( & self . cfg . dir ) 
672+         } ?; 
583673
584-                  metadata. modified ( ) ?. elapsed ( ) . map_err ( |_| { 
585-                      Error :: new ( 
586-                          "the system clock is not functioning correctly.\n \  
674+         metadata. modified ( ) ?. elapsed ( ) . map_err ( |_| { 
675+             Error :: new ( 
676+                 "the system clock is not functioning correctly.\n \  
587677\n \ 
588678, 
589-                     ) 
590-                 } ) 
591-             } ) 
592-             . copied ( ) 
679+             ) 
680+         } ) 
681+     } 
682+     /// Convert the number of hours from config to a `Duration`. 
683+ pub  const  fn  max_age ( & self )  -> Duration  { 
684+         return  Duration :: from_secs ( 5 ) ; 
685+         Duration :: from_secs ( self . cfg . max_age  *  60  *  60 ) 
593686    } 
594687} 
0 commit comments