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!({}))
-}