Skip to main content

gproxy_protocol/adapt/files/
mod.rs

1//! Explicit selected-target file operations; no routing or implicit retry.
2
3mod copy;
4mod crud;
5mod upload;
6
7use crate::{HttpBody, WireResponse, codec::CodecLimits, transform::TransformError};
8
9pub use copy::copy_to_multipart;
10pub use crud::{
11    claude_get, claude_list, delete_empty, gemini_get, gemini_list, openai_get, openai_list,
12};
13pub use upload::{
14    GeminiUploadProgress, GeminiUploadSession, UploadError, UploadFailure, gemini_start_resumable,
15    gemini_upload_chunk, upload_multipart_json,
16};
17
18#[derive(Debug, Clone, Copy)]
19pub struct FileCrudLimits {
20    pub codec: CodecLimits,
21
22    pub max_declared_bytes: u64,
23}
24
25#[derive(Debug)]
26pub enum FileFailure {
27    Transform(TransformError),
28    Rejected(Box<WireResponse<HttpBody>>),
29}
30
31#[derive(Debug)]
32pub struct FileOperationError {
33    pub completed_calls: usize,
34    pub completed_files: usize,
35    pub failure: FileFailure,
36}
37
38impl From<TransformError> for FileOperationError {
39    fn from(value: TransformError) -> Self {
40        Self {
41            completed_calls: 0,
42            completed_files: 0,
43            failure: FileFailure::Transform(value),
44        }
45    }
46}
47
48impl std::fmt::Display for FileOperationError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(
51            f,
52            "file operation failed after {} decoded calls and {} validated files",
53            self.completed_calls, self.completed_files
54        )
55    }
56}
57
58impl std::error::Error for FileOperationError {}
59pub(crate) fn path(path: &str) -> Result<(), TransformError> {
60    if !path.starts_with('/')
61        || path.starts_with("//")
62        || path.contains(['?', '#', '\\', '\r', '\n'])
63    {
64        return Err(TransformError::shape(
65            "files.path",
66            "origin-relative path required, query separate",
67        ));
68    }
69    Ok(())
70}
71
72fn component(value: &str) -> String {
73    let mut out = String::new();
74    use std::fmt::Write;
75    for byte in value.bytes() {
76        if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) {
77            out.push(char::from(byte));
78        } else {
79            write!(&mut out, "%{byte:02X}").unwrap();
80        }
81    }
82    out
83}
84
85fn name(value: &str, gemini: bool) -> Result<String, TransformError> {
86    let value = if gemini {
87        value.strip_prefix("files/").ok_or_else(|| {
88            TransformError::shape("files.name", "Gemini file resource must be files/{id}")
89        })?
90    } else {
91        value
92    };
93    if value.is_empty() {
94        return Err(TransformError::shape("files.id", "empty file id"));
95    }
96    Ok(if gemini {
97        format!("files/{}", component(value))
98    } else {
99        component(value)
100    })
101}