Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit a64072bf authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

wip static files indexation

parent 232acae2
No related branches found
No related tags found
1 merge request!9Upload files
......@@ -9,4 +9,4 @@ public/**/*.js
public/**/view/*
public/standard/test_sitemap.xml
public/standard/dyn_sitemap.xml
public/uploads
\ No newline at end of file
public/assets/uploads
\ No newline at end of file
pub mod static_files;
use crate::env::Env;
use crate::AppState;
use std::fs::create_dir_all;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct StaticFilesIndex(pub Vec<String>);
impl StaticFilesIndex {
fn rec_read_dir(root: &Path, files: &mut Vec<String>, strip_from: &Path) {
for entry in root.read_dir().unwrap() {
if let Ok(entry) = entry {
if entry.path().is_dir() {
StaticFilesIndex::rec_read_dir(&entry.path(), files, strip_from);
} else {
StaticFilesIndex::_push_path(&entry.path(), files, strip_from);
}
}
}
}
fn _push_path(path: &Path, files: &mut Vec<String>, strip_from: &Path) {
let push_path = path.strip_prefix(strip_from).unwrap();
files.push(push_path.to_str().unwrap().to_owned());
}
pub fn rebuild(&mut self, env: &Env) {
let root = env.public_dir.join("assets");
self.0 = Vec::new();
StaticFilesIndex::rec_read_dir(&root, &mut self.0, &env.public_dir);
}
pub fn push_path(&mut self, path: &Path, env: &Env) {
let strip_from = env.public_dir.join("assets");
StaticFilesIndex::_push_path(path, &mut self.0, &strip_from);
}
}
#[derive(Debug, PartialEq)]
pub enum UploadType {
Image,
Sound,
Video,
Doc,
}
pub struct UploadData {
pub up_type: UploadType,
pub filename: String,
}
pub fn create_dir_if_missing(path: std::path::PathBuf) -> std::path::PathBuf {
if !path.exists() {
create_dir_all(&path).unwrap();
}
path
}
pub fn get_uploads_dir(app_state: &AppState) -> std::path::PathBuf {
create_dir_if_missing(app_state.env.public_dir.join("assets/uploads"))
}
pub fn dirname_from_type(upload_type: &UploadType) -> String {
match upload_type {
UploadType::Image => String::from("images"),
UploadType::Sound => String::from("sounds"),
UploadType::Video => String::from("videos"),
UploadType::Doc => String::from("docs"),
}
}
pub fn upload_type_from_file_ext(ext: &String) -> UploadType {
match &ext[..] {
"webp" | "jpg" | "png" | "jpeg" | "bmp" | "gif" => UploadType::Image,
"mp3" | "ogg" | "wav" | "opus" => UploadType::Sound,
"mp4" | "webm" | "ogv" => UploadType::Video,
_ => UploadType::Doc,
}
}
pub fn file_ext(file_name: &String) -> Result<String, String> {
let parts = file_name.split(".").collect::<Vec<&str>>();
let err_msg = format!("Couldn't get extension from filename : {}", file_name);
if parts.len() < 2 {
return Err(err_msg);
}
match parts.last() {
Some(ext) => Ok(ext.to_string()),
None => Err(err_msg),
}
}
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_static_files {
use super::*;
use std::{fs::remove_dir_all, path::PathBuf};
#[test]
fn should_create_missing_directory() {
let missing_path = PathBuf::from("./some_nested/missing_dir");
let created_path = create_dir_if_missing(missing_path);
assert!(created_path.exists());
remove_dir_all(PathBuf::from("./some_nested")).unwrap();
}
#[test]
fn uploads_subdirs_should_be_consistent() {
assert_eq!(
upload_type_from_file_ext(&"jpg".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"jpeg".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"png".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"bmp".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"gif".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"webp".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"mp3".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"ogg".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"wav".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"opus".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"mp4".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"webm".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"ogv".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"any".to_string()),
UploadType::Doc
);
assert_eq!(
upload_type_from_file_ext(&"jszaj".to_string()),
UploadType::Doc
);
}
#[test]
fn should_get_filename_extension() {
fn valid_ext(ext: Result<String, String>) -> String {
assert!(ext.is_ok());
ext.unwrap()
}
let filename = String::from("somefilename.png");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "png");
let filename = String::from("aïe aïe aïe.wav");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "wav");
let filename = String::from("salut Ça va.machin.jpg");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "jpg");
let filename = String::from("no extension");
let ext = file_ext(&filename);
assert!(ext.is_err());
}
}
//! # WEB SERVER FOR THE KUADRADO SOFTWARE WEBSITE
mod app_state;
mod core;
mod crypto;
mod env;
mod init_admin;
......@@ -11,6 +12,7 @@ mod static_view;
mod tls;
mod view;
mod view_resource;
use crate::core::static_files::StaticFilesIndex;
use actix_files::Files;
use actix_web::{
middleware::{normalize::TrailingSlash, Logger, NormalizePath},
......@@ -37,6 +39,10 @@ async fn main() -> std::io::Result<()> {
let server_port_tls = env_var("SERVER_PORT_TLS").expect("SERVER_PORT_TLS is not defined.");
let app_state = AppState::with_default_admin_user().await;
let mut static_files_index = StaticFilesIndex(Vec::new());
static_files_index.rebuild(&app_state.env);
let static_files_index = Data::new(std::sync::Mutex::new(static_files_index));
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
......@@ -47,6 +53,7 @@ async fn main() -> std::io::Result<()> {
)]))
.wrap(actix_web::middleware::Compress::default())
.app_data(Data::new(app_state.clone()))
.app_data(Data::clone(&static_files_index))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
......
use crate::{middleware::AuthenticatedAdminMiddleware, AppState};
use crate::{core::static_files::*, middleware::AuthenticatedAdminMiddleware, AppState};
use actix_multipart::Multipart;
use actix_web::{post, web::Data, HttpRequest, HttpResponse, Responder};
use futures::StreamExt;
use std::{
fs::{create_dir_all, remove_file, File},
fs::{remove_file, File},
io::Write,
};
// TODO separate file writing logic from Multipart implementation and implement the unit tests
fn upload_data_from_multipart_field(field: &actix_multipart::Field) -> Result<UploadData, String> {
match field.content_disposition() {
Some(content_disposition) => match content_disposition.get_filename() {
Some(fname) => match file_ext(&fname.to_string()) {
Ok(ext) => Ok(UploadData {
up_type: upload_type_from_file_ext(&ext),
filename: fname.to_owned(),
}),
Err(msg) => return Err(msg),
},
None => Err("Couldn't retrieve file extension".to_string()),
},
None => Err("Missing content disposition".to_string()),
}
}
async fn write_uploaded_file(
app_state: &AppState,
field: &mut actix_multipart::Field,
filename: &String,
upload_type: UploadType,
) -> Result<String, String> {
let uploads_dir = get_uploads_dir(app_state);
let sub_dir = dirname_from_type(&upload_type);
let filepath = create_dir_if_missing(uploads_dir.join(&sub_dir)).join(&filename);
match File::create(&filepath) {
Err(e) => Err(format!("Error creating file {:?}", e)),
Ok(mut f) => {
// Field in turn is stream of *Bytes* object
while let Some(chunk) = field.next().await {
match chunk {
Ok(chunk) => {
if f.write_all(&chunk).is_err() {
remove_file(&filepath).unwrap();
return Err("Error writing chunk".to_string());
}
}
Err(e) => {
return Err(format!("Error writing file {} : {:?}", filename, e));
}
}
}
Ok(filepath.into_os_string().into_string().unwrap())
}
}
}
#[post("/post-files")]
pub async fn post_files(
app_state: Data<AppState>,
static_files_index: Data<std::sync::Mutex<StaticFilesIndex>>,
mut payload: Multipart,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
req: HttpRequest,
......@@ -19,61 +68,34 @@ pub async fn post_files(
return HttpResponse::Unauthorized().finish();
}
let uploads_dir = app_state.env.public_dir.join("uploads");
if !uploads_dir.exists() {
create_dir_all(&uploads_dir).unwrap();
}
let mut uploaded_filepathes = Vec::new();
let mut files_index = static_files_index.lock().unwrap();
while let Some(item) = payload.next().await {
match item {
Ok(mut field) => {
// Field in turn is stream of *Bytes* object
// A multipart/form-data stream has to contain `content_disposition`
let content_disposition = field
.content_disposition()
.expect("Missing Content Disposition header");
let filename = content_disposition
.get_filename()
.expect("Missing filename");
let filepath = uploads_dir.join(&filename);
let up_data = upload_data_from_multipart_field(&field);
let f = File::create(&filepath);
if f.is_err() {
return HttpResponse::InternalServerError()
.body(format!("Error creating file {:?}", f));
if let Err(msg) = up_data {
return HttpResponse::InternalServerError().body(msg);
}
let mut f = f.unwrap();
let mut error = None;
// Field in turn is stream of *Bytes* object
'chunks: while let Some(chunk) = field.next().await {
match chunk {
Ok(chunk) => {
if f.write_all(&chunk).is_err() {
error = Some("Error writing chunk".to_string());
break 'chunks;
}
}
Err(e) => {
error = Some(format!("Error writing file {} : {:?}", filename, e));
break 'chunks;
}
let up_data = up_data.unwrap();
match write_uploaded_file(
&app_state,
&mut field,
&up_data.filename,
up_data.up_type,
)
.await
{
Err(msg) => return HttpResponse::InternalServerError().body(msg),
Ok(filepath) => {
files_index.push_path(std::path::Path::new(&filepath), &app_state.env);
uploaded_filepathes.push(filepath);
}
}
if let Some(err) = error {
remove_file(&filepath).unwrap();
return HttpResponse::InternalServerError().body(err);
}
uploaded_filepathes.push(String::from(filepath.to_string_lossy()));
}
Err(e) => {
return HttpResponse::InternalServerError().body(format!("FIELD ERR {:?}", e))
......@@ -84,7 +106,7 @@ pub async fn post_files(
HttpResponse::Ok().json(uploaded_filepathes)
}
// EXAMPLE FROM ACTIX REPO
// EXAMPLE FROM ACTIX REPO (using threadpool)
// use futures::TryStreamExt;
// use std::io::Write;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment