1- use anyhow:: Result ;
1+ use anyhow:: { Context , Result } ;
22use serde:: Serialize ;
33use std:: path:: PathBuf ;
44
@@ -100,6 +100,76 @@ pub(super) async fn maybe_write_fixture_artifact(
100100 Ok ( Some ( artifact_path. display ( ) . to_string ( ) ) )
101101}
102102
103+ pub ( crate ) async fn prune_eval_artifacts (
104+ artifact_dir : & std:: path:: Path ,
105+ max_age_days : i64 ,
106+ ) -> Result < usize > {
107+ let artifact_dir = artifact_dir. to_path_buf ( ) ;
108+ tokio:: task:: spawn_blocking ( move || prune_eval_artifacts_blocking ( & artifact_dir, max_age_days) )
109+ . await
110+ . context ( "eval artifact retention task failed" ) ?
111+ }
112+
113+ fn prune_eval_artifacts_blocking (
114+ artifact_dir : & std:: path:: Path ,
115+ max_age_days : i64 ,
116+ ) -> Result < usize > {
117+ if !artifact_dir. exists ( ) {
118+ return Ok ( 0 ) ;
119+ }
120+
121+ let cutoff = std:: time:: SystemTime :: now ( )
122+ . checked_sub ( std:: time:: Duration :: from_secs (
123+ max_age_days. max ( 1 ) as u64 * 86_400 ,
124+ ) )
125+ . unwrap_or ( std:: time:: SystemTime :: UNIX_EPOCH ) ;
126+ prune_eval_artifacts_before ( artifact_dir, cutoff)
127+ }
128+
129+ fn prune_eval_artifacts_before (
130+ artifact_dir : & std:: path:: Path ,
131+ cutoff : std:: time:: SystemTime ,
132+ ) -> Result < usize > {
133+ let mut removed = 0 ;
134+ prune_eval_artifacts_tree ( artifact_dir, cutoff, true , & mut removed) ?;
135+ Ok ( removed)
136+ }
137+
138+ fn prune_eval_artifacts_tree (
139+ path : & std:: path:: Path ,
140+ cutoff : std:: time:: SystemTime ,
141+ preserve_root : bool ,
142+ removed : & mut usize ,
143+ ) -> Result < ( ) > {
144+ for entry in std:: fs:: read_dir ( path) ? {
145+ let entry = entry?;
146+ let entry_path = entry. path ( ) ;
147+ let metadata = std:: fs:: symlink_metadata ( & entry_path) ?;
148+
149+ if metadata. is_dir ( ) {
150+ prune_eval_artifacts_tree ( & entry_path, cutoff, false , removed) ?;
151+ if std:: fs:: read_dir ( & entry_path) ?. next ( ) . is_none ( ) {
152+ std:: fs:: remove_dir ( & entry_path) ?;
153+ * removed += 1 ;
154+ }
155+ } else if metadata. is_file ( )
156+ && metadata
157+ . modified ( )
158+ . map ( |modified| modified < cutoff)
159+ . unwrap_or ( false )
160+ {
161+ std:: fs:: remove_file ( & entry_path) ?;
162+ * removed += 1 ;
163+ }
164+ }
165+
166+ if !preserve_root && std:: fs:: read_dir ( path) ?. next ( ) . is_none ( ) {
167+ return Ok ( ( ) ) ;
168+ }
169+
170+ Ok ( ( ) )
171+ }
172+
103173fn sanitize_path_segment ( value : & str ) -> String {
104174 let mut sanitized = value
105175 . trim ( )
@@ -126,3 +196,44 @@ fn sanitize_path_segment(value: &str) -> String {
126196 sanitized
127197 }
128198}
199+
200+ #[ cfg( test) ]
201+ mod tests {
202+ use super :: * ;
203+ use tempfile:: tempdir;
204+
205+ #[ test]
206+ fn prune_eval_artifacts_removes_stale_files_and_empty_dirs ( ) {
207+ let dir = tempdir ( ) . unwrap ( ) ;
208+ let nested = dir. path ( ) . join ( "fixtures" ) ;
209+ std:: fs:: create_dir_all ( & nested) . unwrap ( ) ;
210+ let artifact = nested. join ( "old.json" ) ;
211+ std:: fs:: write ( & artifact, "{}" ) . unwrap ( ) ;
212+
213+ let removed = prune_eval_artifacts_before (
214+ dir. path ( ) ,
215+ std:: time:: SystemTime :: now ( ) + std:: time:: Duration :: from_secs ( 1 ) ,
216+ )
217+ . unwrap ( ) ;
218+
219+ assert_eq ! ( removed, 2 ) ;
220+ assert ! ( !artifact. exists( ) ) ;
221+ assert ! ( !nested. exists( ) ) ;
222+ }
223+
224+ #[ test]
225+ fn prune_eval_artifacts_keeps_recent_files ( ) {
226+ let dir = tempdir ( ) . unwrap ( ) ;
227+ let nested = dir. path ( ) . join ( "fixtures" ) ;
228+ std:: fs:: create_dir_all ( & nested) . unwrap ( ) ;
229+ let artifact = nested. join ( "recent.json" ) ;
230+ std:: fs:: write ( & artifact, "{}" ) . unwrap ( ) ;
231+
232+ let removed =
233+ prune_eval_artifacts_before ( dir. path ( ) , std:: time:: SystemTime :: UNIX_EPOCH ) . unwrap ( ) ;
234+
235+ assert_eq ! ( removed, 0 ) ;
236+ assert ! ( artifact. exists( ) ) ;
237+ assert ! ( nested. exists( ) ) ;
238+ }
239+ }
0 commit comments