rusticity_term/
s3.rs

1use crate::common::translate_column;
2use crate::common::{format_bytes, ColumnId, UTC_TIMESTAMP_WIDTH};
3use crate::ui::table::Column as TableColumn;
4use ratatui::prelude::*;
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8    for col in [
9        BucketColumn::Name,
10        BucketColumn::Region,
11        BucketColumn::CreationDate,
12    ] {
13        i18n.entry(col.id().to_string())
14            .or_insert_with(|| col.default_name().to_string());
15    }
16
17    for col in [
18        ObjectColumn::Key,
19        ObjectColumn::Size,
20        ObjectColumn::LastModified,
21        ObjectColumn::StorageClass,
22    ] {
23        i18n.entry(col.id().to_string())
24            .or_insert_with(|| col.default_name().to_string());
25    }
26}
27
28pub fn console_url_buckets(region: &str) -> String {
29    format!(
30        "https://{}.console.aws.amazon.com/s3/buckets?region={}",
31        region, region
32    )
33}
34
35pub fn console_url_bucket(region: &str, bucket: &str, prefix: &str) -> String {
36    if prefix.is_empty() {
37        format!(
38            "https://s3.console.aws.amazon.com/s3/buckets/{}?region={}",
39            bucket, region
40        )
41    } else {
42        format!(
43            "https://s3.console.aws.amazon.com/s3/buckets/{}?region={}&prefix={}",
44            bucket,
45            region,
46            urlencoding::encode(prefix)
47        )
48    }
49}
50
51#[derive(Debug, Clone)]
52pub struct Bucket {
53    pub name: String,
54    pub region: String,
55    pub creation_date: String,
56}
57
58#[derive(Debug, Clone)]
59pub struct Object {
60    pub key: String,
61    pub size: i64,
62    pub last_modified: String,
63    pub is_prefix: bool,
64    pub storage_class: String,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub enum BucketColumn {
69    Name,
70    Region,
71    CreationDate,
72}
73
74impl BucketColumn {
75    pub fn id(&self) -> &'static str {
76        match self {
77            BucketColumn::Name => "column.s3.bucket.name",
78            BucketColumn::Region => "column.s3.bucket.region",
79            BucketColumn::CreationDate => "column.s3.bucket.creation_date",
80        }
81    }
82
83    pub fn default_name(&self) -> &'static str {
84        match self {
85            BucketColumn::Name => "Name",
86            BucketColumn::Region => "Region",
87            BucketColumn::CreationDate => "Creation date",
88        }
89    }
90
91    pub fn all() -> [BucketColumn; 3] {
92        [
93            BucketColumn::Name,
94            BucketColumn::Region,
95            BucketColumn::CreationDate,
96        ]
97    }
98
99    pub fn ids() -> Vec<ColumnId> {
100        Self::all().iter().map(|c| c.id()).collect()
101    }
102
103    pub fn from_id(id: &str) -> Option<Self> {
104        match id {
105            "column.s3.bucket.name" => Some(BucketColumn::Name),
106            "column.s3.bucket.region" => Some(BucketColumn::Region),
107            "column.s3.bucket.creation_date" => Some(BucketColumn::CreationDate),
108            _ => None,
109        }
110    }
111
112    pub fn name(&self) -> String {
113        translate_column(self.id(), self.default_name())
114    }
115}
116
117impl TableColumn<Bucket> for BucketColumn {
118    fn name(&self) -> &str {
119        Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
120    }
121
122    fn width(&self) -> u16 {
123        let translated = translate_column(self.id(), self.default_name());
124        translated.len().max(match self {
125            BucketColumn::Name => 30,
126            BucketColumn::Region => 15,
127            BucketColumn::CreationDate => UTC_TIMESTAMP_WIDTH as usize,
128        }) as u16
129    }
130
131    fn render(&self, item: &Bucket) -> (String, Style) {
132        let text = match self {
133            BucketColumn::Name => item.name.clone(),
134            BucketColumn::Region => item.region.clone(),
135            BucketColumn::CreationDate => item.creation_date.clone(),
136        };
137        (text, Style::default())
138    }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum ObjectColumn {
143    Key,
144    Type,
145    LastModified,
146    Size,
147    StorageClass,
148}
149
150impl ObjectColumn {
151    pub fn id(&self) -> &'static str {
152        match self {
153            ObjectColumn::Key => "column.s3.object.key",
154            ObjectColumn::Type => "column.s3.object.type",
155            ObjectColumn::LastModified => "column.s3.object.last_modified",
156            ObjectColumn::Size => "column.s3.object.size",
157            ObjectColumn::StorageClass => "column.s3.object.storage_class",
158        }
159    }
160
161    pub fn default_name(&self) -> &'static str {
162        match self {
163            ObjectColumn::Key => "Name",
164            ObjectColumn::Type => "Type",
165            ObjectColumn::LastModified => "Last modified",
166            ObjectColumn::Size => "Size",
167            ObjectColumn::StorageClass => "Storage class",
168        }
169    }
170
171    pub fn all() -> [ObjectColumn; 5] {
172        [
173            ObjectColumn::Key,
174            ObjectColumn::Type,
175            ObjectColumn::LastModified,
176            ObjectColumn::Size,
177            ObjectColumn::StorageClass,
178        ]
179    }
180
181    pub fn from_id(id: &str) -> Option<Self> {
182        match id {
183            "column.s3.object.key" => Some(ObjectColumn::Key),
184            "column.s3.object.type" => Some(ObjectColumn::Type),
185            "column.s3.object.last_modified" => Some(ObjectColumn::LastModified),
186            "column.s3.object.size" => Some(ObjectColumn::Size),
187            "column.s3.object.storage_class" => Some(ObjectColumn::StorageClass),
188            _ => None,
189        }
190    }
191
192    pub fn name(&self) -> String {
193        translate_column(self.id(), self.default_name())
194    }
195}
196
197impl TableColumn<Object> for ObjectColumn {
198    fn name(&self) -> &str {
199        Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
200    }
201
202    fn width(&self) -> u16 {
203        let translated = translate_column(self.id(), self.default_name());
204        translated.len().max(match self {
205            ObjectColumn::Key => 40,
206            ObjectColumn::Type => 10,
207            ObjectColumn::LastModified => UTC_TIMESTAMP_WIDTH as usize,
208            ObjectColumn::Size => 15,
209            ObjectColumn::StorageClass => 15,
210        }) as u16
211    }
212
213    fn render(&self, item: &Object) -> (String, Style) {
214        let text = match self {
215            ObjectColumn::Key => {
216                let icon = if item.is_prefix { "📁" } else { "📄" };
217                format!("{} {}", icon, item.key)
218            }
219            ObjectColumn::Type => {
220                if item.is_prefix {
221                    "Folder".to_string()
222                } else {
223                    "File".to_string()
224                }
225            }
226            ObjectColumn::LastModified => {
227                if item.last_modified.is_empty() {
228                    String::new()
229                } else {
230                    format!(
231                        "{} (UTC)",
232                        item.last_modified
233                            .split('T')
234                            .next()
235                            .unwrap_or(&item.last_modified)
236                    )
237                }
238            }
239            ObjectColumn::Size => {
240                if item.is_prefix {
241                    String::new()
242                } else {
243                    format_bytes(item.size)
244                }
245            }
246            ObjectColumn::StorageClass => {
247                if item.storage_class.is_empty() {
248                    String::new()
249                } else {
250                    item.storage_class
251                        .chars()
252                        .next()
253                        .unwrap()
254                        .to_uppercase()
255                        .to_string()
256                        + &item.storage_class[1..].to_lowercase()
257                }
258            }
259        };
260        (text, Style::default())
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_bucket_column_ids_have_correct_prefix() {
270        for col in BucketColumn::all() {
271            assert!(
272                col.id().starts_with("column.s3.bucket."),
273                "BucketColumn ID '{}' should start with 'column.s3.bucket.'",
274                col.id()
275            );
276        }
277    }
278
279    #[test]
280    fn test_object_column_ids_have_correct_prefix() {
281        for col in ObjectColumn::all() {
282            assert!(
283                col.id().starts_with("column.s3.object."),
284                "ObjectColumn ID '{}' should start with 'column.s3.object.'",
285                col.id()
286            );
287        }
288    }
289}