Migrate to actix-web 4 (#171)

* Migrate to actix-web 4

* Change expected swagger test status code

* update tokio to 1.0

* fix clippy warnings
This commit is contained in:
Tobias Schmitz 2022-04-24 22:55:38 +02:00 committed by GitHub
parent 90fd6bbcc9
commit 374d0ca56f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 707 additions and 1001 deletions

1550
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,39 +11,41 @@ bundle-sqlite = ["libsqlite3-sys"]
ui = ["native-windows-gui", "native-windows-derive"] ui = ["native-windows-gui", "native-windows-derive"]
[dependencies] [dependencies]
actix-files = { version = "0.5" } actix-files = { version = "0.6" }
actix-web = { version = "3" } actix-web = { version = "4" }
actix-web-httpauth = { version = "0.5.1" } actix-web-httpauth = { version = "0.6" }
anyhow = "1.0.52" actix-test = "=0.1.0-beta.13"
anyhow = "1.0.56"
ape = "0.4.0" ape = "0.4.0"
base64 = "0.13" base64 = "0.13"
branca = "0.10.0" branca = "0.10.1"
cookie = { version = "0.14", features = ["signed", "key-expansion"] } cookie = { version = "0.16", features = ["signed", "key-expansion"] }
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
diesel_migrations = { version = "1.4", features = ["sqlite"] } diesel_migrations = { version = "1.4", features = ["sqlite"] }
futures-util = { version = "0.3" } futures-util = { version = "0.3" }
getopts = "0.2.21" getopts = "0.2.21"
http = "0.2.6" http = "0.2.6"
id3 = "1.0.2" id3 = "1.0.2"
libsqlite3-sys = { version = "0.22", features = ["bundled", "bundled-windows"], optional = true }
lewton = "0.10.2" lewton = "0.10.2"
libsqlite3-sys = { version = "0.22", features = ["bundled", "bundled-windows"], optional = true }
log = "0.4.14" log = "0.4.14"
metaflac = "0.2.5" metaflac = "0.2.5"
mp3-duration = "0.1.10" mp3-duration = "0.1.10"
mp4ameta = "0.11.0" mp4ameta = "0.11.0"
num_cpus = "1.13.1" num_cpus = "1.13.1"
opus_headers = "0.1.2" opus_headers = "0.1.2"
pbkdf2 = "0.11"
percent-encoding = "2.1" percent-encoding = "2.1"
pbkdf2 = "0.10"
rand = "0.8" rand = "0.8"
rayon = "1.5" rayon = "1.5"
regex = "1.5.4" regex = "1.5.5"
rustfm-scrobble = "1.1.1" rustfm-scrobble = "1.1.1"
serde = { version = "1.0.133", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
serde_derive = "1.0.133" serde_derive = "1.0.136"
serde_json = "1.0.74" serde_json = "1.0.79"
simplelog = "0.11.1" simplelog = "0.11.2"
thiserror = "1.0.30" thiserror = "1.0.30"
tokio = "1.0"
toml = "0.5" toml = "0.5"
ureq = "1.5.5" ureq = "1.5.5"
url = "2.2" url = "2.2"
@ -54,7 +56,7 @@ default_features = false
features = ["libsqlite3-sys", "r2d2", "sqlite", "64-column-tables"] features = ["libsqlite3-sys", "r2d2", "sqlite", "64-column-tables"]
[dependencies.image] [dependencies.image]
version = "0.23.14" version = "0.24.1"
default_features = false default_features = false
features = ["bmp", "gif", "jpeg", "png"] features = ["bmp", "gif", "jpeg", "png"]
@ -64,7 +66,7 @@ native-windows-derive = {version = "1.0.4", optional = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
daemonize = "0.4.1" daemonize = "0.4.1"
sd-notify = "0.3.0" sd-notify = "0.4.0"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"

View file

@ -33,13 +33,13 @@ impl Manager {
if let Some(mount_dirs) = &config.mount_dirs { if let Some(mount_dirs) = &config.mount_dirs {
self.vfs_manager self.vfs_manager
.set_mount_dirs(&mount_dirs) .set_mount_dirs(mount_dirs)
.map_err(|_| Error::Unspecified)?; .map_err(|_| Error::Unspecified)?;
} }
if let Some(ddns_config) = &config.ydns { if let Some(ddns_config) = &config.ydns {
self.ddns_manager self.ddns_manager
.set_config(&ddns_config) .set_config(ddns_config)
.map_err(|_| Error::Unspecified)?; .map_err(|_| Error::Unspecified)?;
} }

View file

@ -32,7 +32,7 @@ impl Index {
settings_manager, settings_manager,
pending_reindex: Arc::new(( pending_reindex: Arc::new((
#[allow(clippy::clippy::mutex_atomic)] #[allow(clippy::mutex_atomic)]
Mutex::new(false), Mutex::new(false),
Condvar::new(), Condvar::new(),
)), )),

View file

@ -29,7 +29,7 @@ impl Manager {
fn get_thumbnail_path(&self, image_path: &Path, thumbnailoptions: &Options) -> PathBuf { fn get_thumbnail_path(&self, image_path: &Path, thumbnailoptions: &Options) -> PathBuf {
let hash = Manager::hash(image_path, thumbnailoptions); let hash = Manager::hash(image_path, thumbnailoptions);
let mut thumbnail_path = self.thumbnails_dir_path.clone(); let mut thumbnail_path = self.thumbnails_dir_path.clone();
thumbnail_path.push(format!("{}.jpg", hash.to_string())); thumbnail_path.push(format!("{}.jpg", hash));
thumbnail_path thumbnail_path
} }

View file

@ -40,19 +40,19 @@ fn read_flac(path: &Path) -> Result<DynamicImage> {
fn read_mp3(path: &Path) -> Result<DynamicImage> { fn read_mp3(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_path(path)?; let tag = id3::Tag::read_from_path(path)?;
read_id3(&path, &tag) read_id3(path, &tag)
} }
fn read_aiff(path: &Path) -> Result<DynamicImage> { fn read_aiff(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_aiff_path(path)?; let tag = id3::Tag::read_from_aiff_path(path)?;
read_id3(&path, &tag) read_id3(path, &tag)
} }
fn read_wave(path: &Path) -> Result<DynamicImage> { fn read_wave(path: &Path) -> Result<DynamicImage> {
let tag = id3::Tag::read_from_wav_path(path)?; let tag = id3::Tag::read_from_wav_path(path)?;
read_id3(&path, &tag) read_id3(path, &tag)
} }
fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage> { fn read_id3(path: &Path, tag: &id3::Tag) -> Result<DynamicImage> {

View file

@ -1,18 +1,19 @@
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_web::body::{BoxBody, MessageBody};
use actix_web::http::header::ContentEncoding;
use actix_web::{ use actix_web::{
client::HttpError,
delete, delete,
dev::{BodyEncoding, MessageBody, Payload, Service, ServiceRequest, ServiceResponse}, dev::{Payload, Service, ServiceRequest, ServiceResponse},
error::{BlockingError, ErrorForbidden, ErrorInternalServerError, ErrorUnauthorized}, error::{ErrorForbidden, ErrorInternalServerError, ErrorUnauthorized},
get, get,
http::{ContentEncoding, StatusCode}, http::StatusCode,
post, put, post, put,
web::{self, Data, Json, JsonConfig, ServiceConfig}, web::{self, Data, Json, JsonConfig, ServiceConfig},
FromRequest, HttpMessage, HttpRequest, HttpResponse, Responder, ResponseError, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError,
}; };
use actix_web_httpauth::extractors::{basic::BasicAuth, bearer::BearerAuth}; use actix_web_httpauth::extractors::{basic::BasicAuth, bearer::BearerAuth};
use cookie::{self, *}; use cookie::{self, *};
use futures_util::future::{err, ok, ready, Ready}; use futures_util::future::{err, ok};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use std::future::Future; use std::future::Future;
use std::ops::Deref; use std::ops::Deref;
@ -113,7 +114,7 @@ impl Cookies {
} }
fn add_signed(&mut self, cookie: Cookie<'static>) { fn add_signed(&mut self, cookie: Cookie<'static>) {
self.jar.signed(&self.key).add(cookie); self.jar.signed_mut(&self.key).add(cookie);
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -129,7 +130,6 @@ impl Cookies {
impl FromRequest for Cookies { impl FromRequest for Cookies {
type Error = actix_web::Error; type Error = actix_web::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
type Config = ();
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let request_cookies = match request.cookies() { let request_cookies = match request.cookies() {
@ -168,7 +168,6 @@ struct Auth {
impl FromRequest for Auth { impl FromRequest for Auth {
type Error = actix_web::Error; type Error = actix_web::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
type Config = ();
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
let user_manager = match request.app_data::<Data<user::Manager>>() { let user_manager = match request.app_data::<Data<user::Manager>>() {
@ -256,7 +255,6 @@ struct AdminRights {
impl FromRequest for AdminRights { impl FromRequest for AdminRights {
type Error = actix_web::Error; type Error = actix_web::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
type Config = ();
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
let user_manager = match request.app_data::<Data<user::Manager>>() { let user_manager = match request.app_data::<Data<user::Manager>>() {
@ -289,11 +287,10 @@ impl FromRequest for AdminRights {
pub fn http_auth_middleware< pub fn http_auth_middleware<
B: MessageBody + 'static, B: MessageBody + 'static,
S: Service<Response = ServiceResponse<B>, Request = ServiceRequest, Error = actix_web::Error> S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
+ 'static,
>( >(
request: ServiceRequest, request: ServiceRequest,
service: &mut S, service: &S,
) -> Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, actix_web::Error>>>> { ) -> Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, actix_web::Error>>>> {
let user_manager = match request.app_data::<Data<user::Manager>>() { let user_manager = match request.app_data::<Data<user::Manager>>() {
Some(m) => m.clone(), Some(m) => m.clone(),
@ -303,10 +300,7 @@ pub fn http_auth_middleware<
let (request, mut payload) = request.into_parts(); let (request, mut payload) = request.into_parts();
let auth_future = Auth::from_request(&request, &mut payload); let auth_future = Auth::from_request(&request, &mut payload);
let cookies_future = Cookies::from_request(&request, &mut payload); let cookies_future = Cookies::from_request(&request, &mut payload);
let request = match ServiceRequest::from_parts(request, payload) { let request = ServiceRequest::from_parts(request, payload);
Ok(s) => s,
Err(_) => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))),
};
let response_future = service.call(request); let response_future = service.call(request);
Box::pin(async move { Box::pin(async move {
@ -339,7 +333,7 @@ fn add_auth_cookies<T>(
cookies: &Cookies, cookies: &Cookies,
username: &str, username: &str,
is_admin: bool, is_admin: bool,
) -> Result<(), HttpError> { ) -> Result<(), http::Error> {
let mut cookies = cookies.clone(); let mut cookies = cookies.clone();
cookies.add_signed( cookies.add_signed(
@ -384,25 +378,20 @@ struct MediaFile {
impl MediaFile { impl MediaFile {
fn new(named_file: NamedFile) -> Self { fn new(named_file: NamedFile) -> Self {
Self { Self { named_file }
named_file: named_file,
}
} }
} }
impl Responder for MediaFile { impl Responder for MediaFile {
type Error = actix_web::Error; type Body = BoxBody;
type Future = Ready<Result<HttpResponse, actix_web::Error>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future { fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
let mut response = self.named_file.into_response(req); // Intentionally turn off content encoding for media files because:
if let Ok(r) = response.as_mut() { // 1. There is little value in compressing files that are already compressed (mp3, jpg, etc.)
// Intentionally turn off content encoding for media files because: // 2. The Content-Length header is incompatible with content encoding (other than identity), and can be valuable for clients
// 1. There is little value in compressing files that are already compressed (mp3, jpg, etc.) self.named_file
// 2. The Content-Length header is incompatible with content encoding (other than identity), and can be valuable for clients .set_content_encoding(ContentEncoding::Identity)
r.encoding(ContentEncoding::Identity); .into_response(req)
}
return ready(response);
} }
} }
@ -412,10 +401,10 @@ where
I: Send + 'static, I: Send + 'static,
E: Send + std::fmt::Debug + 'static + Into<APIError>, E: Send + std::fmt::Debug + 'static + Into<APIError>,
{ {
actix_web::web::block(f).await.map_err(|e| match e { actix_web::web::block(f)
BlockingError::Error(e) => e.into(), .await
BlockingError::Canceled => APIError::Unspecified, .map_err(|_| APIError::Unspecified)
}) .and_then(|r| r.map_err(|e| e.into()))
} }
#[get("/version")] #[get("/version")]
@ -488,8 +477,8 @@ async fn put_mount_dirs(
new_mount_dirs: Json<Vec<dto::MountDir>>, new_mount_dirs: Json<Vec<dto::MountDir>>,
) -> Result<HttpResponse, APIError> { ) -> Result<HttpResponse, APIError> {
let new_mount_dirs: Vec<MountDir> = new_mount_dirs let new_mount_dirs: Vec<MountDir> = new_mount_dirs
.to_owned() .iter()
.into_iter() .cloned()
.map(|m| m.into()) .map(|m| m.into())
.collect(); .collect();
block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?; block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?;
@ -646,7 +635,7 @@ async fn browse(
path: web::Path<String>, path: web::Path<String>,
) -> Result<Json<Vec<index::CollectionFile>>, APIError> { ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
let result = block(move || { let result = block(move || {
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
index.browse(Path::new(path.as_ref())) index.browse(Path::new(path.as_ref()))
}) })
.await?; .await?;
@ -666,7 +655,7 @@ async fn flatten(
path: web::Path<String>, path: web::Path<String>,
) -> Result<Json<Vec<index::Song>>, APIError> { ) -> Result<Json<Vec<index::Song>>, APIError> {
let songs = block(move || { let songs = block(move || {
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
index.flatten(Path::new(path.as_ref())) index.flatten(Path::new(path.as_ref()))
}) })
.await?; .await?;
@ -712,7 +701,7 @@ async fn get_audio(
) -> Result<MediaFile, APIError> { ) -> Result<MediaFile, APIError> {
let audio_path = block(move || { let audio_path = block(move || {
let vfs = vfs_manager.get_vfs()?; let vfs = vfs_manager.get_vfs()?;
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
vfs.virtual_to_real(Path::new(path.as_ref())) vfs.virtual_to_real(Path::new(path.as_ref()))
.map_err(|_| APIError::VFSPathNotFound) .map_err(|_| APIError::VFSPathNotFound)
}) })
@ -734,7 +723,7 @@ async fn get_thumbnail(
let thumbnail_path = block(move || { let thumbnail_path = block(move || {
let vfs = vfs_manager.get_vfs()?; let vfs = vfs_manager.get_vfs()?;
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
let image_path = vfs let image_path = vfs
.virtual_to_real(Path::new(path.as_ref())) .virtual_to_real(Path::new(path.as_ref()))
.map_err(|_| APIError::VFSPathNotFound)?; .map_err(|_| APIError::VFSPathNotFound)?;
@ -806,7 +795,7 @@ async fn lastfm_now_playing(
if !user_manager.is_lastfm_linked(&auth.username) { if !user_manager.is_lastfm_linked(&auth.username) {
return Err(APIError::LastFMAccountNotLinked); return Err(APIError::LastFMAccountNotLinked);
} }
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?; lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?;
Ok(()) Ok(())
}) })
@ -825,7 +814,7 @@ async fn lastfm_scrobble(
if !user_manager.is_lastfm_linked(&auth.username) { if !user_manager.is_lastfm_linked(&auth.username) {
return Err(APIError::LastFMAccountNotLinked); return Err(APIError::LastFMAccountNotLinked);
} }
let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?; lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?;
Ok(()) Ok(())
}) })

View file

@ -1,10 +1,9 @@
use actix_web::{ use actix_web::{
middleware::{normalize::TrailingSlash, Compress, Logger, NormalizePath}, middleware::{Compress, Logger, NormalizePath},
rt::System, rt::System,
web::{self, ServiceConfig}, web::{self, ServiceConfig},
App as ActixApp, HttpServer, App as ActixApp, HttpServer,
}; };
use anyhow::*;
use log::error; use log::error;
use crate::app::App; use crate::app::App;
@ -31,7 +30,7 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
web::scope("/api") web::scope("/api")
.configure(api::make_config()) .configure(api::make_config())
.wrap_fn(api::http_auth_middleware) .wrap_fn(api::http_auth_middleware)
.wrap(NormalizePath::new(TrailingSlash::Trim)), .wrap(NormalizePath::trim()),
) )
.service( .service(
actix_files::Files::new("/swagger", app.swagger_dir_path) actix_files::Files::new("/swagger", app.swagger_dir_path)
@ -46,9 +45,9 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
} }
} }
pub fn run(app: App) -> Result<()> { pub fn run(app: App) -> anyhow::Result<()> {
System::run(move || { let address = ("0.0.0.0", app.port);
let address = format!("0.0.0.0:{}", app.port); System::new().block_on(
HttpServer::new(move || { HttpServer::new(move || {
ActixApp::new() ActixApp::new()
.wrap(Logger::default()) .wrap(Logger::default())
@ -57,9 +56,11 @@ pub fn run(app: App) -> Result<()> {
}) })
.disable_signals() .disable_signals()
.bind(address) .bind(address)
.map(|server| server.run()) .map_err(|e| {
.map_err(|e| error!("Error starting HTTP server: {:?}", e)) error!("Error starting HTTP server: {:?}", e);
.ok(); e
})?; })?
.run()
)?;
Ok(()) Ok(())
} }

View file

@ -1,8 +1,7 @@
use actix_test::TestServer;
use actix_web::{ use actix_web::{
middleware::{Compress, Logger}, middleware::{Compress, Logger},
rt::{System, SystemRunner}, rt::{System, SystemRunner},
test,
test::*,
web::Bytes, web::Bytes,
App as ActixApp, App as ActixApp,
}; };
@ -44,7 +43,7 @@ impl ActixTestService {
.timeout(std::time::Duration::from_secs(30)); .timeout(std::time::Duration::from_secs(30));
for (name, value) in request.headers() { for (name, value) in request.headers() {
actix_request = actix_request.set_header(name, value.clone()); actix_request = actix_request.insert_header((name, value.clone()));
} }
if let Some(ref authorization) = self.authorization { if let Some(ref authorization) = self.authorization {
@ -92,8 +91,8 @@ impl TestService for ActixTestService {
let app = App::new(5050, paths).unwrap(); let app = App::new(5050, paths).unwrap();
let system_runner = System::new("test"); let system_runner = System::new();
let server = test::start(move || { let server = actix_test::start(move || {
let config = make_config(app.clone()); let config = make_config(app.clone());
ActixApp::new() ActixApp::new()
.wrap(Logger::default()) .wrap(Logger::default())

View file

@ -61,7 +61,7 @@ pub enum ThumbnailSize {
Native, Native,
} }
#[allow(clippy::clippy::clippy::from_over_into)] #[allow(clippy::from_over_into)]
impl Into<Option<u32>> for ThumbnailSize { impl Into<Option<u32>> for ThumbnailSize {
fn into(self) -> Option<u32> { fn into(self) -> Option<u32> {
match self { match self {

View file

@ -8,8 +8,7 @@ fn can_get_swagger_index() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
let request = protocol::swagger_index(); let request = protocol::swagger_index();
let response = service.fetch(&request); let response = service.fetch(&request);
let status = response.status(); assert_eq!(response.status(), StatusCode::OK);
assert_eq!(status, StatusCode::FOUND);
} }
#[test] #[test]