1- use anyhow:: { Context as _, Result } ;
1+ use anyhow:: { bail , Context as _, Result } ;
22use std:: {
3- fs,
3+ collections:: HashMap ,
4+ fs:: { self , OpenOptions } ,
45 path:: { Path , PathBuf } ,
6+ time:: SystemTime ,
57} ;
8+ use sysinfo:: { DiskExt , RefreshKind , System , SystemExt } ;
69use tracing:: { debug, instrument, warn} ;
710
11+ static LAST_ACCESSED_FILE_NAME : & str = "docsrs_last_accessed" ;
12+
13+ /// gives you the percentage of free disk space on the
14+ /// filesystem where the given `path` lives on.
15+ /// Return value is between 0 and 1.
16+ fn free_disk_space_ratio < P : AsRef < Path > > ( path : P ) -> Result < f32 > {
17+ let sys = System :: new_with_specifics ( RefreshKind :: new ( ) . with_disks ( ) ) ;
18+
19+ let disk_by_mount_point: HashMap < _ , _ > =
20+ sys. disks ( ) . iter ( ) . map ( |d| ( d. mount_point ( ) , d) ) . collect ( ) ;
21+
22+ let path = path. as_ref ( ) ;
23+
24+ if let Some ( disk) = path. ancestors ( ) . find_map ( |p| disk_by_mount_point. get ( p) ) {
25+ Ok ( ( disk. available_space ( ) as f64 / disk. total_space ( ) as f64 ) as f32 )
26+ } else {
27+ bail ! ( "could not find mount point for path {}" , path. display( ) ) ;
28+ }
29+ }
30+
831/// artifact caching with cleanup
932#[ derive( Debug ) ]
1033pub ( crate ) struct ArtifactCache {
@@ -23,10 +46,8 @@ impl ArtifactCache {
2346
2447 /// clean up a target directory.
2548 ///
26- /// Should delete all things that shouldn't leak between
27- /// builds, so:
28- /// - doc-output
29- /// - ...?
49+ /// Will:
50+ /// * delete the doc output in the root & target directories
3051 #[ instrument( skip( self ) ) ]
3152 fn cleanup ( & self , target_dir : & Path ) -> Result < ( ) > {
3253 // proc-macro crates have a `doc` directory
@@ -50,6 +71,78 @@ impl ArtifactCache {
5071 Ok ( ( ) )
5172 }
5273
74+ fn cache_dir_for_key ( & self , cache_key : & str ) -> PathBuf {
75+ self . cache_dir . join ( cache_key)
76+ }
77+
78+ /// update the "last used" marker for the cache key
79+ fn touch ( & self , cache_key : & str ) -> Result < ( ) > {
80+ let file = self
81+ . cache_dir_for_key ( cache_key)
82+ . join ( LAST_ACCESSED_FILE_NAME ) ;
83+
84+ fs:: create_dir_all ( file. parent ( ) . expect ( "we always have a parent" ) ) ?;
85+ if file. exists ( ) {
86+ fs:: remove_file ( & file) ?;
87+ }
88+ OpenOptions :: new ( ) . create ( true ) . write ( true ) . open ( & file) ?;
89+ Ok ( ( ) )
90+ }
91+
92+ /// return the list of cache-directories, sorted by last usage.
93+ ///
94+ /// The oldest / least used cache will be first.
95+ /// To be used for cleanup.
96+ ///
97+ /// A missing age-marker file is interpreted as "old age".
98+ fn all_cache_folders_by_age ( & self ) -> Result < Vec < PathBuf > > {
99+ let mut entries: Vec < ( PathBuf , Option < SystemTime > ) > = fs:: read_dir ( & self . cache_dir ) ?
100+ . filter_map ( Result :: ok)
101+ . filter_map ( |entry| {
102+ let path = entry. path ( ) ;
103+ path. is_dir ( ) . then ( || {
104+ let last_accessed = path
105+ . join ( LAST_ACCESSED_FILE_NAME )
106+ . metadata ( )
107+ . and_then ( |metadata| metadata. modified ( ) )
108+ . ok ( ) ;
109+ ( path, last_accessed)
110+ } )
111+ } )
112+ . collect ( ) ;
113+
114+ // `None` will appear first after sorting
115+ entries. sort_by_key ( |( _, time) | * time) ;
116+
117+ Ok ( entries. into_iter ( ) . map ( |( path, _) | path) . collect ( ) )
118+ }
119+
120+ /// free up disk space by deleting the oldest cache folders.
121+ ///
122+ /// Deletes cache folders until the `free_percent_goal` is reached.
123+ pub ( crate ) fn clear_disk_space ( & self , free_percent_goal : f32 ) -> Result < ( ) > {
124+ let space_ok =
125+ || -> Result < bool > { Ok ( free_disk_space_ratio ( & self . cache_dir ) ? >= free_percent_goal) } ;
126+
127+ if space_ok ( ) ? {
128+ return Ok ( ( ) ) ;
129+ }
130+
131+ for folder in self . all_cache_folders_by_age ( ) ? {
132+ warn ! (
133+ ?folder,
134+ "freeing up disk space by deleting oldest cache folder"
135+ ) ;
136+ fs:: remove_dir_all ( & folder) ?;
137+
138+ if space_ok ( ) ? {
139+ break ;
140+ }
141+ }
142+
143+ Ok ( ( ) )
144+ }
145+
53146 /// restore a cached target directory.
54147 ///
55148 /// Will just move the cache folder into the rustwide
@@ -67,7 +160,7 @@ impl ArtifactCache {
67160 fs:: remove_dir_all ( target_dir) . context ( "could not clean target directory" ) ?;
68161 }
69162
70- let cache_dir = self . cache_dir . join ( cache_key) ;
163+ let cache_dir = self . cache_dir_for_key ( cache_key) ;
71164 if !cache_dir. exists ( ) {
72165 // when there is no existing cache dir,
73166 // we can just create an empty target.
@@ -85,14 +178,15 @@ impl ArtifactCache {
85178 cache_key : & str ,
86179 target_dir : P ,
87180 ) -> Result < ( ) > {
88- let cache_dir = self . cache_dir . join ( cache_key) ;
181+ let cache_dir = self . cache_dir_for_key ( cache_key) ;
89182 if cache_dir. exists ( ) {
90183 fs:: remove_dir_all ( & cache_dir) ?;
91184 }
92185
93186 debug ! ( ?target_dir, ?cache_dir, "saving artifact cache" ) ;
94187 fs:: rename ( & target_dir, & cache_dir) . context ( "could not move target directory to cache" ) ?;
95188 self . cleanup ( & cache_dir) ?;
189+ self . touch ( cache_key) ?;
96190 Ok ( ( ) )
97191 }
98192}
0 commit comments