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
This commit is contained in:
Tobias Schmitz 2021-06-05 11:24:25 +02:00 committed by GitHub
parent f104355076
commit d01583b406
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 133 additions and 27 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ TestConfig.toml
# Runtime artifacts
*.sqlite
polaris.log
polaris.pid
/thumbnails
# Release process artifacts (usually runs on CI)

View file

@ -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",

View file

@ -9,7 +9,10 @@ pub fn generate_thumbnail(image_path: &Path, options: &Options) -> Result<Dynami
let source_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8());
let (source_width, source_height) = source_image.dimensions();
let largest_dimension = cmp::max(source_width, source_height);
let out_dimension = cmp::min(options.max_dimension, largest_dimension);
let out_dimension = cmp::min(
options.max_dimension.unwrap_or(largest_dimension),
largest_dimension,
);
let source_aspect_ratio: f32 = source_width as f32 / source_height as f32;
let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2;

View file

@ -1,6 +1,6 @@
#[derive(Debug, Hash)]
pub struct Options {
pub max_dimension: u32,
pub max_dimension: Option<u32>,
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,
}

View file

@ -703,8 +703,7 @@ async fn get_thumbnail(
path: web::Path<String>,
options_input: web::Query<dto::ThumbnailOptions>,
) -> Result<NamedFile, APIError> {
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()?;

View file

@ -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<ThumbnailSize>,
pub pad: Option<bool>,
}
impl From<ThumbnailOptions> 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<Option<u32>> for ThumbnailSize {
fn into(self) -> Option<u32> {
match self {
Self::Small => Some(400),
Self::Large => Some(1200),
Self::Native => None,
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ListPlaylistsEntry {
pub name: String,

View file

@ -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<ThumbnailSize>, pad: Option<bool>, 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);
}

View file

@ -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<bool>) -> Request<()> {
pub fn thumbnail(path: &Path, size: Option<ThumbnailSize>, pad: Option<bool>) -> 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)