From a86bf5882e49fc7e3817656d0a7708dd36781d97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:32:08 +0000 Subject: [PATCH 1/5] Initial plan From c341bb5e05896e79266cf2f7e57b0b10b496a108 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:41:40 +0000 Subject: [PATCH 2/5] Add optional file overwrite feature with server response Co-authored-by: batonac <4996285+batonac@users.noreply.github.com> --- README.md | 16 +++++++++++ src/main.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7bb7a0..0f415f8 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 contain "xml" (e.g., `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..ca54f3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ 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; @@ -25,6 +26,7 @@ struct Config { webhook_url: String, webhook_method: String, include_content: bool, + overwrite_with_response: bool, } impl Config { @@ -43,11 +45,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 +66,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 +116,56 @@ 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 + if config.overwrite_with_response && config.include_content { + 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) + if content_type.contains("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(2)).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); @@ -142,6 +199,10 @@ async fn main() { 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 +230,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; }); } } From c1a67f9a3334c3ed35c38123de97d5ddcf6da404 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:45:27 +0000 Subject: [PATCH 3/5] Address code review feedback: improve validation and constants Co-authored-by: batonac <4996285+batonac@users.noreply.github.com> --- src/main.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index ca54f3e..1888367 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,10 @@ 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, @@ -125,7 +129,11 @@ async fn trigger_webhook(config: &Config, filepath: PathBuf, ignore_list: Arc { if !response_body.is_empty() { @@ -142,7 +150,7 @@ async fn trigger_webhook(config: &Config, filepath: PathBuf, ignore_list: Arc Date: Thu, 18 Dec 2025 06:18:34 -0500 Subject: [PATCH 4/5] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f415f8..cb7ffdb 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ With `INCLUDE_CONTENT=true`: 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 contain "xml" (e.g., `text/xml` or `application/xml`) +- 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: From 736931d32c1486c8b816ac556dd21202777d3829 Mon Sep 17 00:00:00 2001 From: Kevin Shenk Date: Thu, 18 Dec 2025 06:19:15 -0500 Subject: [PATCH 5/5] feat: should_overwrite_with_response var Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1888367..695c0ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,7 +122,11 @@ async fn trigger_webhook(config: &Config, filepath: PathBuf, ignore_list: Arc