2021-12-03 20:51:14 +00:00
|
|
|
// need this for ructe
|
|
|
|
#![allow(clippy::needless_borrow)]
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
use actix_web::{
|
|
|
|
http::{
|
|
|
|
header::{CacheControl, CacheDirective, ContentType, LastModified, LOCATION},
|
|
|
|
StatusCode,
|
|
|
|
},
|
2021-09-19 19:32:04 +00:00
|
|
|
web, HttpRequest, HttpResponse, HttpResponseBuilder, ResponseError,
|
2020-12-08 21:59:55 +00:00
|
|
|
};
|
2021-03-10 02:24:04 +00:00
|
|
|
use awc::Client;
|
2022-05-28 16:53:46 +00:00
|
|
|
use clap::Parser;
|
2024-02-02 00:14:29 +00:00
|
|
|
use rustls::{
|
|
|
|
sign::CertifiedKey, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore,
|
|
|
|
};
|
2020-12-08 21:59:55 +00:00
|
|
|
use sled::Db;
|
2021-09-12 16:30:22 +00:00
|
|
|
use std::{
|
|
|
|
io::Cursor,
|
|
|
|
net::SocketAddr,
|
|
|
|
path::{Path, PathBuf},
|
2023-11-13 23:07:05 +00:00
|
|
|
sync::Arc,
|
2021-09-12 16:30:22 +00:00
|
|
|
time::SystemTime,
|
|
|
|
};
|
2020-12-08 21:59:55 +00:00
|
|
|
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;
|
2022-06-21 01:49:42 +00:00
|
|
|
mod optional;
|
2020-12-08 21:59:55 +00:00
|
|
|
mod pict;
|
|
|
|
mod store;
|
|
|
|
mod ui;
|
|
|
|
|
2022-06-21 01:49:42 +00:00
|
|
|
use self::{connection::Connection, middleware::ValidToken, optional::Optional, store::Store};
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
/// A simple image collection service backed by pict-rs
|
2022-05-28 16:53:46 +00:00
|
|
|
#[derive(Clone, Debug, Parser)]
|
2022-09-28 23:43:28 +00:00
|
|
|
#[command(author, version, about, long_about = None)]
|
2020-12-08 21:59:55 +00:00
|
|
|
pub struct Config {
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2020-12-08 21:59:55 +00:00
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_ADDR",
|
2024-02-02 00:14:29 +00:00
|
|
|
default_value = "[::]:8082",
|
2020-12-08 21:59:55 +00:00
|
|
|
help = "The address and port the server binds to"
|
|
|
|
)]
|
|
|
|
addr: SocketAddr,
|
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2020-12-08 21:59:55 +00:00
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_UPSTREAM",
|
|
|
|
default_value = "http://localhost:8080",
|
|
|
|
help = "The url of the upstream pict-rs server"
|
|
|
|
)]
|
|
|
|
upstream: Url,
|
2021-09-06 20:54:19 +00:00
|
|
|
|
2024-02-02 00:14:29 +00:00
|
|
|
#[arg(
|
|
|
|
short = 'H',
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_HOST",
|
|
|
|
default_value = "localhost:8082",
|
|
|
|
help = "The host at which pict-rs-aggregator is accessible"
|
|
|
|
)]
|
|
|
|
host: String,
|
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2021-09-06 20:54:19 +00:00
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_DATABASE",
|
|
|
|
default_value = "sled/db-0-34",
|
|
|
|
help = "The path to the database"
|
|
|
|
)]
|
|
|
|
database_path: PathBuf,
|
2021-09-19 19:32:04 +00:00
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2022-03-23 15:02:17 +00:00
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_SLED_CACHE_CAPACITY",
|
|
|
|
default_value = "67108864",
|
|
|
|
help = "The amount of RAM, in bytes, that sled is allowed to consume. Increasing this value can improve performance"
|
|
|
|
)]
|
|
|
|
sled_cache_capacity: u64,
|
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2022-03-23 15:02:17 +00:00
|
|
|
short,
|
2024-02-02 00:14:29 +00:00
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_CONSOLE_ADDRESS",
|
|
|
|
help = "The address at which to bind the tokio-console exporter"
|
|
|
|
)]
|
|
|
|
console_address: Option<SocketAddr>,
|
|
|
|
|
|
|
|
#[arg(
|
2022-03-23 15:02:17 +00:00
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_CONSOLE_EVENT_BUFFER_SIZE",
|
2024-02-02 00:14:29 +00:00
|
|
|
help = "The number of events to buffer in console"
|
2022-03-23 15:02:17 +00:00
|
|
|
)]
|
|
|
|
console_event_buffer_size: Option<usize>,
|
|
|
|
|
2022-09-28 23:43:28 +00:00
|
|
|
#[arg(
|
2021-09-19 19:32:04 +00:00
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_OPENTELEMETRY_URL",
|
|
|
|
help = "URL for the OpenTelemetry Colletor"
|
|
|
|
)]
|
|
|
|
opentelemetry_url: Option<Url>,
|
2024-02-02 00:14:29 +00:00
|
|
|
|
|
|
|
#[arg(
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_CERTIFICATE",
|
|
|
|
help = "The CA Certificate used to verify pict-rs' TLS certificate"
|
|
|
|
)]
|
|
|
|
certificate: Option<PathBuf>,
|
|
|
|
|
|
|
|
#[arg(
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_SERVER_CERTIFICATE",
|
|
|
|
help = "The Certificate used to serve pict-rs-aggregator over TLS"
|
|
|
|
)]
|
|
|
|
server_certificate: Option<PathBuf>,
|
|
|
|
|
|
|
|
#[arg(
|
|
|
|
long,
|
|
|
|
env = "PICTRS_AGGREGATOR_SERVER_PRIVATE_KEY",
|
|
|
|
help = "The Private Key used to serve pict-rs-aggregator over TLS"
|
|
|
|
)]
|
|
|
|
server_private_key: Option<PathBuf>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Tls {
|
|
|
|
certificate: PathBuf,
|
|
|
|
private_key: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Tls {
|
|
|
|
pub fn from_config(config: &Config) -> Option<Self> {
|
|
|
|
config
|
|
|
|
.server_certificate
|
|
|
|
.as_ref()
|
|
|
|
.zip(config.server_private_key.as_ref())
|
|
|
|
.map(|(cert, key)| Tls {
|
|
|
|
certificate: cert.clone(),
|
|
|
|
private_key: key.clone(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn open_keys(&self) -> color_eyre::Result<CertifiedKey> {
|
|
|
|
let cert_bytes = tokio::fs::read(&self.certificate).await?;
|
|
|
|
let key_bytes = tokio::fs::read(&self.private_key).await?;
|
|
|
|
|
|
|
|
let certs = rustls_pemfile::certs(&mut cert_bytes.as_slice())
|
|
|
|
.map(|res| res.map(|c| Certificate(c.to_vec())))
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
|
|
|
|
let key = rustls_pemfile::private_key(&mut key_bytes.as_slice())?
|
|
|
|
.ok_or_else(|| color_eyre::eyre::eyre!("No key in keyfile"))?;
|
|
|
|
|
|
|
|
let signing_key =
|
|
|
|
rustls::sign::any_supported_type(&PrivateKey(Vec::from(key.secret_der())))?;
|
|
|
|
|
|
|
|
Ok(CertifiedKey::new(certs, signing_key))
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn accept() -> &'static str {
|
2023-07-14 01:01:47 +00:00
|
|
|
"image/apng,image/avif,image/gif,image/png,image/jpeg,image/jxl,image/webp,.apng,.avif,.gif,.jpg,.jpeg,.jxl,.png,.webp"
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Config {
|
2021-09-06 20:54:19 +00:00
|
|
|
pub fn db_path(&self) -> &Path {
|
|
|
|
&self.database_path
|
|
|
|
}
|
|
|
|
|
2022-03-23 15:02:17 +00:00
|
|
|
pub fn sled_cache_capacity(&self) -> u64 {
|
|
|
|
self.sled_cache_capacity
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn console_event_buffer_size(&self) -> Option<usize> {
|
|
|
|
self.console_event_buffer_size
|
|
|
|
}
|
|
|
|
|
2024-02-02 00:14:29 +00:00
|
|
|
pub fn console_address(&self) -> Option<SocketAddr> {
|
|
|
|
self.console_address
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
pub fn bind_address(&self) -> SocketAddr {
|
|
|
|
self.addr
|
|
|
|
}
|
2021-09-19 19:32:04 +00:00
|
|
|
|
|
|
|
pub fn opentelemetry_url(&self) -> Option<&Url> {
|
|
|
|
self.opentelemetry_url.as_ref()
|
|
|
|
}
|
2024-02-02 00:14:29 +00:00
|
|
|
|
|
|
|
pub async fn build_rustls_client_config(&self) -> color_eyre::Result<ClientConfig> {
|
|
|
|
let mut root_store = RootCertStore {
|
|
|
|
roots: webpki_roots::TLS_SERVER_ROOTS
|
|
|
|
.iter()
|
|
|
|
.map(|root| {
|
|
|
|
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
|
|
|
root.subject.to_vec(),
|
|
|
|
root.subject_public_key_info.to_vec(),
|
|
|
|
root.name_constraints.as_ref().map(|n| n.to_vec()),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(certificate) = &self.certificate {
|
|
|
|
let bytes = tokio::fs::read(certificate).await?;
|
|
|
|
|
|
|
|
for res in rustls_pemfile::certs(&mut bytes.as_slice()) {
|
|
|
|
root_store.add(&Certificate(res?.to_vec()))?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(ClientConfig::builder()
|
|
|
|
.with_safe_defaults()
|
|
|
|
.with_root_certificates(root_store)
|
|
|
|
.with_no_client_auth())
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 15:44:46 +00:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
2022-01-30 20:52:49 +00:00
|
|
|
pub enum Direction {
|
|
|
|
#[serde(rename = "up")]
|
|
|
|
Up,
|
|
|
|
|
|
|
|
#[serde(rename = "down")]
|
|
|
|
Down,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for Direction {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
match self {
|
|
|
|
Self::Up => write!(f, "up"),
|
|
|
|
Self::Down => write!(f, "down"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct State {
|
|
|
|
upstream: Url,
|
2024-02-02 00:14:29 +00:00
|
|
|
host: String,
|
2020-12-08 21:59:55 +00:00
|
|
|
scope: String,
|
|
|
|
store: Store,
|
2020-12-12 04:33:56 +00:00
|
|
|
startup: SystemTime,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
impl std::fmt::Debug for State {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
f.debug_struct("State")
|
2022-03-23 15:32:58 +00:00
|
|
|
.field("upstream", &self.upstream.as_str())
|
2021-09-19 19:32:04 +00:00
|
|
|
.field("scope", &self.scope)
|
|
|
|
.field("store", &self.store)
|
|
|
|
.field("db", &"Db")
|
|
|
|
.field("startup", &self.startup)
|
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
impl State {
|
|
|
|
fn scoped(&self, s: &str) -> String {
|
2021-09-12 16:30:22 +00:00
|
|
|
if self.scope.is_empty() && s.is_empty() {
|
2020-12-08 21:59:55 +00:00
|
|
|
"/".to_string()
|
2021-09-12 16:30:22 +00:00
|
|
|
} else if s.is_empty() {
|
2020-12-08 21:59:55 +00:00
|
|
|
self.scope.clone()
|
2021-09-12 16:30:22 +00:00
|
|
|
} else if self.scope.is_empty() {
|
2023-03-10 03:57:38 +00:00
|
|
|
format!("/{s}")
|
2020-12-08 21:59:55 +00:00
|
|
|
} else {
|
2023-03-10 03:57:38 +00:00
|
|
|
format!("{}/{s}", self.scope)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
fn create_collection_path(&self) -> String {
|
2020-12-08 21:59:55 +00:00
|
|
|
self.scoped("")
|
|
|
|
}
|
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
fn edit_collection_path(
|
|
|
|
&self,
|
|
|
|
collection_id: Uuid,
|
|
|
|
entry_id: Option<Uuid>,
|
|
|
|
token: &ValidToken,
|
|
|
|
) -> String {
|
|
|
|
if let Some(entry_id) = entry_id {
|
|
|
|
self.scoped(&format!("{collection_id}?token={}#{entry_id}", token.token))
|
|
|
|
} else {
|
|
|
|
self.scoped(&format!("{collection_id}?token={}", token.token))
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
fn update_collection_path(&self, collection_id: Uuid, token: &ValidToken) -> String {
|
|
|
|
self.scoped(&format!("{collection_id}?token={}", token.token))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-12 03:36:07 +00:00
|
|
|
fn delete_collection_path(&self, id: Uuid, token: &ValidToken, confirmed: bool) -> String {
|
|
|
|
if confirmed {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("{id}/delete?token={}&confirmed=true", token.token))
|
2020-12-12 03:36:07 +00:00
|
|
|
} else {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("{id}/delete?token={}", token.token))
|
2020-12-12 03:36:07 +00:00
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
fn public_collection_path(&self, id: Uuid) -> String {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("{id}"))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
fn create_entry_path(&self, collection_id: Uuid, token: &ValidToken) -> String {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("{collection_id}/entry?token={}", token.token))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
fn update_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("{collection_id}/entry/{id}?token={}", token.token))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
fn move_entry_path(
|
|
|
|
&self,
|
|
|
|
collection_id: Uuid,
|
|
|
|
id: Uuid,
|
|
|
|
token: &ValidToken,
|
|
|
|
direction: Direction,
|
|
|
|
) -> String {
|
|
|
|
self.scoped(&format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"{collection_id}/entry/{id}/move/{direction}?token={}",
|
|
|
|
token.token
|
2022-01-30 20:52:49 +00:00
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2020-12-12 03:36:07 +00:00
|
|
|
fn delete_entry_path(
|
|
|
|
&self,
|
|
|
|
collection_id: Uuid,
|
|
|
|
id: Uuid,
|
|
|
|
token: &ValidToken,
|
|
|
|
confirmed: bool,
|
|
|
|
) -> String {
|
|
|
|
if confirmed {
|
|
|
|
self.scoped(&format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"{collection_id}/entry/{id}/delete?token={}&confirmed=true",
|
|
|
|
token.token
|
2020-12-12 03:36:07 +00:00
|
|
|
))
|
|
|
|
} else {
|
|
|
|
self.scoped(&format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"{collection_id}/entry/{id}/delete?token={}",
|
|
|
|
token.token
|
2020-12-12 03:36:07 +00:00
|
|
|
))
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn statics_path(&self, file: &str) -> String {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("static/{file}"))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
fn thumbnail_path(&self, filename: &str, size: u16, extension: pict::Extension) -> String {
|
2020-12-08 21:59:55 +00:00
|
|
|
self.scoped(&format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"image/thumbnail.{extension}?src={filename}&size={size}",
|
2020-12-08 21:59:55 +00:00
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
fn srcset(&self, filename: &str, extension: pict::Extension) -> String {
|
|
|
|
let mut sizes = Vec::new();
|
|
|
|
|
|
|
|
for size in connection::VALID_SIZES {
|
|
|
|
sizes.push(format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"{} {size}w",
|
2022-05-27 23:27:44 +00:00
|
|
|
self.thumbnail_path(filename, *size, extension),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
sizes.join(", ")
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
fn image_path(&self, filename: &str) -> String {
|
2023-03-10 03:57:38 +00:00
|
|
|
self.scoped(&format!("image/full/{filename}"))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn state(config: Config, scope: &str, db: Db) -> Result<State, sled::Error> {
|
|
|
|
Ok(State {
|
|
|
|
upstream: config.upstream,
|
2024-02-02 00:14:29 +00:00
|
|
|
host: config.host,
|
2020-12-08 21:59:55 +00:00
|
|
|
scope: scope.to_string(),
|
|
|
|
store: Store::new(&db)?,
|
2020-12-12 04:33:56 +00:00
|
|
|
startup: SystemTime::now(),
|
2020-12-08 21:59:55 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
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))
|
2023-03-10 03:57:38 +00:00
|
|
|
.route("/healthz", web::get().to(healthz))
|
2021-09-19 19:32:04 +00:00
|
|
|
.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)))
|
2022-01-30 20:52:49 +00:00
|
|
|
.service(
|
|
|
|
web::resource("/move/{direction}").route(web::get().to(move_entry)),
|
|
|
|
)
|
2021-09-19 19:32:04 +00:00
|
|
|
.service(web::resource("/delete").route(web::get().to(delete_entry))),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
fn to_edit_page(
|
|
|
|
collection_id: Uuid,
|
|
|
|
entry_id: Option<Uuid>,
|
|
|
|
token: &ValidToken,
|
|
|
|
state: &State,
|
|
|
|
) -> HttpResponse {
|
2020-12-08 21:59:55 +00:00
|
|
|
HttpResponse::SeeOther()
|
2023-11-13 18:29:50 +00:00
|
|
|
.insert_header((
|
|
|
|
LOCATION,
|
|
|
|
state.edit_collection_path(collection_id, entry_id, token),
|
|
|
|
))
|
2020-12-08 21:59:55 +00:00
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_404(state: &State) -> HttpResponse {
|
|
|
|
HttpResponse::MovedPermanently()
|
2021-02-10 21:20:36 +00:00
|
|
|
.insert_header((LOCATION, state.create_collection_path()))
|
2020-12-08 21:59:55 +00:00
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_home(state: &State) -> HttpResponse {
|
|
|
|
HttpResponse::SeeOther()
|
2021-02-10 21:20:36 +00:00
|
|
|
.insert_header((LOCATION, state.create_collection_path()))
|
2020-12-08 21:59:55 +00:00
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
|
2020-12-12 04:33:56 +00:00
|
|
|
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>,
|
|
|
|
{
|
2023-11-13 23:07:05 +00:00
|
|
|
#[track_caller]
|
2020-12-12 04:33:56 +00:00
|
|
|
fn stateful(self, state: &web::Data<State>) -> Result<T, StateError> {
|
|
|
|
self.map_err(Error::from).map_err(|error| StateError {
|
|
|
|
state: state.clone(),
|
2023-11-13 23:07:05 +00:00
|
|
|
debug: Arc::from(format!("{error:?}")),
|
|
|
|
display: Arc::from(format!("{error}")),
|
2020-12-12 04:33:56 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-10 03:57:38 +00:00
|
|
|
async fn healthz(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
|
|
|
state.store.check_health().await.stateful(&state)?;
|
|
|
|
Ok(HttpResponse::Ok().finish())
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[tracing::instrument(name = "Static files")]
|
2022-02-01 16:47:43 +00:00
|
|
|
async fn static_files(filename: web::Path<String>, state: web::Data<State>) -> HttpResponse {
|
2020-12-08 21:59:55 +00:00
|
|
|
let filename = filename.into_inner();
|
|
|
|
|
|
|
|
if let Some(data) = self::templates::statics::StaticFile::get(&filename) {
|
|
|
|
return HttpResponse::Ok()
|
2021-02-10 21:20:36 +00:00
|
|
|
.insert_header(LastModified(state.startup.into()))
|
|
|
|
.insert_header(CacheControl(vec![
|
2020-12-08 21:59:55 +00:00
|
|
|
CacheDirective::Public,
|
|
|
|
CacheDirective::MaxAge(365 * DAYS),
|
|
|
|
CacheDirective::Extension("immutable".to_owned(), None),
|
|
|
|
]))
|
2021-02-10 21:20:36 +00:00
|
|
|
.insert_header(ContentType(data.mime.clone()))
|
2020-12-08 21:59:55 +00:00
|
|
|
.body(data.content);
|
|
|
|
}
|
|
|
|
|
|
|
|
to_404(&state)
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Not found", skip_all)]
|
2020-12-12 04:33:56 +00:00
|
|
|
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
2020-12-14 20:23:02 +00:00
|
|
|
rendered(
|
2023-01-29 19:51:04 +00:00
|
|
|
|cursor| self::templates::not_found_html(cursor, &state),
|
2020-12-14 20:23:02 +00:00
|
|
|
HttpResponse::NotFound(),
|
|
|
|
)
|
|
|
|
.stateful(&state)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-12 04:33:56 +00:00
|
|
|
struct StateError {
|
|
|
|
state: web::Data<State>,
|
2023-11-13 23:07:05 +00:00
|
|
|
debug: Arc<str>,
|
|
|
|
display: Arc<str>,
|
2020-12-12 04:33:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Debug for StateError {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
2023-11-13 23:07:05 +00:00
|
|
|
f.write_str(&self.debug)
|
2020-12-12 04:33:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for StateError {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
2023-11-13 23:07:05 +00:00
|
|
|
f.write_str(&self.display)
|
2020-12-12 04:33:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ResponseError for StateError {
|
|
|
|
fn status_code(&self) -> StatusCode {
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
|
|
}
|
|
|
|
|
|
|
|
fn error_response(&self) -> HttpResponse {
|
2020-12-14 20:23:02 +00:00
|
|
|
match rendered(
|
2023-11-13 23:07:05 +00:00
|
|
|
|cursor| self::templates::error_html(cursor, &self.display, &self.state),
|
2020-12-14 20:23:02 +00:00
|
|
|
HttpResponse::build(self.status_code()),
|
|
|
|
) {
|
|
|
|
Ok(res) => res,
|
2020-12-12 04:33:56 +00:00
|
|
|
Err(_) => HttpResponse::build(self.status_code())
|
|
|
|
.content_type(mime::TEXT_PLAIN.essence_str())
|
2023-11-13 23:07:05 +00:00
|
|
|
.body(self.display.to_string()),
|
2021-09-20 18:19:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Error {
|
2023-11-13 23:07:05 +00:00
|
|
|
context: color_eyre::Report,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Debug for Error {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
self.context.fmt(f)
|
|
|
|
}
|
2021-09-20 18:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for Error {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
2023-11-13 23:07:05 +00:00
|
|
|
self.context.fmt(f)
|
2021-09-20 18:19:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::error::Error for Error {
|
|
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
2023-11-13 23:07:05 +00:00
|
|
|
self.context.source()
|
2021-09-20 18:19:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> From<T> for Error
|
|
|
|
where
|
|
|
|
ErrorKind: From<T>,
|
|
|
|
{
|
2023-11-13 23:07:05 +00:00
|
|
|
#[track_caller]
|
2021-09-20 18:19:50 +00:00
|
|
|
fn from(error: T) -> Self {
|
|
|
|
Error {
|
2023-11-13 23:07:05 +00:00
|
|
|
context: color_eyre::Report::from(ErrorKind::from(error)),
|
2020-12-12 04:33:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
2021-09-20 18:19:50 +00:00
|
|
|
enum ErrorKind {
|
2023-11-13 23:07:05 +00:00
|
|
|
#[error("Error in IO")]
|
2020-12-08 21:59:55 +00:00
|
|
|
Render(#[from] std::io::Error),
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[error("Error in store")]
|
2020-12-08 21:59:55 +00:00
|
|
|
Store(#[from] self::store::Error),
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[error("Error in pict-rs connection")]
|
2020-12-08 21:59:55 +00:00
|
|
|
Upload(#[from] self::connection::UploadError),
|
|
|
|
|
|
|
|
#[error("{0}")]
|
|
|
|
UploadString(String),
|
2023-11-13 21:38:00 +00:00
|
|
|
|
|
|
|
#[error("Operation canceled")]
|
|
|
|
Canceled,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
2020-12-10 05:09:39 +00:00
|
|
|
pub struct Collection {
|
2020-12-08 21:59:55 +00:00
|
|
|
title: String,
|
|
|
|
description: String,
|
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
pub enum EntryKind {
|
|
|
|
Pending {
|
|
|
|
upload_id: String,
|
|
|
|
},
|
|
|
|
Ready {
|
|
|
|
filename: String,
|
|
|
|
delete_token: String,
|
2023-11-13 21:38:00 +00:00
|
|
|
dimensions: Option<Dimensions>,
|
2022-05-27 23:27:44 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-11-13 21:38:00 +00:00
|
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
|
|
|
|
pub struct Dimensions {
|
|
|
|
width: u32,
|
|
|
|
height: u32,
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
pub struct Entry {
|
2022-06-21 01:49:42 +00:00
|
|
|
title: Optional<String>,
|
|
|
|
description: Optional<String>,
|
|
|
|
link: Optional<Url>,
|
2022-05-27 23:27:44 +00:00
|
|
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
file_info: EntryKind,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
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)?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
struct TokenStorage {
|
|
|
|
token: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TokenStorage {
|
|
|
|
fn verify(&self, token: &Token) -> Result<bool, bcrypt::BcryptError> {
|
2023-11-26 03:19:27 +00:00
|
|
|
bcrypt::verify(token.token.as_bytes(), &self.token)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
2020-12-10 05:09:39 +00:00
|
|
|
struct CollectionPath {
|
|
|
|
collection: Uuid,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
impl CollectionPath {
|
2020-12-08 21:59:55 +00:00
|
|
|
fn key(&self) -> String {
|
2020-12-10 05:09:39 +00:00
|
|
|
format!("{}", self.collection)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
fn order_key(&self) -> String {
|
|
|
|
format!("{}/order", self.collection)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn entry_range(&self) -> std::ops::RangeInclusive<Vec<u8>> {
|
2020-12-10 05:09:39 +00:00
|
|
|
let base = format!("{}/entry/", self.collection).as_bytes().to_vec();
|
2020-12-08 21:59:55 +00:00
|
|
|
let mut start = base.clone();
|
|
|
|
let mut end = base;
|
|
|
|
|
|
|
|
start.push(0x0);
|
|
|
|
end.push(0xff);
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
start..=end
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn token_key(&self) -> String {
|
2020-12-10 05:09:39 +00:00
|
|
|
format!("{}/token", self.collection)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
struct EntryPath {
|
2020-12-10 05:09:39 +00:00
|
|
|
collection: Uuid,
|
2020-12-08 21:59:55 +00:00
|
|
|
entry: Uuid,
|
|
|
|
}
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct MoveEntryPath {
|
|
|
|
collection: Uuid,
|
|
|
|
entry: Uuid,
|
|
|
|
direction: Direction,
|
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
impl Entry {
|
|
|
|
pub(crate) fn filename(&self) -> Option<&str> {
|
|
|
|
if let EntryKind::Ready { filename, .. } = &self.file_info {
|
|
|
|
Some(&filename)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn file_parts(&self) -> Option<(&str, &str)> {
|
|
|
|
if let EntryKind::Ready {
|
|
|
|
filename,
|
|
|
|
delete_token,
|
2023-11-13 21:38:00 +00:00
|
|
|
..
|
2022-05-27 23:27:44 +00:00
|
|
|
} = &self.file_info
|
|
|
|
{
|
|
|
|
Some((&filename, &delete_token))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 21:38:00 +00:00
|
|
|
pub(crate) fn dimensions(&self) -> Option<Dimensions> {
|
|
|
|
if let EntryKind::Ready {
|
|
|
|
dimensions: Some(dimensions),
|
|
|
|
..
|
|
|
|
} = &self.file_info
|
|
|
|
{
|
|
|
|
Some(*dimensions)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
pub(crate) fn upload_id(&self) -> Option<&str> {
|
|
|
|
if let EntryKind::Pending { upload_id } = &self.file_info {
|
|
|
|
Some(&upload_id)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
impl EntryPath {
|
|
|
|
fn key(&self) -> String {
|
2020-12-10 05:09:39 +00:00
|
|
|
format!("{}/entry/{}", self.collection, self.entry)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
impl MoveEntryPath {
|
|
|
|
fn order_key(&self) -> String {
|
|
|
|
format!("{}/order", self.collection)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn entry_range(&self) -> std::ops::RangeInclusive<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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Upload image", skip(req, pl, token, conn, state))]
|
2020-12-08 21:59:55 +00:00
|
|
|
async fn upload(
|
|
|
|
req: HttpRequest,
|
|
|
|
pl: web::Payload,
|
2020-12-10 05:09:39 +00:00
|
|
|
path: web::Path<CollectionPath>,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: ValidToken,
|
|
|
|
conn: web::Data<Connection>,
|
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2022-05-27 23:27:44 +00:00
|
|
|
let uploads = conn.upload(&req, pl).await.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
if uploads.is_err() {
|
|
|
|
return Err(ErrorKind::UploadString(uploads.message().to_owned())).stateful(&state);
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
let upload = uploads
|
2020-12-08 21:59:55 +00:00
|
|
|
.files()
|
|
|
|
.next()
|
2021-09-20 18:19:50 +00:00
|
|
|
.ok_or_else(|| ErrorKind::UploadString("Missing file".to_owned()))
|
2020-12-12 04:33:56 +00:00
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
|
|
|
let entry = Entry {
|
2022-06-21 01:49:42 +00:00
|
|
|
title: None.into(),
|
|
|
|
description: None.into(),
|
|
|
|
link: None.into(),
|
2022-05-27 23:27:44 +00:00
|
|
|
file_info: EntryKind::Pending {
|
|
|
|
upload_id: upload.id().to_owned(),
|
|
|
|
},
|
2020-12-08 21:59:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let entry_path = EntryPath {
|
2020-12-10 05:09:39 +00:00
|
|
|
collection: path.collection,
|
2020-12-08 21:59:55 +00:00
|
|
|
entry: Uuid::new_v4(),
|
|
|
|
};
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
let entry_path2 = entry_path.clone();
|
|
|
|
let state2 = state.clone();
|
|
|
|
let upload = upload.clone();
|
|
|
|
actix_rt::spawn(async move {
|
|
|
|
match conn.claim(upload).await {
|
|
|
|
Ok(images) => {
|
|
|
|
if let Some(image) = images.files().next() {
|
|
|
|
let res = store::GetEntry {
|
|
|
|
entry_path: &entry_path2,
|
|
|
|
}
|
|
|
|
.exec(&state2.store)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
if let Ok(mut entry) = res {
|
|
|
|
entry.file_info = EntryKind::Ready {
|
|
|
|
filename: image.file().to_owned(),
|
|
|
|
delete_token: image.delete_token().to_owned(),
|
2023-11-13 21:38:00 +00:00
|
|
|
dimensions: Some(Dimensions {
|
|
|
|
width: image.width(),
|
|
|
|
height: image.height(),
|
|
|
|
}),
|
2022-05-27 23:27:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let _ = store::UpdateEntry {
|
|
|
|
entry_path: &entry_path2,
|
|
|
|
entry: &entry,
|
|
|
|
}
|
|
|
|
.exec(&state2.store)
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => {
|
2023-03-10 03:57:38 +00:00
|
|
|
tracing::warn!("{e}");
|
2022-05-27 23:27:44 +00:00
|
|
|
let _ = store::DeleteEntry {
|
|
|
|
entry_path: &entry_path2,
|
|
|
|
}
|
|
|
|
.exec(&state2.store)
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-12-08 21:59:55 +00:00
|
|
|
store::CreateEntry {
|
|
|
|
entry_path: &entry_path,
|
|
|
|
entry: &entry,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
Ok(to_edit_page(
|
|
|
|
path.collection,
|
|
|
|
Some(entry_path.entry),
|
|
|
|
&token,
|
|
|
|
&state,
|
|
|
|
))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
struct ImagePath {
|
|
|
|
filename: String,
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Serve image", skip(req, conn, state))]
|
2020-12-08 21:59:55 +00:00
|
|
|
async fn image(
|
|
|
|
req: HttpRequest,
|
|
|
|
path: web::Path<ImagePath>,
|
|
|
|
conn: web::Data<Connection>,
|
2023-11-13 23:07:05 +00:00
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, StateError> {
|
|
|
|
conn.image(&path.filename, &req).await.stateful(&state)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
struct ThumbnailPath {
|
|
|
|
extension: pict::Extension,
|
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
2020-12-08 21:59:55 +00:00
|
|
|
struct ThumbnailQuery {
|
|
|
|
src: String,
|
|
|
|
size: u16,
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Serve thumbnail", skip(req, conn, state))]
|
2020-12-08 21:59:55 +00:00
|
|
|
async fn thumbnail(
|
|
|
|
req: HttpRequest,
|
|
|
|
path: web::Path<ThumbnailPath>,
|
|
|
|
query: web::Query<ThumbnailQuery>,
|
|
|
|
conn: web::Data<Connection>,
|
2023-11-13 23:07:05 +00:00
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-08 21:59:55 +00:00
|
|
|
conn.thumbnail(query.size, &query.src, path.extension, &req)
|
|
|
|
.await
|
2023-11-13 23:07:05 +00:00
|
|
|
.stateful(&state)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Index", skip_all)]
|
2020-12-12 04:33:56 +00:00
|
|
|
async fn index(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
2020-12-14 20:23:02 +00:00
|
|
|
rendered(
|
2023-01-29 19:51:04 +00:00
|
|
|
|cursor| self::templates::index_html(cursor, &state),
|
2020-12-14 20:23:02 +00:00
|
|
|
HttpResponse::Ok(),
|
|
|
|
)
|
|
|
|
.stateful(&state)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Collection", skip(req, token, connection, state))]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn collection(
|
2023-11-13 23:07:05 +00:00
|
|
|
req: HttpRequest,
|
2020-12-10 05:09:39 +00:00
|
|
|
path: web::Path<CollectionPath>,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: Option<ValidToken>,
|
2023-11-13 21:38:00 +00:00
|
|
|
connection: web::Data<Connection>,
|
2023-11-13 23:07:05 +00:00
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-08 21:59:55 +00:00
|
|
|
match token {
|
2023-11-13 23:07:05 +00:00
|
|
|
Some(token) => edit_collection(req, path, token, connection, state.clone())
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state),
|
2023-11-13 21:38:00 +00:00
|
|
|
None => view_collection(path, connection, state.clone())
|
|
|
|
.await
|
|
|
|
.stateful(&state),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn save_dimensions(
|
|
|
|
state: web::Data<State>,
|
|
|
|
connection: web::Data<Connection>,
|
|
|
|
collection_id: Uuid,
|
|
|
|
entry_id: Uuid,
|
|
|
|
filename: String,
|
|
|
|
) -> Result<Entry, Error> {
|
|
|
|
let pict::Details { width, height } = connection.details(&filename).await?;
|
|
|
|
|
|
|
|
let entry_path = EntryPath {
|
|
|
|
collection: collection_id,
|
|
|
|
entry: entry_id,
|
|
|
|
};
|
|
|
|
|
|
|
|
let entry = store::GetEntry {
|
|
|
|
entry_path: &entry_path,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
if entry.dimensions().is_none() {
|
|
|
|
let entry = Entry {
|
|
|
|
file_info: if let EntryKind::Ready {
|
|
|
|
filename,
|
|
|
|
delete_token,
|
|
|
|
..
|
|
|
|
} = entry.file_info
|
|
|
|
{
|
|
|
|
EntryKind::Ready {
|
|
|
|
filename,
|
|
|
|
delete_token,
|
|
|
|
dimensions: Some(Dimensions { width, height }),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
entry.file_info
|
|
|
|
},
|
|
|
|
..entry
|
|
|
|
};
|
|
|
|
|
|
|
|
store::UpdateEntry {
|
|
|
|
entry_path: &entry_path,
|
|
|
|
entry: &entry,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(entry)
|
|
|
|
} else {
|
|
|
|
Ok(entry)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 21:38:00 +00:00
|
|
|
async fn ensure_dimensions(
|
|
|
|
state: web::Data<State>,
|
|
|
|
connection: web::Data<Connection>,
|
|
|
|
collection_id: Uuid,
|
|
|
|
mut entries: Vec<(Uuid, Entry)>,
|
|
|
|
) -> Result<Vec<(Uuid, Entry)>, Error> {
|
|
|
|
let updates = entries
|
|
|
|
.iter()
|
|
|
|
.map(|(entry_id, entry)| {
|
|
|
|
if entry.dimensions().is_none() {
|
|
|
|
if let Some(filename) = entry.filename() {
|
|
|
|
let filename = filename.to_string();
|
|
|
|
let state = state.clone();
|
|
|
|
let connection = connection.clone();
|
|
|
|
|
|
|
|
Some(actix_rt::spawn(save_dimensions(
|
|
|
|
state,
|
|
|
|
connection,
|
|
|
|
collection_id,
|
|
|
|
*entry_id,
|
|
|
|
filename,
|
|
|
|
)))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
for ((_, entry), update) in entries.iter_mut().zip(updates) {
|
|
|
|
if let Some(handle) = update {
|
2023-11-13 23:32:26 +00:00
|
|
|
if let Ok(Ok(updated_entry)) = handle.await {
|
|
|
|
*entry = updated_entry;
|
|
|
|
}
|
2023-11-13 21:38:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(entries)
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "View Collection", skip(connection, state))]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn view_collection(
|
|
|
|
path: web::Path<CollectionPath>,
|
2023-11-13 21:38:00 +00:00
|
|
|
connection: web::Data<Connection>,
|
2020-12-08 21:59:55 +00:00
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
2020-12-12 04:33:56 +00:00
|
|
|
let collection = match state.store.collection(&path).await? {
|
|
|
|
Some(collection) => collection,
|
|
|
|
None => return Ok(to_404(&state)),
|
|
|
|
};
|
2023-11-13 21:38:00 +00:00
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
let entries = state
|
|
|
|
.store
|
|
|
|
.entries(path.order_key(), path.entry_range())
|
|
|
|
.await?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 21:38:00 +00:00
|
|
|
let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?;
|
|
|
|
|
2020-12-14 20:23:02 +00:00
|
|
|
rendered(
|
|
|
|
|cursor| {
|
2023-03-10 03:57:38 +00:00
|
|
|
self::templates::view_collection_html(
|
|
|
|
cursor,
|
|
|
|
path.collection,
|
|
|
|
&collection,
|
|
|
|
&entries,
|
|
|
|
&state,
|
|
|
|
)
|
2020-12-14 20:23:02 +00:00
|
|
|
},
|
|
|
|
HttpResponse::Ok(),
|
|
|
|
)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Edit Collection", skip(req, connection, state))]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn edit_collection(
|
2023-11-13 23:07:05 +00:00
|
|
|
req: HttpRequest,
|
2020-12-10 05:09:39 +00:00
|
|
|
path: web::Path<CollectionPath>,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: ValidToken,
|
2023-11-13 21:38:00 +00:00
|
|
|
connection: web::Data<Connection>,
|
2020-12-08 21:59:55 +00:00
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
2022-02-19 02:08:56 +00:00
|
|
|
let qr = qr(&req, &path, &state);
|
|
|
|
|
2020-12-12 04:33:56 +00:00
|
|
|
let collection = match state.store.collection(&path).await? {
|
|
|
|
Some(collection) => collection,
|
|
|
|
None => return Ok(to_404(&state)),
|
|
|
|
};
|
2022-01-30 20:52:49 +00:00
|
|
|
let entries = state
|
|
|
|
.store
|
|
|
|
.entries(path.order_key(), path.entry_range())
|
|
|
|
.await?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 21:38:00 +00:00
|
|
|
let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?;
|
|
|
|
|
2020-12-14 20:23:02 +00:00
|
|
|
rendered(
|
|
|
|
|cursor| {
|
2023-01-29 19:51:04 +00:00
|
|
|
self::templates::edit_collection_html(
|
2020-12-14 20:23:02 +00:00
|
|
|
cursor,
|
|
|
|
&collection,
|
|
|
|
path.collection,
|
|
|
|
&entries,
|
|
|
|
&token,
|
|
|
|
&state,
|
2022-02-19 02:08:56 +00:00
|
|
|
&qr,
|
2020-12-14 20:23:02 +00:00
|
|
|
)
|
|
|
|
},
|
|
|
|
HttpResponse::Ok(),
|
|
|
|
)
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Create Collection", skip_all)]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn create_collection(
|
|
|
|
collection: web::Form<Collection>,
|
2020-12-08 21:59:55 +00:00
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-10 05:09:39 +00:00
|
|
|
let collection_id = Uuid::new_v4();
|
|
|
|
let collection_path = CollectionPath {
|
|
|
|
collection: collection_id,
|
2020-12-08 21:59:55 +00:00
|
|
|
};
|
|
|
|
let token = Token {
|
|
|
|
token: Uuid::new_v4(),
|
|
|
|
};
|
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
store::CreateCollection {
|
|
|
|
collection_path: &collection_path,
|
|
|
|
collection: &collection,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: &token,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
|
|
|
Ok(to_edit_page(
|
2020-12-10 05:09:39 +00:00
|
|
|
collection_path.collection,
|
2023-11-13 18:29:50 +00:00
|
|
|
None,
|
2020-12-08 21:59:55 +00:00
|
|
|
&ValidToken { token: token.token },
|
|
|
|
&state,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Update Collection", skip(token, state))]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn update_collection(
|
|
|
|
path: web::Path<CollectionPath>,
|
|
|
|
form: web::Form<Collection>,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: ValidToken,
|
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-10 05:09:39 +00:00
|
|
|
store::UpdateCollection {
|
|
|
|
collection_path: &path,
|
|
|
|
collection: &form,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
Ok(to_edit_page(path.collection, None, &token, &state))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-01-30 20:52:49 +00:00
|
|
|
async fn move_entry(
|
|
|
|
path: web::Path<MoveEntryPath>,
|
|
|
|
token: ValidToken,
|
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, StateError> {
|
|
|
|
store::MoveEntry {
|
|
|
|
move_entry_path: &path,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
Ok(to_edit_page(
|
|
|
|
path.collection,
|
|
|
|
Some(path.entry),
|
|
|
|
&token,
|
|
|
|
&state,
|
|
|
|
))
|
2022-01-30 20:52:49 +00:00
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Update Entry", skip(token, state))]
|
2020-12-08 21:59:55 +00:00
|
|
|
async fn update_entry(
|
|
|
|
entry_path: web::Path<EntryPath>,
|
|
|
|
entry: web::Form<Entry>,
|
|
|
|
token: ValidToken,
|
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-08 21:59:55 +00:00
|
|
|
store::UpdateEntry {
|
|
|
|
entry_path: &entry_path,
|
|
|
|
entry: &entry,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
Ok(to_edit_page(
|
|
|
|
entry_path.collection,
|
|
|
|
Some(entry_path.entry),
|
|
|
|
&token,
|
|
|
|
&state,
|
|
|
|
))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-19 19:32:04 +00:00
|
|
|
#[derive(Debug, serde::Deserialize)]
|
2020-12-12 03:36:07 +00:00
|
|
|
struct ConfirmQuery {
|
|
|
|
confirmed: Option<bool>,
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Delete Entry", skip(token, conn, state))]
|
2020-12-08 21:59:55 +00:00
|
|
|
async fn delete_entry(
|
|
|
|
entry_path: web::Path<EntryPath>,
|
2020-12-12 03:36:07 +00:00
|
|
|
query: web::Query<ConfirmQuery>,
|
2020-12-08 21:59:55 +00:00
|
|
|
token: ValidToken,
|
|
|
|
conn: web::Data<Connection>,
|
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
|
|
|
let res = state.store.entry(&entry_path).await.stateful(&state)?;
|
2020-12-12 03:36:07 +00:00
|
|
|
|
2020-12-12 04:33:56 +00:00
|
|
|
let entry = match res {
|
|
|
|
Some(entry) => entry,
|
|
|
|
None => return Ok(to_404(&state)),
|
|
|
|
};
|
|
|
|
|
|
|
|
if !query.confirmed.unwrap_or(false) {
|
2020-12-14 20:23:02 +00:00
|
|
|
return rendered(
|
|
|
|
|cursor| {
|
2023-01-29 19:51:04 +00:00
|
|
|
self::templates::confirm_entry_delete_html(
|
2020-12-14 20:23:02 +00:00
|
|
|
cursor,
|
|
|
|
entry_path.collection,
|
|
|
|
entry_path.entry,
|
|
|
|
&entry,
|
|
|
|
&token,
|
|
|
|
&state,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
HttpResponse::Ok(),
|
2020-12-12 04:33:56 +00:00
|
|
|
)
|
2020-12-14 20:23:02 +00:00
|
|
|
.stateful(&state);
|
2020-12-12 03:36:07 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 23:27:44 +00:00
|
|
|
if let EntryKind::Ready {
|
|
|
|
filename,
|
|
|
|
delete_token,
|
2023-11-13 21:38:00 +00:00
|
|
|
..
|
2022-05-27 23:27:44 +00:00
|
|
|
} = &entry.file_info
|
|
|
|
{
|
|
|
|
conn.delete(filename, delete_token).await.stateful(&state)?;
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
|
|
|
|
store::DeleteEntry {
|
|
|
|
entry_path: &entry_path,
|
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2023-11-13 18:29:50 +00:00
|
|
|
Ok(to_edit_page(entry_path.collection, None, &token, &state))
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
|
2022-02-19 02:08:56 +00:00
|
|
|
fn qr(req: &HttpRequest, path: &web::Path<CollectionPath>, state: &web::Data<State>) -> String {
|
2024-02-02 00:14:29 +00:00
|
|
|
let host = req
|
|
|
|
.head()
|
|
|
|
.headers()
|
|
|
|
.get("host")
|
|
|
|
.and_then(|h| h.to_str().ok())
|
|
|
|
.unwrap_or(&state.host);
|
2022-02-19 02:08:56 +00:00
|
|
|
|
|
|
|
let url = format!(
|
2024-02-02 00:14:29 +00:00
|
|
|
"https://{host}{}",
|
2022-02-19 02:08:56 +00:00
|
|
|
state.public_collection_path(path.collection)
|
|
|
|
);
|
|
|
|
|
|
|
|
let code = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Low).unwrap();
|
|
|
|
|
|
|
|
to_svg_string(&code, 4)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_svg_string(qr: &qrcodegen::QrCode, border: i32) -> String {
|
|
|
|
assert!(border >= 0, "Border must be non-negative");
|
|
|
|
let mut result = String::new();
|
|
|
|
// result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
|
|
// result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
|
|
|
|
let dimension = qr
|
|
|
|
.size()
|
|
|
|
.checked_add(border.checked_mul(2).unwrap())
|
|
|
|
.unwrap();
|
|
|
|
result += &format!(
|
2023-03-10 03:57:38 +00:00
|
|
|
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">\n");
|
2022-02-19 02:08:56 +00:00
|
|
|
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
|
|
|
result += "\t<path d=\"";
|
|
|
|
for y in 0..qr.size() {
|
|
|
|
for x in 0..qr.size() {
|
|
|
|
if qr.get_module(x, y) {
|
|
|
|
if x != 0 || y != 0 {
|
|
|
|
result += " ";
|
|
|
|
}
|
|
|
|
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result += "\" fill=\"#000000\"/>\n";
|
|
|
|
result += "</svg>\n";
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
2023-11-13 23:07:05 +00:00
|
|
|
#[tracing::instrument(name = "Delete Collection", skip(token, conn, state))]
|
2020-12-10 05:09:39 +00:00
|
|
|
async fn delete_collection(
|
|
|
|
path: web::Path<CollectionPath>,
|
2020-12-12 03:36:07 +00:00
|
|
|
query: web::Query<ConfirmQuery>,
|
|
|
|
token: ValidToken,
|
2020-12-08 21:59:55 +00:00
|
|
|
conn: web::Data<Connection>,
|
|
|
|
state: web::Data<State>,
|
2020-12-12 04:33:56 +00:00
|
|
|
) -> Result<HttpResponse, StateError> {
|
2020-12-12 03:36:07 +00:00
|
|
|
if !query.confirmed.unwrap_or(false) {
|
2020-12-12 04:33:56 +00:00
|
|
|
let res = state.store.collection(&path).await.stateful(&state)?;
|
|
|
|
|
|
|
|
let collection = match res {
|
|
|
|
Some(collection) => collection,
|
|
|
|
None => return Ok(to_404(&state)),
|
|
|
|
};
|
2020-12-12 03:36:07 +00:00
|
|
|
|
2020-12-14 20:23:02 +00:00
|
|
|
return rendered(
|
|
|
|
|cursor| {
|
2023-01-29 19:51:04 +00:00
|
|
|
self::templates::confirm_delete_html(
|
2020-12-14 20:23:02 +00:00
|
|
|
cursor,
|
|
|
|
path.collection,
|
|
|
|
&collection,
|
|
|
|
&token,
|
|
|
|
&state,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
HttpResponse::Ok(),
|
|
|
|
)
|
|
|
|
.stateful(&state);
|
2020-12-12 03:36:07 +00:00
|
|
|
}
|
|
|
|
|
2020-12-12 04:33:56 +00:00
|
|
|
let entries = state
|
|
|
|
.store
|
2022-01-30 20:52:49 +00:00
|
|
|
.entries(path.order_key(), path.entry_range())
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
|
|
|
let future_vec = entries
|
|
|
|
.iter()
|
2021-09-06 20:44:01 +00:00
|
|
|
.map(|(_, entry)| {
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
2022-05-27 23:27:44 +00:00
|
|
|
if let EntryKind::Ready {
|
|
|
|
filename,
|
|
|
|
delete_token,
|
2023-11-13 21:38:00 +00:00
|
|
|
..
|
2022-05-27 23:27:44 +00:00
|
|
|
} = entry.file_info.clone()
|
|
|
|
{
|
|
|
|
let conn = conn.clone();
|
|
|
|
actix_rt::spawn(async move {
|
|
|
|
let _ = tx.send(conn.delete(&filename, &delete_token).await);
|
|
|
|
});
|
|
|
|
}
|
2021-09-06 20:44:01 +00:00
|
|
|
rx
|
|
|
|
})
|
2020-12-08 21:59:55 +00:00
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
2021-09-06 20:44:01 +00:00
|
|
|
let mut results = Vec::new();
|
|
|
|
for rx in future_vec {
|
2023-11-13 23:07:05 +00:00
|
|
|
results.push(rx.await.map_err(|_| ErrorKind::Canceled).stateful(&state)?);
|
2021-09-06 20:44:01 +00:00
|
|
|
}
|
2020-12-12 04:41:59 +00:00
|
|
|
|
|
|
|
// 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)?;
|
|
|
|
}
|
|
|
|
}
|
2020-12-08 21:59:55 +00:00
|
|
|
|
2020-12-10 05:09:39 +00:00
|
|
|
store::DeleteCollection {
|
|
|
|
collection_path: &path,
|
2020-12-08 21:59:55 +00:00
|
|
|
}
|
|
|
|
.exec(&state.store)
|
2020-12-12 04:33:56 +00:00
|
|
|
.await
|
|
|
|
.stateful(&state)?;
|
2020-12-08 21:59:55 +00:00
|
|
|
|
|
|
|
Ok(to_home(&state))
|
|
|
|
}
|
2020-12-14 20:23:02 +00:00
|
|
|
|
|
|
|
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)?;
|
2021-09-06 20:44:01 +00:00
|
|
|
let html = cursor.into_inner();
|
|
|
|
let output = minify_html::minify(&html, &minify_html::Cfg::spec_compliant());
|
|
|
|
drop(html);
|
2020-12-14 20:23:02 +00:00
|
|
|
|
|
|
|
Ok(builder
|
|
|
|
.content_type(mime::TEXT_HTML.essence_str())
|
2021-09-06 20:44:01 +00:00
|
|
|
.body(output))
|
2020-12-14 20:23:02 +00:00
|
|
|
}
|