Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 20 additions & 28 deletions src/api/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ use crate::api::Api;
use crate::commands;
use crate::commands::CommandError;

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AssetSignature {
pub(crate) signature: String,
}
/// Keeps the `id=in.(...)` query string clear of the 8 KB request-line limit servers impose.
const MAX_ASSET_IDS_PER_STATUS_REQUEST: usize = 100;

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AssetProcessingStatus {
pub(crate) status: String,
Expand All @@ -16,33 +15,26 @@ pub struct AssetProcessingStatus {
}

impl Api {
pub fn get_version_asset_signatures(
&self,
app_id: &str,
revision: u32,
) -> Result<Vec<AssetSignature>, CommandError> {
Ok(serde_json::from_value(commands::get(
&self.authentication,
&format!(
"v4/assets?select=signature&app_id=eq.{app_id}&app_revision=eq.{revision}&type=eq.edge-app-file"
),
)?)?)
}

pub fn get_processing_statuses(
&self,
app_id: &str,
revision: u32,
asset_ids: &[String],
) -> Result<Vec<AssetProcessingStatus>, CommandError> {
let response = commands::get(
&self.authentication,
&format!(
"v4/assets?select=status,processing_error,title&app_id=eq.{app_id}&app_revision=eq.{revision}&status=neq.finished"
),
)?;
let mut statuses = Vec::new();

for chunk in asset_ids.chunks(MAX_ASSET_IDS_PER_STATUS_REQUEST) {
let response = commands::get(
&self.authentication,
&format!(
"v4/assets?select=status,processing_error,title&id=in.({})&status=neq.finished",
chunk.join(",")
),
)?;

statuses.extend(serde_json::from_value::<Vec<AssetProcessingStatus>>(
response,
)?);
}

Ok(serde_json::from_value::<Vec<AssetProcessingStatus>>(
response,
)?)
Ok(statuses)
}
}
11 changes: 1 addition & 10 deletions src/api/edge_app/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::json;

use crate::api::Api;
use crate::commands;
Expand Down Expand Up @@ -81,12 +80,4 @@ impl Api {
Ok(apps[0].clone())
}
}

pub fn copy_assets(&self, payload: Value) -> Result<Vec<String>, CommandError> {
let response = commands::post(&self.authentication, "v4/edge-apps/copy-assets", &payload)?;
let copied_assets = serde_json::from_value::<Vec<String>>(response)?;

debug!("Copied assets: {copied_assets:?}");
Ok(copied_assets)
}
}
42 changes: 0 additions & 42 deletions src/api/edge_app/channel.rs

This file was deleted.

206 changes: 206 additions & 0 deletions src/api/edge_app/deploy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;

use log::debug;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::api::Api;
use crate::commands::CommandError;

const DEPLOY_TIMEOUT_SECONDS: u64 = 60;

#[derive(Debug, Serialize)]
pub struct DeployPayload {
pub manifest: Value,
pub file_tree: HashMap<String, String>,
pub delete_missing_settings: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct FailedFile {
pub path: String,
pub error: String,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct OutstandingFiles {
#[serde(default)]
pub missing: Vec<String>,
#[serde(default)]
pub pending: Vec<String>,
#[serde(default)]
pub failed: Vec<FailedFile>,
}

pub fn describe_failed_files(files: &[FailedFile]) -> String {
files
.iter()
.map(|file| format!("{}: {}", file.path, file.error))
.collect::<Vec<_>>()
.join("; ")
}

impl fmt::Display for OutstandingFiles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if !self.missing.is_empty() {
parts.push(format!("not uploaded: {}", self.missing.join(", ")));
}
if !self.pending.is_empty() {
parts.push(format!("still processing: {}", self.pending.join(", ")));
}
if !self.failed.is_empty() {
parts.push(format!("failed: {}", describe_failed_files(&self.failed)));
}
if parts.is_empty() {
return write!(f, "the server reported no details");
}

write!(f, "{}", parts.join("; "))
}
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct SettingsDiff {
#[serde(default)]
pub create: Vec<String>,
#[serde(default)]
pub update: Vec<String>,
#[serde(default)]
pub delete: Vec<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployDiff {
#[serde(default)]
pub settings: SettingsDiff,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployPreview {
pub deploy_needed: bool,
#[serde(default)]
pub outstanding: OutstandingFiles,
#[serde(default)]
pub diff: DeployDiff,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployResult {
pub revision: u32,
pub created: bool,
#[serde(default)]
pub published: bool,
#[serde(default)]
pub channel: String,
}

impl Api {
pub fn deploy_preview(
&self,
app_id: &str,
payload: &DeployPayload,
) -> Result<DeployPreview, CommandError> {
let (status, body) = self.post_deploy(app_id, "deploy/preview", payload)?;
if status != StatusCode::OK {
return Err(CommandError::WrongResponseStatus(status.as_u16()));
}

Ok(serde_json::from_value(body)?)
}

pub fn deploy(
&self,
app_id: &str,
payload: &DeployPayload,
) -> Result<DeployResult, CommandError> {
#[derive(Deserialize)]
struct Conflict {
#[serde(default)]
outstanding: OutstandingFiles,
}

let (status, body) = self.post_deploy(app_id, "deploy", payload)?;
match status {
StatusCode::OK => Ok(serde_json::from_value(body)?),
StatusCode::CONFLICT => {
let conflict: Conflict = serde_json::from_value(body)?;
Err(CommandError::DeployRejected(
conflict.outstanding.to_string(),
))
}
_ => Err(CommandError::WrongResponseStatus(status.as_u16())),
}
}

fn post_deploy(
&self,
app_id: &str,
endpoint: &str,
payload: &DeployPayload,
) -> Result<(StatusCode, Value), CommandError> {
let url = format!(
"{}/v3/edge-apps/{app_id}/{endpoint}",
&self.authentication.config.url

Check warning on line 147 in src/api/edge_app/deploy.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `format!` argument

warning: redundant reference in `format!` argument --> src/api/edge_app/deploy.rs:147:13 | 147 | &self.authentication.config.url | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `self.authentication.config.url` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#useless_borrows_in_formatting = note: `#[warn(clippy::useless_borrows_in_formatting)]` on by default
);
debug!("POST {url}");

let response = self
.authentication
.build_client()?
.post(&url)
.timeout(Duration::from_secs(DEPLOY_TIMEOUT_SECONDS))
.json(payload)
.send()?;

let status = response.status();
debug!("POST {url} -> {status}");

match status {
StatusCode::OK | StatusCode::CONFLICT => {
Ok((status, serde_json::from_str(&response.text()?)?))
}
StatusCode::NOT_FOUND => Err(CommandError::AppNotFound(format!(
"Edge App with ID '{app_id}' not found."
))),
_ => {
let body = response.text().unwrap_or_default();
debug!("Response: {body}");
Err(CommandError::WrongResponseStatus(status.as_u16()))
}
}
}
}

#[cfg(test)]
mod tests {
use super::OutstandingFiles;

#[test]
fn test_outstanding_files_when_empty_should_still_describe_itself() {
assert_eq!(
OutstandingFiles::default().to_string(),
"the server reported no details"
);
}

#[test]
fn test_outstanding_files_should_list_every_non_empty_group() {
let outstanding = OutstandingFiles {
missing: vec!["index.html".to_string()],
pending: vec!["logo.png".to_string()],
failed: vec![super::FailedFile {
path: "clip.mp4".to_string(),
error: "unsupported".to_string(),
}],
};

assert_eq!(
outstanding.to_string(),
"not uploaded: index.html; still processing: logo.png; failed: clip.mp4: unsupported"
);
}
}
3 changes: 1 addition & 2 deletions src/api/edge_app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod app;
pub mod channel;
pub mod deploy;
pub mod installation;
pub mod setting;
pub mod version;
Loading
Loading