Commit a54e9551c6e939b713098f90e152f940f973ff28

Authored by Georg Hopp
1 parent 18bb86b1

Copyrights and error handling.

Lots of removed unwraps and added copyright info for uploaded
images.
... ... @@ -2,6 +2,7 @@ Cargo.lock
2 2 **/*.rs.bk
3 3
4 4 **/target
  5 +/data/copyright.png
5 6 /pkg
6 7 /static/ui/
7 8 /var
... ...
... ... @@ -4,6 +4,3 @@ members = [ "common", "server" ]
4 4 [profile.release]
5 5 lto = true
6 6 opt-level = "s"
7   -
8   -[patch.crates-io]
9   -diffy = { path = "./target/patch/diffy-0.2.1" }
... ...
... ... @@ -39,7 +39,7 @@ start:
39 39 cargo watch -i static/ -i var/ \
40 40 -s "PROFILE=$(PROFILE) make run"
41 41
42   -run: build wasm
  42 +run: build wasm data/copyright.png
43 43 $(call msgnl,RUN SERVER)
44 44 @PROFILE=$(PROFILE) cargo run $(CARGO_PROFILE) --bin artshop-server
45 45
... ... @@ -54,6 +54,8 @@ clean:
54 54 @rm -Rf ./static/ui
55 55 $(call msgnl,CLEAN INSTALLED WASM)
56 56 @pushd ui; PROFILE=$(PROFILE) cargo clean; popd
  57 + $(call msgnl,CLEAN COPYRIGHT PNG)
  58 + @rm -Rf ./data/copyright.png
57 59
58 60 $(WASM_TARGET): $(WASM_SOURCES)
59 61 $(call msgnl,BUILD WASM UI)
... ... @@ -71,6 +73,12 @@ $(SERVER_TARGET): $(SERVER_SOURCES)
71 73 $(call msgnl,BUILD SERVER)
72 74 @PROFILE=$(PROFILE) cargo build $(CARGO_PROFILE) --bin artshop-server
73 75
  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 $@
  81 +
74 82 release:
75 83 docker build -t artshop -f build/Dockerfile .
76 84
... ... @@ -99,6 +107,8 @@ downdb:
99 107 @docker stop mariadb-dev||true
100 108 $(call msgnl,REMOVE DB CONTAINER)
101 109 @docker rm -v mariadb-dev||true
  110 + $(call msgnl,REMOVE IMAGES)
  111 + @rm -Rf var/lib/artshop/images
102 112
103 113 devdb:
104 114 $(call msgnl,CREATE DB CONTAINER)
... ...
... ... @@ -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"
... ...
  1 +Copyright © 2022, Stefanies Artshop
... ...
... ... @@ -70,3 +70,12 @@ and parallel:
70 70 https://api.github.com/users/xxxxxxx/events/public
71 71
72 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
... ...
... ... @@ -16,7 +16,7 @@ artshop-common = { path = "../common" }
16 16 async-std = { version = "^1.10", features = ["unstable"] }
17 17 chrono = "0.4.15"
18 18 diesel = { version = "1.4.7", features = ["mysql", "sqlite", "r2d2"] }
19   -diffy = "0.2.1"
  19 +diffy = "0.2.2"
20 20 dotenv = "0.15.0"
21 21 flate2 = "^1.0"
22 22 futures = "^0.3"
... ... @@ -26,13 +26,10 @@ listenfd = "0.3"
26 26 once_cell = "^1.9"
27 27 mime = "^0.3"
28 28 r2d2 = "0.8.9"
  29 +rexiv2 = "^0.9"
29 30 serde = { version = "^1.0", features = ["derive"] }
30 31 serde_derive = "1.0"
31 32 serde_json = "1.0"
  33 +steganography = { git = "https://github.com/teovoinea/steganography" }
32 34 toml = "^0.5"
33 35 uuid = { version = "^0.8", features = ["v4", "v5"] }
34   -
35   -[package.metadata.patch.diffy]
36   -version = "0.2.1"
37   -patches = ["./patches/diffy-0.2.1_fix-adjacent-hunk-parsing.patch"]
38   -
... ...
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 }
... ...
1 1 use std::convert::TryFrom;
  2 +use std::io::SeekFrom;
2 3 use std::sync::Arc;
3 4
4 5 use crate::error::*;
5 6 use crate::routes::image::Size;
6 7 use crate::uuid::Uuid;
7 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;
8 12 use async_std::path::PathBuf;
9 13 use diesel::{Connection, insert_into, delete, update};
10 14 use diesel::prelude::*;
11 15 use serde::{Deserialize, Serialize};
12 16
  17 +use async_std::io::ReadExt;
  18 +
13 19 #[derive(Clone, Debug, Serialize, Deserialize, Queryable, Identifiable)]
14 20 pub struct Image {
15 21 pub id :i32,
... ... @@ -54,6 +60,13 @@ pub struct ImagePatch {
54 60 pub date_updated :String
55 61 }
56 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 +
57 70 impl From<Image> for ImagePatch {
58 71 fn from(image: Image) -> Self {
59 72 let now = chrono::Local::now().naive_local();
... ... @@ -69,44 +82,117 @@ impl From<Image> for ImagePatch {
69 82 }
70 83 }
71 84
72   -#[macro_export]
73   -macro_rules! upload_uuid {
74   - ($u:expr) => {
75   - match &$u.upload_uuid {
76   - Some(uuid) => $crate::uuid::Uuid::try_from(uuid.as_slice()).ok(),
77   - None => None,
78   - }
79   - };
  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
80 90 }
81 91
82   -#[macro_export]
83   -macro_rules! upload_filename {
84   - ($u:expr) => {
85   - $crate::upload_uuid!($u)
86   - . and_then(|uuid| Some(format!( "{}/upload_{}"
87   - , $crate::config::CONFIG.upload_dir()
88   - , uuid )))
89   - };
90   -}
  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 + }
91 108
  109 + Ok(())
  110 +}
92 111
93 112 impl Image {
94   - // TODO some handling for files that are not processed now
95   - // that is uuid is still None.
96   - pub(crate) fn path(&self, size :Size) -> String {
97   - let uuid = Uuid::try_from( self.uuid
98   - . as_ref()
99   - . unwrap()
100   - . as_slice() ).unwrap();
  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 + }
  185 +}
  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()?;
101 190 let uuid_string = format!("{}", uuid);
102 191
103   - let mut image_path = PathBuf::from(CONFIG.images_dir());
104   - image_path.push(&uuid_string.as_str()[..1]);
105   - image_path.push(&uuid_string.as_str()[..2]);
106   - image_path.push(&uuid_string.as_str()[..3]);
107   - image_path.push(&format!("{}_{}", &uuid_string, size));
  192 + let mut upload_path = PathBuf::from(CONFIG.upload_dir());
  193 + upload_path.push(&uuid_string);
108 194
109   - image_path.into_os_string().into_string().unwrap()
  195 + Some(upload_path)
110 196 }
111 197 }
112 198
... ... @@ -132,8 +218,8 @@ pub(crate) fn upload( pool: Arc<Pool>
132 218 })?)
133 219 }
134 220
135   -pub(crate) fn finalize( pool: Arc<Pool>
136   - , item: Image ) -> Result<Image> {
  221 +pub(crate) fn finalize( pool :Arc<Pool>
  222 + , item :Image ) -> Result<Image> {
137 223 use crate::schema::images::dsl::*;
138 224
139 225 let db_connection = pool.get()?;
... ... @@ -154,8 +240,8 @@ pub(crate) fn finalize( pool: Arc<Pool>
154 240 }
155 241 }
156 242
157   -pub(crate) fn get_image( pool: Arc<Pool>
158   - , ident: i32 ) -> Result<Image>
  243 +pub(crate) fn get_image( pool :Arc<Pool>
  244 + , ident :i32 ) -> Result<Image>
159 245 {
160 246 use crate::schema::images::dsl::*;
161 247
... ...
... ... @@ -162,7 +162,7 @@ pub(crate) fn get_markdown( pool: Arc<Pool>
162 162 let patch_data = patch.get_diff_as_string()?;
163 163 let decomp = Patch::from_str(&patch_data)?;
164 164
165   - markdown.content = apply(&markdown.content, &decomp).unwrap();
  165 + markdown.content = apply(&markdown.content, &decomp)?;
166 166 markdown.date_updated = patch.date_created;
167 167 }
168 168 };
... ... @@ -211,8 +211,8 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
211 211 let patch = format!( "{}", create_patch( item.content.as_str()
212 212 , markdown.content.as_str() ));
213 213 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
214   - encoder.write_all(patch.as_bytes()).unwrap();
215   - let compressed = encoder.finish().unwrap();
  214 + encoder.write_all(patch.as_bytes())?;
  215 + let compressed = encoder.finish()?;
216 216
217 217 let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id))
218 218 . order(diff_id.desc())
... ... @@ -240,7 +240,7 @@ pub(crate) fn update_markdown( pool: Arc<Pool>
240 240 update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?;
241 241
242 242 Ok(())
243   - }).unwrap();
  243 + })?;
244 244
245 245 markdown.name = item.name;
246 246 markdown.content = item.content;
... ...
1 1 use std::fmt::Display;
2 2
3   -use crate::{models::image, AppData};
  3 +use crate::{models::image, AppData, error::Error};
4 4
5   -use actix_web::{Error, web};
  5 +use actix_web::{Error as ActixError, web, http::StatusCode};
6 6 use anyhow::Result;
7 7 use serde::{Deserialize, Serialize};
8 8
9   -#[derive(Debug, Deserialize, Serialize)]
  9 +#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
10 10 #[serde (rename_all = "lowercase")]
11 11 pub enum Size {
12 12 Original,
13 13 Large,
14 14 Medium,
15   - Small
  15 + Small,
  16 + Thumbnail
16 17 }
17 18
18 19 #[derive(Debug, Deserialize, Serialize)]
... ... @@ -26,7 +27,8 @@ impl Display for Size {
26 27 Size::Original => "original",
27 28 Size::Large => "large",
28 29 Size::Medium => "medium",
29   - Size::Small => "small"
  30 + Size::Small => "small",
  31 + Size::Thumbnail => "thumbnail"
30 32 };
31 33
32 34 write!(f, "{}", size_str)
... ... @@ -36,16 +38,21 @@ impl Display for Size {
36 38 pub async fn get_image( app_data: web::Data<AppData>
37 39 , ident: web::Path<i32>
38 40 , size: web::Query<SizeQuery>
39   - ) -> Result<actix_files::NamedFile, Error>
  41 + ) -> Result<actix_files::NamedFile, ActixError>
40 42 {
41 43 let pool = app_data.database_pool.clone();
42 44 let ident = ident.into_inner();
43   - let size = size.into_inner().size.or(Some(Size::Large)).unwrap();
  45 + let size = size.into_inner().size.unwrap_or(Size::Large);
44 46
45 47 let image = web::block(move || image::get_image(pool, ident)).await?;
46   - let path = image.path(size);
  48 + let path = image.clone().context()
  49 + . path(size).await
  50 + . ok_or(Error::new( "Image not ready"
  51 + , StatusCode::SERVICE_UNAVAILABLE ))?;
47 52
48 53 Ok( actix_files::NamedFile::open(path)?
49   - . set_content_type(image.mime_type.parse().unwrap())
  54 + . set_content_type( image.mime_type
  55 + . parse()
  56 + . map_err(|e| Error::from(e))? )
50 57 . disable_content_disposition() )
51 58 }
... ...
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}
2   - , sync::Arc
3   - , convert::TryFrom };
4   -use actix_web::web;
5   -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}
6 5 , channel::{Sender, Receiver, bounded}
7   - , path::PathBuf
8   - , io::Result, task::spawn_blocking };
9   -use futures::{ AsyncSeekExt, AsyncReadExt, FutureExt, StreamExt, select
  6 + , task::spawn_blocking };
  7 +use futures::{ FutureExt, StreamExt, select
10 8 , stream::FuturesUnordered};
  9 +use rexiv2::Metadata;
  10 +use steganography::encoder::Encoder;
11 11
12   -use crate::{ models::image::{Image, finalize}
13   - , upload_filename
  12 +use crate::{ models::image::{Image, finalize, ImageContext}
14 13 , config::CONFIG
15 14 , Pool
16 15 , routes::image::Size
17   - , uuid::Uuid };
  16 + , error::Error };
18 17
19 18 use image::{ io::Reader as ImageReader
20 19 , GenericImageView
21   - , imageops::FilterType::Lanczos3
22   - , ImageFormat::Jpeg };
  20 + , imageops::{FilterType::Lanczos3, overlay}
  21 + , ImageFormat::Jpeg, DynamicImage };
23 22
24 23 pub fn launch() -> Sender<(Arc<Pool>, Image)> {
25 24 let (tx_upload_worker, rx_upload_worker)
... ... @@ -48,105 +47,99 @@ pub fn launch() -> Sender<(Arc<Pool>, Image)> {
48 47 tx_upload_worker
49 48 }
50 49
  50 +async fn store_original(context :&mut ImageContext) -> Result<(), Error> {
  51 + let upload_path = context
  52 + . upload_path().await
  53 + . ok_or(Error::new( "Can't retreive upload path"
  54 + , StatusCode::INTERNAL_SERVER_ERROR ))?
  55 + . to_owned();
  56 + let original_path = context
  57 + . path(Size::Original).await
  58 + . ok_or(Error::new( "No path for given size"
  59 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  60 +
  61 + match metadata(&original_path).await {
  62 + Err(e) if e.kind() == ErrorKind::NotFound => {
  63 + copy(&upload_path, &original_path).await?;
  64 + remove_file(&upload_path).await?;
  65 + Ok(())
  66 + },
  67 + Err(e) => Err(e)?,
  68 + Ok(_) => Ok(()),
  69 + }
  70 +}
51 71
52   -async fn worker(pool :Arc<Pool>, mut image :Image) {
53   - let upload_filename = upload_filename!(image).unwrap();
54   - let mut f = File::open(&upload_filename).await.unwrap();
  72 +async fn load_original(context :&mut ImageContext) -> Result<DynamicImage, Error> {
  73 + let original_path = context
  74 + . path(Size::Original).await
  75 + . ok_or(Error::new( "Unable to load original image"
  76 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
55 77
56   - let mut buf = vec!['.' as u8; 3 * 3 * 4096];
57   - get_sample(&mut f, buf.as_mut()).await.unwrap();
58   - let uuid = Uuid::get(CONFIG.namespace(), buf.as_mut());
59   - let uuid_string = format!("{}", uuid);
  78 + spawn_blocking(move || -> Result<DynamicImage, Error> {
  79 + Ok(ImageReader::open(&original_path)? . with_guessed_format()?
  80 + . decode()?)
  81 + }).await
  82 +}
60 83
61   - let image_path = PathBuf::from(CONFIG.images_dir());
62   - let mut orig_path = image_path.clone();
63   - let mut large_path = image_path.clone();
64   - let mut medium_path = image_path.clone();
65   - let mut small_path = image_path.clone();
  84 +async fn save_resized( original :&DynamicImage
  85 + , context :&mut ImageContext
  86 + , size :Size ) -> Result<(), Error> {
  87 + let width = CONFIG.width(size)
  88 + . ok_or(Error::new( "Can't get width for size"
  89 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  90 + let height = CONFIG.height(size)
  91 + . ok_or(Error::new( "Can't get height for size"
  92 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  93 +
  94 + let path = context.path(size).await
  95 + . ok_or(Error::new( "Can't get path for size"
  96 + , StatusCode::INTERNAL_SERVER_ERROR ))?;
  97 +
  98 + let original = original.to_owned();
  99 +
  100 + spawn_blocking(move || -> Result<(), Error> {
  101 + let mut scaled = original.resize(width, height, Lanczos3);
  102 + overlay(&mut scaled, CONFIG.copyright_image(), 0_u32, 0_u32);
  103 +
  104 + if let Size::Thumbnail = size {
  105 + scaled.save_with_format(&path, Jpeg)?;
  106 + } else {
  107 + let stegonography = CONFIG.copyright_steganography().as_bytes();
  108 + let encoder = Encoder::new(stegonography, scaled);
  109 + let scaled = encoder.encode_alpha();
  110 + scaled.save_with_format(&path, Jpeg)?;
  111 + }
66 112
67   - macro_rules! prepare {
68   - ($p:expr, $n:expr) => (
69   - $p.push(&uuid_string.as_str()[..1]);
70   - $p.push(&uuid_string.as_str()[..2]);
71   - $p.push(&uuid_string.as_str()[..3]);
  113 + let exiv = Metadata::new_from_path(&path)?;
  114 + exiv.set_tag_string("Exif.Image.Copyright", CONFIG.copyright_exiv())?;
  115 + exiv.save_to_file(&path)?;
72 116
73   - DirBuilder::new() . recursive(true)
74   - . create(&$p)
75   - . await
76   - . unwrap();
  117 + Ok(())
  118 + }).await
  119 +}
77 120
78   - $p.push(&format!("{}_{}", &uuid_string, $n));
79   - )
80   - }
  121 +async fn worker(pool :Arc<Pool>, image :Image) -> Result<(), Error> {
  122 + let mut context = image.context();
81 123
82   - prepare!(orig_path, Size::Original);
83   - prepare!(large_path, Size::Large);
84   - prepare!(medium_path, Size::Medium);
85   - prepare!(small_path, Size::Small);
  124 + DirBuilder::new() . recursive(true)
  125 + . create(context.base_path().await.unwrap())
  126 + . await
  127 + . unwrap();
86 128
87   - image.upload_uuid = None;
88   - image.uuid = Some(uuid.0.as_bytes().to_vec());
  129 + store_original(&mut context).await.unwrap();
89 130
90   - match metadata(&orig_path).await {
91   - Err(e) if e.kind() == ErrorKind::NotFound => {
92   - copy(&upload_filename, &orig_path).await.unwrap();
93   -
94   - let (dim_x, dim_y) = spawn_blocking(move || {
95   - let img = ImageReader::open(&orig_path).unwrap()
96   - . with_guessed_format().unwrap()
97   - . decode().unwrap();
98   - let (dim_x, dim_y) = img.dimensions();
99   -
100   - img . resize(1280, 1280, Lanczos3)
101   - . save_with_format(&large_path, Jpeg)
102   - . unwrap();
103   - img . resize(800, 800, Lanczos3)
104   - . save_with_format(&medium_path, Jpeg)
105   - . unwrap();
106   - img . resize(400, 400, Lanczos3)
107   - . save_with_format(&small_path, Jpeg)
108   - . unwrap();
109   -
110   - (dim_x as i32, dim_y as i32)
111   - }).await;
112   -
113   - image.dim_x = Some(dim_x);
114   - image.dim_y = Some(dim_y);
115   - },
116   - Err(e) => {
117   - let e :Result<()> = Err(e);
118   - e.unwrap();
119   - },
120   - Ok(_) => {},
121   - }
122   -
123   - remove_file(&upload_filename).await.unwrap();
124   - web::block(move || finalize(pool, image)).await.unwrap();
125   -}
  131 + let original = load_original(&mut context).await.unwrap();
  132 + let (dim_x, dim_y) = original.dimensions();
126 133
127   -async fn read_at( f :&mut File
128   - , pos :SeekFrom
129   - , buf :&mut [u8]) -> std::io::Result<()> {
130   - f.seek(pos).await?;
131   - f.read_exact(buf).await
132   -}
  134 + context.image.dim_x = Some(dim_x as i32);
  135 + context.image.dim_y = Some(dim_y as i32);
133 136
134   -async fn get_sample( f :&mut File
135   - , buf :&mut [u8]) -> std::io::Result<()> {
136   - let file_len = f.metadata().await?.len();
137   - let chunk_size = buf.len() / 3;
  137 + save_resized(&original, &mut context, Size::Large).await?;
  138 + save_resized(&original, &mut context, Size::Medium).await?;
  139 + save_resized(&original, &mut context, Size::Small).await?;
  140 + save_resized(&original, &mut context, Size::Thumbnail).await?;
138 141
139   - read_at(f, SeekFrom::Start(0), &mut buf[0..chunk_size]).await?;
140   - if file_len >= 2 * chunk_size as u64 {
141   - read_at( f
142   - , SeekFrom::End(-(chunk_size as i64))
143   - , &mut buf[2*chunk_size..]).await?;
144   - }
145   - if file_len >= 3 * chunk_size as u64 {
146   - read_at( f
147   - , SeekFrom::Start((file_len-chunk_size as u64) / 2)
148   - , &mut buf[chunk_size..2*chunk_size]).await?;
149   - }
  142 + web::block(move || finalize(pool, context.image)).await?;
150 143
151 144 Ok(())
152 145 }
... ...
... ... @@ -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 +}
... ...
... ... @@ -21,7 +21,7 @@ impl UploadApi {
21 21 , upload.size()
22 22 , upload.data() ).await?;
23 23 match response.status() {
24   - 200 => Ok(self),
  24 + 202 => Ok(self),
25 25 status => Err(Self::status_error(status)),
26 26 }
27 27 }
... ...
Please register or login to post a comment