Skip to main content

systemprompt_cli/commands/core/files/
upload.rs

1//! `core files upload` command with extension-based MIME detection.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::{Path, PathBuf};
7
8use anyhow::{Result, anyhow};
9use base64::Engine;
10use base64::engine::general_purpose::STANDARD;
11use clap::Args;
12use sha2::{Digest, Sha256};
13use systemprompt_files::{FileUploadRequest, FileUploadService, FilesConfig};
14use systemprompt_identifiers::{ContextId, SessionId, UserId};
15use tokio::fs;
16
17use super::types::FileUploadOutput;
18use crate::context::CommandContext;
19use crate::shared::CommandOutput;
20
21#[derive(Debug, Clone, Args)]
22pub struct UploadArgs {
23    #[arg(help = "Path to file to upload")]
24    pub file_path: PathBuf,
25
26    #[arg(long, help = "Context ID (required)")]
27    pub context: String,
28
29    #[arg(long, help = "User ID")]
30    pub user: Option<String>,
31
32    #[arg(long, help = "Session ID")]
33    pub session: Option<String>,
34
35    #[arg(long, help = "Mark as AI-generated content")]
36    pub ai: bool,
37}
38
39pub async fn execute(args: UploadArgs, ctx: &CommandContext) -> Result<CommandOutput> {
40    let app = ctx.app_context().await?;
41    let files_config = FilesConfig::get()?;
42    let service = FileUploadService::new(app.db_pool(), files_config.clone())?;
43
44    if !service.is_enabled() {
45        return Err(anyhow!("File uploads are disabled in configuration"));
46    }
47
48    let file_path = args
49        .file_path
50        .canonicalize()
51        .map_err(|e| anyhow!("File not found: {} - {}", args.file_path.display(), e))?;
52
53    let bytes = fs::read(&file_path).await?;
54    let bytes_base64 = STANDARD.encode(&bytes);
55    let digest = Sha256::digest(&bytes);
56    let checksum_sha256 = digest.iter().fold(String::with_capacity(64), |mut acc, b| {
57        acc.push_str(&format!("{b:02x}"));
58        acc
59    });
60    let size_bytes = bytes.len() as i64;
61
62    let mime_type = detect_mime_type(&file_path);
63    let filename = file_path
64        .file_name()
65        .and_then(|n| n.to_str())
66        .map(String::from);
67
68    let context_id = ContextId::new(args.context);
69
70    let request = FileUploadRequest {
71        name: filename,
72        mime_type: mime_type.clone(),
73        bytes_base64,
74        context_id,
75        user_id: args.user.map(UserId::new),
76        session_id: args.session.map(SessionId::new),
77        trace_id: None,
78    };
79
80    let result = service.upload_file(request).await?;
81
82    let output = FileUploadOutput {
83        file_id: result.file_id,
84        path: result.path,
85        public_url: result.public_url,
86        size_bytes,
87        mime_type,
88        checksum_sha256,
89    };
90
91    Ok(CommandOutput::card_value("File Uploaded", &output))
92}
93
94pub fn detect_mime_type(path: &Path) -> String {
95    systemprompt_models::mime::from_path(path).to_owned()
96}