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}