Commits (11)
-
Lots of removed unwraps and added copyright info for uploaded images.
Showing
36 changed files
with
979 additions
and
223 deletions
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 | } |
data/copyright.txt
0 → 100644
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 | +--- 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; |
server/src/routes/image.rs
0 → 100644
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 | +} |
server/src/routes/ui.rs
0 → 100644
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 |
ui/src/api/image.rs
0 → 100644
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 | +// } |
@@ -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 | } |
ui/src/component/imageselector/logic.rs
0 → 100644
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 | +} |
ui/src/component/imageselector/mod.rs
0 → 100644
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 | +} |
ui/src/component/imageselector/view.rs
0 → 100644
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 | } |
@@ -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 | } |
@@ -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() |