switch
from
...
to
  Create Merge Request
@@ -2,6 +2,7 @@ Cargo.lock @@ -2,6 +2,7 @@ Cargo.lock
2 **/*.rs.bk 2 **/*.rs.bk
3 3
4 **/target 4 **/target
  5 +/data/copyright.png
5 /pkg 6 /pkg
6 /static/ui/ 7 /static/ui/
7 /var 8 /var
1 -.PHONY: start wasm build run clean  
2 -  
3 PROFILE ?= dev 1 PROFILE ?= dev
4 ifeq "$(PROFILE)" "release" 2 ifeq "$(PROFILE)" "release"
5 CARGO_PROFILE = --release 3 CARGO_PROFILE = --release
6 WASM_PROFILE = --release 4 WASM_PROFILE = --release
7 WASM_EXTRA = --no-default-features --features wee_alloc 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 else 8 else
9 CARGO_PROFILE = 9 CARGO_PROFILE =
10 WASM_PROFILE = --dev 10 WASM_PROFILE = --dev
11 WASM_EXTRA = 11 WASM_EXTRA =
  12 +SERVER_TARGET = target/debug/artshop-server
  13 +WASM_TARGET = ui/target/wasm32-unknown-unknown/debug/artshop_frontend.wasm
12 endif 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 define msg 26 define msg
  27 + @printf "\033[38;5;197m%s\033[0m" "$(1)"
  28 +endef
  29 +
  30 +define msgnl
15 @printf "\033[38;5;197m%s\033[0m\n" "$(1)" 31 @printf "\033[38;5;197m%s\033[0m\n" "$(1)"
16 endef 32 endef
17 33
  34 +.PHONY: start run wasm build clean release
  35 +
18 start: 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 @PROFILE=$(PROFILE) wasm-pack build $(WASM_PROFILE) -d ../static/ui \ 67 @PROFILE=$(PROFILE) wasm-pack build $(WASM_PROFILE) -d ../static/ui \
25 -t web ./ui -- $(WASM_EXTRA) 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 @PROFILE=$(PROFILE) cargo build $(CARGO_PROFILE) --bin artshop-server 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 release: 82 release:
36 docker build -t artshop -f build/Dockerfile . 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 devdb: 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 -p 3306:3306 \ 116 -p 3306:3306 \
42 --env MARIADB_USER=artshop \ 117 --env MARIADB_USER=artshop \
43 --env MARIADB_PASSWORD=123456 \ 118 --env MARIADB_PASSWORD=123456 \
44 --env MARIADB_ROOT_PASSWORD=123456 mariadb:latest 119 --env MARIADB_ROOT_PASSWORD=123456 mariadb:latest
45 120
46 enterdb: 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 # docker run -it --network mariadb-dev-network --rm mariadb:latest \ 123 # docker run -it --network mariadb-dev-network --rm mariadb:latest \
49 # mysql -h mariadb-dev -u artshop -p 124 # mysql -h mariadb-dev -u artshop -p
50 125
51 rootdb: 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 +5,17 @@ namespace = "artshop.shome.steffers.org"
5 # url = "./var/lib/artshop/database" 5 # url = "./var/lib/artshop/database"
6 6
7 [locations] 7 [locations]
8 -upload = "/tmp/artshop/uploads" 8 +data = "./data"
9 images = "./var/lib/artshop/images" 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,27 +8,23 @@ pub enum Either<L, R> {
8 8
9 #[derive(Clone, Debug, Serialize, Deserialize)] 9 #[derive(Clone, Debug, Serialize, Deserialize)]
10 pub struct MarkdownJson { 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 #[derive(Clone, Debug, Serialize, Deserialize)] 18 #[derive(Clone, Debug, Serialize, Deserialize)]
19 pub struct MarkdownDiffJson { 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 #[derive(Clone, Debug, Serialize, Deserialize)] 24 #[derive(Clone, Debug, Serialize, Deserialize)]
25 pub struct ImageJson { 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 pub date_created :String, 28 pub date_created :String,
33 pub date_updated :String 29 pub date_updated :String
34 } 30 }
  1 +Copyright © 2022, Stefanies Artshop
@@ -64,3 +64,18 @@ and parallel: @@ -64,3 +64,18 @@ and parallel:
64 64
65 GRANT ALL PRIVILEGES ON artshop.* TO 'artshop'@'%'; 65 GRANT ALL PRIVILEGES ON artshop.* TO 'artshop'@'%';
66 CREATE DATABASE artshop CHARACTER SET = 'utf8mb3' COLLATE = 'utf8mb3_general_ci'; 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,7 +102,7 @@ erstellen. Theoretisch könnte für solche Böcke dann Syntax-Highliting eingeba
102 ```shell 102 ```shell
103 #!/bin/env sh 103 #!/bin/env sh
104 104
105 -FOO="foo" 105 +FOO=\"foo\"
106 106
107 function func() { 107 function func() {
108 local BAR=bar 108 local BAR=bar
@@ -193,6 +193,6 @@ wie hier.</pre> @@ -193,6 +193,6 @@ wie hier.</pre>
193 </ul> 193 </ul>
194 194
195 [lnk1]: https://heise.de/tp/ 'Telepolis' 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 , '2022-01-29 21:33:34.000' 197 , '2022-01-29 21:33:34.000'
198 , '2022-01-29 21:33:34.000' ); 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,15 +8,15 @@ repository = "https://gitlab.weird-web-workers.org/rust/artshop"
8 license = "GPL-3.0-or-later" 8 license = "GPL-3.0-or-later"
9 9
10 [dependencies] 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 anyhow = "1.0" 14 anyhow = "1.0"
15 artshop-common = { path = "../common" } 15 artshop-common = { path = "../common" }
16 -async-std = "^1.10" 16 +async-std = { version = "^1.10", features = ["unstable"] }
17 chrono = "0.4.15" 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 dotenv = "0.15.0" 20 dotenv = "0.15.0"
21 flate2 = "^1.0" 21 flate2 = "^1.0"
22 futures = "^0.3" 22 futures = "^0.3"
@@ -24,9 +24,12 @@ futures-util = { version = "0", features = ["std"] } @@ -24,9 +24,12 @@ futures-util = { version = "0", features = ["std"] }
24 image = "^0.23" 24 image = "^0.23"
25 listenfd = "0.3" 25 listenfd = "0.3"
26 once_cell = "^1.9" 26 once_cell = "^1.9"
  27 +mime = "^0.3"
27 r2d2 = "0.8.9" 28 r2d2 = "0.8.9"
  29 +rexiv2 = "^0.9"
28 serde = { version = "^1.0", features = ["derive"] } 30 serde = { version = "^1.0", features = ["derive"] }
29 serde_derive = "1.0" 31 serde_derive = "1.0"
30 serde_json = "1.0" 32 serde_json = "1.0"
  33 +steganography = { git = "https://github.com/teovoinea/steganography" }
31 toml = "^0.5" 34 toml = "^0.5"
32 uuid = { version = "^0.8", features = ["v4", "v5"] } 35 uuid = { version = "^0.8", features = ["v4", "v5"] }
1 use std::fs::File; 1 use std::fs::File;
2 use std::io::Read; 2 use std::io::Read;
  3 +use image::{ DynamicImage, io::Reader as ImageReader };
3 use once_cell::sync::Lazy; 4 use once_cell::sync::Lazy;
4 use serde::Deserialize; 5 use serde::Deserialize;
  6 +use anyhow::Result;
  7 +use crate::routes::image::Size as ImageSize;
5 8
6 #[derive(Debug, Deserialize)] 9 #[derive(Debug, Deserialize)]
7 struct Database { url :Option<String> } 10 struct Database { url :Option<String> }
8 11
9 #[derive(Debug, Deserialize)] 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 #[derive(Debug, Deserialize)] 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 impl Config { 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 let mut buffer = vec![]; 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 Some(url) => Some(url), 56 Some(url) => Some(url),
32 None => std::env::var("DATABASE_URL").ok() 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 pub fn namespace(&self) -> &str { 67 pub fn namespace(&self) -> &str {
39 - self.namespace.as_str() 68 + self.config_file.namespace.as_str()
40 } 69 }
41 70
42 pub fn upload_dir(&self) -> &str { 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 pub fn images_dir(&self) -> &str { 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 use diesel::result; 6 use diesel::result;
4 -use diffy::ParsePatchError; 7 +use diffy::{ParsePatchError, ApplyError};
  8 +use image::ImageError;
  9 +use mime::FromStrError;
5 use r2d2; 10 use r2d2;
  11 +use rexiv2::Rexiv2Error;
  12 +
  13 +use crate::{Pool, models::image::Image};
6 14
7 type ParentError = Option<Pin<Box<dyn std::error::Error>>>; 15 type ParentError = Option<Pin<Box<dyn std::error::Error>>>;
8 16
9 #[derive(Debug)] 17 #[derive(Debug)]
10 pub struct Error { 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 unsafe impl Send for Error {} 24 unsafe impl Send for Error {}
@@ -22,19 +31,36 @@ impl std::error::Error 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 impl Display for Error { 40 impl Display for Error {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self { 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,6 +68,7 @@ impl From<result::Error> for Error {
42 fn from(source: result::Error) -> Self { 68 fn from(source: result::Error) -> Self {
43 Self { source: Some(Box::pin(source)) 69 Self { source: Some(Box::pin(source))
44 , message: String::from("Diesel Result Error") 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,7 +76,8 @@ impl From<result::Error> for Error {
49 impl From<r2d2::Error> for Error { 76 impl From<r2d2::Error> for Error {
50 fn from(source: r2d2::Error) -> Self { 77 fn from(source: r2d2::Error) -> Self {
51 Self { source: Some(Box::pin(source)) 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,6 +86,7 @@ impl From<std::io::Error> for Error {
58 fn from(source: std::io::Error) -> Self { 86 fn from(source: std::io::Error) -> Self {
59 Self { source: Some(Box::pin(source)) 87 Self { source: Some(Box::pin(source))
60 , message: String::from("IO Error") 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,7 +94,8 @@ impl From<std::io::Error> for Error {
65 impl From<std::str::Utf8Error> for Error { 94 impl From<std::str::Utf8Error> for Error {
66 fn from(source: std::str::Utf8Error) -> Self { 95 fn from(source: std::str::Utf8Error) -> Self {
67 Self { source: Some(Box::pin(source)) 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,7 +103,17 @@ impl From<std::str::Utf8Error> for Error {
73 impl From<ParsePatchError> for Error { 103 impl From<ParsePatchError> for Error {
74 fn from(source: ParsePatchError) -> Self { 104 fn from(source: ParsePatchError) -> Self {
75 Self { source: Some(Box::pin(source)) 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,6 +122,61 @@ impl From<uuid::Error> for Error {
82 fn from(source: uuid::Error) -> Self { 122 fn from(source: uuid::Error) -> Self {
83 Self { source: Some(Box::pin(source)) 123 Self { source: Some(Box::pin(source))
84 , message: String::from("UUID error") 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,8 +12,12 @@ mod upload_worker;
12 use models::image::Image; 12 use models::image::Image;
13 use routes::markdown::*; 13 use routes::markdown::*;
14 use routes::other::*; 14 use routes::other::*;
  15 +use routes::ui::frontend_js;
  16 +use routes::ui::frontend_wasm;
15 use routes::user::*; 17 use routes::user::*;
16 use routes::upload::*; 18 use routes::upload::*;
  19 +use routes::image::*;
  20 +
17 21
18 use actix_web::{guard, web, App, HttpResponse, HttpServer}; 22 use actix_web::{guard, web, App, HttpResponse, HttpServer};
19 use async_std::channel::Sender; 23 use async_std::channel::Sender;
@@ -21,7 +25,6 @@ use diesel::r2d2::{self, ConnectionManager}; @@ -21,7 +25,6 @@ use diesel::r2d2::{self, ConnectionManager};
21 use diesel::MysqlConnection; 25 use diesel::MysqlConnection;
22 use listenfd::ListenFd; 26 use listenfd::ListenFd;
23 use std::sync::Arc; 27 use std::sync::Arc;
24 -use std::ops::Deref;  
25 28
26 pub(crate) type Pool = r2d2::Pool<ConnectionManager<MysqlConnection>>; 29 pub(crate) type Pool = r2d2::Pool<ConnectionManager<MysqlConnection>>;
27 30
@@ -37,8 +40,6 @@ async fn main() -> std::io::Result<()> { @@ -37,8 +40,6 @@ async fn main() -> std::io::Result<()> {
37 40
38 dotenv::dotenv().ok(); 41 dotenv::dotenv().ok();
39 42
40 - println!("CONFIG: {:?}", config::CONFIG.deref());  
41 -  
42 let tx_upload_worker = upload_worker::launch(); 43 let tx_upload_worker = upload_worker::launch();
43 44
44 let database_url = std::env::var("DATABASE_URL").expect("NOT FOUND"); 45 let database_url = std::env::var("DATABASE_URL").expect("NOT FOUND");
@@ -56,6 +57,12 @@ async fn main() -> std::io::Result<()> { @@ -56,6 +57,12 @@ async fn main() -> std::io::Result<()> {
56 . service( web::resource("/upload") 57 . service( web::resource("/upload")
57 . route(web::post().to(upload)) 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 . service( web::resource("/markdowns") 66 . service( web::resource("/markdowns")
60 . route(web::get().to(get_markdowns)) 67 . route(web::get().to(get_markdowns))
61 ) 68 )
@@ -76,6 +83,12 @@ async fn main() -> std::io::Result<()> { @@ -76,6 +83,12 @@ async fn main() -> std::io::Result<()> {
76 . route(web::put().to(update_user)) 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 . service( web::scope("") 92 . service( web::scope("")
80 . route("/", web::get().to(root)) 93 . route("/", web::get().to(root))
81 . route("/api.html", web::get().to(apidoc)) 94 . route("/api.html", web::get().to(apidoc))
  1 +use std::convert::TryFrom;
  2 +use std::io::SeekFrom;
1 use std::sync::Arc; 3 use std::sync::Arc;
2 4
3 use crate::error::*; 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 use diesel::{Connection, insert_into, delete, update}; 13 use diesel::{Connection, insert_into, delete, update};
6 use diesel::prelude::*; 14 use diesel::prelude::*;
7 use serde::{Deserialize, Serialize}; 15 use serde::{Deserialize, Serialize};
8 16
  17 +use async_std::io::ReadExt;
  18 +
9 #[derive(Clone, Debug, Serialize, Deserialize, Queryable, Identifiable)] 19 #[derive(Clone, Debug, Serialize, Deserialize, Queryable, Identifiable)]
10 pub struct Image { 20 pub struct Image {
11 pub id :i32, 21 pub id :i32,
@@ -50,6 +60,13 @@ pub struct ImagePatch { @@ -50,6 +60,13 @@ pub struct ImagePatch {
50 pub date_updated :String 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 impl From<Image> for ImagePatch { 70 impl From<Image> for ImagePatch {
54 fn from(image: Image) -> Self { 71 fn from(image: Image) -> Self {
55 let now = chrono::Local::now().naive_local(); 72 let now = chrono::Local::now().naive_local();
@@ -65,26 +82,119 @@ impl From<Image> for ImagePatch { @@ -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 pub(crate) fn upload( pool: Arc<Pool> 199 pub(crate) fn upload( pool: Arc<Pool>
90 , item: Upload ) -> Result<Image> { 200 , item: Upload ) -> Result<Image> {
@@ -108,8 +218,8 @@ pub(crate) fn upload( pool: Arc<Pool> @@ -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 use crate::schema::images::dsl::*; 223 use crate::schema::images::dsl::*;
114 224
115 let db_connection = pool.get()?; 225 let db_connection = pool.get()?;
@@ -129,3 +239,22 @@ pub(crate) fn finalize( pool: Arc<Pool> @@ -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,7 +162,7 @@ pub(crate) fn get_markdown( pool: Arc<Pool>
162 let patch_data = patch.get_diff_as_string()?; 162 let patch_data = patch.get_diff_as_string()?;
163 let decomp = Patch::from_str(&patch_data)?; 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 markdown.date_updated = patch.date_created; 166 markdown.date_updated = patch.date_created;
167 } 167 }
168 }; 168 };
@@ -211,8 +211,8 @@ pub(crate) fn update_markdown( pool: Arc<Pool> @@ -211,8 +211,8 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
211 let patch = format!( "{}", create_patch( item.content.as_str() 211 let patch = format!( "{}", create_patch( item.content.as_str()
212 , markdown.content.as_str() )); 212 , markdown.content.as_str() ));
213 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best()); 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 let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id)) 217 let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id))
218 . order(diff_id.desc()) 218 . order(diff_id.desc())
@@ -240,7 +240,7 @@ pub(crate) fn update_markdown( pool: Arc<Pool> @@ -240,7 +240,7 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
240 update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?; 240 update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?;
241 241
242 Ok(()) 242 Ok(())
243 - }).unwrap(); 243 + })?;
244 244
245 markdown.name = item.name; 245 markdown.name = item.name;
246 markdown.content = item.content; 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 pub(crate) mod markdown; 2 pub(crate) mod markdown;
2 pub(crate) mod other; 3 pub(crate) mod other;
  4 +pub(crate) mod ui;
3 pub(crate) mod upload; 5 pub(crate) mod upload;
4 pub(crate) mod user; 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 use anyhow::Result; 2 use anyhow::Result;
3 use async_std::fs::DirBuilder; 3 use async_std::fs::DirBuilder;
4 use futures::{stream::StreamExt, AsyncWriteExt}; 4 use futures::{stream::StreamExt, AsyncWriteExt};
5 use async_std::fs::OpenOptions; 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 use crate::config::CONFIG; 8 use crate::config::CONFIG;
9 -use std::convert::TryFrom;  
10 9
11 pub async fn upload( app_data :web::Data<AppData> 10 pub async fn upload( app_data :web::Data<AppData>
12 , mut body :web::Payload 11 , mut body :web::Payload
13 - , request :web::HttpRequest ) -> Result<HttpResponse, Error> 12 + , request :web::HttpRequest
  13 + ) -> Result<HttpResponse, ActixError>
14 { 14 {
15 let pool = app_data.database_pool.clone(); 15 let pool = app_data.database_pool.clone();
16 let worker = app_data.tx_upload_worker.clone(); 16 let worker = app_data.tx_upload_worker.clone();
17 17
18 let upload_uuid = Some(uuid::Uuid::new_v4().as_bytes().to_vec()); 18 let upload_uuid = Some(uuid::Uuid::new_v4().as_bytes().to_vec());
19 let size = request.headers().get("content-length") 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 upload_uuid, 29 upload_uuid,
28 - size, 30 + size: size.unwrap_or(0),
29 mime_type 31 mime_type
30 }; 32 };
31 33
@@ -33,24 +35,33 @@ pub async fn upload( app_data :web::Data<AppData> @@ -33,24 +35,33 @@ pub async fn upload( app_data :web::Data<AppData>
33 . create(CONFIG.upload_dir()) 35 . create(CONFIG.upload_dir())
34 . await?; 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 let mut output = OpenOptions::new(); 40 let mut output = OpenOptions::new();
38 let mut output = output 41 let mut output = output
39 . create(true) 42 . create(true)
40 . write(true) 43 . write(true)
41 . open(&upload_filename).await?; 44 . open(&upload_filename).await?;
42 while let Some(item) = body.next().await { 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 let pool_for_worker = pool.clone(); 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 , channel::{Sender, Receiver, bounded} 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 , stream::FuturesUnordered}; 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 pub fn launch() -> Sender<(Arc<Pool>, Image)> { 23 pub fn launch() -> Sender<(Arc<Pool>, Image)> {
17 let (tx_upload_worker, rx_upload_worker) 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 actix_rt::spawn(async move { 27 actix_rt::spawn(async move {
21 let mut workers = FuturesUnordered::new(); 28 let mut workers = FuturesUnordered::new();
22 29
23 loop { 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,76 +51,121 @@ pub fn launch() -> Sender<(Arc<Pool>, Image)> {
40 tx_upload_worker 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 Ok(()) 170 Ok(())
115 } 171 }
@@ -30,3 +30,11 @@ impl TryFrom<&[u8]> for Uuid { @@ -30,3 +30,11 @@ impl TryFrom<&[u8]> for Uuid {
30 Ok(Self(uuid::Uuid::from_slice(value)?)) 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 +7,7 @@
7 </head> 7 </head>
8 <body> 8 <body>
9 <script type="module"> 9 <script type="module">
10 - import init from '/static/ui/artshop_frontend.js'; 10 + import init from '/ui/artshop_frontend.js';
11 window.addEventListener('load', async () => { 11 window.addEventListener('load', async () => {
12 await init(); 12 await init();
13 }); 13 });
1 -.upload { 1 +.application > div {
2 float: left; 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 padding: 1em; 54 padding: 1em;
5 border-radius: .5em; 55 border-radius: .5em;
6 border: 1px solid #ddd; 56 border: 1px solid #ddd;
@@ -55,13 +105,18 @@ @@ -55,13 +105,18 @@
55 } 105 }
56 106
57 .markdown { 107 .markdown {
58 - float: left;  
59 padding: 1em; 108 padding: 1em;
60 border-radius: .5em; 109 border-radius: .5em;
61 border: 1px solid #ddd; 110 border: 1px solid #ddd;
62 background: #f7f7f7; 111 background: #f7f7f7;
63 } 112 }
64 113
  114 +.markdown img {
  115 + display: block;
  116 + margin-left: auto;
  117 + margin-right: auto;
  118 +}
  119 +
65 .markdown p { 120 .markdown p {
66 text-align: justify; 121 text-align: justify;
67 text-indent: .5em; 122 text-indent: .5em;
@@ -70,7 +125,6 @@ @@ -70,7 +125,6 @@
70 125
71 .markdown > div:first-child { 126 .markdown > div:first-child {
72 position: fixed; 127 position: fixed;
73 - width: inherit;  
74 z-index: 10; 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 pub(crate) mod markdown; 2 pub(crate) mod markdown;
2 pub(crate) mod upload; 3 pub(crate) mod upload;
@@ -21,7 +21,7 @@ impl UploadApi { @@ -21,7 +21,7 @@ impl UploadApi {
21 , upload.size() 21 , upload.size()
22 , upload.data() ).await?; 22 , upload.data() ).await?;
23 match response.status() { 23 match response.status() {
24 - 200 => Ok(self), 24 + 202 => Ok(self),
25 status => Err(Self::status_error(status)), 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,7 +27,7 @@ pub(super) async fn markdown_logic( mut rx_logic: broadcast::Receiver<MarkdownLo
27 MarkdownLogic::Store => { 27 MarkdownLogic::Store => {
28 let new_md = state.get_md(); 28 let new_md = state.get_md();
29 if md.json.content != new_md { 29 if md.json.content != new_md {
30 - md.json.content = state.get_md(); 30 + md.json.content = new_md;
31 md.save().await.unwrap(); 31 md.save().await.unwrap();
32 } 32 }
33 }, 33 },
@@ -58,7 +58,6 @@ pub(super) fn markdown_view( tx_logic: broadcast::Sender<MarkdownLogic> @@ -58,7 +58,6 @@ pub(super) fn markdown_view( tx_logic: broadcast::Sender<MarkdownLogic>
58 58
59 builder! { 59 builder! {
60 <div class="markdown" 60 <div class="markdown"
61 - style:width="33%"  
62 post:build=move |_: &mut Dom| { 61 post:build=move |_: &mut Dom| {
63 tx_logic.try_broadcast(MarkdownLogic::Choose(None)).unwrap(); 62 tx_logic.try_broadcast(MarkdownLogic::Choose(None)).unwrap();
64 } 63 }
  1 +pub(crate) mod imageselector;
1 pub(crate) mod markdown; 2 pub(crate) mod markdown;
2 pub(crate) mod upload; 3 pub(crate) mod upload;
@@ -21,12 +21,31 @@ pub(super) async fn upload_preview_logic( mut rx_canvas :broadcast::Receiver<Dom @@ -21,12 +21,31 @@ pub(super) async fn upload_preview_logic( mut rx_canvas :broadcast::Receiver<Dom
21 let context = canvas 21 let context = canvas
22 . get_context("2d").unwrap().unwrap() 22 . get_context("2d").unwrap().unwrap()
23 . dyn_into::<CanvasRenderingContext2d>().unwrap(); 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 context 43 context
25 . draw_image_with_image_bitmap_and_dw_and_dh( 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 . unwrap(); 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,28 +104,23 @@ pub(super) async fn upload_logic( mut rx_logic :broadcast::Receiver<UploadLogic>
85 } 104 }
86 }, 105 },
87 UploadLogic::Upload => { 106 UploadLogic::Upload => {
88 - let mut remove_ids = vec![]; 107 + let mut uploads_vec = vec![];
  108 + let mut index = 0;
89 109
90 for upload in uploads.read().await.iter() { 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 pub(crate) mod upload; 2 pub(crate) mod upload;
3 mod view; 3 mod view;
4 4
@@ -29,11 +29,13 @@ pub async fn main() -> Result<(), JsValue> { @@ -29,11 +29,13 @@ pub async fn main() -> Result<(), JsValue> {
29 29
30 let md = markdown::new().await; 30 let md = markdown::new().await;
31 let comp = upload::new().await; 31 let comp = upload::new().await;
  32 + let selector = imageselector::new().await;
32 33
33 let page = Component::from(builder! { 34 let page = Component::from(builder! {
34 - <div> 35 + <div class="application">
35 {comp} 36 {comp}
36 {md} 37 {md}
  38 + {selector}
37 </div> 39 </div>
38 }); 40 });
39 page.build()?.run() 41 page.build()?.run()