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 const ID_NAME: &'static str = "column.s3.bucket.name";
76 const ID_REGION: &'static str = "column.s3.bucket.region";
77 const ID_CREATION_DATE: &'static str = "column.s3.bucket.creation_date";
78
79 pub const fn id(&self) -> &'static str {
80 match self {
81 BucketColumn::Name => Self::ID_NAME,
82 BucketColumn::Region => Self::ID_REGION,
83 BucketColumn::CreationDate => Self::ID_CREATION_DATE,
84 }
85 }
86
87 pub const fn default_name(&self) -> &'static str {
88 match self {
89 BucketColumn::Name => "Name",
90 BucketColumn::Region => "Region",
91 BucketColumn::CreationDate => "Creation date",
92 }
93 }
94
95 pub const fn all() -> [BucketColumn; 3] {
96 [
97 BucketColumn::Name,
98 BucketColumn::Region,
99 BucketColumn::CreationDate,
100 ]
101 }
102
103 pub fn ids() -> Vec<ColumnId> {
104 Self::all().iter().map(|c| c.id()).collect()
105 }
106
107 pub fn from_id(id: &str) -> Option<Self> {
108 match id {
109 Self::ID_NAME => Some(BucketColumn::Name),
110 Self::ID_REGION => Some(BucketColumn::Region),
111 Self::ID_CREATION_DATE => Some(BucketColumn::CreationDate),
112 _ => None,
113 }
114 }
115
116 pub fn name(&self) -> String {
117 translate_column(self.id(), self.default_name())
118 }
119}
120
121impl TableColumn<Bucket> for BucketColumn {
122 fn name(&self) -> &str {
123 Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
124 }
125
126 fn width(&self) -> u16 {
127 let translated = translate_column(self.id(), self.default_name());
128 translated.len().max(match self {
129 BucketColumn::Name => 30,
130 BucketColumn::Region => 15,
131 BucketColumn::CreationDate => UTC_TIMESTAMP_WIDTH as usize,
132 }) as u16
133 }
134
135 fn render(&self, item: &Bucket) -> (String, Style) {
136 let text = match self {
137 BucketColumn::Name => item.name.clone(),
138 BucketColumn::Region => item.region.clone(),
139 BucketColumn::CreationDate => item.creation_date.clone(),
140 };
141 (text, Style::default())
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq)]
146pub enum ObjectColumn {
147 Key,
148 Type,
149 LastModified,
150 Size,
151 StorageClass,
152}
153
154impl ObjectColumn {
155 const ID_KEY: &'static str = "column.s3.object.key";
156 const ID_TYPE: &'static str = "column.s3.object.type";
157 const ID_LAST_MODIFIED: &'static str = "column.s3.object.last_modified";
158 const ID_SIZE: &'static str = "column.s3.object.size";
159 const ID_STORAGE_CLASS: &'static str = "column.s3.object.storage_class";
160
161 pub const fn id(&self) -> &'static str {
162 match self {
163 ObjectColumn::Key => Self::ID_KEY,
164 ObjectColumn::Type => Self::ID_TYPE,
165 ObjectColumn::LastModified => Self::ID_LAST_MODIFIED,
166 ObjectColumn::Size => Self::ID_SIZE,
167 ObjectColumn::StorageClass => Self::ID_STORAGE_CLASS,
168 }
169 }
170
171 pub const fn default_name(&self) -> &'static str {
172 match self {
173 ObjectColumn::Key => "Name",
174 ObjectColumn::Type => "Type",
175 ObjectColumn::LastModified => "Last modified",
176 ObjectColumn::Size => "Size",
177 ObjectColumn::StorageClass => "Storage class",
178 }
179 }
180
181 pub const fn all() -> [ObjectColumn; 5] {
182 [
183 ObjectColumn::Key,
184 ObjectColumn::Type,
185 ObjectColumn::LastModified,
186 ObjectColumn::Size,
187 ObjectColumn::StorageClass,
188 ]
189 }
190
191 pub fn from_id(id: &str) -> Option<Self> {
192 match id {
193 Self::ID_KEY => Some(ObjectColumn::Key),
194 Self::ID_TYPE => Some(ObjectColumn::Type),
195 Self::ID_LAST_MODIFIED => Some(ObjectColumn::LastModified),
196 Self::ID_SIZE => Some(ObjectColumn::Size),
197 Self::ID_STORAGE_CLASS => Some(ObjectColumn::StorageClass),
198 _ => None,
199 }
200 }
201
202 pub fn name(&self) -> String {
203 translate_column(self.id(), self.default_name())
204 }
205}
206
207impl TableColumn<Object> for ObjectColumn {
208 fn name(&self) -> &str {
209 Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
210 }
211
212 fn width(&self) -> u16 {
213 let translated = translate_column(self.id(), self.default_name());
214 translated.len().max(match self {
215 ObjectColumn::Key => 40,
216 ObjectColumn::Type => 10,
217 ObjectColumn::LastModified => UTC_TIMESTAMP_WIDTH as usize,
218 ObjectColumn::Size => 15,
219 ObjectColumn::StorageClass => 15,
220 }) as u16
221 }
222
223 fn render(&self, item: &Object) -> (String, Style) {
224 let text = match self {
225 ObjectColumn::Key => {
226 let icon = if item.is_prefix { "📁" } else { "📄" };
227 format!("{} {}", icon, item.key)
228 }
229 ObjectColumn::Type => {
230 if item.is_prefix {
231 "Folder".to_string()
232 } else {
233 "File".to_string()
234 }
235 }
236 ObjectColumn::LastModified => {
237 if item.last_modified.is_empty() {
238 String::new()
239 } else {
240 format!(
241 "{} (UTC)",
242 item.last_modified
243 .split('T')
244 .next()
245 .unwrap_or(&item.last_modified)
246 )
247 }
248 }
249 ObjectColumn::Size => {
250 if item.is_prefix {
251 String::new()
252 } else {
253 format_bytes(item.size)
254 }
255 }
256 ObjectColumn::StorageClass => {
257 if item.storage_class.is_empty() {
258 String::new()
259 } else {
260 item.storage_class
261 .chars()
262 .next()
263 .unwrap()
264 .to_uppercase()
265 .to_string()
266 + &item.storage_class[1..].to_lowercase()
267 }
268 }
269 };
270 (text, Style::default())
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_bucket_column_ids_have_correct_prefix() {
280 for col in BucketColumn::all() {
281 assert!(
282 col.id().starts_with("column.s3.bucket."),
283 "BucketColumn ID '{}' should start with 'column.s3.bucket.'",
284 col.id()
285 );
286 }
287 }
288
289 #[test]
290 fn test_object_column_ids_have_correct_prefix() {
291 for col in ObjectColumn::all() {
292 assert!(
293 col.id().starts_with("column.s3.object."),
294 "ObjectColumn ID '{}' should start with 'column.s3.object.'",
295 col.id()
296 );
297 }
298 }
299}