Skip to main content

async_graphql/types/
upload.rs

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