37 Commits

Author SHA1 Message Date
4def0067cb Optimize release build
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
2025-06-28 22:37:51 +02:00
85b4a12669 Update deps 2025-06-28 22:21:19 +02:00
b7abfc28cc Update dependencies and GitHub Actions workflow
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
- Bump Rust dependencies to latest versions in Cargo.lock - Update
  Docker image build workflow: - Add commit SHA to Docker image tags -
  Pass commit SHA as FILES_SERVER_TAG in deployment webhook - Minor YAML
  formatting improvements
2025-06-21 23:20:51 +02:00
2e98c84ba7 Merge pull request #38 from flibusta-apps/dependabot/cargo/crossbeam-channel-0.5.15
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
Bump crossbeam-channel from 0.5.14 to 0.5.15
2025-04-10 23:09:24 +02:00
dependabot[bot]
a18d1f1094 Bump crossbeam-channel from 0.5.14 to 0.5.15
Bumps [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) from 0.5.14 to 0.5.15.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 14:42:39 +00:00
0572e300ef Merge pull request #37 from flibusta-apps/dependabot/cargo/tokio-1.44.2
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
Bump tokio from 1.44.1 to 1.44.2
2025-04-08 15:56:38 +02:00
dependabot[bot]
cce2586a2e Bump tokio from 1.44.1 to 1.44.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.1 to 1.44.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.1...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 02:12:03 +00:00
d7f702109b Merge pull request #36 from flibusta-apps/dependabot/cargo/openssl-0.10.72
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
Bump openssl from 0.10.70 to 0.10.72
2025-04-04 23:58:21 +02:00
dependabot[bot]
e168f255fb Bump openssl from 0.10.70 to 0.10.72
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.70 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.70...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 20:54:49 +00:00
e91318f728 Update deps
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
2025-03-23 12:50:19 +01:00
b2969f7693 Merge pull request #35 from flibusta-apps/dependabot/cargo/openssl-0.10.70
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
Bump openssl from 0.10.68 to 0.10.70
2025-02-06 10:46:14 +01:00
dependabot[bot]
15091f633d Bump openssl from 0.10.68 to 0.10.70
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.68 to 0.10.70.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.68...openssl-v0.10.70)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 18:53:07 +00:00
4bc0e29c66 Fix
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
2025-01-20 13:39:16 +01:00
7672fc3f60 Update deps
Some checks are pending
Build docker image / Build-Docker-Image (push) Waiting to run
2025-01-19 23:20:25 +01:00
491bb75df2 Update deps
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
2024-12-24 20:32:22 +01:00
88f91af907 Update deps
Some checks failed
Build docker image / Build-Docker-Image (push) Has been cancelled
2024-10-02 14:29:29 +02:00
f2b46817d6 Merge pull request #34 from flibusta-apps/dependabot/cargo/openssl-0.10.66
Bump openssl from 0.10.64 to 0.10.66
2024-07-24 18:55:36 +02:00
dependabot[bot]
fc5bf1190f Bump openssl from 0.10.64 to 0.10.66
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.64 to 0.10.66.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.64...openssl-v0.10.66)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 18:14:28 +00:00
a1b52a1d2e Merge pull request #33 from flibusta-apps/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-18 13:03:17 +02:00
dependabot[bot]
c33be9463d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 09:49:33 +00:00
1104a91570 Fix MessageIdInvalid error 2024-05-22 13:13:02 +02:00
94193fae41 Add logs 2024-05-10 20:55:12 +02:00
cd0cde70de Fix 2024-05-10 20:40:21 +02:00
8ff0a069b1 Fix 2024-05-10 20:36:09 +02:00
e21273a2b8 Delete old files 2024-05-10 20:27:08 +02:00
bd62f1b076 Fix getFile timeouts 2024-05-08 20:36:20 +02:00
b944a9e724 Fix 2024-05-07 17:50:44 +02:00
16a1691212 Ignore teloxide::ApiError::MessageToForwardNotFound 2024-05-07 17:38:26 +02:00
Шатунов Антон
b705b0cb30 Cleanup and remove unused downloader 2024-05-07 02:25:16 +03:00
Шатунов Антон
e18d9555a6 Change download logic to just opening file
Co-authored-by: Kurbanov Bulat <kurbanovbul@gmail.com>
2024-05-07 02:13:17 +03:00
dbd4b547c6 Add logs 2024-05-07 00:42:15 +02:00
adc47f1b75 Show all logs 2024-05-07 00:35:38 +02:00
0976471562 Fix 2024-05-07 00:19:25 +02:00
d2fcf96695 Merge pull request #32 from flibusta-apps/rewrite-to-rust
Add cache
2024-05-06 23:42:09 +02:00
f9b2e8b0a3 Fix 2024-05-06 23:23:45 +02:00
494569d1ac Fix 2024-05-06 23:23:05 +02:00
7319312754 Merge pull request #31 from flibusta-apps/rewrite-to-rust
Fix download
2024-05-06 23:10:21 +02:00
8 changed files with 2013 additions and 926 deletions

View File

@@ -3,18 +3,16 @@ name: Build docker image
on: on:
push: push:
branches: branches:
- 'main' - "main"
jobs: jobs:
Build-Docker-Image: Build-Docker-Image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- id: repository_name - id: repository_name
@@ -22,29 +20,26 @@ jobs:
with: with:
string: ${{ github.repository }} string: ${{ github.repository }}
- - name: Login to ghcr.io
name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build and push
name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
env: env:
IMAGE: ${{ steps.repository_name.outputs.lowercase }} IMAGE: ${{ steps.repository_name.outputs.lowercase }}
with: with:
push: true push: true
platforms: linux/amd64 platforms: linux/amd64
tags: ghcr.io/${{ env.IMAGE }}:latest tags: ghcr.io/${{ env.IMAGE }}:latest,ghcr.io/${{ env.IMAGE }}:${{ github.sha }}
context: . context: .
file: ./docker/production.dockerfile file: ./docker/production.dockerfile
- - name: Invoke deployment hook
name: Invoke deployment hook
uses: joelwmale/webhook-action@master uses: joelwmale/webhook-action@master
with: with:
url: ${{ secrets.WEBHOOK_URL }} url: ${{ secrets.WEBHOOK_URL }}?FILES_SERVER_TAG=${{ github.sha }}

2
.gitignore vendored
View File

@@ -11,6 +11,8 @@ __pycache__
venv venv
.DS_Store
# Added by cargo # Added by cargo
/target /target

2687
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,34 +3,49 @@ name = "telegram_files_server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[profile.release]
opt-level = 3
debug = false
strip = true
lto = true
codegen-units = 1
panic = 'abort'
[profile.profiling]
inherits = "release"
debug = true
strip = false
[dependencies] [dependencies]
serde = "1.0.200" serde = "1.0.219"
serde_json = "1.0.116" serde_json = "1.0.140"
axum = { version = "0.7.5", features = ["multipart"] } axum = { version = "0.8.1", features = ["multipart"] }
axum_typed_multipart = "0.11.1" axum_typed_multipart = "0.16.3"
tracing = "0.1.40" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"]} tracing-subscriber = { version = "0.3.19", features = ["env-filter"]}
tower-http = { version = "0.5.2", features = ["trace"] } tower-http = { version = "0.6.2", features = ["trace"] }
sentry-tracing = "0.32.3" sentry-tracing = "0.41.0"
tokio = "1.37.0" tokio = { version = "1.44.2", features = [ "full" ] }
tokio-util = { version = "0.7.11", features = [ "full" ] } tokio-util = { version = "0.7.14", features = [ "full" ] }
axum-prometheus = "0.6.1" axum-prometheus = "0.8.0"
futures = "0.3.30" futures = "0.3.31"
once_cell = "1.19.0" once_cell = "1.21.1"
teloxide = "0.12.2" teloxide = "0.16.0"
sentry = "0.32.3" sentry = "0.41.0"
dotenv = "0.15.0" dotenvy = "0.15.7"
reqwest = { version = "0.11.10", features = [ reqwest = { version = "0.12.15", features = [
"json", "json",
"stream", "stream",
"multipart", "multipart",
], default-features = false } ], default-features = false }
moka = { version = "0.12.7", features = ["future"] } moka = { version = "0.12.10", features = ["future"] }

View File

@@ -1,7 +1,7 @@
use std::sync::{ use std::{sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Arc,
}; }, time::Duration};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use teloxide::Bot; use teloxide::Bot;
@@ -23,8 +23,18 @@ impl RoundRobinBot {
pub fn get_bot(&self) -> Bot { pub fn get_bot(&self) -> Bot {
let index = self.current_index.fetch_add(1, Ordering::Relaxed) % self.bot_tokens.len(); let index = self.current_index.fetch_add(1, Ordering::Relaxed) % self.bot_tokens.len();
Bot::new(self.bot_tokens[index].clone())
.set_api_url(reqwest::Url::parse(CONFIG.telegram_api_url.as_str()).unwrap()) let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(5 * 60))
.tcp_nodelay(true)
.build()
.unwrap();
Bot::with_client(
self.bot_tokens[index].clone(),
client
).set_api_url(reqwest::Url::parse(CONFIG.telegram_api_url.as_str()).unwrap())
} }
} }

View File

@@ -1,17 +1,13 @@
use std::pin::Pin; use std::error::Error;
use axum::body::Bytes; use axum::body::Bytes;
use futures::TryStreamExt;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Serialize; use serde::Serialize;
use teloxide::{ use teloxide::{
net::Download,
requests::Requester, requests::Requester,
types::{ChatId, InputFile, MessageId, Recipient}, types::{ChatId, InputFile, MessageId, Recipient},
Bot,
}; };
use tokio::io::AsyncRead; use tokio::fs::File;
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::log; use tracing::log;
use moka::future::Cache; use moka::future::Cache;
@@ -48,6 +44,7 @@ pub static TEMP_FILES_CACHE: Lazy<Cache<i32, MessageId>> = Lazy::new(|| {
.build() .build()
}); });
pub async fn upload_file( pub async fn upload_file(
file: Bytes, file: Bytes,
filename: String, filename: String,
@@ -73,7 +70,8 @@ pub async fn upload_file(
} }
} }
pub async fn download_file(chat_id: i64, message_id: i32) -> Option<BotDownloader> {
pub async fn download_file(chat_id: i64, message_id: i32) -> Result<Option<File>, Box<dyn Error>> {
let bot = ROUND_ROBIN_BOT.get_bot(); let bot = ROUND_ROBIN_BOT.get_bot();
let forwarded_message = match bot let forwarded_message = match bot
@@ -86,50 +84,71 @@ pub async fn download_file(chat_id: i64, message_id: i32) -> Option<BotDownloade
{ {
Ok(v) => v, Ok(v) => v,
Err(err) => { Err(err) => {
if let teloxide::RequestError::Api(ref err) = err {
if let teloxide::ApiError::MessageToForwardNotFound = err {
return Ok(None);
}
}
if let teloxide::RequestError::Api(ref err) = err {
if let teloxide::ApiError::MessageIdInvalid = err {
return Ok(None);
}
}
log::error!("Error: {}", err); log::error!("Error: {}", err);
return None; return Err(Box::new(err));
} }
}; };
let file_id = match forwarded_message.document() { let file_id = forwarded_message.document().unwrap().file.id.clone();
Some(v) => v.file.id.clone(),
None => {
log::error!("Document not found!");
return None;
}
};
TEMP_FILES_CACHE.insert(message_id, forwarded_message.id.clone()).await; TEMP_FILES_CACHE.insert(message_id, forwarded_message.id).await;
let path = match bot.get_file(file_id.clone()).await { let path = match bot.get_file(file_id.clone()).await {
Ok(v) => v.path, Ok(v) => v.path,
Err(err) => { Err(err) => {
log::error!("Error: {}", err); log::error!("Error: {}", err);
return None; return Err(Box::new(err));
} }
}; };
return Some(BotDownloader::new(bot, path)); Ok(Some(File::open(path).await?))
} }
pub struct BotDownloader {
bot: Bot, pub async fn clean_files() -> Result<(), Box<dyn Error>> {
file_path: String, let bots_folder = "/var/lib/telegram-bot-api/";
let documents_folder_name = "documents";
let mut bots_folder = tokio::fs::read_dir(bots_folder).await.unwrap();
while let Some(entry) = bots_folder.next_entry().await? {
if !entry.metadata().await.unwrap().is_dir() {
continue;
} }
impl BotDownloader { let documents_folder_path = entry.path().join(documents_folder_name);
pub fn new(bot: Bot, file_path: String) -> Self { if !documents_folder_path.exists() {
Self { bot, file_path } continue;
} }
pub fn get_async_read(self) -> Pin<Box<dyn AsyncRead + Send>> { let mut document_folder = match tokio::fs::read_dir(documents_folder_path.clone()).await {
let stream = self.bot.download_file_stream(&self.file_path); Ok(v) => v,
Err(err) => panic!("Path: {:?}, Error: {:?}", documents_folder_path, err),
};
Box::pin( while let Some(file) = document_folder.next_entry().await? {
stream let metadata = file.metadata().await.unwrap();
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read() if metadata.created()?.elapsed().unwrap().as_secs() > 3600 {
.compat(), match tokio::fs::remove_file(file.path()).await {
) Ok(_) => log::info!("File {:?} removed", file.path()),
Err(err) => log::error!("Error: {}", err),
} }
} }
}
}
Ok(())
}

View File

@@ -12,6 +12,7 @@ use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use tower_http::trace::{self, TraceLayer}; use tower_http::trace::{self, TraceLayer};
use tracing::Level; use tracing::Level;
use tracing::log;
use crate::config::CONFIG; use crate::config::CONFIG;
@@ -44,8 +45,8 @@ pub async fn get_router() -> Router {
let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair();
let app_router = Router::new() let app_router = Router::new()
.route("/upload/", post(upload)) .route("/api/v1/files/upload/", post(upload))
.route("/download_by_message/:chat_id/:message_id", get(download)) .route("/api/v1/files/download_by_message/{chat_id}/{message_id}", get(download))
.layer(DefaultBodyLimit::max(BODY_LIMIT)) .layer(DefaultBodyLimit::max(BODY_LIMIT))
.layer(middleware::from_fn(auth)) .layer(middleware::from_fn(auth))
.layer(prometheus_layer); .layer(prometheus_layer);
@@ -54,8 +55,8 @@ pub async fn get_router() -> Router {
Router::new().route("/metrics", get(|| async move { metric_handle.render() })); Router::new().route("/metrics", get(|| async move { metric_handle.render() }));
Router::new() Router::new()
.nest("/api/v1/files", app_router) .merge(app_router)
.nest("/", metric_router) .merge(metric_router)
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
@@ -89,14 +90,20 @@ async fn upload(data: TypedMultipart<UploadFileRequest>) -> impl IntoResponse {
} }
async fn download(Path((chat_id, message_id)): Path<(i64, i32)>) -> impl IntoResponse { async fn download(Path((chat_id, message_id)): Path<(i64, i32)>) -> impl IntoResponse {
let downloader = download_file(chat_id, message_id).await; let file = match download_file(chat_id, message_id).await {
Ok(v) => {
let data = match downloader { match v {
Some(v) => v.get_async_read(), Some(v) => v,
None => return StatusCode::NOT_FOUND.into_response() None => return StatusCode::NO_CONTENT.into_response(),
}
},
Err(err) => {
log::error!("{}", err);
return StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}; };
let reader = ReaderStream::new(data); let reader = ReaderStream::new(file);
axum::body::Body::from_stream(reader).into_response() axum::body::Body::from_stream(reader).into_response()
} }

View File

@@ -1,17 +1,41 @@
mod config; mod config;
mod core; mod core;
use core::file_utils::clean_files;
use std::{net::SocketAddr, str::FromStr}; use std::{net::SocketAddr, str::FromStr};
use sentry::{integrations::debug_images::DebugImagesIntegration, types::Dsn, ClientOptions}; use sentry::{integrations::debug_images::DebugImagesIntegration, types::Dsn, ClientOptions};
use sentry_tracing::EventFilter; use sentry_tracing::EventFilter;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt};
use crate::core::views::get_router; use crate::core::views::get_router;
async fn start_app() {
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
let app = get_router().await;
println!("Start webserver...");
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
println!("Webserver shutdown...");
}
async fn cron_jobs() {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5 * 60));
loop {
interval.tick().await;
let _ = clean_files().await;
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenv::dotenv().ok(); dotenvy::dotenv().ok();
let options = ClientOptions { let options = ClientOptions {
dsn: Some(Dsn::from_str(&config::CONFIG.sentry_dsn).unwrap()), dsn: Some(Dsn::from_str(&config::CONFIG.sentry_dsn).unwrap()),
@@ -28,16 +52,10 @@ async fn main() {
}); });
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer().with_target(false))
.with(filter::LevelFilter::INFO)
.with(sentry_layer) .with(sentry_layer)
.init(); .init();
let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); tokio::join![cron_jobs(), start_app()];
let app = get_router().await;
println!("Start webserver...");
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
println!("Webserver shutdown...");
} }