Func
This commit is contained in:
21 files changed
+590
-70
No files matched your search
+1
-1
@@ -17,7 +17,7 @@ pub struct User {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Bearer auth.
|
||||
* Check Basic auth.
|
||||
* If present, check password.
|
||||
* If user doesn't exist, create it.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,8 @@ use sqlx::{
|
||||
SqlitePool,
|
||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
|
||||
};
|
||||
use tokio::time::interval;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
|
||||
pub async fn cleanup_notes(pool: SqlitePool) {
|
||||
let mut interval = interval(Duration::from_secs(60));
|
||||
|
||||
+28
-6
@@ -1,4 +1,8 @@
|
||||
use axum::{Router, middleware, routing::get, serve};
|
||||
use axum::{
|
||||
Router, middleware,
|
||||
routing::{get, post},
|
||||
serve,
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
@@ -6,17 +10,35 @@ mod auth;
|
||||
mod db;
|
||||
mod paste;
|
||||
mod routes;
|
||||
use crate::routes::{get_note, get_redirect, home_page};
|
||||
use crate::routes::{
|
||||
delete_note, get_note, get_redirect, home_page, list_notes, post_note, post_once,
|
||||
post_redirect, put_note, put_redirect,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let pool = db::connect_db("sqlite://pastable.db").await?;
|
||||
let database_url =
|
||||
std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://pastable.db".to_owned());
|
||||
let pool = db::connect_db(&database_url).await?;
|
||||
tokio::spawn(db::cleanup_notes(pool.clone()));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(home_page))
|
||||
.route("/to/{id}", get(get_redirect))
|
||||
.route("/{id}", get(get_note))
|
||||
.route("/", get(home_page).post(post_note))
|
||||
.route("/once", post(post_once))
|
||||
.route("/once/{id}", post(post_once))
|
||||
.route("/to", post(post_redirect))
|
||||
.route(
|
||||
"/to/{id}",
|
||||
get(get_redirect).post(post_redirect).put(put_redirect),
|
||||
)
|
||||
.route("/list", get(list_notes))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(get_note)
|
||||
.post(post_note)
|
||||
.put(put_note)
|
||||
.delete(delete_note),
|
||||
)
|
||||
.nest_service("/static", ServeDir::new("static"))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
pool.clone(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct Paste {
|
||||
pub id: String,
|
||||
@@ -19,3 +21,33 @@ pub enum PasteType {
|
||||
TextV1 = 0,
|
||||
UrlV1 = 100,
|
||||
}
|
||||
impl Paste {
|
||||
pub async fn read(
|
||||
db: &SqlitePool,
|
||||
id: &str,
|
||||
paste_type: PasteType,
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
let mut transaction = db.begin().await?;
|
||||
let paste = sqlx::query_as::<_, Self>(
|
||||
"UPDATE notes SET views = views + 1
|
||||
WHERE type = ? AND id = ?
|
||||
AND (expiration IS NULL OR expiration > unixepoch())
|
||||
AND (max_views IS NULL OR views < max_views)
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(paste_type)
|
||||
.bind(id)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
if let Some(paste) = &paste
|
||||
&& paste.max_views.is_some_and(|max| paste.views >= max)
|
||||
{
|
||||
sqlx::query("DELETE FROM notes WHERE id = ?")
|
||||
.bind(&paste.id)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
Ok(paste)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,37 @@
|
||||
use axum::http::{HeaderMap, header::ACCEPT};
|
||||
|
||||
mod create;
|
||||
mod delete_note;
|
||||
mod get_note;
|
||||
mod get_redirect;
|
||||
mod home;
|
||||
mod input;
|
||||
mod list_notes;
|
||||
mod owned_note;
|
||||
mod post_note;
|
||||
mod post_once;
|
||||
mod post_redirect;
|
||||
mod put_note;
|
||||
mod put_redirect;
|
||||
mod update;
|
||||
|
||||
pub use delete_note::delete_note;
|
||||
pub use get_note::get_note;
|
||||
pub use get_redirect::get_redirect;
|
||||
pub use home::home_page;
|
||||
pub use list_notes::list_notes;
|
||||
pub use post_note::post_note;
|
||||
pub use post_once::post_once;
|
||||
pub use post_redirect::post_redirect;
|
||||
pub use put_note::put_note;
|
||||
pub use put_redirect::put_redirect;
|
||||
|
||||
fn db_error(_: sqlx::Error) -> axum::http::StatusCode {
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
fn return_html(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get(ACCEPT)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|accept| accept.contains("text/html"))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use axum::{Extension, extract::Path, http::StatusCode};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{db_error, input::PasteInput};
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
id: Option<Path<String>>,
|
||||
user: Option<Extension<User>>,
|
||||
input: PasteInput,
|
||||
paste_type: PasteType,
|
||||
) -> Result<String, StatusCode> {
|
||||
let mut id = id.map(|Path(id)| id).filter(|id| {
|
||||
!matches!(id.as_str(), "list" | "once" | "to" | "static") && !id.contains('/')
|
||||
});
|
||||
let owner = user.map(|Extension(user)| user.username);
|
||||
let expiration = input.expiration()?;
|
||||
loop {
|
||||
let created = sqlx::query_scalar(
|
||||
"INSERT INTO notes (id, type, content, language, expiration, max_views, created_at, owner)
|
||||
VALUES (COALESCE(?, lower(hex(randomblob(12)))), ?, ?, ?, ?, ?, unixepoch(), ?)
|
||||
ON CONFLICT(id) DO NOTHING RETURNING id",
|
||||
)
|
||||
.bind(id.as_deref())
|
||||
.bind(&paste_type)
|
||||
.bind(&input.content)
|
||||
.bind(&input.language)
|
||||
.bind(expiration)
|
||||
.bind(input.max_views)
|
||||
.bind(owner.as_deref())
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
if let Some(id) = created {
|
||||
return Ok(id);
|
||||
}
|
||||
id = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::auth::User;
|
||||
|
||||
use super::{db_error, owned_note::owned_note};
|
||||
|
||||
pub async fn delete_note(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<String>,
|
||||
user: Option<Extension<User>>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let mut transaction = owned_note(&db, &id, user).await?;
|
||||
sqlx::query("DELETE FROM notes WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
transaction.commit().await.map_err(db_error)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
+7
-40
@@ -2,7 +2,7 @@ use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode, header::ACCEPT},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use irosashi::{CodeToHtmlOptions, Highlighter};
|
||||
@@ -10,6 +10,8 @@ use sqlx::SqlitePool;
|
||||
|
||||
use crate::paste::{Paste, PasteType};
|
||||
|
||||
use super::{db_error, return_html};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "note.html")]
|
||||
pub struct NotePage {
|
||||
@@ -21,39 +23,12 @@ pub async fn get_note(
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let mut transaction = db
|
||||
.begin()
|
||||
let paste = Paste::read(&db, &id, PasteType::TextV1)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let paste = sqlx::query_as::<_, Paste>(
|
||||
"UPDATE notes SET views = views + 1
|
||||
WHERE type = ? AND id = ?
|
||||
AND (expiration IS NULL OR expiration > unixepoch())
|
||||
AND (max_views IS NULL OR views < max_views)
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(PasteType::TextV1)
|
||||
.bind(id)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
.map_err(db_error)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
if paste.max_views.is_some_and(|max| paste.views >= max) {
|
||||
sqlx::query("DELETE FROM notes WHERE id = ?")
|
||||
.bind(&paste.id)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if return_html(headers) {
|
||||
if return_html(&headers) {
|
||||
let highlighter = Highlighter::new().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let code = highlighter
|
||||
.code_to_html(
|
||||
@@ -69,11 +44,3 @@ pub async fn get_note(
|
||||
Ok(paste.content.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
fn return_html(headers: HeaderMap) -> bool {
|
||||
headers
|
||||
.get(ACCEPT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|h| h.contains("text/html"))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -5,20 +5,18 @@ use axum::{
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::paste::PasteType;
|
||||
use crate::paste::{Paste, PasteType};
|
||||
|
||||
use super::db_error;
|
||||
|
||||
pub async fn get_redirect(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Redirect, StatusCode> {
|
||||
let url =
|
||||
sqlx::query_scalar::<_, String>("SELECT content FROM notes WHERE type = ? AND id = ?")
|
||||
.bind(PasteType::UrlV1)
|
||||
.bind(id)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
let paste = Paste::read(&db, &id, PasteType::UrlV1)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
Ok(Redirect::temporary(&url))
|
||||
Ok(Redirect::temporary(&paste.content))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use axum::{
|
||||
Form,
|
||||
extract::{FromRequest, Request},
|
||||
http::{HeaderValue, StatusCode, Uri, header::CONTENT_TYPE},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct PasteInput {
|
||||
#[serde(alias = "href")]
|
||||
pub content: String,
|
||||
pub language: Option<String>,
|
||||
pub expires: Option<i64>,
|
||||
pub max_views: Option<i64>,
|
||||
}
|
||||
|
||||
impl PasteInput {
|
||||
pub fn validate_url(&mut self) -> Result<(), StatusCode> {
|
||||
self.content = self.content.trim().to_owned();
|
||||
let uri: Uri = self.content.parse().map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
if !matches!(uri.scheme_str(), Some("http" | "https"))
|
||||
|| uri.host().is_none()
|
||||
|| HeaderValue::from_str(&self.content).is_err()
|
||||
{
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn expiration(&self) -> Result<Option<i64>, StatusCode> {
|
||||
self.expires
|
||||
.map(|expires| {
|
||||
let now = UNIX_EPOCH
|
||||
.elapsed()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.as_secs() as i64;
|
||||
now.checked_add(expires).ok_or(StatusCode::BAD_REQUEST)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync> FromRequest<S> for PasteInput {
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let form = request
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.split(';').next())
|
||||
.is_some_and(|value| {
|
||||
value
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("application/x-www-form-urlencoded")
|
||||
});
|
||||
let input = if form {
|
||||
Form::<Self>::from_request(request, state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?
|
||||
.0
|
||||
} else {
|
||||
Self {
|
||||
content: String::from_request(request, state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?,
|
||||
..Self::default()
|
||||
}
|
||||
};
|
||||
if input.expires.is_some_and(|expires| expires < 0)
|
||||
|| input.max_views.is_some_and(|max| max < 1)
|
||||
{
|
||||
return Err(StatusCode::BAD_REQUEST.into_response());
|
||||
}
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{
|
||||
auth::User,
|
||||
paste::{Paste, PasteType},
|
||||
};
|
||||
|
||||
use super::{db_error, return_html};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "list.html")]
|
||||
pub struct ListPage {
|
||||
notes: Vec<Paste>,
|
||||
}
|
||||
|
||||
pub async fn list_notes(
|
||||
State(db): State<SqlitePool>,
|
||||
user: Option<Extension<User>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let Extension(user) = user.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
let notes = sqlx::query_as::<_, Paste>(
|
||||
"SELECT * FROM notes WHERE owner = ?
|
||||
AND (expiration IS NULL OR expiration > unixepoch())
|
||||
AND (max_views IS NULL OR views < max_views)
|
||||
ORDER BY created_at, id",
|
||||
)
|
||||
.bind(user.username)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
|
||||
if return_html(&headers) {
|
||||
Ok(ListPage { notes }.into_response())
|
||||
} else {
|
||||
Ok(notes
|
||||
.into_iter()
|
||||
.map(|note| format!("{}\n{}\n", note.id, note.content))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use axum::{Extension, http::StatusCode};
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::{auth::User, paste::Paste};
|
||||
|
||||
use super::db_error;
|
||||
|
||||
pub async fn owned_note(
|
||||
db: &SqlitePool,
|
||||
id: &str,
|
||||
user: Option<Extension<User>>,
|
||||
) -> Result<Transaction<'static, Sqlite>, StatusCode> {
|
||||
let Extension(user) = user.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
let mut transaction = db.begin_with("BEGIN IMMEDIATE").await.map_err(db_error)?;
|
||||
let paste = sqlx::query_as::<_, Paste>(
|
||||
"SELECT * FROM notes WHERE id = ?
|
||||
AND (expiration IS NULL OR expiration > unixepoch())
|
||||
AND (max_views IS NULL OR views < max_views)",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
if paste.owner.as_deref() != Some(&user.username) {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
Ok(transaction)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{create::create, input::PasteInput};
|
||||
|
||||
pub async fn post_note(
|
||||
State(db): State<SqlitePool>,
|
||||
id: Option<Path<String>>,
|
||||
user: Option<Extension<User>>,
|
||||
input: PasteInput,
|
||||
) -> Result<String, StatusCode> {
|
||||
create(&db, id, user, input, PasteType::TextV1).await
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{create::create, input::PasteInput};
|
||||
|
||||
pub async fn post_once(
|
||||
State(db): State<SqlitePool>,
|
||||
id: Option<Path<String>>,
|
||||
user: Option<Extension<User>>,
|
||||
mut input: PasteInput,
|
||||
) -> Result<String, StatusCode> {
|
||||
input.max_views = Some(1);
|
||||
create(&db, id, user, input, PasteType::TextV1).await
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{create::create, input::PasteInput};
|
||||
|
||||
pub async fn post_redirect(
|
||||
State(db): State<SqlitePool>,
|
||||
id: Option<Path<String>>,
|
||||
user: Option<Extension<User>>,
|
||||
mut input: PasteInput,
|
||||
) -> Result<String, StatusCode> {
|
||||
input.validate_url()?;
|
||||
create(&db, id, user, input, PasteType::UrlV1).await
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{input::PasteInput, update::update};
|
||||
|
||||
pub async fn put_note(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<String>,
|
||||
user: Option<Extension<User>>,
|
||||
input: PasteInput,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
update(&db, &id, user, input, PasteType::TextV1).await
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{input::PasteInput, update::update};
|
||||
|
||||
pub async fn put_redirect(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<String>,
|
||||
user: Option<Extension<User>>,
|
||||
mut input: PasteInput,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
input.validate_url()?;
|
||||
update(&db, &id, user, input, PasteType::UrlV1).await
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use axum::{Extension, http::StatusCode};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{auth::User, paste::PasteType};
|
||||
|
||||
use super::{db_error, input::PasteInput, owned_note::owned_note};
|
||||
|
||||
pub async fn update(
|
||||
db: &SqlitePool,
|
||||
id: &str,
|
||||
user: Option<Extension<User>>,
|
||||
input: PasteInput,
|
||||
paste_type: PasteType,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let expiration = input.expiration()?;
|
||||
let mut transaction = owned_note(db, id, user).await?;
|
||||
let result = sqlx::query(
|
||||
"UPDATE notes SET content = ?, language = COALESCE(?, language),
|
||||
expiration = COALESCE(?, expiration), max_views = COALESCE(?, max_views),
|
||||
updated_at = unixepoch()
|
||||
WHERE id = ? AND type = ?",
|
||||
)
|
||||
.bind(input.content)
|
||||
.bind(input.language)
|
||||
.bind(expiration)
|
||||
.bind(input.max_views)
|
||||
.bind(id)
|
||||
.bind(paste_type)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
transaction.commit().await.map_err(db_error)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
Reference in new issue
Block a user