mirror of
https://github.com/agersant/polaris
synced 2024-11-10 02:04:13 +00:00
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:
parent
f104355076
commit
d01583b406
11 changed files with 133 additions and 27 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
- name: Install libsqlite3-dev
|
- name: Install libsqlite3-dev
|
||||||
if: contains(matrix.os, 'ubuntu') && !contains(matrix.features, 'bundle-sqlite')
|
if: contains(matrix.os, 'ubuntu') && !contains(matrix.features, 'bundle-sqlite')
|
||||||
run: sudo apt-get update && sudo apt-get install libsqlite3-dev
|
run: sudo apt-get update && sudo apt-get install libsqlite3-dev
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
|
|
12
.github/workflows/coverage.yml
vendored
12
.github/workflows/coverage.yml
vendored
|
@ -15,14 +15,16 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Polaris
|
- name: Checkout Polaris
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
- name: Install Tarpaulin
|
- name: Run Tarpaulin
|
||||||
run: cargo install cargo-tarpaulin
|
uses: actions-rs/tarpaulin@v0.1
|
||||||
- name: Run Tests
|
with:
|
||||||
run: cargo tarpaulin --all-features --ignore-tests --out Xml
|
args: '--all-features --ignore-tests'
|
||||||
|
out-type: Xml
|
||||||
|
timeout: 240
|
||||||
- name: Upload Results
|
- name: Upload Results
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
|
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
target_branch: release
|
target_branch: release
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Checkout Release Branch
|
- name: Checkout Release Branch
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: release
|
ref: release
|
||||||
- name: Update Polaris Version in Cargo.toml
|
- name: Update Polaris Version in Cargo.toml
|
||||||
|
@ -70,7 +70,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Polaris
|
- name: Checkout Polaris
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: release
|
ref: release
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
|
@ -106,7 +106,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Polaris
|
- name: Checkout Polaris
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: release
|
ref: release
|
||||||
- name: Make release
|
- name: Make release
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,6 +10,7 @@ TestConfig.toml
|
||||||
# Runtime artifacts
|
# Runtime artifacts
|
||||||
*.sqlite
|
*.sqlite
|
||||||
polaris.log
|
polaris.log
|
||||||
|
polaris.pid
|
||||||
/thumbnails
|
/thumbnails
|
||||||
|
|
||||||
# Release process artifacts (usually runs on CI)
|
# Release process artifacts (usually runs on CI)
|
||||||
|
|
|
@ -791,6 +791,16 @@
|
||||||
"type": "string"
|
"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",
|
"name": "pad",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
|
|
|
@ -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_image = DynamicImage::ImageRgb8(read(image_path)?.into_rgb8());
|
||||||
let (source_width, source_height) = source_image.dimensions();
|
let (source_width, source_height) = source_image.dimensions();
|
||||||
let largest_dimension = cmp::max(source_width, source_height);
|
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 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;
|
let is_almost_square = source_aspect_ratio > 0.8 && source_aspect_ratio < 1.2;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
pub max_dimension: u32,
|
pub max_dimension: Option<u32>,
|
||||||
pub resize_if_almost_square: bool,
|
pub resize_if_almost_square: bool,
|
||||||
pub pad_to_square: bool,
|
pub pad_to_square: bool,
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ pub struct Options {
|
||||||
impl Default for Options {
|
impl Default for Options {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_dimension: 400,
|
max_dimension: Some(400),
|
||||||
resize_if_almost_square: true,
|
resize_if_almost_square: true,
|
||||||
pad_to_square: true,
|
pad_to_square: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -703,8 +703,7 @@ async fn get_thumbnail(
|
||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
options_input: web::Query<dto::ThumbnailOptions>,
|
options_input: web::Query<dto::ThumbnailOptions>,
|
||||||
) -> Result<NamedFile, APIError> {
|
) -> Result<NamedFile, APIError> {
|
||||||
let mut options = thumbnail::Options::default();
|
let options = thumbnail::Options::from(options_input.0);
|
||||||
options.pad_to_square = options_input.pad.unwrap_or(options.pad_to_square);
|
|
||||||
|
|
||||||
let thumbnail_path = block(move || {
|
let thumbnail_path = block(move || {
|
||||||
let vfs = vfs_manager.get_vfs()?;
|
let vfs = vfs_manager.get_vfs()?;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
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_MAJOR_VERSION: i32 = 6;
|
||||||
pub const API_MINOR_VERSION: i32 = 0;
|
pub const API_MINOR_VERSION: i32 = 0;
|
||||||
|
@ -39,9 +39,37 @@ pub struct AuthQueryParameters {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ThumbnailOptions {
|
pub struct ThumbnailOptions {
|
||||||
|
pub size: Option<ThumbnailSize>,
|
||||||
pub pad: Option<bool>,
|
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)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct ListPlaylistsEntry {
|
pub struct ListPlaylistsEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use http::{header, HeaderValue, StatusCode};
|
use http::{header, HeaderValue, StatusCode};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::service::dto::ThumbnailSize;
|
||||||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||||
use crate::test_name;
|
use crate::test_name;
|
||||||
|
|
||||||
|
@ -84,8 +85,9 @@ fn thumbnail_requires_auth() {
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let size = None;
|
||||||
let pad = None;
|
let pad = None;
|
||||||
let request = protocol::thumbnail(&path, pad);
|
let request = protocol::thumbnail(&path, size, pad);
|
||||||
let response = service.fetch(&request);
|
let response = service.fetch(&request);
|
||||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
@ -102,8 +104,9 @@ fn thumbnail_golden_path() {
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let size = None;
|
||||||
let pad = None;
|
let pad = None;
|
||||||
let request = protocol::thumbnail(&path, pad);
|
let request = protocol::thumbnail(&path, size, pad);
|
||||||
let response = service.fetch_bytes(&request);
|
let response = service.fetch_bytes(&request);
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
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 path: PathBuf = ["not_my_collection"].iter().collect();
|
||||||
|
|
||||||
|
let size = None;
|
||||||
let pad = None;
|
let pad = None;
|
||||||
let request = protocol::thumbnail(&path, pad);
|
let request = protocol::thumbnail(&path, size, pad);
|
||||||
let response = service.fetch(&request);
|
let response = service.fetch(&request);
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use http::{method::Method, Request};
|
use http::{Method, Request};
|
||||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::app::user;
|
|
||||||
use crate::service::dto;
|
use crate::service::dto;
|
||||||
|
use crate::{app::user, service::dto::ThumbnailSize};
|
||||||
|
|
||||||
pub fn web_index() -> Request<()> {
|
pub fn web_index() -> Request<()> {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
|
@ -200,14 +200,32 @@ pub fn audio(path: &Path) -> Request<()> {
|
||||||
.unwrap()
|
.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 path = path.to_string_lossy();
|
||||||
let mut endpoint = format!("/api/thumbnail/{}", url_encode(path.as_ref()));
|
let mut params = String::new();
|
||||||
match pad {
|
if let Some(s) = size {
|
||||||
Some(true) => endpoint.push_str("?pad=true"),
|
params.push('?');
|
||||||
Some(false) => endpoint.push_str("?pad=false"),
|
match s {
|
||||||
None => (),
|
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()
|
Request::builder()
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.uri(&endpoint)
|
.uri(&endpoint)
|
||||||
|
|
Loading…
Reference in a new issue