More refactor

This commit is contained in:
Daniel M 2022-03-31 01:16:39 +02:00
parent 2e10b54f32
commit 59de02d34d
5 changed files with 224 additions and 243 deletions

View File

@ -1,90 +1,82 @@
use std::collections::{ HashMap, VecDeque }; use std::collections::{HashMap, VecDeque};
use std::time::SystemTime;
use std::io::stdout; use std::io::stdout;
use std::time::SystemTime;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crossterm::cursor::{ MoveToPreviousLine }; use crossterm::cursor::MoveToPreviousLine;
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ Clear, ClearType }; use crossterm::style::Print;
use crossterm::style::{ Print }; use crossterm::terminal::{Clear, ClearType};
use crate::errors::*; use crate::errors::*;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DlStatus { pub enum DlStatus {
Init { Init { bytes_total: u64, filename: String },
bytes_total: u64, Update { speed_mbps: f32, bytes_curr: u64 },
filename: String Done { duration_ms: u64 },
}, DoneErr { filename: String },
Update {
speed_mbps: f32,
bytes_curr: u64
},
Done {
duration_ms: u64
},
DoneErr {
filename: String
},
Skipped, Skipped,
Message(String) Message(String),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DlReport { pub struct DlReport {
pub id: u32, pub id: u32,
pub status: DlStatus pub status: DlStatus,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct DlReporter { pub struct DlReporter {
id: u32, id: u32,
transmitter: mpsc::UnboundedSender<DlReport> transmitter: mpsc::UnboundedSender<DlReport>,
} }
impl DlReporter { impl DlReporter {
pub fn new(id: u32, transmitter: mpsc::UnboundedSender<DlReport>) -> DlReporter { pub fn new(id: u32, transmitter: mpsc::UnboundedSender<DlReport>) -> DlReporter {
DlReporter { DlReporter {
id: id, id: id,
transmitter: transmitter transmitter: transmitter,
} }
} }
pub fn send(& self, status: DlStatus) { pub fn send(&self, status: DlStatus) {
// This should not fail, so unwrap it here instead propagating the error // This should not fail, so unwrap it here instead propagating the error
self.transmitter.send( self.transmitter
DlReport { .send(DlReport {
id: self.id, id: self.id,
status: status status,
} })
).unwrap(); .unwrap();
} }
} }
struct InfoHolder { struct InfoHolder {
filename: String, filename: String,
total_size: u64, total_size: u64,
progress: u64, progress: u64,
speed_mbps: f32 speed_mbps: f32,
} }
impl InfoHolder { impl InfoHolder {
fn new(filename: String, total_size: u64) -> InfoHolder { fn new(filename: String, total_size: u64) -> InfoHolder {
InfoHolder { InfoHolder {
filename, filename,
total_size, total_size,
progress: 0, progress: 0,
speed_mbps: 0.0 speed_mbps: 0.0,
} }
} }
} }
fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mut VecDeque<String>, moved_lines: u16, file_count_completed: i32, file_count_total: i32) -> ResBE<u16> { fn print_accumulated_report(
statuses: &HashMap<u32, InfoHolder>,
msg_queue: &mut VecDeque<String>,
moved_lines: u16,
file_count_completed: i32,
file_count_total: i32,
) -> ResBE<u16> {
let mut dl_speed_sum = 0.0; let mut dl_speed_sum = 0.0;
execute!( execute!(
@ -94,7 +86,6 @@ fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mu
)?; )?;
for msg in msg_queue.drain(..) { for msg in msg_queue.drain(..) {
let ct_now = chrono::Local::now(); let ct_now = chrono::Local::now();
execute!( execute!(
@ -104,7 +95,7 @@ fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mu
Print("\n") Print("\n")
)?; )?;
} }
execute!( execute!(
stdout(), stdout(),
Print(format!("----------------------------------------")), Print(format!("----------------------------------------")),
@ -113,12 +104,14 @@ fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mu
)?; )?;
for (_k, v) in statuses { for (_k, v) in statuses {
let percent_complete = v.progress as f64 / v.total_size as f64 * 100.0; let percent_complete = v.progress as f64 / v.total_size as f64 * 100.0;
execute!( execute!(
stdout(), stdout(),
Print(format!("Status: {:6.2} mb/s {:5.2}% completed '{}'", v.speed_mbps, percent_complete, v.filename)), Print(format!(
"Status: {:6.2} mb/s {:5.2}% completed '{}'",
v.speed_mbps, percent_complete, v.filename
)),
Clear(ClearType::UntilNewLine), Clear(ClearType::UntilNewLine),
Print("\n") Print("\n")
)?; )?;
@ -132,7 +125,10 @@ fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mu
stdout(), stdout(),
Clear(ClearType::CurrentLine), Clear(ClearType::CurrentLine),
Print("\n"), Print("\n"),
Print(format!(" =>> Accumulated download speed: {:6.2} mb/s {}/{} files, {:.0}%", dl_speed_sum, file_count_completed, file_count_total, file_percent_completed)), Print(format!(
" =>> Accumulated download speed: {:6.2} mb/s {}/{} files, {:.0}%",
dl_speed_sum, file_count_completed, file_count_total, file_percent_completed
)),
Clear(ClearType::UntilNewLine), Clear(ClearType::UntilNewLine),
Print("\n"), Print("\n"),
Clear(ClearType::FromCursorDown), Clear(ClearType::FromCursorDown),
@ -143,8 +139,10 @@ fn print_accumulated_report(statuses: & HashMap<u32, InfoHolder>, msg_queue: &mu
Ok(statuses.len() as u16 + 3) Ok(statuses.len() as u16 + 3)
} }
pub async fn watch_and_print_reports(mut receiver: mpsc::UnboundedReceiver<DlReport>, file_count_total: i32) -> ResBE<()> { pub async fn watch_and_print_reports(
mut receiver: mpsc::UnboundedReceiver<DlReport>,
file_count_total: i32,
) -> ResBE<()> {
let mut statuses: HashMap<u32, InfoHolder> = HashMap::new(); let mut statuses: HashMap<u32, InfoHolder> = HashMap::new();
let mut moved_lines = 0; let mut moved_lines = 0;
let mut msg_queue = VecDeque::new(); let mut msg_queue = VecDeque::new();
@ -157,23 +155,25 @@ pub async fn watch_and_print_reports(mut receiver: mpsc::UnboundedReceiver<DlRep
while let Some(update) = receiver.recv().await { while let Some(update) = receiver.recv().await {
match update.status { match update.status {
DlStatus::Init { DlStatus::Init {
bytes_total, bytes_total,
filename filename,
} => { } => {
msg_queue.push_back(format!("Starting download for file '{}'", &filename)); msg_queue.push_back(format!("Starting download for file '{}'", &filename));
statuses.insert(update.id, InfoHolder::new(filename, bytes_total)); statuses.insert(update.id, InfoHolder::new(filename, bytes_total));
moved_lines = print_accumulated_report(&statuses, &mut msg_queue, moved_lines, file_count_done, file_count_total)?; moved_lines = print_accumulated_report(
&statuses,
}, &mut msg_queue,
moved_lines,
file_count_done,
file_count_total,
)?;
}
DlStatus::Update { DlStatus::Update {
speed_mbps, speed_mbps,
bytes_curr bytes_curr,
} => { } => {
// Scope the reference to prevent borrowing conflict later // Scope the reference to prevent borrowing conflict later
{ {
let s = &mut statuses.get_mut(&update.id).unwrap(); let s = &mut statuses.get_mut(&update.id).unwrap();
@ -182,68 +182,82 @@ pub async fn watch_and_print_reports(mut receiver: mpsc::UnboundedReceiver<DlRep
} }
if t_last.elapsed().unwrap().as_millis() > 500 { if t_last.elapsed().unwrap().as_millis() > 500 {
moved_lines = print_accumulated_report(&statuses, &mut msg_queue, moved_lines, file_count_done, file_count_total)?; moved_lines = print_accumulated_report(
&statuses,
&mut msg_queue,
moved_lines,
file_count_done,
file_count_total,
)?;
t_last = SystemTime::now(); t_last = SystemTime::now();
} }
}
}, DlStatus::Done { duration_ms } => {
DlStatus::Done {
duration_ms
} => {
msg_queue.push_back(format!( msg_queue.push_back(format!(
"Finished downloading '{}' with {:.2} mb in {:.2} seconds", "Finished downloading '{}' with {:.2} mb in {:.2} seconds",
&statuses.get(&update.id).unwrap().filename, &statuses.get(&update.id).unwrap().filename,
(statuses.get(&update.id).unwrap().total_size as f32 / 1_000_000.0), (statuses.get(&update.id).unwrap().total_size as f32 / 1_000_000.0),
(duration_ms as f32 / 1_000.0) (duration_ms as f32 / 1_000.0)
)); ));
statuses.remove(&update.id); statuses.remove(&update.id);
file_count_completed += 1; file_count_completed += 1;
file_count_done += 1; file_count_done += 1;
}
}, DlStatus::DoneErr { filename } => {
DlStatus::DoneErr { msg_queue.push_back(format!("Error: Download failed: '{}'", filename));
filename
} => {
msg_queue.push_back(format!(
"Error: Download failed: '{}'", filename
));
// Don't care if it exists, just make sure it is gone // Don't care if it exists, just make sure it is gone
statuses.remove(&update.id); statuses.remove(&update.id);
// Refresh display // Refresh display
moved_lines = print_accumulated_report(&statuses, &mut msg_queue, moved_lines, file_count_done, file_count_total)?; moved_lines = print_accumulated_report(
&statuses,
&mut msg_queue,
moved_lines,
file_count_done,
file_count_total,
)?;
t_last = SystemTime::now(); t_last = SystemTime::now();
file_count_failed += 1; file_count_failed += 1;
file_count_done += 1; file_count_done += 1;
}
},
DlStatus::Message(msg) => { DlStatus::Message(msg) => {
msg_queue.push_back(msg); msg_queue.push_back(msg);
moved_lines = print_accumulated_report(&statuses, &mut msg_queue, moved_lines, file_count_done, file_count_total)?; moved_lines = print_accumulated_report(
&statuses,
&mut msg_queue,
moved_lines,
file_count_done,
file_count_total,
)?;
t_last = SystemTime::now(); t_last = SystemTime::now();
}, }
DlStatus::Skipped => { DlStatus::Skipped => {
file_count_completed += 1; file_count_completed += 1;
file_count_done += 1; file_count_done += 1;
} }
} }
} }
print_accumulated_report(&statuses, &mut msg_queue, moved_lines, file_count_done, file_count_total)?; print_accumulated_report(
&statuses,
&mut msg_queue,
moved_lines,
file_count_done,
file_count_total,
)?;
execute!( execute!(
stdout(), stdout(),
MoveToPreviousLine(2), MoveToPreviousLine(2),
Print(format!("All done! {}/{} completed, {} failed\n", file_count_completed, file_count_total, file_count_failed)), Print(format!(
"All done! {}/{} completed, {} failed\n",
file_count_completed, file_count_total, file_count_failed
)),
Clear(ClearType::FromCursorDown) Clear(ClearType::FromCursorDown)
)?; )?;

View File

@ -1,27 +1,25 @@
use std::path::Path;
use tokio::io::{ AsyncWriteExt, AsyncSeekExt };
use std::time::SystemTime;
use percent_encoding::percent_decode_str;
use std::io::SeekFrom;
use tokio::sync::mpsc;
use futures::stream::FuturesUnordered; use futures::stream::FuturesUnordered;
use futures::StreamExt; use futures::StreamExt;
use percent_encoding::percent_decode_str;
use std::io::SeekFrom;
use std::path::Path;
use std::time::SystemTime;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
use tokio::sync::mpsc;
use crate::errors::*;
use crate::dlreport::*; use crate::dlreport::*;
use crate::errors::*;
struct RollingAverage { struct RollingAverage {
index: usize, index: usize,
data: Vec<f64> data: Vec<f64>,
} }
impl RollingAverage { impl RollingAverage {
fn new(size: usize) -> Self { fn new(size: usize) -> Self {
RollingAverage { RollingAverage {
index: 0, index: 0,
data: Vec::with_capacity(size) data: Vec::with_capacity(size),
} }
} }
@ -51,38 +49,49 @@ impl RollingAverage {
fn add(&mut self, val: f64) { fn add(&mut self, val: f64) {
if self.data.capacity() == self.data.len() { if self.data.capacity() == self.data.len() {
self.data[self.index] = val; self.data[self.index] = val;
self.index += 1; self.index += 1;
if self.index >= self.data.capacity() { if self.index >= self.data.capacity() {
self.index = 0; self.index = 0;
} }
} else { } else {
self.data.push(val); self.data.push(val);
} }
} }
} }
/// Get the filename at the end of the given URL. This will decode the URL Encoding. /// Get the filename at the end of the given URL. This will decode the URL Encoding.
pub fn url_to_filename(url: &str) -> String { pub fn url_to_filename(url: &str) -> String {
let url_dec = percent_decode_str(&url).decode_utf8_lossy().to_owned().to_string(); let url_dec = percent_decode_str(&url)
let file_name = std::path::Path::new(&url_dec).file_name().unwrap().to_str().unwrap(); .decode_utf8_lossy()
.to_owned()
.to_string();
let file_name = std::path::Path::new(&url_dec)
.file_name()
.unwrap()
.to_str()
.unwrap();
// Split at ? and return the first part. If no ? is present, this just returns the full string // Split at ? and return the first part. If no ? is present, this just returns the full string
file_name.split("?").next().unwrap().to_string() file_name.split("?").next().unwrap().to_string()
} }
pub async fn download_feedback(url: &str, into_file: &str, rep: DlReporter, content_length: Option<u64>) -> ResBE<()> { pub async fn download_feedback(
url: &str,
download_feedback_chunks(url, into_file, rep, None, false, content_length).await into_file: &Path,
rep: DlReporter,
content_length: Option<u64>,
) -> ResBE<()> {
download_feedback_chunks(url, into_file, rep, None, content_length).await
} }
pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporter, from_to: Option<(u64, u64)>, seek_from: bool, content_length: Option<u64>) -> ResBE<()> { pub async fn download_feedback_chunks(
let into_file = Path::new(into_file); url: &str,
into_file: &Path,
rep: DlReporter,
from_to: Option<(u64, u64)>,
content_length: Option<u64>,
) -> ResBE<()> {
let mut content_length = match content_length { let mut content_length = match content_length {
Some(it) => it, Some(it) => it,
None => { None => {
@ -92,15 +101,14 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
}; };
// Send the HTTP request to download the given link // Send the HTTP request to download the given link
let mut req = reqwest::Client::new() let mut req = reqwest::Client::new().get(url);
.get(url);
// Add range header if needed // Add range header if needed
if let Some(from_to) = from_to { if let Some((from, to)) = from_to {
req = req.header(reqwest::header::RANGE, format!("bytes={}-{}", from_to.0, from_to.1)); req = req.header(reqwest::header::RANGE, format!("bytes={}-{}", from, to));
content_length = from_to.1 - from_to.0 + 1; content_length = to - from + 1;
} }
// Actually send the request and get the response // Actually send the request and get the response
let mut resp = req.send().await?; let mut resp = req.send().await?;
@ -108,37 +116,27 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(DlError::BadHttpStatus.into()); return Err(DlError::BadHttpStatus.into());
} }
// Open the local output file // Open the local output file
let mut ofile = tokio::fs::OpenOptions::new(); let mut opts = tokio::fs::OpenOptions::new();
let mut ofile = opts
// Create the file if not existant .create(true)
ofile.create(true) .write(true)
// Open in write mode .truncate(!from_to.is_some())
.write(true); .open(into_file)
.await?;
// If seek_from is specified, the file cant be overwritten if from_to.is_some() {
if !seek_from {
// Delete and overwrite the file
ofile.truncate(true);
}
let mut ofile = ofile.open(into_file).await?;
if seek_from {
ofile.seek(SeekFrom::Start(from_to.unwrap().0)).await?; ofile.seek(SeekFrom::Start(from_to.unwrap().0)).await?;
} }
let filename = into_file.file_name().unwrap().to_str().unwrap(); let filename = into_file.file_name().unwrap().to_str().unwrap();
// Report the download start // Report the download start
rep.send( rep.send(DlStatus::Init {
DlStatus::Init { bytes_total: content_length,
bytes_total: content_length, filename: filename.to_string(),
filename: filename.to_string() });
}
);
let mut curr_progress = 0; let mut curr_progress = 0;
let mut speed_mbps = 0.0; let mut speed_mbps = 0.0;
@ -154,17 +152,15 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
// Read data from server as long as new data is available // Read data from server as long as new data is available
while let Some(chunk) = resp.chunk().await? { while let Some(chunk) = resp.chunk().await? {
let datalen = chunk.len() as u64; let datalen = chunk.len() as u64;
buff.extend(chunk); buff.extend(chunk);
// Buffer in memory first and only write to disk if the threshold is reached. // Buffer in memory first and only write to disk if the threshold is reached.
// This reduces the number of small disk writes and thereby reduces the // This reduces the number of small disk writes and thereby reduces the
// io bottleneck that occurs on HDDs with many small writes in different // io bottleneck that occurs on HDDs with many small writes in different
// files and offsets at the same time // files and offsets at the same time
if buff.len() >= 1_000_000 { if buff.len() >= 1_000_000 {
// Write the received data into the file // Write the received data into the file
ofile.write_all(&buff).await?; ofile.write_all(&buff).await?;
@ -179,14 +175,11 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
let t_elapsed = t_last_speed.elapsed()?.as_secs_f64(); let t_elapsed = t_last_speed.elapsed()?.as_secs_f64();
// Update the reported download speed after every 5MB or every second // Update the reported download speed after every 3MB or every second
// depending on what happens first // depending on what happens first
if last_bytecount >= 3_000_000 || t_elapsed >= 0.8 { if last_bytecount >= 3_000_000 || t_elapsed >= 0.8 {
// Update rolling average // Update rolling average
average_speed.add( average_speed.add(((last_bytecount as f64) / t_elapsed) / 1_000_000.0);
((last_bytecount as f64) / t_elapsed) / 1_000_000.0
);
speed_mbps = average_speed.value() as f32; speed_mbps = average_speed.value() as f32;
@ -196,12 +189,10 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
} }
// Send status update report // Send status update report
rep.send( rep.send(DlStatus::Update {
DlStatus::Update { speed_mbps,
speed_mbps: speed_mbps, bytes_curr: curr_progress,
bytes_curr: curr_progress });
}
);
} }
if buff.len() > 0 { if buff.len() > 0 {
@ -211,34 +202,32 @@ pub async fn download_feedback_chunks(url: &str, into_file: &str, rep: DlReporte
if curr_progress != content_length { if curr_progress != content_length {
return Err(DlError::HttpNoData.into()); return Err(DlError::HttpNoData.into());
} }
// Ensure that IO is completed // Ensure that IO is completed
//ofile.flush().await?; //ofile.flush().await?;
let duration_ms = t_start.elapsed()?.as_millis() as u64; let duration_ms = t_start.elapsed()?.as_millis() as u64;
// Send report that the download is finished // Send report that the download is finished
rep.send( rep.send(DlStatus::Done { duration_ms });
DlStatus::Done {
duration_ms: duration_ms
}
);
Ok(()) Ok(())
} }
// This will spin up multiple tasks that and manage the status updates for them. // This will spin up multiple tasks that and manage the status updates for them.
// The combined status will be reported back to the caller // The combined status will be reported back to the caller
pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter, conn_count: u32, content_length: Option<u64>) -> ResBE<()> { pub async fn download_feedback_multi(
url: &str,
into_file: &Path,
rep: DlReporter,
conn_count: u32,
content_length: Option<u64>,
) -> ResBE<()> {
let content_length = match content_length { let content_length = match content_length {
Some(it) => it, Some(it) => it,
None => { None => http_get_filesize_and_range_support(url).await?.0,
let (content_length, _) = http_get_filesize_and_range_support(url).await?;
content_length
}
}; };
// Create zeroed file with 1 byte too much. This will be truncated on download // Create zeroed file with 1 byte too much. This will be truncated on download
// completion and can indicate that the file is not suitable for continuation // completion and can indicate that the file is not suitable for continuation
create_zeroed_file(into_file, content_length as usize + 1).await?; create_zeroed_file(into_file, content_length as usize + 1).await?;
@ -252,21 +241,16 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
let t_start = SystemTime::now(); let t_start = SystemTime::now();
for index in 0 .. conn_count { for index in 0..conn_count {
let url = url.clone().to_owned(); let url = url.clone().to_owned();
let into_file = into_file.clone().to_owned(); let into_file = into_file.clone().to_owned();
let tx = tx.clone(); let tx = tx.clone();
joiners.push(tokio::spawn(async move { joiners.push(tokio::spawn(async move {
let rep = DlReporter::new(index, tx.clone()); let rep = DlReporter::new(index, tx.clone());
let mut from_to = ( let mut from_to = (index as u64 * chunksize, (index + 1) as u64 * chunksize - 1);
index as u64 * chunksize,
(index+1) as u64 * chunksize - 1
);
if index == conn_count - 1 { if index == conn_count - 1 {
from_to.1 += rest; from_to.1 += rest;
@ -275,10 +259,17 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
let specific_content_length = from_to.1 - from_to.0 + 1; let specific_content_length = from_to.1 - from_to.0 + 1;
// Delay each chunk-download to reduce the number of simultanious connection attempts // Delay each chunk-download to reduce the number of simultanious connection attempts
tokio::time::sleep(tokio::time::Duration::from_millis(50 *index as u64)).await; tokio::time::sleep(tokio::time::Duration::from_millis(50 * index as u64)).await;
download_feedback_chunks(&url, &into_file, rep, Some(from_to), true, Some(specific_content_length)).await.map_err(|e| e.to_string())
download_feedback_chunks(
&url,
&into_file,
rep,
Some(from_to),
Some(specific_content_length),
)
.await
.map_err(|e| e.to_string())
})) }))
} }
@ -288,7 +279,7 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
rep.send(DlStatus::Init { rep.send(DlStatus::Init {
bytes_total: content_length, bytes_total: content_length,
filename: filename.to_string() filename: filename.to_string(),
}); });
let rep_task = rep.clone(); let rep_task = rep.clone();
@ -296,7 +287,6 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
let mut t_last = t_start.clone(); let mut t_last = t_start.clone();
let manager_handle = tokio::task::spawn(async move { let manager_handle = tokio::task::spawn(async move {
let rep = rep_task; let rep = rep_task;
//let mut dl_speeds = vec![0.0_f32; conn_count as usize]; //let mut dl_speeds = vec![0.0_f32; conn_count as usize];
let mut progresses = vec![0; conn_count as usize]; let mut progresses = vec![0; conn_count as usize];
@ -307,22 +297,17 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
while let Some(update) = rx.recv().await { while let Some(update) = rx.recv().await {
match update.status { match update.status {
DlStatus::Init { DlStatus::Init {
bytes_total: _, bytes_total: _,
filename: _ filename: _,
} => { } => {}
},
DlStatus::Update { DlStatus::Update {
speed_mbps: _, speed_mbps: _,
bytes_curr bytes_curr,
} => { } => {
//dl_speeds[update.id as usize] = speed_mbps; //dl_speeds[update.id as usize] = speed_mbps;
progresses[update.id as usize] = bytes_curr; progresses[update.id as usize] = bytes_curr;
let progress_curr = progresses.iter().sum(); let progress_curr = progresses.iter().sum();
let progress_delta = progress_curr - progress_last; let progress_delta = progress_curr - progress_last;
let t_elapsed = t_last.elapsed().unwrap().as_secs_f64(); let t_elapsed = t_last.elapsed().unwrap().as_secs_f64();
@ -331,48 +316,35 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
// currently executes always, but might change // currently executes always, but might change
if progress_delta >= 5_000_000 { if progress_delta >= 5_000_000 {
average_speed.add(((progress_delta as f64) / 1_000_000.0) / t_elapsed);
average_speed.add(
((progress_delta as f64) / 1_000_000.0) / t_elapsed
);
progress_last = progress_curr; progress_last = progress_curr;
t_last = SystemTime::now(); t_last = SystemTime::now();
} }
rep.send(DlStatus::Update { rep.send(DlStatus::Update {
speed_mbps: speed_mbps, speed_mbps: speed_mbps,
bytes_curr: progress_curr bytes_curr: progress_curr,
}); });
}
}, DlStatus::Done { duration_ms: _ } => {
DlStatus::Done {
duration_ms: _
} => {
//dl_speeds[update.id as usize] = 0.0; //dl_speeds[update.id as usize] = 0.0;
}
},
// Just forwared everything else to the calling receiver
// Just forwared everything else to the calling receiver _ => rep.send(update.status),
_ => rep.send(update.status)
} }
} }
}); });
let mut joiners: FuturesUnordered<_> = joiners.into_iter().collect(); let mut joiners: FuturesUnordered<_> = joiners.into_iter().collect();
// Validate if the tasks were successful. This will always grab the next completed // Validate if the tasks were successful. This will always grab the next completed
// task, independent from the original order in the joiners list // task, independent from the original order in the joiners list
while let Some(output) = joiners.next().await { while let Some(output) = joiners.next().await {
// If any of the download tasks fail, abort the rest and delete the file // If any of the download tasks fail, abort the rest and delete the file
// since it is non-recoverable anyways // since it is non-recoverable anyways
if let Err(e) = output? { if let Err(e) = output? {
for handle in joiners.iter() { for handle in joiners.iter() {
handle.abort(); handle.abort();
} }
@ -398,14 +370,13 @@ pub async fn download_feedback_multi(url: &str, into_file: &str, rep: DlReporter
ofile.set_len(content_length).await?; ofile.set_len(content_length).await?;
rep.send(DlStatus::Done { rep.send(DlStatus::Done {
duration_ms: t_start.elapsed()?.as_millis() as u64 duration_ms: t_start.elapsed()?.as_millis() as u64,
}); });
Ok(()) Ok(())
} }
async fn create_zeroed_file(file: &str, filesize: usize) -> ResBE<()> { async fn create_zeroed_file(file: &Path, filesize: usize) -> ResBE<()> {
let ofile = tokio::fs::OpenOptions::new() let ofile = tokio::fs::OpenOptions::new()
.create(true) .create(true)
// Open in write mode // Open in write mode
@ -414,21 +385,18 @@ async fn create_zeroed_file(file: &str, filesize: usize) -> ResBE<()> {
.truncate(true) .truncate(true)
.open(file) .open(file)
.await?; .await?;
ofile.set_len(filesize as u64).await?; ofile.set_len(filesize as u64).await?;
Ok(()) Ok(())
} }
pub async fn http_get_filesize_and_range_support(url: &str) -> ResBE<(u64, bool)> { pub async fn http_get_filesize_and_range_support(url: &str) -> ResBE<(u64, bool)> {
let resp = reqwest::Client::new() let resp = reqwest::Client::new().head(url).send().await?;
.head(url)
.send().await?;
if let Some(filesize) = resp.headers().get(reqwest::header::CONTENT_LENGTH) { if let Some(filesize) = resp.headers().get(reqwest::header::CONTENT_LENGTH) {
if let Ok(val_str) = filesize.to_str() { if let Ok(val_str) = filesize.to_str() {
if let Ok(val) = val_str.parse::<u64>() { if let Ok(val) = val_str.parse::<u64>() {
let mut range_supported = false; let mut range_supported = false;
if let Some(range) = resp.headers().get(reqwest::header::ACCEPT_RANGES) { if let Some(range) = resp.headers().get(reqwest::header::ACCEPT_RANGES) {
@ -447,9 +415,6 @@ pub async fn http_get_filesize_and_range_support(url: &str) -> ResBE<(u64, bool)
Err(DlError::ContentLengthUnknown.into()) Err(DlError::ContentLengthUnknown.into())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -485,7 +450,7 @@ mod tests {
// This should replace the oldest value (index 1) // This should replace the oldest value (index 1)
ra.add(40.0); ra.add(40.0);
assert_eq!(3, ra.data.len()); assert_eq!(3, ra.data.len());
assert_eq!(3, ra.data.capacity()); assert_eq!(3, ra.data.capacity());
@ -496,7 +461,6 @@ mod tests {
assert_eq!(20.0, ra.data[1]); assert_eq!(20.0, ra.data[1]);
assert_eq!(30.0, ra.data[2]); assert_eq!(30.0, ra.data[2]);
ra.add(50.0); ra.add(50.0);
ra.add(60.0); ra.add(60.0);
ra.add(70.0); ra.add(70.0);
@ -504,6 +468,5 @@ mod tests {
assert_eq!(70.0, ra.data[0]); assert_eq!(70.0, ra.data[0]);
assert_eq!(50.0, ra.data[1]); assert_eq!(50.0, ra.data[1]);
assert_eq!(60.0, ra.data[2]); assert_eq!(60.0, ra.data[2]);
} }
} }

View File

@ -1,5 +1,5 @@
use std::error::Error; use std::error::Error;
use std::fmt::{ self, Display, Formatter }; use std::fmt::{self, Display, Formatter};
/// Result Boxed Error /// Result Boxed Error
pub type ResBE<T> = Result<T, Box<dyn Error>>; pub type ResBE<T> = Result<T, Box<dyn Error>>;
@ -10,20 +10,18 @@ pub enum DlError {
BadHttpStatus, BadHttpStatus,
ContentLengthUnknown, ContentLengthUnknown,
HttpNoData, HttpNoData,
Other(String) Other(String),
} }
impl Error for DlError {} impl Error for DlError {}
impl Display for DlError { impl Display for DlError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
DlError::BadHttpStatus => write!(f, "Bad http response status"), DlError::BadHttpStatus => write!(f, "Bad http response status"),
DlError::ContentLengthUnknown => write!(f, "Content-Length is unknown"), DlError::ContentLengthUnknown => write!(f, "Content-Length is unknown"),
DlError::HttpNoData => write!(f, "Http server sent no more data"), DlError::HttpNoData => write!(f, "Http server sent no more data"),
DlError::Other(s) => write!(f, "Unknown download error: '{}'", s) DlError::Other(s) => write!(f, "Unknown download error: '{}'", s),
} }
} }
}
}

View File

@ -1,4 +1,10 @@
use std::{collections::VecDeque, path::Path, process::exit, sync::Arc, time::SystemTime}; use std::{
collections::VecDeque,
path::{Path, PathBuf},
process::exit,
sync::Arc,
time::SystemTime,
};
use clap::Parser; use clap::Parser;
use download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support}; use download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support};
@ -137,13 +143,13 @@ async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_
.clone() .clone()
.unwrap_or_else(|| download::url_to_filename(&url).into()); .unwrap_or_else(|| download::url_to_filename(&url).into());
let into_file = cli_args let into_file: PathBuf = cli_args
.outdir .outdir
.join(Path::new(&file_name)) .join(Path::new(&file_name))
.to_str() .to_str()
.unwrap() .unwrap()
.to_string(); .to_string()
let path_into_file = Path::new(&into_file); .into();
let (filesize, range_supported) = match http_get_filesize_and_range_support(&url).await { let (filesize, range_supported) = match http_get_filesize_and_range_support(&url).await {
Ok((filesize, range_supported)) => (filesize, range_supported), Ok((filesize, range_supported)) => (filesize, range_supported),
@ -157,8 +163,8 @@ async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_
}; };
// If file with same name is present locally, check filesize // If file with same name is present locally, check filesize
if path_into_file.exists() { if into_file.exists() {
let local_filesize = std::fs::metadata(path_into_file).unwrap().len(); let local_filesize = std::fs::metadata(&into_file).unwrap().len();
if filesize == local_filesize { if filesize == local_filesize {
reporter.send(DlStatus::Message(format!( reporter.send(DlStatus::Message(format!(

View File

@ -1,5 +1,5 @@
use regex::Regex; use regex::Regex;
use std::io::{ Error, ErrorKind }; use std::io::{Error, ErrorKind};
use crate::errors::ResBE; use crate::errors::ResBE;
@ -13,12 +13,11 @@ Link generation code:
- `$6` is dependent on the file - `$6` is dependent on the file
- The numbers in the calculation part ($2`, `$3`, `$4` and `$5`) are hard coded - The numbers in the calculation part ($2`, `$3`, `$4` and `$5`) are hard coded
``` ```
document.getElementById('dlbutton').href = "/d/0Ky7p1C6/" + (186549 % 51245 + 186549 % 913) + "/some-file-name.part1.rar"; document.getElementById('dlbutton').href = "/d/0Ky7p1C6/" + (186549 % 51245 + 186549 % 913) + "/some-file-name.part1.rar";
``` ```
*/ */
pub async fn resolve_link(url: &str) -> ResBE<String> { pub async fn resolve_link(url: &str) -> ResBE<String> {
// Regex to check if the provided url is a zippyshare download url // Regex to check if the provided url is a zippyshare download url
let re = Regex::new(r"(https://www\d*\.zippyshare\.com)")?; let re = Regex::new(r"(https://www\d*\.zippyshare\.com)")?;
if !re.is_match(&url) { if !re.is_match(&url) {
@ -29,17 +28,18 @@ pub async fn resolve_link(url: &str) -> ResBE<String> {
let base_host = &re.captures(&url).unwrap()[0]; let base_host = &re.captures(&url).unwrap()[0];
// Download the html body for the download page // Download the html body for the download page
let body = reqwest::get(url).await? let body = reqwest::get(url).await?.text().await?;
.text().await?;
// Regex to match the javascript part of the html that generates the real download link // Regex to match the javascript part of the html that generates the real download link
let re_link = Regex::new(r#"document\.getElementById\('dlbutton'\)\.href = "(/d/.+/)" \+ \((\d+) % (\d+) \+ \d+ % (\d+)\) \+ "(.+)";"#)?; let re_link = Regex::new(
r#"document\.getElementById\('dlbutton'\)\.href = "(/d/.+/)" \+ \((\d+) % (\d+) \+ \d+ % (\d+)\) \+ "(.+)";"#,
)?;
let cap_link = match re_link.captures(&body) { let cap_link = match re_link.captures(&body) {
Some(cap) => cap, Some(cap) => cap,
None => return Err(Error::new(ErrorKind::Other, "Link not found").into()) None => return Err(Error::new(ErrorKind::Other, "Link not found").into()),
}; };
let url_start = &cap_link[1]; let url_start = &cap_link[1];
let url_end = &cap_link[5]; let url_end = &cap_link[5];
let n2: i32 = i32::from_str_radix(&cap_link[2], 10)?; let n2: i32 = i32::from_str_radix(&cap_link[2], 10)?;