files_sdk/automation/
automations.rs

1//! Automation operations
2//!
3//! Automations enable scheduled or event-driven file operations without manual intervention.
4//! Create workflows that automatically process files based on schedules, file events, or webhooks.
5//!
6//! # Features
7//!
8//! - Schedule automated file operations (copy, move, delete)
9//! - Trigger actions on file events (upload, download, modify)
10//! - Configure recurring tasks (daily, weekly, monthly)
11//! - Set up webhook-triggered automations
12//! - Manage syncs with remote servers
13//! - Import files from external URLs
14//!
15//! # Automation Types
16//!
17//! - `create_folder` - Create directories automatically
18//! - `delete_file` - Delete files matching patterns
19//! - `copy_file` - Copy files to destinations
20//! - `move_file` - Move files between locations
21//! - `run_sync` - Execute sync operations
22//! - `import_file` - Import from external URLs
23//!
24//! # Example
25//!
26//! ```no_run
27//! use files_sdk::{FilesClient, AutomationHandler};
28//!
29//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! let client = FilesClient::builder()
31//!     .api_key("your-api-key")
32//!     .build()?;
33//!
34//! let handler = AutomationHandler::new(client);
35//!
36//! // Create automation to copy uploaded files to archive daily
37//! let automation = handler.create(
38//!     "copy_file",
39//!     Some("/uploads/*.pdf"),
40//!     Some("/archive/"),
41//!     None,
42//!     Some("day"),
43//!     Some("/uploads"),
44//!     Some("daily")
45//! ).await?;
46//!
47//! println!("Created automation ID: {}", automation.id.unwrap());
48//!
49//! // Manually trigger the automation
50//! handler.manual_run(automation.id.unwrap()).await?;
51//! # Ok(())
52//! # }
53//! ```
54
55use crate::{FilesClient, PaginationInfo, Result};
56use serde::{Deserialize, Serialize};
57use serde_json::json;
58
59/// Automation type enum
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum AutomationType {
63    CreateFolder,
64    DeleteFile,
65    CopyFile,
66    MoveFile,
67    As2Send,
68    RunSync,
69    ImportFile,
70}
71
72/// Automation trigger type
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum AutomationTrigger {
76    Daily,
77    Custom,
78    Webhook,
79    Email,
80    Action,
81    Interval,
82}
83
84/// An Automation entity
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AutomationEntity {
87    /// Automation ID
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub id: Option<i64>,
90
91    /// Force automation runs to be serialized
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub always_serialize_jobs: Option<bool>,
94
95    /// Always overwrite files with matching size
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub always_overwrite_size_matching_files: Option<bool>,
98
99    /// Automation type
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub automation: Option<String>,
102
103    /// Indicates if the automation has been deleted
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub deleted: Option<bool>,
106
107    /// Description for this Automation
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub description: Option<String>,
110
111    /// String to replace in destination path
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub destination_replace_from: Option<String>,
114
115    /// Replacement string for destination path
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub destination_replace_to: Option<String>,
118
119    /// Destination paths
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub destinations: Option<Vec<String>>,
122
123    /// If true, this automation will not run
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub disabled: Option<bool>,
126
127    /// Glob pattern to exclude files
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub exclude_pattern: Option<String>,
130
131    /// Flatten destination folder structure
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub flatten_destination_structure: Option<bool>,
134
135    /// Group IDs associated with automation
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub group_ids: Option<Vec<i64>>,
138
139    /// Holiday region for scheduling
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub holiday_region: Option<String>,
142
143    /// Human readable schedule description
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub human_readable_schedule: Option<String>,
146
147    /// Ignore locked folders
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub ignore_locked_folders: Option<bool>,
150
151    /// URLs to import from
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub import_urls: Option<Vec<serde_json::Value>>,
154
155    /// Automation interval (day, week, month, etc.)
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub interval: Option<String>,
158
159    /// Last modification time
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub last_modified_at: Option<String>,
162
163    /// Use legacy folder matching
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub legacy_folder_matching: Option<bool>,
166
167    /// Legacy sync IDs
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub legacy_sync_ids: Option<Vec<i64>>,
170
171    /// Automation name
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub name: Option<String>,
174
175    /// Overwrite existing files
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub overwrite_files: Option<bool>,
178
179    /// Path on which this Automation runs
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub path: Option<String>,
182
183    /// Path timezone
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub path_time_zone: Option<String>,
186
187    /// Recurring day of interval
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub recurring_day: Option<i64>,
190
191    /// Retry interval on failure (minutes)
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub retry_on_failure_interval_in_minutes: Option<i64>,
194
195    /// Number of retry attempts on failure
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub retry_on_failure_number_of_attempts: Option<i64>,
198
199    /// Custom schedule configuration
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub schedule: Option<serde_json::Value>,
202
203    /// Days of week for schedule
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub schedule_days_of_week: Option<Vec<i64>>,
206
207    /// Schedule timezone
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub schedule_time_zone: Option<String>,
210
211    /// Times of day for schedule
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub schedule_times_of_day: Option<Vec<String>>,
214
215    /// Source path/glob
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub source: Option<String>,
218
219    /// Sync IDs
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub sync_ids: Option<Vec<i64>>,
222
223    /// Trigger type
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub trigger: Option<String>,
226
227    /// Actions that trigger this automation
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub trigger_actions: Option<Vec<String>>,
230
231    /// User ID that owns this automation
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub user_id: Option<i64>,
234
235    /// User IDs associated with automation
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub user_ids: Option<Vec<i64>>,
238
239    /// Automation value/configuration
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub value: Option<serde_json::Value>,
242
243    /// Webhook URL
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub webhook_url: Option<String>,
246}
247
248/// Handler for automation operations
249pub struct AutomationHandler {
250    client: FilesClient,
251}
252
253impl AutomationHandler {
254    /// Create a new automation handler
255    pub fn new(client: FilesClient) -> Self {
256        Self { client }
257    }
258
259    /// List all automations
260    ///
261    /// Returns a paginated list of automation workflows with optional filtering
262    /// by automation type.
263    ///
264    /// # Arguments
265    ///
266    /// * `cursor` - Pagination cursor from previous response
267    /// * `per_page` - Number of results per page (max 10,000)
268    /// * `automation` - Filter by automation type (e.g., "copy_file", "move_file")
269    ///
270    /// # Returns
271    ///
272    /// A tuple containing:
273    /// - Vector of `AutomationEntity` objects
274    /// - `PaginationInfo` with cursors for next/previous pages
275    ///
276    /// # Example
277    ///
278    /// ```no_run
279    /// use files_sdk::{FilesClient, AutomationHandler};
280    ///
281    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
282    /// let client = FilesClient::builder().api_key("key").build()?;
283    /// let handler = AutomationHandler::new(client);
284    ///
285    /// // List all automations
286    /// let (automations, pagination) = handler.list(None, Some(50), None).await?;
287    ///
288    /// for automation in automations {
289    ///     println!("{}: {} - Disabled: {}",
290    ///         automation.name.unwrap_or_default(),
291    ///         automation.automation.unwrap_or_default(),
292    ///         automation.disabled.unwrap_or(false));
293    /// }
294    ///
295    /// // Filter by type
296    /// let (copy_automations, _) = handler.list(None, None, Some("copy_file")).await?;
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub async fn list(
301        &self,
302        cursor: Option<&str>,
303        per_page: Option<i64>,
304        automation: Option<&str>,
305    ) -> Result<(Vec<AutomationEntity>, PaginationInfo)> {
306        let mut params = vec![];
307        if let Some(c) = cursor {
308            params.push(("cursor", c.to_string()));
309        }
310        if let Some(pp) = per_page {
311            params.push(("per_page", pp.to_string()));
312        }
313        if let Some(a) = automation {
314            params.push(("automation", a.to_string()));
315        }
316
317        let query = if params.is_empty() {
318            String::new()
319        } else {
320            format!(
321                "?{}",
322                params
323                    .iter()
324                    .map(|(k, v)| format!("{}={}", k, v))
325                    .collect::<Vec<_>>()
326                    .join("&")
327            )
328        };
329
330        let response = self
331            .client
332            .get_raw(&format!("/automations{}", query))
333            .await?;
334        let automations: Vec<AutomationEntity> = serde_json::from_value(response)?;
335
336        let pagination = PaginationInfo {
337            cursor_next: None,
338            cursor_prev: None,
339        };
340
341        Ok((automations, pagination))
342    }
343
344    /// Get details of a specific automation
345    ///
346    /// # Arguments
347    ///
348    /// * `id` - Automation ID
349    ///
350    /// # Returns
351    ///
352    /// An `AutomationEntity` with complete automation configuration
353    ///
354    /// # Example
355    ///
356    /// ```no_run
357    /// use files_sdk::{FilesClient, AutomationHandler};
358    ///
359    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
360    /// let client = FilesClient::builder().api_key("key").build()?;
361    /// let handler = AutomationHandler::new(client);
362    ///
363    /// let automation = handler.get(12345).await?;
364    /// println!("Automation: {}", automation.name.unwrap_or_default());
365    /// println!("Schedule: {}", automation.human_readable_schedule.unwrap_or_default());
366    /// # Ok(())
367    /// # }
368    /// ```
369    pub async fn get(&self, id: i64) -> Result<AutomationEntity> {
370        let response = self.client.get_raw(&format!("/automations/{}", id)).await?;
371        Ok(serde_json::from_value(response)?)
372    }
373
374    /// Create a new automation workflow
375    ///
376    /// Creates an automation that performs file operations automatically based on
377    /// schedules or triggers.
378    ///
379    /// # Arguments
380    ///
381    /// * `automation` - Type of automation: "copy_file", "move_file", "delete_file",
382    ///   "create_folder", "run_sync", "import_file" (required)
383    /// * `source` - Source path or glob pattern (e.g., "/uploads/*.pdf")
384    /// * `destination` - Single destination path
385    /// * `destinations` - Multiple destination paths (use instead of destination)
386    /// * `interval` - Schedule interval: "day", "week", "month", "year"
387    /// * `path` - Base path where automation operates
388    /// * `trigger` - Trigger type: "daily", "custom", "webhook", "email", "action", "interval"
389    ///
390    /// # Returns
391    ///
392    /// The newly created `AutomationEntity`
393    ///
394    /// # Example
395    ///
396    /// ```no_run
397    /// use files_sdk::{FilesClient, AutomationHandler};
398    ///
399    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
400    /// let client = FilesClient::builder().api_key("key").build()?;
401    /// let handler = AutomationHandler::new(client);
402    ///
403    /// // Create daily automation to archive PDFs
404    /// let automation = handler.create(
405    ///     "copy_file",
406    ///     Some("/uploads/*.pdf"),
407    ///     Some("/archive/daily/"),
408    ///     None,
409    ///     Some("day"),
410    ///     Some("/uploads"),
411    ///     Some("daily")
412    /// ).await?;
413    ///
414    /// println!("Created automation: {}", automation.id.unwrap());
415    /// # Ok(())
416    /// # }
417    /// ```
418    #[allow(clippy::too_many_arguments)]
419    pub async fn create(
420        &self,
421        automation: &str,
422        source: Option<&str>,
423        destination: Option<&str>,
424        destinations: Option<Vec<String>>,
425        interval: Option<&str>,
426        path: Option<&str>,
427        trigger: Option<&str>,
428    ) -> Result<AutomationEntity> {
429        let mut request_body = json!({
430            "automation": automation,
431        });
432
433        if let Some(s) = source {
434            request_body["source"] = json!(s);
435        }
436        if let Some(d) = destination {
437            request_body["destination"] = json!(d);
438        }
439        if let Some(dests) = destinations {
440            request_body["destinations"] = json!(dests);
441        }
442        if let Some(i) = interval {
443            request_body["interval"] = json!(i);
444        }
445        if let Some(p) = path {
446            request_body["path"] = json!(p);
447        }
448        if let Some(t) = trigger {
449            request_body["trigger"] = json!(t);
450        }
451
452        let response = self.client.post_raw("/automations", request_body).await?;
453        Ok(serde_json::from_value(response)?)
454    }
455
456    /// Update an existing automation
457    ///
458    /// Modifies automation configuration. Only provided fields are updated;
459    /// omitted fields remain unchanged.
460    ///
461    /// # Arguments
462    ///
463    /// * `id` - Automation ID to update
464    /// * `source` - New source path or glob pattern
465    /// * `destination` - New destination path
466    /// * `interval` - New schedule interval
467    /// * `disabled` - Enable (false) or disable (true) the automation
468    ///
469    /// # Returns
470    ///
471    /// The updated `AutomationEntity`
472    ///
473    /// # Example
474    ///
475    /// ```no_run
476    /// use files_sdk::{FilesClient, AutomationHandler};
477    ///
478    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
479    /// let client = FilesClient::builder().api_key("key").build()?;
480    /// let handler = AutomationHandler::new(client);
481    ///
482    /// // Disable an automation temporarily
483    /// let automation = handler.update(
484    ///     12345,
485    ///     None,
486    ///     None,
487    ///     None,
488    ///     Some(true)
489    /// ).await?;
490    ///
491    /// println!("Automation disabled");
492    /// # Ok(())
493    /// # }
494    /// ```
495    #[allow(clippy::too_many_arguments)]
496    pub async fn update(
497        &self,
498        id: i64,
499        source: Option<&str>,
500        destination: Option<&str>,
501        interval: Option<&str>,
502        disabled: Option<bool>,
503    ) -> Result<AutomationEntity> {
504        let mut request_body = json!({});
505
506        if let Some(s) = source {
507            request_body["source"] = json!(s);
508        }
509        if let Some(d) = destination {
510            request_body["destination"] = json!(d);
511        }
512        if let Some(i) = interval {
513            request_body["interval"] = json!(i);
514        }
515        if let Some(dis) = disabled {
516            request_body["disabled"] = json!(dis);
517        }
518
519        let response = self
520            .client
521            .patch_raw(&format!("/automations/{}", id), request_body)
522            .await?;
523        Ok(serde_json::from_value(response)?)
524    }
525
526    /// Delete an automation permanently
527    ///
528    /// Removes the automation and stops all scheduled or triggered executions.
529    /// This operation cannot be undone.
530    ///
531    /// # Arguments
532    ///
533    /// * `id` - Automation ID to delete
534    ///
535    /// # Example
536    ///
537    /// ```no_run
538    /// use files_sdk::{FilesClient, AutomationHandler};
539    ///
540    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
541    /// let client = FilesClient::builder().api_key("key").build()?;
542    /// let handler = AutomationHandler::new(client);
543    ///
544    /// handler.delete(12345).await?;
545    /// println!("Automation deleted");
546    /// # Ok(())
547    /// # }
548    /// ```
549    pub async fn delete(&self, id: i64) -> Result<()> {
550        self.client
551            .delete_raw(&format!("/automations/{}", id))
552            .await?;
553        Ok(())
554    }
555
556    /// Manually trigger an automation execution
557    ///
558    /// Immediately executes the automation regardless of its schedule or trigger settings.
559    /// Useful for testing or running an automation on-demand.
560    ///
561    /// # Arguments
562    ///
563    /// * `id` - Automation ID to execute
564    ///
565    /// # Returns
566    ///
567    /// JSON response with execution details and status
568    ///
569    /// # Example
570    ///
571    /// ```no_run
572    /// use files_sdk::{FilesClient, AutomationHandler};
573    ///
574    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
575    /// let client = FilesClient::builder().api_key("key").build()?;
576    /// let handler = AutomationHandler::new(client);
577    ///
578    /// // Manually trigger the automation to run now
579    /// let result = handler.manual_run(12345).await?;
580    /// println!("Automation executed: {:?}", result);
581    /// # Ok(())
582    /// # }
583    /// ```
584    pub async fn manual_run(&self, id: i64) -> Result<serde_json::Value> {
585        let response = self
586            .client
587            .post_raw(&format!("/automations/{}/manual_run", id), json!({}))
588            .await?;
589        Ok(response)
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[test]
598    fn test_handler_creation() {
599        let client = FilesClient::builder().api_key("test-key").build().unwrap();
600        let _handler = AutomationHandler::new(client);
601    }
602}