Added / fixed a few small things

Closes #43, #44, #45, #46
This commit is contained in:
Mutzi 2022-10-14 01:59:29 +02:00
parent 0399a86699
commit b2d67f6ed4
8 changed files with 148 additions and 36 deletions

View File

@ -35,6 +35,7 @@ mime_guess = "2.0.4"
zip = { version = "0.6.2", default-features = false } zip = { version = "0.6.2", default-features = false }
base64 = "0.13.0" base64 = "0.13.0"
image = "0.24.4" image = "0.24.4"
fast_image_resize = "1.0.0"
stretto = "0.7.1" stretto = "0.7.1"
rustracing = "0.6.0" rustracing = "0.6.0"

View File

@ -1,6 +1,7 @@
pub mod routes; pub mod routes;
use std::{ use std::{
cmp::max,
collections::VecDeque, collections::VecDeque,
iter::Iterator, iter::Iterator,
sync::{ sync::{
@ -232,3 +233,15 @@ pub fn generate_path_dto(
get_path 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
)
}

View File

@ -2,10 +2,14 @@ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
num::NonZeroU32,
ops::DerefMut, ops::DerefMut,
sync::atomic::Ordering sync::atomic::Ordering
}; };
use image::DynamicImage;
use fast_image_resize as fir;
use fast_image_resize::PixelType;
use rustracing_jaeger::Span; use rustracing_jaeger::Span;
use tiny_http::{Request, Response, ResponseBox, StatusCode}; use tiny_http::{Request, Response, ResponseBox, StatusCode};
@ -15,7 +19,7 @@ use crate::{
dto, dto,
header, header,
metrics, 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<ResponseBox, AppError> { pub fn root(_: &Span, _: &mut Request, _: &mut DBConnection, info: UserInfo) -> Result<ResponseBox, AppError> {
@ -164,6 +168,7 @@ pub fn upload(
let mut file_size = 0_i64; let mut file_size = 0_i64;
let file_name = format!("./files/{}", node.id); let file_name = format!("./files/{}", node.id);
let mut file_buf = Vec::<u8>::new();
{ {
let _span = metrics::span("receive_file", span); let _span = metrics::span("receive_file", span);
let mut buf = vec![0_u8; 8 * 1024 * 1024]; let mut buf = vec![0_u8; 8 * 1024 * 1024];
@ -176,6 +181,9 @@ pub fn upload(
if r == 0 { if r == 0 {
break; break;
} }
if file_size < 20 * 1024 * 1024 {
file_buf.write_all(&buf[..r]).unwrap();
}
file.write_all(&buf[..r]).unwrap(); file.write_all(&buf[..r]).unwrap();
file_size += r as i64; file_size += r as i64;
} }
@ -185,19 +193,73 @@ pub fn upload(
.with_label_values(&[node.owner_id.to_string().as_str()]) .with_label_values(&[node.owner_id.to_string().as_str()])
.add(file_size - node.size.unwrap_or(0)); .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 = (|| { node.has_preview = (|| {
if file_size > 20 * 1024 * 1024 { if file_size > file_buf.len() as i64 {
return None; return None;
} }
let mime = mime_guess::from_path(std::path::Path::new(&node.name)).first()?.to_string(); let mime = mime_guess::from_path(std::path::Path::new(&node.name)).first()?.to_string();
let img = image::load( let img = {
std::io::BufReader::new(File::open(file_name.clone()).unwrap()), let _span = metrics::span("generate_preview_load", &prev_span);
image::ImageFormat::from_mime_type(mime)? image::load_from_memory_with_format(
) file_buf.as_slice(),
.ok()?; image::ImageFormat::from_mime_type(mime)?
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"); .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(()) Some(())
})() })()
.is_some(); .is_some();

View File

@ -84,28 +84,31 @@ export async function upload_file(
token: string, token: string,
file: UploadFile, file: UploadFile,
onProgress: (progressEvent: ProgressEvent) => void onProgress: (progressEvent: ProgressEvent) => void
): Promise<Responses.Success | Responses.Error> { ): Promise<[Responses.Success | Responses.Error, boolean]> {
const node = await create_file(token, file.parent, file.file.name); 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) 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 return [
.post(`/api/fs/upload/${node.id}`, file.file, { await axios
headers: { .post(`/api/fs/upload/${node.id}`, file.file, {
Authorization: 'Bearer ' + token, headers: {
'Content-type': 'multipart/form-data' Authorization: 'Bearer ' + token,
}, 'Content-type': 'multipart/form-data'
onUploadProgress: onProgress },
}) onUploadProgress: onProgress
.then((res) => { })
console.log(res); .then((res) => {
return res.data; console.log(res);
}) return res.data;
.catch((err) => { })
console.log(err); .catch((err) => {
return err.response.data; console.log(err);
}); return err.response.data;
}),
'exists' in node
];
} }
export function download_file(token: string, id: number) { export function download_file(token: string, id: number) {

View File

@ -48,7 +48,7 @@ async function startDelete() {
await resp.body.pipeTo(logWriter); await resp.body.pipeTo(logWriter);
} catch (err) { } catch (err) {
log.value += `Error: ${err}\n`; log.value += `Error: ${err}\n`;
logInst.value?.scrollTo({ position: 'top' }); logInst.value?.scrollTo({ position: 'bottom', slient: true });
} }
} }
} }

View File

@ -115,6 +115,8 @@ watch(
v-else-if="fileType === fileTypes.IMAGE && src !== ''" v-else-if="fileType === fileTypes.IMAGE && src !== ''"
:src="src" :src="src"
:alt="node.name" :alt="node.name"
:img-props="{ style: 'max-width: 80vw; max-height: 70vh;' }"
object-fit="contain"
/> />
<iframe <iframe
v-else-if="fileType === fileTypes.IFRAME && src !== ''" v-else-if="fileType === fileTypes.IFRAME && src !== ''"
@ -137,4 +139,4 @@ watch(
</n-grid> </n-grid>
</template> </template>
<style scoped></style> <style scoped lang="scss"></style>

View File

@ -8,6 +8,7 @@ import filesize from 'filesize';
const props = defineProps<{ const props = defineProps<{
file: UploadFile; file: UploadFile;
abort: boolean;
}>(); }>();
const progress = ref(0); const progress = ref(0);
@ -15,13 +16,26 @@ const percentage = ref(0);
const err = ref(''); const err = ref('');
const status = ref<Status>('info'); const status = ref<Status>('info');
const shown = ref(true); const shown = ref(true);
const existed = ref(false);
async function startUpload(token: string, done: () => void) { async function startUpload(token: string, done: () => void) {
const resp = await FS.upload_file(token, props.file, (e) => { let sendDone = false;
if (props.abort) {
done();
shown.value = false;
return;
}
const resp_tuple = await FS.upload_file(token, props.file, (e) => {
progress.value = e.loaded; progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100; percentage.value = (e.loaded / e.total) * 100;
if (e.loaded == e.total) done(); if (e.loaded == e.total) {
sendDone = true;
done();
}
}); });
const resp = resp_tuple[0];
existed.value = resp_tuple[1];
if (!sendDone) done();
percentage.value = 100; percentage.value = 100;
if (isErrorResponse(resp)) { if (isErrorResponse(resp)) {
err.value = resp.message ?? 'Error'; err.value = resp.message ?? 'Error';
@ -60,6 +74,12 @@ defineExpose({
<div v-else-if="err !== ''"> <div v-else-if="err !== ''">
{{ file.fullName }} - Error: {{ err }} {{ file.fullName }} - Error: {{ err }}
</div> </div>
<div v-else-if="existed">
{{ file.fullName }} - Old file overridden
</div>
<div v-else-if="status !== 'success'">
{{ file.fullName }} - Processing...
</div>
<div v-else>{{ file.fullName }} - Completed</div> <div v-else>{{ file.fullName }} - Completed</div>
<n-progress <n-progress
type="line" type="line"

View File

@ -3,20 +3,29 @@ import type { TokenInjectType, UploadFile } from '@/api';
import { ref, inject } from 'vue'; import { ref, inject } from 'vue';
import { update_token } from '@/api'; import { update_token } from '@/api';
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue'; import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
import { NCard } from 'naive-ui'; import { NCard, NButton } from 'naive-ui';
import semaphore from 'semaphore';
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const entries = ref<typeof UploadEntry[]>([]); const entries = ref<typeof UploadEntry[]>([]);
const abortUpload = ref(false);
async function startUpload() { async function startUpload() {
const token = await update_token(jwt); const token = await update_token(jwt);
if (!token) return; if (!token) return;
const ents: typeof UploadEntry[] = entries.value; const ents: typeof UploadEntry[] = entries.value;
const allProms: Promise<void>[] = []; const allProms: Promise<void>[] = [];
const uploadSem = semaphore(5);
for (const entry of ents) { for (const entry of ents) {
await new Promise<void>((resolve) => allProms.push(
allProms.push(entry.startUpload(token, resolve)) new Promise<void>((resolve) => {
uploadSem.take(async () => {
await entry.startUpload(token, () => uploadSem.leave());
resolve();
});
})
); );
} }
await Promise.all(allProms); await Promise.all(allProms);
@ -32,11 +41,13 @@ defineProps<{
<template> <template>
<n-card title="Uploading files" style="margin: 20px"> <n-card title="Uploading files" style="margin: 20px">
<n-button type="error" @click="abortUpload = true">Abort</n-button>
<UploadEntry <UploadEntry
v-for="f in files" v-for="f in files"
:key="f.file.name" :key="f.file.name"
ref="entries" ref="entries"
:file="f" :file="f"
:abort="abortUpload"
/> />
</n-card> </n-card>
</template> </template>