systemprompt_cli/commands/core/files/
upload.rs1use std::path::{Path, PathBuf};
2
3use anyhow::{anyhow, Result};
4use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6use clap::Args;
7use sha2::{Digest, Sha256};
8use systemprompt_files::{FileUploadRequest, FileUploadService, FilesConfig};
9use systemprompt_identifiers::{ContextId, SessionId, UserId};
10use systemprompt_runtime::AppContext;
11use tokio::fs;
12
13use super::types::FileUploadOutput;
14use crate::shared::CommandResult;
15use crate::CliConfig;
16
17#[derive(Debug, Clone, Args)]
18pub struct UploadArgs {
19 #[arg(help = "Path to file to upload")]
20 pub file_path: PathBuf,
21
22 #[arg(long, help = "Context ID (required)")]
23 pub context: String,
24
25 #[arg(long, help = "User ID")]
26 pub user: Option<String>,
27
28 #[arg(long, help = "Session ID")]
29 pub session: Option<String>,
30
31 #[arg(long, help = "Mark as AI-generated content")]
32 pub ai: bool,
33}
34
35pub async fn execute(
36 args: UploadArgs,
37 _config: &CliConfig,
38) -> Result<CommandResult<FileUploadOutput>> {
39 let ctx = AppContext::new().await?;
40 let files_config = FilesConfig::get()?;
41 let service = FileUploadService::new(ctx.db_pool(), files_config.clone())?;
42
43 if !service.is_enabled() {
44 return Err(anyhow!("File uploads are disabled in configuration"));
45 }
46
47 let file_path = args
48 .file_path
49 .canonicalize()
50 .map_err(|e| anyhow!("File not found: {} - {}", args.file_path.display(), e))?;
51
52 let bytes = fs::read(&file_path).await?;
53 let bytes_base64 = STANDARD.encode(&bytes);
54 let digest = Sha256::digest(&bytes);
55 #[allow(clippy::expect_used)]
56 let checksum_sha256 = digest.iter().fold(String::with_capacity(64), |mut acc, b| {
57 use std::fmt::Write;
58 write!(acc, "{b:02x}").expect("write to String is infallible");
59 acc
60 });
61 let size_bytes = bytes.len() as i64;
62
63 let mime_type = detect_mime_type(&file_path);
64 let filename = file_path
65 .file_name()
66 .and_then(|n| n.to_str())
67 .map(String::from);
68
69 let context_id = ContextId::new(args.context);
70
71 let request = FileUploadRequest {
72 name: filename,
73 mime_type: mime_type.clone(),
74 bytes_base64,
75 context_id,
76 user_id: args.user.map(UserId::new),
77 session_id: args.session.map(SessionId::new),
78 trace_id: None,
79 };
80
81 let result = service.upload_file(request).await?;
82
83 let output = FileUploadOutput {
84 file_id: result.file_id,
85 path: result.path,
86 public_url: result.public_url,
87 size_bytes,
88 mime_type,
89 checksum_sha256,
90 };
91
92 Ok(CommandResult::card(output).with_title("File Uploaded"))
93}
94
95pub fn detect_mime_type(path: &Path) -> String {
96 let extension = path
97 .extension()
98 .and_then(|e| e.to_str())
99 .map(str::to_lowercase);
100
101 match extension.as_deref() {
102 Some("jpg" | "jpeg") => "image/jpeg".to_string(),
103 Some("png") => "image/png".to_string(),
104 Some("gif") => "image/gif".to_string(),
105 Some("webp") => "image/webp".to_string(),
106 Some("svg") => "image/svg+xml".to_string(),
107 Some("bmp") => "image/bmp".to_string(),
108 Some("tiff" | "tif") => "image/tiff".to_string(),
109 Some("ico") => "image/x-icon".to_string(),
110 Some("pdf") => "application/pdf".to_string(),
111 Some("doc") => "application/msword".to_string(),
112 Some("docx") => {
113 "application/vnd.openxmlformats-officedocument.wordprocessingml.document".to_string()
114 },
115 Some("xls") => "application/vnd.ms-excel".to_string(),
116 Some("xlsx") => {
117 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".to_string()
118 },
119 Some("ppt") => "application/vnd.ms-powerpoint".to_string(),
120 Some("pptx") => {
121 "application/vnd.openxmlformats-officedocument.presentationml.presentation".to_string()
122 },
123 Some("txt") => "text/plain".to_string(),
124 Some("csv") => "text/csv".to_string(),
125 Some("md") => "text/markdown".to_string(),
126 Some("html" | "htm") => "text/html".to_string(),
127 Some("json") => "application/json".to_string(),
128 Some("xml") => "application/xml".to_string(),
129 Some("rtf") => "application/rtf".to_string(),
130 Some("mp3") => "audio/mpeg".to_string(),
131 Some("wav") => "audio/wav".to_string(),
132 Some("ogg") => "audio/ogg".to_string(),
133 Some("aac") => "audio/aac".to_string(),
134 Some("flac") => "audio/flac".to_string(),
135 Some("m4a") => "audio/mp4".to_string(),
136 Some("mp4") => "video/mp4".to_string(),
137 Some("webm") => "video/webm".to_string(),
138 Some("mov") => "video/quicktime".to_string(),
139 Some("avi") => "video/x-msvideo".to_string(),
140 Some("mkv") => "video/x-matroska".to_string(),
141 _ => "application/octet-stream".to_string(),
142 }
143}