Skip to main content

datalab_cli/commands/
files.rs

1use crate::client::DatalabClient;
2use crate::error::{DatalabError, Result};
3use crate::output::Progress;
4use clap::{Args, Subcommand};
5use serde_json::json;
6use std::fs;
7use std::path::PathBuf;
8
9#[derive(Subcommand, Debug)]
10pub enum FilesCommand {
11    /// Upload a file to Datalab storage
12    Upload(UploadArgs),
13    /// List uploaded files
14    List(ListArgs),
15    /// Get file metadata
16    Get(GetArgs),
17    /// Download a file
18    Download(DownloadArgs),
19    /// Delete a file
20    Delete(DeleteArgs),
21}
22
23#[derive(Args, Debug)]
24pub struct UploadArgs {
25    /// File to upload
26    #[arg(value_name = "FILE")]
27    pub file: PathBuf,
28
29    /// Request timeout in seconds
30    #[arg(long, default_value = "300", value_name = "SECS")]
31    pub timeout: u64,
32}
33
34#[derive(Args, Debug)]
35pub struct ListArgs {
36    /// Results per page (max 100)
37    #[arg(long, default_value = "50", value_name = "N")]
38    pub limit: u32,
39
40    /// Pagination offset
41    #[arg(long, default_value = "0", value_name = "N")]
42    pub offset: u32,
43
44    /// Request timeout in seconds
45    #[arg(long, default_value = "60", value_name = "SECS")]
46    pub timeout: u64,
47}
48
49#[derive(Args, Debug)]
50pub struct GetArgs {
51    /// File ID
52    #[arg(value_name = "FILE_ID")]
53    pub file_id: String,
54
55    /// Request timeout in seconds
56    #[arg(long, default_value = "60", value_name = "SECS")]
57    pub timeout: u64,
58}
59
60#[derive(Args, Debug)]
61pub struct DownloadArgs {
62    /// File ID
63    #[arg(value_name = "FILE_ID")]
64    pub file_id: String,
65
66    /// Output file path
67    #[arg(long, short, value_name = "FILE")]
68    pub output: Option<PathBuf>,
69
70    /// URL validity duration in seconds (max 86400)
71    #[arg(long, default_value = "3600", value_name = "SECS")]
72    pub expires_in: u32,
73
74    /// Request timeout in seconds
75    #[arg(long, default_value = "300", value_name = "SECS")]
76    pub timeout: u64,
77}
78
79#[derive(Args, Debug)]
80pub struct DeleteArgs {
81    /// File ID
82    #[arg(value_name = "FILE_ID")]
83    pub file_id: String,
84
85    /// Request timeout in seconds
86    #[arg(long, default_value = "60", value_name = "SECS")]
87    pub timeout: u64,
88}
89
90pub async fn execute(cmd: FilesCommand, progress: &Progress) -> Result<()> {
91    match cmd {
92        FilesCommand::Upload(args) => upload(args, progress).await,
93        FilesCommand::List(args) => list(args, progress).await,
94        FilesCommand::Get(args) => get(args, progress).await,
95        FilesCommand::Download(args) => download(args, progress).await,
96        FilesCommand::Delete(args) => delete(args, progress).await,
97    }
98}
99
100async fn upload(args: UploadArgs, progress: &Progress) -> Result<()> {
101    let client = DatalabClient::new(Some(args.timeout))?;
102
103    if !args.file.exists() {
104        return Err(DatalabError::FileNotFound(args.file.clone()));
105    }
106
107    progress.start("upload", Some(&args.file.to_string_lossy()));
108
109    let filename = args
110        .file
111        .file_name()
112        .and_then(|n| n.to_str())
113        .unwrap_or("file")
114        .to_string();
115
116    let content_type = mime_guess::from_path(&args.file)
117        .first_or_octet_stream()
118        .to_string();
119
120    let request_body = json!({
121        "filename": filename,
122        "content_type": content_type,
123    });
124
125    let response = client.post_json("files/upload", &request_body).await?;
126
127    let upload_url = response
128        .get("upload_url")
129        .and_then(|v| v.as_str())
130        .ok_or_else(|| DatalabError::ProcessingFailed("No upload URL returned".to_string()))?;
131
132    let file_id = response
133        .get("file_id")
134        .and_then(|v| v.as_str())
135        .ok_or_else(|| DatalabError::ProcessingFailed("No file ID returned".to_string()))?;
136
137    client
138        .upload_file_to_presigned_url(upload_url, &args.file, &content_type, progress)
139        .await?;
140
141    let confirm_response = client.get(&format!("files/{}/confirm", file_id)).await?;
142
143    println!(
144        "{}",
145        serde_json::to_string_pretty(&json!({
146            "file_id": file_id,
147            "reference": response.get("reference"),
148            "upload_status": confirm_response.get("upload_status"),
149            "file_size": confirm_response.get("file_size"),
150        }))?
151    );
152
153    Ok(())
154}
155
156async fn list(args: ListArgs, progress: &Progress) -> Result<()> {
157    let client = DatalabClient::new(Some(args.timeout))?;
158
159    progress.start("list-files", None);
160
161    let path = format!("files?limit={}&offset={}", args.limit, args.offset);
162    let response = client.get(&path).await?;
163
164    println!("{}", serde_json::to_string_pretty(&response)?);
165
166    Ok(())
167}
168
169async fn get(args: GetArgs, progress: &Progress) -> Result<()> {
170    let client = DatalabClient::new(Some(args.timeout))?;
171
172    progress.start("get-file", Some(&args.file_id));
173
174    let path = format!("files/{}", args.file_id);
175    let response = client.get(&path).await?;
176
177    println!("{}", serde_json::to_string_pretty(&response)?);
178
179    Ok(())
180}
181
182async fn download(args: DownloadArgs, progress: &Progress) -> Result<()> {
183    let client = DatalabClient::new(Some(args.timeout))?;
184
185    progress.start("download", Some(&args.file_id));
186
187    let path = format!(
188        "files/{}/download?expires_in={}",
189        args.file_id, args.expires_in
190    );
191    let response = client.get(&path).await?;
192
193    if let Some(output_path) = args.output {
194        let download_url = response
195            .get("download_url")
196            .and_then(|v| v.as_str())
197            .ok_or_else(|| {
198                DatalabError::ProcessingFailed("No download URL returned".to_string())
199            })?;
200
201        let http_client = reqwest::Client::new();
202        let file_response = http_client.get(download_url).send().await?;
203
204        if !file_response.status().is_success() {
205            return Err(DatalabError::ApiError {
206                status: file_response.status().as_u16(),
207                message: "Failed to download file".to_string(),
208            });
209        }
210
211        let bytes = file_response.bytes().await?;
212        fs::write(&output_path, &bytes)?;
213
214        println!(
215            "{}",
216            serde_json::to_string_pretty(&json!({
217                "downloaded_to": output_path.to_string_lossy(),
218                "size": bytes.len(),
219            }))?
220        );
221    } else {
222        println!("{}", serde_json::to_string_pretty(&response)?);
223    }
224
225    Ok(())
226}
227
228async fn delete(args: DeleteArgs, progress: &Progress) -> Result<()> {
229    let client = DatalabClient::new(Some(args.timeout))?;
230
231    progress.start("delete", Some(&args.file_id));
232
233    let path = format!("files/{}", args.file_id);
234    client.delete(&path).await?;
235
236    println!(
237        "{}",
238        serde_json::to_string_pretty(&json!({
239            "deleted": true,
240            "file_id": args.file_id,
241        }))?
242    );
243
244    Ok(())
245}