diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 57f97f1..64b70bc 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -35,6 +35,7 @@ mime_guess = "2.0.4" zip = { version = "0.6.2", default-features = false } base64 = "0.13.0" image = "0.24.4" +fast_image_resize = "1.0.0" stretto = "0.7.1" rustracing = "0.6.0" diff --git a/backend/src/routes/fs/mod.rs b/backend/src/routes/fs/mod.rs index a401d62..bcffd74 100644 --- a/backend/src/routes/fs/mod.rs +++ b/backend/src/routes/fs/mod.rs @@ -1,6 +1,7 @@ pub mod routes; use std::{ + cmp::max, collections::VecDeque, iter::Iterator, sync::{ @@ -232,3 +233,15 @@ pub fn generate_path_dto( get_path } + +fn resize_dimensions(width: u32, height: u32) -> (u32, u32) { + let wratio = 300.0 / width as f64; + let hratio = 300.0 / height as f64; + + let ratio = f64::min(wratio, hratio); + + ( + max((width as f64 * ratio).round() as u64, 1) as u32, + max((height as f64 * ratio).round() as u64, 1) as u32 + ) +} diff --git a/backend/src/routes/fs/routes.rs b/backend/src/routes/fs/routes.rs index 3298cb3..19860d0 100644 --- a/backend/src/routes/fs/routes.rs +++ b/backend/src/routes/fs/routes.rs @@ -2,10 +2,14 @@ use std::{ collections::{BTreeSet, HashMap}, fs::File, io::{Read, Write}, + num::NonZeroU32, ops::DerefMut, sync::atomic::Ordering }; +use image::DynamicImage; +use fast_image_resize as fir; +use fast_image_resize::PixelType; use rustracing_jaeger::Span; use tiny_http::{Request, Response, ResponseBox, StatusCode}; @@ -15,7 +19,7 @@ use crate::{ dto, header, metrics, - routes::{filters::UserInfo, get_reply, AppError, ChannelReader} + routes::{filters::UserInfo, fs::resize_dimensions, get_reply, AppError, ChannelReader} }; pub fn root(_: &Span, _: &mut Request, _: &mut DBConnection, info: UserInfo) -> Result { @@ -164,6 +168,7 @@ pub fn upload( let mut file_size = 0_i64; let file_name = format!("./files/{}", node.id); + let mut file_buf = Vec::::new(); { let _span = metrics::span("receive_file", span); let mut buf = vec![0_u8; 8 * 1024 * 1024]; @@ -176,6 +181,9 @@ pub fn upload( if r == 0 { break; } + if file_size < 20 * 1024 * 1024 { + file_buf.write_all(&buf[..r]).unwrap(); + } file.write_all(&buf[..r]).unwrap(); file_size += r as i64; } @@ -185,19 +193,73 @@ pub fn upload( .with_label_values(&[node.owner_id.to_string().as_str()]) .add(file_size - node.size.unwrap_or(0)); { - let _span = metrics::span("generate_preview", span); + let prev_span = metrics::span("generate_preview", span); node.has_preview = (|| { - if file_size > 20 * 1024 * 1024 { + if file_size > file_buf.len() as i64 { return None; } let mime = mime_guess::from_path(std::path::Path::new(&node.name)).first()?.to_string(); - let img = image::load( - std::io::BufReader::new(File::open(file_name.clone()).unwrap()), - image::ImageFormat::from_mime_type(mime)? - ) - .ok()?; - let img = img.resize(300, 300, image::imageops::FilterType::Triangle); - img.save(std::path::Path::new(&(file_name + "_preview.jpg"))).expect("Failed to save preview image"); + let img = { + let _span = metrics::span("generate_preview_load", &prev_span); + image::load_from_memory_with_format( + file_buf.as_slice(), + image::ImageFormat::from_mime_type(mime)? + ) + .ok()? + }; + let img = { + let _span = metrics::span("generate_preview_convert", &prev_span); + let width = NonZeroU32::try_from(img.width()).unwrap(); + let height = NonZeroU32::try_from(img.height()).unwrap(); + match img { + DynamicImage::ImageLuma8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8), + DynamicImage::ImageLumaA8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x2), + DynamicImage::ImageRgb8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x3), + DynamicImage::ImageRgba8(v) => fir::Image::from_vec_u8(width, height, v.into_raw(), fir::PixelType::U8x4), + DynamicImage::ImageLuma16(_) => fir::Image::from_vec_u8(width, height, img.to_luma8().into_raw(), fir::PixelType::U8), + DynamicImage::ImageLumaA16(_) => fir::Image::from_vec_u8(width, height, img.to_luma_alpha8().into_raw(), fir::PixelType::U8x2), + DynamicImage::ImageRgb16(_) => fir::Image::from_vec_u8(width, height, img.to_rgb8().into_raw(), fir::PixelType::U8x3), + DynamicImage::ImageRgba16(_) => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4), + DynamicImage::ImageRgb32F(_) => fir::Image::from_vec_u8(width, height, img.to_rgb8().into_raw(), fir::PixelType::U8x3), + DynamicImage::ImageRgba32F(_) => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4), + _ => fir::Image::from_vec_u8(width, height, img.to_rgba8().into_raw(), fir::PixelType::U8x4) + }.expect("Failed to convert preview image") + }; + let img = { + let _span = metrics::span("generate_preview_resize", &prev_span); + let new_dim = resize_dimensions(img.width().get(), img.height().get()); + let mut dst = fir::Image::new( + NonZeroU32::try_from(new_dim.0).unwrap(), + NonZeroU32::try_from(new_dim.1).unwrap(), + img.pixel_type() + ); + fir::Resizer::new(fir::ResizeAlg::SuperSampling( + fir::FilterType::Hamming, + 2 + )) + .resize(&img.view(), &mut dst.view_mut()) + .expect("Failed to resize preview image"); + dst + }; + let _span = metrics::span("generate_preview_save", &prev_span); + let mut file = std::io::BufWriter::new( + File::create(std::path::Path::new(&(file_name + "_preview.jpg"))) + .expect("Failed to open preview image file") + ); + image::codecs::jpeg::JpegEncoder::new(&mut file) + .encode( + img.buffer(), + img.width().get(), + img.height().get(), + match img.pixel_type() { + PixelType::U8 => image::ColorType::L8, + PixelType::U8x2 => image::ColorType::La8, + PixelType::U8x3 => image::ColorType::Rgb8, + PixelType::U8x4 => image::ColorType::Rgba8, + _ => unreachable!() + } + ) + .expect("Failed to save preview image"); Some(()) })() .is_some(); diff --git a/frontend/src/api/fs.ts b/frontend/src/api/fs.ts index b06b4cc..b14f518 100644 --- a/frontend/src/api/fs.ts +++ b/frontend/src/api/fs.ts @@ -84,28 +84,31 @@ export async function upload_file( token: string, file: UploadFile, onProgress: (progressEvent: ProgressEvent) => void -): Promise { +): Promise<[Responses.Success | Responses.Error, boolean]> { const node = await create_file(token, file.parent, file.file.name); - if (isErrorResponse(node)) return node; + if (isErrorResponse(node)) return [node, false]; if ('exists' in node && !node.isFile) - return { statusCode: 400, message: 'File exists as folder' }; + return [{ statusCode: 400, message: 'File exists as folder' }, false]; - return axios - .post(`/api/fs/upload/${node.id}`, file.file, { - headers: { - Authorization: 'Bearer ' + token, - 'Content-type': 'multipart/form-data' - }, - onUploadProgress: onProgress - }) - .then((res) => { - console.log(res); - return res.data; - }) - .catch((err) => { - console.log(err); - return err.response.data; - }); + return [ + await axios + .post(`/api/fs/upload/${node.id}`, file.file, { + headers: { + Authorization: 'Bearer ' + token, + 'Content-type': 'multipart/form-data' + }, + onUploadProgress: onProgress + }) + .then((res) => { + console.log(res); + return res.data; + }) + .catch((err) => { + console.log(err); + return err.response.data; + }), + 'exists' in node + ]; } export function download_file(token: string, id: number) { diff --git a/frontend/src/components/DirViewer/DeleteModal.vue b/frontend/src/components/DirViewer/DeleteModal.vue index a271513..4889121 100644 --- a/frontend/src/components/DirViewer/DeleteModal.vue +++ b/frontend/src/components/DirViewer/DeleteModal.vue @@ -48,7 +48,7 @@ async function startDelete() { await resp.body.pipeTo(logWriter); } catch (err) { log.value += `Error: ${err}\n`; - logInst.value?.scrollTo({ position: 'top' }); + logInst.value?.scrollTo({ position: 'bottom', slient: true }); } } } diff --git a/frontend/src/components/FileViewer/FileViewer.vue b/frontend/src/components/FileViewer/FileViewer.vue index b4645a8..c5d34a5 100644 --- a/frontend/src/components/FileViewer/FileViewer.vue +++ b/frontend/src/components/FileViewer/FileViewer.vue @@ -115,6 +115,8 @@ watch( v-else-if="fileType === fileTypes.IMAGE && src !== ''" :src="src" :alt="node.name" + :img-props="{ style: 'max-width: 80vw; max-height: 70vh;' }" + object-fit="contain" />