Add delete files and dockerize application
This commit is contained in:
File diff suppressed because one or more lines are too long
38
assets/static/js/data-delete.js
Normal file
38
assets/static/js/data-delete.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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', {
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
32
docker-compose.yml
Normal 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:
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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))
|
||||||
}
|
}
|
||||||
|
@@ -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())),
|
||||||
|
Reference in New Issue
Block a user