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