diff --git a/README.md b/README.md index f7bb7a0..cb7ffdb 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ services: | `WEBHOOK_URL` | (required) | URL to send webhook requests to | | `WEBHOOK_METHOD` | `POST` | HTTP method for webhook requests | | `INCLUDE_CONTENT` | `false` | Include full XML file content in payload | +| `OVERWRITE_WITH_RESPONSE` | `false` | Overwrite file with server response (requires `INCLUDE_CONTENT=true`) | | `RUST_LOG` | - | Set log level (trace, debug, info, warn, error) | ## Webhook Payload @@ -86,6 +87,21 @@ With `INCLUDE_CONTENT=true`: } ``` +## File Overwrite Feature + +When `OVERWRITE_WITH_RESPONSE=true` is set (along with `INCLUDE_CONTENT=true`), the watcher will overwrite the original XML file with the response from the webhook server. This feature has the following requirements: + +- The webhook must respond with a successful HTTP status code (2xx) +- The response `Content-Type` header must start with `text/xml` or `application/xml` +- The response body must not be empty + +When these conditions are met, the watcher will: +1. Overwrite the original file with the response content +2. Temporarily ignore watch events for that file to prevent triggering a new webhook +3. Log the overwrite operation + +This is useful for scenarios where the server processes the XML and returns a modified or transformed version. + ## Development ### Run locally (without Docker) diff --git a/src/main.rs b/src/main.rs index d509a64..695c0ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ use chrono::Utc; -use log::{error, info}; +use log::{error, info, warn}; use notify::{Event, RecursiveMode, Result as NotifyResult, Watcher}; use reqwest::Client; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::env; use std::path::{Path, PathBuf}; -use std::sync::mpsc::channel; +use std::sync::{mpsc::channel, Arc, Mutex}; use std::time::Duration; use tokio::time::sleep; +// Duration to keep files in the ignore list after overwriting them +// This prevents triggering new webhook events when we modify the file +const IGNORE_DURATION_SECS: u64 = 2; + #[derive(Debug, Serialize, Deserialize)] struct WebhookPayload { event: String, @@ -25,6 +30,7 @@ struct Config { webhook_url: String, webhook_method: String, include_content: bool, + overwrite_with_response: bool, } impl Config { @@ -43,11 +49,16 @@ impl Config { .unwrap_or_else(|_| "false".to_string()) .to_lowercase() == "true"; + let overwrite_with_response = env::var("OVERWRITE_WITH_RESPONSE") + .unwrap_or_else(|_| "false".to_string()) + .to_lowercase() == "true"; + Ok(Config { watch_dir, webhook_url, webhook_method, include_content, + overwrite_with_response, }) } } @@ -59,7 +70,7 @@ fn is_xml_file(path: &Path) -> bool { .unwrap_or(false) } -async fn trigger_webhook(config: &Config, filepath: PathBuf) { +async fn trigger_webhook(config: &Config, filepath: PathBuf, ignore_list: Arc>>) { let filename = filepath .file_name() .and_then(|f| f.to_str()) @@ -109,6 +120,64 @@ async fn trigger_webhook(config: &Config, filepath: PathBuf) { let status = response.status(); if status.is_success() { info!(" Webhook sent successfully (HTTP {})", status.as_u16()); + + // Handle overwriting the file with response if enabled + let should_overwrite_with_response = |config: &Config| { + config.overwrite_with_response && config.include_content + }; + + if should_overwrite_with_response(&config) { + let content_type = response.headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + // Check if content type is appropriate (text/xml or application/xml) + // Accept content types that start with these prefixes (may include charset parameter) + let is_xml = content_type.starts_with("text/xml") + || content_type.starts_with("application/xml"); + + if is_xml { + match response.text().await { + Ok(response_body) => { + if !response_body.is_empty() { + // Add file to ignore list before writing + { + let mut ignore = ignore_list.lock().unwrap(); + ignore.insert(filepath.clone()); + } + + match tokio::fs::write(&filepath, &response_body).await { + Ok(_) => { + info!(" File overwritten with response content"); + // Keep file in ignore list for a short time + let ignore_list_clone = Arc::clone(&ignore_list); + let filepath_clone = filepath.clone(); + tokio::spawn(async move { + sleep(Duration::from_secs(IGNORE_DURATION_SECS)).await; + let mut ignore = ignore_list_clone.lock().unwrap(); + ignore.remove(&filepath_clone); + }); + } + Err(e) => { + error!(" Failed to overwrite file: {}", e); + // Remove from ignore list on failure + let mut ignore = ignore_list.lock().unwrap(); + ignore.remove(&filepath); + } + } + } else { + warn!(" Response body is empty, not overwriting file"); + } + } + Err(e) => { + error!(" Failed to read response body: {}", e); + } + } + } else { + warn!(" Response content-type '{}' is not XML, not overwriting file", content_type); + } + } } else { let body = response.text().await.unwrap_or_default(); error!(" Webhook failed (HTTP {}): {}", status.as_u16(), body); @@ -137,11 +206,20 @@ async fn main() { std::process::exit(1); } + // Warn if overwrite is enabled without content inclusion + if config.overwrite_with_response && !config.include_content { + warn!("OVERWRITE_WITH_RESPONSE is enabled but INCLUDE_CONTENT is disabled. File overwrite will not work without including content in the webhook."); + } + info!("Starting XML file watcher..."); info!(" Watch directory: {}", config.watch_dir.display()); info!(" Webhook URL: {}", config.webhook_url); info!(" Webhook method: {}", config.webhook_method); info!(" Include content: {}", config.include_content); + info!(" Overwrite with response: {}", config.overwrite_with_response); + + // Create an ignore list for files we've just modified + let ignore_list: Arc>> = Arc::new(Mutex::new(HashSet::new())); let (tx, rx) = channel(); @@ -169,11 +247,23 @@ async fn main() { if matches!(event.kind, notify::EventKind::Create(_)) { for path in event.paths { if path.is_file() && is_xml_file(&path) { + // Check if this file is in the ignore list + let should_ignore = { + let ignore = ignore_list.lock().unwrap(); + ignore.contains(&path) + }; + + if should_ignore { + info!("Ignoring file event for recently modified file: {}", path.display()); + continue; + } + // Small delay to ensure file is fully written let config_clone = config.clone(); + let ignore_list_clone = Arc::clone(&ignore_list); tokio::spawn(async move { sleep(Duration::from_millis(500)).await; - trigger_webhook(&config_clone, path).await; + trigger_webhook(&config_clone, path, ignore_list_clone).await; }); } }