Skip to main content

raps_cli/commands/object/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Object management commands
5//!
6//! Commands for uploading, downloading, listing, and deleting objects in OSS buckets.
7
8mod copy;
9mod download;
10mod upload;
11
12use anyhow::Result;
13use clap::Subcommand;
14use std::path::PathBuf;
15
16use crate::output::OutputFormat;
17use raps_kernel::prompts;
18use raps_oss::OssClient;
19
20use copy::{batch_copy_objects, batch_rename_objects, copy_object, rename_object};
21use download::{delete_object, download_object, get_signed_url, list_objects, object_info};
22use upload::{upload_batch, upload_object};
23
24#[derive(Debug, Subcommand)]
25pub enum ObjectCommands {
26    /// Upload a file to a bucket (use `-` to read from stdin)
27    Upload {
28        /// Bucket key
29        bucket: Option<String>,
30
31        /// Path to the file to upload, or `-` for stdin
32        file: PathBuf,
33
34        /// Object key (defaults to filename; required when reading from stdin)
35        #[arg(short, long)]
36        key: Option<String>,
37
38        /// Resume interrupted upload (for large files)
39        #[arg(short, long)]
40        resume: bool,
41    },
42
43    /// Upload multiple files in parallel
44    #[command(name = "upload-batch")]
45    UploadBatch {
46        /// Bucket key
47        bucket: Option<String>,
48
49        /// Files to upload
50        files: Vec<PathBuf>,
51
52        /// Number of parallel uploads (default: 4)
53        #[arg(short, long, default_value = "4")]
54        parallel: usize,
55
56        /// Resume a previously interrupted batch upload
57        #[arg(long)]
58        resume: bool,
59    },
60
61    /// Download an object from a bucket (use `--out-file -` to write to stdout)
62    Download {
63        /// Bucket key
64        bucket: Option<String>,
65
66        /// Object key to download
67        object: Option<String>,
68
69        /// Output file path (defaults to object key; use `-` for stdout)
70        #[arg(long = "out-file")]
71        out_file: Option<PathBuf>,
72    },
73
74    /// List objects in a bucket
75    List {
76        /// Bucket key
77        bucket: Option<String>,
78    },
79
80    /// Delete an object from a bucket
81    Delete {
82        /// Bucket key
83        bucket: Option<String>,
84
85        /// Object key to delete
86        object: Option<String>,
87
88        /// Skip confirmation prompt
89        #[arg(short = 'y', long)]
90        yes: bool,
91    },
92
93    /// Get a signed S3 URL for direct download (bypasses OSS servers)
94    SignedUrl {
95        /// Bucket key
96        bucket: String,
97
98        /// Object key
99        object: String,
100
101        /// Expiration time in minutes (1-60, default 2)
102        #[arg(short, long)]
103        minutes: Option<u32>,
104    },
105
106    /// Get detailed information about an object
107    Info {
108        /// Bucket key
109        bucket: String,
110        /// Object key
111        object: String,
112    },
113
114    /// Copy an object to another bucket or key
115    Copy {
116        /// Source bucket key
117        #[arg(long)]
118        source_bucket: String,
119        /// Source object key
120        #[arg(long)]
121        source_object: String,
122        /// Destination bucket key
123        #[arg(long)]
124        dest_bucket: String,
125        /// Destination object key (defaults to source object key)
126        #[arg(long)]
127        dest_object: Option<String>,
128    },
129
130    /// Rename an object within a bucket
131    Rename {
132        /// Bucket key
133        bucket: String,
134        /// Current object key
135        object: String,
136        /// New object key
137        #[arg(long)]
138        new_key: String,
139    },
140
141    /// Batch copy objects from one bucket to another
142    #[command(name = "batch-copy")]
143    BatchCopy {
144        /// Source bucket key
145        source_bucket: String,
146        /// Destination bucket key
147        dest_bucket: String,
148        /// Filter objects by key prefix
149        #[arg(long)]
150        prefix: Option<String>,
151        /// Comma-separated specific object keys to copy
152        #[arg(long)]
153        keys: Option<String>,
154    },
155
156    /// Batch rename objects within a bucket
157    #[command(name = "batch-rename")]
158    BatchRename {
159        /// Bucket key
160        bucket: String,
161        /// Pattern to match in object keys
162        #[arg(long)]
163        from: String,
164        /// Replacement pattern for matched keys
165        #[arg(long)]
166        to: String,
167    },
168}
169
170impl ObjectCommands {
171    pub async fn execute(self, client: &OssClient, output_format: OutputFormat) -> Result<()> {
172        match self {
173            ObjectCommands::Upload {
174                bucket,
175                file,
176                key,
177                resume,
178            } => upload_object(client, bucket, file, key, resume, output_format).await,
179            ObjectCommands::UploadBatch {
180                bucket,
181                files,
182                parallel,
183                resume,
184            } => upload_batch(client, bucket, files, parallel, resume, output_format).await,
185            ObjectCommands::Download {
186                bucket,
187                object,
188                out_file,
189            } => download_object(client, bucket, object, out_file, output_format).await,
190            ObjectCommands::List { bucket } => list_objects(client, bucket, output_format).await,
191            ObjectCommands::Delete {
192                bucket,
193                object,
194                yes,
195            } => delete_object(client, bucket, object, yes, output_format).await,
196            ObjectCommands::SignedUrl {
197                bucket,
198                object,
199                minutes,
200            } => get_signed_url(client, &bucket, &object, minutes, output_format).await,
201            ObjectCommands::Info { bucket, object } => {
202                object_info(client, &bucket, &object, output_format).await
203            }
204            ObjectCommands::Copy {
205                source_bucket,
206                source_object,
207                dest_bucket,
208                dest_object,
209            } => {
210                copy_object(
211                    client,
212                    &source_bucket,
213                    &source_object,
214                    &dest_bucket,
215                    dest_object.as_deref(),
216                    output_format,
217                )
218                .await
219            }
220            ObjectCommands::Rename {
221                bucket,
222                object,
223                new_key,
224            } => rename_object(client, &bucket, &object, &new_key, output_format).await,
225            ObjectCommands::BatchCopy {
226                source_bucket,
227                dest_bucket,
228                prefix,
229                keys,
230            } => {
231                batch_copy_objects(
232                    client,
233                    &source_bucket,
234                    &dest_bucket,
235                    prefix,
236                    keys,
237                    output_format,
238                )
239                .await
240            }
241            ObjectCommands::BatchRename { bucket, from, to } => {
242                batch_rename_objects(client, &bucket, &from, &to, output_format).await
243            }
244        }
245    }
246}
247
248pub(super) async fn select_bucket(client: &OssClient, provided: Option<String>) -> Result<String> {
249    match provided {
250        Some(b) => Ok(b),
251        None => {
252            let buckets = client.list_buckets().await?;
253            if buckets.is_empty() {
254                anyhow::bail!("No buckets found. Create a bucket first using 'raps bucket create'");
255            }
256
257            let bucket_keys: Vec<String> = buckets.iter().map(|b| b.bucket_key.clone()).collect();
258
259            let selection = prompts::select("Select bucket", &bucket_keys)?;
260            Ok(bucket_keys[selection].clone())
261        }
262    }
263}
264
265/// Format file size in human-readable format
266pub(super) fn format_size(bytes: u64) -> String {
267    const KB: u64 = 1024;
268    const MB: u64 = KB * 1024;
269    const GB: u64 = MB * 1024;
270
271    if bytes >= GB {
272        format!("{:.2} GB", bytes as f64 / GB as f64)
273    } else if bytes >= MB {
274        format!("{:.2} MB", bytes as f64 / MB as f64)
275    } else if bytes >= KB {
276        format!("{:.2} KB", bytes as f64 / KB as f64)
277    } else {
278        format!("{} B", bytes)
279    }
280}
281
282/// Truncate string with ellipsis
283pub(super) fn truncate_str(s: &str, max_len: usize) -> String {
284    if s.len() <= max_len {
285        s.to_string()
286    } else {
287        format!("{}...", &s[..max_len - 3])
288    }
289}