Delete pool
This commit is contained in:
parent
7553dd31dc
commit
261623960e
7 changed files with 100 additions and 26 deletions
|
|
@ -19,21 +19,21 @@ Still very much an early WIP.
|
||||||
|
|
||||||
### Roadmap
|
### Roadmap
|
||||||
|
|
||||||
- [ ] Delete pools
|
|
||||||
- [ ] Logging and improved error handling
|
- [ ] Logging and improved error handling
|
||||||
- [ ] Lossless compression
|
- [ ] Lossless compression
|
||||||
- [ ] Caching
|
- [ ] Caching
|
||||||
- [ ] Text media
|
- [ ] Text media
|
||||||
- [ ] Improve CSS
|
- [ ] Improve CSS
|
||||||
- [ ] Garbage collection background tasks (tags, pools)
|
- [ ] Background tasks for garbage collection (dangling tags)
|
||||||
- [ ] User management
|
- [ ] User management
|
||||||
|
- [ ] Testing
|
||||||
- [ ] Migrate to Cot...?
|
- [ ] Migrate to Cot...?
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
### Prerequisites
|
### Dependencies
|
||||||
|
|
||||||
- `ffmpeg` and `ffprobe`
|
- `ffmpeg` (with `ffprobe`)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
|
|
||||||
18
src/error.rs
18
src/error.rs
|
|
@ -8,32 +8,46 @@ use axum::{
|
||||||
#[template(path = "pages/not_found.html")]
|
#[template(path = "pages/not_found.html")]
|
||||||
struct NotFoundTemplate;
|
struct NotFoundTemplate;
|
||||||
|
|
||||||
|
/// Errors from Samey.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SameyError {
|
pub enum SameyError {
|
||||||
|
/// Integer conversion error.
|
||||||
#[error("Integer conversion error: {0}")]
|
#[error("Integer conversion error: {0}")]
|
||||||
IntConversion(#[from] std::num::TryFromIntError),
|
IntConversion(#[from] std::num::TryFromIntError),
|
||||||
|
/// Integer parsing error.
|
||||||
#[error("Integer parsing error: {0}")]
|
#[error("Integer parsing error: {0}")]
|
||||||
IntParse(#[from] std::num::ParseIntError),
|
IntParse(#[from] std::num::ParseIntError),
|
||||||
|
/// IO error.
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
IO(#[from] std::io::Error),
|
||||||
|
/// Task error.
|
||||||
#[error("Task error: {0}")]
|
#[error("Task error: {0}")]
|
||||||
Join(#[from] tokio::task::JoinError),
|
Join(#[from] tokio::task::JoinError),
|
||||||
|
/// Template render error.
|
||||||
#[error("Template render error: {0}")]
|
#[error("Template render error: {0}")]
|
||||||
Render(#[from] askama::Error),
|
Render(#[from] askama::Error),
|
||||||
|
/// Database error.
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
Database(#[from] sea_orm::error::DbErr),
|
Database(#[from] sea_orm::error::DbErr),
|
||||||
|
/// File streaming error.
|
||||||
#[error("File streaming error: {0}")]
|
#[error("File streaming error: {0}")]
|
||||||
Multipart(#[from] axum::extract::multipart::MultipartError),
|
Multipart(#[from] axum::extract::multipart::MultipartError),
|
||||||
|
/// Image error.
|
||||||
#[error("Image error: {0}")]
|
#[error("Image error: {0}")]
|
||||||
Image(#[from] image::ImageError),
|
Image(#[from] image::ImageError),
|
||||||
#[error("Not found")]
|
/// Authentication error.
|
||||||
NotFound,
|
|
||||||
#[error("Authentication error: {0}")]
|
#[error("Authentication error: {0}")]
|
||||||
Authentication(String),
|
Authentication(String),
|
||||||
|
/// Not found.
|
||||||
|
#[error("Not found")]
|
||||||
|
NotFound,
|
||||||
|
/// Not allowed.
|
||||||
#[error("Not allowed")]
|
#[error("Not allowed")]
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
/// Bad request.
|
||||||
#[error("Bad request: {0}")]
|
#[error("Bad request: {0}")]
|
||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
|
/// Custom internal error.
|
||||||
#[error("Internal error: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/lib.rs
34
src/lib.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Sam's small image board.
|
||||||
|
|
||||||
pub(crate) mod auth;
|
pub(crate) mod auth;
|
||||||
pub(crate) mod config;
|
pub(crate) mod config;
|
||||||
pub(crate) mod entities;
|
pub(crate) mod entities;
|
||||||
|
|
@ -63,14 +65,26 @@ pub(crate) struct AppState {
|
||||||
app_config: Arc<RwLock<AppConfig>>,
|
app_config: Arc<RwLock<AppConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to create a single user.
|
||||||
|
///
|
||||||
|
/// You can specify if they must be an admin user via the `is_admin` flag.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use samey::create_user;
|
||||||
|
///
|
||||||
|
/// # async fn _main() {
|
||||||
|
/// let db = sea_orm::Database::connect("sqlite:db.sqlite3?mode=rwc").await.unwrap();
|
||||||
|
/// create_user(db, "admin", "secretPassword", true).await.expect("Unable to add admin user");
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
username: String,
|
username: &str,
|
||||||
password: String,
|
password: &str,
|
||||||
is_admin: bool,
|
is_admin: bool,
|
||||||
) -> Result<(), SameyError> {
|
) -> Result<(), SameyError> {
|
||||||
SameyUser::insert(samey_user::ActiveModel {
|
SameyUser::insert(samey_user::ActiveModel {
|
||||||
username: Set(username),
|
username: Set(username.into()),
|
||||||
password: Set(generate_hash(password)),
|
password: Set(generate_hash(password)),
|
||||||
is_admin: Set(is_admin),
|
is_admin: Set(is_admin),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -80,6 +94,18 @@ pub async fn create_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an Axum router for a Samey application.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use samey::get_router;
|
||||||
|
///
|
||||||
|
/// # async fn _main() {
|
||||||
|
/// let db = sea_orm::Database::connect("sqlite:db.sqlite3?mode=rwc").await.unwrap();
|
||||||
|
/// let app = get_router(db, "files").await.unwrap();
|
||||||
|
/// let listener = tokio::net::TcpListener::bind(("0.0.0.0", 3000)).await.unwrap();
|
||||||
|
/// axum::serve(listener, app).await.unwrap();
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub async fn get_router(
|
pub async fn get_router(
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
files_dir: impl AsRef<Path>,
|
files_dir: impl AsRef<Path>,
|
||||||
|
|
@ -123,7 +149,7 @@ pub async fn get_router(
|
||||||
.route_with_tsr("/pools", get(get_pools))
|
.route_with_tsr("/pools", get(get_pools))
|
||||||
.route_with_tsr("/pools/{page}", get(get_pools_page))
|
.route_with_tsr("/pools/{page}", get(get_pools_page))
|
||||||
.route_with_tsr("/pool", post(create_pool))
|
.route_with_tsr("/pool", post(create_pool))
|
||||||
.route_with_tsr("/pool/{pool_id}", get(view_pool))
|
.route_with_tsr("/pool/{pool_id}", get(view_pool).delete(delete_pool))
|
||||||
.route_with_tsr("/pool/{pool_id}/name", put(change_pool_name))
|
.route_with_tsr("/pool/{pool_id}/name", put(change_pool_name))
|
||||||
.route_with_tsr("/pool/{pool_id}/public", put(change_pool_visibility))
|
.route_with_tsr("/pool/{pool_id}/public", put(change_pool_visibility))
|
||||||
.route_with_tsr("/pool/{pool_id}/post", post(add_post_to_pool))
|
.route_with_tsr("/pool/{pool_id}/post", post(add_post_to_pool))
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::AddAdminUser { username, password } => {
|
Commands::AddAdminUser { username, password } => {
|
||||||
create_user(db, username, password, true)
|
create_user(db, &username, &password, true)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to add admin user");
|
.expect("Unable to add admin user");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub(crate) fn generate_thumbnail(
|
||||||
max_thumbnail_dimension: u32,
|
max_thumbnail_dimension: u32,
|
||||||
) -> Result<(), SameyError> {
|
) -> Result<(), SameyError> {
|
||||||
let status = Command::new("ffmpeg")
|
let status = Command::new("ffmpeg")
|
||||||
.args(&[
|
.args([
|
||||||
"-i",
|
"-i",
|
||||||
input_path,
|
input_path,
|
||||||
"-vf",
|
"-vf",
|
||||||
|
|
@ -39,7 +39,7 @@ pub(crate) fn generate_thumbnail(
|
||||||
|
|
||||||
pub(crate) fn get_dimensions_for_video(input_path: &str) -> Result<(u32, u32), SameyError> {
|
pub(crate) fn get_dimensions_for_video(input_path: &str) -> Result<(u32, u32), SameyError> {
|
||||||
let output = Command::new("ffprobe")
|
let output = Command::new("ffprobe")
|
||||||
.args(&[
|
.args([
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
"-select_streams",
|
"-select_streams",
|
||||||
|
|
|
||||||
46
src/views.rs
46
src/views.rs
|
|
@ -357,6 +357,7 @@ pub(crate) async fn upload(
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.open(&file_path)?;
|
.open(&file_path)?;
|
||||||
while let Some(chunk) = field.chunk().await? {
|
while let Some(chunk) = field.chunk().await? {
|
||||||
file.write_all(&chunk)?;
|
file.write_all(&chunk)?;
|
||||||
|
|
@ -407,6 +408,7 @@ pub(crate) async fn upload(
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.open(&file_path)?;
|
.open(&file_path)?;
|
||||||
while let Some(chunk) = field.chunk().await? {
|
while let Some(chunk) = field.chunk().await? {
|
||||||
file.write_all(&chunk)?;
|
file.write_all(&chunk)?;
|
||||||
|
|
@ -528,13 +530,13 @@ pub(crate) async fn search_tags(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState { db, .. }): State<AppState>,
|
||||||
Form(body): Form<SearchTagsForm>,
|
Form(body): Form<SearchTagsForm>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let tags = match body.tags[..body.selection_end].split(' ').last() {
|
let tags = match body.tags[..body.selection_end].split(' ').next_back() {
|
||||||
Some(mut tag) => {
|
Some(mut tag) => {
|
||||||
tag = tag.trim();
|
tag = tag.trim();
|
||||||
if tag.is_empty() {
|
if tag.is_empty() {
|
||||||
vec![]
|
vec![]
|
||||||
} else if tag.starts_with(NEGATIVE_PREFIX) {
|
} else if let Some(stripped_tag) = tag.strip_prefix(NEGATIVE_PREFIX) {
|
||||||
if tag[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) {
|
if stripped_tag.starts_with(RATING_PREFIX) {
|
||||||
[
|
[
|
||||||
format!("{}u", RATING_PREFIX),
|
format!("{}u", RATING_PREFIX),
|
||||||
format!("{}s", RATING_PREFIX),
|
format!("{}s", RATING_PREFIX),
|
||||||
|
|
@ -542,7 +544,7 @@ pub(crate) async fn search_tags(
|
||||||
format!("{}e", RATING_PREFIX),
|
format!("{}e", RATING_PREFIX),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|t| t.starts_with(&tag[NEGATIVE_PREFIX.len()..]))
|
.filter(|t| t.starts_with(stripped_tag))
|
||||||
.map(|tag| SearchTag {
|
.map(|tag| SearchTag {
|
||||||
value: format!("-{}", &tag),
|
value: format!("-{}", &tag),
|
||||||
name: tag,
|
name: tag,
|
||||||
|
|
@ -552,7 +554,7 @@ pub(crate) async fn search_tags(
|
||||||
SameyTag::find()
|
SameyTag::find()
|
||||||
.filter(Expr::cust_with_expr(
|
.filter(Expr::cust_with_expr(
|
||||||
"LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')",
|
"LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')",
|
||||||
tag[NEGATIVE_PREFIX.len()..].to_lowercase(),
|
stripped_tag.to_lowercase(),
|
||||||
))
|
))
|
||||||
.limit(10)
|
.limit(10)
|
||||||
.all(&db)
|
.all(&db)
|
||||||
|
|
@ -1115,12 +1117,10 @@ pub(crate) async fn sort_pool(
|
||||||
};
|
};
|
||||||
let max_index = if body.new_index == posts.len().saturating_sub(1) {
|
let max_index = if body.new_index == posts.len().saturating_sub(1) {
|
||||||
None
|
None
|
||||||
} else {
|
} else if body.new_index < body.old_index {
|
||||||
if body.new_index < body.old_index {
|
|
||||||
Some(body.new_index)
|
Some(body.new_index)
|
||||||
} else {
|
} else {
|
||||||
Some(body.new_index + 1)
|
Some(body.new_index + 1)
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let min = min_index.map(|index| posts[index].position).unwrap_or(0.0);
|
let min = min_index.map(|index| posts[index].position).unwrap_or(0.0);
|
||||||
let max = max_index
|
let max = max_index
|
||||||
|
|
@ -1148,6 +1148,30 @@ pub(crate) async fn sort_pool(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_pool(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_id): Path<i32>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_edit {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
SameyPool::delete_by_id(pool_id).exec(&db).await?;
|
||||||
|
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk edit tag views
|
// Bulk edit tag views
|
||||||
|
|
||||||
enum BulkEditTagMessage {
|
enum BulkEditTagMessage {
|
||||||
|
|
@ -1629,14 +1653,14 @@ pub(crate) async fn submit_post_details(
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = match body.title.trim() {
|
let title = match body.title.trim() {
|
||||||
title if title.is_empty() => None,
|
"" => None,
|
||||||
title => Some(title.to_owned()),
|
title => Some(title.to_owned()),
|
||||||
};
|
};
|
||||||
let description = match body.description.trim() {
|
let description = match body.description.trim() {
|
||||||
description if description.is_empty() => None,
|
"" => None,
|
||||||
description => Some(description.to_owned()),
|
description => Some(description.to_owned()),
|
||||||
};
|
};
|
||||||
let parent_post = if let Some(parent_id) = body.parent_post.trim().parse().ok() {
|
let parent_post = if let Ok(parent_id) = body.parent_post.trim().parse() {
|
||||||
match filter_posts_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
match filter_posts_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,16 @@
|
||||||
value="true"
|
value="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
hx-confirm="Are you sure that you want to delete this pool? This can't be undone!"
|
||||||
|
hx-delete="/pool/{{ pool.id }}"
|
||||||
|
hx-target="body"
|
||||||
|
hx-replace-url="/"
|
||||||
|
>
|
||||||
|
Delete pool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue