1use std::fmt;
7use std::future::IntoFuture;
8
9use ferrin_provider_util::media_type::detect_media_type;
10use ferrin_spec::BoxFuture;
11use ferrin_spec::FilesRef;
12use ferrin_spec::MediaType;
13use ferrin_spec::ProviderReference;
14use ferrin_spec::error::ProviderError;
15pub use ferrin_spec::files::DeleteFileResult;
16pub use ferrin_spec::files::DownloadFileResult;
17pub use ferrin_spec::files::FileMetadataResult;
18use ferrin_spec::files::FileReferenceOptions;
19pub use ferrin_spec::files::UploadData;
20use ferrin_spec::files::UploadFileOptions;
21pub use ferrin_spec::files::UploadFileResult;
22use tracing::Instrument;
23
24use crate::error::Error;
25use crate::modality::ModalityOptions;
26use crate::modality::impl_modality_builder;
27use crate::telemetry::ModelIdentity;
28use crate::telemetry::spans;
29
30const TEXT_CHECK_LENGTH: usize = 512;
32
33#[must_use]
37pub fn upload_file(files: impl Into<FilesRef>, data: impl Into<UploadData>) -> UploadFile {
38 UploadFile {
39 files: files.into(),
40 data: data.into(),
41 media_type: None,
42 filename: None,
43 base: ModalityOptions::default(),
44 }
45}
46
47pub struct UploadFile {
49 files: FilesRef,
50 data: UploadData,
51 media_type: Option<MediaType>,
52 filename: Option<String>,
53 base: ModalityOptions,
54}
55
56impl fmt::Debug for UploadFile {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.debug_struct("UploadFile")
59 .field("files", &self.files)
60 .field("data", &self.data)
61 .field("media_type", &self.media_type)
62 .field("filename", &self.filename)
63 .field("base", &self.base)
64 .finish()
65 }
66}
67
68impl UploadFile {
69 #[must_use]
71 pub fn media_type(mut self, media_type: impl Into<MediaType>) -> Self {
72 self.media_type = Some(media_type.into());
73 self
74 }
75
76 #[must_use]
78 pub fn filename(mut self, filename: impl Into<String>) -> Self {
79 self.filename = Some(filename.into());
80 self
81 }
82}
83
84impl_modality_builder!(@no_retry UploadFile);
85
86impl IntoFuture for UploadFile {
87 type Output = Result<UploadFileResult, Error>;
88 type IntoFuture = BoxFuture<'static, Self::Output>;
89
90 fn into_future(self) -> Self::IntoFuture {
91 Box::pin(async move {
92 let identity = service_identity(&self.files);
93 let span = spans::modality_span("upload_file", &identity);
94 let media_type = self
95 .media_type
96 .unwrap_or_else(|| default_media_type(&self.data));
97 let files = self.files.clone();
98 let base = self.base.clone();
99 let data = self.data;
100 let filename = self.filename;
101 base.run(|base, token| {
102 async move {
103 let result = files
104 .upload_file(UploadFileOptions {
105 data,
106 media_type,
107 filename,
108 headers: base.request_headers(),
109 provider_options: base.provider_options.clone(),
110 cancellation: token,
111 })
112 .await
113 .map_err(Error::from)?;
114 spans::log_warnings(&result.warnings, &identity);
115 Ok(result)
116 }
117 .instrument(span)
118 })
119 .await
120 })
121 }
122}
123
124pub(crate) fn is_likely_text(bytes: &[u8]) -> bool {
127 let sample = &bytes[..bytes.len().min(TEXT_CHECK_LENGTH)];
128 !sample.is_empty()
129 && sample
130 .iter()
131 .all(|byte| *byte >= 0x20 || matches!(byte, 0x09 | 0x0a | 0x0d))
132}
133
134fn default_media_type(data: &UploadData) -> MediaType {
135 match data {
136 UploadData::Text(_) => MediaType::new("text/plain"),
137 UploadData::Bytes(bytes) => detect_media_type(bytes).unwrap_or_else(|| {
138 if is_likely_text(bytes) {
139 MediaType::new("text/plain")
140 } else {
141 MediaType::new("application/octet-stream")
142 }
143 }),
144 #[allow(unreachable_patterns, reason = "UploadData is non-exhaustive")]
145 _ => MediaType::new("application/octet-stream"),
146 }
147}
148
149fn service_identity(files: &FilesRef) -> ModelIdentity {
150 ModelIdentity::new(files.provider().clone(), "files")
151}
152
153macro_rules! file_operation {
154 ($(#[$meta:meta])* $name:ident, $ty:ident, $result:ty, $supports:ident, $method:ident, $label:literal) => {
155 $(#[$meta])*
156 #[must_use]
157 pub fn $name(files: impl Into<FilesRef>, file: ProviderReference) -> $ty {
158 $ty {
159 files: files.into(),
160 file,
161 base: ModalityOptions::default(),
162 }
163 }
164
165 #[doc = concat!("Builder returned by [`", stringify!($name), "`]; `.await` runs the call.")]
166 #[derive(Debug)]
167 pub struct $ty {
168 files: FilesRef,
169 file: ProviderReference,
170 base: ModalityOptions,
171 }
172
173 impl_modality_builder!(@no_retry $ty);
174
175 impl IntoFuture for $ty {
176 type Output = Result<$result, Error>;
177 type IntoFuture = BoxFuture<'static, Self::Output>;
178
179 fn into_future(self) -> Self::IntoFuture {
180 Box::pin(async move {
181 let identity = service_identity(&self.files);
182 let span = spans::modality_span($label, &identity);
183 if !self.files.$supports() {
184 return Err(Error::from(ProviderError::unsupported($label)));
185 }
186 let files = self.files.clone();
187 let base = self.base.clone();
188 let file = self.file;
189 base.run(|base, token| {
190 async move {
191 files
192 .$method(FileReferenceOptions {
193 file,
194 headers: base.request_headers(),
195 provider_options: base.provider_options.clone(),
196 cancellation: token,
197 })
198 .await
199 .map_err(Error::from)
200 }
201 .instrument(span)
202 })
203 .await
204 })
205 }
206 }
207 };
208}
209
210file_operation!(
211 get_file_metadata,
214 GetFileMetadata,
215 FileMetadataResult,
216 supports_get_file_metadata,
217 get_file_metadata,
218 "get_file_metadata"
219);
220
221file_operation!(
222 download_file,
225 DownloadFile,
226 DownloadFileResult,
227 supports_download_file,
228 download_file,
229 "download_file"
230);
231
232file_operation!(
233 delete_file,
236 DeleteFile,
237 DeleteFileResult,
238 supports_delete_file,
239 delete_file,
240 "delete_file"
241);