835 lines
21 KiB
Rust
835 lines
21 KiB
Rust
// need this for ructe
|
|
#![allow(clippy::needless_borrow)]
|
|
|
|
use actix_web::{
|
|
http::{
|
|
header::{CacheControl, CacheDirective, ContentType, LastModified, LOCATION},
|
|
StatusCode,
|
|
},
|
|
web, HttpRequest, HttpResponse, HttpResponseBuilder, ResponseError,
|
|
};
|
|
use awc::Client;
|
|
use sled::Db;
|
|
use std::{
|
|
io::Cursor,
|
|
net::SocketAddr,
|
|
path::{Path, PathBuf},
|
|
time::SystemTime,
|
|
};
|
|
use structopt::StructOpt;
|
|
use tracing_error::SpanTrace;
|
|
use url::Url;
|
|
use uuid::Uuid;
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
|
|
|
const HOURS: u32 = 60 * 60;
|
|
const DAYS: u32 = 24 * HOURS;
|
|
|
|
mod connection;
|
|
mod middleware;
|
|
mod pict;
|
|
mod store;
|
|
mod ui;
|
|
|
|
use self::{connection::Connection, middleware::ValidToken, store::Store};
|
|
|
|
#[derive(Clone, Debug, StructOpt)]
|
|
pub struct Config {
|
|
#[structopt(
|
|
short,
|
|
long,
|
|
env = "PICTRS_AGGREGATOR_ADDR",
|
|
default_value = "0.0.0.0:8082",
|
|
help = "The address and port the server binds to"
|
|
)]
|
|
addr: SocketAddr,
|
|
|
|
#[structopt(
|
|
short,
|
|
long,
|
|
env = "PICTRS_AGGREGATOR_UPSTREAM",
|
|
default_value = "http://localhost:8080",
|
|
help = "The url of the upstream pict-rs server"
|
|
)]
|
|
upstream: Url,
|
|
|
|
#[structopt(
|
|
short,
|
|
long,
|
|
env = "PICTRS_AGGREGATOR_DATABASE",
|
|
default_value = "sled/db-0-34",
|
|
help = "The path to the database"
|
|
)]
|
|
database_path: PathBuf,
|
|
|
|
#[structopt(
|
|
short,
|
|
long,
|
|
env = "PICTRS_AGGREGATOR_OPENTELEMETRY_URL",
|
|
help = "URL for the OpenTelemetry Colletor"
|
|
)]
|
|
opentelemetry_url: Option<Url>,
|
|
}
|
|
|
|
pub fn accept() -> &'static str {
|
|
"image/png,image/jpeg,image/webp,.jpg,.jpeg,.png,.webp"
|
|
}
|
|
|
|
impl Config {
|
|
pub fn db_path(&self) -> &Path {
|
|
&self.database_path
|
|
}
|
|
|
|
pub fn bind_address(&self) -> SocketAddr {
|
|
self.addr
|
|
}
|
|
|
|
pub fn opentelemetry_url(&self) -> Option<&Url> {
|
|
self.opentelemetry_url.as_ref()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct State {
|
|
upstream: Url,
|
|
scope: String,
|
|
store: Store,
|
|
startup: SystemTime,
|
|
}
|
|
|
|
impl std::fmt::Debug for State {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("State")
|
|
.field("upstream", &self.upstream)
|
|
.field("scope", &self.scope)
|
|
.field("store", &self.store)
|
|
.field("db", &"Db")
|
|
.field("startup", &self.startup)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
fn scoped(&self, s: &str) -> String {
|
|
if self.scope.is_empty() && s.is_empty() {
|
|
"/".to_string()
|
|
} else if s.is_empty() {
|
|
self.scope.clone()
|
|
} else if self.scope.is_empty() {
|
|
format!("/{}", s)
|
|
} else {
|
|
format!("{}/{}", self.scope, s)
|
|
}
|
|
}
|
|
|
|
fn create_collection_path(&self) -> String {
|
|
self.scoped("")
|
|
}
|
|
|
|
fn edit_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
|
self.scoped(&format!("{}?token={}", id, token.token))
|
|
}
|
|
|
|
fn update_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
|
self.scoped(&format!("{}?token={}", id, token.token))
|
|
}
|
|
|
|
fn delete_collection_path(&self, id: Uuid, token: &ValidToken, confirmed: bool) -> String {
|
|
if confirmed {
|
|
self.scoped(&format!(
|
|
"{}/delete?token={}&confirmed=true",
|
|
id, token.token
|
|
))
|
|
} else {
|
|
self.scoped(&format!("{}/delete?token={}", id, token.token))
|
|
}
|
|
}
|
|
|
|
fn public_collection_path(&self, id: Uuid) -> String {
|
|
self.scoped(&format!("{}", id))
|
|
}
|
|
|
|
fn create_entry_path(&self, collection_id: Uuid, token: &ValidToken) -> String {
|
|
self.scoped(&format!("{}/entry?token={}", collection_id, token.token))
|
|
}
|
|
|
|
fn update_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
|
|
self.scoped(&format!(
|
|
"{}/entry/{}?token={}",
|
|
collection_id, id, token.token
|
|
))
|
|
}
|
|
|
|
fn delete_entry_path(
|
|
&self,
|
|
collection_id: Uuid,
|
|
id: Uuid,
|
|
token: &ValidToken,
|
|
confirmed: bool,
|
|
) -> String {
|
|
if confirmed {
|
|
self.scoped(&format!(
|
|
"{}/entry/{}/delete?token={}&confirmed=true",
|
|
collection_id, id, token.token
|
|
))
|
|
} else {
|
|
self.scoped(&format!(
|
|
"{}/entry/{}/delete?token={}",
|
|
collection_id, id, token.token
|
|
))
|
|
}
|
|
}
|
|
|
|
fn statics_path(&self, file: &str) -> String {
|
|
self.scoped(&format!("static/{}", file))
|
|
}
|
|
|
|
fn thumbnail_path(&self, entry: &Entry, size: u16, extension: pict::Extension) -> String {
|
|
self.scoped(&format!(
|
|
"image/thumbnail.{}?src={}&size={}",
|
|
extension, entry.filename, size
|
|
))
|
|
}
|
|
|
|
fn srcset(&self, entry: &Entry, extension: pict::Extension) -> String {
|
|
connection::VALID_SIZES
|
|
.iter()
|
|
.map(|size| format!("{} {}w", self.thumbnail_path(entry, *size, extension), size,))
|
|
.collect::<Vec<String>>()
|
|
.join(", ")
|
|
}
|
|
|
|
fn image_path(&self, entry: &Entry) -> String {
|
|
self.scoped(&format!("image/full/{}", entry.filename))
|
|
}
|
|
}
|
|
|
|
pub fn state(config: Config, scope: &str, db: Db) -> Result<State, sled::Error> {
|
|
Ok(State {
|
|
upstream: config.upstream,
|
|
scope: scope.to_string(),
|
|
store: Store::new(&db)?,
|
|
startup: SystemTime::now(),
|
|
})
|
|
}
|
|
|
|
pub fn configure(cfg: &mut web::ServiceConfig, state: State, client: Client) {
|
|
cfg.app_data(web::Data::new(Connection::new(
|
|
state.upstream.clone(),
|
|
client,
|
|
)))
|
|
.app_data(web::Data::new(state))
|
|
.service(web::resource("/static/{filename}").route(web::get().to(static_files)))
|
|
.service(web::resource("/404").route(web::get().to(not_found)))
|
|
.service(
|
|
web::scope("/image")
|
|
.service(web::resource("/thumbnail.{extension}").route(web::get().to(thumbnail)))
|
|
.service(web::resource("/full/{filename}").route(web::get().to(image))),
|
|
)
|
|
.service(
|
|
web::resource("/")
|
|
.route(web::get().to(index))
|
|
.route(web::post().to(create_collection)),
|
|
)
|
|
.service(
|
|
web::scope("/{collection}")
|
|
.wrap(middleware::Verify)
|
|
.service(
|
|
web::resource("")
|
|
.route(web::get().to(collection))
|
|
.route(web::post().to(update_collection)),
|
|
)
|
|
.service(web::resource("/delete").route(web::get().to(delete_collection)))
|
|
.service(
|
|
web::scope("/entry")
|
|
.service(web::resource("").route(web::post().to(upload)))
|
|
.service(
|
|
web::scope("/{entry}")
|
|
.service(web::resource("").route(web::post().to(update_entry)))
|
|
.service(web::resource("/delete").route(web::get().to(delete_entry))),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
fn to_edit_page(id: Uuid, token: &ValidToken, state: &State) -> HttpResponse {
|
|
HttpResponse::SeeOther()
|
|
.insert_header((LOCATION, state.edit_collection_path(id, token)))
|
|
.finish()
|
|
}
|
|
|
|
fn to_404(state: &State) -> HttpResponse {
|
|
HttpResponse::MovedPermanently()
|
|
.insert_header((LOCATION, state.create_collection_path()))
|
|
.finish()
|
|
}
|
|
|
|
fn to_home(state: &State) -> HttpResponse {
|
|
HttpResponse::SeeOther()
|
|
.insert_header((LOCATION, state.create_collection_path()))
|
|
.finish()
|
|
}
|
|
|
|
trait ResultExt<T> {
|
|
fn stateful(self, state: &web::Data<State>) -> Result<T, StateError>;
|
|
}
|
|
|
|
impl<T, E> ResultExt<T> for Result<T, E>
|
|
where
|
|
Error: From<E>,
|
|
{
|
|
fn stateful(self, state: &web::Data<State>) -> Result<T, StateError> {
|
|
self.map_err(Error::from).map_err(|error| StateError {
|
|
state: state.clone(),
|
|
error,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(name = "Static files")]
|
|
fn static_files(filename: web::Path<String>, state: web::Data<State>) -> HttpResponse {
|
|
let filename = filename.into_inner();
|
|
|
|
if let Some(data) = self::templates::statics::StaticFile::get(&filename) {
|
|
return HttpResponse::Ok()
|
|
.insert_header(LastModified(state.startup.into()))
|
|
.insert_header(CacheControl(vec![
|
|
CacheDirective::Public,
|
|
CacheDirective::MaxAge(365 * DAYS),
|
|
CacheDirective::Extension("immutable".to_owned(), None),
|
|
]))
|
|
.insert_header(ContentType(data.mime.clone()))
|
|
.body(data.content);
|
|
}
|
|
|
|
to_404(&state)
|
|
}
|
|
|
|
#[tracing::instrument(name = "Not found")]
|
|
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
|
rendered(
|
|
|cursor| self::templates::not_found(cursor, &state),
|
|
HttpResponse::NotFound(),
|
|
)
|
|
.stateful(&state)
|
|
}
|
|
|
|
struct StateError {
|
|
state: web::Data<State>,
|
|
error: Error,
|
|
}
|
|
|
|
impl std::fmt::Debug for StateError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
f.debug_struct("StateError")
|
|
.field("error", &self.error)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for StateError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(f, "{}", self.error)
|
|
}
|
|
}
|
|
|
|
impl ResponseError for StateError {
|
|
fn status_code(&self) -> StatusCode {
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
}
|
|
|
|
fn error_response(&self) -> HttpResponse {
|
|
match rendered(
|
|
|cursor| self::templates::error(cursor, &self.error.kind.to_string(), &self.state),
|
|
HttpResponse::build(self.status_code()),
|
|
) {
|
|
Ok(res) => res,
|
|
Err(_) => HttpResponse::build(self.status_code())
|
|
.content_type(mime::TEXT_PLAIN.essence_str())
|
|
.body(self.error.kind.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Error {
|
|
context: SpanTrace,
|
|
kind: ErrorKind,
|
|
}
|
|
|
|
impl std::fmt::Display for Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "{}", self.kind)?;
|
|
std::fmt::Display::fmt(&self.context, f)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for Error {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
self.kind.source()
|
|
}
|
|
}
|
|
|
|
impl<T> From<T> for Error
|
|
where
|
|
ErrorKind: From<T>,
|
|
{
|
|
fn from(error: T) -> Self {
|
|
Error {
|
|
context: SpanTrace::capture(),
|
|
kind: error.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
enum ErrorKind {
|
|
#[error("{0}")]
|
|
Render(#[from] std::io::Error),
|
|
|
|
#[error("{0}")]
|
|
Store(#[from] self::store::Error),
|
|
|
|
#[error("{0}")]
|
|
Upload(#[from] self::connection::UploadError),
|
|
|
|
#[error("{0}")]
|
|
UploadString(String),
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
pub struct Collection {
|
|
title: String,
|
|
description: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
|
pub struct Entry {
|
|
title: String,
|
|
description: String,
|
|
filename: String,
|
|
delete_token: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
pub struct Token {
|
|
token: Uuid,
|
|
}
|
|
|
|
impl Token {
|
|
fn hash(&self) -> Result<TokenStorage, bcrypt::BcryptError> {
|
|
use bcrypt::{hash, DEFAULT_COST};
|
|
|
|
Ok(TokenStorage {
|
|
token: hash(self.token.as_bytes(), DEFAULT_COST)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
struct TokenStorage {
|
|
token: String,
|
|
}
|
|
|
|
impl TokenStorage {
|
|
fn verify(&self, token: &Token) -> Result<bool, bcrypt::BcryptError> {
|
|
bcrypt::verify(&token.token.as_bytes(), &self.token)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct CollectionPath {
|
|
collection: Uuid,
|
|
}
|
|
|
|
impl CollectionPath {
|
|
fn key(&self) -> String {
|
|
format!("{}", self.collection)
|
|
}
|
|
|
|
fn entry_range(&self) -> std::ops::Range<Vec<u8>> {
|
|
let base = format!("{}/entry/", self.collection).as_bytes().to_vec();
|
|
let mut start = base.clone();
|
|
let mut end = base;
|
|
|
|
start.push(0x0);
|
|
end.push(0xff);
|
|
|
|
start..end
|
|
}
|
|
|
|
fn token_key(&self) -> String {
|
|
format!("{}/token", self.collection)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct EntryPath {
|
|
collection: Uuid,
|
|
entry: Uuid,
|
|
}
|
|
|
|
impl EntryPath {
|
|
fn key(&self) -> String {
|
|
format!("{}/entry/{}", self.collection, self.entry)
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(name = "Upload image", skip(pl))]
|
|
async fn upload(
|
|
req: HttpRequest,
|
|
pl: web::Payload,
|
|
path: web::Path<CollectionPath>,
|
|
token: ValidToken,
|
|
conn: web::Data<Connection>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
let images = conn.upload(&req, pl).await.stateful(&state)?;
|
|
|
|
if images.is_err() {
|
|
return Err(ErrorKind::UploadString(images.message().to_owned())).stateful(&state);
|
|
}
|
|
|
|
let image = images
|
|
.files()
|
|
.next()
|
|
.ok_or_else(|| ErrorKind::UploadString("Missing file".to_owned()))
|
|
.stateful(&state)?;
|
|
|
|
let entry = Entry {
|
|
title: String::new(),
|
|
description: String::new(),
|
|
filename: image.file().to_owned(),
|
|
delete_token: image.delete_token().to_owned(),
|
|
};
|
|
|
|
let entry_path = EntryPath {
|
|
collection: path.collection,
|
|
entry: Uuid::new_v4(),
|
|
};
|
|
|
|
store::CreateEntry {
|
|
entry_path: &entry_path,
|
|
entry: &entry,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_edit_page(path.collection, &token, &state))
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct ImagePath {
|
|
filename: String,
|
|
}
|
|
|
|
#[tracing::instrument(name = "Serve image")]
|
|
async fn image(
|
|
req: HttpRequest,
|
|
path: web::Path<ImagePath>,
|
|
conn: web::Data<Connection>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
conn.image(&path.filename, &req).await
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct ThumbnailPath {
|
|
extension: pict::Extension,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct ThumbnailQuery {
|
|
src: String,
|
|
size: u16,
|
|
}
|
|
|
|
#[tracing::instrument(name = "Serve thumbnail")]
|
|
async fn thumbnail(
|
|
req: HttpRequest,
|
|
path: web::Path<ThumbnailPath>,
|
|
query: web::Query<ThumbnailQuery>,
|
|
conn: web::Data<Connection>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
conn.thumbnail(query.size, &query.src, path.extension, &req)
|
|
.await
|
|
}
|
|
|
|
#[tracing::instrument(name = "Index")]
|
|
async fn index(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
|
rendered(
|
|
|cursor| self::templates::index(cursor, &state),
|
|
HttpResponse::Ok(),
|
|
)
|
|
.stateful(&state)
|
|
}
|
|
|
|
#[tracing::instrument(name = "Collection")]
|
|
async fn collection(
|
|
path: web::Path<CollectionPath>,
|
|
token: Option<ValidToken>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
match token {
|
|
Some(token) => edit_collection(path, token, state.clone())
|
|
.await
|
|
.stateful(&state),
|
|
None => view_collection(path, state.clone()).await.stateful(&state),
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(name = "View Collection")]
|
|
async fn view_collection(
|
|
path: web::Path<CollectionPath>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let collection = match state.store.collection(&path).await? {
|
|
Some(collection) => collection,
|
|
None => return Ok(to_404(&state)),
|
|
};
|
|
let entries = state.store.entries(path.entry_range()).await?;
|
|
|
|
rendered(
|
|
|cursor| {
|
|
self::templates::view_collection(cursor, path.collection, &collection, &entries, &state)
|
|
},
|
|
HttpResponse::Ok(),
|
|
)
|
|
}
|
|
|
|
#[tracing::instrument(name = "Edit Collection")]
|
|
async fn edit_collection(
|
|
path: web::Path<CollectionPath>,
|
|
token: ValidToken,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let collection = match state.store.collection(&path).await? {
|
|
Some(collection) => collection,
|
|
None => return Ok(to_404(&state)),
|
|
};
|
|
let entries = state.store.entries(path.entry_range()).await?;
|
|
|
|
rendered(
|
|
|cursor| {
|
|
self::templates::edit_collection(
|
|
cursor,
|
|
&collection,
|
|
path.collection,
|
|
&entries,
|
|
&token,
|
|
&state,
|
|
)
|
|
},
|
|
HttpResponse::Ok(),
|
|
)
|
|
}
|
|
|
|
#[tracing::instrument(name = "Create Collection")]
|
|
async fn create_collection(
|
|
collection: web::Form<Collection>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
let collection_id = Uuid::new_v4();
|
|
let collection_path = CollectionPath {
|
|
collection: collection_id,
|
|
};
|
|
let token = Token {
|
|
token: Uuid::new_v4(),
|
|
};
|
|
|
|
store::CreateCollection {
|
|
collection_path: &collection_path,
|
|
collection: &collection,
|
|
token: &token,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_edit_page(
|
|
collection_path.collection,
|
|
&ValidToken { token: token.token },
|
|
&state,
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(name = "Update Collection")]
|
|
async fn update_collection(
|
|
path: web::Path<CollectionPath>,
|
|
form: web::Form<Collection>,
|
|
token: ValidToken,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
store::UpdateCollection {
|
|
collection_path: &path,
|
|
collection: &form,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_edit_page(path.collection, &token, &state))
|
|
}
|
|
|
|
#[tracing::instrument(name = "Update Entry")]
|
|
async fn update_entry(
|
|
entry_path: web::Path<EntryPath>,
|
|
entry: web::Form<Entry>,
|
|
token: ValidToken,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
store::UpdateEntry {
|
|
entry_path: &entry_path,
|
|
entry: &entry,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_edit_page(entry_path.collection, &token, &state))
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct ConfirmQuery {
|
|
confirmed: Option<bool>,
|
|
}
|
|
|
|
#[tracing::instrument(name = "Delete Entry")]
|
|
async fn delete_entry(
|
|
entry_path: web::Path<EntryPath>,
|
|
query: web::Query<ConfirmQuery>,
|
|
token: ValidToken,
|
|
conn: web::Data<Connection>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
let res = state.store.entry(&entry_path).await.stateful(&state)?;
|
|
|
|
let entry = match res {
|
|
Some(entry) => entry,
|
|
None => return Ok(to_404(&state)),
|
|
};
|
|
|
|
if !query.confirmed.unwrap_or(false) {
|
|
return rendered(
|
|
|cursor| {
|
|
self::templates::confirm_entry_delete(
|
|
cursor,
|
|
entry_path.collection,
|
|
entry_path.entry,
|
|
&entry,
|
|
&token,
|
|
&state,
|
|
)
|
|
},
|
|
HttpResponse::Ok(),
|
|
)
|
|
.stateful(&state);
|
|
}
|
|
|
|
conn.delete(&entry.filename, &entry.delete_token)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
store::DeleteEntry {
|
|
entry_path: &entry_path,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_edit_page(entry_path.collection, &token, &state))
|
|
}
|
|
|
|
#[tracing::instrument(name = "Delete Collection")]
|
|
async fn delete_collection(
|
|
path: web::Path<CollectionPath>,
|
|
query: web::Query<ConfirmQuery>,
|
|
token: ValidToken,
|
|
conn: web::Data<Connection>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, StateError> {
|
|
if !query.confirmed.unwrap_or(false) {
|
|
let res = state.store.collection(&path).await.stateful(&state)?;
|
|
|
|
let collection = match res {
|
|
Some(collection) => collection,
|
|
None => return Ok(to_404(&state)),
|
|
};
|
|
|
|
return rendered(
|
|
|cursor| {
|
|
self::templates::confirm_delete(
|
|
cursor,
|
|
path.collection,
|
|
&collection,
|
|
&token,
|
|
&state,
|
|
)
|
|
},
|
|
HttpResponse::Ok(),
|
|
)
|
|
.stateful(&state);
|
|
}
|
|
|
|
let entries = state
|
|
.store
|
|
.entries(path.entry_range())
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
let future_vec = entries
|
|
.iter()
|
|
.map(|(_, entry)| {
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
let entry: Entry = entry.clone();
|
|
let conn = conn.clone();
|
|
actix_rt::spawn(async move {
|
|
let _ = tx.send(conn.delete(&entry.filename, &entry.delete_token).await);
|
|
});
|
|
rx
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut results = Vec::new();
|
|
for rx in future_vec {
|
|
results.push(
|
|
rx.await
|
|
.map_err(|_| ErrorKind::UploadString("Canceled".to_string()))
|
|
.stateful(&state)?,
|
|
);
|
|
}
|
|
|
|
// Only bail before deleting collection if all images failed deletion
|
|
// It is possible that some images were already deleted
|
|
if results.iter().all(|r| r.is_err()) {
|
|
for result in results {
|
|
result.stateful(&state)?;
|
|
}
|
|
}
|
|
|
|
store::DeleteCollection {
|
|
collection_path: &path,
|
|
}
|
|
.exec(&state.store)
|
|
.await
|
|
.stateful(&state)?;
|
|
|
|
Ok(to_home(&state))
|
|
}
|
|
|
|
fn rendered(
|
|
f: impl FnOnce(&mut Cursor<Vec<u8>>) -> std::io::Result<()>,
|
|
mut builder: HttpResponseBuilder,
|
|
) -> Result<HttpResponse, Error> {
|
|
let mut cursor = Cursor::new(vec![]);
|
|
(f)(&mut cursor)?;
|
|
let html = cursor.into_inner();
|
|
let output = minify_html::minify(&html, &minify_html::Cfg::spec_compliant());
|
|
drop(html);
|
|
|
|
Ok(builder
|
|
.content_type(mime::TEXT_HTML.essence_str())
|
|
.body(output))
|
|
}
|