salvo_oapi/extract/payload/
file.rs

1use std::ops::{Deref, DerefMut};
2use std::path::PathBuf;
3
4use salvo_core::extract::{Extractible, Metadata};
5use salvo_core::http::form::FilePart;
6use salvo_core::http::header::CONTENT_TYPE;
7use salvo_core::http::{HeaderMap, Mime, ParseError};
8use salvo_core::{Request, async_trait};
9
10use crate::endpoint::EndpointArgRegister;
11use crate::{
12    Array, BasicType, Components, Content, KnownFormat, Object, Operation, RequestBody, Schema,
13    SchemaFormat,
14};
15
16/// Represents the upload file.
17#[derive(Clone, Debug)]
18pub struct FormFile {
19    name: Option<String>,
20    /// The headers of the part
21    headers: HeaderMap,
22    /// A temporary file containing the file content
23    path: PathBuf,
24    /// Optionally, the size of the file.  This is filled when multiparts are parsed, but is
25    /// not necessary when they are generated.
26    size: u64,
27}
28impl FormFile {
29    /// Create a new `FormFile` from a `FilePart`.
30    #[must_use]
31    pub fn new(file_part: &FilePart) -> Self {
32        Self {
33            name: file_part.name().map(|s| s.to_owned()),
34            headers: file_part.headers().clone(),
35            path: file_part.path().to_owned(),
36            size: file_part.size(),
37        }
38    }
39
40    /// Get file name.
41    #[inline]
42    #[must_use]
43    pub fn name(&self) -> Option<&str> {
44        self.name.as_deref()
45    }
46    /// Get file name mutable reference.
47    #[inline]
48    pub fn name_mut(&mut self) -> Option<&mut String> {
49        self.name.as_mut()
50    }
51    /// Get headers.
52    #[inline]
53    #[must_use]
54    pub fn headers(&self) -> &HeaderMap {
55        &self.headers
56    }
57    /// Get headers mutable reference.
58    pub fn headers_mut(&mut self) -> &mut HeaderMap {
59        &mut self.headers
60    }
61    /// Get content type.
62    #[inline]
63    pub fn content_type(&self) -> Option<Mime> {
64        self.headers
65            .get(CONTENT_TYPE)
66            .and_then(|h| h.to_str().ok())
67            .and_then(|v| v.parse().ok())
68    }
69    /// Get file path.
70    #[inline]
71    #[must_use]
72    pub fn path(&self) -> &PathBuf {
73        &self.path
74    }
75    /// Get file size.
76    #[inline]
77    #[must_use]
78    pub fn size(&self) -> u64 {
79        self.size
80    }
81}
82
83impl<'ex> Extractible<'ex> for FormFile {
84    fn metadata() -> &'static Metadata {
85        static METADATA: Metadata = Metadata::new("");
86        &METADATA
87    }
88    #[allow(refining_impl_trait)]
89    async fn extract(_req: &'ex mut Request) -> Result<Self, ParseError> {
90        panic!("query parameter can not be extracted from request")
91    }
92    #[allow(refining_impl_trait)]
93    async fn extract_with_arg(req: &'ex mut Request, arg: &str) -> Result<Self, ParseError> {
94        req.file(arg)
95            .await
96            .map(Self::new)
97            .ok_or_else(|| ParseError::other("file not found"))
98    }
99}
100
101#[async_trait]
102impl EndpointArgRegister for FormFile {
103    fn register(_components: &mut Components, operation: &mut Operation, arg: &str) {
104        let schema = Schema::from(
105            Object::new().property(
106                arg,
107                Object::with_type(BasicType::String)
108                    .format(SchemaFormat::KnownFormat(KnownFormat::Binary)),
109            ),
110        );
111
112        if let Some(request_body) = &mut operation.request_body {
113            request_body
114                .contents
115                .insert("multipart/form-data".into(), Content::new(schema));
116        } else {
117            let request_body = RequestBody::new()
118                .description("Upload a file.")
119                .add_content("multipart/form-data", Content::new(schema));
120            operation.request_body = Some(request_body);
121        }
122    }
123}
124
125/// Represents the upload files.
126#[derive(Clone, Debug)]
127pub struct FormFiles(pub Vec<FormFile>);
128impl FormFiles {
129    /// Create a new `FormFiles` from a `Vec<&FilePart>`.
130    #[must_use]
131    pub fn new(file_parts: Vec<&FilePart>) -> Self {
132        Self(file_parts.into_iter().map(FormFile::new).collect())
133    }
134
135    /// Get inner files.
136    #[must_use]
137    pub fn into_inner(self) -> Vec<FormFile> {
138        self.0
139    }
140}
141impl Deref for FormFiles {
142    type Target = Vec<FormFile>;
143
144    fn deref(&self) -> &Self::Target {
145        &self.0
146    }
147}
148
149impl DerefMut for FormFiles {
150    fn deref_mut(&mut self) -> &mut Self::Target {
151        &mut self.0
152    }
153}
154
155impl<'ex> Extractible<'ex> for FormFiles {
156    fn metadata() -> &'static Metadata {
157        static METADATA: Metadata = Metadata::new("");
158        &METADATA
159    }
160    #[allow(refining_impl_trait)]
161    async fn extract(_req: &'ex mut Request) -> Result<Self, ParseError> {
162        panic!("query parameter can not be extracted from request")
163    }
164    #[allow(refining_impl_trait)]
165    async fn extract_with_arg(req: &'ex mut Request, arg: &str) -> Result<Self, ParseError> {
166        Ok(Self(
167            req.files(arg)
168                .await
169                .ok_or_else(|| ParseError::other("file not found"))?
170                .iter()
171                .map(FormFile::new)
172                .collect(),
173        ))
174    }
175}
176
177#[async_trait]
178impl EndpointArgRegister for FormFiles {
179    fn register(_components: &mut Components, operation: &mut Operation, arg: &str) {
180        let schema = Schema::from(
181            Object::new().property(
182                arg,
183                Array::new().items(Schema::from(
184                    Object::with_type(BasicType::String)
185                        .format(SchemaFormat::KnownFormat(KnownFormat::Binary)),
186                )),
187            ),
188        );
189        if let Some(request_body) = &mut operation.request_body {
190            request_body
191                .contents
192                .insert("multipart/form-data".into(), Content::new(schema));
193        } else {
194            let request_body = RequestBody::new()
195                .description("Upload files.")
196                .add_content("multipart/form-data", Content::new(schema));
197            operation.request_body = Some(request_body);
198        }
199    }
200}