Data upload from web
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,4 +16,7 @@ target/
|
|||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
||||||
|
uploads/
|
||||||
|
assets/static/css/main.css
|
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -783,9 +783,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.7.2"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
@@ -1661,6 +1661,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-range",
|
"axum-range",
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
@@ -43,6 +43,7 @@ fluent-templates = { version = "0.8.0", features = ["tera"] }
|
|||||||
unic-langid = "0.9.4"
|
unic-langid = "0.9.4"
|
||||||
axum-range = "0.4.0"
|
axum-range = "0.4.0"
|
||||||
axum-extra = { version = "0.9.4", features = ["multipart", "typed-header", "cookie"] }
|
axum-extra = { version = "0.9.4", features = ["multipart", "typed-header", "cookie"] }
|
||||||
|
bytes = "1.8.0"
|
||||||
# /view engine
|
# /view engine
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
39
assets/static/js/data-upload.js
Normal file
39
assets/static/js/data-upload.js
Normal file
@@ -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();
|
||||||
|
});
|
12
assets/views/website/data-upload.html
Normal file
12
assets/views/website/data-upload.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "website/base.html" %} {% block content %}
|
||||||
|
<script src="/static/js/data-upload.js" defer></script>
|
||||||
|
<div class="w-full mt-16"></div>
|
||||||
|
<form id="data-upload" class="text-black">
|
||||||
|
<label class="text-white" for="file">Upload a file:</label>
|
||||||
|
<input id="file-input" type="file" id="file" name="file" required />
|
||||||
|
<label class="text-white" for="protected">Protected:</label>
|
||||||
|
<input id="protected-input" type="checkbox" id="protected" name="protected" />
|
||||||
|
<button class="text-white" type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content%}
|
@@ -40,6 +40,10 @@ server:
|
|||||||
# ====================================
|
# ====================================
|
||||||
#
|
#
|
||||||
# for use with the view_engine in initializers/view_engine.rs
|
# for use with the view_engine in initializers/view_engine.rs
|
||||||
|
limit_payload:
|
||||||
|
enable: true
|
||||||
|
body_limit: 1gb
|
||||||
|
|
||||||
static:
|
static:
|
||||||
enable: true
|
enable: true
|
||||||
must_exist: true
|
must_exist: true
|
||||||
|
@@ -8,7 +8,6 @@ mod m20241029_235230_skills;
|
|||||||
mod m20241030_002154_jobs;
|
mod m20241030_002154_jobs;
|
||||||
mod m20241030_024340_projects;
|
mod m20241030_024340_projects;
|
||||||
mod m20241030_024830_data;
|
mod m20241030_024830_data;
|
||||||
mod m20241102_204505_add_file_name_to_data;
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -16,7 +15,6 @@ impl MigratorTrait for Migrator {
|
|||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
vec![
|
vec![
|
||||||
// inject-below
|
// inject-below
|
||||||
Box::new(m20241102_204505_add_file_name_to_data::Migration),
|
|
||||||
Box::new(m20241030_024830_data::Migration),
|
Box::new(m20241030_024830_data::Migration),
|
||||||
Box::new(m20241030_024340_projects::Migration),
|
Box::new(m20241030_024340_projects::Migration),
|
||||||
Box::new(m20241030_002154_jobs::Migration),
|
Box::new(m20241030_002154_jobs::Migration),
|
||||||
@@ -24,4 +22,4 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20220101_000001_users::Migration),
|
Box::new(m20220101_000001_users::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ impl MigrationTrait for Migration {
|
|||||||
.col(pk_auto(Data::Id))
|
.col(pk_auto(Data::Id))
|
||||||
.col(string(Data::FileUrl))
|
.col(string(Data::FileUrl))
|
||||||
.col(boolean(Data::Protected))
|
.col(boolean(Data::Protected))
|
||||||
|
.col(string(Data::FileName))
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -31,7 +32,5 @@ enum Data {
|
|||||||
Id,
|
Id,
|
||||||
FileUrl,
|
FileUrl,
|
||||||
Protected,
|
Protected,
|
||||||
|
FileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,10 +11,11 @@ use axum_range::Ranged;
|
|||||||
use axum_extra::headers::Range;
|
use axum_extra::headers::Range;
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
|
|
||||||
|
use crate::models::users;
|
||||||
use crate::services;
|
use crate::services;
|
||||||
|
|
||||||
pub async fn get_data(
|
pub async fn get_data(
|
||||||
auth: auth::JWT,
|
auth: Option<auth::JWT>,
|
||||||
range: Option<TypedHeader<Range>>,
|
range: Option<TypedHeader<Range>>,
|
||||||
Path(file_name): Path<String>,
|
Path(file_name): Path<String>,
|
||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
@@ -27,13 +28,18 @@ pub async fn upload_data(
|
|||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
payload: Multipart,
|
payload: Multipart,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
|
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => return unauthorized("Unauthorized"),
|
||||||
|
}
|
||||||
|
|
||||||
services::data::add(&auth, &ctx, payload).await?;
|
services::data::add(&auth, &ctx, payload).await?;
|
||||||
format::html("<h1>File uploaded successfully</h1>")
|
format::html("<h1>File uploaded successfully</h1>")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.prefix("api/data/")
|
.prefix("api/data")
|
||||||
.add("/upload", post(upload_data))
|
.add("/upload", post(upload_data))
|
||||||
.add("/:file_name", get(get_data))
|
.add("/:file_name", get(get_data))
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
#![allow(clippy::unused_async)]
|
#![allow(clippy::unused_async)]
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
use crate::models::users;
|
||||||
use crate::views;
|
use crate::views;
|
||||||
|
|
||||||
pub async fn render_index(
|
pub async fn render_index(
|
||||||
@@ -13,11 +14,22 @@ pub async fn render_index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_login(ViewEngine(v): ViewEngine<TeraView>) -> impl IntoResponse {
|
pub async fn render_login(ViewEngine(v): ViewEngine<TeraView>) -> impl IntoResponse {
|
||||||
views::website::login(v).await
|
views::auth::login(v).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_upload(
|
||||||
|
auth: auth::JWT,
|
||||||
|
ViewEngine(v): ViewEngine<TeraView>,
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||||
|
|
||||||
|
views::data::upload(v).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.add("/", get(render_index))
|
.add("/", get(render_index))
|
||||||
|
.add("/upload", get(render_upload))
|
||||||
.add("/login", get(render_login))
|
.add("/login", get(render_login))
|
||||||
}
|
}
|
||||||
|
@@ -21,29 +21,43 @@ pub async fn get_data_by_file_name(file_name: &str, ctx: &AppContext) -> ModelRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve_data_file(
|
pub async fn serve_data_file(
|
||||||
auth: &auth::JWT,
|
auth: &Option<auth::JWT>,
|
||||||
range: Option<TypedHeader<Range>>,
|
range: Option<TypedHeader<Range>>,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
ctx: &AppContext,
|
ctx: &AppContext,
|
||||||
) -> Result<Ranged<KnownSize<File>>> {
|
) -> Result<Ranged<KnownSize<File>>> {
|
||||||
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 {
|
if data.protected {
|
||||||
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
match auth {
|
||||||
Ok(_) => {}
|
None => return unauthorized("Unauthorized"),
|
||||||
Err(_) => 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?;
|
match File::open(&data.file_url).await {
|
||||||
let body = KnownSize::file(file).await?;
|
Ok(file) => {
|
||||||
let range = range.map(|TypedHeader(range)| range);
|
let body = KnownSize::file(file).await?;
|
||||||
Ok(Ranged::new(range, body))
|
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<Model> {
|
pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> ModelResult<Model> {
|
||||||
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||||
|
|
||||||
let mut protected = None;
|
let mut protected = None;
|
||||||
let mut file_name = None;
|
let mut file_name = None;
|
||||||
|
let mut content = None;
|
||||||
|
let mut file_path = None;
|
||||||
|
|
||||||
while let Some(field) = payload
|
while let Some(field) = payload
|
||||||
.next_field()
|
.next_field()
|
||||||
@@ -64,26 +78,25 @@ pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) ->
|
|||||||
protected = Some(value);
|
protected = Some(value);
|
||||||
}
|
}
|
||||||
"file" => {
|
"file" => {
|
||||||
file_name = match field.file_name() {
|
let og_file_name = field
|
||||||
Some(file_name) => Some(String::from(file_name)),
|
.file_name()
|
||||||
None => return Err(ModelError::Any("Failed to get file name".into())),
|
.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() {
|
let temp_file_name = uuid::Uuid::new_v4().to_string();
|
||||||
return Err(ModelError::Any("Failed to get file name".into()));
|
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()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ModelError::Any("Failed to get bytes".into()))?;
|
.map_err(|_| ModelError::Any("Failed to get bytes".into()))?;
|
||||||
|
|
||||||
match ctx.storage.as_ref().upload(path.as_path(), &content).await {
|
content = Some(data_content.clone());
|
||||||
Ok(_) => {}
|
|
||||||
Err(_) => return Err(ModelError::Any("Failed to save file to storage".into())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -91,6 +104,7 @@ pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) ->
|
|||||||
|
|
||||||
let protected =
|
let protected =
|
||||||
protected.ok_or_else(|| ModelError::Any("Protected field is required".into()))?;
|
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 file_name = file_name.ok_or_else(|| ModelError::Any("File field is required".into()))?;
|
||||||
|
|
||||||
let mut item = ActiveModel {
|
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.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 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)
|
Ok(item)
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
use loco_rs::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::models::_entities::users;
|
use crate::models::_entities::users;
|
||||||
@@ -39,3 +40,7 @@ impl CurrentResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||||
|
format::render().view(&v, "website/login.html", data!({}))
|
||||||
|
}
|
||||||
|
5
src/views/data.rs
Normal file
5
src/views/data.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
pub async fn upload(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||||
|
format::render().view(&v, "website/data-upload.html", data!({}))
|
||||||
|
}
|
@@ -1,2 +1,3 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod data;
|
||||||
pub mod website;
|
pub mod website;
|
||||||
|
@@ -12,7 +12,3 @@ pub async fn index(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoRe
|
|||||||
data!({ "skills": skills, "jobs": jobs }),
|
data!({ "skills": skills, "jobs": jobs }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
|
||||||
format::render().view(&v, "website/login.html", data!({}))
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user