Skip to main content

raps_da/
appbundles.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! AppBundle operations for the Design Automation API.
5
6use anyhow::{Context, Result};
7use serde::Serialize;
8
9use raps_kernel::http;
10
11use crate::DesignAutomationClient;
12use crate::types::*;
13
14impl DesignAutomationClient {
15    /// List all app bundles
16    pub async fn list_appbundles(&self) -> Result<Vec<String>> {
17        let token = self.auth.get_token().await?;
18        let url = format!("{}/appbundles", self.config.da_url());
19
20        let response = http::send_with_retry(&self.config.http_config, || {
21            self.http_client.get(&url).bearer_auth(&token)
22        })
23        .await?;
24
25        if !response.status().is_success() {
26            let status = response.status();
27            let error_text = response.text().await.unwrap_or_default();
28            anyhow::bail!("Failed to list appbundles ({status}): {error_text}");
29        }
30
31        let paginated: PaginatedResponse<String> = response
32            .json()
33            .await
34            .context("Failed to parse appbundles response")?;
35
36        Ok(paginated.data)
37    }
38
39    /// Create a new app bundle
40    pub async fn create_appbundle(
41        &self,
42        id: &str,
43        engine: &str,
44        description: Option<&str>,
45    ) -> Result<AppBundleDetails> {
46        let token = self.auth.get_token().await?;
47        let url = format!("{}/appbundles", self.config.da_url());
48
49        let request = CreateAppBundleRequest {
50            id: id.to_string(),
51            engine: engine.to_string(),
52            description: description.map(|s| s.to_string()),
53        };
54
55        let response = http::send_with_retry(&self.config.http_config, || {
56            self.http_client
57                .post(&url)
58                .bearer_auth(&token)
59                .header("Content-Type", "application/json")
60                .json(&request)
61        })
62        .await?;
63
64        if !response.status().is_success() {
65            let status = response.status();
66            let error_text = response.text().await.unwrap_or_default();
67            anyhow::bail!("Failed to create appbundle ({status}): {error_text}");
68        }
69
70        let appbundle: AppBundleDetails = response
71            .json()
72            .await
73            .context("Failed to parse appbundle response")?;
74
75        Ok(appbundle)
76    }
77
78    /// Create an alias for an app bundle
79    pub async fn create_appbundle_alias(
80        &self,
81        bundle_id: &str,
82        alias: &str,
83        version: i32,
84    ) -> Result<()> {
85        let token = self.auth.get_token().await?;
86        let url = format!("{}/appbundles/{}/aliases", self.config.da_url(), bundle_id);
87
88        #[derive(Serialize)]
89        struct AliasRequest {
90            id: String,
91            version: i32,
92        }
93
94        let request = AliasRequest {
95            id: alias.to_string(),
96            version,
97        };
98
99        let response = http::send_with_retry(&self.config.http_config, || {
100            self.http_client
101                .post(&url)
102                .bearer_auth(&token)
103                .header("Content-Type", "application/json")
104                .json(&request)
105        })
106        .await?;
107
108        if !response.status().is_success() {
109            let status = response.status();
110            let error_text = response.text().await.unwrap_or_default();
111            anyhow::bail!("Failed to create appbundle alias ({status}): {error_text}");
112        }
113
114        Ok(())
115    }
116
117    /// Delete an app bundle
118    pub async fn delete_appbundle(&self, id: &str) -> Result<()> {
119        let token = self.auth.get_token().await?;
120        let url = format!("{}/appbundles/{}", self.config.da_url(), id);
121
122        let response = http::send_with_retry(&self.config.http_config, || {
123            self.http_client.delete(&url).bearer_auth(&token)
124        })
125        .await?;
126
127        if !response.status().is_success() {
128            let status = response.status();
129            let error_text = response.text().await.unwrap_or_default();
130            anyhow::bail!("Failed to delete appbundle ({status}): {error_text}");
131        }
132
133        Ok(())
134    }
135
136    /// Upload an app bundle archive (.zip) using pre-signed S3 URL
137    ///
138    /// After creating an app bundle, the response includes `upload_parameters`
139    /// with an `endpoint_url` and `form_data` fields. This method POSTs the
140    /// archive file as multipart/form-data to that pre-signed URL.
141    ///
142    /// # Arguments
143    /// * `upload_params` - The upload parameters from the create_appbundle response
144    /// * `file_path` - Path to the .zip archive to upload
145    pub async fn upload_appbundle(
146        &self,
147        upload_params: &UploadParameters,
148        file_path: &std::path::Path,
149    ) -> Result<()> {
150        let endpoint_url = upload_params
151            .endpoint_url
152            .as_deref()
153            .ok_or_else(|| anyhow::anyhow!("Upload parameters missing endpoint URL"))?;
154
155        // Validate file exists and is a zip
156        if !file_path.exists() {
157            anyhow::bail!("File not found: {}", file_path.display());
158        }
159
160        let extension = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
161        if extension != "zip" {
162            anyhow::bail!(
163                "Expected .zip archive, got .{} ({})",
164                extension,
165                file_path.display()
166            );
167        }
168
169        // Read the file
170        let file_bytes = tokio::fs::read(file_path)
171            .await
172            .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
173
174        let file_name = file_path
175            .file_name()
176            .and_then(|n| n.to_str())
177            .unwrap_or("bundle.zip")
178            .to_string();
179
180        // Build multipart form with form_data fields + the file
181        let mut form = reqwest::multipart::Form::new();
182
183        // Add all form_data fields first (required by S3 pre-signed POST)
184        if let Some(ref form_data) = upload_params.form_data {
185            for (key, value) in form_data {
186                form = form.text(key.clone(), value.clone());
187            }
188        }
189
190        // Add the file as the last field (S3 requires "file" to be last)
191        let file_part = reqwest::multipart::Part::bytes(file_bytes)
192            .file_name(file_name)
193            .mime_str("application/octet-stream")?;
194        form = form.part("file", file_part);
195
196        // POST to the pre-signed URL (no auth header needed -- S3 pre-signed)
197        let response = self
198            .http_client
199            .post(endpoint_url)
200            .multipart(form)
201            .send()
202            .await
203            .context("Failed to upload app bundle archive")?;
204
205        if !response.status().is_success() {
206            let status = response.status();
207            let error_text = response.text().await.unwrap_or_default();
208            anyhow::bail!("Failed to upload app bundle ({status}): {error_text}");
209        }
210
211        Ok(())
212    }
213}