feat: fork aim-downloader into crates/aim-downloader to make it compatible with windows (#996)

* feat: support build on windows

* revert change

* feat: migrate aim source code

* revert change

* [autofix.ci] apply automated fixes

* fix test

* remove test dir, skip test

* fix test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
r0.7
Eric 2023-12-10 10:48:21 +08:00 committed by GitHub
parent 95d15f1224
commit 1fa0106ca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1195 additions and 1417 deletions

1513
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ members = [
"crates/tabby-inference",
"crates/llama-cpp-bindings",
"crates/http-api-bindings",
"crates/aim-downloader",
"crates/juniper-axum",
"ee/tabby-webserver",
]

View File

@ -0,0 +1,26 @@
[package]
name = "aim-downloader"
version.workspace = true
edition.workspace = true
authors.workspace = true
homepage.workspace = true
[dependencies]
async-stream = "0.3.5"
clap = { version = "4.4.11", features = ["cargo", "string"] }
custom_error = "1.9.2"
dotenvy = "0.15.7"
futures-util = "0.3.29"
home = "0.5.5"
indicatif = "0.17.7"
netrc = "0.4.1"
regex.workspace = true
reqwest = { workspace = true, features = ["stream"] }
sha2 = "0.10.8"
strfmt = "0.2.4"
tokio = { workspace = true, features = ["full"] }
tokio-util = { version="0.7.10", features = ["full"] }
url-parse = "1.0.7"
[dev-dependencies]
serial_test = "2.0.0"

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-3021 Mihai Galos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,3 @@
# http_downloader
Adopted from https://github.com/mihaigalos/aim

View File

@ -0,0 +1,248 @@
use url_parse::core::Parser;
use crate::netrc::netrc;
#[derive(Debug)]
pub struct ParsedAddress {
pub server: String,
pub username: String,
pub password: String,
pub path_segments: Vec<String>,
pub file: String,
}
impl PartialEq for ParsedAddress {
fn eq(&self, other: &Self) -> bool {
let result = self.server == other.server
&& self.username == other.username
&& self.password == other.password
&& self.file == other.file;
let mut paths_equal = true;
for it in self.path_segments.iter().zip(self.path_segments.iter()) {
let (left, right) = it;
paths_equal = paths_equal && (left == right);
}
result && paths_equal
}
}
impl ParsedAddress {
pub fn parse_address(address: &str, silent: bool) -> ParsedAddress {
let netrc = netrc(silent);
let url = Parser::new(None).parse(address).unwrap();
let server = format!(
"{}:{}",
url.host_str()
.ok_or_else(|| panic!("failed to parse hostname from url: {url}"))
.unwrap(),
url.port_or_known_default()
.ok_or_else(|| panic!("failed to parse port from url: {url}"))
.unwrap(),
);
let url_username = url.username();
let username = if url_username.is_none() {
"anonymous".to_string()
} else {
url.username().unwrap()
};
let password = url.password().unwrap_or_else(|| "anonymous".to_string());
if !silent && username != "anonymous" && password != "anonymous" {
println!("🔑 Parsed credentials from URL.");
}
let (username, password) = ParsedAddress::mixin_netrc(&netrc, &server, username, password);
let mut path_segments: Vec<String> = url
.path_segments()
.ok_or_else(|| panic!("failed to get url path segments: {url}"))
.unwrap();
let file = path_segments
.pop()
.ok_or_else(|| panic!("got empty path segments from url: {url}"))
.unwrap();
ParsedAddress {
server,
username,
password,
path_segments,
file,
}
}
fn mixin_netrc(
netrc: &Option<netrc::Netrc>,
server: &str,
username: String,
password: String,
) -> (String, String) {
let mut user = username.clone();
let mut pass = password.clone();
if !netrc.is_none() && username == "anonymous" && password == "anonymous" {
for host in netrc.as_ref().unwrap().hosts.iter().enumerate() {
let (_i, (netrc_name, machine)) = host;
let mut name = netrc_name.to_string();
if let Some(port) = machine.port {
name = name + ":" + &port.to_string()[..];
}
if server == name {
user = machine.login.clone();
pass = machine.password.clone().unwrap();
break;
}
}
}
(user, pass)
}
}
#[tokio::test]
async fn parseaddress_operator_equals_works_when_typical() {
let left = ParsedAddress {
server: "do.main".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["my".to_string(), "path".to_string()],
file: "pass".to_string(),
};
let right = ParsedAddress {
server: "do.main".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["my".to_string(), "path".to_string()],
file: "pass".to_string(),
};
assert!(left == right);
}
#[tokio::test]
async fn parseaddress_operator_equals_fails_when_not_equal() {
let left = ParsedAddress {
server: "do.main".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["my".to_string(), "path".to_string()],
file: "pass".to_string(),
};
let right = ParsedAddress {
server: "do".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["my".to_string(), "path".to_string()],
file: "pass".to_string(),
};
assert!(left != right);
}
#[tokio::test]
async fn parse_works() {
let expected = ParsedAddress {
server: "do.main:21".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["index".to_string()],
file: "file".to_string(),
};
let actual = ParsedAddress::parse_address("ftp://user:pass@do.main:21/index/file", true);
assert_eq!(actual, expected);
}
#[tokio::test]
async fn mixin_works() {
let expected_username = "test";
let expected_password = "p@ssw0rd";
let input = "machine example.com login test password p@ssw0rd";
let input = std::io::BufReader::new(input.as_bytes());
let netrc = netrc::Netrc::parse(input).unwrap();
let username_decoded_from_url = "anonymous".to_string();
let password_decoded_from_url = "anonymous".to_string();
let (actual_username, actual_password) = ParsedAddress::mixin_netrc(
&Some(netrc),
"example.com",
username_decoded_from_url,
password_decoded_from_url,
);
assert_eq!(actual_username, expected_username);
assert_eq!(actual_password, expected_password);
}
#[tokio::test]
async fn mixin_works_with_port() {
let expected_username = "test";
let expected_password = "p@ssw0rd";
let input = "machine example.com login test password p@ssw0rd port 443";
let input = std::io::BufReader::new(input.as_bytes());
let netrc = netrc::Netrc::parse(input).unwrap();
let username_decoded_from_url = "anonymous".to_string();
let password_decoded_from_url = "anonymous".to_string();
let (actual_username, actual_password) = ParsedAddress::mixin_netrc(
&Some(netrc),
"example.com:443",
username_decoded_from_url,
password_decoded_from_url,
);
assert_eq!(actual_username, expected_username);
assert_eq!(actual_password, expected_password);
}
#[tokio::test]
async fn parse_works_with_netrc_mixin() {
let expected = ParsedAddress {
server: "do.main:21".to_string(),
username: "test".to_string(),
password: "p@ssw0rd".to_string(),
path_segments: vec!["index".to_string()],
file: "file".to_string(),
};
let data = "machine do.main login test password p@ssw0rd port 21";
std::fs::write(".netrc.test", data).expect("Unable to write file");
let actual = ParsedAddress::parse_address("ftp://do.main/index/file", true);
assert_eq!(actual, expected);
std::fs::remove_file(".netrc.test").unwrap();
}
#[tokio::test]
async fn parse_works_when_ssh_user() {
let expected = ParsedAddress {
server: "localhost:2223".to_string(),
username: "user".to_string(),
password: "anonymous".to_string(),
path_segments: vec!["".to_string()],
file: "file".to_string(),
};
let actual = ParsedAddress::parse_address("ssh://user@localhost:2223/file", true);
assert_eq!(actual, expected);
}
#[tokio::test]
async fn parse_works_when_not_silent() {
let expected = ParsedAddress {
server: "localhost:2223".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
path_segments: vec!["".to_string()],
file: "file".to_string(),
};
let actual = ParsedAddress::parse_address("ssh://user:pass@localhost:2223/file", false);
assert_eq!(actual, expected);
}

View File

@ -0,0 +1,165 @@
use std::{collections::HashMap, env};
use dotenvy::dotenv;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use strfmt::strfmt;
const DEFAULT_AIM_PROGRESSBAR_DOWNLOADED_MESSAGE: &str = "🎯 Downloaded {input} to {output}";
const DEFAULT_AIM_PROGRESSBAR_MESSAGE_FORMAT: &str = "🎯 Transferring {url}";
const DEFAULT_AIM_PROGRESSBAR_PROGRESS_CHARS: &str = "█▉▊▋▌▍▎▏ ";
const DEFAULT_AIM_PROGRESSBAR_TEMPLATE: &str = "{msg}\n{spinner:.cyan} {elapsed_precise} ▕{bar:.white}▏ {bytes}/{total_bytes} {bytes_per_sec} ETA {eta}.";
const DEFAULT_AIM_PROGRESSBAR_UPLOADED_MESSAGE: &str = "🎯 Uploaded {input} to {output}";
const THRESHOLD_IF_TOTALBYTES_BELOW_THEN_AUTO_SILENT_MODE: u64 = 1024 * 1024;
fn construct_progress_bar(
total_size: u64,
url: &str,
message_format: &str,
progress_chars: &str,
template: &str,
) -> indicatif::ProgressBar {
let pb = ProgressBar::new(total_size);
pb.set_draw_target(ProgressDrawTarget::hidden());
let mut vars: HashMap<String, String> = HashMap::new();
if message_format.contains("{url}") {
vars.insert("url".to_string(), url.to_string());
}
pb.set_message(strfmt(message_format, &vars).unwrap());
pb.set_style(
ProgressStyle::default_bar()
.template(template)
.unwrap()
.progress_chars(progress_chars),
);
pb
}
pub struct WrappedBar {
pub silent: bool,
pub output: Option<indicatif::ProgressBar>,
downloaded_message: String,
uploaded_message: String,
}
impl WrappedBar {
pub fn new_empty() -> Self {
WrappedBar {
silent: true,
output: None,
downloaded_message: "".to_string(),
uploaded_message: "".to_string(),
}
}
pub fn new_empty_verbose() -> Self {
WrappedBar {
silent: false,
output: None,
downloaded_message: "".to_string(),
uploaded_message: "".to_string(),
}
}
pub fn new(total_size: u64, url: &str, silent: bool) -> Self {
dotenv().ok();
let message_format = &env::var("AIM_PROGRESSBAR_MESSAGE_FORMAT")
.unwrap_or_else(|_| DEFAULT_AIM_PROGRESSBAR_MESSAGE_FORMAT.to_string());
let progress_chars = &env::var("AIM_PROGRESSBAR_PROGRESS_CHARS")
.unwrap_or_else(|_| DEFAULT_AIM_PROGRESSBAR_PROGRESS_CHARS.to_string());
let template = &env::var("AIM_PROGRESSBAR_TEMPLATE")
.unwrap_or_else(|_| DEFAULT_AIM_PROGRESSBAR_TEMPLATE.to_string());
let downloaded_message = &env::var("AIM_PROGRESSBAR_DOWNLOADED_MESSAGE")
.unwrap_or_else(|_| DEFAULT_AIM_PROGRESSBAR_DOWNLOADED_MESSAGE.to_string());
let uploaded_message = &env::var("AIM_PROGRESSBAR_UPLOADED_MESSAGE")
.unwrap_or_else(|_| DEFAULT_AIM_PROGRESSBAR_UPLOADED_MESSAGE.to_string());
let output = match silent {
false => Some(construct_progress_bar(
total_size,
url,
message_format,
progress_chars,
template,
)),
true => None,
};
WrappedBar {
silent,
output,
downloaded_message: downloaded_message.to_string(),
uploaded_message: uploaded_message.to_string(),
}
}
pub fn set_length(&mut self, len: u64) {
if len < THRESHOLD_IF_TOTALBYTES_BELOW_THEN_AUTO_SILENT_MODE {
self.silent = true;
}
if !self.silent {
self.output
.as_ref()
.unwrap()
.set_draw_target(ProgressDrawTarget::stderr());
self.output.as_ref().unwrap().set_length(len);
}
}
pub fn set_position(&self, pos: u64) {
if !self.silent {
self.output.as_ref().unwrap().set_position(pos);
}
}
pub fn finish_download(&self, input: &str, output: &str) {
if !self.silent {
let mut vars: HashMap<String, String> = HashMap::new();
if self.downloaded_message.contains("{input}") {
vars.insert("input".to_string(), input.to_string());
}
if self.downloaded_message.contains("{output}") {
vars.insert("output".to_string(), output.to_string());
}
self.output
.as_ref()
.unwrap()
.finish_with_message(strfmt(&self.downloaded_message, &vars).unwrap());
}
}
pub fn finish_upload(&self, input: &str, output: &str) {
if !self.silent {
let mut vars: HashMap<String, String> = HashMap::new();
if self.uploaded_message.contains("{input}") {
vars.insert("input".to_string(), input.to_string());
}
if self.uploaded_message.contains("{output}") {
vars.insert("output".to_string(), output.to_string());
}
self.output
.as_ref()
.unwrap()
.finish_with_message(strfmt(&self.uploaded_message, &vars).unwrap());
}
}
}
#[test]
fn test_bar_finish_download_works_when_typical() {
let bar = WrappedBar::new(42, "url", false);
bar.finish_download("", "");
}
#[test]
fn test_bar_finish_upload_works_when_typical() {
let bar = WrappedBar::new(42, "url", false);
bar.finish_upload("", "");
}
#[test]
fn test_bar_set_length_works_when_typical() {
let mut bar = WrappedBar::new(42, "url", false);
bar.set_length(42);
}

View File

@ -0,0 +1,7 @@
use clap::crate_version;
pub const CLIENT_ID: &str = concat!(
env!("CARGO_PKG_REPOSITORY"),
"/releases/tag/",
crate_version!()
);

View File

@ -0,0 +1,29 @@
extern crate custom_error;
use custom_error::custom_error;
custom_error! {
pub ValidateError
Sha256Mismatch = "Invalid sha256.",
}
custom_error! {
pub HTTPHeaderError
NotPresent = "Cannot find requested header.",
}
impl From<ValidateError> for std::io::Error {
fn from(cause: ValidateError) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, cause.to_string())
}
}
#[test]
fn test_from_validate_error_to_std_io_error_works_when_typical() {
let _ = match Err(ValidateError::Sha256Mismatch) {
Ok(v) => v,
Err(e) => {
let error: std::io::Error = e.into();
error
}
};
}

View File

@ -0,0 +1,77 @@
use std::{fs, io, str};
use sha2::{Digest, Sha256};
use crate::error::ValidateError;
pub struct HashChecker;
impl HashChecker {
pub fn check(filename: &str, expected_hash: &str) -> Result<(), ValidateError> {
let mut result = Ok(());
if filename != "stdout" && (!expected_hash.is_empty()) {
let actual_hash = HashChecker::sha256sum(filename);
if actual_hash != expected_hash {
result = Err(ValidateError::Sha256Mismatch);
}
match result {
Ok(()) => println!("✅ Checksum OK."),
Err(ValidateError::Sha256Mismatch) => println!(
"❌ Checksum verification failed for {filename}:\n expected: {expected_hash}\n got: {actual_hash}"),
}
}
result
}
fn sha256sum(filename: &str) -> String {
let mut hasher = Sha256::new();
let mut file = fs::File::open(filename).unwrap();
io::copy(&mut file, &mut hasher).unwrap();
let computed_hash = hasher.finalize();
drop(file);
format!("{computed_hash:x}")
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_err {
($expression:expr, $($pattern:tt)+) => {
match $expression {
$($pattern)+ => (),
ref e => panic!("expected `{}` but got `{:?}`", stringify!($($pattern)+), e),
}
}
}
#[test]
fn test_check_api_fails_when_checksum_mismatch() {
let expected = "AAAA847124bfb9d9a9d44af6f00d8003006c44b9ef9ba458b5d4d3fc5f81bde5";
assert_err!(
HashChecker::check("LICENCE.md", expected),
Err(ValidateError::Sha256Mismatch)
);
}
#[test]
fn test_sha256sum_api() {
let expected = "21d7847124bfb9d9a9d44af6f00d8003006c44b9ef9ba458b5d4d3fc5f81bde5";
let actual = HashChecker::sha256sum("LICENCE.md");
assert_eq!(actual, expected);
}
#[test]
fn test_check_api_works_when_typical() {
let expected = "21d7847124bfb9d9a9d44af6f00d8003006c44b9ef9ba458b5d4d3fc5f81bde5";
let is_match = HashChecker::check("LICENCE.md", expected).is_ok();
assert!(is_match);
}
}

View File

@ -0,0 +1,268 @@
use std::{cmp::min, io::Error};
use futures_util::StreamExt;
use regex::Regex;
use reqwest::Client;
use tokio_util::io::ReaderStream;
use crate::{
address::ParsedAddress, bar::WrappedBar, consts::*, error::ValidateError, hash::HashChecker, io,
};
pub struct HTTPSHandler;
impl HTTPSHandler {
pub async fn get(
input: &str,
output: &str,
bar: &mut WrappedBar,
expected_sha256: &str,
) -> Result<(), ValidateError> {
HTTPSHandler::_get(input, output, bar).await?;
HashChecker::check(output, expected_sha256)
}
pub async fn put(input: &str, output: &str, mut bar: WrappedBar) -> Result<(), ValidateError> {
let parsed_address = ParsedAddress::parse_address(output, bar.silent);
let file = tokio::fs::File::open(&input)
.await
.expect("Cannot open input file for HTTPS read");
let total_size = file
.metadata()
.await
.expect("Cannot determine input file size for HTTPS read")
.len();
let input_ = input.to_string();
let output_ = output.to_string();
let mut reader_stream = ReaderStream::new(file);
let mut uploaded = HTTPSHandler::get_already_uploaded(output, bar.silent).await;
bar.set_length(total_size);
let async_stream = async_stream::stream! {
while let Some(chunk) = reader_stream.next().await {
if let Ok(chunk) = &chunk {
let new = min(uploaded + (chunk.len() as u64), total_size);
uploaded = new;
bar.set_position(new);
if uploaded >= total_size {
bar.finish_upload(&input_, &output_);
}
}
yield chunk;
}
};
let response = reqwest::Client::new()
.put(output)
.header("content-type", "application/octet-stream")
.header(
"Range",
"bytes=".to_owned() + &uploaded.to_string()[..] + "-",
)
.header(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static(CLIENT_ID),
)
.basic_auth(parsed_address.username, Some(parsed_address.password))
.body(reqwest::Body::wrap_stream(async_stream))
.send()
.await
.unwrap();
println!("{:?}", response.text().await.unwrap());
Ok(())
}
pub async fn get_links(input: String) -> Result<Vec<String>, Error> {
let mut result = Vec::new();
let res = HTTPSHandler::list(&input).await.unwrap();
let lines: Vec<&str> = res.split('\n').collect();
for line in lines {
let re = Regex::new(r#".*href="/(.+?)".*"#).unwrap();
let caps = re.captures(line);
if let Some(e) = caps {
result.push(e.get(1).unwrap().as_str().to_string())
}
}
result.push("..".to_string());
result.sort();
Ok(result)
}
async fn list(input: &str) -> Result<String, ValidateError> {
let is_silent = true;
let parsed_address = ParsedAddress::parse_address(input, is_silent);
let res = Client::new()
.get(input)
.header(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static(CLIENT_ID),
)
.basic_auth(parsed_address.username, Some(parsed_address.password))
.send()
.await
.map_err(|_| format!("Failed to GET from {}", &input))
.unwrap()
.text()
.await
.unwrap();
Ok(res)
}
async fn _get(input: &str, output: &str, bar: &mut WrappedBar) -> Result<(), ValidateError> {
let parsed_address = ParsedAddress::parse_address(input, bar.silent);
let (mut out, mut downloaded) = io::get_output(output, bar.silent);
let res = Client::new()
.get(input)
.header(
"Range",
"bytes=".to_owned() + &downloaded.to_string()[..] + "-",
)
.header(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static(CLIENT_ID),
)
.basic_auth(parsed_address.username, Some(parsed_address.password))
.send()
.await
.map_err(|_| format!("Failed to GET from {} to {}", &input, &output))
.unwrap();
let total_size = downloaded + res.content_length().unwrap_or(0);
bar.set_length(total_size);
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item.map_err(|_| "Error while downloading.").unwrap();
out.write_all(&chunk)
.map_err(|_| "Error while writing to output.")
.unwrap();
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
bar.set_position(new);
}
bar.finish_download(input, output);
Ok(())
}
async fn get_already_uploaded(output: &str, silent: bool) -> u64 {
let parsed_address = ParsedAddress::parse_address(output, silent);
let res = Client::new()
.get(output)
.header(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static(CLIENT_ID),
)
.basic_auth(parsed_address.username, Some(parsed_address.password))
.send()
.await
.map_err(|_| format!("Failed to GET already uploaded size from {}", &output))
.unwrap();
res.content_length().unwrap_or(0)
}
}
#[ignore]
#[tokio::test]
async fn get_https_works() {
let expected_hash = "0e0f0d7139c8c7e3ff20cb243e94bc5993517d88e8be8d59129730607d5c631b";
let out_file = "tokei-x86_64-unknown-linux-gnu.tar.gz";
let result = HTTPSHandler::get("https://github.com/XAMPPRocky/tokei/releases/download/v12.0.4/tokei-x86_64-unknown-linux-gnu.tar.gz", out_file, &mut WrappedBar::new_empty(), expected_hash).await;
assert!(result.is_ok());
std::fs::remove_file(out_file).unwrap();
}
#[ignore]
#[tokio::test]
async fn get_resume_works() {
let expected_size = 561553;
let out_file = "test/dua-v2.10.2-x86_64-unknown-linux-musl.tar.gz";
std::fs::copy(
"test/incomplete_dua-v2.10.2-x86_64-unknown-linux-musl.tar.gz",
out_file,
)
.unwrap();
let _ = HTTPSHandler::get("https://github.com/Byron/dua-cli/releases/download/v2.10.2/dua-v2.10.2-x86_64-unknown-linux-musl.tar.gz", out_file, &mut WrappedBar::new_empty_verbose(), "").await;
let actual_size = std::fs::metadata(out_file).unwrap().len();
assert_eq!(actual_size, expected_size);
std::fs::remove_file(out_file).unwrap();
}
#[ignore]
#[tokio::test]
async fn list_works_when_typical() {
let expected = r#"<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
"#;
let result = HTTPSHandler::list("https://example.com").await.unwrap();
let result = str::replace(&result, " \n ", "");
let result = str::replace(&result, "</style> \n</head>", "</style>\n</head>");
assert_eq!(result, expected);
}
#[ignore]
#[tokio::test]
async fn get_links_works_when_typical() {
let expected = "..";
let result = HTTPSHandler::get_links("https://github.com/mihaigalos/aim/releases".to_string())
.await
.unwrap();
assert_eq!(result[0], expected);
}

View File

@ -0,0 +1,94 @@
use std::{fs::File, io::Write};
fn get_output_file(path: &str, silent: bool) -> (Option<std::fs::File>, u64) {
let mut transferred: u64 = 0;
let mut file = None;
if path != "stdout" {
if std::path::Path::new(path).exists() {
if !silent {
println!("File exists. Resuming.");
}
file = Some(
std::fs::OpenOptions::new()
.write(true)
.append(true)
.open(path)
.unwrap(),
);
let file_size = std::fs::metadata(path).unwrap().len();
transferred = file_size;
} else {
if !silent {
println!("Writing to new file.");
}
file = Some(
File::create(path)
.map_err(|_| format!("Failed to create file '{path}'"))
.unwrap(),
);
}
}
(file, transferred)
}
pub fn get_output(path: &str, silent: bool) -> (Box<dyn Write + Send>, u64) {
let (file, transferred) = get_output_file(path, silent);
let output: Box<dyn Write + Send> = Box::new(std::io::BufWriter::new(match path {
"stdout" => Box::new(std::io::stdout()) as Box<dyn Write + Send>,
_ => Box::new(file.unwrap()) as Box<dyn Write + Send>,
}));
(output, transferred)
}
#[test]
fn test_get_output_file_file_is_none_when_stdout() {
let is_silet = true;
let (file, _) = get_output_file("stdout", is_silet);
assert!(file.is_none());
}
#[test]
fn test_get_output_file_pos_is_zero_when_stdout() {
let is_silet = true;
let (_, position) = get_output_file("stdout", is_silet);
assert_eq!(position, 0);
}
#[test]
fn test_get_output_file_file_is_none_when_newfile() {
let is_silet = true;
let filename = "test_get_output_file_file_is_none_when_newfile";
let (file, _) = get_output_file(filename, is_silet);
assert!(file.is_some());
std::fs::remove_file(filename).unwrap();
}
#[test]
fn test_get_output_file_file_is_none_when_newfile_and_not_silent() {
let is_silet = false;
let filename = "test_get_output_file_file_is_none_when_newfile_and_not_silent";
let (file, _) = get_output_file(filename, is_silet);
assert!(file.is_some());
std::fs::remove_file(filename).unwrap();
}
#[test]
fn test_get_output_file_file_is_none_when_existingfile_and_not_silent() {
use std::io::Write;
let is_silet = false;
let filename = "test_get_output_file_file_is_none_when_existingfile_and_not_silent";
let expected_position_byte = 4;
let mut file = File::create(filename).unwrap();
file.write_all(b"1234").unwrap();
let (_, position) = get_output_file(filename, is_silet);
assert_eq!(position, expected_position_byte);
std::fs::remove_file(filename).unwrap();
}

View File

@ -0,0 +1,10 @@
pub mod bar;
pub mod https;
mod address;
mod consts;
mod error;
mod hash;
mod io;
mod netrc;
mod untildify;

View File

@ -0,0 +1,64 @@
use std::{io::BufReader, path::Path};
use netrc::Netrc;
use crate::untildify::untildify;
fn get_possible_netrc_path(silent: bool) -> String {
let candidates = vec![
".netrc",
".netrc.test",
".netrc.test_https",
".netrc.test_ftp",
".netrc.test_unit",
"~/.netrc",
];
for candidate in candidates {
let candidate = untildify(candidate);
if Path::new(&candidate).exists() {
if !silent {
println!("🔑 Parsed .netrc from: {candidate}");
}
return candidate;
}
}
"".to_string()
}
pub fn netrc(silent: bool) -> Option<netrc::Netrc> {
let mut result = None;
let path = get_possible_netrc_path(silent);
if !path.is_empty() {
let file = std::fs::File::open(path).unwrap();
let parsed = Netrc::parse(BufReader::new(file));
result = Some(parsed.unwrap());
}
result
}
#[test]
fn test_netrc_with_file_works_when_typical() {
use std::io::Write;
let mut file = std::fs::File::create(".netrc.test_unit").unwrap();
file.write_all(b"machine mydomain.com login myuser password mypass port 1234")
.unwrap();
assert!(netrc(true).is_some());
assert!(netrc(false).is_some());
std::fs::remove_file(".netrc.test_unit").unwrap();
}
#[ignore]
#[test]
fn test_netrc_with_file_works_when_typical_and_not_silent() {
use std::io::Write;
let mut file = std::fs::File::create(".netrc.test_unit").unwrap();
file.write_all(b"machine mydomain.com login myuser password mypass port 1234")
.unwrap();
let netrc = netrc(false);
assert!(netrc.is_some());
std::fs::remove_file(".netrc.test_unit").unwrap();
}

View File

@ -0,0 +1,80 @@
//! Replace `~` with user home directory across all platforms
//!
//! Unit tests come from https://github.com/sathishsoundharajan/untildify (MIT LICENSE)
pub fn untildify(input_path: &str) -> String {
if input_path.is_empty() {
return String::from(input_path);
}
if let Some(home) = home::home_dir() {
if input_path == r"~" {
return home.into_os_string().into_string().unwrap();
}
if input_path.starts_with(r"~/") || input_path.starts_with(r"~\") {
if let Ok(path) = home.join(&input_path[2..]).into_os_string().into_string() {
return path;
}
}
}
String::from(input_path)
}
#[cfg(any(unix, target_os = "redox"))]
#[cfg(test)]
mod tests {
use std::{env, path::Path};
use super::*;
#[test]
fn test_returns_untildfyed_string() {
env::remove_var("HOME");
let home = Path::new("/User/Untildify");
env::set_var("HOME", home.as_os_str());
assert_eq!(untildify("~/Desktop"), "/User/Untildify/Desktop");
assert_eq!(untildify("~/a/b/c/d/e"), "/User/Untildify/a/b/c/d/e");
assert_eq!(untildify("~/"), "/User/Untildify/");
}
#[test]
fn test_returns_empty_string() {
env::remove_var("HOME");
let home = Path::new("/User/Untildify");
env::set_var("HOME", home.as_os_str());
assert_eq!(untildify("Desktop"), "Desktop");
assert_eq!(untildify(""), "");
assert_eq!(untildify("/"), "/");
// assert_eq!(untildify("~/Desktop/~/Code"), "/User/Untildify/Desktop/");
}
#[test]
fn test_with_dot_folders() {
env::remove_var("HOME");
let home = Path::new("/User/Untildify");
env::set_var("HOME", home.as_os_str());
assert_eq!(untildify("~/.ssh/id_rsa"), "/User/Untildify/.ssh/id_rsa");
}
}
#[cfg(target_os = "windows")]
#[cfg(test)]
mod tests {
use std::env;
use super::*;
#[test]
fn test_returns_untildfyed_string() {
env::set_var("USERPROFILE", r"C:\Users\Admin");
assert_eq!(untildify(r"~\Desktop"), r"C:\Users\Admin\Desktop");
assert_eq!(untildify(r"~\a\b\c\d\e"), r"C:\Users\Admin\a\b\c\d\e");
assert_eq!(untildify(r"~\"), r"C:\Users\Admin\");
}
}

View File

@ -4,9 +4,9 @@ version = "0.7.0-dev"
edition = "2021"
[dependencies]
aim-downloader = { path = "../aim-downloader" }
tabby-common = { path = "../tabby-common" }
anyhow = { workspace = true }
tracing = { workspace = true }
tokio-retry = "0.3.0"
sha256 = "1.4.0"
aim = "1.8.5"

View File

@ -1,6 +1,6 @@
use std::{fs, path::Path};
use aim::bar::WrappedBar;
use aim_downloader::{bar::WrappedBar, https};
use anyhow::{anyhow, Result};
use tabby_common::registry::{parse_model_id, ModelRegistry};
use tokio_retry::{
@ -60,7 +60,7 @@ async fn download_file(url: &str, path: &Path) -> Result<()> {
let mut bar = WrappedBar::new(0, url, false);
aim::https::HTTPSHandler::get(url, &intermediate_filename, &mut bar, "").await?;
https::HTTPSHandler::get(url, &intermediate_filename, &mut bar, "").await?;
fs::rename(intermediate_filename, filename)?;
Ok(())