Skip to main content

elizaos_plugin_s3_storage/
lib.rs

1//! # S3 Storage Plugin
2//!
3//! A Rust plugin for uploading and managing files in Amazon S3 or S3-compatible
4//! storage services. This plugin provides a high-level API for common storage
5//! operations including file uploads, byte uploads, JSON uploads, and signed URL
6//! generation.
7//!
8//! ## Features
9//!
10//! - File upload from local paths
11//! - Direct byte data upload
12//! - JSON serialization and upload
13//! - Pre-signed URL generation for secure access
14//! - Support for custom S3-compatible endpoints
15//!
16//! ## Example
17//!
18//! ```rust,no_run
19//! use elizaos_plugin_s3_storage::{S3StoragePlugin, S3StorageConfig};
20//!
21//! async fn example() -> Result<(), Box<dyn std::error::Error>> {
22//!     let config = S3StorageConfig::new(
23//!         "access_key",
24//!         "secret_key",
25//!         "us-east-1",
26//!         "my-bucket"
27//!     );
28//!     let plugin = S3StoragePlugin::new(config).await?;
29//!     Ok(())
30//! }
31//! ```
32
33#![warn(missing_docs)]
34
35pub mod client;
36pub mod error;
37pub mod service;
38pub mod types;
39
40pub use client::S3StorageClient;
41pub use error::{Result, S3StorageError};
42pub use service::AwsS3Service;
43pub use types::*;
44
45use anyhow::Result as AnyhowResult;
46
47/// High-level S3 storage plugin for file and data management.
48///
49/// This struct wraps an [`S3StorageClient`] and provides convenient methods
50/// for uploading files, bytes, and JSON data to S3-compatible storage.
51pub struct S3StoragePlugin {
52    client: S3StorageClient,
53}
54
55impl S3StoragePlugin {
56    /// Creates a new S3 storage plugin with the provided configuration.
57    ///
58    /// # Arguments
59    ///
60    /// * `config` - The S3 storage configuration including credentials and bucket info
61    ///
62    /// # Returns
63    ///
64    /// A `Result` containing the initialized plugin or an error if initialization fails.
65    pub async fn new(config: S3StorageConfig) -> Result<Self> {
66        let client = S3StorageClient::new(config).await?;
67        Ok(Self { client })
68    }
69
70    /// Uploads a file from the local filesystem to S3.
71    ///
72    /// # Arguments
73    ///
74    /// * `file_path` - Path to the local file to upload
75    /// * `sub_directory` - Optional subdirectory within the bucket
76    /// * `use_signed_url` - Whether to generate a signed URL for the uploaded file
77    /// * `expires_in` - Expiration time in seconds for the signed URL
78    ///
79    /// # Returns
80    ///
81    /// A `Result` containing the upload result with the file's S3 key and optional signed URL.
82    pub async fn upload_file(
83        &self,
84        file_path: &str,
85        sub_directory: Option<&str>,
86        use_signed_url: bool,
87        expires_in: u64,
88    ) -> Result<UploadResult> {
89        self.client
90            .upload_file(file_path, sub_directory, use_signed_url, expires_in)
91            .await
92    }
93
94    /// Uploads raw byte data to S3.
95    ///
96    /// # Arguments
97    ///
98    /// * `data` - The byte data to upload
99    /// * `file_name` - Name for the file in S3
100    /// * `content_type` - MIME type of the content (e.g., "image/png")
101    /// * `sub_directory` - Optional subdirectory within the bucket
102    /// * `use_signed_url` - Whether to generate a signed URL for the uploaded file
103    /// * `expires_in` - Expiration time in seconds for the signed URL
104    ///
105    /// # Returns
106    ///
107    /// A `Result` containing the upload result with the file's S3 key and optional signed URL.
108    pub async fn upload_bytes(
109        &self,
110        data: bytes::Bytes,
111        file_name: &str,
112        content_type: &str,
113        sub_directory: Option<&str>,
114        use_signed_url: bool,
115        expires_in: u64,
116    ) -> Result<UploadResult> {
117        self.client
118            .upload_bytes(
119                data,
120                file_name,
121                content_type,
122                sub_directory,
123                use_signed_url,
124                expires_in,
125            )
126            .await
127    }
128
129    /// Uploads JSON data to S3.
130    ///
131    /// The data is serialized to JSON format before uploading.
132    ///
133    /// # Arguments
134    ///
135    /// * `json_data` - The JSON value to upload
136    /// * `file_name` - Optional custom filename (auto-generated if not provided)
137    /// * `sub_directory` - Optional subdirectory within the bucket
138    /// * `use_signed_url` - Whether to generate a signed URL for the uploaded file
139    /// * `expires_in` - Expiration time in seconds for the signed URL
140    ///
141    /// # Returns
142    ///
143    /// A `Result` containing the JSON upload result with additional metadata.
144    pub async fn upload_json(
145        &self,
146        json_data: &serde_json::Value,
147        file_name: Option<&str>,
148        sub_directory: Option<&str>,
149        use_signed_url: bool,
150        expires_in: u64,
151    ) -> Result<JsonUploadResult> {
152        self.client
153            .upload_json(
154                json_data,
155                file_name,
156                sub_directory,
157                use_signed_url,
158                expires_in,
159            )
160            .await
161    }
162
163    /// Generates a pre-signed URL for accessing an existing S3 object.
164    ///
165    /// # Arguments
166    ///
167    /// * `key` - The S3 object key
168    /// * `expires_in` - Expiration time in seconds for the signed URL
169    ///
170    /// # Returns
171    ///
172    /// A `Result` containing the pre-signed URL string.
173    pub async fn generate_signed_url(&self, key: &str, expires_in: u64) -> Result<String> {
174        self.client.generate_signed_url(key, expires_in).await
175    }
176
177    /// Returns a reference to the underlying S3 storage client.
178    ///
179    /// This allows direct access to the client for advanced operations
180    /// not covered by the plugin's high-level API.
181    pub fn client(&self) -> &S3StorageClient {
182        &self.client
183    }
184}
185
186/// Creates an S3 storage plugin using environment variables for configuration.
187///
188/// This function reads the following environment variables:
189/// - `AWS_ACCESS_KEY_ID` (required)
190/// - `AWS_SECRET_ACCESS_KEY` (required)
191/// - `AWS_REGION` (required)
192/// - `AWS_S3_BUCKET` (required)
193/// - `AWS_S3_UPLOAD_PATH` (optional)
194/// - `AWS_S3_ENDPOINT` (optional, for S3-compatible services)
195///
196/// # Returns
197///
198/// A `Result` containing the initialized plugin or an error if required
199/// environment variables are missing.
200pub async fn get_s3_storage_plugin() -> AnyhowResult<S3StoragePlugin> {
201    let access_key = std::env::var("AWS_ACCESS_KEY_ID")
202        .map_err(|_| anyhow::anyhow!("AWS_ACCESS_KEY_ID environment variable is required"))?;
203    let secret_key = std::env::var("AWS_SECRET_ACCESS_KEY")
204        .map_err(|_| anyhow::anyhow!("AWS_SECRET_ACCESS_KEY environment variable is required"))?;
205    let region = std::env::var("AWS_REGION")
206        .map_err(|_| anyhow::anyhow!("AWS_REGION environment variable is required"))?;
207    let bucket = std::env::var("AWS_S3_BUCKET")
208        .map_err(|_| anyhow::anyhow!("AWS_S3_BUCKET environment variable is required"))?;
209
210    let mut config = S3StorageConfig::new(&access_key, &secret_key, &region, &bucket);
211
212    if let Ok(upload_path) = std::env::var("AWS_S3_UPLOAD_PATH") {
213        config = config.upload_path(&upload_path);
214    }
215
216    if let Ok(endpoint) = std::env::var("AWS_S3_ENDPOINT") {
217        config = config.endpoint(&endpoint);
218    }
219
220    S3StoragePlugin::new(config)
221        .await
222        .map_err(|e| anyhow::anyhow!("Failed to create S3 storage plugin: {}", e))
223}