Skip to content

Commit

Permalink
refactor: [torrust#157] extract service: proxy
Browse files Browse the repository at this point in the history
Decoupling services from actix-web framework.
  • Loading branch information
josecelano committed May 18, 2023
1 parent d58f3cc commit c9fb249
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 67 deletions.
5 changes: 4 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::common::AppData;
use crate::config::Configuration;
use crate::databases::database;
use crate::services::category::{self, DbCategoryRepository};
use crate::services::proxy;
use crate::services::user::DbUserRepository;
use crate::tracker::statistics_importer::StatisticsImporter;
use crate::{mailer, routes, tracker};
Expand Down Expand Up @@ -49,10 +50,11 @@ pub async fn run(configuration: Configuration) -> Running {
let tracker_statistics_importer =
Arc::new(StatisticsImporter::new(cfg.clone(), tracker_service.clone(), database.clone()).await);
let mailer_service = Arc::new(mailer::Service::new(cfg.clone()).await);
let image_cache_service = Arc::new(ImageCacheService::new(cfg.clone()).await);
let image_cache_service: Arc<ImageCacheService> = Arc::new(ImageCacheService::new(cfg.clone()).await);
let category_repository = Arc::new(DbCategoryRepository::new(database.clone()));
let user_repository = Arc::new(DbUserRepository::new(database.clone()));
let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone()));
let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone()));

// Build app container

Expand All @@ -67,6 +69,7 @@ pub async fn run(configuration: Configuration) -> Running {
category_repository,
user_repository,
category_service,
proxy_service,
));

// Start repeating task to import tracker torrent data and updating
Expand Down
4 changes: 4 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::cache::image::manager::ImageCacheService;
use crate::config::Configuration;
use crate::databases::database::Database;
use crate::services::category::{self, DbCategoryRepository};
use crate::services::proxy;
use crate::services::user::DbUserRepository;
use crate::tracker::statistics_importer::StatisticsImporter;
use crate::{mailer, tracker};
Expand All @@ -23,6 +24,7 @@ pub struct AppData {
pub category_repository: Arc<DbCategoryRepository>,
pub user_repository: Arc<DbUserRepository>,
pub category_service: Arc<category::Service>,
pub proxy_service: Arc<proxy::Service>,
}

impl AppData {
Expand All @@ -38,6 +40,7 @@ impl AppData {
category_repository: Arc<DbCategoryRepository>,
user_repository: Arc<DbUserRepository>,
category_service: Arc<category::Service>,
proxy_service: Arc<proxy::Service>,
) -> AppData {
AppData {
cfg,
Expand All @@ -50,6 +53,7 @@ impl AppData {
category_repository,
user_repository,
category_service,
proxy_service,
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod models;
pub mod routes;
pub mod services;
pub mod tracker;
pub mod ui;
pub mod upgrades;
pub mod utils;

Expand Down
99 changes: 33 additions & 66 deletions src/routes/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,11 @@
use std::sync::Once;

use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use bytes::Bytes;
use text_to_png::TextRenderer;

use crate::cache::image::manager::Error;
use crate::common::WebAppData;
use crate::errors::ServiceResult;
use crate::routes::API_VERSION;

static ERROR_IMAGE_LOADER: Once = Once::new();

static mut ERROR_IMAGE_URL_IS_UNREACHABLE: Bytes = Bytes::new();
static mut ERROR_IMAGE_URL_IS_NOT_AN_IMAGE: Bytes = Bytes::new();
static mut ERROR_IMAGE_TOO_BIG: Bytes = Bytes::new();
static mut ERROR_IMAGE_USER_QUOTA_MET: Bytes = Bytes::new();
static mut ERROR_IMAGE_UNAUTHENTICATED: Bytes = Bytes::new();

const ERROR_IMG_FONT_SIZE: u8 = 16;
const ERROR_IMG_COLOR: &str = "Red";

const ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT: &str = "Could not find image.";
const ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT: &str = "Invalid image.";
const ERROR_IMAGE_TOO_BIG_TEXT: &str = "Image is too big.";
const ERROR_IMAGE_USER_QUOTA_MET_TEXT: &str = "Image proxy quota met.";
const ERROR_IMAGE_UNAUTHENTICATED_TEXT: &str = "Sign in to see image.";
use crate::ui::proxy::{load_error_images, map_error_to_image};

pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
Expand All @@ -35,59 +15,46 @@ pub fn init(cfg: &mut web::ServiceConfig) {
load_error_images();
}

fn generate_img_from_text(text: &str) -> Bytes {
let renderer = TextRenderer::default();

Bytes::from(
renderer
.render_text_to_png_data(text, ERROR_IMG_FONT_SIZE, ERROR_IMG_COLOR)
.unwrap()
.data,
)
}

fn load_error_images() {
ERROR_IMAGE_LOADER.call_once(|| unsafe {
ERROR_IMAGE_URL_IS_UNREACHABLE = generate_img_from_text(ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT);
ERROR_IMAGE_URL_IS_NOT_AN_IMAGE = generate_img_from_text(ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT);
ERROR_IMAGE_TOO_BIG = generate_img_from_text(ERROR_IMAGE_TOO_BIG_TEXT);
ERROR_IMAGE_USER_QUOTA_MET = generate_img_from_text(ERROR_IMAGE_USER_QUOTA_MET_TEXT);
ERROR_IMAGE_UNAUTHENTICATED = generate_img_from_text(ERROR_IMAGE_UNAUTHENTICATED_TEXT);
});
}

/// Get the proxy image.
///
/// # Errors
///
/// This function will return `Ok` only for now.
pub async fn get_proxy_image(req: HttpRequest, app_data: WebAppData, path: web::Path<String>) -> ServiceResult<impl Responder> {
// Check for optional user.
let opt_user = app_data.auth.get_user_compact_from_request(&req).await.ok();

let encoded_url = path.into_inner();
let url = urlencoding::decode(&encoded_url).unwrap_or_default();

match app_data.image_cache_manager.get_image_by_url(&url, opt_user).await {
Ok(image_bytes) => Ok(HttpResponse::build(StatusCode::OK)
.content_type("image/png")
.append_header(("Cache-Control", "max-age=15552000"))
.body(image_bytes)),
Err(e) => unsafe {
// Handling status codes in the frontend other tan OK is quite a pain.
// Return OK for now.
let (_status_code, error_image_bytes): (StatusCode, Bytes) = match e {
Error::UrlIsUnreachable => (StatusCode::GATEWAY_TIMEOUT, ERROR_IMAGE_URL_IS_UNREACHABLE.clone()),
Error::UrlIsNotAnImage => (StatusCode::BAD_REQUEST, ERROR_IMAGE_URL_IS_NOT_AN_IMAGE.clone()),
Error::ImageTooBig => (StatusCode::BAD_REQUEST, ERROR_IMAGE_TOO_BIG.clone()),
Error::UserQuotaMet => (StatusCode::TOO_MANY_REQUESTS, ERROR_IMAGE_USER_QUOTA_MET.clone()),
Error::Unauthenticated => (StatusCode::UNAUTHORIZED, ERROR_IMAGE_UNAUTHENTICATED.clone()),
};

let user_id = app_data.auth.get_user_id_from_request(&req).await.ok();

match user_id {
Some(user_id) => {
// Get image URL from URL path
let encoded_image_url = path.into_inner();
let image_url = urlencoding::decode(&encoded_image_url).unwrap_or_default();

match app_data.proxy_service.get_image_by_url(&image_url, &user_id).await {
Ok(image_bytes) => {
// Returns the cached image.
Ok(HttpResponse::build(StatusCode::OK)
.content_type("image/png")
.append_header(("Cache-Control", "max-age=15552000"))
.body(image_bytes))
}
Err(e) =>
// Returns an error image.
// Handling status codes in the frontend other tan OK is quite a pain.
// Return OK for now.
{
Ok(HttpResponse::build(StatusCode::OK)
.content_type("image/png")
.append_header(("Cache-Control", "no-cache"))
.body(map_error_to_image(&e)))
}
}
}
None => {
// Unauthenticated users can't see images.
Ok(HttpResponse::build(StatusCode::OK)
.content_type("image/png")
.append_header(("Cache-Control", "no-cache"))
.body(error_image_bytes))
},
.body(map_error_to_image(&Error::Unauthenticated)))
}
}
}
1 change: 1 addition & 0 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod about;
pub mod category;
pub mod proxy;
pub mod user;
46 changes: 46 additions & 0 deletions src/services/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Image cache proxy.
//!
//! The image cache proxy is a service that allows users to proxy images
//! through the server.
//!
//! Sample URL:
//!
//! <http://0.0.0.0:3000/v1/proxy/image/https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fthumb%2F2%2F21%2FMandel_zoom_00_mandelbrot_set.jpg%2F1280px-Mandel_zoom_00_mandelbrot_set.jpg>
use std::sync::Arc;

use bytes::Bytes;

use super::user::DbUserRepository;
use crate::cache::image::manager::{Error, ImageCacheService};
use crate::models::user::UserId;

pub struct Service {
image_cache_service: Arc<ImageCacheService>,
user_repository: Arc<DbUserRepository>,
}

impl Service {
#[must_use]
pub fn new(image_cache_service: Arc<ImageCacheService>, user_repository: Arc<DbUserRepository>) -> Self {
Self {
image_cache_service,
user_repository,
}
}

/// It gets image by URL and caches it.
///
/// # Errors
///
/// It returns an error if:
///
/// * The image URL is unreachable.
/// * The image URL is not an image.
/// * The image is too big.
/// * The user quota is met.
pub async fn get_image_by_url(&self, url: &str, user_id: &UserId) -> Result<Bytes, Error> {
let user = self.user_repository.get_compact_user(user_id).await.ok();

self.image_cache_service.get_image_by_url(url, user).await
}
}
2 changes: 2 additions & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//! User interface module. Presentation layer.
pub mod proxy;
56 changes: 56 additions & 0 deletions src/ui/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::sync::Once;

use bytes::Bytes;
use text_to_png::TextRenderer;

use crate::cache::image::manager::Error;

pub static ERROR_IMAGE_LOADER: Once = Once::new();

pub static mut ERROR_IMAGE_URL_IS_UNREACHABLE: Bytes = Bytes::new();
pub static mut ERROR_IMAGE_URL_IS_NOT_AN_IMAGE: Bytes = Bytes::new();
pub static mut ERROR_IMAGE_TOO_BIG: Bytes = Bytes::new();
pub static mut ERROR_IMAGE_USER_QUOTA_MET: Bytes = Bytes::new();
pub static mut ERROR_IMAGE_UNAUTHENTICATED: Bytes = Bytes::new();

const ERROR_IMG_FONT_SIZE: u8 = 16;
const ERROR_IMG_COLOR: &str = "Red";

const ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT: &str = "Could not find image.";
const ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT: &str = "Invalid image.";
const ERROR_IMAGE_TOO_BIG_TEXT: &str = "Image is too big.";
const ERROR_IMAGE_USER_QUOTA_MET_TEXT: &str = "Image proxy quota met.";
const ERROR_IMAGE_UNAUTHENTICATED_TEXT: &str = "Sign in to see image.";

pub fn load_error_images() {
ERROR_IMAGE_LOADER.call_once(|| unsafe {
ERROR_IMAGE_URL_IS_UNREACHABLE = generate_img_from_text(ERROR_IMAGE_URL_IS_UNREACHABLE_TEXT);
ERROR_IMAGE_URL_IS_NOT_AN_IMAGE = generate_img_from_text(ERROR_IMAGE_URL_IS_NOT_AN_IMAGE_TEXT);
ERROR_IMAGE_TOO_BIG = generate_img_from_text(ERROR_IMAGE_TOO_BIG_TEXT);
ERROR_IMAGE_USER_QUOTA_MET = generate_img_from_text(ERROR_IMAGE_USER_QUOTA_MET_TEXT);
ERROR_IMAGE_UNAUTHENTICATED = generate_img_from_text(ERROR_IMAGE_UNAUTHENTICATED_TEXT);
});
}

pub fn map_error_to_image(error: &Error) -> Bytes {
unsafe {
match error {
Error::UrlIsUnreachable => ERROR_IMAGE_URL_IS_UNREACHABLE.clone(),
Error::UrlIsNotAnImage => ERROR_IMAGE_URL_IS_NOT_AN_IMAGE.clone(),
Error::ImageTooBig => ERROR_IMAGE_TOO_BIG.clone(),
Error::UserQuotaMet => ERROR_IMAGE_USER_QUOTA_MET.clone(),
Error::Unauthenticated => ERROR_IMAGE_UNAUTHENTICATED.clone(),
}
}
}

fn generate_img_from_text(text: &str) -> Bytes {
let renderer = TextRenderer::default();

Bytes::from(
renderer
.render_text_to_png_data(text, ERROR_IMG_FONT_SIZE, ERROR_IMG_COLOR)
.unwrap()
.data,
)
}

0 comments on commit c9fb249

Please sign in to comment.