Add delete files and dockerize application

This commit is contained in:
2024-12-09 15:54:08 +01:00
parent 75f1e59816
commit 3e6a8e3e37
10 changed files with 136 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
const deleteForms = document.querySelectorAll('.delete-form');
const deleteData = async (form) => {
const fileIdInput = form.querySelector('.file-id');
const fileId = fileIdInput.value;
if (!fileId) {
console.warn('No file selected');
return;
}
try {
const response = await fetch(`/api/data/id/${fileId}`, {
method: 'DELETE',
});
if (response.ok) {
alert('Data deleted successfully');
window.location.reload();
fileIdInput.value = '';
} else {
console.error(
'Failed to delete data ',
response.status,
response.statusText
);
}
} catch (error) {
console.error('Error deleting data ', error);
}
};
deleteForms.forEach((form) => {
const deleteButton = form.querySelector('.delete-button');
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
deleteData(form);
});
});

View File

@@ -1,6 +1,7 @@
const form = document.getElementById('data-upload'); const form = document.getElementById('data-upload');
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('file-input');
const protectedInput = document.getElementById('protected-input'); const protectedInput = document.getElementById('protected');
const uniqueNameInput = document.getElementById('unique_name');
const uploadData = async () => { const uploadData = async () => {
if (!fileInput.files.length) { if (!fileInput.files.length) {
@@ -11,6 +12,7 @@ const uploadData = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
formData.append('protected', protectedInput.checked ? 'true' : 'false'); formData.append('protected', protectedInput.checked ? 'true' : 'false');
formData.append('unique_name', uniqueNameInput.checked ? 'true' : 'false');
try { try {
const response = await fetch('/api/data/upload', { const response = await fetch('/api/data/upload', {

View File

@@ -6,7 +6,11 @@
<input id="file-input" type="file" id="file" name="file" required /> <input id="file-input" type="file" id="file" name="file" required />
<div> <div>
<label class="text-white" for="protected">Protected:</label> <label class="text-white" for="protected">Protected:</label>
<input id="protected-input" type="checkbox" id="protected" name="protected" /> <input type="checkbox" id="protected" name="protected" />
</div>
<div>
<label class="text-white" for="unique_name">Unique name:</label>
<input type="checkbox" id="unique_name" name="unique_name" />
</div> </div>
<button class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" type="submit">Upload</button> <button class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" type="submit">Upload</button>
</form> </form>

View File

@@ -1,4 +1,5 @@
{% extends "website/base.html" %} {% block content %} {% extends "website/base.html" %} {% block content %}
<script src="/static/js/data-delete.js" defer></script>
<span class="mt-8"></span> <span class="mt-8"></span>
<a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" href="/upload">Add new file</a> <a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" href="/upload">Add new file</a>
<table class="table-fixed"> <table class="table-fixed">
@@ -18,6 +19,12 @@
<td>{{ file.protected }}</td> <td>{{ file.protected }}</td>
<td><a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600" <td><a class="p-2 text-gray-900 bg-yellow-500 rounded-sm shadow hover:bg-yellow-600"
href="/api/data/{{ file.file_name }}">Download</a></td> href="/api/data/{{ file.file_name }}">Download</a></td>
<td>
<form class="delete-form">
<input class="file-id" id="file_id" type="hidden" name="file_id" value="{{ file.id }}" />
<button class="p-2 text-gray-900 bg-red-500 rounded-sm shadow hover:bg-red-600 delete-button">Delete</button>
</form>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -7,7 +7,7 @@ logger:
# Enable pretty backtrace (sets RUST_BACKTRACE=1) # Enable pretty backtrace (sets RUST_BACKTRACE=1)
pretty_backtrace: true pretty_backtrace: true
# Log level, options: trace, debug, info, warn or error. # Log level, options: trace, debug, info, warn or error.
level: debug level: {{ get_env(name="LOGGER_LEVEL", default="debug") }}
# Define the logging format. options: compact, pretty or json # Define the logging format. options: compact, pretty or json
format: compact format: compact
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
@@ -19,6 +19,7 @@ server:
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT} # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
port: 5150 port: 5150
# The UI hostname or IP address that mailers will point to. # The UI hostname or IP address that mailers will point to.
binding: {{ get_env(name="BINDING", default="localhost") }}
host: {{ get_env(name="HOST", default="http://localhost") }} host: {{ get_env(name="HOST", default="http://localhost") }}
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
middlewares: middlewares:
@@ -53,25 +54,6 @@ server:
path: "assets/static" path: "assets/static"
fallback: "assets/static/404.html" fallback: "assets/static/404.html"
#
# (2) Client side app static config
# =================================
#
# Note that you need to go in `frontend` and run your frontend build first,
# e.g.: $ npm install & npm build
#
# (client-block-start)
# static:
# enable: true
# must_exist: true
# precompressed: false
# folder:
# uri: "/"
# path: "frontend/dist"
# fallback: "frontend/dist/index.html"
# (client-block-end)
#
# Worker Configuration # Worker Configuration
workers: workers:
# specifies the worker mode. Options: # specifies the worker mode. Options:

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
services:
web:
build: .
volumes:
- uploads_volume:/usr/app/uploads
environment:
- HOST=http://0.0.0.0
- DATABASE_URL=postgres://user:password@db:5432/db
- JWT_SECRET=PqRwLF2rhHe8J21oBeHychangeit
- LOGGER_LEVEL=info
- BINDING=0.0.0.0
depends_on:
db:
condition: service_healthy
ports:
- 5150:5150
db:
image: postgres:latest
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=db
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
uploads_volume:

View File

@@ -10,8 +10,7 @@ FROM debian:bookworm-slim
WORKDIR /usr/app WORKDIR /usr/app
COPY --from=builder /usr/src/assets/static /usr/app/assets/static COPY --from=builder /usr/src/assets /usr/app/assets
COPY --from=builder /usr/src/assets/static/404.html /usr/app/assets/static/404.html
COPY --from=builder /usr/src/config /usr/app/config COPY --from=builder /usr/src/config /usr/app/config
COPY --from=builder /usr/src/target/release/gabrielkaszewski_rs-cli /usr/app/gabrielkaszewski_rs-cli COPY --from=builder /usr/src/target/release/gabrielkaszewski_rs-cli /usr/app/gabrielkaszewski_rs-cli

View File

@@ -14,7 +14,7 @@ use axum_extra::TypedHeader;
use crate::models::users; use crate::models::users;
use crate::services; use crate::services;
pub async fn get_data( async fn get_data(
auth: Option<auth::JWT>, auth: Option<auth::JWT>,
range: Option<TypedHeader<Range>>, range: Option<TypedHeader<Range>>,
Path(file_name): Path<String>, Path(file_name): Path<String>,
@@ -23,7 +23,7 @@ pub async fn get_data(
services::data::serve_data_file(&auth, range, &file_name, &ctx).await services::data::serve_data_file(&auth, range, &file_name, &ctx).await
} }
pub async fn upload_data( async fn upload_data(
auth: auth::JWT, auth: auth::JWT,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
payload: Multipart, payload: Multipart,
@@ -37,9 +37,24 @@ pub async fn upload_data(
format::html("<h1>File uploaded successfully</h1>") format::html("<h1>File uploaded successfully</h1>")
} }
async fn delete_data(
auth: auth::JWT,
State(ctx): State<AppContext>,
Path(file_id): Path<i32>,
) -> Result<Response> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
services::data::delete_data_by_id(file_id, &ctx).await?;
format::html("<h1>File deleted 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))
.add("/id/:file_id", delete(delete_data))
} }

View File

@@ -109,7 +109,8 @@ pub async fn add(
let mut protected = None; let mut protected = None;
let mut file_name = None; let mut file_name = None;
let mut content = None; let mut content = None;
let mut file_path = None; let mut unique_name = None;
let mut ext = None;
while let Some(field) = payload while let Some(field) = payload
.next_field() .next_field()
@@ -128,25 +129,23 @@ pub async fn add(
.parse::<bool>() .parse::<bool>()
.map_err(|_| ModelError::Any("Failed to parse bool".into()))?; .map_err(|_| ModelError::Any("Failed to parse bool".into()))?;
protected = Some(value); protected = Some(value);
} },
"unique_name" => {
let value = field
.text()
.await
.map_err(|_| ModelError::Any("Failed to get text".into()))?
.parse::<bool>()
.map_err(|_| ModelError::Any("Failed to parse bool".into()))?;
unique_name = Some(value);
},
"file" => { "file" => {
let (og_file_name, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?; let (og_file_name, extension) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
tracing::info!("File name: {:?}", og_file_name); let og_file_name = format!("{}.{}", og_file_name, extension);
ext = Some(extension.clone());
let temp_file_name = if uuid_name { file_name = Some(og_file_name.clone());
let temp_file_name = uuid::Uuid::new_v4().to_string();
format!("{}.{}", temp_file_name, ext)
} else {
og_file_name.to_string()
};
tracing::info!("Temp file name: {:?}", temp_file_name);
file_name = Some(temp_file_name.clone());
let path = PathBuf::from(temp_file_name);
file_path = Some(path.clone());
let data_content = field let data_content = field
.bytes() .bytes()
@@ -161,9 +160,20 @@ pub async fn add(
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 unique_name = unique_name.unwrap_or(true);
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 file_name = match (uuid_name, unique_name) {
(true, true) => {
let temp_file_name = uuid::Uuid::new_v4().to_string();
format!("{}.{}", temp_file_name, ext.unwrap_or("txt".to_string()))
}
_ => file_name,
};
let path = PathBuf::from(file_name.clone());
let mut item = ActiveModel { let mut item = ActiveModel {
..Default::default() ..Default::default()
}; };
@@ -174,13 +184,12 @@ pub async fn add(
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()))?; let content = content.ok_or_else(|| ModelError::Any("Content is required".into()))?;
match ctx match ctx
.storage .storage
.as_ref() .as_ref()
.upload(file_path.as_path(), &content) .upload(path.as_path(), &content)
.await .await
{ {
Ok(_) => {} Ok(_) => {}
@@ -232,7 +241,7 @@ pub async fn delete_data_by_id(id: i32, ctx: &AppContext) -> ModelResult<()> {
match data { match data {
Some(data) => { Some(data) => {
let path = PathBuf::from(&data.file_url); let path = PathBuf::from(&data.file_name);
match ctx.storage.as_ref().delete(&path).await { match ctx.storage.as_ref().delete(&path).await {
Ok(_) => {} Ok(_) => {}
Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())), Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())),