2023-08-28 23:46:13 +00:00
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
|
|
|
use actix_web::{
|
|
|
|
body::BodyStream, error::ErrorInternalServerError, web, App, HttpResponse, HttpServer,
|
|
|
|
};
|
|
|
|
use clap::Parser;
|
2023-08-29 00:19:22 +00:00
|
|
|
use console_subscriber::ConsoleLayer;
|
|
|
|
use opentelemetry::{
|
|
|
|
sdk::{propagation::TraceContextPropagator, Resource},
|
|
|
|
KeyValue,
|
|
|
|
};
|
|
|
|
use opentelemetry_otlp::WithExportConfig;
|
2023-08-28 23:46:13 +00:00
|
|
|
use reqwest::{redirect::Policy, Client};
|
2023-08-29 00:19:22 +00:00
|
|
|
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
|
|
|
use reqwest_tracing::TracingMiddleware;
|
|
|
|
use tracing_actix_web::TracingLogger;
|
|
|
|
use tracing_error::ErrorLayer;
|
|
|
|
use tracing_subscriber::{
|
|
|
|
filter::Targets, layer::SubscriberExt, registry::LookupSpan, Layer, Registry,
|
|
|
|
};
|
2023-08-28 23:46:13 +00:00
|
|
|
use url::Url;
|
|
|
|
|
|
|
|
#[derive(Debug, Parser)]
|
|
|
|
struct Args {
|
2023-08-29 00:25:45 +00:00
|
|
|
#[clap(
|
|
|
|
short,
|
|
|
|
long,
|
|
|
|
env = "PICTRS_ADMIN__BIND_ADDRESS",
|
|
|
|
default_value = "127.0.0.0:8084"
|
|
|
|
)]
|
2023-08-28 23:46:13 +00:00
|
|
|
bind_address: SocketAddr,
|
2023-08-29 00:25:45 +00:00
|
|
|
#[clap(
|
|
|
|
long,
|
|
|
|
env = "PICTRS_ADMIN__PICTRS_ENDPOINT",
|
|
|
|
default_value = "http://localhost:8080"
|
|
|
|
)]
|
2023-08-29 00:19:22 +00:00
|
|
|
pict_rs_endpoint: Url,
|
|
|
|
#[clap(long, env = "PICTRS_ADMIN__PICTRS_API_KEY")]
|
2023-08-28 23:46:13 +00:00
|
|
|
pict_rs_api_key: String,
|
2023-08-29 00:19:22 +00:00
|
|
|
#[clap(long, env = "PICTRS_ADMIN__OPENTELEMETRY_URL")]
|
|
|
|
opentelemetry_url: Option<Url>,
|
|
|
|
#[clap(long, env = "PICTRS_ADMIN__OPENTELEMETRY_EVENT_BUFFER_SIZE")]
|
|
|
|
opentelemetry_event_buffer_size: Option<usize>,
|
2023-08-28 21:06:29 +00:00
|
|
|
}
|
2023-08-28 23:46:13 +00:00
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct PictrsClient {
|
2023-08-29 00:19:22 +00:00
|
|
|
client: ClientWithMiddleware,
|
|
|
|
pict_rs_endpoint: Url,
|
2023-08-28 23:46:13 +00:00
|
|
|
pict_rs_api_key: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
|
|
|
enum OkMessage {
|
|
|
|
#[serde(rename = "ok")]
|
|
|
|
Ok,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
struct PictrsDetails {
|
|
|
|
width: u16,
|
|
|
|
height: u16,
|
|
|
|
frames: Option<u32>,
|
|
|
|
content_type: String,
|
|
|
|
created_at: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
struct PictrsHash {
|
|
|
|
hex: String,
|
|
|
|
aliases: Vec<String>,
|
|
|
|
details: Option<PictrsDetails>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
pub struct PictrsPage {
|
|
|
|
#[allow(dead_code)]
|
|
|
|
limit: usize,
|
|
|
|
current: Option<String>,
|
|
|
|
prev: Option<String>,
|
|
|
|
next: Option<String>,
|
|
|
|
hashes: Vec<PictrsHash>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
enum PageResponse {
|
|
|
|
Ok {
|
|
|
|
#[allow(dead_code)]
|
|
|
|
msg: OkMessage,
|
|
|
|
page: PictrsPage,
|
|
|
|
},
|
|
|
|
Err {
|
|
|
|
msg: String,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
|
|
struct AliasQuery<'a> {
|
|
|
|
alias: &'a str,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PictrsHash {
|
2023-08-29 01:54:07 +00:00
|
|
|
fn media_link(&self) -> Option<String> {
|
2023-08-28 23:46:13 +00:00
|
|
|
self.aliases.first().map(|alias| format!("/image/{alias}"))
|
|
|
|
}
|
2023-08-29 01:54:07 +00:00
|
|
|
|
|
|
|
fn is_video(&self) -> bool {
|
|
|
|
self.details
|
|
|
|
.as_ref()
|
|
|
|
.map(|d| d.content_type.starts_with("video"))
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
2023-08-28 23:46:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl PictrsPage {
|
|
|
|
fn prev_link(&self) -> Option<String> {
|
|
|
|
self.prev.as_ref().map(|slug| format!("/?slug={slug}"))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn next_link(&self) -> Option<String> {
|
|
|
|
self.next.as_ref().map(|slug| format!("/?slug={slug}"))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn purge_link(&self, hash: &PictrsHash) -> Option<String> {
|
|
|
|
hash.aliases.first().map(|alias| {
|
|
|
|
if let Some(slug) = &self.current {
|
|
|
|
format!("/purge/{alias}?slug={slug}")
|
|
|
|
} else {
|
|
|
|
format!("/purge/{alias}")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PictrsClient {
|
2023-08-29 00:19:22 +00:00
|
|
|
async fn page(&self, slug: Option<String>) -> Result<PageResponse, reqwest_middleware::Error> {
|
|
|
|
let mut url = self.pict_rs_endpoint.clone();
|
2023-08-28 23:46:13 +00:00
|
|
|
url.set_path("/internal/hashes");
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
.client
|
|
|
|
.get(url.as_str())
|
|
|
|
.header("x-api-token", &self.pict_rs_api_key)
|
|
|
|
.query(&PageQuery { slug })
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
response.json().await.map_err(From::from)
|
2023-08-28 23:46:13 +00:00
|
|
|
}
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
async fn purge(&self, alias: &str) -> Result<(), reqwest_middleware::Error> {
|
|
|
|
let mut url = self.pict_rs_endpoint.clone();
|
2023-08-28 23:46:13 +00:00
|
|
|
url.set_path("/internal/purge");
|
|
|
|
|
|
|
|
let _ = self
|
|
|
|
.client
|
|
|
|
.post(url.as_str())
|
|
|
|
.header("x-api-token", &self.pict_rs_api_key)
|
2023-08-29 00:26:09 +00:00
|
|
|
.query(&AliasQuery { alias })
|
2023-08-28 23:46:13 +00:00
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
async fn proxy_image(&self, alias: &str) -> Result<HttpResponse, reqwest_middleware::Error> {
|
|
|
|
let mut url = self.pict_rs_endpoint.clone();
|
2023-08-28 23:46:13 +00:00
|
|
|
url.set_path(&format!("/image/original/{alias}"));
|
|
|
|
|
|
|
|
let response = self.client.get(url.as_str()).send().await?;
|
|
|
|
|
|
|
|
let mut client_res = HttpResponse::build(response.status());
|
|
|
|
|
|
|
|
for (name, value) in response
|
|
|
|
.headers()
|
|
|
|
.iter()
|
|
|
|
.filter(|(h, _)| *h != "connection")
|
|
|
|
{
|
|
|
|
client_res.insert_header((name.clone(), value.clone()));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(client_res.body(BodyStream::new(response.bytes_stream())))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
|
|
struct PageQuery {
|
|
|
|
slug: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn index(
|
|
|
|
web::Query(PageQuery { slug }): web::Query<PageQuery>,
|
|
|
|
client: web::Data<PictrsClient>,
|
|
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
|
|
let page = client.page(slug).await.map_err(ErrorInternalServerError)?;
|
|
|
|
|
|
|
|
let page = match page {
|
|
|
|
PageResponse::Ok { page, .. } => page,
|
|
|
|
PageResponse::Err { msg } => return Err(ErrorInternalServerError(msg)),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
templates::index_html(&mut buf, &page).map_err(ErrorInternalServerError)?;
|
2023-08-29 00:19:22 +00:00
|
|
|
let body = minify_html::minify(&buf, &minify_html::Cfg::spec_compliant());
|
2023-08-28 23:46:13 +00:00
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
2023-08-28 23:46:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn image(
|
|
|
|
alias: web::Path<String>,
|
|
|
|
client: web::Data<PictrsClient>,
|
|
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
|
|
client
|
|
|
|
.proxy_image(&alias)
|
|
|
|
.await
|
|
|
|
.map_err(ErrorInternalServerError)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct ConfirmQuery {
|
|
|
|
confirm: Option<u8>,
|
|
|
|
slug: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn purge(
|
|
|
|
alias: web::Path<String>,
|
|
|
|
client: web::Data<PictrsClient>,
|
|
|
|
web::Query(ConfirmQuery { confirm, slug }): web::Query<ConfirmQuery>,
|
|
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
|
|
let return_link = slug
|
|
|
|
.as_ref()
|
|
|
|
.map(|s| format!("/?slug={s}"))
|
|
|
|
.unwrap_or_else(|| String::from("/"));
|
|
|
|
|
|
|
|
if confirm.is_some() {
|
|
|
|
client
|
|
|
|
.purge(&alias)
|
|
|
|
.await
|
|
|
|
.map_err(ErrorInternalServerError)?;
|
|
|
|
|
|
|
|
return Ok(HttpResponse::SeeOther()
|
|
|
|
.insert_header(("location", return_link))
|
|
|
|
.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
let purge_link = slug
|
|
|
|
.map(|s| format!("/purge/{alias}?&slug={s}&confirm=1"))
|
|
|
|
.unwrap_or_else(|| format!("/purge/{alias}?confirm=1"));
|
|
|
|
|
|
|
|
let image_link = format!("/image/{alias}");
|
|
|
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
templates::purge_html(&mut buf, &image_link, &purge_link, &return_link)
|
|
|
|
.map_err(ErrorInternalServerError)?;
|
2023-08-29 00:19:22 +00:00
|
|
|
let body = minify_html::minify(&buf, &minify_html::Cfg::spec_compliant());
|
2023-08-28 23:46:13 +00:00
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
2023-08-28 23:46:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn serve_static(name: web::Path<String>) -> HttpResponse {
|
|
|
|
if let Some(data) = templates::statics::StaticFile::get(&name) {
|
|
|
|
HttpResponse::Ok().body(data.content)
|
|
|
|
} else {
|
|
|
|
HttpResponse::NotFound().finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
fn init_tracing(
|
|
|
|
service_name: &'static str,
|
|
|
|
opentelemetry_url: Option<&Url>,
|
|
|
|
console_event_buffer_size: Option<usize>,
|
|
|
|
) -> color_eyre::Result<()> {
|
|
|
|
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
|
|
|
|
|
|
|
|
tracing_log::LogTracer::init()?;
|
|
|
|
|
|
|
|
let targets: Targets = std::env::var("RUST_LOG")
|
|
|
|
.unwrap_or_else(|_| "info".into())
|
|
|
|
.parse()?;
|
|
|
|
|
|
|
|
let format_layer = tracing_subscriber::fmt::layer().with_filter(targets.clone());
|
|
|
|
|
|
|
|
let subscriber = Registry::default()
|
|
|
|
.with(format_layer)
|
|
|
|
.with(ErrorLayer::default());
|
|
|
|
|
|
|
|
if let Some(buffer_size) = console_event_buffer_size {
|
|
|
|
let console_layer = ConsoleLayer::builder()
|
|
|
|
.with_default_env()
|
|
|
|
.server_addr(([0, 0, 0, 0], 6669))
|
|
|
|
.event_buffer_capacity(buffer_size)
|
|
|
|
.spawn();
|
|
|
|
|
|
|
|
let subscriber = subscriber.with(console_layer);
|
|
|
|
|
|
|
|
init_subscriber(subscriber, targets, opentelemetry_url, service_name)
|
|
|
|
} else {
|
|
|
|
init_subscriber(subscriber, targets, opentelemetry_url, service_name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn init_subscriber<S>(
|
|
|
|
subscriber: S,
|
|
|
|
targets: Targets,
|
|
|
|
opentelemetry_url: Option<&Url>,
|
|
|
|
service_name: &'static str,
|
|
|
|
) -> color_eyre::Result<()>
|
|
|
|
where
|
|
|
|
S: SubscriberExt + Send + Sync,
|
|
|
|
for<'a> S: LookupSpan<'a>,
|
|
|
|
{
|
|
|
|
if let Some(url) = opentelemetry_url {
|
|
|
|
let tracer =
|
|
|
|
opentelemetry_otlp::new_pipeline()
|
|
|
|
.tracing()
|
|
|
|
.with_trace_config(opentelemetry::sdk::trace::config().with_resource(
|
|
|
|
Resource::new(vec![KeyValue::new("service.name", service_name)]),
|
|
|
|
))
|
|
|
|
.with_exporter(
|
|
|
|
opentelemetry_otlp::new_exporter()
|
|
|
|
.tonic()
|
|
|
|
.with_endpoint(url.as_str()),
|
|
|
|
)
|
|
|
|
.install_batch(opentelemetry::runtime::Tokio)?;
|
|
|
|
|
|
|
|
let otel_layer = tracing_opentelemetry::layer()
|
|
|
|
.with_tracer(tracer)
|
|
|
|
.with_filter(targets);
|
|
|
|
|
|
|
|
let subscriber = subscriber.with(otel_layer);
|
|
|
|
|
|
|
|
tracing::subscriber::set_global_default(subscriber)?;
|
|
|
|
} else {
|
|
|
|
tracing::subscriber::set_global_default(subscriber)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-08-28 23:46:13 +00:00
|
|
|
#[actix_web::main]
|
2023-08-29 00:19:22 +00:00
|
|
|
async fn main() -> color_eyre::Result<()> {
|
2023-08-28 23:46:13 +00:00
|
|
|
let Args {
|
|
|
|
bind_address,
|
2023-08-29 00:19:22 +00:00
|
|
|
pict_rs_endpoint,
|
2023-08-28 23:46:13 +00:00
|
|
|
pict_rs_api_key,
|
2023-08-29 00:19:22 +00:00
|
|
|
opentelemetry_url,
|
|
|
|
opentelemetry_event_buffer_size,
|
2023-08-28 23:46:13 +00:00
|
|
|
} = Args::parse();
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
init_tracing(
|
|
|
|
"pict-rs-admin",
|
|
|
|
opentelemetry_url.as_ref(),
|
|
|
|
opentelemetry_event_buffer_size,
|
|
|
|
)?;
|
|
|
|
|
2023-08-28 23:46:13 +00:00
|
|
|
let client = Client::builder()
|
|
|
|
.user_agent("pict-rs-admin v0.1.0")
|
|
|
|
.redirect(Policy::none())
|
|
|
|
.build()?;
|
|
|
|
|
2023-08-29 00:19:22 +00:00
|
|
|
let client = ClientBuilder::new(client)
|
|
|
|
.with(TracingMiddleware::default())
|
|
|
|
.build();
|
|
|
|
|
2023-08-28 23:46:13 +00:00
|
|
|
let client = PictrsClient {
|
|
|
|
client,
|
2023-08-29 00:19:22 +00:00
|
|
|
pict_rs_endpoint,
|
2023-08-28 23:46:13 +00:00
|
|
|
pict_rs_api_key,
|
|
|
|
};
|
|
|
|
|
|
|
|
HttpServer::new(move || {
|
|
|
|
App::new()
|
2023-08-29 00:19:22 +00:00
|
|
|
.wrap(TracingLogger::default())
|
2023-08-28 23:46:13 +00:00
|
|
|
.app_data(web::Data::new(client.clone()))
|
|
|
|
.route("/", web::get().to(index))
|
|
|
|
.route("/image/{path}", web::get().to(image))
|
|
|
|
.route("/purge/{path}", web::get().to(purge))
|
|
|
|
.route("/static/{path}", web::get().to(serve_static))
|
|
|
|
})
|
|
|
|
.bind(bind_address)?
|
|
|
|
.run()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|