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
2 changes: 2 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand};

use crate::commands::asset::AssetArgs;
use crate::commands::project::ProjectArgs;
use crate::commands::task::TaskArgs;
use crate::commands::upload::UploadArgs;

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -44,6 +45,7 @@ pub struct Cli {
pub enum Command {
Asset(AssetArgs),
Project(ProjectArgs),
Task(TaskArgs),
Upload(UploadArgs),
}

Expand Down
19 changes: 19 additions & 0 deletions src/commands/api_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,22 @@ pub fn create_config() -> Configuration {
cfg
}

pub fn format_api_error<E: std::fmt::Debug>(e: &tellers_api_client::apis::Error<E>) -> String {
let mut message = format!("{}", e);
match e {
tellers_api_client::apis::Error::Reqwest(req_err) => {
if let Some(status) = req_err.status() {
message.push_str(&format!("; http_status: {}", status));
}
}
tellers_api_client::apis::Error::ResponseError(resp) => {
message.push_str(&format!("; http_status: {}", resp.status));
if !resp.content.is_empty() {
message.push_str(&format!("; response: {}", resp.content));
}
}
_ => {}
}
message
}

1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pub mod api_config;
pub mod asset;
pub mod prompt;
pub mod project;
pub mod task;
pub mod upload;
49 changes: 30 additions & 19 deletions src/commands/project/export.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::time::Duration;

use clap::Args;
use tellers_api_client::apis::accepts_api_key_api as api;

use crate::commands::api_config;
use crate::commands::{api_config, task};
use crate::output;

#[derive(Args, Debug)]
pub struct ExportArgs {
Expand All @@ -18,6 +21,10 @@ pub struct ExportArgs {

#[arg(long, env = "TELLERS_AUTH_BEARER")]
pub auth_bearer: Option<String>,

/// Poll GET /users/tasks/{task_id} until the export completes.
#[arg(long, default_value_t = false)]
pub wait: bool,
}

const ALLOWED_RENDITIONS: &[&str] = &["360p", "480p", "720p", "1080p", "1440p", "4k"];
Expand Down Expand Up @@ -66,27 +73,31 @@ pub fn run(args: ExportArgs) -> Result<(), String> {
bearer_header.as_deref(),
)
.await
.map_err(|e| {
let mut m = format!("export failed: {}", e);
match &e {
tellers_api_client::apis::Error::Reqwest(req_err) => {
if let Some(status) = req_err.status() {
m.push_str(&format!("; http_status: {}", status));
}
}
tellers_api_client::apis::Error::ResponseError(resp) => {
m.push_str(&format!("; http_status: {}", resp.status));
if !resp.content.is_empty() {
m.push_str(&format!("; response: {}", resp.content));
}
}
_ => {}
}
m
})?;
.map_err(|e| api_config::format_api_error(&e))?;

println!("task_id: {}", resp.task_id);
println!("asset_id: {}", resp.asset_id);

if args.wait {
output::info(format!(
"Waiting for export task {} to complete...",
resp.task_id
));
let result = task::wait_for_user_task(
&cfg,
&resp.task_id,
&api_key,
bearer_header.as_deref(),
Duration::from_secs(2),
)
.await?;
println!(
"{}",
serde_json::to_string_pretty(&result)
.map_err(|e| format!("failed to encode export result: {}", e))?
);
}

Ok(())
})
}
264 changes: 264 additions & 0 deletions src/commands/task.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
use std::time::Duration;

use clap::{Args, Subcommand};
use serde::Deserialize;
use tellers_api_client::apis::accepts_api_key_api as api;
use tellers_api_client::apis::configuration::Configuration;
use tokio::time::sleep;

use crate::commands::api_config;
use crate::output;

#[derive(Args, Debug)]
pub struct TaskArgs {
#[command(subcommand)]
pub command: TaskCommand,
}

#[derive(Subcommand, Debug)]
pub enum TaskCommand {
/// Fetch the current status of a user task.
Get(GetArgs),
/// Poll a user task until it completes or fails.
Wait(WaitArgs),
/// Cancel a running user task.
Cancel(CancelArgs),
}

#[derive(Args, Debug)]
pub struct GetArgs {
pub task_id: String,

#[arg(long, env = "TELLERS_API_KEY")]
pub api_key: Option<String>,

#[arg(long, env = "TELLERS_AUTH_BEARER")]
pub auth_bearer: Option<String>,
}

#[derive(Args, Debug)]
pub struct WaitArgs {
pub task_id: String,

#[arg(long, default_value_t = 2)]
pub interval_secs: u64,

#[arg(long, env = "TELLERS_API_KEY")]
pub api_key: Option<String>,

#[arg(long, env = "TELLERS_AUTH_BEARER")]
pub auth_bearer: Option<String>,
}

#[derive(Args, Debug)]
pub struct CancelArgs {
pub task_id: String,

#[arg(long, env = "TELLERS_API_KEY")]
pub api_key: Option<String>,

#[arg(long, env = "TELLERS_AUTH_BEARER")]
pub auth_bearer: Option<String>,
}

#[derive(Debug, Clone)]
pub enum UserTaskStatus {
Pending { progress: Option<f64> },
Complete { result: serde_json::Value },
Failed { result: Option<serde_json::Value> },
}

#[derive(Deserialize)]
struct RawUserTaskResponse {
status: String,
#[serde(default)]
progress: Option<f64>,
#[serde(default)]
result: Option<serde_json::Value>,
}

pub fn run(args: TaskArgs) -> Result<(), String> {
match args.command {
TaskCommand::Get(get_args) => run_get(get_args),
TaskCommand::Wait(wait_args) => run_wait(wait_args),
TaskCommand::Cancel(cancel_args) => run_cancel(cancel_args),
}
}

fn run_get(args: GetArgs) -> Result<(), String> {
let cfg = api_config::create_config();
let api_key = api_config::get_api_key(args.api_key)?;
let bearer = api_config::get_bearer_header(args.auth_bearer);

tokio::runtime::Runtime::new()
.map_err(|e| format!("failed to start runtime: {}", e))?
.block_on(async move {
let status =
fetch_user_task(&cfg, &args.task_id, &api_key, bearer.as_deref()).await?;
println!("{}", serde_json::to_string_pretty(&status_to_json(status))
.map_err(|e| format!("failed to encode task status: {}", e))?);
Ok(())
})
}

fn run_wait(args: WaitArgs) -> Result<(), String> {
let cfg = api_config::create_config();
let api_key = api_config::get_api_key(args.api_key)?;
let bearer = api_config::get_bearer_header(args.auth_bearer);

tokio::runtime::Runtime::new()
.map_err(|e| format!("failed to start runtime: {}", e))?
.block_on(async move {
output::info(format!(
"Polling /users/tasks/{} every {}s...",
args.task_id, args.interval_secs
));
let result = wait_for_user_task(
&cfg,
&args.task_id,
&api_key,
bearer.as_deref(),
Duration::from_secs(args.interval_secs),
)
.await?;
println!("{}", serde_json::to_string_pretty(&result)
.map_err(|e| format!("failed to encode task result: {}", e))?);
Ok(())
})
}

fn run_cancel(args: CancelArgs) -> Result<(), String> {
let cfg = api_config::create_config();
let api_key = api_config::get_api_key(args.api_key)?;
let bearer = api_config::get_bearer_header(args.auth_bearer);

tokio::runtime::Runtime::new()
.map_err(|e| format!("failed to start runtime: {}", e))?
.block_on(async move {
let resp = api::cancel_user_task_users_tasks_task_id_delete(
&cfg,
&args.task_id,
Some(&api_key),
bearer.as_deref(),
)
.await
.map_err(|e| api_config::format_api_error(&e))?;

println!("task_id: {}", resp.task_id);
println!("previous_state: {:?}", resp.previous_state);
println!("description: {}", resp.description);
Ok(())
})
}

pub async fn fetch_user_task(
cfg: &Configuration,
task_id: &str,
api_key: &str,
bearer: Option<&str>,
) -> Result<UserTaskStatus, String> {
let uri = format!(
"{}/users/tasks/{}",
cfg.base_path.trim_end_matches('/'),
urlencoding_encode(task_id)
);
let mut req = cfg.client.request(reqwest::Method::GET, &uri);
if let Some(user_agent) = &cfg.user_agent {
req = req.header(reqwest::header::USER_AGENT, user_agent.clone());
}
req = req.header("x-api-key", api_key);
if let Some(bearer) = bearer {
req = req.header("authorization", bearer);
}

let resp = req
.send()
.await
.map_err(|e| format!("failed to fetch task: {}", e))?;
let status_code = resp.status();
let body = resp
.text()
.await
.map_err(|e| format!("failed to read task response: {}", e))?;
if !status_code.is_success() {
return Err(format!(
"failed to fetch task; http_status: {}; response: {}",
status_code, body
));
}

let raw: RawUserTaskResponse = serde_json::from_str(&body)
.map_err(|e| format!("failed to parse task response: {}; body: {}", e, body))?;

parse_user_task_status(&raw)
}

pub async fn wait_for_user_task(
cfg: &Configuration,
task_id: &str,
api_key: &str,
bearer: Option<&str>,
poll_interval: Duration,
) -> Result<serde_json::Value, String> {
loop {
match fetch_user_task(cfg, task_id, api_key, bearer).await? {
UserTaskStatus::Pending { progress } => {
if let Some(progress) = progress {
output::info(format!("task progress: {:.0}%", progress_to_percent(progress)));
}
sleep(poll_interval).await;
}
UserTaskStatus::Complete { result } => return Ok(result),
UserTaskStatus::Failed { result } => {
let detail = result
.map(|v| v.to_string())
.unwrap_or_else(|| "unknown error".to_string());
return Err(format!("task failed: {}", detail));
}
}
}
}

fn parse_user_task_status(raw: &RawUserTaskResponse) -> Result<UserTaskStatus, String> {
match raw.status.as_str() {
"PENDING" => Ok(UserTaskStatus::Pending {
progress: raw.progress,
}),
"COMPLETE" => Ok(UserTaskStatus::Complete {
result: raw.result.clone().unwrap_or(serde_json::Value::Null),
}),
"FAILED" => Ok(UserTaskStatus::Failed {
result: raw.result.clone(),
}),
other => Err(format!("unknown task status: {}", other)),
}
}

fn status_to_json(status: UserTaskStatus) -> serde_json::Value {
match status {
UserTaskStatus::Pending { progress } => serde_json::json!({
"status": "PENDING",
"progress": progress,
}),
UserTaskStatus::Complete { result } => serde_json::json!({
"status": "COMPLETE",
"result": result,
}),
UserTaskStatus::Failed { result } => serde_json::json!({
"status": "FAILED",
"result": result,
}),
}
}

fn progress_to_percent(progress: f64) -> f64 {
if (0.0..=1.0).contains(&progress) {
progress * 100.0
} else {
progress.clamp(0.0, 100.0)
}
}

fn urlencoding_encode(value: &str) -> String {
url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ fn main() {
}
}
}
Some(cli::Command::Task(task_args)) => {
if let Err(error) = commands::task::run(task_args) {
eprintln!("error: {}", error);
std::process::exit(1);
}
}
Some(cli::Command::Upload(upload_args)) => {
let suppress_plain_error = matches!(
&upload_args.command,
Expand Down
Loading
Loading