switch
from
...
to
  Create Merge Request
... ... @@ -2,6 +2,7 @@ Cargo.lock
2 2 **/*.rs.bk
3 3
4 4 **/target
  5 +/data/copyright.png
5 6 /pkg
6 7 /static/ui/
7 8 /var
... ...
1   -.PHONY: start wasm build run clean
2   -
3 1 PROFILE ?= dev
4 2 ifeq "$(PROFILE)" "release"
5 3 CARGO_PROFILE = --release
6 4 WASM_PROFILE = --release
7 5 WASM_EXTRA = --no-default-features --features wee_alloc
  6 +SERVER_TARGET = target/release/artshop-server
  7 +WASM_TARGET = ui/target/wasm32-unknown-unknown/release/artshop_frontend.wasm
8 8 else
9 9 CARGO_PROFILE =
10 10 WASM_PROFILE = --dev
11 11 WASM_EXTRA =
  12 +SERVER_TARGET = target/debug/artshop-server
  13 +WASM_TARGET = ui/target/wasm32-unknown-unknown/debug/artshop_frontend.wasm
12 14 endif
13 15
  16 +SERVER_SOURCES = common/Cargo.toml \
  17 + server/Cargo.toml \
  18 + $(shell find common -name "*.rs") \
  19 + $(shell find server -name "*.rs")
  20 +WASM_DEPLOY = static/ui/artshop_frontend_bg.wasm
  21 +WASM_SOURCES = common/Cargo.toml \
  22 + ui/Cargo.toml \
  23 + $(shell find common -name "*.rs") \
  24 + $(shell find ui -name "*.rs")
  25 +
14 26 define msg
  27 + @printf "\033[38;5;197m%s\033[0m" "$(1)"
  28 +endef
  29 +
  30 +define msgnl
15 31 @printf "\033[38;5;197m%s\033[0m\n" "$(1)"
16 32 endef
17 33
  34 +.PHONY: start run wasm build clean release
  35 +
18 36 start:
19   - systemfd --no-pid -s 0.0.0.0:3000 -- \
20   - cargo watch -i static/ -i var/ -s "PROFILE=$(PROFILE) make run"
  37 + $(call msgnl,NOTIFY REBUILD RUN)
  38 + @systemfd --no-pid -s 0.0.0.0:3000 -- \
  39 + cargo watch -i static/ -i var/ \
  40 + -s "PROFILE=$(PROFILE) make run"
21 41
22   -wasm:
23   - $(call msg,BUILD WASM UI)
  42 +run: build wasm data/copyright.png
  43 + $(call msgnl,RUN SERVER)
  44 + @PROFILE=$(PROFILE) cargo run $(CARGO_PROFILE) --bin artshop-server
  45 +
  46 +wasm: $(WASM_DEPLOY)
  47 +
  48 +build: $(SERVER_TARGET)
  49 +
  50 +clean:
  51 + $(call msgnl,CLEAN WORKSPACE)
  52 + @PROFILE=$(PROFILE) cargo clean
  53 + $(call msgnl,CLEAN INSTALLED WASM)
  54 + @rm -Rf ./static/ui
  55 + $(call msgnl,CLEAN INSTALLED WASM)
  56 + @pushd ui; PROFILE=$(PROFILE) cargo clean; popd
  57 + $(call msgnl,CLEAN COPYRIGHT PNG)
  58 + @rm -Rf ./data/copyright.png
  59 +
  60 +$(WASM_TARGET): $(WASM_SOURCES)
  61 + $(call msgnl,BUILD WASM UI)
  62 + @PROFILE=$(PROFILE) wasm-pack build $(WASM_PROFILE) -d ../static/ui \
  63 + -t web --mode no-install ./ui -- $(WASM_EXTRA)
  64 +
  65 +$(WASM_DEPLOY): $(WASM_TARGET)
  66 + $(call msgnl,INSTALL WASM UI)
24 67 @PROFILE=$(PROFILE) wasm-pack build $(WASM_PROFILE) -d ../static/ui \
25 68 -t web ./ui -- $(WASM_EXTRA)
26 69
27   -build:
28   - $(call msg,BUILD SERVER)
  70 +$(SERVER_TARGET): $(SERVER_SOURCES)
  71 + $(call msgnl,PATCH DIFFY)
  72 + @PROFILE=$(PROFILE) cargo patch
  73 + $(call msgnl,BUILD SERVER)
29 74 @PROFILE=$(PROFILE) cargo build $(CARGO_PROFILE) --bin artshop-server
30 75
31   -run: build wasm
32   - $(call msg,RUN SERVER)
33   - @PROFILE=$(PROFILE) cargo run $(CARGO_PROFILE) --bin artshop-server
  76 +data/copyright.png: data/copyright.txt
  77 + $(call msgnl,CREATE COPYRIGHT PNG)
  78 + @cat $< | convert -pointsize 24 -font Helvetica \
  79 + -background transparent -fill "rgba(255,255,255,0.35)" \
  80 + text:- -trim +repage $@
34 81
35 82 release:
36 83 docker build -t artshop -f build/Dockerfile .
37 84
  85 +.PHONY: createdb devdb downdb enterdb rootdb
  86 +
  87 +createdb: downdb devdb
  88 + $(call msg,TRY TO CONNECT TO DB )
  89 + @while true;\
  90 + do\
  91 + echo "SELECT VERSION();"|\
  92 + docker exec -i mariadb-dev \
  93 + mysql -p123456 >/dev/null 2>&1 && break;\
  94 + echo -n ".";\
  95 + sleep 3;\
  96 + done; echo
  97 + $(call msgnl,INITIALIZE APPLICATION DB)
  98 + echo "CREATE DATABASE artshop CHARACTER SET = 'utf8mb3' \
  99 + COLLATE = 'utf8mb3_general_ci'; \
  100 + GRANT ALL PRIVILEGES ON artshop.* TO 'artshop'@'%'"|\
  101 + docker exec -i mariadb-dev mysql -p123456
  102 + $(call msgnl,RUN MIGRATIONS)
  103 + @diesel migration run
  104 +
  105 +downdb:
  106 + $(call msgnl,STOP DB CONTAINER)
  107 + @docker stop mariadb-dev||true
  108 + $(call msgnl,REMOVE DB CONTAINER)
  109 + @docker rm -v mariadb-dev||true
  110 + $(call msgnl,REMOVE IMAGES)
  111 + @rm -Rf var/lib/artshop/images
  112 +
38 113 devdb:
39   - docker network create mariadb-dev-network
40   - docker run --detach --network mariadb-dev-network --name mariadb-dev \
  114 + $(call msgnl,CREATE DB CONTAINER)
  115 + @docker run --detach --name mariadb-dev \
41 116 -p 3306:3306 \
42 117 --env MARIADB_USER=artshop \
43 118 --env MARIADB_PASSWORD=123456 \
44 119 --env MARIADB_ROOT_PASSWORD=123456 mariadb:latest
45 120
46 121 enterdb:
47   - docker exec -it mariadb-dev mysql -D artshop -u artshop -p
  122 + docker exec -it mariadb-dev mysql -D artshop -u artshop -p123456||true
48 123 # docker run -it --network mariadb-dev-network --rm mariadb:latest \
49 124 # mysql -h mariadb-dev -u artshop -p
50 125
51 126 rootdb:
52   - docker exec -it mariadb-dev mysql -p
53   -
54   -clean:
55   - cargo clean
56   - rm -Rf ./static/ui
  127 + docker exec -it mariadb-dev mysql -p123456||true
... ...
... ... @@ -5,5 +5,17 @@ namespace = "artshop.shome.steffers.org"
5 5 # url = "./var/lib/artshop/database"
6 6
7 7 [locations]
8   -upload = "/tmp/artshop/uploads"
  8 +data = "./data"
9 9 images = "./var/lib/artshop/images"
  10 +upload = "/tmp/artshop/uploads"
  11 +
  12 +[sizes]
  13 +large = { width = 1280, height = 1280 }
  14 +medium = { width = 800, height = 800 }
  15 +small = { width = 400, height = 400 }
  16 +thumbnail = {width = 100, height = 100 }
  17 +
  18 +[copyright]
  19 +image_path = "./data/copyright.png"
  20 +steganography = "Copyright © 2022, Stefanies Artshop, All rights reserved"
  21 +exiv = "Copyright, Stefanies Artshop, 2022. All rights reserved"
... ...
... ... @@ -8,27 +8,23 @@ pub enum Either<L, R> {
8 8
9 9 #[derive(Clone, Debug, Serialize, Deserialize)]
10 10 pub struct MarkdownJson {
11   - pub name: String,
12   - pub content: String,
13   - pub number_of_versions: i32,
14   - pub date_created: String,
15   - pub date_updated: String,
  11 + pub name :String,
  12 + pub content :String,
  13 + pub number_of_versions :i32,
  14 + pub date_created :String,
  15 + pub date_updated :String,
16 16 }
17 17
18 18 #[derive(Clone, Debug, Serialize, Deserialize)]
19 19 pub struct MarkdownDiffJson {
20   - pub id: i32,
21   - pub date_created: String,
  20 + pub id :i32,
  21 + pub date_created :String,
22 22 }
23 23
24 24 #[derive(Clone, Debug, Serialize, Deserialize)]
25 25 pub struct ImageJson {
26   - pub upload_uuid :Option<Vec<u8>>,
27   - pub uuid :Option<Vec<u8>>,
28   - pub size :i32,
29   - pub dim_x :Option<i32>,
30   - pub dim_y :Option<i32>,
31   - pub mime_type :String,
  26 + pub id :i32,
  27 + pub uuid :Vec<u8>,
32 28 pub date_created :String,
33 29 pub date_updated :String
34 30 }
... ...
  1 +Copyright © 2022, Stefanies Artshop
... ...
... ... @@ -64,3 +64,18 @@ and parallel:
64 64
65 65 GRANT ALL PRIVILEGES ON artshop.* TO 'artshop'@'%';
66 66 CREATE DATABASE artshop CHARACTER SET = 'utf8mb3' COLLATE = 'utf8mb3_general_ci';
  67 +
  68 +# Get information for github user while not having own account
  69 +
  70 + https://api.github.com/users/xxxxxxx/events/public
  71 +
  72 +where xxxxxxx is the github username. Then search for email.
  73 +
  74 +# Create a Copyright Watermark ...
  75 +
  76 + cat text.txt | convert -pointsize 24 -font Helvetica -background black \
  77 + -fill white text:- -trim +repage text.jpg
  78 +
  79 + cat copyright.txt | convert -pointsize 24 -font Helvetica \
  80 + -background transparent -fill "rgba(255,255,255,0.35)" \
  81 + text:- -trim +repage copyright.png
... ...
... ... @@ -102,7 +102,7 @@ erstellen. Theoretisch könnte für solche Böcke dann Syntax-Highliting eingeba
102 102 ```shell
103 103 #!/bin/env sh
104 104
105   -FOO="foo"
  105 +FOO=\"foo\"
106 106
107 107 function func() {
108 108 local BAR=bar
... ... @@ -193,6 +193,6 @@ wie hier.</pre>
193 193 </ul>
194 194
195 195 [lnk1]: https://heise.de/tp/ 'Telepolis'
196   -[lnk2]: https://arkdown.land/markdown-table 'markdown.land' "
  196 +[lnk2]: https://arkdown.land/markdown-table 'markdown.land'"
197 197 , '2022-01-29 21:33:34.000'
198 198 , '2022-01-29 21:33:34.000' );
... ...
  1 +-- This file should undo anything in `up.sql`
  2 +DROP VIEW images_blob_as_b64;
... ...
  1 +CREATE VIEW images_blob_as_b64 AS
  2 +SELECT id, TO_BASE64(upload_uuid), TO_BASE64(uuid), size, dim_x, dim_y,
  3 + mime_type, date_created, date_updated
  4 +FROM images;
... ...
  1 +--- src/patch/parse.rs 2022-01-31 14:20:14.539741482 +0100
  2 ++++ src/patch/parse.rs 2022-01-31 14:21:27.058846339 +0100
  3 +@@ -193,8 +193,8 @@
  4 +
  5 + fn verify_hunks_in_order<T: ?Sized>(hunks: &[Hunk<'_, T>]) -> bool {
  6 + for hunk in hunks.windows(2) {
  7 +- if hunk[0].old_range.end() >= hunk[1].old_range.start()
  8 +- || hunk[0].new_range.end() >= hunk[1].new_range.start()
  9 ++ if hunk[0].old_range.end() > hunk[1].old_range.start()
  10 ++ || hunk[0].new_range.end() > hunk[1].new_range.start()
  11 + {
  12 + return false;
  13 + }
... ...
... ... @@ -8,15 +8,15 @@ repository = "https://gitlab.weird-web-workers.org/rust/artshop"
8 8 license = "GPL-3.0-or-later"
9 9
10 10 [dependencies]
11   -actix-files = "0.2"
12   -actix-rt = "1.1.1"
13   -actix-web = "2.0"
  11 +actix-files = "^0.5"
  12 +actix-rt = "^1.1"
  13 +actix-web = "^3.3"
14 14 anyhow = "1.0"
15 15 artshop-common = { path = "../common" }
16   -async-std = "^1.10"
  16 +async-std = { version = "^1.10", features = ["unstable"] }
17 17 chrono = "0.4.15"
18   -diesel = { version = "1.4.7", features = ["mysql", "sqlite", "r2d2"]}
19   -diffy = "0.2"
  18 +diesel = { version = "1.4.7", features = ["mysql", "sqlite", "r2d2"] }
  19 +diffy = "0.2.2"
20 20 dotenv = "0.15.0"
21 21 flate2 = "^1.0"
22 22 futures = "^0.3"
... ... @@ -24,9 +24,12 @@ futures-util = { version = "0", features = ["std"] }
24 24 image = "^0.23"
25 25 listenfd = "0.3"
26 26 once_cell = "^1.9"
  27 +mime = "^0.3"
27 28 r2d2 = "0.8.9"
  29 +rexiv2 = "^0.9"
28 30 serde = { version = "^1.0", features = ["derive"] }
29 31 serde_derive = "1.0"
30 32 serde_json = "1.0"
  33 +steganography = { git = "https://github.com/teovoinea/steganography" }
31 34 toml = "^0.5"
32 35 uuid = { version = "^0.8", features = ["v4", "v5"] }
... ...
1 1 use std::fs::File;
2 2 use std::io::Read;
  3 +use image::{ DynamicImage, io::Reader as ImageReader };
3 4 use once_cell::sync::Lazy;
4 5 use serde::Deserialize;
  6 +use anyhow::Result;
  7 +use crate::routes::image::Size as ImageSize;
5 8
6 9 #[derive(Debug, Deserialize)]
7 10 struct Database { url :Option<String> }
8 11
9 12 #[derive(Debug, Deserialize)]
10   -struct Locations { upload :String
11   - , images :String }
  13 +struct Locations { data :String
  14 + , images :String
  15 + , upload :String }
12 16
13 17 #[derive(Debug, Deserialize)]
14   -pub(crate) struct Config { namespace :String
15   - , database :Database
16   - , locations :Locations }
  18 +struct Size { width :u32
  19 + , height :u32 }
17 20
18   -pub(crate) static CONFIG :Lazy<Config> = Lazy::new(|| Config::load());
  21 +#[derive(Debug, Deserialize)]
  22 +struct Sizes { large :Size
  23 + , medium :Size
  24 + , small :Size
  25 + , thumbnail :Size }
  26 +
  27 +#[derive(Debug, Deserialize)]
  28 +struct Copyright { image_path :String
  29 + , steganography :String
  30 + , exiv :String }
  31 +
  32 +#[derive(Debug, Deserialize)]
  33 +pub(crate) struct ConfigFile { namespace :String
  34 + , database :Database
  35 + , locations :Locations
  36 + , sizes :Sizes
  37 + , copyright :Copyright }
  38 +
  39 +pub(crate) struct Config { config_file :ConfigFile
  40 + , copyright_image :DynamicImage }
  41 +
  42 +pub(crate) static CONFIG :Lazy<Config> =
  43 + Lazy::new(|| Config::load().unwrap());
19 44
20 45 impl Config {
21   - pub fn load() -> Config {
22   - let filename = std::env::var("CONFIG").unwrap();
  46 + pub fn load() -> Result<Self> {
  47 + let filename = std::env::var("CONFIG")?;
23 48
24 49 let mut buffer = vec![];
25   - let mut file = File::open(filename).unwrap();
  50 + let mut file = File::open(filename)?;
26 51
27   - file.read_to_end(&mut buffer).unwrap();
28   - let mut config :Config = toml::from_slice(&buffer).unwrap();
  52 + file.read_to_end(&mut buffer)?;
  53 + let mut config_file :ConfigFile = toml::from_slice(&buffer)?;
29 54
30   - config.database.url = match config.database.url {
  55 + config_file.database.url = match config_file.database.url {
31 56 Some(url) => Some(url),
32 57 None => std::env::var("DATABASE_URL").ok()
33 58 };
34 59
35   - config
  60 + let copyright_image = ImageReader::open(&config_file.copyright.image_path)?
  61 + . with_guessed_format()?
  62 + . decode()?;
  63 +
  64 + Ok(Self { config_file, copyright_image })
36 65 }
37 66
38 67 pub fn namespace(&self) -> &str {
39   - self.namespace.as_str()
  68 + self.config_file.namespace.as_str()
40 69 }
41 70
42 71 pub fn upload_dir(&self) -> &str {
43   - self.locations.upload.as_str()
  72 + self.config_file.locations.upload.as_str()
44 73 }
45 74
46 75 pub fn images_dir(&self) -> &str {
47   - self.locations.images.as_str()
  76 + self.config_file.locations.images.as_str()
  77 + }
  78 +
  79 + pub fn width(&self, size :ImageSize) -> Option<u32> {
  80 + match size {
  81 + ImageSize::Original => None,
  82 + ImageSize::Large => Some(self.config_file.sizes.large.width),
  83 + ImageSize::Medium => Some(self.config_file.sizes.medium.width),
  84 + ImageSize::Small => Some(self.config_file.sizes.small.width),
  85 + ImageSize::Thumbnail => Some(self.config_file.sizes.thumbnail.width),
  86 + }
  87 + }
  88 +
  89 + pub fn height(&self, size :ImageSize) -> Option<u32> {
  90 + match size {
  91 + ImageSize::Original => None,
  92 + ImageSize::Large => Some(self.config_file.sizes.large.height),
  93 + ImageSize::Medium => Some(self.config_file.sizes.medium.height),
  94 + ImageSize::Small => Some(self.config_file.sizes.small.height),
  95 + ImageSize::Thumbnail => Some(self.config_file.sizes.thumbnail.height),
  96 + }
  97 + }
  98 +
  99 + pub fn copyright_image(&self) -> &DynamicImage {
  100 + &self.copyright_image
  101 + }
  102 +
  103 + pub fn copyright_steganography(&self) -> &str {
  104 + self.config_file.copyright.steganography.as_str()
  105 + }
  106 +
  107 + pub fn copyright_exiv(&self) -> &str {
  108 + self.config_file.copyright.exiv.as_str()
48 109 }
49 110 }
... ...
1   -use std::{fmt::Display, pin::Pin};
  1 +use std::{fmt::Display, pin::Pin, sync::Arc};
2 2
  3 +use actix_rt::blocking::BlockingError;
  4 +use actix_web::{http::{StatusCode, header::ToStrError}, ResponseError};
  5 +use async_std::channel::SendError;
3 6 use diesel::result;
4   -use diffy::ParsePatchError;
  7 +use diffy::{ParsePatchError, ApplyError};
  8 +use image::ImageError;
  9 +use mime::FromStrError;
5 10 use r2d2;
  11 +use rexiv2::Rexiv2Error;
  12 +
  13 +use crate::{Pool, models::image::Image};
6 14
7 15 type ParentError = Option<Pin<Box<dyn std::error::Error>>>;
8 16
9 17 #[derive(Debug)]
10 18 pub struct Error {
11   - source: ParentError,
12   - message: String,
  19 + source :ParentError,
  20 + message :String,
  21 + status :Option<StatusCode>
13 22 }
14 23
15 24 unsafe impl Send for Error {}
... ... @@ -22,19 +31,36 @@ impl std::error::Error for Error {
22 31 }
23 32 }
24 33
  34 +impl ResponseError for Error {
  35 + fn status_code(&self) -> StatusCode {
  36 + self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
  37 + }
  38 +}
  39 +
25 40 impl Display for Error {
26 41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 42 match self {
28   - Error { source: Some(source), message } =>
29   - write!(f, "{}: {}", message, source),
30   - Error { source: None, message } => write!(f, "{}", message),
  43 + Error { source: Some(source), message, status } =>
  44 + write!( f
  45 + , "[{}] {}: {}"
  46 + , status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
  47 + , message
  48 + , source ),
  49 + Error { source: None, message, status } =>
  50 + write!( f
  51 + , "[{}] {}"
  52 + , status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
  53 + , message ),
31 54 }
32 55 }
33 56 }
34 57
35   -impl From<&str> for Error {
36   - fn from(message: &str) -> Self {
37   - Self { source: None, message: String::from(message) }
  58 +impl Error {
  59 + pub(crate) fn new(message :&str, status :StatusCode) -> Self {
  60 + Self { source: None
  61 + , message: String::from(message)
  62 + , status: Some(status)
  63 + }
38 64 }
39 65 }
40 66
... ... @@ -42,6 +68,7 @@ impl From<result::Error> for Error {
42 68 fn from(source: result::Error) -> Self {
43 69 Self { source: Some(Box::pin(source))
44 70 , message: String::from("Diesel Result Error")
  71 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
45 72 }
46 73 }
47 74 }
... ... @@ -49,7 +76,8 @@ impl From<result::Error> for Error {
49 76 impl From<r2d2::Error> for Error {
50 77 fn from(source: r2d2::Error) -> Self {
51 78 Self { source: Some(Box::pin(source))
52   - , message: String::from("Diesel Result Error")
  79 + , message: String::from("R2D2 Pool Error")
  80 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
53 81 }
54 82 }
55 83 }
... ... @@ -58,6 +86,7 @@ impl From<std::io::Error> for Error {
58 86 fn from(source: std::io::Error) -> Self {
59 87 Self { source: Some(Box::pin(source))
60 88 , message: String::from("IO Error")
  89 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
61 90 }
62 91 }
63 92 }
... ... @@ -65,7 +94,8 @@ impl From<std::io::Error> for Error {
65 94 impl From<std::str::Utf8Error> for Error {
66 95 fn from(source: std::str::Utf8Error) -> Self {
67 96 Self { source: Some(Box::pin(source))
68   - , message: String::from("IO Error")
  97 + , message: String::from("UTF8 Error")
  98 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
69 99 }
70 100 }
71 101 }
... ... @@ -73,7 +103,17 @@ impl From<std::str::Utf8Error> for Error {
73 103 impl From<ParsePatchError> for Error {
74 104 fn from(source: ParsePatchError) -> Self {
75 105 Self { source: Some(Box::pin(source))
76   - , message: String::from("IO Error")
  106 + , message: String::from("Diffy Error")
  107 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  108 + }
  109 + }
  110 +}
  111 +
  112 +impl From<ApplyError> for Error {
  113 + fn from(source: ApplyError) -> Self {
  114 + Self { source: Some(Box::pin(source))
  115 + , message: String::from("Diffy Error")
  116 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
77 117 }
78 118 }
79 119 }
... ... @@ -82,6 +122,61 @@ impl From<uuid::Error> for Error {
82 122 fn from(source: uuid::Error) -> Self {
83 123 Self { source: Some(Box::pin(source))
84 124 , message: String::from("UUID error")
  125 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  126 + }
  127 + }
  128 +}
  129 +
  130 +impl From<FromStrError> for Error {
  131 + fn from(source: FromStrError) -> Self {
  132 + Self { source: Some(Box::pin(source))
  133 + , message: String::from("Mime error")
  134 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  135 + }
  136 + }
  137 +}
  138 +
  139 +impl From<ToStrError> for Error {
  140 + fn from(source: ToStrError) -> Self {
  141 + Self { source: Some(Box::pin(source))
  142 + , message: String::from("Header error")
  143 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  144 + }
  145 + }
  146 +}
  147 +
  148 +impl From<SendError<(Arc<Pool>, Image)>> for Error {
  149 + fn from(source: SendError<(Arc<Pool>, Image)>) -> Self {
  150 + Self { source: Some(Box::pin(source))
  151 + , message: String::from("Image Worker send error")
  152 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  153 + }
  154 + }
  155 +}
  156 +
  157 +impl From<ImageError> for Error {
  158 + fn from(source: ImageError) -> Self {
  159 + Self { source: Some(Box::pin(source))
  160 + , message: String::from("Image processing error")
  161 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  162 + }
  163 + }
  164 +}
  165 +
  166 +impl From<Rexiv2Error> for Error {
  167 + fn from(source: Rexiv2Error) -> Self {
  168 + Self { source: Some(Box::pin(source))
  169 + , message: String::from("Exiv error")
  170 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
  171 + }
  172 + }
  173 +}
  174 +
  175 +impl From<BlockingError<Error>> for Error {
  176 + fn from(source: BlockingError<Error>) -> Self {
  177 + Self { source: Some(Box::pin(source))
  178 + , message: String::from("web::block error")
  179 + , status: Some(StatusCode::INTERNAL_SERVER_ERROR)
85 180 }
86 181 }
87 182 }
... ...
... ... @@ -12,8 +12,12 @@ mod upload_worker;
12 12 use models::image::Image;
13 13 use routes::markdown::*;
14 14 use routes::other::*;
  15 +use routes::ui::frontend_js;
  16 +use routes::ui::frontend_wasm;
15 17 use routes::user::*;
16 18 use routes::upload::*;
  19 +use routes::image::*;
  20 +
17 21
18 22 use actix_web::{guard, web, App, HttpResponse, HttpServer};
19 23 use async_std::channel::Sender;
... ... @@ -21,7 +25,6 @@ use diesel::r2d2::{self, ConnectionManager};
21 25 use diesel::MysqlConnection;
22 26 use listenfd::ListenFd;
23 27 use std::sync::Arc;
24   -use std::ops::Deref;
25 28
26 29 pub(crate) type Pool = r2d2::Pool<ConnectionManager<MysqlConnection>>;
27 30
... ... @@ -37,8 +40,6 @@ async fn main() -> std::io::Result<()> {
37 40
38 41 dotenv::dotenv().ok();
39 42
40   - println!("CONFIG: {:?}", config::CONFIG.deref());
41   -
42 43 let tx_upload_worker = upload_worker::launch();
43 44
44 45 let database_url = std::env::var("DATABASE_URL").expect("NOT FOUND");
... ... @@ -56,6 +57,12 @@ async fn main() -> std::io::Result<()> {
56 57 . service( web::resource("/upload")
57 58 . route(web::post().to(upload))
58 59 )
  60 + . service( web::resource("/images")
  61 + . route(web::get().to(get_images))
  62 + )
  63 + . service( web::resource("/images/{id}")
  64 + . route(web::get().to(get_image))
  65 + )
59 66 . service( web::resource("/markdowns")
60 67 . route(web::get().to(get_markdowns))
61 68 )
... ... @@ -76,6 +83,12 @@ async fn main() -> std::io::Result<()> {
76 83 . route(web::put().to(update_user))
77 84 )
78 85 )
  86 + . service( web::scope("/ui")
  87 + . route( "/artshop_frontend.js"
  88 + , web::get().to(frontend_js) )
  89 + . route( "/artshop_frontend_bg.wasm"
  90 + , web::get().to(frontend_wasm))
  91 + )
79 92 . service( web::scope("")
80 93 . route("/", web::get().to(root))
81 94 . route("/api.html", web::get().to(apidoc))
... ...
  1 +use std::convert::TryFrom;
  2 +use std::io::SeekFrom;
1 3 use std::sync::Arc;
2 4
3 5 use crate::error::*;
4   -use crate::{schema::*, Pool};
  6 +use crate::routes::image::Size;
  7 +use crate::uuid::Uuid;
  8 +use crate::{schema::*, Pool, config::CONFIG};
  9 +use actix_web::http::StatusCode;
  10 +use async_std::fs::File;
  11 +use async_std::io::prelude::SeekExt;
  12 +use async_std::path::PathBuf;
5 13 use diesel::{Connection, insert_into, delete, update};
6 14 use diesel::prelude::*;
7 15 use serde::{Deserialize, Serialize};
8 16
  17 +use async_std::io::ReadExt;
  18 +
9 19 #[derive(Clone, Debug, Serialize, Deserialize, Queryable, Identifiable)]
10 20 pub struct Image {
11 21 pub id :i32,
... ... @@ -50,6 +60,13 @@ pub struct ImagePatch {
50 60 pub date_updated :String
51 61 }
52 62
  63 +pub struct ImageContext {
  64 + pub image :Image,
  65 + base_path :Option<PathBuf>,
  66 + uuid_string :Option<String>,
  67 + upload_path :Option<PathBuf>
  68 +}
  69 +
53 70 impl From<Image> for ImagePatch {
54 71 fn from(image: Image) -> Self {
55 72 let now = chrono::Local::now().naive_local();
... ... @@ -65,26 +82,119 @@ impl From<Image> for ImagePatch {
65 82 }
66 83 }
67 84
68   -#[macro_export]
69   -macro_rules! upload_uuid {
70   - ($u:expr) => {
71   - match &$u.upload_uuid {
72   - Some(uuid) => $crate::uuid::Uuid::try_from(uuid.as_slice()).ok(),
73   - None => None,
74   - }
75   - };
  85 +async fn read_at( f :&mut File
  86 + , pos :SeekFrom
  87 + , buf :&mut [u8]) -> std::io::Result<()> {
  88 + f.seek(pos).await?;
  89 + f.read_exact(buf).await
76 90 }
77 91
78   -#[macro_export]
79   -macro_rules! upload_filename {
80   - ($u:expr) => {
81   - $crate::upload_uuid!($u)
82   - . and_then(|uuid| Some(format!( "{}/upload_{}"
83   - , $crate::config::CONFIG.upload_dir()
84   - , uuid )))
85   - };
  92 +async fn get_sample( f :&mut File
  93 + , buf :&mut [u8]) -> std::io::Result<()> {
  94 + let file_len = f.metadata().await?.len();
  95 + let chunk_size = buf.len() / 3;
  96 +
  97 + read_at(f, SeekFrom::Start(0), &mut buf[0..chunk_size]).await?;
  98 + if file_len >= 2 * chunk_size as u64 {
  99 + read_at( f
  100 + , SeekFrom::End(-(chunk_size as i64))
  101 + , &mut buf[2*chunk_size..]).await?;
  102 + }
  103 + if file_len >= 3 * chunk_size as u64 {
  104 + read_at( f
  105 + , SeekFrom::Start((file_len-chunk_size as u64) / 2)
  106 + , &mut buf[chunk_size..2*chunk_size]).await?;
  107 + }
  108 +
  109 + Ok(())
  110 +}
  111 +
  112 +impl Image {
  113 + pub(crate) fn context(self) -> ImageContext {
  114 + ImageContext { image :self
  115 + , base_path :None
  116 + , uuid_string :None
  117 + , upload_path :None }
  118 + }
  119 +}
  120 +
  121 +impl ImageContext {
  122 + pub(crate) async fn upload_path(&mut self) -> Option<&PathBuf> {
  123 + if self.upload_path.is_none() {
  124 + let uuid = Uuid::try_from(self.image.upload_uuid.clone()?).ok()?;
  125 + let uuid_string = format!("{}", uuid);
  126 +
  127 + let mut upload_path = PathBuf::from(CONFIG.upload_dir());
  128 + upload_path.push(&uuid_string);
  129 +
  130 + self.upload_path = Some(upload_path);
  131 + }
  132 +
  133 + self.upload_path.as_ref()
  134 + }
  135 +
  136 + async fn uuid_string(&mut self) -> Result<&String> {
  137 + if self.uuid_string.is_none() {
  138 + let uuid :Uuid;
  139 +
  140 + if let Some(u) = &self.image.uuid {
  141 + uuid = Uuid::try_from(u.as_ref())?;
  142 + } else {
  143 + let path = self
  144 + . upload_path().await
  145 + . ok_or(Error::new( "No upload and no uuid"
  146 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  147 +
  148 + let mut f = File::open(&path).await?;
  149 + let mut buf = vec!['.' as u8; 3 * 3 * 4096];
  150 +
  151 + get_sample(&mut f, buf.as_mut()).await?;
  152 + uuid = Uuid::get(CONFIG.namespace(), buf.as_mut());
  153 +
  154 + self.image.uuid = Some(uuid.0.as_bytes().to_vec());
  155 + self.image.upload_uuid = None;
  156 + }
  157 +
  158 + self.uuid_string = Some(format!("{}", uuid));
  159 + }
  160 +
  161 + Ok(self.uuid_string.as_ref().unwrap())
  162 + }
  163 +
  164 + pub(crate) async fn base_path(&mut self) -> Option<&PathBuf> {
  165 + if self.upload_path.is_none() {
  166 + let uuid_string = self.uuid_string().await.ok()?;
  167 +
  168 + let mut base_path = PathBuf::from(CONFIG.images_dir());
  169 + base_path.push(&uuid_string.as_str()[..1]);
  170 + base_path.push(&uuid_string.as_str()[..2]);
  171 + base_path.push(&uuid_string.as_str()[..3]);
  172 +
  173 + self.base_path = Some(base_path);
  174 + }
  175 +
  176 + self.base_path.as_ref()
  177 + }
  178 +
  179 + pub(crate) async fn path(&mut self, size :Size) -> Option<PathBuf> {
  180 + let mut path = self.base_path().await?.to_owned();
  181 + path.push(&format!("{}_{}", &self.uuid_string().await.ok()?, size));
  182 +
  183 + Some(path)
  184 + }
86 185 }
87 186
  187 +impl Upload {
  188 + pub(crate) async fn upload_path(&mut self) -> Option<PathBuf> {
  189 + let uuid = Uuid::try_from(self.upload_uuid.clone()?).ok()?;
  190 + let uuid_string = format!("{}", uuid);
  191 +
  192 + let mut upload_path = PathBuf::from(CONFIG.upload_dir());
  193 + upload_path.push(&uuid_string);
  194 +
  195 + Some(upload_path)
  196 + }
  197 +}
88 198
89 199 pub(crate) fn upload( pool: Arc<Pool>
90 200 , item: Upload ) -> Result<Image> {
... ... @@ -108,8 +218,8 @@ pub(crate) fn upload( pool: Arc<Pool>
108 218 })?)
109 219 }
110 220
111   -pub(crate) fn finalize( pool: Arc<Pool>
112   - , item: Image ) -> Result<Image> {
  221 +pub(crate) fn finalize( pool :Arc<Pool>
  222 + , item :Image ) -> Result<Image> {
113 223 use crate::schema::images::dsl::*;
114 224
115 225 let db_connection = pool.get()?;
... ... @@ -129,3 +239,22 @@ pub(crate) fn finalize( pool: Arc<Pool>
129 239 },
130 240 }
131 241 }
  242 +
  243 +pub(crate) fn get_images(pool: Arc<Pool>) -> Result<Vec<Image>>
  244 +{
  245 + use crate::schema::images::dsl::*;
  246 + let db_connection = pool.get()?;
  247 + Ok(images.load::<Image>(&db_connection)?)
  248 +}
  249 +
  250 +pub(crate) fn get_image( pool :Arc<Pool>
  251 + , ident :i32 ) -> Result<Image>
  252 +{
  253 + use crate::schema::images::dsl::*;
  254 +
  255 + let db_connection = pool.get()?;
  256 +
  257 + Ok( images
  258 + . filter(id.eq(ident))
  259 + . first::<Image>(&db_connection)? )
  260 +}
... ...
... ... @@ -162,7 +162,7 @@ pub(crate) fn get_markdown( pool: Arc<Pool>
162 162 let patch_data = patch.get_diff_as_string()?;
163 163 let decomp = Patch::from_str(&patch_data)?;
164 164
165   - markdown.content = apply(&markdown.content, &decomp).unwrap();
  165 + markdown.content = apply(&markdown.content, &decomp)?;
166 166 markdown.date_updated = patch.date_created;
167 167 }
168 168 };
... ... @@ -211,8 +211,8 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
211 211 let patch = format!( "{}", create_patch( item.content.as_str()
212 212 , markdown.content.as_str() ));
213 213 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
214   - encoder.write_all(patch.as_bytes()).unwrap();
215   - let compressed = encoder.finish().unwrap();
  214 + encoder.write_all(patch.as_bytes())?;
  215 + let compressed = encoder.finish()?;
216 216
217 217 let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id))
218 218 . order(diff_id.desc())
... ... @@ -240,7 +240,7 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
240 240 update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?;
241 241
242 242 Ok(())
243   - }).unwrap();
  243 + })?;
244 244
245 245 markdown.name = item.name;
246 246 markdown.content = item.content;
... ...
  1 +use std::fmt::Display;
  2 +
  3 +use crate::{models::image, AppData, error::Error};
  4 +
  5 +use actix_web::{Error as ActixError, web, HttpResponse, http::StatusCode};
  6 +use anyhow::Result;
  7 +use serde::{Deserialize, Serialize};
  8 +
  9 +#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
  10 +#[serde (rename_all = "lowercase")]
  11 +pub enum Size {
  12 + Original,
  13 + Large,
  14 + Medium,
  15 + Small,
  16 + Thumbnail
  17 +}
  18 +
  19 +#[derive(Debug, Deserialize, Serialize)]
  20 +pub struct SizeQuery {
  21 + size :Option<Size>,
  22 +}
  23 +
  24 +impl Display for Size {
  25 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  26 + let size_str = match self {
  27 + Size::Original => "original",
  28 + Size::Large => "large",
  29 + Size::Medium => "medium",
  30 + Size::Small => "small",
  31 + Size::Thumbnail => "thumbnail"
  32 + };
  33 +
  34 + write!(f, "{}", size_str)
  35 + }
  36 +}
  37 +
  38 +pub async fn get_images(app_data: web::Data<AppData>)
  39 + -> Result<HttpResponse, ActixError>
  40 +{
  41 + let pool = app_data.database_pool.clone();
  42 +
  43 + Ok(web::block(move || image::get_images(pool))
  44 + . await
  45 + . map(|images| HttpResponse::Ok().json(images))
  46 + . map_err(|_| HttpResponse::InternalServerError())?)
  47 +}
  48 +
  49 +pub async fn get_image( app_data: web::Data<AppData>
  50 + , ident: web::Path<i32>
  51 + , size: web::Query<SizeQuery>
  52 + ) -> Result<actix_files::NamedFile, ActixError>
  53 +{
  54 + let pool = app_data.database_pool.clone();
  55 + let ident = ident.into_inner();
  56 + let size = size.into_inner().size.unwrap_or(Size::Large);
  57 +
  58 + let image = web::block(move || image::get_image(pool, ident)).await?;
  59 + let path = image.clone().context()
  60 + . path(size).await
  61 + . ok_or(Error::new( "Image not ready"
  62 + , StatusCode::SERVICE_UNAVAILABLE ))?;
  63 +
  64 + Ok( actix_files::NamedFile::open(path)?
  65 + . set_content_type( image.mime_type
  66 + . parse()
  67 + . map_err(|e| Error::from(e))? )
  68 + . disable_content_disposition() )
  69 +}
... ...
  1 +pub(crate) mod image;
1 2 pub(crate) mod markdown;
2 3 pub(crate) mod other;
  4 +pub(crate) mod ui;
3 5 pub(crate) mod upload;
4 6 pub(crate) mod user;
... ...
  1 +use actix_web::Error;
  2 +use actix_files::NamedFile;
  3 +use anyhow::Result;
  4 +
  5 +pub async fn frontend_js() -> Result<NamedFile, Error> {
  6 + Ok( NamedFile::open("static/ui/artshop_frontend.js")?
  7 + . set_content_type(mime::APPLICATION_JAVASCRIPT) )
  8 +}
  9 +
  10 +pub async fn frontend_wasm() -> Result<NamedFile, Error> {
  11 + Ok( NamedFile::open("static/ui/artshop_frontend_bg.wasm")?
  12 + . set_content_type("application/wasm".parse::<mime::Mime>().unwrap()) )
  13 +}
... ...
1   -use actix_web::{Error, HttpResponse, web};
  1 +use actix_web::{Error as ActixError, HttpResponse, web, http::StatusCode};
2 2 use anyhow::Result;
3 3 use async_std::fs::DirBuilder;
4 4 use futures::{stream::StreamExt, AsyncWriteExt};
5 5 use async_std::fs::OpenOptions;
6 6
7   -use crate::{AppData, models::image::{Upload, self}, upload_filename};
  7 +use crate::{AppData, models::image::{Upload, self}, error::Error};
8 8 use crate::config::CONFIG;
9   -use std::convert::TryFrom;
10 9
11 10 pub async fn upload( app_data :web::Data<AppData>
12 11 , mut body :web::Payload
13   - , request :web::HttpRequest ) -> Result<HttpResponse, Error>
  12 + , request :web::HttpRequest
  13 + ) -> Result<HttpResponse, ActixError>
14 14 {
15 15 let pool = app_data.database_pool.clone();
16 16 let worker = app_data.tx_upload_worker.clone();
17 17
18 18 let upload_uuid = Some(uuid::Uuid::new_v4().as_bytes().to_vec());
19 19 let size = request.headers().get("content-length")
20   - . and_then(|h| Some(h.to_str().unwrap().parse::<i32>()))
21   - . unwrap().unwrap();
22   - let mime_type = String::from( request.headers().get("content-type")
23   - . and_then(|h| Some(h.to_str().unwrap()))
24   - . unwrap() );
  20 + . and_then(|h| h.to_str().ok())
  21 + . and_then(|s| s.parse::<i32>().ok());
  22 + let mime_type = request.headers().get("content-type")
  23 + . and_then(|h| h.to_str().ok())
  24 + . ok_or(Error::new( "Upload expects content-type"
  25 + , StatusCode::BAD_REQUEST ))?;
  26 + let mime_type = String::from(mime_type);
25 27
26   - let upload = Upload {
  28 + let mut upload = Upload {
27 29 upload_uuid,
28   - size,
  30 + size: size.unwrap_or(0),
29 31 mime_type
30 32 };
31 33
... ... @@ -33,24 +35,33 @@ pub async fn upload( app_data :web::Data<AppData>
33 35 . create(CONFIG.upload_dir())
34 36 . await?;
35 37
36   - let upload_filename = upload_filename!(upload).unwrap();
  38 + let mut upload_size = 0;
  39 + let upload_filename = upload.upload_path().await.unwrap();
37 40 let mut output = OpenOptions::new();
38 41 let mut output = output
39 42 . create(true)
40 43 . write(true)
41 44 . open(&upload_filename).await?;
42 45 while let Some(item) = body.next().await {
43   - output.write_all(&item?).await?;
  46 + let item = item?;
  47 + output.write_all(&item).await?;
  48 + upload_size += item.len() as i32;
  49 + }
  50 + output.flush().await?;
  51 +
  52 + if let Some(size) = size {
  53 + if size != upload_size {
  54 + Err(Error::new( "Did not receive expected size"
  55 + , StatusCode::BAD_REQUEST ))?
  56 + }
  57 + } else {
  58 + upload.size = upload_size;
44 59 }
45   - output.flush().await.unwrap();
46 60
47 61 let pool_for_worker = pool.clone();
48   - Ok( match web::block(move || image::upload(pool, upload)).await {
49   - Ok(image) => {
50   - // TODO handle this as error response...
51   - worker.send((pool_for_worker, image.clone())).await.unwrap();
52   - HttpResponse::Ok().json(image)
53   - },
54   - Err(_) => HttpResponse::InternalServerError().finish()
55   - } )
  62 + let image = web::block(move || image::upload(pool, upload)).await?;
  63 + worker . send((pool_for_worker, image.clone())).await
  64 + . map_err(|e| Error::from(e))?;
  65 +
  66 + Ok(HttpResponse::Accepted().json(image))
56 67 }
... ...
1   -use std::{io::{SeekFrom, ErrorKind}, sync::Arc};
2   -use actix_web::web;
3   -use async_std::{ fs::{File, DirBuilder, copy, metadata, remove_file}
  1 +use std::{ io::ErrorKind
  2 + , sync::Arc };
  3 +use actix_web::{web, http::StatusCode};
  4 +use async_std::{ fs::{DirBuilder, copy, metadata, remove_file}
4 5 , channel::{Sender, Receiver, bounded}
5   - , path::PathBuf
6   - , io::Result };
7   -use futures::{ AsyncSeekExt, AsyncReadExt, FutureExt, StreamExt, select
  6 + , task::spawn_blocking };
  7 +use futures::{ FutureExt, StreamExt, select
8 8 , stream::FuturesUnordered};
  9 +use rexiv2::Metadata;
  10 +use steganography::encoder::Encoder;
9 11
10   -use crate::{models::image::{Image, finalize}, upload_filename, config::CONFIG, Pool};
11   -use crate::uuid::Uuid;
  12 +use crate::{ models::image::{Image, finalize, ImageContext}
  13 + , config::CONFIG
  14 + , Pool
  15 + , routes::image::Size
  16 + , error::Error };
12 17
13   -use std::convert::TryFrom;
14   -use image::{io::Reader as ImageReader, GenericImageView};
  18 +use image::{ io::Reader as ImageReader
  19 + , GenericImageView
  20 + , imageops::{FilterType::Lanczos3, overlay}
  21 + , ImageFormat::Jpeg, DynamicImage };
15 22
16 23 pub fn launch() -> Sender<(Arc<Pool>, Image)> {
17 24 let (tx_upload_worker, rx_upload_worker)
18   - : (Sender<(Arc<Pool>, Image)>, Receiver<(Arc<Pool>, Image)>) = bounded(32);
  25 + : (Sender<(Arc<Pool>, Image)>, Receiver<(Arc<Pool>, Image)>) = bounded(100);
19 26
20 27 actix_rt::spawn(async move {
21 28 let mut workers = FuturesUnordered::new();
22 29
23 30 loop {
24   - select! {
25   - image = rx_upload_worker.recv().fuse() => {
26   - match image {
27   - Err(_) => break,
28   - Ok((pool, image)) => workers.push(worker(pool, image)),
29   - }
30   - },
31   - _result = workers.next() => {},
  31 + if workers.len() <= 3 {
  32 + select! {
  33 + image = rx_upload_worker.recv().fuse() => {
  34 + match image {
  35 + Err(_) => break,
  36 + Ok((pool, image)) => workers.push(worker(pool, image)),
  37 + }
  38 + },
  39 + _result = workers.next() => {},
  40 + }
  41 + } else {
  42 + workers.next().await;
32 43 }
33 44 }
34 45
... ... @@ -40,76 +51,121 @@ pub fn launch() -> Sender<(Arc<Pool>, Image)> {
40 51 tx_upload_worker
41 52 }
42 53
  54 +async fn store_original(context :&mut ImageContext) -> Result<u64, Error> {
  55 + let upload_path = context
  56 + . upload_path().await
  57 + . ok_or(Error::new( "Can't retreive upload path"
  58 + , StatusCode::INTERNAL_SERVER_ERROR ))?
  59 + . to_owned();
  60 + let original_path = context
  61 + . path(Size::Original).await
  62 + . ok_or(Error::new( "No path for given size"
  63 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  64 +
  65 + let result = match metadata(&original_path).await {
  66 + Err(e) if e.kind() == ErrorKind::NotFound => {
  67 + match copy(&upload_path, &original_path).await {
  68 + Ok(size) => Ok(size),
  69 + Err(e) => Err(Error::from(e)),
  70 + }
  71 + },
  72 + Err(e) => Err(Error::from(e)),
  73 + Ok(_) => Err(Error::new( "File already exists"
  74 + , StatusCode::CONFLICT )),
  75 + };
43 76
44   -async fn worker(pool :Arc<Pool>, mut image :Image) {
45   - let upload_filename = upload_filename!(image).unwrap();
46   - let mut f = File::open(&upload_filename).await.unwrap();
47   -
48   - let mut buf = vec!['.' as u8; 3 * 3 * 4096];
49   - get_sample(&mut f, buf.as_mut()).await.unwrap();
50   - let uuid = Uuid::get(CONFIG.namespace(), buf.as_mut());
51   - let uuid_string = format!("{}", uuid);
  77 + remove_file(&upload_path).await?;
52 78
53   - let mut image_path = PathBuf::from(CONFIG.images_dir());
54   - image_path.push(&uuid_string.as_str()[..2]);
55   - image_path.push(&uuid_string.as_str()[..5]);
  79 + result
  80 +}
56 81
57   - DirBuilder::new() . recursive(true)
58   - . create(&image_path)
59   - . await
60   - . unwrap();
  82 +async fn load_original(context :&mut ImageContext) -> Result<DynamicImage, Error> {
  83 + let original_path = context
  84 + . path(Size::Original).await
  85 + . ok_or(Error::new( "Unable to load original image"
  86 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
61 87
62   - image_path.push(&uuid_string);
  88 + spawn_blocking(move || -> Result<DynamicImage, Error> {
  89 + Ok(ImageReader::open(&original_path)? . with_guessed_format()?
  90 + . decode()?)
  91 + }).await
  92 +}
63 93
64   - image.upload_uuid = None;
65   - image.uuid = Some(uuid.0.as_bytes().to_vec());
  94 +async fn save_resized( original :&DynamicImage
  95 + , context :&mut ImageContext
  96 + , size :Size ) -> Result<(), Error> {
  97 + let width = CONFIG.width(size)
  98 + . ok_or(Error::new( "Can't get width for size"
  99 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  100 + let height = CONFIG.height(size)
  101 + . ok_or(Error::new( "Can't get height for size"
  102 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  103 +
  104 + let path = context.path(size).await
  105 + . ok_or(Error::new( "Can't get path for size"
  106 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  107 +
  108 + let original = original.to_owned();
  109 +
  110 + match metadata(&path).await {
  111 + Err(e) if e.kind() == ErrorKind::NotFound =>
  112 + spawn_blocking(move || -> Result<(), Error> {
  113 + let mut scaled = original.resize(width, height, Lanczos3);
  114 +
  115 + if let Size::Thumbnail = size {
  116 + } else {
  117 + overlay(&mut scaled, CONFIG.copyright_image(), 0_u32, 0_u32);
  118 + }
  119 +
  120 + let stegonography = CONFIG.copyright_steganography().as_bytes();
  121 + let encoder = Encoder::new(stegonography, scaled);
  122 + let scaled = encoder.encode_alpha();
  123 + scaled.save_with_format(&path, Jpeg)?;
  124 +
  125 + let exiv = Metadata::new_from_path(&path)?;
  126 + exiv.set_tag_string("Exif.Image.Copyright", CONFIG.copyright_exiv())?;
  127 + exiv.save_to_file(&path)?;
  128 +
  129 + Ok(())
  130 + }).await,
  131 + Err(e) => Err(e)?,
  132 + Ok(_) => Err(Error::new( "File already exists"
  133 + , StatusCode::CONFLICT )),
  134 + }
  135 +}
66 136
67   - match metadata(&image_path).await {
68   - Err(e) if e.kind() == ErrorKind::NotFound => {
69   - copy(&upload_filename, &image_path).await.unwrap();
  137 +async fn worker(pool :Arc<Pool>, image :Image) -> Result<(), Error> {
  138 + let mut context = image.context();
  139 + let base_path = context.base_path().await
  140 + . ok_or(Error::new( "Missing base_path"
  141 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
70 142
71   - let img = ImageReader::open(&image_path).unwrap()
72   - . with_guessed_format().unwrap()
73   - . decode().unwrap();
74   - let (dim_x, dim_y) = img.dimensions();
  143 + DirBuilder::new() . recursive(true)
  144 + . create(base_path)
  145 + . await?;
75 146
76   - image.dim_x = Some(dim_x as i32);
77   - image.dim_y = Some(dim_y as i32);
78   - },
79   - Err(e) => {
80   - let e :Result<()> = Err(e);
81   - e.unwrap();
82   - },
83   - Ok(_) => {},
84   - }
  147 + store_original(&mut context).await.unwrap_or(0);
85 148
86   - remove_file(&upload_filename).await.unwrap();
87   - web::block(move || finalize(pool, image)).await.unwrap();
88   -}
  149 + if let Ok(original) = load_original(&mut context).await {
  150 + let (dim_x, dim_y) = original.dimensions();
89 151
90   -async fn read_at( f :&mut File
91   - , pos :SeekFrom
92   - , buf :&mut [u8]) -> std::io::Result<()> {
93   - f.seek(pos).await?;
94   - f.read_exact(buf).await
95   -}
  152 + context.image.dim_x = Some(dim_x as i32);
  153 + context.image.dim_y = Some(dim_y as i32);
96 154
97   -async fn get_sample( f :&mut File
98   - , buf :&mut [u8]) -> std::io::Result<()> {
99   - let file_len = f.metadata().await?.len();
100   - let chunk_size = buf.len() / 3;
  155 + macro_rules! save_resized{
  156 + ($s:expr) => { save_resized(&original, &mut context, $s) }
  157 + }
101 158
102   - read_at(f, SeekFrom::Start(0), &mut buf[0..chunk_size]).await?;
103   - if file_len >= 2 * chunk_size as u64 {
104   - read_at( f
105   - , SeekFrom::End(-(chunk_size as i64))
106   - , &mut buf[2*chunk_size..]).await?;
107   - }
108   - if file_len >= 3 * chunk_size as u64 {
109   - read_at( f
110   - , SeekFrom::Start((file_len-chunk_size as u64) / 2)
111   - , &mut buf[chunk_size..2*chunk_size]).await?;
  159 + save_resized!(Size::Large).await.unwrap_or(());
  160 + save_resized!(Size::Medium).await.unwrap_or(());
  161 + save_resized!(Size::Small).await.unwrap_or(());
  162 + save_resized!(Size::Thumbnail).await.unwrap_or(());
112 163 }
113 164
  165 + // TODO Think about two simpler functions than finanlize...
  166 + // One to update one do remove the new entry depending if the
  167 + // entry exists already...
  168 + web::block(move || finalize(pool, context.image)).await?;
  169 +
114 170 Ok(())
115 171 }
... ...
... ... @@ -30,3 +30,11 @@ impl TryFrom<&[u8]> for Uuid {
30 30 Ok(Self(uuid::Uuid::from_slice(value)?))
31 31 }
32 32 }
  33 +
  34 +impl TryFrom<Vec<u8>> for Uuid {
  35 + type Error = Error;
  36 +
  37 + fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
  38 + Ok(Self(uuid::Uuid::from_slice(value.as_slice())?))
  39 + }
  40 +}
... ...
... ... @@ -7,7 +7,7 @@
7 7 </head>
8 8 <body>
9 9 <script type="module">
10   - import init from '/static/ui/artshop_frontend.js';
  10 + import init from '/ui/artshop_frontend.js';
11 11 window.addEventListener('load', async () => {
12 12 await init();
13 13 });
... ...
1   -.upload {
  1 +.application > div {
2 2 float: left;
3   - width: 400px;
  3 + width: 30%;
  4 +}
  5 +
  6 +.selector {
  7 + padding: 1em;
  8 + border-radius: .5em;
  9 + border: 1px solid #ddd;
  10 + background: #f7f7f7;
  11 +}
  12 +
  13 +.selector img {
  14 + max-width: 75px;
  15 + max-height: 75px;
  16 + width: auto;
  17 + height: auto;
  18 + vertical-align: middle;
  19 +}
  20 +
  21 +.selector > ul {
  22 + display: flex;
  23 + flex-wrap: wrap;
  24 + gap: 5px;
  25 + justify-content: space-between;
  26 + align-items: center;
  27 + max-height: 15em;
  28 + width: calc(100% - 15px);
  29 + overflow-y: auto;
  30 + overflow-x: auto;
  31 + background: #ffffff;
  32 + border-radius: .35em;
  33 + border: 2px solid #bbb;
  34 + cursor: default;
  35 + padding-left: 5px;
  36 + padding-right: 5px;
  37 + margin-top: 0;
  38 + margin-bottom: 0;
  39 +}
  40 +
  41 +.selector > ul > li {
  42 + display: unset;
  43 + border: 1px solid black;
  44 + width: fit-content;
  45 + height: fit-content;
  46 + margin-bottom: .15em;
  47 +}
  48 +
  49 +.selector > ul > li:last-child {
  50 + margin-bottom: 0;
  51 +}
  52 +
  53 +.upload {
4 54 padding: 1em;
5 55 border-radius: .5em;
6 56 border: 1px solid #ddd;
... ... @@ -55,13 +105,18 @@
55 105 }
56 106
57 107 .markdown {
58   - float: left;
59 108 padding: 1em;
60 109 border-radius: .5em;
61 110 border: 1px solid #ddd;
62 111 background: #f7f7f7;
63 112 }
64 113
  114 +.markdown img {
  115 + display: block;
  116 + margin-left: auto;
  117 + margin-right: auto;
  118 +}
  119 +
65 120 .markdown p {
66 121 text-align: justify;
67 122 text-indent: .5em;
... ... @@ -70,7 +125,6 @@
70 125
71 126 .markdown > div:first-child {
72 127 position: fixed;
73   - width: inherit;
74 128 z-index: 10;
75 129 }
76 130
... ...
  1 +use std::fmt::Display;
  2 +
  3 +use artshop_common::types::ImageJson;
  4 +
  5 +use super::super::error::*;
  6 +use super::super::client::Client;
  7 +
  8 +#[derive(Debug, Clone)]
  9 +pub struct Image {
  10 + pub json: ImageJson,
  11 +}
  12 +
  13 +pub(crate) async fn images() -> Result<Vec<Image>> {
  14 + let client = Client::new()?;
  15 + let (response, data) = client.get("/api/v0/images").await?;
  16 +
  17 + match response.status() {
  18 + 200 => Ok ( serde_json::from_str(data.as_str())
  19 + . map(|images :Vec<ImageJson>| {
  20 + images.into_iter().map(|json| Image { json }).collect()
  21 + })? ),
  22 + status => Err(status_error(status)),
  23 + }
  24 +}
  25 +
  26 +fn status_error<I: Display>(status :I) -> Error {
  27 + let err_str = format!("Invalid response status: {}", status);
  28 + Error::from(err_str.as_str())
  29 +}
  30 +
  31 +// impl Image {
  32 +// }
... ...
  1 +pub(crate) mod image;
1 2 pub(crate) mod markdown;
2 3 pub(crate) mod upload;
... ...
... ... @@ -21,7 +21,7 @@ impl UploadApi {
21 21 , upload.size()
22 22 , upload.data() ).await?;
23 23 match response.status() {
24   - 200 => Ok(self),
  24 + 202 => Ok(self),
25 25 status => Err(Self::status_error(status)),
26 26 }
27 27 }
... ...
  1 +use mogwai::prelude::*;
  2 +
  3 +use crate::api::image::{Image, images};
  4 +
  5 +use super::{PatchSender, view::image_preview_view};
  6 +
  7 +pub(super) async fn image_preview_logic() {
  8 +}
  9 +
  10 +pub(super) async fn selector_logic( mut rx_dom :broadcast::Receiver<Dom>
  11 + , tx_previews :PatchSender) {
  12 + let mut previews :ListPatchModel<Image> = ListPatchModel::new();
  13 +
  14 + mogwai::spawn(previews.stream().for_each(move |patch| {
  15 + let patch :ListPatch<ViewBuilder<Dom>> = patch.map(|i| {
  16 + let view = image_preview_view(
  17 + String::from(format!( "/api/v0/images/{}?size=thumbnail"
  18 + , i.json.id )));
  19 + let logic = image_preview_logic();
  20 +
  21 + Component::from(view).with_logic(logic).into()
  22 + });
  23 + let tx_previews = tx_previews.clone();
  24 +
  25 + async move {
  26 + tx_previews.send(patch).await.unwrap();
  27 + }
  28 + }));
  29 +
  30 + if let Some(_) = rx_dom.next().await {
  31 + for image in images().await.unwrap().into_iter() {
  32 + previews.list_patch_push(image);
  33 + }
  34 + }
  35 +}
... ...
  1 +pub(crate) mod logic;
  2 +mod view;
  3 +
  4 +use mogwai::prelude::*;
  5 +
  6 +use self::{view::selector_view, logic::selector_logic};
  7 +
  8 +type PatchSender = mpmc::Sender<ListPatch<ViewBuilder<Dom>>>;
  9 +type PatchReceiver = mpmc::Receiver<ListPatch<ViewBuilder<Dom>>>;
  10 +
  11 +pub(crate) async fn new() -> Component<Dom> {
  12 + let (tx_previews, rx_previews) = mpmc::bounded(1);
  13 + let (tx_dom, rx_dom) = broadcast::bounded(1);
  14 +
  15 + let view = selector_view(tx_dom, rx_previews);
  16 + let logic = selector_logic(rx_dom, tx_previews);
  17 +
  18 + Component::from(view).with_logic(logic)
  19 +}
... ...
  1 +use mogwai::prelude::*;
  2 +
  3 +use crate::component::imageselector::PatchReceiver;
  4 +
  5 +pub(super) fn image_preview_view(image_url :String) -> ViewBuilder<Dom> {
  6 + builder! {
  7 + <li><img src=image_url/></li>
  8 + }
  9 +}
  10 +
  11 +pub(super) fn selector_view( tx_dom :broadcast::Sender<Dom>
  12 + , rx_previews :PatchReceiver) -> ViewBuilder<Dom> {
  13 + let post_build = move |dom: &mut Dom| {
  14 + tx_dom.try_broadcast(dom.clone()).unwrap();
  15 + };
  16 +
  17 + builder! {
  18 + <div class="selector">
  19 + <ul patch:children=rx_previews
  20 + post:build=post_build>
  21 + </ul>
  22 + </div>
  23 + }
  24 +}
... ...
... ... @@ -27,7 +27,7 @@ pub(super) async fn markdown_logic( mut rx_logic: broadcast::Receiver<MarkdownLo
27 27 MarkdownLogic::Store => {
28 28 let new_md = state.get_md();
29 29 if md.json.content != new_md {
30   - md.json.content = state.get_md();
  30 + md.json.content = new_md;
31 31 md.save().await.unwrap();
32 32 }
33 33 },
... ...
... ... @@ -58,7 +58,6 @@ pub(super) fn markdown_view( tx_logic: broadcast::Sender<MarkdownLogic>
58 58
59 59 builder! {
60 60 <div class="markdown"
61   - style:width="33%"
62 61 post:build=move |_: &mut Dom| {
63 62 tx_logic.try_broadcast(MarkdownLogic::Choose(None)).unwrap();
64 63 }
... ...
  1 +pub(crate) mod imageselector;
1 2 pub(crate) mod markdown;
2 3 pub(crate) mod upload;
... ...
... ... @@ -21,12 +21,31 @@ pub(super) async fn upload_preview_logic( mut rx_canvas :broadcast::Receiver<Dom
21 21 let context = canvas
22 22 . get_context("2d").unwrap().unwrap()
23 23 . dyn_into::<CanvasRenderingContext2d>().unwrap();
  24 + let bitmap = upload.bitmap();
  25 + let image_width = bitmap.width() as f64;
  26 + let image_height = bitmap.height() as f64;
  27 + let canvas_width = canvas.width() as f64;
  28 + let canvas_height = canvas.height() as f64;
  29 +
  30 + /* scale with aspect ratio */
  31 + let (ox, oy, width, height) = if image_width > image_height {
  32 + let f = canvas_width / image_width;
  33 + let dest_height = image_height * f;
  34 + let o_y = (canvas_height - dest_height) / 2.0;
  35 + (0.0, o_y, canvas_width, dest_height)
  36 + } else {
  37 + let f = canvas_height / image_height;
  38 + let dest_width = image_width * f;
  39 + let o_x = (canvas_width - dest_width) / 2.0;
  40 + (o_x, 0.0, dest_width, canvas_height)
  41 + };
  42 +
24 43 context
25 44 . draw_image_with_image_bitmap_and_dw_and_dh(
26   - &upload.bitmap()
27   - , 0.0, 0.0
28   - , canvas.width() as f64, canvas.height() as f64 )
  45 + &bitmap
  46 + , ox, oy, width, height )
29 47 . unwrap();
  48 + bitmap.close();
30 49 }
31 50 }
32 51
... ... @@ -85,28 +104,23 @@ pub(super) async fn upload_logic( mut rx_logic :broadcast::Receiver<UploadLogic>
85 104 }
86 105 },
87 106 UploadLogic::Upload => {
88   - let mut remove_ids = vec![];
  107 + let mut uploads_vec = vec![];
  108 + let mut index = 0;
89 109
90 110 for upload in uploads.read().await.iter() {
91   - match api.store(upload).await {
92   - Ok(_) => remove_ids.push(upload.id),
93   - Err(e) => log::error!("{:?}", e),
94   - }
  111 + uploads_vec.push(upload.clone());
95 112 }
96 113
97   - for id in remove_ids.iter() {
98   - let mut found = None;
99   -
100   - for (upload, index) in uploads.read().await.iter().zip(0..) {
101   - if upload.id == *id {
102   - found = Some(index);
103   - break;
  114 + for upload in uploads_vec.iter() {
  115 + match api.store(upload).await {
  116 + Ok(_) => {
  117 + uploads.list_patch_remove(index).unwrap();
  118 + },
  119 + Err(e) => {
  120 + log::error!("{:?}", e);
  121 + index += 1;
104 122 }
105 123 }
106   -
107   - if let Some(index) = found {
108   - uploads.list_patch_remove(index).unwrap();
109   - }
110 124 }
111 125 }
112 126 }
... ...
1   -mod logic;
  1 +pub(crate) mod logic;
2 2 pub(crate) mod upload;
3 3 mod view;
4 4
... ...
... ... @@ -29,11 +29,13 @@ pub async fn main() -> Result<(), JsValue> {
29 29
30 30 let md = markdown::new().await;
31 31 let comp = upload::new().await;
  32 + let selector = imageselector::new().await;
32 33
33 34 let page = Component::from(builder! {
34   - <div>
  35 + <div class="application">
35 36 {comp}
36 37 {md}
  38 + {selector}
37 39 </div>
38 40 });
39 41 page.build()?.run()
... ...