mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-23 00:03:33 +02:00
172 lines
8.7 KiB
Rust
172 lines
8.7 KiB
Rust
use crate::api::model::AppState;
|
|
use crate::api::model::{DownloadQueue, FileDownload, FileDownloadRequest};
|
|
use crate::model::{AppConfig, VideoDownloadConfig};
|
|
use crate::utils::request;
|
|
use tokio::sync::RwLock;
|
|
use futures::stream::TryStreamExt;
|
|
use log::info;
|
|
use serde_json::{json, Value};
|
|
use tokio::fs::File;
|
|
use tokio::io::AsyncWriteExt;
|
|
use std::ops::Deref;
|
|
use std::sync::Arc;
|
|
use tokio::fs;
|
|
use axum::response::IntoResponse;
|
|
use shared::utils::bytes_to_megabytes;
|
|
use shared::error::to_io_error;
|
|
use crate::utils::request::create_client;
|
|
|
|
async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwest::Client) -> Result<(), String> {
|
|
if let Some(file_download) = active.read().await.as_ref().as_ref() {
|
|
match client.get(file_download.url.clone()).send().await {
|
|
Ok(response) => {
|
|
match fs::create_dir_all(&file_download.file_dir).await {
|
|
Ok(()) => {
|
|
if let Some(file_path_str) = file_download.file_path.to_str() {
|
|
info!("Downloading {file_path_str}");
|
|
match File::create(&file_download.file_path).await {
|
|
Ok(mut file) => {
|
|
let mut downloaded: u64 = 0;
|
|
let mut stream = response.bytes_stream().map_err(to_io_error);
|
|
loop {
|
|
match stream.try_next().await {
|
|
Ok(item) => {
|
|
if let Some(chunk) = item {
|
|
match file.write_all(&chunk).await {
|
|
Ok(()) => {
|
|
downloaded += chunk.len() as u64;
|
|
if let Some(lock) = active.write().await.as_mut() {
|
|
lock.size = downloaded;
|
|
}
|
|
}
|
|
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
|
}
|
|
} else {
|
|
let megabytes = bytes_to_megabytes(downloaded);
|
|
info!("Downloaded {file_path_str}, filesize: {megabytes}MB");
|
|
if let Some(lock) = active.write().await.as_mut() {
|
|
lock.size = downloaded;
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
|
}
|
|
}
|
|
}
|
|
Err(err) => Err(format!("Error while writing to file: {file_path_str} {err}"))
|
|
}
|
|
} else {
|
|
Err("Error file-download file-path unknown".to_string())
|
|
}
|
|
}
|
|
Err(err) => Err(format!("Error while creating directory for file: {} {}", &file_download.file_dir.to_str().unwrap_or("?"), err))
|
|
}
|
|
}
|
|
Err(err) => Err(format!("Error while opening url: {} {}", &file_download.url, err))
|
|
}
|
|
} else {
|
|
Err("No active file download".to_string())
|
|
}
|
|
}
|
|
|
|
async fn run_download_queue(cfg: &AppConfig, download_cfg: &VideoDownloadConfig, download_queue: &Arc<DownloadQueue>) -> Result<(), String> {
|
|
let next_download = download_queue.as_ref().queue.lock().await.pop_front();
|
|
if next_download.is_some() {
|
|
{ *download_queue.as_ref().active.write().await = next_download; }
|
|
let config = cfg.config.load();
|
|
let disabled_headers = config
|
|
.reverse_proxy
|
|
.as_ref()
|
|
.and_then(|r| r.disabled_header.clone());
|
|
let headers = request::get_request_headers(Some(&download_cfg.headers), None, disabled_headers.as_ref());
|
|
let dq = Arc::clone(download_queue);
|
|
|
|
match create_client(cfg).default_headers(headers).build() {
|
|
Ok(client) => {
|
|
tokio::spawn(async move {
|
|
loop {
|
|
if dq.active.read().await.deref().is_some() {
|
|
match download_file(Arc::clone(&dq.active), &client).await {
|
|
Ok(()) => {
|
|
if let Some(fd) = &mut *dq.active.write().await {
|
|
fd.finished = true;
|
|
dq.finished.write().await.push(fd.clone());
|
|
}
|
|
}
|
|
Err(err) => {
|
|
if let Some(fd) = &mut *dq.active.write().await {
|
|
fd.finished = true;
|
|
fd.error = Some(err);
|
|
dq.finished.write().await.push(fd.clone());
|
|
}
|
|
}
|
|
}
|
|
*dq.active.write().await = dq.queue.lock().await.pop_front();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
Err(_) => return Err("Failed to build http client".to_string()),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
macro_rules! download_info {
|
|
($file_download:expr) => {
|
|
json!({"uuid": $file_download.uuid, "filename": $file_download.filename,
|
|
"filesize": $file_download.size, "finished": $file_download.finished,
|
|
"error": $file_download.error})
|
|
}
|
|
}
|
|
|
|
pub async fn queue_download_file(
|
|
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
|
axum::extract::Json(req): axum::extract::Json<FileDownloadRequest>,
|
|
) -> impl axum::response::IntoResponse + Send {
|
|
let app_config = &*app_state.app_config;
|
|
|
|
let config = app_config.config.load();
|
|
if let Some(video_cfg) = config.video.as_ref() {
|
|
if let Some(download_cfg) = video_cfg.download.as_ref() {
|
|
if download_cfg.directory.is_empty() {
|
|
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download.directory configuration"}))).into_response();
|
|
}
|
|
match FileDownload::new(req.url.as_str(), req.filename.as_str(), download_cfg) {
|
|
Some(file_download) => {
|
|
app_state.downloads.queue.lock().await.push_back(file_download.clone());
|
|
if app_state.downloads.active.read().await.is_none() {
|
|
match run_download_queue(&app_state.app_config, download_cfg, &app_state.downloads).await {
|
|
Ok(()) => {}
|
|
Err(err) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err}))).into_response(),
|
|
}
|
|
}
|
|
axum::Json(download_info!(&file_download)).into_response()
|
|
}
|
|
None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(),
|
|
}
|
|
} else {
|
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download configuration"}))).into_response()
|
|
}
|
|
} else {
|
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video configuration"}))).into_response()
|
|
}
|
|
}
|
|
|
|
pub async fn download_file_info(
|
|
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
|
) -> impl axum::response::IntoResponse + Send {
|
|
let finished_list: &[Value] = &app_state.downloads.finished.write().await.drain(..)
|
|
.map(|fd| download_info!(fd)).collect::<Vec<Value>>();
|
|
|
|
(*app_state.downloads.active.read().await).as_ref().map_or_else(|| axum::Json(json!({
|
|
"completed": true, "downloads": finished_list
|
|
})), |file_download| axum::Json(json!({
|
|
"completed": false, "downloads": finished_list, "active": download_info!(file_download)
|
|
})))
|
|
}
|