diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..4596dcb --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,81 @@ +name: Deploy + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + build-and-upload: + name: Build and upload + runs-on: ${{ matrix.os }} + + strategy: + matrix: + include: + - build: linux + os: ubuntu-latest + target: x86_64-unknown-linux-musl + + - build: windows + os: windows-latest + target: x86_64-pc-windows-msvc + + - build: macos-arm64 + os: macos-latest + target: aarch64-apple-darwin + + - build: macos-x86_64 + os: macos-latest + target: x86_64-apple-darwin + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get the release version from the tag + shell: bash + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --verbose --release --target ${{ matrix.target }} + + - name: Build archive + shell: bash + run: | + # Replace with the name of your binary + binary_name="swish" + + dirname="$binary_name-${{ env.VERSION }}-${{ matrix.target }}" + mkdir "$dirname" + if [ "${{ matrix.os }}" = "windows-latest" ]; then + mv "target/${{ matrix.target }}/release/$binary_name.exe" "$dirname" + else + mv "target/${{ matrix.target }}/release/$binary_name" "$dirname" + fi + + if [ "${{ matrix.os }}" = "windows-latest" ]; then + 7z a "$dirname.zip" "$dirname" + echo "ASSET=$dirname.zip" >> $GITHUB_ENV + else + tar -czf "$dirname.tar.gz" "$dirname" + echo "ASSET=$dirname.tar.gz" >> $GITHUB_ENV + fi + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + ${{ env.ASSET }} diff --git a/Cargo.lock b/Cargo.lock index f5af9bd..ddb2533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.101" @@ -344,6 +353,7 @@ checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -507,6 +517,7 @@ dependencies = [ "curl", "indicatif", "log", + "openssl-sys", "regex", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 539513f..1854849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ regex = "1.10.3" serde_json = "1.0.114" sha2 = "0.10.8" simple_logger = "4.3.3" +openssl-sys = { version = "0.9", features = ["vendored"] } diff --git a/src/api/handlers/mod.rs b/src/api/handlers/mod.rs index 97363c9..2bcca2e 100644 --- a/src/api/handlers/mod.rs +++ b/src/api/handlers/mod.rs @@ -36,11 +36,6 @@ pub struct DataHandler { pub data: Vec, } -impl DataHandler { - pub fn new() -> Self { - Self { data: Vec::new() } - } -} impl Handler for DataHandler { fn write(&mut self, data: &[u8]) -> Result { diff --git a/src/api/mod.rs b/src/api/mod.rs index b4e33a6..036861f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ +/// Man... told you this was a mess + use curl::easy::Easy2; use std::fs::File; use std::sync::{Arc, Mutex}; @@ -56,9 +58,8 @@ pub fn new_easy2_download( url: String, custom_headers: Option>, file: File, + file_size: u64, ) -> Result>, curl::Error> { - let file_metadata = file.metadata().unwrap(); - let file_size = file_metadata.len(); let progress_bar = ProgressBar::new(file_size as u64); progress_bar.set_style(ProgressStyle::default_bar() diff --git a/src/errors.rs b/src/errors.rs index 82453ce..5342a01 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,17 +5,14 @@ use std::fmt; #[derive(Debug)] pub enum SwishError { InvalidUrl { url: String }, - InavlidArg { arg: String }, InvalidJson { json: String }, InvalidResponse { response: String }, - InvalidFile { file: String }, - InvalidPath { path: String }, CurlError { error: CurlError }, FileError { error: std::io::Error }, - JSONError { error: serde_json::Error }, NotFound { url: String }, PasswordRequired, InvalidPassword, + DownloadNumberExceeded, } impl fmt::Display for SwishError { @@ -24,15 +21,12 @@ impl fmt::Display for SwishError { SwishError::InvalidUrl { url } => write!(f, "Invalid URL: {}", url), SwishError::InvalidJson { json } => write!(f, "Invalid JSON: {}", json), SwishError::NotFound { url } => write!(f, "Not Found: {}, Maybe link has expired", url), - SwishError::InvalidFile { file } => write!(f, "Invalid File: {}", file), - SwishError::InvalidPath { path } => write!(f, "Invalid Path: {}", path), SwishError::CurlError { error } => write!(f, "Curl Error: {}", error), SwishError::InvalidResponse { response } => write!(f, "Invalid Response: {}", response), SwishError::FileError { error } => write!(f, "File Error: {}", error), - SwishError::JSONError { error } => write!(f, "JSON Error: {}", error), - SwishError::InavlidArg { arg } => write!(f, "Invalid Argument: {}", arg), SwishError::PasswordRequired => write!(f, "A password is required to download this file please provide it using the -p flag or --password flag"), SwishError::InvalidPassword => write!(f, "The password provided is incorrect"), + SwishError::DownloadNumberExceeded => write!(f, "The number of download has been exceeded"), } } } diff --git a/src/main.rs b/src/main.rs index 4b27327..9f96e42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,14 @@ +/// Hey there! +/// As you can see, Im a real noob in Rust and dev in general, so please be kind with me. +/// I hope someone with no skill issues could refactor the wole code base and make it readable and maintainable. +/// Sorry for the mess x) at least it seems to work for now \o/ + mod api; mod errors; mod swissfiles; use std::path::PathBuf; +use swissfiles::uploadparameters::UploadParameters; +use swissfiles::Swissfiles; use clap::Parser; use errors::SwishError; @@ -17,21 +24,13 @@ struct Cli { #[arg(short, long, value_name = "password")] password: Option, - /// Define the email recipients for the file(s) uploaded - #[arg(short, long, value_name = "john@doe.com, ...", value_parser = validate_email)] - recipients_email: Option, - - /// Define the email author for the file(s) uploaded - #[arg(short, long, value_name = "john@doe.com", value_parser = validate_email)] - author_email: Option, - /// Define the message for the file(s) uploaded #[arg(short, long, value_name = "Hello World")] message: Option, /// Define the max number of downloads for the file(s) uploaded #[arg(short, long, value_name = "250", value_parser = validate_number_download)] - number_download: Option, + number_download: Option, /// Define the number of days the file(s) will be available for download #[arg(short, long, value_name = "30", value_parser = validate_duration)] @@ -40,55 +39,47 @@ struct Cli { fn main() -> Result<(), SwishError> { simple_logger::SimpleLogger::new() - .with_level(log::LevelFilter::Debug) + .with_level(log::LevelFilter::Info) .init() .unwrap(); let cli = Cli::parse(); let arg = cli.file; + //check if the arg is a link if is_swisstransfer_link(&arg) { //Construct the swissfiles from the link - let swissfiles = swissfiles::Swissfiles::new_remotefiles(&arg, cli.password.as_deref())?; + let swissfiles = Swissfiles::new_remotefiles(&arg, cli.password.as_deref())?; //Download the files swissfiles.download(None)?; return Ok(()); } - + //check if the arg is a path if path_exists(&arg) { - let mut params = swissfiles::uploadparameters::UploadParameters::default(); + let path = PathBuf::from(&arg); + let mut params = UploadParameters::default(); if let Some(password) = cli.password { params.password = password; } - if let Some(recipients_email) = cli.recipients_email { - params.recipients_emails = recipients_email - .split(",") - .map(|email| email.to_string()) - .collect(); - } - - if let Some(author_email) = cli.author_email { - params.author_email = author_email; - } - if let Some(message) = cli.message { params.message = message; } if let Some(number_download) = cli.number_download { - params.number_of_download = number_download.to_str().unwrap().parse().unwrap(); + params.number_of_download = number_download.parse().unwrap(); } if let Some(duration) = cli.duration { params.duration = duration.parse().unwrap(); } - let local_files = swissfiles::Swissfiles::new_localfiles(&arg, ¶ms)?; - local_files.upload()?; + let local_files = Swissfiles::new_localfiles(path, ¶ms)?; + let download_link = local_files.upload()?; + println!("Download link: {}", download_link); return Ok(()); } @@ -96,11 +87,6 @@ fn main() -> Result<(), SwishError> { Err(SwishError::InvalidUrl { url: arg }) } -fn verify_link_format(link: &str) -> bool { - let re = Regex::new(r"^https://www\.swisstransfer\.com/d/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$").unwrap(); - re.is_match(link) -} - fn is_swisstransfer_link(link: &str) -> bool { let re = Regex::new(r"^https://www\.swisstransfer\.com/d/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$").unwrap(); re.is_match(link) @@ -111,34 +97,23 @@ fn path_exists(path: &str) -> bool { PathBuf::from(path).exists() } -fn validate_email(val: &str) -> Result<(), String> { - let re = Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$").unwrap(); - val.split(',').map(str::trim).try_for_each(|email| { - if re.is_match(email) { - Ok(()) - } else { - Err(format!("\"{}\" is not a valid email", email)) - } - }) -} - -fn validate_number_download(val: &str) -> Result<(), String> { - let number = val.parse::().map_err(|_| "Must be a valid number")?; +fn validate_number_download(val: &str) -> Result { + let number = val.parse::().map_err(|_| "Must be a valid number")?; if number < 1 || number > 250 { Err(String::from( "Number of downloads must be between 1 and 250", )) } else { - Ok(()) + Ok(val.to_string()) } } -fn validate_duration(val: &str) -> Result<(), String> { +fn validate_duration(val: &str) -> Result { let number = val.parse::().map_err(|_| "Must be a valid number")?; - if number < 1 || number > 30 { - Err(String::from("Duration must be between 1 and 30 days")) + if [1, 7, 15, 30].contains(&number) { + Ok(val.to_string()) } else { - Ok(()) + Err(String::from("Duration must be 1, 7, 15 or 30")) } } @@ -147,30 +122,61 @@ mod tests { use super::*; #[test] - fn test_verify_link_format() { + fn test_is_swisstransfer_link() { + let link = "https://www.swisstransfer.com/d/8b3b3b3b-3b3b-3b3b-3b3b-3b3b3b3b3b3b"; + assert_eq!(is_swisstransfer_link(link), true); + let link = "http://www.swisstransfer.com/d/8b3b3b3b-3b3b-3b3b-3b3b-3b3b3b3b3b3b/"; + assert_eq!(is_swisstransfer_link(link), false); + let link = "https://www.swisstransfer.ch/d/8b3b3b3b-3b3b-3b3b-3b3b-3b3b3b3b3b3b/"; + assert_eq!(is_swisstransfer_link(link), false); + let link = "www.swisstransfer.com/d/8b3b3b3b-3b3b-3b3b-3b3b-3b3b3b3b3b3b"; + assert_eq!(is_swisstransfer_link(link), false); + let link = "https://www.swisstransfer.com/8b3b3b3b-3b3b-3b3b-3b3b-3b3b3b3b3b3b"; + assert_eq!(is_swisstransfer_link(link), false); + } + + #[test] + fn test_path_exists() { + let path = "Cargo.toml"; + assert_eq!(path_exists(path), true); + let path = "Cargo.toml2"; + assert_eq!(path_exists(path), false); + } + + #[test] + fn test_validate_number_download() { + let number = "250"; + assert_eq!(validate_number_download(number), Ok(number.to_string())); + let number = "251"; assert_eq!( - verify_link_format( - "https://www.swisstransfer.com/d/3215702a-bed4-4cec-9eb6-d731048a2312" - ), - true + validate_number_download(number), + Err(String::from("Number of downloads must be between 1 and 250")) ); + let number = "0"; assert_eq!( - verify_link_format( - "https://www.swisstransfer.com/d/3215702a-bed4-4cec-9eb6-d731048a231" - ), - false + validate_number_download(number), + Err(String::from("Number of downloads must be between 1 and 250")) ); + let number = "a"; + assert_eq!(validate_number_download(number), Err(String::from("Must be a valid number"))); + } + + #[test] + fn test_validate_duration() { + let duration = "30"; + assert_eq!(validate_duration(duration), Ok(duration.to_string())); + let duration = "31"; assert_eq!( - verify_link_format( - "https://www.swisstransfer.com/d/3215702a-bed4-4cec-9eb6-d731048a2312/" - ), - false + validate_duration(duration), + Err(String::from("Duration must be 1, 7, 15 or 30")) ); + let duration = "0"; assert_eq!( - verify_link_format( - "https://www.swisstransfer.com/f/3215702a-bed4-4cec-9eb6-d731048a2312" - ), - false + validate_duration(duration), + Err(String::from("Duration must be 1, 7, 15 or 30")) ); + let duration = "a"; + assert_eq!(validate_duration(duration), Err(String::from("Must be a valid number"))); } + } diff --git a/src/swissfiles/mod.rs b/src/swissfiles/mod.rs index 3c65a44..7745f4b 100644 --- a/src/swissfiles/mod.rs +++ b/src/swissfiles/mod.rs @@ -114,10 +114,9 @@ impl Swissfiles { } pub fn new_localfiles( - path: &str, + path: PathBuf, upload_parameter: &UploadParameters, ) -> Result { - let path = PathBuf::from(path); //Wow that sucks let path_clone = path.clone(); @@ -163,7 +162,7 @@ impl Swissfiles { Ok(()) } - pub fn upload(&self) -> Result<(), SwishError> { + pub fn upload(&self) -> Result { for file in &self.files { match file { Swissfile::Local(local_swissfile) => { @@ -177,8 +176,8 @@ impl Swissfiles { } } - println!("Here is your download link {}", self.finalize_upload()?); - Ok(()) + let download_link = self.finalize_upload()?; + Ok(download_link) } fn finalize_upload(&self) -> Result { @@ -243,7 +242,7 @@ fn get_container( "lang": upload_parameter.lang, "recaptcha": "nope", "files": files_string, - "recipientsEmails": "[]" + "recipientsEmails": "[]" // We might want to add this feature later seems pretty useless for my use case }); let payload_string = serde_json::to_string(&payload).unwrap(); diff --git a/src/swissfiles/swissfile/mod.rs b/src/swissfiles/swissfile/mod.rs index b6ab6d1..58459b5 100644 --- a/src/swissfiles/swissfile/mod.rs +++ b/src/swissfiles/swissfile/mod.rs @@ -72,8 +72,6 @@ impl LocalSwissfile { } Ok(()) - // Send a final request to the uploadComplete endpoint - //let remote_swissfile = self.finalize_upload()?; THIS HAS TO BE DONE IN THE CONTAINER } fn build_chunked_upload_url(&self, chunk: &Chunk) -> String { @@ -192,10 +190,20 @@ impl RemoteSwissfile { let file = std::fs::File::create(&out_path)?; let url = self.url.clone(); - let easy2 = new_easy2_download(url, None, file)?; + let easy2 = new_easy2_download(url, None, file, self.size)?; easy2.perform()?; - Ok(()) + match easy2.response_code()? { + 500 => { + // Clean up the file as it is invalid anyway + std::fs::remove_file(&out_path)?; + + // we are not sure but we can assume that this is the error x) + Err(SwishError::DownloadNumberExceeded) + }, + _ => Ok(()), + } + } } diff --git a/src/swissfiles/uploadparameters/mod.rs b/src/swissfiles/uploadparameters/mod.rs index 3834e73..31b05a8 100644 --- a/src/swissfiles/uploadparameters/mod.rs +++ b/src/swissfiles/uploadparameters/mod.rs @@ -3,7 +3,7 @@ pub struct UploadParameters { pub author_email: String, pub password: String, pub message: String, - pub number_of_download: u64, + pub number_of_download: u16, pub lang: String, pub recipients_emails: Vec, } diff --git a/tests/integrations.rs b/tests/integrations.rs index d20d363..b9b63ed 100644 --- a/tests/integrations.rs +++ b/tests/integrations.rs @@ -1,8 +1,5 @@ -use swish::api::upload; -use swish::api::download; -use swish::localfiles; -use swish::swissfiles::Swissfiles; -use swish::swissfiles::swissfile::Swissfile; +use swish::swissfiles::{uploadparameters::UploadParameters, Swissfiles}; + const TEST_FILE_BASEPATH: &str = "tests/file_samples/"; const TEST_FILE_DOWNLOADED_BASEPATH: &str = "tests/downloaded_files/"; @@ -37,40 +34,22 @@ fn test_single_file_upload_download(){ let actual_file_name = file_path.file_name().unwrap().to_str().unwrap(); let orignal_hash = hash_file(file_path.to_str().unwrap()); - let default_params = localfiles::parameters::Parameters::default(); - let local_files = localfiles::Localfiles::new(&file_path.to_str().unwrap().to_string(), default_params); - let download_link = upload(&local_files).unwrap(); + let default_params = UploadParameters::default(); + + //upload the file + let local_files = Swissfiles::new_localfiles(file_path.clone(), &default_params).unwrap(); + let download_link = local_files.upload().unwrap(); - // Wait for security checks on infomaniak's side - print!("Waiting for security checks on Infomaniak's side (30s)"); - std::thread::sleep(std::time::Duration::from_secs(30)); + // Download the file + let remote_files = Swissfiles::new_remotefiles(&download_link, None).unwrap(); + remote_files.download(None).unwrap(); - download(&download_link, None, Some(TEST_FILE_DOWNLOADED_BASEPATH)).unwrap(); let downloaded_file_path = TEST_FILE_DOWNLOADED_BASEPATH.to_string() + actual_file_name; let downloaded_hash = hash_file(&downloaded_file_path); assert_eq!(orignal_hash, downloaded_hash); std::fs::remove_file(&downloaded_file_path).unwrap(); } -#[test] -fn test_multiple_files_upload_download(){ - let file_paths = std::fs::read_dir(TEST_FILE_BASEPATH).unwrap().map(|entry| entry.unwrap().path()).collect::>(); - let orignal_hashes = file_paths.iter().map(|path| hash_file(path.to_str().unwrap())).collect::>(); - let default_params = localfiles::parameters::Parameters::default(); - let local_files = localfiles::Localfiles::new(&TEST_FILE_BASEPATH.to_string(), default_params); - let download_link = upload(&local_files).unwrap(); - - // Wait for security checks on infomaniak's side - print!("Waiting for security checks on Infomaniak's side (30s)"); - std::thread::sleep(std::time::Duration::from_secs(30)); - - download(&download_link, None, Some(TEST_FILE_DOWNLOADED_BASEPATH)).unwrap(); - let downloaded_file_paths = file_paths.iter().map(|path| TEST_FILE_DOWNLOADED_BASEPATH.to_string() + path.file_name().unwrap().to_str().unwrap()).collect::>(); - let downloaded_hashes = downloaded_file_paths.iter().map(|path| hash_file(path)).collect::>(); - assert_eq!(orignal_hashes, downloaded_hashes); - downloaded_file_paths.iter().for_each(|path| std::fs::remove_file(path).unwrap()); -} -