Skip to main content

alien_commands/server/
storage.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use alien_bindings::traits::Storage;
5use alien_error::Context;
6use chrono::{DateTime, Utc};
7
8#[cfg(feature = "server")]
9use object_store::path::Path as StoragePath;
10
11use crate::error::{ErrorData, Result};
12
13/// Helper functions for storage operations in ARC server
14pub struct ArcStorageHelper {
15    storage: Arc<dyn Storage>,
16}
17
18impl ArcStorageHelper {
19    pub fn new(storage: Arc<dyn Storage>) -> Self {
20        Self { storage }
21    }
22
23    /// Generate a presigned PUT URL for command params upload
24    pub async fn generate_params_put_url(
25        &self,
26        command_id: &str,
27        expires_in: Duration,
28    ) -> Result<String> {
29        let path = StoragePath::from(format!("arc/commands/{}/params", command_id));
30        let presigned = self
31            .storage
32            .presigned_put(&path, expires_in)
33            .await
34            .context(ErrorData::StorageOperationFailed {
35                message: "Failed to create presigned PUT URL".to_string(),
36                operation: Some("presigned_put".to_string()),
37                path: Some(path.to_string()),
38            })?;
39
40        Ok(presigned.url())
41    }
42
43    /// Generate a presigned GET URL for command params download
44    pub async fn generate_params_get_url(
45        &self,
46        command_id: &str,
47        expires_in: Duration,
48    ) -> Result<String> {
49        let path = StoragePath::from(format!("arc/commands/{}/params", command_id));
50        let presigned = self
51            .storage
52            .presigned_get(&path, expires_in)
53            .await
54            .context(ErrorData::StorageOperationFailed {
55                message: "Failed to create presigned GET URL".to_string(),
56                operation: Some("presigned_get".to_string()),
57                path: Some(path.to_string()),
58            })?;
59
60        Ok(presigned.url())
61    }
62
63    /// Generate a presigned PUT URL for response body upload
64    pub async fn generate_response_put_url(
65        &self,
66        command_id: &str,
67        expires_in: Duration,
68    ) -> Result<String> {
69        let path = StoragePath::from(format!("arc/commands/{}/response", command_id));
70        let presigned = self
71            .storage
72            .presigned_put(&path, expires_in)
73            .await
74            .context(ErrorData::StorageOperationFailed {
75                message: "Failed to create response PUT URL".to_string(),
76                operation: Some("presigned_put".to_string()),
77                path: Some(path.to_string()),
78            })?;
79
80        Ok(presigned.url())
81    }
82
83    /// Generate a presigned GET URL for response body download
84    pub async fn generate_response_get_url(
85        &self,
86        command_id: &str,
87        expires_in: Duration,
88    ) -> Result<String> {
89        let path = StoragePath::from(format!("arc/commands/{}/response", command_id));
90        let presigned = self
91            .storage
92            .presigned_get(&path, expires_in)
93            .await
94            .context(ErrorData::StorageOperationFailed {
95                message: "Failed to create response GET URL".to_string(),
96                operation: Some("presigned_get".to_string()),
97                path: Some(path.to_string()),
98            })?;
99
100        Ok(presigned.url())
101    }
102
103    /// Clean up storage objects for a completed command
104    pub async fn cleanup_command_storage(&self, command_id: &str) -> Result<()> {
105        let params_path = StoragePath::from(format!("arc/commands/{}/params", command_id));
106        let response_path = StoragePath::from(format!("arc/commands/{}/response", command_id));
107
108        // Best effort cleanup - don't fail if objects don't exist
109        if let Err(e) = self.storage.delete(&params_path).await {
110            tracing::warn!("Failed to cleanup params for command {}: {}", command_id, e);
111        }
112
113        if let Err(e) = self.storage.delete(&response_path).await {
114            tracing::warn!(
115                "Failed to cleanup response for command {}: {}",
116                command_id,
117                e
118            );
119        }
120
121        Ok(())
122    }
123
124    /// Get the base storage URL for this binding
125    pub fn get_base_url(&self) -> String {
126        self.storage.get_url().to_string()
127    }
128}
129
130/// Configuration for storage URL generation
131#[derive(Debug, Clone)]
132pub struct StorageUrlConfig {
133    /// Default expiration time for presigned URLs
134    pub default_expires_in: Duration,
135    /// Maximum expiration time allowed
136    pub max_expires_in: Duration,
137}
138
139impl Default for StorageUrlConfig {
140    fn default() -> Self {
141        Self {
142            default_expires_in: Duration::from_secs(3600), // 1 hour
143            max_expires_in: Duration::from_secs(24 * 3600), // 24 hours
144        }
145    }
146}
147
148impl StorageUrlConfig {
149    /// Validate and clamp expiration time to allowed range
150    pub fn validate_expires_in(&self, requested: Duration) -> Duration {
151        if requested > self.max_expires_in {
152            self.max_expires_in
153        } else if requested.is_zero() {
154            self.default_expires_in
155        } else {
156            requested
157        }
158    }
159
160    /// Calculate expiration time from a target datetime
161    pub fn expires_in_until(&self, target: DateTime<Utc>) -> Duration {
162        let now = Utc::now();
163        if target <= now {
164            Duration::from_secs(60) // Minimum 1 minute
165        } else {
166            let diff = target.signed_duration_since(now);
167            let seconds = diff.num_seconds().max(60) as u64; // Minimum 1 minute
168            self.validate_expires_in(Duration::from_secs(seconds))
169        }
170    }
171}