Skip to main content

boundless_market/storage/
config.rs

1// Copyright 2026 Boundless Foundation, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration types for storage uploaders.
16
17use std::path::PathBuf;
18
19use clap::{builder::ArgPredicate, Args, ValueEnum};
20use derive_builder::Builder;
21use url::Url;
22
23/// The type of storage uploader to use for uploads.
24#[derive(Default, Clone, Debug, ValueEnum, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum StorageUploaderType {
27    /// No storage uploader.
28    #[default]
29    None,
30    /// S3 storage uploader.
31    #[cfg(feature = "s3")]
32    S3,
33    /// Google Cloud Storage uploader.
34    #[cfg(feature = "gcs")]
35    Gcs,
36    /// Pinata storage uploader.
37    Pinata,
38    /// Temporary file storage uploader.
39    File,
40    /// In-memory mock storage uploader for testing.
41    #[cfg(feature = "test-utils")]
42    Mock,
43}
44
45/// Configuration for the storage module (upload providers).
46///
47/// This configuration is used to set up storage uploaders for uploading programs and inputs.
48///
49/// # Authentication
50///
51/// - **S3**: Uses the AWS SDK default credential chain (environment variables,
52///   `~/.aws/credentials`, IAM role, etc.). No explicit credentials needed in config.
53/// - **GCS**: Uses the Google Cloud SDK default credential chain (ADC) via
54///   `GOOGLE_APPLICATION_CREDENTIALS`, workload identity, or `gcloud auth application-default login`.
55///   Explicit credentials can be provided via `gcs_credentials_json`.
56/// - **Pinata**: Requires a JWT token.
57#[derive(Clone, Default, Debug, Args, Builder)]
58#[non_exhaustive]
59pub struct StorageUploaderConfig {
60    /// Storage uploader to use [possible values: s3, gcs, pinata, file]
61    ///
62    /// - For 's3', the following option is required:
63    ///   --s3-bucket (optionally: --s3-url, --aws-region)
64    /// - For 'gcs', the following option is required:
65    ///   --gcs-bucket (optionally: --gcs-url, --gcs-credentials-json)
66    /// - For 'pinata', the following option is required:
67    ///   --pinata-jwt (optionally: --pinata-api-url, --ipfs-gateway-url)
68    /// - For 'file', no additional options are required (optionally: --file-path)
69    #[arg(long, env, value_enum, default_value = "none", default_value_ifs = [
70        ("s3_bucket", ArgPredicate::IsPresent, "s3"),
71        ("gcs_bucket", ArgPredicate::IsPresent, "gcs"),
72        ("pinata_jwt", ArgPredicate::IsPresent, "pinata"),
73        ("file_path", ArgPredicate::IsPresent, "file")
74    ])]
75    #[builder(default)]
76    pub storage_uploader: StorageUploaderType,
77
78    // **S3 Storage Uploader Options**
79    /// S3 bucket name
80    #[cfg(feature = "s3")]
81    #[arg(long, env, required_if_eq("storage_uploader", "s3"))]
82    #[builder(setter(strip_option, into), default)]
83    pub s3_bucket: Option<String>,
84    /// S3 endpoint URL (optional, for S3-compatible services like MinIO)
85    #[cfg(feature = "s3")]
86    #[arg(long, env)]
87    #[builder(setter(strip_option, into), default)]
88    pub s3_url: Option<String>,
89    /// AWS access key (optional, uses AWS default credential chain if not set)
90    #[cfg(feature = "s3")]
91    #[arg(long, env)]
92    #[builder(setter(strip_option, into), default)]
93    pub aws_access_key_id: Option<String>,
94    /// AWS secret key (required if aws_access_key_id is set)
95    #[cfg(feature = "s3")]
96    #[arg(long, env)]
97    #[builder(setter(strip_option, into), default)]
98    pub aws_secret_access_key: Option<String>,
99    /// AWS region (optional, can be inferred from environment)
100    #[cfg(feature = "s3")]
101    #[arg(long, env)]
102    #[builder(setter(strip_option, into), default)]
103    pub aws_region: Option<String>,
104    /// Use presigned URLs for S3 (default: true)
105    #[cfg(feature = "s3")]
106    #[arg(long, env)]
107    #[builder(setter(strip_option), default)]
108    pub s3_presigned: Option<bool>,
109    /// Return public HTTPS URLs instead of s3:// or presigned URLs (requires bucket to be public)
110    #[cfg(feature = "s3")]
111    #[arg(long, env)]
112    #[builder(setter(strip_option), default)]
113    pub s3_public_url: Option<bool>,
114
115    // **GCS Storage Uploader Options**
116    /// GCS bucket name
117    #[cfg(feature = "gcs")]
118    #[arg(long, env, required_if_eq("storage_uploader", "gcs"))]
119    #[builder(setter(strip_option, into), default)]
120    pub gcs_bucket: Option<String>,
121    /// GCS endpoint URL (optional, for emulators like fake-gcs-server)
122    #[cfg(feature = "gcs")]
123    #[arg(long, env)]
124    #[builder(setter(strip_option), default)]
125    pub gcs_url: Option<String>,
126    /// GCS service account credentials JSON (optional, uses ADC if not set)
127    #[cfg(feature = "gcs")]
128    #[arg(long, env)]
129    #[builder(setter(strip_option, into), default)]
130    pub gcs_credentials_json: Option<String>,
131    /// Return public HTTPS URLs instead of gs:// URLs (requires bucket to be publicly readable)
132    #[cfg(feature = "gcs")]
133    #[arg(long, env)]
134    #[builder(setter(strip_option), default)]
135    pub gcs_public_url: Option<bool>,
136
137    // **Pinata Storage Uploader Options**
138    /// Pinata JWT
139    #[arg(long, env, required_if_eq("storage_uploader", "pinata"))]
140    #[builder(setter(strip_option, into), default)]
141    pub pinata_jwt: Option<String>,
142    /// Pinata API URL
143    #[arg(long, env)]
144    #[builder(setter(strip_option), default)]
145    pub pinata_api_url: Option<Url>,
146    /// Pinata gateway URL
147    #[arg(long, env)]
148    #[builder(setter(strip_option), default)]
149    pub ipfs_gateway_url: Option<Url>,
150
151    // **File Storage Uploader Options**
152    /// Path for file storage uploader
153    #[arg(long)]
154    #[builder(setter(strip_option, into), default)]
155    pub file_path: Option<PathBuf>,
156}
157
158impl StorageUploaderConfig {
159    /// Create a new builder to construct a config.
160    pub fn builder() -> StorageUploaderConfigBuilder {
161        Default::default()
162    }
163
164    /// Create a new configuration for a [StorageUploaderType::File].
165    pub fn dev_mode() -> Self {
166        Self { storage_uploader: StorageUploaderType::File, ..Default::default() }
167    }
168}
169
170/// Default IPFS gateway URL for fallback downloads.
171pub const DEFAULT_IPFS_GATEWAY_URL: &str = "https://gateway.beboundless.cloud";
172
173/// Configuration for download operations (construction-time settings).
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct StorageDownloaderConfig {
176    /// Maximum size in bytes for downloaded content.
177    pub max_size: usize,
178    /// Maximum number of retry attempts for failed downloads.
179    ///
180    /// If not set, nothing is retried.
181    pub max_retries: Option<u8>,
182    /// Optional cache directory for storing downloaded images and inputs.
183    ///
184    /// If not set, files will be re-downloaded every time.
185    pub cache_dir: Option<PathBuf>,
186    /// Optional IPFS gateway URL for fallback when downloading IPFS content.
187    ///
188    /// When set, if an HTTP download fails for a URL containing `/ipfs/`,
189    /// the downloader will retry with this gateway.
190    pub ipfs_gateway: Option<Url>,
191}
192
193impl Default for StorageDownloaderConfig {
194    fn default() -> Self {
195        Self {
196            max_size: usize::MAX,
197            max_retries: None,
198            cache_dir: None,
199            // Safe to unwrap: DEFAULT_IPFS_GATEWAY_URL is a valid URL constant
200            ipfs_gateway: Some(Url::parse(DEFAULT_IPFS_GATEWAY_URL).unwrap()),
201        }
202    }
203}