Rename to Collections, use pict-rs's 'resize' instead of 'thumbnail'

This commit is contained in:
asonix 2020-12-09 23:09:39 -06:00
parent 4f270a5d41
commit f7d1baf3ec
13 changed files with 142 additions and 140 deletions

2
Cargo.lock generated
View file

@ -1334,7 +1334,7 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pict-rs-aggregator"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"actix-web",
"anyhow",

View file

@ -1,6 +1,6 @@
[package]
name = "pict-rs-aggregator"
version = "0.1.1"
version = "0.1.2"
authors = ["asonix <asonix@asonix.dog>"]
edition = "2018"
build = "src/build.rs"

View file

@ -2,7 +2,7 @@ version: '3.3'
services:
pictrs:
image: asonix/pictrs:v0.2.6-r0
image: asonix/pictrs:v0.3.0-alpha.0-r0
user: root
restart: always
volumes:

View file

@ -88,7 +88,7 @@ impl Connection {
fn thumbnail_url(&self, size: u16, file: &str, extension: Extension) -> String {
let mut url = self.upstream.clone();
url.set_path(&format!("/image/process.{}", extension));
url.set_query(Some(&format!("src={}&thumbnail={}", file, size)));
url.set_query(Some(&format!("src={}&resize={}", file, size)));
url.to_string()
}

View file

@ -77,41 +77,41 @@ impl State {
}
}
fn create_aggregation_path(&self) -> String {
fn create_collection_path(&self) -> String {
self.scoped("")
}
fn edit_aggregation_path(&self, id: Uuid, token: &ValidToken) -> String {
fn edit_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!("{}?token={}", id, token.token))
}
fn update_aggregation_path(&self, id: Uuid, token: &ValidToken) -> String {
fn update_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!("{}?token={}", id, token.token))
}
fn delete_aggregation_path(&self, id: Uuid, token: &ValidToken) -> String {
fn delete_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!("{}/delete?token={}", id, token.token))
}
fn public_aggregation_path(&self, id: Uuid) -> String {
fn public_collection_path(&self, id: Uuid) -> String {
self.scoped(&format!("{}", id))
}
fn create_entry_path(&self, aggregation_id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!("{}/entry?token={}", aggregation_id, token.token))
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, aggregation_id: Uuid, id: Uuid, token: &ValidToken) -> String {
fn update_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!(
"{}/entry/{}?token={}",
aggregation_id, id, token.token
collection_id, id, token.token
))
}
fn delete_entry_path(&self, aggregation_id: Uuid, id: Uuid, token: &ValidToken) -> String {
fn delete_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
self.scoped(&format!(
"{}/entry/{}/delete?token={}",
aggregation_id, id, token.token
collection_id, id, token.token
))
}
@ -162,17 +162,17 @@ pub fn service(client: Client, state: State) -> Scope {
.service(
web::resource("")
.route(web::get().to(index))
.route(web::post().to(create_aggregation)),
.route(web::post().to(create_collection)),
)
.service(
web::scope("/{aggregation}")
web::scope("/{collection}")
.wrap(middleware::Verify)
.service(
web::resource("")
.route(web::get().to(aggregation))
.route(web::post().to(update_aggregation)),
.route(web::get().to(collection))
.route(web::post().to(update_collection)),
)
.service(web::resource("/delete").route(web::get().to(delete_aggregation)))
.service(web::resource("/delete").route(web::get().to(delete_collection)))
.service(
web::scope("/entry")
.service(web::resource("").route(web::post().to(upload)))
@ -189,19 +189,19 @@ pub fn service(client: Client, state: State) -> Scope {
fn to_edit_page(id: Uuid, token: &ValidToken, state: &State) -> HttpResponse {
HttpResponse::SeeOther()
.header(LOCATION, state.edit_aggregation_path(id, token))
.header(LOCATION, state.edit_collection_path(id, token))
.finish()
}
fn to_404(state: &State) -> HttpResponse {
HttpResponse::MovedPermanently()
.header(LOCATION, state.create_aggregation_path())
.header(LOCATION, state.create_collection_path())
.finish()
}
fn to_home(state: &State) -> HttpResponse {
HttpResponse::SeeOther()
.header(LOCATION, state.create_aggregation_path())
.header(LOCATION, state.create_collection_path())
.finish()
}
@ -266,7 +266,7 @@ impl ResponseError for Error {
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Aggregation {
pub struct Collection {
title: String,
description: String,
}
@ -306,17 +306,17 @@ impl TokenStorage {
}
#[derive(serde::Deserialize)]
struct AggregationPath {
aggregation: Uuid,
struct CollectionPath {
collection: Uuid,
}
impl AggregationPath {
impl CollectionPath {
fn key(&self) -> String {
format!("{}", self.aggregation)
format!("{}", self.collection)
}
fn entry_range(&self) -> std::ops::Range<Vec<u8>> {
let base = format!("{}/entry/", self.aggregation).as_bytes().to_vec();
let base = format!("{}/entry/", self.collection).as_bytes().to_vec();
let mut start = base.clone();
let mut end = base;
@ -327,26 +327,26 @@ impl AggregationPath {
}
fn token_key(&self) -> String {
format!("{}/token", self.aggregation)
format!("{}/token", self.collection)
}
}
#[derive(serde::Deserialize)]
struct EntryPath {
aggregation: Uuid,
collection: Uuid,
entry: Uuid,
}
impl EntryPath {
fn key(&self) -> String {
format!("{}/entry/{}", self.aggregation, self.entry)
format!("{}/entry/{}", self.collection, self.entry)
}
}
async fn upload(
req: HttpRequest,
pl: web::Payload,
path: web::Path<AggregationPath>,
path: web::Path<CollectionPath>,
token: ValidToken,
conn: web::Data<Connection>,
state: web::Data<State>,
@ -370,7 +370,7 @@ async fn upload(
};
let entry_path = EntryPath {
aggregation: path.aggregation,
collection: path.collection,
entry: Uuid::new_v4(),
};
@ -381,7 +381,7 @@ async fn upload(
.exec(&state.store)
.await?;
Ok(to_edit_page(path.aggregation, &token, &state))
Ok(to_edit_page(path.collection, &token, &state))
}
#[derive(serde::Deserialize)]
@ -428,30 +428,30 @@ async fn index(state: web::Data<State>) -> Result<HttpResponse, Error> {
.body(cursor.into_inner()))
}
async fn aggregation(
path: web::Path<AggregationPath>,
async fn collection(
path: web::Path<CollectionPath>,
token: Option<ValidToken>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
match token {
Some(token) => edit_aggregation(path, token, state).await,
None => view_aggregation(path, state).await,
Some(token) => edit_collection(path, token, state).await,
None => view_collection(path, state).await,
}
}
async fn view_aggregation(
path: web::Path<AggregationPath>,
async fn view_collection(
path: web::Path<CollectionPath>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let aggregation = state.store.aggregation(&path).await?;
let collection = state.store.collection(&path).await?;
let entries = state.store.entries(path.entry_range()).await?;
let mut cursor = Cursor::new(vec![]);
self::templates::view_aggregation(
self::templates::view_collection(
&mut cursor,
path.aggregation,
&aggregation,
path.collection,
&collection,
&entries,
&state,
)?;
@ -461,20 +461,20 @@ async fn view_aggregation(
.body(cursor.into_inner()))
}
async fn edit_aggregation(
path: web::Path<AggregationPath>,
async fn edit_collection(
path: web::Path<CollectionPath>,
token: ValidToken,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let aggregation = state.store.aggregation(&path).await?;
let collection = state.store.collection(&path).await?;
let entries = state.store.entries(path.entry_range()).await?;
let mut cursor = Cursor::new(vec![]);
self::templates::edit_aggregation(
self::templates::edit_collection(
&mut cursor,
&aggregation,
path.aggregation,
&collection,
path.collection,
&entries,
&token,
&state,
@ -485,47 +485,47 @@ async fn edit_aggregation(
.body(cursor.into_inner()))
}
async fn create_aggregation(
aggregation: web::Form<Aggregation>,
async fn create_collection(
collection: web::Form<Collection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let aggregation_id = Uuid::new_v4();
let aggregation_path = AggregationPath {
aggregation: aggregation_id,
let collection_id = Uuid::new_v4();
let collection_path = CollectionPath {
collection: collection_id,
};
let token = Token {
token: Uuid::new_v4(),
};
store::CreateAggregation {
aggregation_path: &aggregation_path,
aggregation: &aggregation,
store::CreateCollection {
collection_path: &collection_path,
collection: &collection,
token: &token,
}
.exec(&state.store)
.await?;
Ok(to_edit_page(
aggregation_path.aggregation,
collection_path.collection,
&ValidToken { token: token.token },
&state,
))
}
async fn update_aggregation(
path: web::Path<AggregationPath>,
form: web::Form<Aggregation>,
async fn update_collection(
path: web::Path<CollectionPath>,
form: web::Form<Collection>,
token: ValidToken,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
store::UpdateAggregation {
aggregation_path: &path,
aggregation: &form,
store::UpdateCollection {
collection_path: &path,
collection: &form,
}
.exec(&state.store)
.await?;
Ok(to_edit_page(path.aggregation, &token, &state))
Ok(to_edit_page(path.collection, &token, &state))
}
async fn update_entry(
@ -541,7 +541,7 @@ async fn update_entry(
.exec(&state.store)
.await?;
Ok(to_edit_page(entry_path.aggregation, &token, &state))
Ok(to_edit_page(entry_path.collection, &token, &state))
}
async fn delete_entry(
@ -560,11 +560,11 @@ async fn delete_entry(
.exec(&state.store)
.await?;
Ok(to_edit_page(entry_path.aggregation, &token, &state))
Ok(to_edit_page(entry_path.collection, &token, &state))
}
async fn delete_aggregation(
path: web::Path<AggregationPath>,
async fn delete_collection(
path: web::Path<CollectionPath>,
_token: ValidToken,
conn: web::Data<Connection>,
state: web::Data<State>,
@ -578,8 +578,8 @@ async fn delete_aggregation(
futures::future::try_join_all(future_vec).await?;
store::DeleteAggregation {
aggregation_path: &path,
store::DeleteCollection {
collection_path: &path,
}
.exec(&state.store)
.await?;

View file

@ -3,6 +3,7 @@ use actix_web::{
middleware::{Compress, Logger},
App, HttpServer,
};
use std::time::Duration;
use structopt::StructOpt;
#[actix_web::main]
@ -20,6 +21,7 @@ async fn main() -> Result<(), anyhow::Error> {
HttpServer::new(move || {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.header("User-Agent", "pict_rs_aggregator-v0.1.0")
.finish();

View file

@ -100,7 +100,7 @@ where
let state_fut = web::Data::<crate::State>::extract(&req);
let token_fut = Option::<web::Query<crate::Token>>::extract(&req);
let path_fut = web::Path::<crate::AggregationPath>::extract(&req);
let path_fut = web::Path::<crate::CollectionPath>::extract(&req);
let req = ServiceRequest::from_parts(req, pl)
.map_err(|_| VerifyError)
@ -136,7 +136,7 @@ where
}
async fn verify(
path: &crate::AggregationPath,
path: &crate::CollectionPath,
token: crate::Token,
state: &crate::State,
) -> Result<(), VerifyError> {

View file

@ -1,4 +1,4 @@
use crate::{Aggregation, AggregationPath, Entry, EntryPath, Token};
use crate::{Collection, CollectionPath, Entry, EntryPath, Token};
use actix_web::web;
use sled::{Db, Tree};
use std::ops::Range;
@ -9,19 +9,19 @@ pub(crate) struct Store {
tree: Tree,
}
pub(crate) struct CreateAggregation<'a> {
pub(crate) aggregation_path: &'a AggregationPath,
pub(crate) aggregation: &'a Aggregation,
pub(crate) struct CreateCollection<'a> {
pub(crate) collection_path: &'a CollectionPath,
pub(crate) collection: &'a Collection,
pub(crate) token: &'a Token,
}
pub(crate) struct UpdateAggregation<'a> {
pub(crate) aggregation_path: &'a AggregationPath,
pub(crate) aggregation: &'a Aggregation,
pub(crate) struct UpdateCollection<'a> {
pub(crate) collection_path: &'a CollectionPath,
pub(crate) collection: &'a Collection,
}
pub(crate) struct DeleteAggregation<'a> {
pub(crate) aggregation_path: &'a AggregationPath,
pub(crate) struct DeleteCollection<'a> {
pub(crate) collection_path: &'a CollectionPath,
}
pub(crate) struct CreateEntry<'a> {
@ -38,21 +38,21 @@ pub(crate) struct DeleteEntry<'a> {
pub(crate) entry_path: &'a EntryPath,
}
impl<'a> CreateAggregation<'a> {
impl<'a> CreateCollection<'a> {
pub(crate) async fn exec(self, store: &Store) -> Result<(), Error> {
store.create_aggregation(self).await
store.create_collection(self).await
}
}
impl<'a> UpdateAggregation<'a> {
impl<'a> UpdateCollection<'a> {
pub(crate) async fn exec(self, store: &Store) -> Result<(), Error> {
store.update_aggregation(self).await
store.update_collection(self).await
}
}
impl<'a> DeleteAggregation<'a> {
impl<'a> DeleteCollection<'a> {
pub(crate) async fn exec(self, store: &Store) -> Result<(), Error> {
store.delete_aggregation(self).await
store.delete_collection(self).await
}
}
@ -77,15 +77,15 @@ impl<'a> DeleteEntry<'a> {
impl Store {
pub(crate) fn new(db: &Db) -> Result<Self, sled::Error> {
Ok(Store {
tree: db.open_tree("aggregations")?,
tree: db.open_tree("collections")?,
})
}
async fn create_aggregation(&self, config: CreateAggregation<'_>) -> Result<(), Error> {
let aggregation_key = config.aggregation_path.key();
let aggregation_value = serde_json::to_string(&config.aggregation)?;
async fn create_collection(&self, config: CreateCollection<'_>) -> Result<(), Error> {
let collection_key = config.collection_path.key();
let collection_value = serde_json::to_string(&config.collection)?;
let token_key = config.aggregation_path.token_key();
let token_key = config.collection_path.token_key();
let token2 = config.token.clone();
let token_value = serde_json::to_string(&web::block(move || token2.hash()).await?)?;
@ -93,7 +93,7 @@ impl Store {
web::block(move || {
tree.transaction(move |tree| {
tree.insert(aggregation_key.as_bytes(), aggregation_value.as_bytes())?;
tree.insert(collection_key.as_bytes(), collection_value.as_bytes())?;
tree.insert(token_key.as_bytes(), token_value.as_bytes())?;
Ok(())
})
@ -103,22 +103,22 @@ impl Store {
Ok(())
}
async fn update_aggregation(&self, config: UpdateAggregation<'_>) -> Result<(), Error> {
let aggregation_key = config.aggregation_path.key();
let aggregation_value = serde_json::to_string(&config.aggregation)?;
async fn update_collection(&self, config: UpdateCollection<'_>) -> Result<(), Error> {
let collection_key = config.collection_path.key();
let collection_value = serde_json::to_string(&config.collection)?;
let tree = self.tree.clone();
web::block(move || tree.insert(aggregation_key.as_bytes(), aggregation_value.as_bytes()))
web::block(move || tree.insert(collection_key.as_bytes(), collection_value.as_bytes()))
.await?;
Ok(())
}
async fn delete_aggregation(&self, config: DeleteAggregation<'_>) -> Result<(), Error> {
let entry_range = config.aggregation_path.entry_range();
let token_key = config.aggregation_path.token_key();
let aggregation_key = config.aggregation_path.key();
async fn delete_collection(&self, config: DeleteCollection<'_>) -> Result<(), Error> {
let entry_range = config.collection_path.entry_range();
let token_key = config.collection_path.token_key();
let collection_key = config.collection_path.key();
let tree = self.tree.clone();
@ -133,7 +133,7 @@ impl Store {
tree.remove(key)?;
}
tree.remove(token_key.as_bytes())?;
tree.remove(aggregation_key.as_bytes())?;
tree.remove(collection_key.as_bytes())?;
Ok(())
})?;
@ -175,19 +175,19 @@ impl Store {
Ok(())
}
pub(crate) async fn aggregation(
pub(crate) async fn collection(
&self,
path: &AggregationPath,
) -> Result<crate::Aggregation, Error> {
let aggregation_key = path.key();
path: &CollectionPath,
) -> Result<crate::Collection, Error> {
let collection_key = path.key();
let tree = self.tree.clone();
let opt = web::block(move || tree.get(aggregation_key.as_bytes())).await?;
let opt = web::block(move || tree.get(collection_key.as_bytes())).await?;
match opt {
Some(a) => {
let aggregation = serde_json::from_slice(&a)?;
Ok(aggregation)
let collection = serde_json::from_slice(&a)?;
Ok(collection)
}
None => Err(Error::NotFound),
}
@ -232,7 +232,7 @@ impl Store {
Ok(v)
}
pub(crate) async fn token(&self, path: &AggregationPath) -> Result<crate::TokenStorage, Error> {
pub(crate) async fn token(&self, path: &CollectionPath) -> Result<crate::TokenStorage, Error> {
let token_key = path.token_key();
let tree = self.tree.clone();

View file

@ -1,10 +1,10 @@
@use crate::{ui::ButtonKind, Aggregation, Entry, State, ValidToken};
@use crate::{ui::ButtonKind, Collection, Entry, State, ValidToken};
@use super::{button, button_link, image_preview, file_input, layout, text_area, text_input, statics::file_upload_js};
@use uuid::Uuid;
@(aggregation: &Aggregation, aggregation_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State)
@(collection: &Collection, collection_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State)
@:layout(state, "Edit Aggregation", None, {
@:layout(state, "Edit Collection", None, {
<script
src="@state.statics_path(&file_upload_js.name)"
type="text/javascript"
@ -13,11 +13,11 @@
}, {
<section>
<article class="content-group">
<h3>Share Aggregation</h3>
<h3>Share Collection</h3>
</article>
<article class="content-group">
<a
href="@state.public_aggregation_path(aggregation_id)"
href="@state.public_collection_path(collection_id)"
target="_blank"
rel="noopen noreferer"
>
@ -27,18 +27,18 @@
</section>
<section>
<article class="content-group">
<h3>Edit Aggregation</h3>
<h3>Edit Collection</h3>
</article>
<article class="content-group">
<p class="subtitle"><a href="@state.edit_aggregation_path(aggregation_id, token)">Do not lose this link</a></p>
<p class="subtitle"><a href="@state.edit_collection_path(collection_id, token)">Do not lose this link</a></p>
</article>
<article class="content-group">
<form method="POST" action="@state.update_aggregation_path(aggregation_id, token)">
@:text_input("title", Some("Title"), Some(&aggregation.title))
@:text_area("description", Some("Description"), Some(&aggregation.description))
<form method="POST" action="@state.update_collection_path(collection_id, token)">
@:text_input("title", Some("Title"), Some(&collection.title))
@:text_area("description", Some("Description"), Some(&collection.description))
<div class="button-space">
@:button("Update Aggregation", ButtonKind::Submit)
@:button_link("Delete Aggregation", &state.delete_aggregation_path(aggregation_id, token), ButtonKind::Outline)
@:button("Update Collection", ButtonKind::Submit)
@:button_link("Delete Collection", &state.delete_collection_path(collection_id, token), ButtonKind::Outline)
</div>
</form>
</article>
@ -55,14 +55,14 @@
</div>
</div>
<div class="edit-item">
<form method="POST" action="@state.update_entry_path(aggregation_id, *id, token)">
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">
@:text_input("title", Some("Title"), Some(&entry.title))
@:text_area("description", Some("Description"), Some(&entry.description))
<input type="hidden" name="filename" value="@entry.filename" />
<input type="hidden" name="delete_token" value="@entry.delete_token" />
<div class="button-space">
@:button("Update Image", ButtonKind::Submit)
@:button_link("Delete Image", &state.delete_entry_path(aggregation_id, *id, token), ButtonKind::Outline)
@:button_link("Delete Image", &state.delete_entry_path(collection_id, *id, token), ButtonKind::Outline)
</div>
</form>
</div>
@ -76,7 +76,7 @@
<article>
<form
method="POST"
action="@state.create_entry_path(aggregation_id, token)"
action="@state.create_entry_path(collection_id, token)"
enctype="multipart/form-data"
>
<div class="content-group">

View file

@ -3,19 +3,19 @@
@(state: &State)
@:layout(state, "Aggregation", None, {}, {
@:layout(state, "Collection", None, {}, {
<section>
<article>
<form method="POST" action="@state.create_aggregation_path()">
<form method="POST" action="@state.create_collection_path()">
<div class="content-group">
<h3><legend>Create Aggregation</legend></h3>
<h3><legend>Create Collection</legend></h3>
</div>
<div class="content-group">
@:text_input("title", Some("Title"), None)
@:text_area("description", Some("Description"), None)
</div>
<div class="content-group">
@:button("Create Aggregation", ButtonKind::Submit)
@:button("Create Collection", ButtonKind::Submit)
</div>
</form>
</article>

View file

@ -15,7 +15,7 @@
@if let Some(description) = description {
<meta property="og:description" content="@description" />
} else {
<meta property="og:description" content="Aggregate and share images" />
<meta property="og:description" content="Collect and share images" />
}
<meta property="og:type" content="website" />
<link rel="shortcut icon" type="image/png" href="@state.statics_path(&favicon_ico.name)">

View file

@ -9,7 +9,7 @@
<h3>Not Found</h3>
</article>
<article class="content-group">
<p><a href="@state.create_aggregation_path()">Return Home</a></p>
<p><a href="@state.create_collection_path()">Return Home</a></p>
</article>
</section>
})

View file

@ -1,11 +1,11 @@
@use crate::{Aggregation, Entry, State};
@use crate::{Collection, Entry, State};
@use super::{layout, image_preview};
@use uuid::Uuid;
@(id: Uuid, aggregation: &Aggregation, entries: &[(Uuid, Entry)], state: &State)
@(id: Uuid, collection: &Collection, entries: &[(Uuid, Entry)], state: &State)
@:layout(state, &aggregation.title, Some(&aggregation.description), {
<meta property="og:url" content="@state.public_aggregation_path(id)" />
@:layout(state, &collection.title, Some(&collection.description), {
<meta property="og:url" content="@state.public_collection_path(id)" />
@for (_, entry) in entries {
<meta property="og:image" content="@state.image_path(entry)" />
}
@ -13,10 +13,10 @@
<section>
<article>
<div class="content-group">
<h3>@aggregation.title</h3>
<h3>@collection.title</h3>
</div>
<div class="content-group">
<p class="subtitle">@aggregation.description</p>
<p class="subtitle">@collection.description</p>
</div>
</article>
<ul>