Use rexiv2 for metadata removal

This commit is contained in:
asonix 2020-06-14 21:41:45 -05:00
parent eaeb12ed60
commit 154914e61a
11 changed files with 436 additions and 170 deletions

28
Cargo.lock generated
View file

@ -920,6 +920,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gexiv2-sys"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc8f7e79962171a99792ff6895fac7abe89380c02f9abf9dc73c88f2e56697c"
dependencies = [
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.10.3" version = "0.10.3"
@ -1378,6 +1388,7 @@ dependencies = [
"mime", "mime",
"once_cell", "once_cell",
"rand", "rand",
"rexiv2",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1422,6 +1433,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
[[package]] [[package]]
name = "png" name = "png"
version = "0.16.5" version = "0.16.5"
@ -1611,6 +1628,17 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "rexiv2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982534bb5ab05ec02973487cb3338392dc68fdc8e189fdcf268ef8c95a78cfa8"
dependencies = [
"gexiv2-sys",
"libc",
"num-rational",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.16.14" version = "0.16.14"

View file

@ -24,6 +24,7 @@ image = "0.23.4"
mime = "0.3.1" mime = "0.3.1"
once_cell = "1.4.0" once_cell = "1.4.0"
rand = "0.7.3" rand = "0.7.3"
rexiv2 = "0.9.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha2 = "0.9.0" sha2 = "0.9.0"

View file

@ -1,30 +1,106 @@
# Build FROM rustembedded/cross:x86_64-unknown-linux-gnu AS x86_64-builder
FROM ekidd/rust-musl-builder:1.44.0 as rust
ARG UID=1000
ARG GID=1000
ENV TOOLCHAIN=stable
ENV TARGET=x86_64-unknown-linux-gnu
ENV TOOL=x86_64-linux-gnu
RUN \
apt-get update && \
apt-get upgrade -y
RUN \
addgroup --gid "${GID}" build && \
adduser \
--disabled-password \
--gecos "" \
--ingroup build \
--uid "${UID}" \
--home /opt/build \
build
ADD https://sh.rustup.rs /opt/build/rustup.sh
RUN \
chown -R build:build /opt/build
USER build
WORKDIR /opt/build
ENV PATH=$PATH:/opt/build/.cargo/bin
RUN \
chmod +x rustup.sh && \
./rustup.sh --default-toolchain $TOOLCHAIN --profile minimal -y && \
rustup target add $TARGET
FROM x86_64-builder as builder
USER root
RUN \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get -y install libgexiv2-dev:amd64
USER build
ENV USER=build
# Cache deps # Cache deps
WORKDIR /app RUN \
RUN sudo chown -R rust:rust . cargo new repo
RUN USER=root cargo new server
WORKDIR /app/server WORKDIR /opt/build/repo
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
RUN sudo chown -R rust:rust .
RUN mkdir -p ./src/bin \ USER root
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs RUN \
RUN cargo build --release chown -R build:build ./
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/pict_rs*
USER build
RUN \
mkdir -p ./src && \
echo 'fn main() { println!("Dummy") }' > ./src/main.rs && \
cargo build --release && \
rm -rf ./src
COPY src ./src/ COPY src ./src/
USER root
RUN \
chown -R build:build ./src && \
rm -r ./target/release/deps/pict_rs-*
USER build
# Build for release # Build for release
RUN cargo build --frozen --release RUN cargo build --frozen --release
FROM alpine:3.11 FROM ubuntu:20.04
ARG UID=1000
ARG GID=1000
RUN apt-get update \
&& apt-get install -y libgexiv2-2
# Copy resources # Copy resources
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/pict-rs /app/pict-rs COPY --from=builder /opt/build/repo/target/release/pict-rs /usr/bin/pict-rs
RUN addgroup -g 1000 pictrs RUN \
RUN adduser -D -s /bin/sh -u 1000 -G pictrs pictrs addgroup -gid "${GID}" pictrs && \
RUN chown pictrs:pictrs /app/pict-rs adduser \
--disabled-password \
--gecos "" \
--ingroup pictrs \
--uid "${UID}" \
--home /opt/pictrs \
pictrs
WORKDIR /opt/pictrs
USER pictrs USER pictrs
EXPOSE 8080 EXPOSE 8080
CMD ["/app/pict-rs"] CMD ["/usr/bin/pict-rs"]

View file

@ -1,11 +1,11 @@
FROM rustembedded/cross:x86_64-unknown-linux-musl AS x86_64-builder FROM rustembedded/cross:x86_64-unknown-linux-gnu AS x86_64-builder
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ENV TOOLCHAIN=stable ENV TOOLCHAIN=stable
ENV TARGET=x86_64-unknown-linux-musl ENV TARGET=x86_64-unknown-linux-gnu
ENV TOOL=x86_64-linux-musl ENV TOOL=x86_64-linux-gnu
RUN \ RUN \
apt-get update && \ apt-get update && \
@ -29,7 +29,7 @@ RUN \
USER build USER build
WORKDIR /opt/build WORKDIR /opt/build
ENV PATH=/opt/build/.cargo/bin:/usr/local/musl/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV PATH=$PATH:/opt/build/.cargo/bin
RUN \ RUN \
chmod +x rustup.sh && \ chmod +x rustup.sh && \
@ -38,10 +38,22 @@ RUN \
FROM x86_64-builder as builder FROM x86_64-builder as builder
USER root
RUN \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get -y install libgexiv2-dev:amd64
USER build
ARG TAG=master ARG TAG=master
ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs
ARG BINARY=pict-rs ARG BINARY=pict-rs
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_PATH=/usr/lib/$TOOL/pkgconfig:/usr/lib/$PKGCONFIG
RUN \ RUN \
git clone -b $TAG $REPOSITORY repo git clone -b $TAG $REPOSITORY repo
@ -51,18 +63,26 @@ RUN \
cargo build --release --target $TARGET && \ cargo build --release --target $TARGET && \
$TOOL-strip target/$TARGET/release/$BINARY $TOOL-strip target/$TARGET/release/$BINARY
FROM amd64/alpine:3.11 FROM amd64/ubuntu:20.04
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ARG BINARY=pict-rs ARG BINARY=pict-rs
COPY --from=builder /opt/build/repo/target/x86_64-unknown-linux-musl/release/$BINARY /usr/bin/$BINARY COPY --from=builder /opt/build/repo/target/x86_64-unknown-linux-gnu/release/$BINARY /usr/bin/$BINARY
RUN \ RUN \
apk add tini && \ apt-get update && \
addgroup -g $GID pictrs && \ apt-get -y upgrade && \
adduser -D -G pictrs -u $UID -g "" -h /opt/pictrs pictrs apt-get -y install tini libgexiv2-2 && \
addgroup --gid $GID pictrs && \
adduser \
--disabled-password \
--gecos "" \
--ingroup pictrs \
--uid $UID \
--home /opt/pictrs \
pictrs
RUN \ RUN \
chown -R pictrs:pictrs /mnt chown -R pictrs:pictrs /mnt
@ -70,5 +90,5 @@ RUN \
VOLUME /mnt VOLUME /mnt
WORKDIR /opt/pictrs WORKDIR /opt/pictrs
USER pictrs USER pictrs
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"] CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"]

View file

@ -1,11 +1,11 @@
FROM rustembedded/cross:arm-unknown-linux-musleabihf AS arm32v7-builder FROM rustembedded/cross:arm-unknown-linux-gnueabihf AS arm32v7-builder
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ENV TOOLCHAIN=stable ENV TOOLCHAIN=stable
ENV TARGET=arm-unknown-linux-musleabihf ENV TARGET=arm-unknown-linux-gnueabihf
ENV TOOL=arm-linux-musleabihf ENV TOOL=arm-linux-gnueabihf
RUN \ RUN \
apt-get update && \ apt-get update && \
@ -29,7 +29,7 @@ RUN \
USER build USER build
WORKDIR /opt/build WORKDIR /opt/build
ENV PATH=/opt/build/.cargo/bin:/usr/local/musl/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV PATH=$PATH:/opt/build/.cargo/bin
RUN \ RUN \
chmod +x rustup.sh && \ chmod +x rustup.sh && \
@ -38,10 +38,22 @@ RUN \
FROM arm32v7-builder as builder FROM arm32v7-builder as builder
USER root
RUN \
dpkg --add-architecture armhf && \
apt-get update && \
apt-get -y install libgexiv2-dev:armhf
USER build
ARG TAG=master ARG TAG=master
ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs
ARG BINARY=pict-rs ARG BINARY=pict-rs
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_PATH=/usr/lib/$TOOL/pkgconfig:/usr/lib/$PKGCONFIG
RUN \ RUN \
git clone -b $TAG $REPOSITORY repo git clone -b $TAG $REPOSITORY repo
@ -51,18 +63,26 @@ RUN \
cargo build --release --target $TARGET && \ cargo build --release --target $TARGET && \
$TOOL-strip target/$TARGET/release/$BINARY $TOOL-strip target/$TARGET/release/$BINARY
FROM arm32v7/alpine:3.11 FROM arm32v7/ubuntu:20.04
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ARG BINARY=pict-rs ARG BINARY=pict-rs
COPY --from=builder /opt/build/repo/target/arm-unknown-linux-musleabihf/release/$BINARY /usr/bin/$BINARY COPY --from=builder /opt/build/repo/target/arm-unknown-linux-gnueabihf/release/$BINARY /usr/bin/$BINARY
RUN \ RUN \
apk add tini && \ apt-get update && \
addgroup -g $GID pictrs && \ apt-get -y upgrade && \
adduser -D -G pictrs -u $UID -g "" -h /opt/pictrs pictrs apt-get -y install tini libgexiv2-2 && \
addgroup --gid $GID pictrs && \
adduser \
--disabled-password \
--gecos "" \
--ingroup pictrs \
--uid $UID \
--home /opt/pictrs \
pictrs
RUN \ RUN \
chown -R pictrs:pictrs /mnt chown -R pictrs:pictrs /mnt
@ -70,5 +90,5 @@ RUN \
VOLUME /mnt VOLUME /mnt
WORKDIR /opt/pictrs WORKDIR /opt/pictrs
USER pictrs USER pictrs
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"] CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"]

View file

@ -1,11 +1,11 @@
FROM rustembedded/cross:aarch64-unknown-linux-musl AS aarch64-builder FROM rustembedded/cross:aarch64-unknown-linux-gnu AS aarch64-builder
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ENV TOOLCHAIN=stable ENV TOOLCHAIN=stable
ENV TARGET=aarch64-unknown-linux-musl ENV TARGET=aarch64-unknown-linux-gnu
ENV TOOL=aarch64-linux-musl ENV TOOL=aarch64-linux-gnu
RUN \ RUN \
apt-get update && \ apt-get update && \
@ -29,7 +29,7 @@ RUN \
USER build USER build
WORKDIR /opt/build WORKDIR /opt/build
ENV PATH=/opt/build/.cargo/bin:/usr/local/musl/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV PATH=$PATH:/opt/build/.cargo/bin
RUN \ RUN \
chmod +x rustup.sh && \ chmod +x rustup.sh && \
@ -38,10 +38,22 @@ RUN \
FROM aarch64-builder as builder FROM aarch64-builder as builder
USER root
RUN \
dpkg --add-architecture arm64 && \
apt-get update && \
apt-get -y install libgexiv2-dev:arm64
USER build
ARG TAG=master ARG TAG=master
ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs ARG REPOSITORY=https://git.asonix.dog/asonix/pict-rs
ARG BINARY=pict-rs ARG BINARY=pict-rs
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_PATH=/usr/lib/$TOOL/pkgconfig:/usr/lib/$PKGCONFIG
RUN \ RUN \
git clone -b $TAG $REPOSITORY repo git clone -b $TAG $REPOSITORY repo
@ -51,18 +63,26 @@ RUN \
cargo build --release --target $TARGET && \ cargo build --release --target $TARGET && \
$TOOL-strip target/$TARGET/release/$BINARY $TOOL-strip target/$TARGET/release/$BINARY
FROM arm64v8/alpine:3.11 FROM arm64v8/ubuntu:20.04
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
ARG BINARY=pict-rs ARG BINARY=pict-rs
COPY --from=builder /opt/build/repo/target/aarch64-unknown-linux-musl/release/$BINARY /usr/bin/$BINARY COPY --from=builder /opt/build/repo/target/aarch64-unknown-linux-gnu/release/$BINARY /usr/bin/$BINARY
RUN \ RUN \
apk add tini && \ apt-get update && \
addgroup -g $GID pictrs && \ apt-get -y upgrade && \
adduser -D -G pictrs -u $UID -g "" -h /opt/pictrs pictrs apt-get -y install tini libgexiv2-2 && \
addgroup --gid $GID pictrs && \
adduser \
--disabled-password \
--gecos "" \
--ingroup pictrs \
--uid $UID \
--home /opt/pictrs \
pictrs
RUN \ RUN \
chown -R pictrs:pictrs /mnt chown -R pictrs:pictrs /mnt
@ -70,5 +90,5 @@ RUN \
VOLUME /mnt VOLUME /mnt
WORKDIR /opt/pictrs WORKDIR /opt/pictrs
USER pictrs USER pictrs
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"] CMD ["/usr/bin/pict-rs", "-p", "/mnt", "-a", "0.0.0.0:8080", "-w", "thumbnail"]

View file

@ -90,22 +90,6 @@ pub(crate) enum Format {
Png, Png,
} }
impl Format {
pub(crate) fn to_image_format(&self) -> image::ImageFormat {
match self {
Format::Jpeg => image::ImageFormat::Jpeg,
Format::Png => image::ImageFormat::Png,
}
}
pub(crate) fn to_mime(&self) -> mime::Mime {
match self {
Format::Jpeg => mime::IMAGE_JPEG,
Format::Png => mime::IMAGE_PNG,
}
}
}
impl std::str::FromStr for Format { impl std::str::FromStr for Format {
type Err = FormatError; type Err = FormatError;

View file

@ -39,9 +39,6 @@ pub(crate) enum UploadError {
#[error("Provided token did not match expected token")] #[error("Provided token did not match expected token")]
InvalidToken, InvalidToken,
#[error("Uploaded content could not be validated as an image")]
InvalidImage(image::error::ImageError),
#[error("Unsupported image format")] #[error("Unsupported image format")]
UnsupportedFormat, UnsupportedFormat,
@ -65,6 +62,12 @@ pub(crate) enum UploadError {
#[error("Error validating Gif file, {0}")] #[error("Error validating Gif file, {0}")]
Gif(#[from] GifError), Gif(#[from] GifError),
#[error("Tried to create file, but file already exists")]
FileExists,
#[error("File metadata could not be parsed, {0}")]
Validate(#[from] rexiv2::Rexiv2Error),
} }
impl From<actix_web::client::SendRequestError> for UploadError { impl From<actix_web::client::SendRequestError> for UploadError {

View file

@ -7,6 +7,7 @@ use actix_web::{
web, App, HttpResponse, HttpServer, web, App, HttpResponse, HttpServer,
}; };
use futures::stream::{Stream, TryStreamExt}; use futures::stream::{Stream, TryStreamExt};
use image::{ImageFormat, ImageOutputFormat};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{collections::HashSet, path::PathBuf}; use std::{collections::HashSet, path::PathBuf};
use structopt::StructOpt; use structopt::StructOpt;
@ -150,6 +151,7 @@ async fn download(
}))) })))
} }
/// Delete aliases and files
#[instrument(skip(manager))] #[instrument(skip(manager))]
async fn delete( async fn delete(
manager: web::Data<UploadManager>, manager: web::Data<UploadManager>,
@ -162,6 +164,16 @@ async fn delete(
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
fn convert_format(format: ImageFormat) -> Result<ImageOutputFormat, UploadError> {
match format {
ImageFormat::Jpeg => Ok(ImageOutputFormat::Jpeg(100)),
ImageFormat::Png => Ok(ImageOutputFormat::Png),
ImageFormat::Gif => Ok(ImageOutputFormat::Gif),
ImageFormat::Bmp => Ok(ImageOutputFormat::Bmp),
_ => Err(UploadError::UnsupportedFormat),
}
}
/// Serve files /// Serve files
#[instrument(skip(manager, whitelist))] #[instrument(skip(manager, whitelist))]
async fn serve( async fn serve(
@ -220,8 +232,8 @@ async fn serve(
debug!("Exporting image"); debug!("Exporting image");
let img_bytes: bytes::Bytes = web::block(move || { let img_bytes: bytes::Bytes = web::block(move || {
let mut bytes = std::io::Cursor::new(vec![]); let mut bytes = std::io::Cursor::new(vec![]);
img.write_to(&mut bytes, format)?; img.write_to(&mut bytes, convert_format(format)?)?;
Ok(bytes::Bytes::from(bytes.into_inner())) as Result<_, image::error::ImageError> Ok(bytes::Bytes::from(bytes.into_inner())) as Result<_, UploadError>
}) })
.await?; .await?;

View file

@ -1,6 +1,6 @@
use crate::{config::Format, error::UploadError, safe_save_file, to_ext, validate::validate_image}; use crate::{config::Format, error::UploadError, to_ext, validate::validate_image};
use actix_web::web; use actix_web::web;
use futures::stream::{Stream, StreamExt}; use futures::stream::{Stream, StreamExt, TryStreamExt};
use sha2::Digest; use sha2::Digest;
use std::{path::PathBuf, pin::Pin, sync::Arc}; use std::{path::PathBuf, pin::Pin, sync::Arc};
use tracing::{debug, error, info, instrument, warn, Span}; use tracing::{debug, error, info, instrument, warn, Span};
@ -272,29 +272,32 @@ impl UploadManager {
) -> Result<String, UploadError> ) -> Result<String, UploadError>
where where
UploadError: From<E>, UploadError: From<E>,
E: Unpin,
{ {
// -- READ IN BYTES FROM CLIENT --
debug!("Reading stream"); debug!("Reading stream");
let bytes = read_stream(stream).await?; let tmpfile = tmp_file();
safe_save_stream(tmpfile.clone(), stream).await?;
let (bytes, content_type) = if validate { let content_type = if validate {
debug!("Validating image"); debug!("Validating image");
let format = self.inner.format.clone(); let format = self.inner.format.clone();
validate_image(bytes, format).await? validate_image(tmpfile.clone(), format).await?
} else { } else {
(bytes, content_type) content_type
}; };
// -- DUPLICATE CHECKS -- // -- DUPLICATE CHECKS --
// Cloning bytes is fine because it's actually a pointer // Cloning bytes is fine because it's actually a pointer
debug!("Hashing bytes"); debug!("Hashing bytes");
let hash = self.hash(bytes.clone()).await?; let hash = self.hash(tmpfile.clone()).await?;
debug!("Storing alias"); debug!("Storing alias");
self.add_existing_alias(&hash, &alias).await?; self.add_existing_alias(&hash, &alias).await?;
debug!("Saving file"); debug!("Saving file");
self.save_upload(bytes, hash, content_type).await?; self.save_upload(tmpfile, hash, content_type).await?;
// Return alias to file // Return alias to file
Ok(alias) Ok(alias)
@ -305,27 +308,29 @@ impl UploadManager {
pub(crate) async fn upload<E>(&self, stream: UploadStream<E>) -> Result<String, UploadError> pub(crate) async fn upload<E>(&self, stream: UploadStream<E>) -> Result<String, UploadError>
where where
UploadError: From<E>, UploadError: From<E>,
E: Unpin,
{ {
// -- READ IN BYTES FROM CLIENT -- // -- READ IN BYTES FROM CLIENT --
debug!("Reading stream"); debug!("Reading stream");
let bytes = read_stream(stream).await?; let tmpfile = tmp_file();
safe_save_stream(tmpfile.clone(), stream).await?;
// -- VALIDATE IMAGE -- // -- VALIDATE IMAGE --
debug!("Validating image"); debug!("Validating image");
let format = self.inner.format.clone(); let format = self.inner.format.clone();
let (bytes, content_type) = validate_image(bytes, format).await?; let content_type = validate_image(tmpfile.clone(), format).await?;
// -- DUPLICATE CHECKS -- // -- DUPLICATE CHECKS --
// Cloning bytes is fine because it's actually a pointer // Cloning bytes is fine because it's actually a pointer
debug!("Hashing bytes"); debug!("Hashing bytes");
let hash = self.hash(bytes.clone()).await?; let hash = self.hash(tmpfile.clone()).await?;
debug!("Adding alias"); debug!("Adding alias");
let alias = self.add_alias(&hash, content_type.clone()).await?; let alias = self.add_alias(&hash, content_type.clone()).await?;
debug!("Saving file"); debug!("Saving file");
self.save_upload(bytes, hash, content_type).await?; self.save_upload(tmpfile, hash, content_type).await?;
// Return alias to file // Return alias to file
Ok(alias) Ok(alias)
@ -405,7 +410,7 @@ impl UploadManager {
// check duplicates & store image if new // check duplicates & store image if new
async fn save_upload( async fn save_upload(
&self, &self,
bytes: bytes::Bytes, tmpfile: PathBuf,
hash: Hash, hash: Hash,
content_type: mime::Mime, content_type: mime::Mime,
) -> Result<(), UploadError> { ) -> Result<(), UploadError> {
@ -421,19 +426,29 @@ impl UploadManager {
let mut real_path = self.image_dir(); let mut real_path = self.image_dir();
real_path.push(name); real_path.push(name);
safe_save_file(real_path, bytes).await?; safe_move_file(tmpfile, real_path).await?;
Ok(()) Ok(())
} }
// produce a sh256sum of the uploaded file // produce a sh256sum of the uploaded file
async fn hash(&self, bytes: bytes::Bytes) -> Result<Hash, UploadError> { async fn hash(&self, tmpfile: PathBuf) -> Result<Hash, UploadError> {
let mut hasher = self.inner.hasher.clone(); let mut hasher = self.inner.hasher.clone();
let hash = web::block(move || {
let mut stream = actix_fs::read_to_stream(tmpfile).await?;
while let Some(res) = stream.next().await {
let bytes = res?;
hasher = web::block(move || {
hasher.update(&bytes); hasher.update(&bytes);
Ok(hasher.finalize_reset().to_vec()) as Result<_, UploadError> Ok(hasher) as Result<_, UploadError>
}) })
.await?; .await?;
}
let hash =
web::block(move || Ok(hasher.finalize_reset().to_vec()) as Result<_, UploadError>)
.await?;
Ok(Hash::new(hash)) Ok(Hash::new(hash))
} }
@ -620,20 +635,68 @@ impl UploadManager {
} }
} }
#[instrument(skip(stream))] pub(crate) fn tmp_file() -> PathBuf {
async fn read_stream<E>(mut stream: UploadStream<E>) -> Result<bytes::Bytes, UploadError> use rand::distributions::{Alphanumeric, Distribution};
where let limit: usize = 10;
UploadError: From<E>, let rng = rand::thread_rng();
{
let mut bytes = bytes::BytesMut::new();
while let Some(res) = stream.next().await { let s: String = Alphanumeric.sample_iter(rng).take(limit).collect();
let new = res?;
debug!("Extending with {} bytes", new.len()); let name = format!("{}.tmp", s);
bytes.extend(new);
let mut path = std::env::temp_dir();
path.push("pict-rs");
path.push(&name);
path
}
#[instrument]
async fn safe_move_file(from: PathBuf, to: PathBuf) -> Result<(), UploadError> {
if let Some(path) = to.parent() {
debug!("Creating directory {:?}", path);
actix_fs::create_dir_all(path.to_owned()).await?;
} }
Ok(bytes.freeze()) debug!("Checking if {:?} already exists", to);
if let Err(e) = actix_fs::metadata(to.clone()).await {
if e.kind() != Some(std::io::ErrorKind::NotFound) {
return Err(e.into());
}
} else {
return Err(UploadError::FileExists);
}
debug!("Moving {:?} to {:?}", from, to);
actix_fs::rename(from, to).await?;
Ok(())
}
#[instrument(skip(stream))]
async fn safe_save_stream<E>(to: PathBuf, stream: UploadStream<E>) -> Result<(), UploadError>
where
UploadError: From<E>,
E: Unpin,
{
if let Some(path) = to.parent() {
debug!("Creating directory {:?}", path);
actix_fs::create_dir_all(path.to_owned()).await?;
}
debug!("Checking if {:?} alreayd exists", to);
if let Err(e) = actix_fs::metadata(to.clone()).await {
if e.kind() != Some(std::io::ErrorKind::NotFound) {
return Err(e.into());
}
} else {
return Err(UploadError::FileExists);
}
debug!("Writing stream to {:?}", to);
let stream = stream.err_into::<UploadError>();
actix_fs::write_stream(to, stream).await?;
Ok(())
} }
async fn remove_path(path: sled::IVec) -> Result<(), UploadError> { async fn remove_path(path: sled::IVec) -> Result<(), UploadError> {

View file

@ -1,9 +1,13 @@
use crate::{config::Format, error::UploadError}; use crate::{config::Format, error::UploadError, upload_manager::tmp_file};
use actix_web::web; use actix_web::web;
use bytes::Bytes; use image::{io::Reader, ImageDecoder, ImageEncoder, ImageFormat};
use image::{ImageDecoder, ImageEncoder, ImageFormat}; use rexiv2::{MediaType, Metadata};
use std::io::Cursor; use std::{
use tracing::{debug, instrument, Span}; fs::File,
io::{BufReader, BufWriter, Write},
path::PathBuf,
};
use tracing::{debug, instrument, trace, Span};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub(crate) enum GifError { pub(crate) enum GifError {
@ -15,86 +19,119 @@ pub(crate) enum GifError {
} }
// import & export image using the image crate // import & export image using the image crate
#[instrument(skip(bytes, prescribed_format))] #[instrument]
pub(crate) async fn validate_image( pub(crate) async fn validate_image(
bytes: Bytes, tmpfile: PathBuf,
prescribed_format: Option<Format>, prescribed_format: Option<Format>,
) -> Result<(Bytes, mime::Mime), UploadError> { ) -> Result<mime::Mime, UploadError> {
let span = Span::current(); let span = Span::current();
let tup = web::block(move || { let content_type = web::block(move || {
let entered = span.enter(); let entered = span.enter();
if let Some(prescribed) = prescribed_format {
debug!("Load from memory");
let img = image::load_from_memory(&bytes).map_err(UploadError::InvalidImage)?;
debug!("Loaded");
let mime = prescribed.to_mime(); let meta = Metadata::new_from_path(&tmpfile)?;
debug!("Writing"); let content_type = match (prescribed_format, meta.get_media_type()?) {
let mut bytes = Cursor::new(vec![]); (_, MediaType::Gif) => {
img.write_to(&mut bytes, prescribed.to_image_format())?; let newfile = tmp_file();
debug!("Written"); validate_gif(&tmpfile, &newfile)?;
return Ok((Bytes::from(bytes.into_inner()), mime));
mime::IMAGE_GIF
} }
(Some(Format::Jpeg), MediaType::Jpeg) | (None, MediaType::Jpeg) => {
validate(&tmpfile, ImageFormat::Jpeg)?;
let format = image::guess_format(&bytes).map_err(UploadError::InvalidImage)?; meta.clear();
meta.save_to_file(&tmpfile)?;
debug!("Validating {:?}", format); mime::IMAGE_JPEG
let res = match format { }
ImageFormat::Png => Ok((validate_png(bytes)?, mime::IMAGE_PNG)), (Some(Format::Png), MediaType::Png) | (None, MediaType::Png) => {
ImageFormat::Jpeg => Ok((validate_jpg(bytes)?, mime::IMAGE_JPEG)), validate(&tmpfile, ImageFormat::Png)?;
ImageFormat::Bmp => Ok((validate_bmp(bytes)?, mime::IMAGE_BMP)),
ImageFormat::Gif => Ok((validate_gif(bytes)?, mime::IMAGE_GIF)), meta.clear();
_ => Err(UploadError::UnsupportedFormat), meta.save_to_file(&tmpfile)?;
mime::IMAGE_PNG
}
(Some(Format::Jpeg), _) => {
let newfile = tmp_file();
convert(&tmpfile, &newfile, ImageFormat::Jpeg)?;
mime::IMAGE_JPEG
}
(Some(Format::Png), _) => {
let newfile = tmp_file();
convert(&tmpfile, &newfile, ImageFormat::Png)?;
mime::IMAGE_PNG
}
(_, MediaType::Bmp) => {
let newfile = tmp_file();
validate_bmp(&tmpfile, &newfile)?;
mime::IMAGE_BMP
}
_ => return Err(UploadError::UnsupportedFormat),
}; };
debug!("Validated");
drop(entered); drop(entered);
res Ok(content_type) as Result<mime::Mime, UploadError>
}) })
.await?; .await?;
Ok(tup) Ok(content_type)
} }
#[instrument(skip(bytes))] #[instrument]
fn validate_png(bytes: Bytes) -> Result<Bytes, UploadError> { fn convert(from: &PathBuf, to: &PathBuf, format: ImageFormat) -> Result<(), UploadError> {
let decoder = image::png::PngDecoder::new(Cursor::new(&bytes))?; debug!("Converting");
let reader = Reader::new(BufReader::new(File::open(from)?)).with_guessed_format()?;
let mut bytes = Cursor::new(vec![]); if reader.format() != Some(format) {
let encoder = image::png::PNGEncoder::new(&mut bytes); return Err(UploadError::UnsupportedFormat);
}
let img = reader.decode()?;
img.save_with_format(to, format)?;
std::fs::rename(to, from)?;
Ok(())
}
#[instrument]
fn validate(path: &PathBuf, format: ImageFormat) -> Result<(), UploadError> {
debug!("Validating");
let reader = Reader::new(BufReader::new(File::open(path)?)).with_guessed_format()?;
if reader.format() != Some(format) {
return Err(UploadError::UnsupportedFormat);
}
reader.decode()?;
Ok(())
}
#[instrument]
fn validate_bmp(from: &PathBuf, to: &PathBuf) -> Result<(), UploadError> {
debug!("Transmuting BMP");
let decoder = image::bmp::BmpDecoder::new(BufReader::new(File::open(from)?))?;
let mut writer = BufWriter::new(File::create(to)?);
let encoder = image::bmp::BMPEncoder::new(&mut writer);
validate_still_image(decoder, encoder)?; validate_still_image(decoder, encoder)?;
Ok(Bytes::from(bytes.into_inner())) writer.flush()?;
std::fs::rename(to, from)?;
Ok(())
} }
#[instrument(skip(bytes))] #[instrument]
fn validate_jpg(bytes: Bytes) -> Result<Bytes, UploadError> { fn validate_gif(from: &PathBuf, to: &PathBuf) -> Result<(), GifError> {
let decoder = image::jpeg::JpegDecoder::new(Cursor::new(&bytes))?; debug!("Transmuting GIF");
let mut bytes = Cursor::new(vec![]);
let encoder = image::jpeg::JPEGEncoder::new(&mut bytes);
validate_still_image(decoder, encoder)?;
Ok(Bytes::from(bytes.into_inner()))
}
#[instrument(skip(bytes))]
fn validate_bmp(bytes: Bytes) -> Result<Bytes, UploadError> {
let decoder = image::bmp::BmpDecoder::new(Cursor::new(&bytes))?;
let mut bytes = Cursor::new(vec![]);
let encoder = image::bmp::BMPEncoder::new(&mut bytes);
validate_still_image(decoder, encoder)?;
Ok(Bytes::from(bytes.into_inner()))
}
#[instrument(skip(bytes))]
fn validate_gif(bytes: Bytes) -> Result<Bytes, GifError> {
use gif::{Parameter, SetParameter}; use gif::{Parameter, SetParameter};
let mut decoder = gif::Decoder::new(Cursor::new(&bytes)); let mut decoder = gif::Decoder::new(BufReader::new(File::open(from)?));
decoder.set(gif::ColorOutput::Indexed); decoder.set(gif::ColorOutput::Indexed);
@ -104,19 +141,21 @@ fn validate_gif(bytes: Bytes) -> Result<Bytes, GifError> {
let height = reader.height(); let height = reader.height();
let global_palette = reader.global_palette().unwrap_or(&[]); let global_palette = reader.global_palette().unwrap_or(&[]);
let mut bytes = Cursor::new(vec![]); let mut writer = BufWriter::new(File::create(to)?);
{ let mut encoder = gif::Encoder::new(&mut writer, width, height, global_palette)?;
let mut encoder = gif::Encoder::new(&mut bytes, width, height, global_palette)?;
gif::Repeat::Infinite.set_param(&mut encoder)?; gif::Repeat::Infinite.set_param(&mut encoder)?;
while let Some(frame) = reader.read_next_frame()? { while let Some(frame) = reader.read_next_frame()? {
debug!("Writing frame"); trace!("Writing frame");
encoder.write_frame(frame)?; encoder.write_frame(frame)?;
} }
}
Ok(Bytes::from(bytes.into_inner())) drop(encoder);
writer.flush()?;
std::fs::rename(to, from)?;
Ok(())
} }
fn validate_still_image<'a, D, E>(decoder: D, encoder: E) -> Result<(), UploadError> fn validate_still_image<'a, D, E>(decoder: D, encoder: E) -> Result<(), UploadError>