From d01583b4069931343cf34f3c3fcc00093812214d Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Sat, 5 Jun 2021 11:24:25 +0200 Subject: [PATCH] add api parameter for thumbnail size (#144) * add api parameter for thumbnail size * make max_dimension optinal in case of native resolution * add tests for thumbnail size * fix typo * fix thumbnail size tests * make unwrap more explicit * remove print statement * update workflows * reduce thumbnail variations * add removed token * Update coverage.yml * fix typo * hopefully prevent coverage timeout - split up thumnail tests - reduce threadcount used for test execution * get thread count using github actions specific step * use fixed thread count of 4 * run coverage tests in release mode * ignore large and native thumbnail_size tests in coverage --- .github/workflows/build.yml | 2 +- .github/workflows/coverage.yml | 12 ++++---- .github/workflows/release.yml | 6 ++-- .gitignore | 1 + docs/swagger/polaris-api.json | 10 +++++++ src/app/thumbnail/generate.rs | 5 +++- src/app/thumbnail/options.rs | 4 +-- src/service/actix/api.rs | 3 +- src/service/dto.rs | 30 +++++++++++++++++++- src/service/test/media.rs | 51 ++++++++++++++++++++++++++++++++-- src/service/test/protocol.rs | 36 ++++++++++++++++++------ 11 files changed, 133 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1cb45f..cce54b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Install libsqlite3-dev if: contains(matrix.os, 'ubuntu') && !contains(matrix.features, 'bundle-sqlite') run: sudo apt-get update && sudo apt-get install libsqlite3-dev - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7a4db5d..e513267 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,14 +15,16 @@ jobs: steps: - name: Checkout Polaris - uses: actions/checkout@v1 + uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal - - name: Install Tarpaulin - run: cargo install cargo-tarpaulin - - name: Run Tests - run: cargo tarpaulin --all-features --ignore-tests --out Xml + - name: Run Tarpaulin + uses: actions-rs/tarpaulin@v0.1 + with: + args: '--all-features --ignore-tests' + out-type: Xml + timeout: 240 - name: Upload Results uses: codecov/codecov-action@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25032a0..02855f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: target_branch: release github_token: ${{ secrets.GITHUB_TOKEN }} - name: Checkout Release Branch - uses: actions/checkout@master + uses: actions/checkout@v2 with: ref: release - name: Update Polaris Version in Cargo.toml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout Polaris - uses: actions/checkout@v1 + uses: actions/checkout@v2 with: ref: release - name: Install Rust Toolchain @@ -106,7 +106,7 @@ jobs: steps: - name: Checkout Polaris - uses: actions/checkout@v1 + uses: actions/checkout@v2 with: ref: release - name: Make release diff --git a/.gitignore b/.gitignore index 3bc29b8..0ae085c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ TestConfig.toml # Runtime artifacts *.sqlite polaris.log +polaris.pid /thumbnails # Release process artifacts (usually runs on CI) diff --git a/docs/swagger/polaris-api.json b/docs/swagger/polaris-api.json index ed38f24..a920ac5 100644 --- a/docs/swagger/polaris-api.json +++ b/docs/swagger/polaris-api.json @@ -791,6 +791,16 @@ "type": "string" } }, + { + "name": "size", + "in": "query", + "description": "The maximum size of the thumbnail, either small (400x400), large (1200x1200) or native", + "schema": { + "type": "string", + "enum": ["small", "large", "native"], + "default": "small" + } + }, { "name": "pad", "in": "query", diff --git a/src/app/thumbnail/generate.rs b/src/app/thumbnail/generate.rs index 4698fdb..94c56bc 100644 --- a/src/app/thumbnail/generate.rs +++ b/src/app/thumbnail/generate.rs @@ -9,7 +9,10 @@ pub fn generate_thumbnail(image_path: &Path, options: &Options) -> Result 0.8 && source_aspect_ratio < 1.2; diff --git a/src/app/thumbnail/options.rs b/src/app/thumbnail/options.rs index 281e2e4..3219245 100644 --- a/src/app/thumbnail/options.rs +++ b/src/app/thumbnail/options.rs @@ -1,6 +1,6 @@ #[derive(Debug, Hash)] pub struct Options { - pub max_dimension: u32, + pub max_dimension: Option, pub resize_if_almost_square: bool, pub pad_to_square: bool, } @@ -8,7 +8,7 @@ pub struct Options { impl Default for Options { fn default() -> Self { Self { - max_dimension: 400, + max_dimension: Some(400), resize_if_almost_square: true, pad_to_square: true, } diff --git a/src/service/actix/api.rs b/src/service/actix/api.rs index 7097546..fdc7659 100644 --- a/src/service/actix/api.rs +++ b/src/service/actix/api.rs @@ -703,8 +703,7 @@ async fn get_thumbnail( path: web::Path, options_input: web::Query, ) -> Result { - let mut options = thumbnail::Options::default(); - options.pad_to_square = options_input.pad.unwrap_or(options.pad_to_square); + let options = thumbnail::Options::from(options_input.0); let thumbnail_path = block(move || { let vfs = vfs_manager.get_vfs()?; diff --git a/src/service/dto.rs b/src/service/dto.rs index 75d4769..b5fa39c 100644 --- a/src/service/dto.rs +++ b/src/service/dto.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::app::{config, ddns, settings, user, vfs}; +use crate::app::{config, ddns, settings, thumbnail, user, vfs}; pub const API_MAJOR_VERSION: i32 = 6; pub const API_MINOR_VERSION: i32 = 0; @@ -39,9 +39,37 @@ pub struct AuthQueryParameters { #[derive(Serialize, Deserialize)] pub struct ThumbnailOptions { + pub size: Option, pub pad: Option, } +impl From for thumbnail::Options { + fn from(dto: ThumbnailOptions) -> Self { + let mut options = thumbnail::Options::default(); + options.max_dimension = dto.size.map_or(options.max_dimension, Into::into); + options.pad_to_square = dto.pad.unwrap_or(options.pad_to_square); + options + } +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ThumbnailSize { + Small, + Large, + Native, +} + +impl Into> for ThumbnailSize { + fn into(self) -> Option { + match self { + Self::Small => Some(400), + Self::Large => Some(1200), + Self::Native => None, + } + } +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ListPlaylistsEntry { pub name: String, diff --git a/src/service/test/media.rs b/src/service/test/media.rs index 9c2bf61..9c171b9 100644 --- a/src/service/test/media.rs +++ b/src/service/test/media.rs @@ -1,6 +1,7 @@ use http::{header, HeaderValue, StatusCode}; use std::path::PathBuf; +use crate::service::dto::ThumbnailSize; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -84,8 +85,9 @@ fn thumbnail_requires_auth() { .iter() .collect(); + let size = None; let pad = None; - let request = protocol::thumbnail(&path, pad); + let request = protocol::thumbnail(&path, size, pad); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -102,8 +104,9 @@ fn thumbnail_golden_path() { .iter() .collect(); + let size = None; let pad = None; - let request = protocol::thumbnail(&path, pad); + let request = protocol::thumbnail(&path, size, pad); let response = service.fetch_bytes(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -116,8 +119,50 @@ fn thumbnail_bad_path_returns_not_found() { let path: PathBuf = ["not_my_collection"].iter().collect(); + let size = None; let pad = None; - let request = protocol::thumbnail(&path, pad); + let request = protocol::thumbnail(&path, size, pad); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } + +#[test] +fn thumbnail_size_default() { + thumbnail_size(&test_name!(), None, None, 400); +} + +#[test] +fn thumbnail_size_small() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400); +} + +#[test] +#[cfg(not(tarpaulin))] +fn thumbnail_size_large() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200); +} + +#[test] +#[cfg(not(tarpaulin))] +fn thumbnail_size_native() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423); +} + +fn thumbnail_size(name: &str, size: Option, pad: Option, expected: u32) { + let mut service = ServiceType::new(name); + service.complete_initial_setup(); + service.login_admin(); + service.index(); + service.login(); + + let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic", "Folder.png"] + .iter() + .collect(); + + let request = protocol::thumbnail(&path, size, pad); + let response = service.fetch_bytes(&request); + assert_eq!(response.status(), StatusCode::OK); + let thumbnail = image::load_from_memory(response.body()).unwrap().to_rgb8(); + assert_eq!(thumbnail.width(), expected); + assert_eq!(thumbnail.height(), expected); +} diff --git a/src/service/test/protocol.rs b/src/service/test/protocol.rs index 1123ae7..0ff2b7a 100644 --- a/src/service/test/protocol.rs +++ b/src/service/test/protocol.rs @@ -1,9 +1,9 @@ -use http::{method::Method, Request}; +use http::{Method, Request}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use std::path::Path; -use crate::app::user; use crate::service::dto; +use crate::{app::user, service::dto::ThumbnailSize}; pub fn web_index() -> Request<()> { Request::builder() @@ -200,14 +200,32 @@ pub fn audio(path: &Path) -> Request<()> { .unwrap() } -pub fn thumbnail(path: &Path, pad: Option) -> Request<()> { +pub fn thumbnail(path: &Path, size: Option, pad: Option) -> Request<()> { let path = path.to_string_lossy(); - let mut endpoint = format!("/api/thumbnail/{}", url_encode(path.as_ref())); - match pad { - Some(true) => endpoint.push_str("?pad=true"), - Some(false) => endpoint.push_str("?pad=false"), - None => (), - }; + let mut params = String::new(); + if let Some(s) = size { + params.push('?'); + match s { + ThumbnailSize::Small => params.push_str("size=small"), + ThumbnailSize::Large => params.push_str("size=large"), + ThumbnailSize::Native => params.push_str("size=native"), + }; + } + if let Some(p) = pad { + if params.is_empty() { + params.push('?'); + } else { + params.push('&'); + } + if p { + params.push_str("pad=true"); + } else { + params.push_str("pad=false"); + }; + } + + let endpoint = format!("/api/thumbnail/{}{}", url_encode(path.as_ref()), params); + Request::builder() .method(Method::GET) .uri(&endpoint)