Skip to main content

raps_cli/commands/
folder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Folder management commands
5//!
6//! Commands for listing, creating, and managing folders (requires 3-legged auth).
7
8use anyhow::{Context, Result};
9use clap::Subcommand;
10use colored::Colorize;
11use dialoguer::Input;
12#[allow(unused_imports)]
13use raps_kernel::prompts;
14use serde::Serialize;
15
16use crate::commands::interactive;
17use crate::commands::tracked::tracked_op;
18
19use crate::output::OutputFormat;
20use raps_acc::permissions::FolderPermissionsClient;
21use raps_dm::DataManagementClient;
22// use raps_kernel::output::OutputFormat;
23
24#[derive(Debug, Subcommand)]
25pub enum FolderCommands {
26    /// List folder contents
27    List {
28        /// Project ID
29        project_id: Option<String>,
30        /// Folder ID
31        folder_id: Option<String>,
32        /// Hub ID (for interactive mode)
33        #[arg(long, hide = true)]
34        hub_id: Option<String>,
35    },
36
37    /// Create a new folder
38    Create {
39        /// Project ID
40        project_id: Option<String>,
41        /// Parent folder ID
42        parent_folder_id: Option<String>,
43        /// Folder name (interactive if not provided)
44        #[arg(short, long)]
45        name: Option<String>,
46        /// Hub ID (for interactive mode)
47        #[arg(long, hide = true)]
48        hub_id: Option<String>,
49    },
50
51    /// Rename a folder
52    Rename {
53        /// Project ID
54        project_id: Option<String>,
55        /// Folder ID
56        folder_id: Option<String>,
57        /// New folder name (interactive if not provided)
58        #[arg(short, long)]
59        name: Option<String>,
60        /// Hub ID (for interactive mode)
61        #[arg(long, hide = true)]
62        hub_id: Option<String>,
63    },
64
65    /// Delete a folder
66    Delete {
67        /// Project ID
68        project_id: Option<String>,
69        /// Folder ID
70        folder_id: Option<String>,
71        /// Hub ID (for interactive mode)
72        #[arg(long, hide = true)]
73        hub_id: Option<String>,
74    },
75
76    /// Show permissions (rights) for a folder
77    Rights {
78        /// Project ID
79        project_id: Option<String>,
80        /// Folder ID
81        folder_id: Option<String>,
82        /// Hub ID (for interactive mode)
83        #[arg(long, hide = true)]
84        hub_id: Option<String>,
85    },
86}
87
88impl FolderCommands {
89    pub async fn execute(
90        self,
91        client: &DataManagementClient,
92        permissions_client: &FolderPermissionsClient,
93        output_format: OutputFormat,
94    ) -> Result<()> {
95        match self {
96            FolderCommands::List {
97                project_id,
98                folder_id,
99                hub_id,
100            } => {
101                let (p_id, f_id) =
102                    resolve_folder_args(client, hub_id, project_id, folder_id).await?;
103                list_folder_contents(client, &p_id, &f_id, output_format).await
104            }
105            FolderCommands::Create {
106                project_id,
107                parent_folder_id,
108                name,
109                hub_id,
110            } => {
111                let (p_id, f_id) =
112                    resolve_folder_args(client, hub_id, project_id, parent_folder_id).await?;
113                create_folder(client, &p_id, &f_id, name, output_format).await
114            }
115            FolderCommands::Rename {
116                project_id,
117                folder_id,
118                name,
119                hub_id,
120            } => {
121                let (p_id, f_id) =
122                    resolve_folder_args(client, hub_id, project_id, folder_id).await?;
123                rename_folder(client, &p_id, &f_id, name, output_format).await
124            }
125            FolderCommands::Delete {
126                project_id,
127                folder_id,
128                hub_id,
129            } => {
130                let (p_id, f_id) =
131                    resolve_folder_args(client, hub_id, project_id, folder_id).await?;
132                if !raps_kernel::interactive::should_proceed_destructive("delete this folder") {
133                    println!("Operation cancelled.");
134                    return Ok(());
135                }
136                delete_folder(client, &p_id, &f_id, output_format).await
137            }
138            FolderCommands::Rights {
139                project_id,
140                folder_id,
141                hub_id,
142            } => {
143                let (p_id, f_id) =
144                    resolve_folder_args(client, hub_id, project_id, folder_id).await?;
145                folder_rights(permissions_client, &p_id, &f_id, output_format).await
146            }
147        }
148    }
149}
150
151async fn resolve_folder_args(
152    client: &DataManagementClient,
153    opt_hub_id: Option<String>,
154    opt_project_id: Option<String>,
155    opt_folder_id: Option<String>,
156) -> Result<(String, String)> {
157    let hub_id = match (&opt_hub_id, &opt_project_id, &opt_folder_id) {
158        (Some(h), _, _) => h.clone(),
159        (None, Some(_), Some(_)) => String::new(), // Not needed if both P and F are provided
160        (None, _, _) => interactive::prompt_for_hub(client).await?,
161    };
162
163    let project_id = match opt_project_id {
164        Some(p) => p,
165        None => interactive::prompt_for_project(client, &hub_id).await?,
166    };
167
168    let folder_id = match opt_folder_id {
169        Some(f) => f,
170        None => interactive::prompt_for_folder(client, &hub_id, &project_id).await?,
171    };
172
173    Ok((project_id, folder_id))
174}
175
176#[derive(Serialize)]
177struct FolderItemOutput {
178    id: String,
179    name: String,
180    item_type: String,
181}
182
183async fn list_folder_contents(
184    client: &DataManagementClient,
185    project_id: &str,
186    folder_id: &str,
187    output_format: OutputFormat,
188) -> Result<()> {
189    let contents = tracked_op("Fetching folder contents", output_format, || async {
190        client
191            .list_folder_contents(project_id, folder_id)
192            .await
193            .context(format!(
194                "Failed to list folder '{}' contents. Verify folder ID and permissions",
195                folder_id
196            ))
197    })
198    .await?;
199
200    let items: Vec<FolderItemOutput> = contents
201        .iter()
202        .map(|item| {
203            let item_type = item
204                .get("type")
205                .and_then(|t| t.as_str())
206                .unwrap_or("unknown");
207            let id = item.get("id").and_then(|i| i.as_str()).unwrap_or("unknown");
208            let name = item
209                .get("attributes")
210                .and_then(|a| a.get("displayName").or(a.get("name")))
211                .and_then(|n| n.as_str())
212                .unwrap_or("Unnamed");
213
214            FolderItemOutput {
215                id: id.to_string(),
216                name: name.to_string(),
217                item_type: item_type.to_string(),
218            }
219        })
220        .collect();
221
222    if items.is_empty() {
223        match output_format {
224            OutputFormat::Table => println!("{}", "Folder is empty.".yellow()),
225            _ => {
226                output_format.write(&Vec::<FolderItemOutput>::new())?;
227            }
228        }
229        return Ok(());
230    }
231
232    match output_format {
233        OutputFormat::Table => {
234            println!("\n{}", "Folder Contents:".bold());
235            println!("{}", "-".repeat(80));
236
237            for item in &items {
238                let icon = if item.item_type == "folders" {
239                    "[folder]"
240                } else {
241                    "[file]"
242                };
243                let type_label = if item.item_type == "folders" {
244                    "folder"
245                } else {
246                    "item"
247                };
248
249                println!("  {} {} [{}]", icon, item.name.cyan(), type_label.dimmed());
250                println!("    {} {}", "ID:".dimmed(), item.id);
251            }
252
253            println!("{}", "-".repeat(80));
254        }
255        _ => {
256            output_format.write(&items)?;
257        }
258    }
259    Ok(())
260}
261
262#[derive(Serialize)]
263struct CreateFolderOutput {
264    success: bool,
265    id: String,
266    name: String,
267}
268
269async fn create_folder(
270    client: &DataManagementClient,
271    project_id: &str,
272    parent_folder_id: &str,
273    name: Option<String>,
274    output_format: OutputFormat,
275) -> Result<()> {
276    let folder_name = match name {
277        Some(n) => n,
278        None => {
279            // In non-interactive mode, require the name
280            if interactive::is_non_interactive() {
281                anyhow::bail!("Folder name is required in non-interactive mode. Use --name flag.");
282            }
283            Input::new()
284                .with_prompt("Enter folder name")
285                .interact_text()?
286        }
287    };
288
289    if output_format.supports_colors() {
290        println!("{}", "Creating folder...".dimmed());
291    }
292
293    let folder = client
294        .create_folder(project_id, parent_folder_id, &folder_name)
295        .await
296        .context(format!(
297            "Failed to create folder '{}' in project '{}'. Check parent folder permissions",
298            folder_name, project_id
299        ))?;
300
301    let output = CreateFolderOutput {
302        success: true,
303        id: folder.id.clone(),
304        name: folder.attributes.name.clone(),
305    };
306
307    match output_format {
308        OutputFormat::Table => {
309            println!("{} Folder created successfully!", "✓".green().bold());
310            println!("  {} {}", "Name:".bold(), output.name.cyan());
311            println!("  {} {}", "ID:".bold(), output.id);
312        }
313        _ => {
314            output_format.write(&output)?;
315        }
316    }
317
318    Ok(())
319}
320
321#[derive(Serialize)]
322struct RenameFolderOutput {
323    success: bool,
324    id: String,
325    name: String,
326}
327
328async fn rename_folder(
329    client: &DataManagementClient,
330    project_id: &str,
331    folder_id: &str,
332    new_name: Option<String>,
333    output_format: OutputFormat,
334) -> Result<()> {
335    let name_str = match new_name {
336        Some(n) => n,
337        None => {
338            if interactive::is_non_interactive() {
339                anyhow::bail!("Folder name is required in non-interactive mode. Use --name flag.");
340            }
341            Input::new()
342                .with_prompt("Enter new folder name")
343                .interact_text()?
344        }
345    };
346
347    if output_format.supports_colors() {
348        println!("{}", "Renaming folder...".dimmed());
349    }
350
351    let folder = client
352        .rename_folder(project_id, folder_id, &name_str)
353        .await
354        .context(format!(
355            "Failed to rename folder '{}'. Check permissions and that folder exists",
356            folder_id
357        ))?;
358
359    let output = RenameFolderOutput {
360        success: true,
361        id: folder.id.clone(),
362        name: folder.attributes.name.clone(),
363    };
364
365    match output_format {
366        OutputFormat::Table => {
367            println!("{} Folder renamed successfully!", "✓".green().bold());
368            println!("  {} {}", "Name:".bold(), output.name.cyan());
369            println!("  {} {}", "ID:".bold(), output.id);
370        }
371        _ => {
372            output_format.write(&output)?;
373        }
374    }
375
376    Ok(())
377}
378
379#[derive(Serialize)]
380struct DeleteFolderOutput {
381    success: bool,
382    folder_id: String,
383    message: String,
384}
385
386async fn delete_folder(
387    client: &DataManagementClient,
388    project_id: &str,
389    folder_id: &str,
390    output_format: OutputFormat,
391) -> Result<()> {
392    if output_format.supports_colors() {
393        println!("{}", "Deleting folder...".dimmed());
394    }
395
396    client
397        .delete_folder(project_id, folder_id)
398        .await
399        .context(format!(
400            "Failed to delete folder '{}'. Folder may not be empty or you lack permissions",
401            folder_id
402        ))?;
403
404    let output = DeleteFolderOutput {
405        success: true,
406        folder_id: folder_id.to_string(),
407        message: "Folder deleted successfully!".to_string(),
408    };
409
410    match output_format {
411        OutputFormat::Table => {
412            println!("{} {}", "✓".green().bold(), output.message);
413        }
414        _ => {
415            output_format.write(&output)?;
416        }
417    }
418    Ok(())
419}
420
421#[derive(Serialize)]
422struct FolderRightOutput {
423    subject_id: String,
424    subject_type: String,
425    actions: Vec<String>,
426    inherited_from: Option<String>,
427}
428
429async fn folder_rights(
430    client: &FolderPermissionsClient,
431    project_id: &str,
432    folder_id: &str,
433    output_format: OutputFormat,
434) -> Result<()> {
435    let permissions = tracked_op("Fetching folder permissions", output_format, || async {
436        client
437            .get_permissions(project_id, folder_id)
438            .await
439            .context(format!(
440                "Failed to get permissions for folder '{}'",
441                folder_id
442            ))
443    })
444    .await?;
445
446    let items: Vec<FolderRightOutput> = permissions
447        .iter()
448        .map(|p| FolderRightOutput {
449            subject_id: p.subject_id.clone(),
450            subject_type: p.subject_type.clone(),
451            actions: p.actions.clone(),
452            inherited_from: p.inherited_from.clone(),
453        })
454        .collect();
455
456    if items.is_empty() {
457        match output_format {
458            OutputFormat::Table => println!("{}", "No permissions found for this folder.".yellow()),
459            _ => {
460                output_format.write(&Vec::<FolderRightOutput>::new())?;
461            }
462        }
463        return Ok(());
464    }
465
466    match output_format {
467        OutputFormat::Table => {
468            println!("\n{}", "Folder Permissions:".bold());
469            println!("{}", "-".repeat(80));
470
471            for item in &items {
472                let inherited = item.inherited_from.as_deref().unwrap_or("direct");
473                println!(
474                    "  {} {} [{}]",
475                    item.subject_type.cyan(),
476                    item.subject_id,
477                    inherited.dimmed()
478                );
479                println!("    {} {}", "Actions:".dimmed(), item.actions.join(", "));
480            }
481
482            println!("{}", "-".repeat(80));
483        }
484        _ => {
485            output_format.write(&items)?;
486        }
487    }
488    Ok(())
489}