parent
0399a86699
commit
b2d67f6ed4
@ -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"
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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::load_from_memory_with_format(
|
||||||
|
file_buf.as_slice(),
|
||||||
image::ImageFormat::from_mime_type(mime)?
|
image::ImageFormat::from_mime_type(mime)?
|
||||||
)
|
)
|
||||||
.ok()?;
|
.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_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();
|
||||||
|
@ -84,13 +84,14 @@ 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 [
|
||||||
|
await axios
|
||||||
.post(`/api/fs/upload/${node.id}`, file.file, {
|
.post(`/api/fs/upload/${node.id}`, file.file, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer ' + token,
|
Authorization: 'Bearer ' + token,
|
||||||
@ -105,7 +106,9 @@ export async function upload_file(
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return err.response.data;
|
return err.response.data;
|
||||||
});
|
}),
|
||||||
|
'exists' in node
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download_file(token: string, id: number) {
|
export function download_file(token: string, id: number) {
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user