Skip to main content

s3rm_rs/config/
mod.rs

1pub mod args;
2
3use crate::callback::event_manager::EventManager;
4use crate::callback::filter_manager::FilterManager;
5use crate::types::{ClientConfigLocation, S3Credentials, StoragePath};
6use aws_sdk_s3::types::RequestPayer;
7use aws_smithy_types::checksum_config::RequestChecksumCalculation;
8use chrono::{DateTime, Utc};
9use fancy_regex::Regex;
10
11/// Main configuration for the s3rm-rs deletion pipeline.
12///
13/// Holds all settings needed to configure and run a [`DeletionPipeline`](crate::DeletionPipeline):
14/// target bucket/prefix, AWS credentials, worker pool size, filter rules,
15/// safety flags (dry-run, force, max-delete), and callback registrations.
16///
17/// Adapted from s3sync's Config, removing source-specific and sync-specific options.
18/// Only target-related configuration is retained since s3rm-rs operates on a single S3 target.
19///
20/// # Quick Start
21///
22/// The recommended way to build a `Config` is [`build_config_from_args`](crate::build_config_from_args),
23/// which performs all CLI-level validation (conflicting flags, rate-limit vs batch-size, etc.):
24///
25/// ```no_run
26/// let config = s3rm_rs::build_config_from_args([
27///     "s3rm", "s3://my-bucket/logs/2024/", "--dry-run", "--force",
28/// ]).expect("invalid arguments");
29/// ```
30///
31/// Alternatively, [`Config::for_target`] creates a minimal configuration with sensible
32/// defaults, but **command-line validation checks are not performed**:
33///
34/// ```
35/// use s3rm_rs::Config;
36///
37/// let config = Config::for_target("my-bucket", "logs/2024/");
38/// assert_eq!(config.worker_size, 16);
39/// assert_eq!(config.batch_size, 200);
40/// ```
41///
42/// Then customize fields as needed:
43///
44/// ```
45/// use s3rm_rs::Config;
46///
47/// let mut config = Config::for_target("my-bucket", "logs/2024/");
48/// config.dry_run = true;
49/// config.force = true;
50/// config.worker_size = 100;
51/// config.max_delete = Some(10_000);
52/// ```
53///
54/// # Default
55///
56/// [`Config::default()`] creates a configuration targeting an empty bucket/prefix.
57/// You must set the `target` field before running a pipeline.
58///
59/// ```
60/// use s3rm_rs::Config;
61/// use s3rm_rs::types::StoragePath;
62///
63/// let mut config = Config::default();
64/// config.target = StoragePath::S3 {
65///     bucket: "my-bucket".into(),
66///     prefix: "prefix/".into(),
67/// };
68/// ```
69#[derive(Debug, Clone)]
70pub struct Config {
71    pub target: StoragePath,
72    pub show_no_progress: bool,
73    pub target_client_config: Option<ClientConfig>,
74    pub force_retry_config: ForceRetryConfig,
75    pub tracing_config: Option<TracingConfig>,
76    pub worker_size: u16,
77    pub warn_as_error: bool,
78    pub dry_run: bool,
79    pub rate_limit_objects: Option<u32>,
80    pub max_parallel_listings: u16,
81    pub object_listing_queue_size: u32,
82    pub max_parallel_listing_max_depth: u16,
83    pub allow_parallel_listings_in_express_one_zone: bool,
84    pub filter_config: FilterConfig,
85    pub max_keys: i32,
86    pub auto_complete_shell: Option<clap_complete::shells::Shell>,
87    pub event_callback_lua_script: Option<String>,
88    pub filter_callback_lua_script: Option<String>,
89    pub allow_lua_os_library: bool,
90    pub allow_lua_unsafe_vm: bool,
91    pub lua_vm_memory_limit: usize,
92    pub lua_callback_timeout_milliseconds: u64,
93    pub if_match: bool,
94    pub max_delete: Option<u64>,
95    // Callback managers
96    pub filter_manager: FilterManager,
97    pub event_manager: EventManager,
98    // Deletion-specific options
99    pub batch_size: u16,
100    pub delete_all_versions: bool,
101    pub force: bool,
102    // Testing flag: enables user-defined callbacks (for library testing)
103    pub test_user_defined_callback: bool,
104}
105
106impl Config {
107    /// Create a `Config` with sensible defaults for the given S3 bucket and prefix.
108    ///
109    /// All fields are set to production-ready defaults matching the CLI defaults
110    /// (16 workers, batch size 200, etc.). The `force` flag is set to `true` to
111    /// skip interactive confirmation prompts, which is appropriate for programmatic use.
112    ///
113    /// **Note:** This bypasses CLI-level validation (conflicting flags, rate-limit
114    /// vs batch-size, etc.). Prefer [`build_config_from_args`](crate::build_config_from_args)
115    /// when possible.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use s3rm_rs::Config;
121    ///
122    /// let config = Config::for_target("my-bucket", "logs/");
123    /// assert_eq!(config.batch_size, 200);
124    /// assert!(config.force); // no interactive prompts
125    /// ```
126    pub fn for_target(bucket: &str, prefix: &str) -> Self {
127        Config {
128            target: StoragePath::S3 {
129                bucket: bucket.to_string(),
130                prefix: prefix.to_string(),
131            },
132            force: true,
133            ..Config::default()
134        }
135    }
136}
137
138impl Default for Config {
139    /// Create a `Config` with sensible defaults.
140    ///
141    /// The `target` defaults to an empty bucket/prefix — set it before running
142    /// a pipeline. All other fields use production defaults matching the CLI.
143    fn default() -> Self {
144        Config {
145            target: StoragePath::S3 {
146                bucket: String::new(),
147                prefix: String::new(),
148            },
149            show_no_progress: false,
150            target_client_config: None,
151            force_retry_config: ForceRetryConfig::default(),
152            tracing_config: None,
153            worker_size: 16,
154            warn_as_error: false,
155            dry_run: false,
156            rate_limit_objects: None,
157            max_parallel_listings: 16,
158            object_listing_queue_size: 200_000,
159            max_parallel_listing_max_depth: 2,
160            allow_parallel_listings_in_express_one_zone: false,
161            filter_config: FilterConfig::default(),
162            max_keys: 1000,
163            auto_complete_shell: None,
164            event_callback_lua_script: None,
165            filter_callback_lua_script: None,
166            allow_lua_os_library: false,
167            allow_lua_unsafe_vm: false,
168            lua_vm_memory_limit: 64 * 1024 * 1024,
169            lua_callback_timeout_milliseconds: 10_000,
170            if_match: false,
171            max_delete: None,
172            filter_manager: FilterManager::new(),
173            event_manager: EventManager::new(),
174            batch_size: 200,
175            delete_all_versions: false,
176            force: false,
177            test_user_defined_callback: false,
178        }
179    }
180}
181
182impl Default for ForceRetryConfig {
183    fn default() -> Self {
184        ForceRetryConfig {
185            force_retry_count: 0,
186            force_retry_interval_milliseconds: 1000,
187        }
188    }
189}
190
191/// AWS S3 client configuration.
192///
193/// Reused from s3sync's ClientConfig with credential loading,
194/// region configuration, endpoint setup, retry config, and timeout config.
195#[derive(Debug, Clone)]
196pub struct ClientConfig {
197    pub client_config_location: ClientConfigLocation,
198    pub credential: S3Credentials,
199    pub region: Option<String>,
200    pub endpoint_url: Option<String>,
201    pub force_path_style: bool,
202    pub accelerate: bool,
203    pub request_payer: Option<RequestPayer>,
204    pub retry_config: RetryConfig,
205    pub cli_timeout_config: CLITimeoutConfig,
206    pub disable_stalled_stream_protection: bool,
207    pub request_checksum_calculation: RequestChecksumCalculation,
208}
209
210/// Retry configuration for AWS SDK operations.
211///
212/// Reused from s3sync's retry configuration with exponential backoff.
213#[derive(Debug, Clone)]
214pub struct RetryConfig {
215    pub aws_max_attempts: u32,
216    pub initial_backoff_milliseconds: u64,
217}
218
219/// Timeout configuration for AWS SDK operations.
220///
221/// Reused from s3sync's CLI timeout configuration.
222#[derive(Debug, Clone)]
223pub struct CLITimeoutConfig {
224    pub operation_timeout_milliseconds: Option<u64>,
225    pub operation_attempt_timeout_milliseconds: Option<u64>,
226    pub connect_timeout_milliseconds: Option<u64>,
227    pub read_timeout_milliseconds: Option<u64>,
228}
229
230/// Tracing (logging) configuration.
231///
232/// Reused from s3sync's tracing configuration supporting verbosity levels,
233/// JSON format, color control, and AWS SDK tracing.
234#[derive(Debug, Clone, Copy)]
235pub struct TracingConfig {
236    pub tracing_level: log::Level,
237    pub json_tracing: bool,
238    pub aws_sdk_tracing: bool,
239    pub span_events_tracing: bool,
240    pub disable_color_tracing: bool,
241}
242
243/// Force retry configuration for application-level retries
244/// (in addition to AWS SDK retries).
245///
246/// Reused from s3sync's force retry configuration.
247#[derive(Debug, Clone, Copy)]
248pub struct ForceRetryConfig {
249    pub force_retry_count: u32,
250    pub force_retry_interval_milliseconds: u64,
251}
252
253/// Filter configuration for object selection.
254///
255/// Adapted from s3sync's FilterConfig, removing sync-specific filters
256/// (check_size, check_etag, etc.) and retaining deletion-relevant filters.
257#[derive(Debug, Clone, Default)]
258pub struct FilterConfig {
259    pub before_time: Option<DateTime<Utc>>,
260    pub after_time: Option<DateTime<Utc>>,
261    pub include_regex: Option<Regex>,
262    pub exclude_regex: Option<Regex>,
263    pub include_content_type_regex: Option<Regex>,
264    pub exclude_content_type_regex: Option<Regex>,
265    pub include_metadata_regex: Option<Regex>,
266    pub exclude_metadata_regex: Option<Regex>,
267    pub include_tag_regex: Option<Regex>,
268    pub exclude_tag_regex: Option<Regex>,
269    pub larger_size: Option<u64>,
270    pub smaller_size: Option<u64>,
271    pub keep_latest_only: bool,
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::test_utils::init_dummy_tracing_subscriber;
278
279    #[test]
280    fn retry_config_creation() {
281        init_dummy_tracing_subscriber();
282
283        let retry_config = RetryConfig {
284            aws_max_attempts: 3,
285            initial_backoff_milliseconds: 100,
286        };
287        assert_eq!(retry_config.aws_max_attempts, 3);
288        assert_eq!(retry_config.initial_backoff_milliseconds, 100);
289    }
290
291    #[test]
292    fn cli_timeout_config_creation() {
293        init_dummy_tracing_subscriber();
294
295        let timeout_config = CLITimeoutConfig {
296            operation_timeout_milliseconds: Some(30000),
297            operation_attempt_timeout_milliseconds: Some(10000),
298            connect_timeout_milliseconds: Some(5000),
299            read_timeout_milliseconds: Some(5000),
300        };
301        assert_eq!(timeout_config.operation_timeout_milliseconds, Some(30000));
302        assert_eq!(
303            timeout_config.operation_attempt_timeout_milliseconds,
304            Some(10000)
305        );
306    }
307
308    #[test]
309    fn cli_timeout_config_no_timeouts() {
310        init_dummy_tracing_subscriber();
311
312        let timeout_config = CLITimeoutConfig {
313            operation_timeout_milliseconds: None,
314            operation_attempt_timeout_milliseconds: None,
315            connect_timeout_milliseconds: None,
316            read_timeout_milliseconds: None,
317        };
318        assert!(timeout_config.operation_timeout_milliseconds.is_none());
319    }
320
321    #[test]
322    fn tracing_config_creation() {
323        init_dummy_tracing_subscriber();
324
325        let tracing_config = TracingConfig {
326            tracing_level: log::Level::Info,
327            json_tracing: false,
328            aws_sdk_tracing: false,
329            span_events_tracing: false,
330            disable_color_tracing: false,
331        };
332        assert_eq!(tracing_config.tracing_level, log::Level::Info);
333        assert!(!tracing_config.json_tracing);
334    }
335
336    #[test]
337    fn force_retry_config_creation() {
338        init_dummy_tracing_subscriber();
339
340        let force_retry = ForceRetryConfig {
341            force_retry_count: 3,
342            force_retry_interval_milliseconds: 1000,
343        };
344        assert_eq!(force_retry.force_retry_count, 3);
345        assert_eq!(force_retry.force_retry_interval_milliseconds, 1000);
346    }
347
348    #[test]
349    fn filter_config_default() {
350        init_dummy_tracing_subscriber();
351
352        let filter_config = FilterConfig::default();
353        assert!(filter_config.before_time.is_none());
354        assert!(filter_config.after_time.is_none());
355        assert!(filter_config.include_regex.is_none());
356        assert!(filter_config.exclude_regex.is_none());
357        assert!(filter_config.larger_size.is_none());
358        assert!(filter_config.smaller_size.is_none());
359    }
360
361    // ------------------------------------------------------------------
362    // Config::for_target and Config::default tests
363    // (Covers uncovered constructors — lines 113-166)
364    // ------------------------------------------------------------------
365
366    #[test]
367    fn config_for_target_sets_bucket_and_prefix() {
368        init_dummy_tracing_subscriber();
369
370        let config = Config::for_target("my-bucket", "logs/2024/");
371        let StoragePath::S3 { bucket, prefix } = &config.target;
372        assert_eq!(bucket, "my-bucket");
373        assert_eq!(prefix, "logs/2024/");
374    }
375
376    #[test]
377    fn config_for_target_sets_force_true() {
378        // Library usage should skip interactive prompts by default.
379        let config = Config::for_target("bucket", "prefix/");
380        assert!(config.force);
381    }
382
383    #[test]
384    fn config_for_target_uses_default_worker_and_batch_size() {
385        let config = Config::for_target("bucket", "");
386        assert_eq!(config.worker_size, 16);
387        assert_eq!(config.batch_size, 200);
388    }
389
390    #[test]
391    fn config_for_target_has_sensible_defaults() {
392        let config = Config::for_target("bucket", "prefix/");
393        assert!(!config.dry_run);
394        assert!(!config.delete_all_versions);
395        assert!(!config.if_match);
396        assert!(config.max_delete.is_none());
397        assert!(config.tracing_config.is_none());
398        assert!(config.target_client_config.is_none());
399        assert!(config.rate_limit_objects.is_none());
400        assert!(!config.warn_as_error);
401        assert!(!config.test_user_defined_callback);
402    }
403
404    #[test]
405    fn config_default_has_empty_target() {
406        let config = Config::default();
407        let StoragePath::S3 { bucket, prefix } = &config.target;
408        assert!(bucket.is_empty());
409        assert!(prefix.is_empty());
410    }
411
412    #[test]
413    fn config_default_does_not_set_force() {
414        // Default config (not library convenience) should NOT skip prompts.
415        let config = Config::default();
416        assert!(!config.force);
417    }
418
419    #[test]
420    fn config_default_field_values() {
421        let config = Config::default();
422        assert_eq!(config.worker_size, 16);
423        assert_eq!(config.batch_size, 200);
424        assert!(!config.show_no_progress);
425        assert!(!config.dry_run);
426        assert!(!config.warn_as_error);
427        assert_eq!(config.max_parallel_listings, 16);
428        assert_eq!(config.object_listing_queue_size, 200_000);
429        assert_eq!(config.max_parallel_listing_max_depth, 2);
430        assert!(!config.allow_parallel_listings_in_express_one_zone);
431        assert_eq!(config.max_keys, 1000);
432        assert!(config.auto_complete_shell.is_none());
433        assert!(config.event_callback_lua_script.is_none());
434        assert!(config.filter_callback_lua_script.is_none());
435        assert!(!config.allow_lua_os_library);
436        assert!(!config.allow_lua_unsafe_vm);
437        assert_eq!(config.lua_vm_memory_limit, 64 * 1024 * 1024);
438        assert_eq!(config.lua_callback_timeout_milliseconds, 10_000);
439        assert!(!config.if_match);
440        assert!(!config.delete_all_versions);
441    }
442
443    #[test]
444    fn force_retry_config_default_values() {
445        let frc = ForceRetryConfig::default();
446        assert_eq!(frc.force_retry_count, 0);
447        assert_eq!(frc.force_retry_interval_milliseconds, 1000);
448    }
449}