Skip to main content

raps_cli/commands/
item.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Item (file) management commands
5//!
6//! Commands for listing, viewing, and downloading items (requires 3-legged auth).
7
8use anyhow::Result;
9use clap::Subcommand;
10use colored::Colorize;
11use serde::Serialize;
12
13use crate::commands::tracked::tracked_op;
14use crate::output::OutputFormat;
15use raps_dm::DataManagementClient;
16// use raps_kernel::output::OutputFormat;
17
18#[derive(Debug, Subcommand)]
19pub enum ItemCommands {
20    /// Get item details
21    Info {
22        /// Project ID
23        project_id: String,
24        /// Item ID
25        item_id: String,
26    },
27
28    /// List item versions
29    Versions {
30        /// Project ID
31        project_id: String,
32        /// Item ID
33        item_id: String,
34    },
35
36    /// Create an item from an OSS object (bind OSS upload to ACC folder)
37    #[command(name = "create-from-oss")]
38    CreateFromOss {
39        /// Project ID (with "b." prefix)
40        project_id: String,
41        /// Target folder ID (get from folder list)
42        folder_id: String,
43        /// Display name for the item
44        #[arg(short, long)]
45        name: String,
46        /// OSS object ID (urn:adsk.objects:os.object:bucket/objectkey)
47        #[arg(long)]
48        object_id: String,
49    },
50
51    /// Delete an item from a project
52    Delete {
53        /// Project ID
54        project_id: String,
55        /// Item ID
56        item_id: String,
57    },
58
59    /// Rename an item (update display name)
60    Rename {
61        /// Project ID
62        project_id: String,
63        /// Item ID
64        item_id: String,
65        /// New display name
66        #[arg(short, long)]
67        name: String,
68    },
69}
70
71impl ItemCommands {
72    pub async fn execute(
73        self,
74        client: &DataManagementClient,
75        output_format: OutputFormat,
76    ) -> Result<()> {
77        match self {
78            ItemCommands::Info {
79                project_id,
80                item_id,
81            } => item_info(client, &project_id, &item_id, output_format).await,
82            ItemCommands::Versions {
83                project_id,
84                item_id,
85            } => list_versions(client, &project_id, &item_id, output_format).await,
86            ItemCommands::CreateFromOss {
87                project_id,
88                folder_id,
89                name,
90                object_id,
91            } => {
92                create_from_oss(
93                    client,
94                    &project_id,
95                    &folder_id,
96                    &name,
97                    &object_id,
98                    output_format,
99                )
100                .await
101            }
102            ItemCommands::Delete {
103                project_id,
104                item_id,
105            } => delete_item(client, &project_id, &item_id, output_format).await,
106            ItemCommands::Rename {
107                project_id,
108                item_id,
109                name,
110            } => rename_item(client, &project_id, &item_id, &name, output_format).await,
111        }
112    }
113}
114
115#[derive(Serialize)]
116struct ItemInfoOutput {
117    id: String,
118    name: String,
119    item_type: String,
120    create_time: Option<String>,
121    modified_time: Option<String>,
122    extension_type: Option<String>,
123    extension_version: Option<String>,
124}
125
126async fn item_info(
127    client: &DataManagementClient,
128    project_id: &str,
129    item_id: &str,
130    output_format: OutputFormat,
131) -> Result<()> {
132    let item = tracked_op("Fetching item details", output_format, || {
133        client.get_item(project_id, item_id)
134    })
135    .await?;
136
137    let extension_type = item
138        .attributes
139        .extension
140        .as_ref()
141        .and_then(|e| e.extension_type.clone());
142    let extension_version = item
143        .attributes
144        .extension
145        .as_ref()
146        .and_then(|e| e.version.clone());
147
148    let output = ItemInfoOutput {
149        id: item.id.clone(),
150        name: item.attributes.display_name.clone(),
151        item_type: item.item_type.clone(),
152        create_time: item.attributes.create_time.clone(),
153        modified_time: item.attributes.last_modified_time.clone(),
154        extension_type,
155        extension_version,
156    };
157
158    match output_format {
159        OutputFormat::Table => {
160            println!("\n{}", "Item Details".bold());
161            println!("{}", "-".repeat(60));
162            println!("  {} {}", "Name:".bold(), output.name.cyan());
163            println!("  {} {}", "ID:".bold(), output.id);
164            println!("  {} {}", "Type:".bold(), output.item_type);
165
166            if let Some(ref create_time) = output.create_time {
167                println!("  {} {}", "Created:".bold(), create_time);
168            }
169
170            if let Some(ref modified_time) = output.modified_time {
171                println!("  {} {}", "Modified:".bold(), modified_time);
172            }
173
174            if let Some(ref ext_type) = output.extension_type {
175                println!("  {} {}", "Extension:".bold(), ext_type);
176            }
177            if let Some(version) = output.extension_version {
178                println!("  {} {}", "Ext Version:".bold(), version);
179            }
180
181            println!("{}", "-".repeat(60));
182            println!(
183                "\n{}",
184                "Use 'raps item versions' to see version history".dimmed()
185            );
186        }
187        _ => {
188            output_format.write(&output)?;
189        }
190    }
191    Ok(())
192}
193
194#[derive(Serialize)]
195struct VersionOutput {
196    version_number: Option<i32>,
197    name: String,
198    size: Option<u64>,
199    size_human: Option<String>,
200    create_time: Option<String>,
201}
202
203async fn list_versions(
204    client: &DataManagementClient,
205    project_id: &str,
206    item_id: &str,
207    output_format: OutputFormat,
208) -> Result<()> {
209    let versions = tracked_op("Fetching item versions", output_format, || {
210        client.get_item_versions(project_id, item_id)
211    })
212    .await?;
213
214    let version_outputs: Vec<VersionOutput> = versions
215        .iter()
216        .map(|v| {
217            let name = v
218                .attributes
219                .display_name
220                .as_ref()
221                .or(Some(&v.attributes.name))
222                .cloned()
223                .unwrap_or_default();
224            VersionOutput {
225                version_number: v.attributes.version_number,
226                name,
227                size: v.attributes.storage_size.map(|s| s as u64),
228                size_human: v.attributes.storage_size.map(|s| format_size(s as u64)),
229                create_time: v.attributes.create_time.clone(),
230            }
231        })
232        .collect();
233
234    if version_outputs.is_empty() {
235        match output_format {
236            OutputFormat::Table => println!("{}", "No versions found.".yellow()),
237            _ => {
238                output_format.write(&Vec::<VersionOutput>::new())?;
239            }
240        }
241        return Ok(());
242    }
243
244    match output_format {
245        OutputFormat::Table => {
246            println!("\n{}", "Item Versions:".bold());
247            println!("{}", "-".repeat(80));
248            println!(
249                "{:<6} {:<40} {:>12} {}",
250                "Ver".bold(),
251                "Name".bold(),
252                "Size".bold(),
253                "Created".bold()
254            );
255            println!("{}", "-".repeat(80));
256
257            for version in &version_outputs {
258                let ver_num = version
259                    .version_number
260                    .map(|n| n.to_string())
261                    .unwrap_or_else(|| "-".to_string());
262                let name = truncate_str(&version.name, 40);
263                let size = version.size_human.as_deref().unwrap_or("-");
264                let created = version.create_time.as_deref().unwrap_or("-");
265
266                println!(
267                    "{:<6} {:<40} {:>12} {}",
268                    ver_num.cyan(),
269                    name,
270                    size,
271                    created.dimmed()
272                );
273            }
274
275            println!("{}", "-".repeat(80));
276        }
277        _ => {
278            output_format.write(&version_outputs)?;
279        }
280    }
281    Ok(())
282}
283
284/// Format file size in human-readable format
285fn format_size(bytes: u64) -> String {
286    const KB: u64 = 1024;
287    const MB: u64 = KB * 1024;
288    const GB: u64 = MB * 1024;
289
290    if bytes >= GB {
291        format!("{:.2} GB", bytes as f64 / GB as f64)
292    } else if bytes >= MB {
293        format!("{:.2} MB", bytes as f64 / MB as f64)
294    } else if bytes >= KB {
295        format!("{:.2} KB", bytes as f64 / KB as f64)
296    } else {
297        format!("{} B", bytes)
298    }
299}
300
301/// Truncate string with ellipsis
302fn truncate_str(s: &str, max_len: usize) -> String {
303    if s.len() <= max_len {
304        s.to_string()
305    } else {
306        format!("{}...", &s[..max_len - 3])
307    }
308}
309
310#[derive(Serialize)]
311struct CreateFromOssOutput {
312    success: bool,
313    item_id: String,
314    name: String,
315    message: String,
316}
317
318async fn create_from_oss(
319    client: &DataManagementClient,
320    project_id: &str,
321    folder_id: &str,
322    name: &str,
323    object_id: &str,
324    output_format: OutputFormat,
325) -> Result<()> {
326    if output_format.supports_colors() {
327        println!("{}", "Creating item from OSS object...".dimmed());
328        println!("  {} {}", "Project:".bold(), project_id);
329        println!("  {} {}", "Folder:".bold(), folder_id);
330        println!("  {} {}", "Name:".bold(), name.cyan());
331        println!("  {} {}", "Object ID:".bold(), object_id.dimmed());
332    }
333
334    // Create the item using the Data Management API
335    let item = client
336        .create_item_from_storage(project_id, folder_id, name, object_id)
337        .await?;
338
339    let output = CreateFromOssOutput {
340        success: true,
341        item_id: item.id.clone(),
342        name: item.attributes.display_name.clone(),
343        message: format!("Item '{}' created successfully from OSS object", name),
344    };
345
346    match output_format {
347        OutputFormat::Table => {
348            println!("\n{} {}", "✓".green().bold(), output.message);
349            println!("  {} {}", "Item ID:".bold(), output.item_id);
350            println!("  {} {}", "Name:".bold(), output.name.cyan());
351        }
352        _ => {
353            output_format.write(&output)?;
354        }
355    }
356
357    Ok(())
358}
359
360async fn delete_item(
361    client: &DataManagementClient,
362    project_id: &str,
363    item_id: &str,
364    output_format: OutputFormat,
365) -> Result<()> {
366    if output_format.supports_colors() {
367        println!("{}", "Deleting item...".dimmed());
368    }
369
370    client.delete_item(project_id, item_id).await?;
371
372    match output_format {
373        OutputFormat::Table => {
374            println!("\n{} Item deleted successfully!", "✓".green().bold());
375            println!("  {} {}", "Item ID:".bold(), item_id.cyan());
376        }
377        _ => {
378            output_format.write(&serde_json::json!({
379                "id": item_id,
380                "deleted": true
381            }))?;
382        }
383    }
384
385    Ok(())
386}
387
388#[derive(Serialize)]
389struct RenameItemOutput {
390    id: String,
391    name: String,
392    renamed: bool,
393}
394
395async fn rename_item(
396    client: &DataManagementClient,
397    project_id: &str,
398    item_id: &str,
399    new_name: &str,
400    output_format: OutputFormat,
401) -> Result<()> {
402    if output_format.supports_colors() {
403        println!("{}", "Renaming item...".dimmed());
404    }
405
406    let item = client.rename_item(project_id, item_id, new_name).await?;
407
408    let output = RenameItemOutput {
409        id: item.id.clone(),
410        name: item.attributes.display_name.clone(),
411        renamed: true,
412    };
413
414    match output_format {
415        OutputFormat::Table => {
416            println!("\n{} Item renamed successfully!", "✓".green().bold());
417            println!("  {} {}", "Item ID:".bold(), output.id.cyan());
418            println!("  {} {}", "Name:".bold(), output.name.cyan());
419        }
420        _ => {
421            output_format.write(&output)?;
422        }
423    }
424
425    Ok(())
426}