Compare commits
No commits in common. "16d0edbbb67baf61d1fe610bc2f39ad1cc279c69" and "59de02d34dd425eb67fb209d3cbdfca79b95480f" have entirely different histories.
16d0edbbb6
...
59de02d34d
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -11,12 +11,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.56"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@ -176,7 +170,6 @@ dependencies = [
|
|||||||
name = "ffdl"
|
name = "ffdl"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@ -184,7 +177,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -991,26 +983,6 @@ version = "0.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
|
|||||||
@ -14,5 +14,3 @@ regex = "1.5.5"
|
|||||||
crossterm = "0.23.1"
|
crossterm = "0.23.1"
|
||||||
clap = { version = "3.1.6", features = [ "derive" ] }
|
clap = { version = "3.1.6", features = [ "derive" ] }
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
thiserror = "1.0.30"
|
|
||||||
anyhow = "1.0.56"
|
|
||||||
|
|||||||
16
src/args.rs
16
src/args.rs
@ -18,6 +18,14 @@ pub struct CLIArgs {
|
|||||||
)]
|
)]
|
||||||
pub outdir: PathBuf,
|
pub outdir: PathBuf,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
short = 'i',
|
||||||
|
long = "into-file",
|
||||||
|
value_name = "FILENAME",
|
||||||
|
help = "Force filename. This only works for single file downloads",
|
||||||
|
)]
|
||||||
|
pub into_file: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
short = 'n',
|
short = 'n',
|
||||||
long = "num-files",
|
long = "num-files",
|
||||||
@ -39,6 +47,14 @@ pub struct CLIArgs {
|
|||||||
)]
|
)]
|
||||||
pub conn_count: NonZeroU32,
|
pub conn_count: NonZeroU32,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
short = 'z',
|
||||||
|
long = "zippy",
|
||||||
|
help = "The provided URLs are zippyshare URLs and need to be \
|
||||||
|
resolved to direct download urls",
|
||||||
|
)]
|
||||||
|
pub zippy: bool,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
short = 'l',
|
short = 'l',
|
||||||
long = "listfile",
|
long = "listfile",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use crossterm::execute;
|
|||||||
use crossterm::style::Print;
|
use crossterm::style::Print;
|
||||||
use crossterm::terminal::{Clear, ClearType};
|
use crossterm::terminal::{Clear, ClearType};
|
||||||
|
|
||||||
use anyhow::Result;
|
use crate::errors::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DlStatus {
|
pub enum DlStatus {
|
||||||
@ -35,7 +35,10 @@ pub struct DlReporter {
|
|||||||
|
|
||||||
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 { id, transmitter }
|
DlReporter {
|
||||||
|
id: id,
|
||||||
|
transmitter: transmitter,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self, status: DlStatus) {
|
pub fn send(&self, status: DlStatus) {
|
||||||
@ -47,46 +50,6 @@ impl DlReporter {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, bytes_total: u64, filename: String) {
|
|
||||||
self.send(DlStatus::Init {
|
|
||||||
bytes_total,
|
|
||||||
filename,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, speed_mbps: f32, bytes_curr: u64) {
|
|
||||||
self.send(DlStatus::Update {
|
|
||||||
speed_mbps,
|
|
||||||
bytes_curr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn done(&self, duration_ms: u64) {
|
|
||||||
self.send(DlStatus::Done { duration_ms })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn done_err(&self, filename: String) {
|
|
||||||
self.send(DlStatus::DoneErr { filename })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn skipped(&self) {
|
|
||||||
self.send(DlStatus::Skipped);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn msg(&self, msg: String) {
|
|
||||||
self.send(DlStatus::Message(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! report_msg {
|
|
||||||
($rep:ident, $fmt:expr) => {
|
|
||||||
DlReporter::msg(&$rep, $fmt.to_string());
|
|
||||||
};
|
|
||||||
($rep:ident, $fmt:expr, $($fmt2:expr),+) => {
|
|
||||||
DlReporter::msg(&$rep, format!($fmt, $($fmt2,)+));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InfoHolder {
|
struct InfoHolder {
|
||||||
@ -113,7 +76,7 @@ fn print_accumulated_report(
|
|||||||
moved_lines: u16,
|
moved_lines: u16,
|
||||||
file_count_completed: i32,
|
file_count_completed: i32,
|
||||||
file_count_total: i32,
|
file_count_total: i32,
|
||||||
) -> Result<u16> {
|
) -> ResBE<u16> {
|
||||||
let mut dl_speed_sum = 0.0;
|
let mut dl_speed_sum = 0.0;
|
||||||
|
|
||||||
execute!(
|
execute!(
|
||||||
@ -135,12 +98,12 @@ fn print_accumulated_report(
|
|||||||
|
|
||||||
execute!(
|
execute!(
|
||||||
stdout(),
|
stdout(),
|
||||||
Print("----------------------------------------".to_string()),
|
Print(format!("----------------------------------------")),
|
||||||
Clear(ClearType::UntilNewLine),
|
Clear(ClearType::UntilNewLine),
|
||||||
Print("\n")
|
Print("\n")
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for v in statuses.values() {
|
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!(
|
||||||
@ -179,7 +142,7 @@ fn print_accumulated_report(
|
|||||||
pub async fn watch_and_print_reports(
|
pub async fn watch_and_print_reports(
|
||||||
mut receiver: mpsc::UnboundedReceiver<DlReport>,
|
mut receiver: mpsc::UnboundedReceiver<DlReport>,
|
||||||
file_count_total: i32,
|
file_count_total: i32,
|
||||||
) -> Result<()> {
|
) -> 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();
|
||||||
|
|||||||
106
src/download.rs
106
src/download.rs
@ -1,4 +1,3 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
@ -25,7 +24,7 @@ impl RollingAverage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn value(&self) -> f64 {
|
fn value(&self) -> f64 {
|
||||||
if self.data.is_empty() {
|
if self.data.len() == 0 {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
let mut max = self.data[0];
|
let mut max = self.data[0];
|
||||||
@ -64,7 +63,7 @@ impl RollingAverage {
|
|||||||
|
|
||||||
/// 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)
|
let url_dec = percent_decode_str(&url)
|
||||||
.decode_utf8_lossy()
|
.decode_utf8_lossy()
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.to_string();
|
.to_string();
|
||||||
@ -74,7 +73,7 @@ pub fn url_to_filename(url: &str) -> String {
|
|||||||
.to_str()
|
.to_str()
|
||||||
.unwrap();
|
.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(
|
pub async fn download_feedback(
|
||||||
@ -82,7 +81,7 @@ pub async fn download_feedback(
|
|||||||
into_file: &Path,
|
into_file: &Path,
|
||||||
rep: DlReporter,
|
rep: DlReporter,
|
||||||
content_length: Option<u64>,
|
content_length: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> ResBE<()> {
|
||||||
download_feedback_chunks(url, into_file, rep, None, content_length).await
|
download_feedback_chunks(url, into_file, rep, None, content_length).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +91,13 @@ pub async fn download_feedback_chunks(
|
|||||||
rep: DlReporter,
|
rep: DlReporter,
|
||||||
from_to: Option<(u64, u64)>,
|
from_to: Option<(u64, u64)>,
|
||||||
content_length: Option<u64>,
|
content_length: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> ResBE<()> {
|
||||||
let mut content_length = match content_length {
|
let mut content_length = match content_length {
|
||||||
Some(it) => it,
|
Some(it) => it,
|
||||||
None => http_get_filesize_and_range_support(url).await?.filesize,
|
None => {
|
||||||
|
let (content_length, _) = http_get_filesize_and_range_support(url).await?;
|
||||||
|
content_length
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the HTTP request to download the given link
|
// Send the HTTP request to download the given link
|
||||||
@ -120,7 +122,7 @@ pub async fn download_feedback_chunks(
|
|||||||
let mut ofile = opts
|
let mut ofile = opts
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(from_to.is_none())
|
.truncate(!from_to.is_some())
|
||||||
.open(into_file)
|
.open(into_file)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -131,7 +133,10 @@ pub async fn download_feedback_chunks(
|
|||||||
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.init(content_length, filename.to_string());
|
rep.send(DlStatus::Init {
|
||||||
|
bytes_total: content_length,
|
||||||
|
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;
|
||||||
@ -184,10 +189,13 @@ pub async fn download_feedback_chunks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send status update report
|
// Send status update report
|
||||||
rep.update(speed_mbps, curr_progress);
|
rep.send(DlStatus::Update {
|
||||||
|
speed_mbps,
|
||||||
|
bytes_curr: curr_progress,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !buff.is_empty() {
|
if buff.len() > 0 {
|
||||||
ofile.write_all(&buff).await?;
|
ofile.write_all(&buff).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +209,7 @@ pub async fn download_feedback_chunks(
|
|||||||
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.done(duration_ms);
|
rep.send(DlStatus::Done { duration_ms });
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -214,10 +222,10 @@ pub async fn download_feedback_multi(
|
|||||||
rep: DlReporter,
|
rep: DlReporter,
|
||||||
conn_count: u32,
|
conn_count: u32,
|
||||||
content_length: Option<u64>,
|
content_length: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> ResBE<()> {
|
||||||
let content_length = match content_length {
|
let content_length = match content_length {
|
||||||
Some(it) => it,
|
Some(it) => it,
|
||||||
None => http_get_filesize_and_range_support(url).await?.filesize,
|
None => http_get_filesize_and_range_support(url).await?.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@ -234,8 +242,8 @@ pub async fn download_feedback_multi(
|
|||||||
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.to_owned();
|
let url = url.clone().to_owned();
|
||||||
let into_file = into_file.to_owned();
|
let into_file = into_file.clone().to_owned();
|
||||||
|
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
|
|
||||||
@ -261,6 +269,7 @@ pub async fn download_feedback_multi(
|
|||||||
Some(specific_content_length),
|
Some(specific_content_length),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,11 +277,14 @@ pub async fn download_feedback_multi(
|
|||||||
|
|
||||||
let filename = Path::new(into_file).file_name().unwrap().to_str().unwrap();
|
let filename = Path::new(into_file).file_name().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
rep.init(content_length, filename.to_string());
|
rep.send(DlStatus::Init {
|
||||||
|
bytes_total: content_length,
|
||||||
|
filename: filename.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
let rep_task = rep.clone();
|
let rep_task = rep.clone();
|
||||||
|
|
||||||
let mut t_last = t_start;
|
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;
|
||||||
@ -310,7 +322,10 @@ pub async fn download_feedback_multi(
|
|||||||
t_last = SystemTime::now();
|
t_last = SystemTime::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
rep.update(speed_mbps, progress_curr);
|
rep.send(DlStatus::Update {
|
||||||
|
speed_mbps: speed_mbps,
|
||||||
|
bytes_curr: progress_curr,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
DlStatus::Done { duration_ms: _ } => {
|
DlStatus::Done { duration_ms: _ } => {
|
||||||
|
|
||||||
@ -338,7 +353,7 @@ pub async fn download_feedback_multi(
|
|||||||
|
|
||||||
tokio::fs::remove_file(&into_file).await?;
|
tokio::fs::remove_file(&into_file).await?;
|
||||||
|
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,53 +369,50 @@ pub async fn download_feedback_multi(
|
|||||||
|
|
||||||
ofile.set_len(content_length).await?;
|
ofile.set_len(content_length).await?;
|
||||||
|
|
||||||
rep.done(t_start.elapsed()?.as_millis() as u64);
|
rep.send(DlStatus::Done {
|
||||||
|
duration_ms: t_start.elapsed()?.as_millis() as u64,
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_zeroed_file(file: &Path, filesize: usize) -> Result<()> {
|
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
|
||||||
.write(true)
|
.write(true)
|
||||||
|
// Delete and overwrite the file
|
||||||
.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 struct HttpFileInfo {
|
pub async fn http_get_filesize_and_range_support(url: &str) -> ResBE<(u64, bool)> {
|
||||||
pub filesize: u64,
|
|
||||||
pub range_support: bool,
|
|
||||||
pub filename: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn http_get_filesize_and_range_support(url: &str) -> Result<HttpFileInfo> {
|
|
||||||
let resp = reqwest::Client::new().head(url).send().await?;
|
let resp = reqwest::Client::new().head(url).send().await?;
|
||||||
|
|
||||||
let filesize = resp
|
if let Some(filesize) = resp.headers().get(reqwest::header::CONTENT_LENGTH) {
|
||||||
.headers()
|
if let Ok(val_str) = filesize.to_str() {
|
||||||
.get(reqwest::header::CONTENT_LENGTH)
|
if let Ok(val) = val_str.parse::<u64>() {
|
||||||
.and_then(|it| it.to_str().unwrap().parse::<u64>().ok())
|
let mut range_supported = false;
|
||||||
.ok_or(DlError::ContentLengthUnknown)?;
|
|
||||||
|
|
||||||
let range = resp
|
if let Some(range) = resp.headers().get(reqwest::header::ACCEPT_RANGES) {
|
||||||
.headers()
|
if let Ok(range) = range.to_str() {
|
||||||
.get(reqwest::header::ACCEPT_RANGES)
|
if range == "bytes" {
|
||||||
.and_then(|it| it.to_str().ok());
|
range_supported = true;
|
||||||
let range_support = matches!(range, Some("bytes"));
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filename = url_to_filename(url);
|
return Ok((val, range_supported));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let info = HttpFileInfo {
|
Err(DlError::ContentLengthUnknown.into())
|
||||||
filesize,
|
|
||||||
range_support,
|
|
||||||
filename,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
use thiserror::Error;
|
use std::error::Error;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
/// Result Boxed Error
|
||||||
|
pub type ResBE<T> = Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Error, Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DlError {
|
pub enum DlError {
|
||||||
#[error("Bad http response status")]
|
|
||||||
BadHttpStatus,
|
BadHttpStatus,
|
||||||
#[error("Content-Length is unknown")]
|
|
||||||
ContentLengthUnknown,
|
ContentLengthUnknown,
|
||||||
#[error("Http server sent no more data")]
|
|
||||||
HttpNoData,
|
HttpNoData,
|
||||||
#[error("Unknown download error: '{0}'")]
|
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Error for DlError {}
|
||||||
|
|
||||||
|
impl Display for DlError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
DlError::BadHttpStatus => write!(f, "Bad http response status"),
|
||||||
|
DlError::ContentLengthUnknown => write!(f, "Content-Length is unknown"),
|
||||||
|
DlError::HttpNoData => write!(f, "Http server sent no more data"),
|
||||||
|
DlError::Other(s) => write!(f, "Unknown download error: '{}'", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
93
src/main.rs
93
src/main.rs
@ -7,6 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
@ -18,13 +19,10 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
args::CLIArgs,
|
args::CLIArgs,
|
||||||
dlreport::{watch_and_print_reports, DlReport, DlReporter},
|
dlreport::{watch_and_print_reports, DlReport, DlReporter, DlStatus},
|
||||||
download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support},
|
errors::ResBE,
|
||||||
zippy::is_zippyshare_url,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
mod dlreport;
|
mod dlreport;
|
||||||
mod download;
|
mod download;
|
||||||
@ -39,7 +37,7 @@ struct DlRequest {
|
|||||||
type SyncQueue = Arc<Mutex<VecDeque<DlRequest>>>;
|
type SyncQueue = Arc<Mutex<VecDeque<DlRequest>>>;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> ResBE<()> {
|
||||||
let args = CLIArgs::parse();
|
let args = CLIArgs::parse();
|
||||||
|
|
||||||
// Combine all urls taken from files and the ones provided on the command line
|
// Combine all urls taken from files and the ones provided on the command line
|
||||||
@ -63,7 +61,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a listfile and return all urls found in it
|
/// Parse a listfile and return all urls found in it
|
||||||
async fn urls_from_listfile(listfile: &Path) -> Result<Vec<String>> {
|
async fn urls_from_listfile(listfile: &Path) -> ResBE<Vec<String>> {
|
||||||
let text = tokio::fs::read_to_string(listfile).await?;
|
let text = tokio::fs::read_to_string(listfile).await?;
|
||||||
let urls = text
|
let urls = text
|
||||||
.lines()
|
.lines()
|
||||||
@ -75,7 +73,7 @@ async fn urls_from_listfile(listfile: &Path) -> Result<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download all files in parallel according to the provided CLI arguments
|
// Download all files in parallel according to the provided CLI arguments
|
||||||
async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> Result<()> {
|
async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> ResBE<()> {
|
||||||
let num_urls = raw_urls.len();
|
let num_urls = raw_urls.len();
|
||||||
let urls: SyncQueue = Default::default();
|
let urls: SyncQueue = Default::default();
|
||||||
|
|
||||||
@ -115,19 +113,24 @@ async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_args: CLIArgs) {
|
async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_args: CLIArgs) {
|
||||||
while let Some(dlreq) = urls.lock().await.pop_front() {
|
loop {
|
||||||
|
// Get the next url to download or break if there are no more urls
|
||||||
|
let dlreq = match urls.lock().await.pop_front() {
|
||||||
|
Some(it) => it,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
let reporter = DlReporter::new(dlreq.id as u32, reporter.clone());
|
let reporter = DlReporter::new(dlreq.id as u32, reporter.clone());
|
||||||
|
|
||||||
// Resolve the zippy url to the direct download url if necessary
|
// Resolve the zippy url to the direct download url if necessary
|
||||||
let url = if is_zippyshare_url(&dlreq.url) {
|
let url = if cli_args.zippy {
|
||||||
match zippy::resolve_link(&dlreq.url).await {
|
match zippy::resolve_link(&dlreq.url).await {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
report_msg!(
|
reporter.send(DlStatus::Message(format!(
|
||||||
reporter,
|
"Zippyshare link could not be resolved: {}",
|
||||||
"Zippyshare link could not be resolved, skipping: {}",
|
|
||||||
dlreq.url
|
dlreq.url
|
||||||
);
|
)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,64 +138,72 @@ async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_
|
|||||||
dlreq.url.to_string()
|
dlreq.url.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = match http_get_filesize_and_range_support(&url).await {
|
let file_name = cli_args
|
||||||
Ok(it) => it,
|
.into_file
|
||||||
Err(_e) => {
|
.clone()
|
||||||
report_msg!(reporter, "Error while querying metadata: {url}");
|
.unwrap_or_else(|| download::url_to_filename(&url).into());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let into_file: PathBuf = cli_args
|
let into_file: PathBuf = cli_args
|
||||||
.outdir
|
.outdir
|
||||||
.join(Path::new(&info.filename))
|
.join(Path::new(&file_name))
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string()
|
.to_string()
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
let (filesize, range_supported) = match http_get_filesize_and_range_support(&url).await {
|
||||||
|
Ok((filesize, range_supported)) => (filesize, range_supported),
|
||||||
|
Err(_e) => {
|
||||||
|
reporter.send(DlStatus::Message(format!(
|
||||||
|
"Error while querying metadata: {}",
|
||||||
|
url
|
||||||
|
)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If file with same name is present locally, check filesize
|
// If file with same name is present locally, check filesize
|
||||||
if into_file.exists() {
|
if into_file.exists() {
|
||||||
let local_filesize = std::fs::metadata(&into_file).unwrap().len();
|
let local_filesize = std::fs::metadata(&into_file).unwrap().len();
|
||||||
|
|
||||||
if info.filesize == local_filesize {
|
if filesize == local_filesize {
|
||||||
report_msg!(
|
reporter.send(DlStatus::Message(format!(
|
||||||
reporter,
|
|
||||||
"Skipping file '{}': already present",
|
"Skipping file '{}': already present",
|
||||||
info.filename
|
file_name.display()
|
||||||
);
|
)));
|
||||||
reporter.skipped();
|
reporter.send(DlStatus::Skipped);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
report_msg!(
|
reporter.send(DlStatus::Message(format!(
|
||||||
reporter,
|
|
||||||
"Replacing file '{}': present but not completed",
|
"Replacing file '{}': present but not completed",
|
||||||
&info.filename
|
&file_name.display()
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dl_status = if cli_args.conn_count.get() == 1 {
|
let dl_status = if cli_args.conn_count.get() == 1 {
|
||||||
download_feedback(&url, &into_file, reporter.clone(), Some(info.filesize)).await
|
download_feedback(&url, &into_file, reporter.clone(), Some(filesize)).await
|
||||||
} else if !info.range_support {
|
} else if !range_supported {
|
||||||
report_msg!(
|
reporter.send(DlStatus::Message(format!(
|
||||||
reporter,
|
"Server does not support range headers. Downloading with single connection: {}",
|
||||||
"Server does not support range headers. Downloading with single connection: {url}"
|
url
|
||||||
);
|
)));
|
||||||
download_feedback(&url, &into_file, reporter.clone(), Some(info.filesize)).await
|
download_feedback(&url, &into_file, reporter.clone(), Some(filesize)).await
|
||||||
} else {
|
} else {
|
||||||
download_feedback_multi(
|
download_feedback_multi(
|
||||||
&url,
|
&url,
|
||||||
&into_file,
|
&into_file,
|
||||||
reporter.clone(),
|
reporter.clone(),
|
||||||
cli_args.conn_count.get(),
|
cli_args.conn_count.get(),
|
||||||
Some(info.filesize),
|
Some(filesize),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
|
|
||||||
if dl_status.is_err() {
|
if dl_status.is_err() {
|
||||||
reporter.done_err(info.filename);
|
reporter.send(DlStatus::DoneErr {
|
||||||
|
filename: file_name.to_str().unwrap().to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/zippy.rs
19
src/zippy.rs
@ -1,12 +1,7 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::io::{Error, ErrorKind};
|
use std::io::{Error, ErrorKind};
|
||||||
|
|
||||||
pub fn is_zippyshare_url(url: &str) -> bool {
|
use crate::errors::ResBE;
|
||||||
Regex::new(r"^https?://(?:www\d*\.)?zippyshare\.com/v/[0-9a-zA-Z]+/file\.html$")
|
|
||||||
.unwrap()
|
|
||||||
.is_match(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Updated: 07.03.2022
|
Updated: 07.03.2022
|
||||||
@ -22,15 +17,15 @@ Link generation code:
|
|||||||
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) -> Result<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) {
|
||||||
return Err(Error::new(ErrorKind::Other, "URL is not a zippyshare url").into());
|
return Err(Error::new(ErrorKind::Other, "URL is not a zippyshare url").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the hostname (with https:// prefix) for later
|
// Extract the hostname (with https:// prefix) for later
|
||||||
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?.text().await?;
|
let body = reqwest::get(url).await?.text().await?;
|
||||||
@ -47,10 +42,10 @@ pub async fn resolve_link(url: &str) -> Result<String> {
|
|||||||
|
|
||||||
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 = cap_link[2].parse()?;
|
let n2: i32 = i32::from_str_radix(&cap_link[2], 10)?;
|
||||||
let n3: i32 = cap_link[3].parse()?;
|
let n3: i32 = i32::from_str_radix(&cap_link[3], 10)?;
|
||||||
let n4 = n2;
|
let n4 = n2;
|
||||||
let n5: i32 = cap_link[4].parse()?;
|
let n5: i32 = i32::from_str_radix(&cap_link[4], 10)?;
|
||||||
|
|
||||||
let mixed = n2 % n3 + n4 % n5;
|
let mixed = n2 % n3 + n4 % n5;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user