Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM rust:1.80-slim AS builder
FROM rust:1.90-slim AS builder

RUN apt update \
&& apt install -y \
pkg-config \
libssl-dev \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app
Expand All @@ -13,14 +13,14 @@ RUN cargo install --path .

########################################################################################################################

FROM debian:12-slim
FROM debian:13-slim

LABEL org.opencontainers.image.source=https://github.com/s373r/freshrss-image-cache-service-rs

RUN apt update \
&& apt install -y \
ca-certificates \
libssl3 \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates

Expand Down
51 changes: 36 additions & 15 deletions src/app_service.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::error::Error;
use std::fmt;
use crate::dtos::CachedImage;
use anyhow::{bail, Context, Result};
use sha3::{Digest, Sha3_256};
use std::path::PathBuf;
use axum::http::HeaderValue;
use reqwest::Response;
use sha3::{Digest, Sha3_256};
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use url::Url;
Expand All @@ -15,6 +15,7 @@ pub struct CachedImagePaths {
data_folder: PathBuf,
data_path: PathBuf,
mime_type_path: PathBuf,
filename: String,
}

#[derive(Clone, Debug)]
Expand All @@ -31,7 +32,6 @@ pub struct AppService {
bypass_info: Option<CloudFlareBypassProxy>,
}


#[derive(Debug)]
struct CaptchaError {
mime_type: String,
Expand All @@ -45,8 +45,12 @@ impl fmt::Display for CaptchaError {
}

impl AppService {
pub fn new(access_token: String, images_dir: PathBuf, disable_https_validation: bool,
bypass_info: Option<CloudFlareBypassProxy>) -> Self {
pub fn new(
access_token: String,
images_dir: PathBuf,
disable_https_validation: bool,
bypass_info: Option<CloudFlareBypassProxy>,
) -> Self {
Self {
access_token,
images_dir,
Expand Down Expand Up @@ -76,6 +80,7 @@ impl AppService {
data: image_content,
mime_type,
extracted_from_cache,
filename: cached_image_paths.filename,
})
}

Expand All @@ -100,11 +105,13 @@ impl AppService {
// Use two first letters of the file name for the intermediate directory, to
// make sure we don't end up with a huge number of files.
let data_folder = self.images_dir.join(&image_content_file_name[0..2]);
let filename = image_content_file_name.clone();

Ok(CachedImagePaths {
data_path: data_folder.join(image_content_file_name),
mime_type_path: data_folder.join(mime_type_file_name),
data_folder,
filename,
})
}

Expand Down Expand Up @@ -156,26 +163,37 @@ impl AppService {
Ok(())
}

async fn try_get_image(&self, image_url: &Url, deploy_workarounds: bool) -> Result<(Response, HeaderValue)> {
let mut client_builder = reqwest::Client::builder()
.danger_accept_invalid_certs(self.disable_https_validation);
async fn try_get_image(
&self,
image_url: &Url,
deploy_workarounds: bool,
) -> Result<(Response, HeaderValue)> {
let mut client_builder =
reqwest::Client::builder().danger_accept_invalid_certs(self.disable_https_validation);

if deploy_workarounds {
let bypass = self.bypass_info.as_ref().unwrap();
let proxy = reqwest::Proxy::all(bypass.proxy_url.clone())
.expect("Failed to create a proxy")
.basic_auth(&bypass.proxy_login.clone(), &bypass.proxy_password.clone());
client_builder =
client_builder.danger_accept_invalid_certs(true).proxy(proxy);
client_builder = client_builder
.danger_accept_invalid_certs(true)
.proxy(proxy);
}

let client = client_builder.build().expect("Failed to build the HTTP client");
let client = client_builder
.build()
.expect("Failed to build the HTTP client");

let mut headers = reqwest::header::HeaderMap::new();
headers.insert("user-agent", "curl/7.86.0".parse().unwrap());
headers.insert("accept", "*/*".parse().unwrap());

let image_response = client.get(image_url.clone()).headers(headers).send().await?;
let image_response = client
.get(image_url.clone())
.headers(headers)
.send()
.await?;

let Some(mime_type) = image_response.headers().get("content-type").cloned() else {
bail!("Failed to get the content-type header for image: {image_url}");
Expand All @@ -184,7 +202,10 @@ impl AppService {
// CloudFlare often returns CAPTCHAs for images, so we need to check for that.
let mime_str = String::from_utf8_lossy(mime_type.as_bytes());
if !mime_str.starts_with("image/") && !mime_str.starts_with("video/") {
return Err(CaptchaError { mime_type: mime_str.parse()? }.into());
return Err(CaptchaError {
mime_type: mime_str.parse()?,
}
.into());
}
Ok((image_response, mime_type))
}
Expand Down
45 changes: 45 additions & 0 deletions src/dtos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub struct CachedImage {
pub data: Vec<u8>,
pub mime_type: String,
pub extracted_from_cache: bool,
pub filename: String,
}

impl IntoResponse for CachedImage {
Expand Down Expand Up @@ -36,6 +37,50 @@ impl IntoResponse for CachedImage {
HeaderValue::from_str(cache_status_value).unwrap(),
);

let filename_with_ext = format!(
"{}.{}",
self.filename,
mime_to_extension(&self.mime_type).unwrap_or("bin")
);

headers.insert(
HeaderName::from_str("Content-Disposition").unwrap(),
HeaderValue::from_str(&format!("attachment; filename=\"{}\"", filename_with_ext))
.unwrap(),
);

response
}
}

fn mime_to_extension(mime_type: &str) -> Option<&'static str> {
match mime_type {
// Common image formats
"image/jpeg" => Some("jpg"),
"image/png" => Some("png"),
"image/gif" => Some("gif"),
"image/webp" => Some("webp"),
"image/svg+xml" => Some("svg"),
"image/bmp" => Some("bmp"),
"image/x-icon" => Some("ico"),
"image/vnd.microsoft.icon" => Some("ico"),
"image/tiff" => Some("tiff"),
"image/avif" => Some("avif"),
"image/heic" => Some("heic"),
"image/heif" => Some("heif"),

// Less common but valid image formats
"image/x-bmp" => Some("bmp"),
"image/x-ms-bmp" => Some("bmp"),
"image/apng" => Some("apng"),
"image/jxl" => Some("jxl"),

// RAW formats
"image/x-canon-cr2" => Some("cr2"),
"image/x-canon-crw" => Some("crw"),
"image/x-nikon-nef" => Some("nef"),
"image/x-sony-arw" => Some("arw"),

_ => None,
}
}