1
- use anyhow:: { Context as _, Result } ;
1
+ use anyhow:: { bail , Context as _, Result } ;
2
2
use std:: {
3
- fs,
3
+ collections:: HashMap ,
4
+ fs:: { self , OpenOptions } ,
4
5
path:: { Path , PathBuf } ,
6
+ time:: SystemTime ,
5
7
} ;
8
+ use sysinfo:: { DiskExt , RefreshKind , System , SystemExt } ;
6
9
use tracing:: { debug, instrument, warn} ;
7
10
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
+
8
31
/// artifact caching with cleanup
9
32
#[ derive( Debug ) ]
10
33
pub ( crate ) struct ArtifactCache {
@@ -23,10 +46,8 @@ impl ArtifactCache {
23
46
24
47
/// clean up a target directory.
25
48
///
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
30
51
#[ instrument( skip( self ) ) ]
31
52
fn cleanup ( & self , target_dir : & Path ) -> Result < ( ) > {
32
53
// proc-macro crates have a `doc` directory
@@ -50,6 +71,78 @@ impl ArtifactCache {
50
71
Ok ( ( ) )
51
72
}
52
73
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
+
53
146
/// restore a cached target directory.
54
147
///
55
148
/// Will just move the cache folder into the rustwide
@@ -67,7 +160,7 @@ impl ArtifactCache {
67
160
fs:: remove_dir_all ( target_dir) . context ( "could not clean target directory" ) ?;
68
161
}
69
162
70
- let cache_dir = self . cache_dir . join ( cache_key) ;
163
+ let cache_dir = self . cache_dir_for_key ( cache_key) ;
71
164
if !cache_dir. exists ( ) {
72
165
// when there is no existing cache dir,
73
166
// we can just create an empty target.
@@ -85,14 +178,15 @@ impl ArtifactCache {
85
178
cache_key : & str ,
86
179
target_dir : P ,
87
180
) -> Result < ( ) > {
88
- let cache_dir = self . cache_dir . join ( cache_key) ;
181
+ let cache_dir = self . cache_dir_for_key ( cache_key) ;
89
182
if cache_dir. exists ( ) {
90
183
fs:: remove_dir_all ( & cache_dir) ?;
91
184
}
92
185
93
186
debug ! ( ?target_dir, ?cache_dir, "saving artifact cache" ) ;
94
187
fs:: rename ( & target_dir, & cache_dir) . context ( "could not move target directory to cache" ) ?;
95
188
self . cleanup ( & cache_dir) ?;
189
+ self . touch ( cache_key) ?;
96
190
Ok ( ( ) )
97
191
}
98
192
}
0 commit comments