gproxy_protocol/adapt/files/
mod.rs1mod 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}