From 1bb774eb007644f5b3c41b2b7adeaf1091915844 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 3 Nov 2024 02:46:38 +0100 Subject: [PATCH] Data upload from web --- .gitignore | 5 +- Cargo.lock | 5 +- Cargo.toml | 1 + assets/static/js/data-upload.js | 39 ++++++++++ assets/views/website/data-upload.html | 12 +++ config/development.yaml | 4 + migration/src/lib.rs | 4 +- migration/src/m20241030_024830_data.rs | 5 +- .../m20241102_204505_add_file_name_to_data.rs | 35 --------- src/controllers/data.rs | 10 ++- src/controllers/website.rs | 14 +++- src/services/data.rs | 76 +++++++++++++------ src/views/auth.rs | 5 ++ src/views/data.rs | 5 ++ src/views/mod.rs | 1 + src/views/website.rs | 4 - 16 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 assets/static/js/data-upload.js create mode 100644 assets/views/website/data-upload.html delete mode 100644 migration/src/m20241102_204505_add_file_name_to_data.rs create mode 100644 src/views/data.rs diff --git a/.gitignore b/.gitignore index d83d21a..7536997 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -*.sqlite \ No newline at end of file +*.sqlite + +uploads/ +assets/static/css/main.css \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 379ae55..2f0209e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -783,9 +783,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" @@ -1661,6 +1661,7 @@ dependencies = [ "axum", "axum-extra", "axum-range", + "bytes", "chrono", "fluent-templates", "include_dir", diff --git a/Cargo.toml b/Cargo.toml index 9c46899..2c02bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ fluent-templates = { version = "0.8.0", features = ["tera"] } unic-langid = "0.9.4" axum-range = "0.4.0" axum-extra = { version = "0.9.4", features = ["multipart", "typed-header", "cookie"] } +bytes = "1.8.0" # /view engine [[bin]] diff --git a/assets/static/js/data-upload.js b/assets/static/js/data-upload.js new file mode 100644 index 0000000..11e9222 --- /dev/null +++ b/assets/static/js/data-upload.js @@ -0,0 +1,39 @@ +const form = document.getElementById('data-upload'); +const fileInput = document.getElementById('file-input'); +const protectedInput = document.getElementById('protected-input'); + +const uploadData = async () => { + if (!fileInput.files.length) { + console.warn('No file selected'); + return; + } + + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + formData.append('protected', protectedInput.checked ? 'true' : 'false'); + + try { + const response = await fetch('/api/data/upload', { + method: 'POST', + body: formData, + }); + + if (response.ok) { + alert('Data uploaded successfully'); + form.reset(); + } else { + console.error( + 'Failed to upload data ', + response.status, + response.statusText + ); + } + } catch (error) { + console.error('Error uploading data ', error); + } +}; + +form.addEventListener('submit', (event) => { + event.preventDefault(); + uploadData(); +}); diff --git a/assets/views/website/data-upload.html b/assets/views/website/data-upload.html new file mode 100644 index 0000000..6866490 --- /dev/null +++ b/assets/views/website/data-upload.html @@ -0,0 +1,12 @@ +{% extends "website/base.html" %} {% block content %} + +
+
+ + + + + +
+ +{% endblock content%} diff --git a/config/development.yaml b/config/development.yaml index e50f678..7b320ad 100644 --- a/config/development.yaml +++ b/config/development.yaml @@ -40,6 +40,10 @@ server: # ==================================== # # for use with the view_engine in initializers/view_engine.rs + limit_payload: + enable: true + body_limit: 1gb + static: enable: true must_exist: true diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 924e93e..bf6d965 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -8,7 +8,6 @@ mod m20241029_235230_skills; mod m20241030_002154_jobs; mod m20241030_024340_projects; mod m20241030_024830_data; -mod m20241102_204505_add_file_name_to_data; pub struct Migrator; #[async_trait::async_trait] @@ -16,7 +15,6 @@ impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ // inject-below - Box::new(m20241102_204505_add_file_name_to_data::Migration), Box::new(m20241030_024830_data::Migration), Box::new(m20241030_024340_projects::Migration), Box::new(m20241030_002154_jobs::Migration), @@ -24,4 +22,4 @@ impl MigratorTrait for Migrator { Box::new(m20220101_000001_users::Migration), ] } -} \ No newline at end of file +} diff --git a/migration/src/m20241030_024830_data.rs b/migration/src/m20241030_024830_data.rs index ae511cc..40d814d 100644 --- a/migration/src/m20241030_024830_data.rs +++ b/migration/src/m20241030_024830_data.rs @@ -13,6 +13,7 @@ impl MigrationTrait for Migration { .col(pk_auto(Data::Id)) .col(string(Data::FileUrl)) .col(boolean(Data::Protected)) + .col(string(Data::FileName)) .to_owned(), ) .await @@ -31,7 +32,5 @@ enum Data { Id, FileUrl, Protected, - + FileName, } - - diff --git a/migration/src/m20241102_204505_add_file_name_to_data.rs b/migration/src/m20241102_204505_add_file_name_to_data.rs deleted file mode 100644 index 2c6dd64..0000000 --- a/migration/src/m20241102_204505_add_file_name_to_data.rs +++ /dev/null @@ -1,35 +0,0 @@ -use sea_orm_migration::{prelude::*, schema::*}; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[derive(DeriveIden)] -enum Data { - Table, - FileName, -} - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Data::Table) - .add_column_if_not_exists(string(Data::FileName)) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .alter_table( - Table::alter() - .table(Data::Table) - .drop_column(Data::FileName) - .to_owned(), - ) - .await - } -} diff --git a/src/controllers/data.rs b/src/controllers/data.rs index 9f14d46..59f1a72 100644 --- a/src/controllers/data.rs +++ b/src/controllers/data.rs @@ -11,10 +11,11 @@ use axum_range::Ranged; use axum_extra::headers::Range; use axum_extra::TypedHeader; +use crate::models::users; use crate::services; pub async fn get_data( - auth: auth::JWT, + auth: Option, range: Option>, Path(file_name): Path, State(ctx): State, @@ -27,13 +28,18 @@ pub async fn upload_data( State(ctx): State, payload: Multipart, ) -> Result { + match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await { + Ok(_) => {} + Err(_) => return unauthorized("Unauthorized"), + } + services::data::add(&auth, &ctx, payload).await?; format::html("

File uploaded successfully

") } pub fn routes() -> Routes { Routes::new() - .prefix("api/data/") + .prefix("api/data") .add("/upload", post(upload_data)) .add("/:file_name", get(get_data)) } diff --git a/src/controllers/website.rs b/src/controllers/website.rs index b7a016a..e638887 100644 --- a/src/controllers/website.rs +++ b/src/controllers/website.rs @@ -3,6 +3,7 @@ #![allow(clippy::unused_async)] use loco_rs::prelude::*; +use crate::models::users; use crate::views; pub async fn render_index( @@ -13,11 +14,22 @@ pub async fn render_index( } pub async fn render_login(ViewEngine(v): ViewEngine) -> impl IntoResponse { - views::website::login(v).await + views::auth::login(v).await +} + +pub async fn render_upload( + auth: auth::JWT, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; + + views::data::upload(v).await } pub fn routes() -> Routes { Routes::new() .add("/", get(render_index)) + .add("/upload", get(render_upload)) .add("/login", get(render_login)) } diff --git a/src/services/data.rs b/src/services/data.rs index 31a5430..8ab2627 100644 --- a/src/services/data.rs +++ b/src/services/data.rs @@ -21,29 +21,43 @@ pub async fn get_data_by_file_name(file_name: &str, ctx: &AppContext) -> ModelRe } pub async fn serve_data_file( - auth: &auth::JWT, + auth: &Option, range: Option>, file_name: &str, ctx: &AppContext, ) -> Result>> { - let data = get_data_by_file_name(&file_name, &ctx).await?; + let data = match get_data_by_file_name(&file_name, &ctx).await { + Ok(data) => data, + Err(_) => return not_found(), + }; + if data.protected { - match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await { - Ok(_) => {} - Err(_) => return unauthorized("Unauthorized"), + match auth { + None => return unauthorized("Unauthorized"), + Some(auth) => match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await { + Ok(_) => {} + Err(_) => return unauthorized("Unauthorized"), + }, } } - let file = File::open(&data.file_url).await?; - let body = KnownSize::file(file).await?; - let range = range.map(|TypedHeader(range)| range); - Ok(Ranged::new(range, body)) + match File::open(&data.file_url).await { + Ok(file) => { + let body = KnownSize::file(file).await?; + let range = range.map(|TypedHeader(range)| range); + Ok(Ranged::new(range, body)) + } + Err(_) => return not_found(), + } } pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> ModelResult { let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; + let mut protected = None; let mut file_name = None; + let mut content = None; + let mut file_path = None; while let Some(field) = payload .next_field() @@ -64,26 +78,25 @@ pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> protected = Some(value); } "file" => { - file_name = match field.file_name() { - Some(file_name) => Some(String::from(file_name)), - None => return Err(ModelError::Any("Failed to get file name".into())), - }; + let og_file_name = field + .file_name() + .ok_or_else(|| ModelError::Any("Failed to get file name".into()))?; + let ext = String::from(og_file_name.split('.').last().unwrap_or("txt")); - if file_name.is_none() { - return Err(ModelError::Any("Failed to get file name".into())); - } + let temp_file_name = uuid::Uuid::new_v4().to_string(); + let temp_file_name = format!("{}.{}", temp_file_name, ext); - let path = PathBuf::from("uploads").join(file_name.as_ref().unwrap()); + file_name = Some(temp_file_name.clone()); - let content = field + let path = PathBuf::from(temp_file_name); + file_path = Some(path.clone()); + + let data_content = field .bytes() .await .map_err(|_| ModelError::Any("Failed to get bytes".into()))?; - - match ctx.storage.as_ref().upload(path.as_path(), &content).await { - Ok(_) => {} - Err(_) => return Err(ModelError::Any("Failed to save file to storage".into())), - } + + content = Some(data_content.clone()); } _ => {} } @@ -91,6 +104,7 @@ pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> let protected = protected.ok_or_else(|| ModelError::Any("Protected field is required".into()))?; + let file_name = file_name.ok_or_else(|| ModelError::Any("File field is required".into()))?; let mut item = ActiveModel { @@ -98,8 +112,22 @@ pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> }; item.protected = Set(protected); - item.file_name = Set(file_name); + item.file_name = Set(file_name.clone()); + item.file_url = Set(format!("uploads/{}", file_name)); let item = item.insert(&ctx.db).await?; + + let file_path = file_path.ok_or_else(|| ModelError::Any("File path is required".into()))?; + let content = content.ok_or_else(|| ModelError::Any("Content is required".into()))?; + + match ctx + .storage + .as_ref() + .upload(file_path.as_path(), &content) + .await + { + Ok(_) => {} + Err(_) => return Err(ModelError::Any("Failed to save file to storage".into())), + } Ok(item) } diff --git a/src/views/auth.rs b/src/views/auth.rs index 3d2d74f..46f7b28 100644 --- a/src/views/auth.rs +++ b/src/views/auth.rs @@ -1,3 +1,4 @@ +use loco_rs::prelude::*; use serde::{Deserialize, Serialize}; use crate::models::_entities::users; @@ -39,3 +40,7 @@ impl CurrentResponse { } } } + +pub async fn login(v: impl ViewRenderer) -> Result { + format::render().view(&v, "website/login.html", data!({})) +} diff --git a/src/views/data.rs b/src/views/data.rs new file mode 100644 index 0000000..a59f0c8 --- /dev/null +++ b/src/views/data.rs @@ -0,0 +1,5 @@ +use loco_rs::prelude::*; + +pub async fn upload(v: impl ViewRenderer) -> Result { + format::render().view(&v, "website/data-upload.html", data!({})) +} diff --git a/src/views/mod.rs b/src/views/mod.rs index 41de790..c6b6c71 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -1,2 +1,3 @@ pub mod auth; +pub mod data; pub mod website; diff --git a/src/views/website.rs b/src/views/website.rs index d9c5948..54e19c4 100644 --- a/src/views/website.rs +++ b/src/views/website.rs @@ -12,7 +12,3 @@ pub async fn index(v: impl ViewRenderer, ctx: &AppContext) -> Result Result { - format::render().view(&v, "website/login.html", data!({})) -}