1use std::io::{BufRead, Read, Seek, Write};
2
3use aes_gcm::{
4 aead::{AeadCore, AeadMutInPlace, OsRng},
5 Aes256Gcm, Key, KeyInit, Nonce,
6};
7use image::{DynamicImage, ImageBuffer};
8use revolt_config::{config, report_internal_error, FilesS3};
9use revolt_result::{create_error, Result};
10
11use aws_sdk_s3::{
12 config::{Credentials, Region},
13 Client, Config,
14};
15
16use base64::prelude::*;
17use tempfile::NamedTempFile;
18use tiny_skia::Pixmap;
19
20pub const AUTHENTICATION_TAG_SIZE_BYTES: usize = 16;
22
23pub fn create_client(s3_config: FilesS3) -> Client {
25 let provider_name = "my-creds";
26 let creds = Credentials::new(
27 s3_config.access_key_id,
28 s3_config.secret_access_key,
29 None,
30 None,
31 provider_name,
32 );
33
34 let config = Config::builder()
35 .region(Region::new(s3_config.region))
36 .endpoint_url(s3_config.endpoint)
37 .force_path_style(s3_config.path_style_buckets)
38 .credentials_provider(creds)
39 .build();
40
41 Client::from_conf(config)
42}
43
44pub fn create_cipher(key: &str) -> Aes256Gcm {
46 let key = &BASE64_STANDARD.decode(key).expect("valid base64 string")[..];
47 let key: &Key<Aes256Gcm> = key.into();
48 Aes256Gcm::new(key)
49}
50
51pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<Vec<u8>> {
53 let config = config().await;
54 let client = create_client(config.files.s3);
55
56 let mut obj =
58 report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?;
59
60 let mut buf = vec![];
62 while let Some(bytes) = obj.body.next().await {
63 let data = report_internal_error!(bytes)?;
64 report_internal_error!(buf.write_all(&data))?;
65 }
68
69 if nonce.is_empty() {
71 return Ok(buf);
72 }
73
74 let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..];
76 let nonce: &Nonce<typenum::consts::U12> = nonce.into();
77
78 create_cipher(&config.files.encryption_key)
80 .decrypt_in_place(nonce, b"", &mut buf)
81 .map_err(|_| create_error!(InternalError))?;
82
83 buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
85
86 Ok(buf)
87}
88
89pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result<String> {
91 let config = config().await;
92 let client = create_client(config.files.s3);
93
94 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
96
97 let mut buf = [buf, &[0; AUTHENTICATION_TAG_SIZE_BYTES]].concat();
99
100 create_cipher(&config.files.encryption_key)
102 .encrypt_in_place(&nonce, b"", &mut buf)
103 .map_err(|_| create_error!(InternalError))?;
104
105 report_internal_error!(
107 client
108 .put_object()
109 .bucket(bucket_id)
110 .key(path)
111 .body(buf.into())
112 .send()
113 .await
114 )?;
115
116 Ok(BASE64_STANDARD.encode(nonce))
117}
118
119pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> {
121 let config = config().await;
122 let client = create_client(config.files.s3);
123
124 report_internal_error!(
125 client
126 .delete_object()
127 .bucket(bucket_id)
128 .key(path)
129 .send()
130 .await
131 )?;
132
133 Ok(())
134}
135
136pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> {
138 if let Ok(size) = imagesize::size(f.path())
139 .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}"))
140 {
141 Some((size.width, size.height))
142 } else {
143 None
144 }
145}
146
147pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> {
149 match mime {
150 "image/svg+xml" => {
151 let tree =
152 report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?;
153
154 let size = tree.size();
155 Some((size.width() as usize, size.height() as usize))
156 }
157 _ => {
158 if let Ok(size) = imagesize::blob_size(v)
159 .inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}"))
160 {
161 Some((size.width, size.height))
162 } else {
163 None
164 }
165 }
166 }
167}
168
169pub fn video_size(f: &NamedTempFile) -> Option<(i64, i64)> {
171 if let Ok(data) = ffprobe::ffprobe(f.path())
172 .inspect_err(|err| tracing::error!("Failed to ffprobe file! {err:?}"))
173 {
174 for stream in data.streams {
176 if let (Some(w), Some(h)) = (stream.width, stream.height) {
177 return Some((w, h));
178 }
179 }
180
181 None
182 } else {
183 None
184 }
185}
186
187pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Result<DynamicImage> {
189 match mime {
190 "image/jxl" => {
192 let jxl_image = report_internal_error!(jxl_oxide::JxlImage::builder().read(reader))?;
193 if let Ok(frame) = jxl_image.render_frame(0) {
194 match frame.color_channels().len() {
195 3 => Ok(DynamicImage::ImageRgb8(
196 DynamicImage::ImageRgb32F(
197 ImageBuffer::from_vec(
198 jxl_image.width(),
199 jxl_image.height(),
200 frame.image().buf().to_vec(),
201 )
202 .ok_or_else(|| create_error!(ImageProcessingFailed))?,
203 )
204 .to_rgb8(),
205 )),
206 4 => Ok(DynamicImage::ImageRgba8(
207 DynamicImage::ImageRgba32F(
208 ImageBuffer::from_vec(
209 jxl_image.width(),
210 jxl_image.height(),
211 frame.image().buf().to_vec(),
212 )
213 .ok_or_else(|| create_error!(ImageProcessingFailed))?,
214 )
215 .to_rgba8(),
216 )),
217 _ => Err(create_error!(ImageProcessingFailed)),
218 }
219 } else {
220 Err(create_error!(ImageProcessingFailed))
221 }
222 }
223 "image/svg+xml" => {
225 let mut buf = Vec::new();
227 report_internal_error!(reader.read_to_end(&mut buf))?;
228
229 let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?;
230 let size = tree.size();
231 let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32)
232 .ok_or_else(|| create_error!(ImageProcessingFailed))?;
233
234 let mut pixmap_mut = pixmap.as_mut();
235 resvg::render(&tree, Default::default(), &mut pixmap_mut);
236
237 Ok(DynamicImage::ImageRgba8(
238 ImageBuffer::from_vec(
239 size.width() as u32,
240 size.height() as u32,
241 pixmap.data().to_vec(),
242 )
243 .ok_or_else(|| create_error!(ImageProcessingFailed))?,
244 ))
245 }
246 _ => report_internal_error!(report_internal_error!(
248 image::ImageReader::new(reader).with_guessed_format()
249 )?
250 .decode()),
251 }
252}
253
254pub fn is_valid_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> bool {
256 match mime {
257 "image/jxl" => jxl_oxide::JxlImage::builder()
259 .read(reader)
260 .inspect_err(|err| tracing::error!("Failed to read JXL! {err:?}"))
261 .is_ok(),
262 _ => !matches!(
264 image::ImageReader::new(reader)
265 .with_guessed_format()
266 .inspect_err(|err| tracing::error!("Failed to read image! {err:?}"))
267 .map(|f| f.decode()),
268 Err(_) | Ok(Err(_))
269 ),
270 }
271}
272
273pub async fn create_thumbnail(image: DynamicImage, tag: &str) -> Vec<u8> {
275 let config = config().await;
277 let [w, h] = config.files.preview.get(tag).unwrap();
278
279 let image = image.thumbnail(image.width().min(*w as u32), image.height().min(*h as u32));
285
286 let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder.");
288 if config.files.webp_quality != 100.0 {
289 encoder.encode(config.files.webp_quality).to_vec()
290 } else {
291 encoder.encode_lossless().to_vec()
292 }
293}