use std::fmt;
use rust_i18n::t;
use tabled::Tabled;
use timeweb_rs::{
apis::s3_api, models as s3_models, models::create_storage_request::Type as StorageType
};
use crate::{error::TwcError, output::OutputFormat};
fn fmt_id<T: std::fmt::Display>(v: T) -> String {
v.to_string()
}
#[derive(Tabled)]
struct StorageRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Status")]
status: String,
#[tabled(rename = "Location")]
location: String,
#[tabled(rename = "Type")]
r#type: String,
#[tabled(rename = "Size (GB)")]
size_gb: String
}
impl fmt::Display for StorageRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {} {}",
self.id, self.name, self.status, self.location, self.r#type, self.size_gb
)
}
}
#[derive(Tabled)]
struct StorageUserRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Access Key")]
access_key: String,
#[tabled(rename = "Secret Key")]
secret_key: String
}
impl fmt::Display for StorageUserRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.id, self.access_key, self.secret_key)
}
}
#[derive(Tabled)]
struct SubdomainRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Subdomain")]
subdomain: String,
#[tabled(rename = "Status")]
status: String
}
impl fmt::Display for SubdomainRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.id, self.subdomain, self.status)
}
}
#[derive(Tabled)]
struct PresetRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Description")]
description: String,
#[tabled(rename = "Disk (GB)")]
disk: String,
#[tabled(rename = "Price")]
price: String,
#[tabled(rename = "Location")]
location: String,
#[tabled(rename = "Class")]
storage_class: String
}
impl fmt::Display for PresetRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {} {}",
self.id, self.description, self.disk, self.price, self.location, self.storage_class
)
}
}
fn fmt_disk_size(size_kb: f64) -> String {
let gb = size_kb / 1_048_576.0;
format!("{gb:.2}")
}
pub async fn list(
config: &timeweb_rs::apis::configuration::Configuration,
_limit: Option<i32>,
_offset: Option<i32>,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = s3_api::get_storages(config).await?;
let rows: Vec<StorageRow> = resp
.buckets
.iter()
.map(|b| StorageRow {
id: fmt_id(b.id),
name: b.name.clone(),
status: format!("{:?}", b.status),
location: b.location.clone(),
r#type: format!("{:?}", b.r#type),
size_gb: fmt_disk_size(b.disk_stats.size)
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_storages_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.buckets)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for b in &resp.buckets {
println!("{}\t{}", fmt_id(b.id), b.name);
}
}
}
Ok(())
}
pub async fn info(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = s3_api::get_storage(config, id).await?;
let bucket = &resp.bucket;
match format {
OutputFormat::Table => {
println!("ID: {}", fmt_id(bucket.id));
println!("Name: {}", bucket.name);
println!("Description: {}", bucket.description);
println!("Status: {:?}", bucket.status);
println!("Type: {:?}", bucket.r#type);
println!("Location: {}", bucket.location);
println!("Hostname: {}", bucket.hostname);
println!("Storage Class: {:?}", bucket.storage_class);
println!(
"Disk Size: {} GB",
fmt_disk_size(bucket.disk_stats.size)
);
println!(
"Disk Used: {} GB",
fmt_disk_size(bucket.disk_stats.used)
);
println!("Unlimited Disk: {}", bucket.disk_stats.is_unlimited);
println!("Objects: {:.0}", bucket.object_amount);
println!(
"Preset ID: {}",
bucket.preset_id.map_or_else(|| "-".to_string(), fmt_id)
);
println!(
"Auto Upgrade: {}",
if bucket.is_allow_auto_upgrade {
"yes"
} else {
"no"
}
);
println!("Project ID: {}", fmt_id(bucket.project_id));
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.bucket)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!(
"{}\t{}\t{:?}",
fmt_id(bucket.id),
bucket.name,
bucket.status
);
}
}
Ok(())
}
pub async fn create(
config: &timeweb_rs::apis::configuration::Configuration,
name: &str,
_preset_id: Option<f64>,
format: OutputFormat
) -> Result<(), TwcError> {
let req = s3_models::CreateStorageRequest::new(name.to_string(), StorageType::Private);
let resp = s3_api::create_storage(config, req).await?;
let bucket = &resp.bucket;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.storage_created", name => bucket.name, id => fmt_id(bucket.id))
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.bucket)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(bucket.id), bucket.name);
}
}
Ok(())
}
pub async fn delete(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32
) -> Result<(), TwcError> {
s3_api::delete_storage(config, id, None, None).await?;
println!("{}", t!("cli.storage_deleted", id => id));
Ok(())
}
pub async fn update(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32,
description: Option<&str>,
format: OutputFormat
) -> Result<(), TwcError> {
let mut update = s3_models::UpdateStorageRequest::new();
if let Some(d) = description {
update.description = Some(d.to_string());
}
let resp = s3_api::update_storage(config, id, update).await?;
let bucket = &resp.bucket;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.storage_updated", name => bucket.name, id => fmt_id(bucket.id))
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.bucket)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(bucket.id), bucket.name);
}
}
Ok(())
}
pub async fn user_list(
config: &timeweb_rs::apis::configuration::Configuration,
_id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = s3_api::get_storage_users(config).await?;
let rows: Vec<StorageUserRow> = resp
.users
.iter()
.map(|u| StorageUserRow {
id: fmt_id(u.id),
access_key: u.access_key.clone(),
secret_key: u.secret_key.clone()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_users_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.users)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for u in &resp.users {
println!("{}\t{}", fmt_id(u.id), u.access_key);
}
}
}
Ok(())
}
pub async fn user_update(
config: &timeweb_rs::apis::configuration::Configuration,
user_id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let secret_key = format!("new-{}", chrono::Utc::now().timestamp_micros());
let resp = s3_api::update_storage_user(
config,
user_id,
s3_models::UpdateStorageUserRequest::new(secret_key)
)
.await?;
let user = &resp.user;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.storage_user_updated", access_key => user.access_key, id => fmt_id(user.id))
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.user)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(user.id), user.access_key);
}
}
Ok(())
}
pub async fn transfer(
config: &timeweb_rs::apis::configuration::Configuration,
_target_id: Option<i32>
) -> Result<(), TwcError> {
s3_api::transfer_storage(
config,
s3_models::TransferStorageRequest::new(
String::new(),
String::new(),
String::new(),
false,
String::new(),
String::new(),
String::new()
)
)
.await?;
println!("{}", t!("cli.transfer_initiated"));
Ok(())
}
pub async fn subdomain_list(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = s3_api::get_storage_subdomains(config, id).await?;
let rows: Vec<SubdomainRow> = resp
.subdomains
.iter()
.map(|s| SubdomainRow {
id: fmt_id(s.id),
subdomain: s.subdomain.clone(),
status: format!("{:?}", s.status)
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_subdomains_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.subdomains)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for s in &resp.subdomains {
println!("{}\t{}", fmt_id(s.id), s.subdomain);
}
}
}
Ok(())
}
pub async fn subdomain_add(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32,
subdomain: &str
) -> Result<(), TwcError> {
let req = s3_models::AddStorageSubdomainsRequest::new(vec![subdomain.to_string()]);
s3_api::add_storage_subdomains(config, id, req).await?;
println!(
"{}",
t!("cli.subdomain_added", subdomain => subdomain, id => id)
);
Ok(())
}
pub async fn subdomain_delete(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32,
subdomain: &str
) -> Result<(), TwcError> {
let req = s3_models::AddStorageSubdomainsRequest::new(vec![subdomain.to_string()]);
s3_api::delete_storage_subdomains(config, id, req).await?;
println!(
"{}",
t!("cli.subdomain_deleted", subdomain => subdomain, id => id)
);
Ok(())
}
pub async fn genconfig(
config: &timeweb_rs::apis::configuration::Configuration,
id: i32
) -> Result<(), TwcError> {
let resp = s3_api::get_storage(config, id).await?;
let bucket = &resp.bucket;
let prefix = format!("{}.", bucket.name);
let endpoint = bucket
.hostname
.strip_prefix(&prefix)
.unwrap_or(&bucket.hostname);
println!("[default]");
println!("access_key = {}", bucket.access_key);
println!("secret_key = {}", bucket.secret_key);
println!("bucket_location = {}", bucket.location);
println!("host_base = {endpoint}");
println!("host_bucket = %(bucket)s.{endpoint}");
println!("use_https = True");
println!("signature_v2 = False");
Ok(())
}
pub async fn preset_list(
config: &timeweb_rs::apis::configuration::Configuration,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = s3_api::get_storages_presets(config).await?;
let rows: Vec<PresetRow> = resp
.storages_presets
.iter()
.map(|p| PresetRow {
id: fmt_id(p.id),
description: p.description_short.clone(),
disk: fmt_disk_size(p.disk),
price: format!("{:.2}", p.price),
location: format!("{:?}", p.location),
storage_class: format!("{:?}", p.storage_class)
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_presets_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.storages_presets)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for p in &resp.storages_presets {
println!(
"{}\t{}\t{}",
fmt_id(p.id),
p.description_short,
fmt_disk_size(p.disk)
);
}
}
}
Ok(())
}