async_graphql/types/
upload.rs

1use std::{borrow::Cow, io::Read, ops::Deref, sync::Arc};
2
3#[cfg(feature = "unblock")]
4use futures_util::io::AsyncRead;
5
6use crate::{
7    registry, registry::MetaTypeId, Context, InputType, InputValueError, InputValueResult, Value,
8};
9
10/// A file upload value.
11pub struct UploadValue {
12    /// The name of the file.
13    pub filename: String,
14    /// The content type of the file.
15    pub content_type: Option<String>,
16    /// The file data.
17    #[cfg(feature = "tempfile")]
18    pub content: std::fs::File,
19    /// The file data.
20    #[cfg(not(feature = "tempfile"))]
21    pub content: bytes::Bytes,
22}
23
24impl UploadValue {
25    /// Attempt to clone the upload value. This type's `Clone` implementation
26    /// simply calls this and panics on failure.
27    ///
28    /// # Errors
29    ///
30    /// Fails if cloning the inner `File` fails.
31    pub fn try_clone(&self) -> std::io::Result<Self> {
32        #[cfg(feature = "tempfile")]
33        {
34            Ok(Self {
35                filename: self.filename.clone(),
36                content_type: self.content_type.clone(),
37                content: self.content.try_clone()?,
38            })
39        }
40
41        #[cfg(not(feature = "tempfile"))]
42        {
43            Ok(Self {
44                filename: self.filename.clone(),
45                content_type: self.content_type.clone(),
46                content: self.content.clone(),
47            })
48        }
49    }
50
51    /// Convert to a `Read`.
52    ///
53    /// **Note**: this is a *synchronous/blocking* reader.
54    pub fn into_read(self) -> impl Read + Sync + Send + 'static {
55        #[cfg(feature = "tempfile")]
56        {
57            self.content
58        }
59
60        #[cfg(not(feature = "tempfile"))]
61        {
62            std::io::Cursor::new(self.content)
63        }
64    }
65
66    /// Convert to a `AsyncRead`.
67    #[cfg(feature = "unblock")]
68    #[cfg_attr(docsrs, doc(cfg(feature = "unblock")))]
69    pub fn into_async_read(self) -> impl AsyncRead + Sync + Send + 'static {
70        #[cfg(feature = "tempfile")]
71        {
72            blocking::Unblock::new(self.content)
73        }
74
75        #[cfg(not(feature = "tempfile"))]
76        {
77            std::io::Cursor::new(self.content)
78        }
79    }
80
81    /// Returns the size of the file, in bytes.
82    pub fn size(&self) -> std::io::Result<u64> {
83        #[cfg(feature = "tempfile")]
84        {
85            self.content.metadata().map(|meta| meta.len())
86        }
87
88        #[cfg(not(feature = "tempfile"))]
89        {
90            Ok(self.content.len() as u64)
91        }
92    }
93}
94
95/// Uploaded file
96///
97/// **Reference:** <https://github.com/jaydenseric/graphql-multipart-request-spec>
98///
99///
100/// Graphql supports file uploads via `multipart/form-data`.
101/// Enable this feature by accepting an argument of type `Upload` (single file)
102/// or `Vec<Upload>` (multiple files) in your mutation like in the example blow.
103///
104///
105/// # Example
106/// *[Full Example](<https://github.com/async-graphql/examples/blob/master/models/files/src/lib.rs>)*
107///
108/// ```
109/// use async_graphql::*;
110///
111/// struct Mutation;
112///
113/// #[Object]
114/// impl Mutation {
115///     async fn upload(&self, ctx: &Context<'_>, file: Upload) -> bool {
116///         println!("upload: filename={}", file.value(ctx).unwrap().filename);
117///         true
118///     }
119/// }
120/// ```
121/// # Example Curl Request
122///
123/// Assuming you have defined your Mutation like in the example above,
124/// you can now upload a file `myFile.txt` with the below curl command:
125///
126/// ```curl
127/// curl 'localhost:8000' \
128/// --form 'operations={
129///         "query": "mutation ($file: Upload!) { upload(file: $file)  }",
130///         "variables": { "file": null }}' \
131/// --form 'map={ "0": ["variables.file"] }' \
132/// --form '0=@myFile.txt'
133/// ```
134#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
135pub struct Upload(pub usize);
136
137impl Upload {
138    /// Get the upload value.
139    pub fn value(&self, ctx: &Context<'_>) -> std::io::Result<UploadValue> {
140        ctx.query_env.uploads[self.0].try_clone()
141    }
142}
143
144impl Deref for Upload {
145    type Target = usize;
146
147    fn deref(&self) -> &Self::Target {
148        &self.0
149    }
150}
151
152impl InputType for Upload {
153    type RawValueType = Self;
154
155    fn type_name() -> Cow<'static, str> {
156        Cow::Borrowed("Upload")
157    }
158
159    fn create_type_info(registry: &mut registry::Registry) -> String {
160        registry.create_input_type::<Self, _>(MetaTypeId::Scalar, |_| registry::MetaType::Scalar {
161            name: Self::type_name().to_string(),
162            description: None,
163            is_valid: Some(Arc::new(|value| matches!(value, Value::String(_)))),
164            visible: None,
165            inaccessible: false,
166            tags: Default::default(),
167            specified_by_url: Some(
168                "https://github.com/jaydenseric/graphql-multipart-request-spec".to_string(),
169            ),
170            directive_invocations: Default::default(),
171        })
172    }
173
174    fn parse(value: Option<Value>) -> InputValueResult<Self> {
175        const PREFIX: &str = "#__graphql_file__:";
176        let value = value.unwrap_or_default();
177        if let Value::String(s) = &value {
178            if let Some(filename) = s.strip_prefix(PREFIX) {
179                return Ok(Upload(filename.parse::<usize>().unwrap()));
180            }
181        }
182        Err(InputValueError::expected_type(value))
183    }
184
185    fn to_value(&self) -> Value {
186        Value::Null
187    }
188
189    fn as_raw_value(&self) -> Option<&Self::RawValueType> {
190        Some(self)
191    }
192}