diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..34a30b9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Miru Release + +on: + release: + types: [created] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + output_name: eru_linux + - os: macos-latest + output_name: eru_macos + - os: windows-latest + output_name: eru_windows.exe + + steps: + - uses: actions/checkout@v2 + + - name: Install dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libcurl4-openssl-dev + + - name: Install dependencies (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install curl + + - name: Install dependencies (Windows) + if: matrix.os == 'windows-latest' + run: | + vcpkg install curl:x64-windows + vcpkg integrate install + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build ${{github.workspace}}/build --config Release + + - name: Rename binary + run: | + if [ "${{ matrix.os }}" == "windows-latest" ]; then + mv ${{github.workspace}}/build/src/Release/eru.exe ${{github.workspace}}/${{ matrix.output_name }} + else + mv ${{github.workspace}}/build/src/eru ${{github.workspace}}/${{ matrix.output_name }} + fi + shell: bash + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ${{github.workspace}}/${{ matrix.output_name }} + asset_name: ${{ matrix.output_name }} + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e356c8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + + +/build/* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..fa6474c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.14) +project(file_downloader VERSION 1.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +FetchContent_Declare( + cpr + GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG 1.10.x +) +FetchContent_MakeAvailable(cpr) + +FetchContent_Declare( + cli11 + GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git + GIT_TAG v2.3.2 +) +FetchContent_MakeAvailable(cli11) + +FetchContent_Declare( + indicators + GIT_REPOSITORY https://github.com/p-ranav/indicators.git + GIT_TAG v2.3 +) +FetchContent_MakeAvailable(indicators) + +add_subdirectory(src) diff --git a/README.md b/README.md new file mode 100644 index 0000000..322e2ba --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Eru: Multi-threaded File Downloader + +Eru is a simple and efficient multi-threaded file downloader designed to accelerate your download speeds. Written in C++, Eru leverages multiple threads to download file chunks concurrently, potentially increasing download speeds significantly. I made this as I had a simple requirement and did not want to buy IDM or pirate it. + +## Features + +- Multi-threaded downloading +- Progress bar with real-time updates +- Automatic filename detection from URL +- Customizable number of download threads +- Supports both HTTP and HTTPS +- Displays download speed and estimated time remaining +- Automatic merging of downloaded chunks + +## Requirements + +- C++17 compatible compiler +- CMake 3.14 or higher +- libcurl +- CLI11 +- indicators + +## Building from Source + +1. Clone the repository: + ``` + git clone https://github.com/Dank-del/eru.git + cd eru + ``` + +2. Create a build directory and navigate to it: + ``` + mkdir build && cd build + ``` + +3. Configure the project with CMake: + ``` + cmake .. + ``` + +4. Build the project: + ``` + cmake --build . + ``` + +## Usage + +After building, you can run Eru using the following command: + +``` +./src/eru --url [options] +``` + +### Options + +- `-u, --url `: Specify the URL of the file to download (required) +- `-o, --output `: Set the output filename (optional, auto-detected if not specified) +- `-t, --threads `: Set the number of download threads (default: 4) +- `-a, --about`: Display information about Eru +- `-h, --help`: Show help message + +### Examples + +1. Download a file using default settings: + ``` + ./src/eru --url https://example.com/large_file.zip + ``` + +2. Download a file with a custom output name and 8 threads: + ``` + ./src/eru --url https://example.com/large_file.zip --output my_file.zip --threads 8 + ``` + +3. Display information about Eru: + ``` + ./src/eru --about + ``` + +## Contributing + +Contributions to Eru are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Author + +Sayan Biswas +- Email: me@sayanbiswas.in + +## Acknowledgments + +- [CPR](https://github.com/libcpr/cpr) for HTTP requests +- [CLI11](https://github.com/CLIUtils/CLI11) for command-line parsing +- [indicators](https://github.com/p-ranav/indicators) for progress bars diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8171f95 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(downloader STATIC + downloader.cpp + downloader.h +) + +target_link_libraries(downloader PUBLIC cpr::cpr indicators::indicators) + +add_executable(eru main.cpp) +target_link_libraries(eru PRIVATE downloader CLI11::CLI11 indicators::indicators) diff --git a/src/downloader.cpp b/src/downloader.cpp new file mode 100644 index 0000000..8688f17 --- /dev/null +++ b/src/downloader.cpp @@ -0,0 +1,134 @@ +#include "downloader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::mutex console_mutex; +std::atomic total_progress(0); + +std::string get_filename_from_url(const std::string &url) +{ + size_t pos = url.find_last_of("/"); + if (pos != std::string::npos) + { + return url.substr(pos + 1); + } + return "downloaded_file"; +} + +std::string format_size(size_t size) +{ + double size_mb = static_cast(size) / (1024 * 1024); + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << size_mb; + return ss.str(); +} + +void Downloader::download_file(const std::string &url, const std::optional &output_file, int num_threads) +{ + cpr::Response r = cpr::Head(cpr::Url{url}); + if (r.status_code != 200) + { + std::cerr << "Failed to get file information. Status code: " << r.status_code << std::endl; + return; + } + + size_t file_size = std::stoull(r.header["Content-Length"]); + size_t chunk_size = std::ceil(static_cast(file_size) / num_threads); + + std::string filename = output_file.value_or(get_filename_from_url(url)); + std::cout << "Downloading to: " << filename << std::endl; + std::cout << "File size: " << format_size(file_size) << " MB" << std::endl; + + indicators::ProgressBar progress_bar{ + indicators::option::BarWidth{50}, + indicators::option::Start{"["}, + indicators::option::Fill{"="}, + indicators::option::Lead{">"}, + indicators::option::Remainder{" "}, + indicators::option::End{"]"}, + indicators::option::ForegroundColor{indicators::Color::green}, + indicators::option::ShowElapsedTime{true}, + indicators::option::ShowRemainingTime{true}, + indicators::option::FontStyles{std::vector{indicators::FontStyle::bold}}}; + + std::vector threads; + for (int i = 0; i < num_threads; ++i) + { + size_t start = i * chunk_size; + size_t end = (i == num_threads - 1) ? file_size - 1 : (i + 1) * chunk_size - 1; + threads.emplace_back(&Downloader::download_chunk, url, start, end, filename, i); + } + + while (total_progress < file_size) + { + size_t current_progress = total_progress.load(); + progress_bar.set_progress(100.0 * current_progress / file_size); + std::cout << "\rDownloaded: " << format_size(current_progress) << " MB / " << format_size(file_size) << " MB" << std::flush; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + for (auto &t : threads) + { + t.join(); + } + + progress_bar.set_progress(100); + std::cout << "\nDownload complete. Merging chunks..." << std::endl; + + merge_chunks(filename, num_threads); + + std::cout << "File saved as: " << filename << std::endl; +} + +void Downloader::download_chunk(const std::string &url, size_t start, size_t end, const std::string &output_file, int chunk_id) +{ + std::string chunk_file = output_file + ".part" + std::to_string(chunk_id); + std::ofstream out(chunk_file, std::ios::binary); + + cpr::Response r = cpr::Get(cpr::Url{url}, + cpr::Header{{"Range", "bytes=" + std::to_string(start) + "-" + std::to_string(end)}}, + cpr::WriteCallback([&](const std::string &data, intptr_t) + { + out.write(data.c_str(), data.length()); + total_progress += data.length(); + return true; })); + + out.close(); + + if (r.status_code != 206) + { + std::lock_guard lock(console_mutex); + std::cerr << "Failed to download chunk " << chunk_id << ". Status code: " << r.status_code << std::endl; + } +} + +void Downloader::merge_chunks(const std::string &output_file, int num_chunks) +{ + std::ofstream out(output_file, std::ios::binary); + + for (int i = 0; i < num_chunks; ++i) + { + std::string chunk_file = output_file + ".part" + std::to_string(i); + std::ifstream in(chunk_file, std::ios::binary); + out << in.rdbuf(); + in.close(); + std::remove(chunk_file.c_str()); + } + + out.close(); +} + +bool Downloader::test_connection(const std::string &url) +{ + cpr::Response r = cpr::Head(cpr::Url{url}); + return r.status_code == 200; +} diff --git a/src/downloader.h b/src/downloader.h new file mode 100644 index 0000000..c66cfd7 --- /dev/null +++ b/src/downloader.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class Downloader +{ +public: + static void download_file(const std::string &url, const std::optional &output_file, int num_threads); + static bool test_connection(const std::string &url); + +private: + static void download_chunk(const std::string &url, size_t start, size_t end, const std::string &output_file, int chunk_id); + static void merge_chunks(const std::string &output_file, int num_chunks); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a4ce478 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,73 @@ +#include "downloader.h" +#include +#include + +#define ERU_VERSION "1.0.0" + +void print_about() +{ + std::cout << "Eru - A Multi-threaded File Downloader" << std::endl; + std::cout << "Version: " << ERU_VERSION << std::endl; + std::cout << "Author: Sayan Biswas" << std::endl; + std::cout << "Email: me@sayanbiswas.in" << std::endl; + std::cout << std::endl; + std::cout << "Eru is a simple and efficient multi-threaded file downloader" << std::endl; + std::cout << "designed to accelerate your download speeds." << std::endl; +} + +int main(int argc, char *argv[]) +{ + std::string url; + std::optional output_file; + int num_threads = 4; + bool show_about = false; + + CLI::App app{"Eru - Multi-threaded file downloader"}; + app.add_option("-u,--url", url, "URL of the file to download"); + app.add_option("-o,--output", output_file, "Output file name (optional)"); + app.add_option("-t,--threads", num_threads, "Number of threads to use"); + app.add_flag("-a,--about", show_about, "Show information about Eru"); + + CLI11_PARSE(app, argc, argv); + + if (show_about) + { + print_about(); + return 0; + } + + if (url.empty()) + { + std::cerr << "Error: URL is required. Use -u or --url to specify the download URL." << std::endl; + std::cout << "Use --help for more information." << std::endl; + return 1; + } + + std::cout << "Eru v" << ERU_VERSION << std::endl; + std::cout << "URL: " << url << std::endl; + if (output_file) + { + std::cout << "Output file: " << *output_file << std::endl; + } + std::cout << "Threads: " << num_threads << std::endl; + + std::cout << "Testing connection..." << std::endl; + if (!Downloader::test_connection(url)) + { + std::cerr << "Failed to connect to the URL. Please check your internet connection and the URL." << std::endl; + return 1; + } + std::cout << "Connection test successful." << std::endl; + + try + { + Downloader::download_file(url, output_file, num_threads); + } + catch (const std::exception &e) + { + std::cerr << "An error occurred during download: " << e.what() << std::endl; + return 1; + } + + return 0; +}