diff --git a/CMakeLists.txt b/CMakeLists.txt index 437d18c..fb3b0e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required (VERSION 3.22) project(ffmpeg-version-manager LANGUAGES C CXX - VERSION 0.1.6 + VERSION 0.1.7 ) set(CMAKE_CXX_STANDARD 17) @@ -129,6 +129,10 @@ add_executable(ffmpeg-version-manager src/ui_elements.cc src/cli.cc src/tui.cc + src/curl_tools.cc + src/env_settings.linux.cc + src/env_settings.windows.cc + src/compress.cc ) target_include_directories(ffmpeg-version-manager diff --git a/README.md b/README.md index 05c8567..1b7047d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # FFmpeg Version Manager -Note: I made this code in a rush, this means that the code is hard to read. - A command-line tool to manage multiple versions of FFmpeg, enabling seamless switching between different versions for development and testing. ## Features @@ -11,6 +9,11 @@ A command-line tool to manage multiple versions of FFmpeg, enabling seamless swi - Build from source for custom versions - Cross-platform support (Linux, Windows) +*Note: On Linux only works if you are using bash & ~/.bashrc, help me to improve this to more envs* + +*Note: In runtime requires $HOME for bash or %USERPROFILE% for Windows (it is usually always available; this is only to advise if you are using scripts or any other env)* + + ## Installation ### Pre-built Binaries diff --git a/include/cli.hh b/include/cli.hh index 19c01b5..838391a 100644 --- a/include/cli.hh +++ b/include/cli.hh @@ -3,4 +3,4 @@ int run_cli(int argc, char *argv[]); -#endif // CLI_H \ No newline at end of file +#endif // CLI_H diff --git a/include/compress.hh b/include/compress.hh new file mode 100644 index 0000000..714c5a0 --- /dev/null +++ b/include/compress.hh @@ -0,0 +1,13 @@ +#include +#include + +#ifndef COMPRESS_H +#define COMPRESS_H + +int extract_from_memory_to_folder(const std::string &filedata, const std::filesystem::path &destination_dir, void* callback_data, void (*callback)(void*, size_t, size_t)); +inline int extract_from_memory_to_folder(const std::string &filedata, const std::filesystem::path &destination_dir) +{ + return extract_from_memory_to_folder(filedata, destination_dir, NULL, NULL); +} + +#endif // COMPRESS_H diff --git a/include/curl_tools.hh b/include/curl_tools.hh new file mode 100644 index 0000000..1d2d088 --- /dev/null +++ b/include/curl_tools.hh @@ -0,0 +1,16 @@ +#include +#include + +#ifndef CURL_TOOLS_H +#define CURL_TOOLS_H + +bool is_curl_cert_error(CURLcode res); +CURL* init_curl_request(std::string url); +void set_unsecure_curl(CURL* curl); +void set_progress_callback_curl(CURL* curl, void* callback_data, int (*callback)(void*, curl_off_t, curl_off_t, curl_off_t, curl_off_t)); +void destroy_curl(CURL* curl); +CURLcode launch_curl_request(CURL* curl); +CURLcode launch_curl_request_result(CURL* curl, std::string* result); +int quick_curl_request(std::string url, std::string* result); + +#endif // CURL_TOOLS_H diff --git a/include/environment.hh b/include/environment.hh index 75b13aa..23a61e2 100644 --- a/include/environment.hh +++ b/include/environment.hh @@ -1,4 +1,5 @@ #include +#include #ifndef ENVIRONMENT_H #define ENVIRONMENT_H @@ -7,4 +8,7 @@ int setup_env(std::string version); int remove_env(); std::filesystem::path get_ffmpeg_vm_dir(); -#endif // ENVIRONMENT_H \ No newline at end of file +int os_setup_env(std::string version, std::filesystem::path ffmpeg_vm_dir, const char* home); +int os_remove_env(std::filesystem::path ffmpeg_vm_dir, const char* home); + +#endif // ENVIRONMENT_H diff --git a/include/request.hh b/include/request.hh index 527f690..ffef12d 100644 --- a/include/request.hh +++ b/include/request.hh @@ -14,8 +14,20 @@ typedef struct FFMPEG_VERSION_S { std::string url; } FFMPEG_VERSION; +inline std::ostream& operator<<(std::ostream& ostr, const FFMPEG_VERSION& ver) +{ + ostr << ver.version << ": " << ver.url; + + return ostr; +} + +inline bool operator<(const FFMPEG_VERSION& a, const FFMPEG_VERSION& b) +{ + return a.version < b.version; +} + std::vector get_ffmpeg_versions(); -std::string download_file(std::string url, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen); -int extract(const std::string &filedata, const std::filesystem::path &destination_dir, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen); +std::string display_download_file(std::string url, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen); +int display_extract(const std::string &filedata, const std::filesystem::path &destination_dir, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen); #endif // REQUEST_H diff --git a/include/tui.hh b/include/tui.hh index 081dc0b..a91f7cf 100644 --- a/include/tui.hh +++ b/include/tui.hh @@ -3,4 +3,4 @@ int run_tui(); -#endif // TUI_H \ No newline at end of file +#endif // TUI_H diff --git a/include/ui_elements.hh b/include/ui_elements.hh index 450f88c..f57bc4c 100644 --- a/include/ui_elements.hh +++ b/include/ui_elements.hh @@ -10,4 +10,4 @@ void display_alert(ftxui::Element content, const std::chrono::seconds t); std::string generate_slider(float percent); std::string center_text(std::string str); -#endif // UI_ELEMENTS_H \ No newline at end of file +#endif // UI_ELEMENTS_H diff --git a/src/cli.cc b/src/cli.cc index ce8b5a0..4355e9f 100644 --- a/src/cli.cc +++ b/src/cli.cc @@ -8,6 +8,8 @@ #include "environment.hh" #include "request.hh" +#include "curl_tools.hh" +#include "compress.hh" using namespace std; @@ -28,7 +30,7 @@ OPTIONS default_options() void print_help() { - cout << "ffmpeg-version-manager v0.1.6" << endl; + cout << "ffmpeg-version-manager v0.1.7" << endl; cout << "Arguments:" << endl; cout << " -h/--help -> Print this screen." << endl; cout << " -u/--uninstall -> Uninstall the current ffmpeg-vm installed and the current env." << endl; @@ -155,10 +157,23 @@ int run_cli(int argc, char *argv[]) filesystem::path downloaddir = get_ffmpeg_vm_dir(); cout << "Downloading ffmpeg " + version.version + "..." << endl; - const string fdata = download_file(version.url, NULL, NULL); + string fdata; + int status = quick_curl_request(version.url, &fdata); + + if(status != 0) + { + cout << "Error on download the file" << endl; + return 2; + } cout << "Extracting files..." << endl; - extract(fdata, downloaddir, NULL, NULL); + status = extract_from_memory_to_folder(fdata, downloaddir); + + if(status != 0) + { + cout << "Error on extracting the file" << endl; + return 3; + } #ifdef _WIN32 cout << "Done! Please open a new terminal to load the custom env for ffmpeg-vm" << endl; diff --git a/src/compress.cc b/src/compress.cc new file mode 100644 index 0000000..853413c --- /dev/null +++ b/src/compress.cc @@ -0,0 +1,178 @@ +#include "compress.hh" + +#include +#include + +#include +#include + +using namespace std; +namespace fs = std::filesystem; + +int extract_from_memory_to_folder(const std::string &filedata, const std::filesystem::path &destination_dir, void* callback_data, void (*callback)(void*, size_t, size_t)) +{ + struct archive *archiv; + struct archive_entry *entry; + int result; + + archiv = archive_read_new(); + archive_read_support_filter_all(archiv); // Support for gzip, bzip2, xz, etc. + archive_read_support_format_all(archiv); // Support for tar, zip, 7zip, etc. + + // Load from memory + result = archive_read_open_memory(archiv, filedata.data(), filedata.size()); + + if (result != ARCHIVE_OK) + { + cerr << "Error opening archive from memory: " << archive_error_string(archiv) << endl; + archive_read_free(archiv); + return 1; + } + + // Number of chars to remove at the start of the path + size_t ffmpeg_entry_path_lenght = 0; + + size_t total_entries = 0; + size_t processed_entries = 0; + + // Read all entry to know how many files are + while (archive_read_next_header(archiv, &entry) == ARCHIVE_OK) { + total_entries++; + archive_read_data_skip(archiv); // Saltar los datos para solo contar + } + + // Restart the read + archive_read_free(archiv); + archiv = archive_read_new(); + archive_read_support_filter_all(archiv); + archive_read_support_format_all(archiv); + result = archive_read_open_memory(archiv, filedata.data(), filedata.size()); + + if (result != ARCHIVE_OK) + { + cerr << "Error reopening archive from memory: " << archive_error_string(archiv) << endl; + archive_read_free(archiv); + return 1; + } + + // Read every entry (file/dir) inside the compressed file + while (archive_read_next_header(archiv, &entry) == ARCHIVE_OK) + { + if (total_entries > 0 && callback != NULL) + { + processed_entries++; + + callback(callback_data, processed_entries, total_entries); + } + + fs::path entry_path = fs::path(archive_entry_pathname(entry)); + + if (ffmpeg_entry_path_lenght != 0) + { + string temp = entry_path.string(); + + temp.erase(0, ffmpeg_entry_path_lenght); + + entry_path = fs::path(temp); + } + + const fs::path full_dest_path = destination_dir / entry_path; + + /* + The first entry is the root folder + because we want all in ffmpeg-vm/... and not in ffmpeg-vm/something/..., we check this path to remove + */ + if (ffmpeg_entry_path_lenght == 0 && entry_path.string().rfind("ffmpeg") == 0) // Check if starts with ffmpeg + { + ffmpeg_entry_path_lenght = entry_path.string().length(); + continue; + } + + // Make sure that the folder exist + if (full_dest_path.has_parent_path()) + { + fs::create_directories(full_dest_path.parent_path()); + } + + // If is only a directory, create the dir + if (archive_entry_filetype(entry) == AE_IFDIR) + { + fs::create_directories(full_dest_path); + + continue; + } + +#ifndef _WIN32 + // If is only a symlink, create the symlink + if (archive_entry_filetype(entry) == AE_IFLNK) + { + const char* link_target_cstr = archive_entry_symlink(entry); + + if (link_target_cstr) { + // fs::create_symlink can fail if the link exist + std::error_code ec; + fs::remove(full_dest_path, ec); // Remove to prevent errors + fs::create_symlink(fs::path(link_target_cstr), full_dest_path); + } else { + cerr << "Warning: could not read symlink target for " << full_dest_path << endl; + } + + continue; + } +#endif + + // if it's a file, write it to disk + ofstream outfile(full_dest_path, ios::binary); + + if (!outfile) + { + cerr << "Error: Could not open file for writing: " << full_dest_path << endl; + archive_read_close(archiv); + archive_read_free(archiv); + return 2; + } + + const void *buff; // Points to the data, no need to free, is only a pointer in memory + size_t size; + la_int64_t offset; + + // Read the blocks and write it + while ((result = archive_read_data_block(archiv, &buff, &size, &offset)) == ARCHIVE_OK) + { + outfile.write(static_cast(buff), size); + } + + if (result != ARCHIVE_EOF) + { + cerr << "Error reading data block: " << archive_error_string(archiv) << endl; + archive_read_close(archiv); + archive_read_free(archiv); + return 3; + } + +#ifndef _WIN32 + // Restore permissions + if (archive_entry_filetype(entry) != AE_IFLNK) { + try { + fs::permissions(full_dest_path, static_cast(archive_entry_perm(entry)), fs::perm_options::replace); + } catch (const exception& e) { + cerr << "Warning: could not set permissions for " << full_dest_path << ". " << e.what() << endl; + } + } +#endif + } + + // Check if error on readling last header + result = archive_read_close(archiv); + + if (result != ARCHIVE_OK) + { + cerr << "Error closing archive: " << archive_error_string(archiv) << endl; + archive_read_free(archiv); + return 4; + } + + archive_read_free(archiv); + + return 0; +} diff --git a/src/curl_tools.cc b/src/curl_tools.cc new file mode 100644 index 0000000..bbbb0c4 --- /dev/null +++ b/src/curl_tools.cc @@ -0,0 +1,97 @@ +#include +#include + +using namespace std; + +// Callback function to write curl response to a string +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *response) +{ + size_t totalSize = size * nmemb; + response->append((char*)contents, totalSize); + return totalSize; +} + +bool is_curl_cert_error(CURLcode res) +{ + if(res != CURLE_OK) cout << curl_easy_strerror(res) << endl; + + return res == CURLE_PEER_FAILED_VERIFICATION || res == CURLE_SSL_CACERT || res == CURLE_SSL_CACERT_BADFILE; +} + +CURL* init_curl_request(string url) +{ + CURL *curl; + + curl = curl_easy_init(); + + if (!curl) { + cerr << "Failed to initialize curl" << endl; + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + return curl; +} + +void set_unsecure_curl(CURL* curl) +{ + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + + cout << "set_unsecure_curl: Disabling SSL Check" << endl; +} + +void set_progress_callback_curl(CURL* curl, void* callback_data, int (*callback)(void*, curl_off_t, curl_off_t, curl_off_t, curl_off_t)) +{ + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, callback_data); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); // Enable download progress +} + +void destroy_curl(CURL* curl) +{ + curl_easy_cleanup(curl); +} + +CURLcode launch_curl_request(CURL* curl) +{ + return curl_easy_perform(curl); +} + +CURLcode launch_curl_request_result(CURL* curl, string* result) +{ + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, result); + + return curl_easy_perform(curl); +} + +int quick_curl_request(string url, string* result) +{ + CURL *curl = init_curl_request(url); + + if(curl == NULL) return 1; + + CURLcode res = launch_curl_request_result(curl, result); + + if(is_curl_cert_error(res)) + { + result->clear(); + + set_unsecure_curl(curl); + res = launch_curl_request_result(curl, result); + } + + destroy_curl(curl); + + if(res != CURLE_OK) + { + cout << "curl_request: Error on request: " << curl_easy_strerror(res) << endl; + + return 2; + } + + return 0; +} diff --git a/src/env_settings.linux.cc b/src/env_settings.linux.cc new file mode 100644 index 0000000..170fd8c --- /dev/null +++ b/src/env_settings.linux.cc @@ -0,0 +1,108 @@ +#ifndef _WIN32 + +#define HOME "HOME" + +#include "environment.hh" + +#include +#include +#include +#include +#include + +// If you using namespace std and windows.h it creates a confict in std::byte and windows byte +namespace fs = std::filesystem; + +int os_setup_env(std::string version, std::filesystem::path ffmpeg_vm_dir, const char* home) +{ + fs::path bashrc_path = fs::path(home) / ".bashrc"; + std::ifstream bashrc_in(bashrc_path); + if (!bashrc_in.is_open()) { + std::cerr << "Error: Could not open " << bashrc_path << std::endl; + return 3; + } + + std::string content((std::istreambuf_iterator(bashrc_in)), std::istreambuf_iterator()); + bashrc_in.close(); + + const std::string start_marker = "# --- ffmpeg-vm start ---"; + const std::string end_marker = "# --- ffmpeg-vm end ---"; + const std::string new_section = start_marker + + "\nexport PATH=\"" + (ffmpeg_vm_dir / "bin").string() + ":$PATH\"\n" + + "export FFMPEGVM_PATH=\"" + ffmpeg_vm_dir.string() + "\"\n" + + end_marker; + + if (content.find(start_marker) != std::string::npos) { + std::cout << "ffmpeg-vm section already exists in .bashrc" << std::endl; + return 4; + } + + std::ofstream bashrc_out(bashrc_path, std::ios_base::app); + if (!bashrc_out.is_open()) { + std::cerr << "Error: Could not open " << bashrc_path << " for writing." << std::endl; + return 5; + } + bashrc_out << "\n" << new_section << "\n"; + bashrc_out.close(); + + std::ofstream ffmpeg_version(ffmpeg_vm_dir / "VERSION"); + + if (!ffmpeg_version.is_open()) { + std::cerr << "Error: Could not open " << (ffmpeg_vm_dir / "VERSION") << " for writing." << std::endl; + return 5; + } + + ffmpeg_version << version << "\n"; + ffmpeg_version.close(); + + return 0; +} + +int os_remove_env(std::filesystem::path ffmpeg_vm_dir, const char* home) +{ + fs::path bashrc_path = fs::path(home) / ".bashrc"; + std::ifstream bashrc_in(bashrc_path); + if (!bashrc_in.is_open()) { + std::cerr << "Error: Could not open " << bashrc_path << std::endl; + return 2; + } + + std::string content((std::istreambuf_iterator(bashrc_in)), std::istreambuf_iterator()); + bashrc_in.close(); + + const std::string start_marker = "# --- ffmpeg-vm start ---"; + const std::string end_marker = "# --- ffmpeg-vm end ---"; + + size_t start_pos = content.find(start_marker); + if (start_pos == std::string::npos) { + std::cout << "No ffmpeg-vm section found in .bashrc" << std::endl; + return 4; + } + size_t end_pos = content.find(end_marker, start_pos); + if (end_pos == std::string::npos) { + std::cout << "No end marker found after start marker in .bashrc" << std::endl; + return 5; + } + end_pos += end_marker.length(); + + // Handle newline characters to avoid extra empty lines + if (end_pos < content.length() && content[end_pos] == '\n') { + end_pos++; + } else if (start_pos > 0 && content[start_pos - 1] == '\n') { + start_pos--; + } + + content.erase(start_pos - 1, end_pos - start_pos + 1); /* -1 and +1 to include the \n */ + + std::ofstream bashrc_out(bashrc_path); + if (!bashrc_out.is_open()) { + std::cerr << "Error: Could not open " << bashrc_path << " for writing." << std::endl; + return 6; + } + bashrc_out << content; + bashrc_out.close(); + + return 0; +} + +#endif diff --git a/src/env_settings.windows.cc b/src/env_settings.windows.cc new file mode 100644 index 0000000..78cf8b4 --- /dev/null +++ b/src/env_settings.windows.cc @@ -0,0 +1,148 @@ +#ifdef _WIN32 + +#include "environment.hh" + +#include +#include +#include +#include +#include + +// If you using namespace std and windows.h it creates a confict in std::byte and windows byte +namespace fs = std::filesystem; + +#define HOME "USERPROFILE" + +#include + +static std::string get_registry_value(HKEY root_key, const std::string& sub_key, const std::string& value_name) +{ + HKEY hKey; + std::string result; + LONG lResult = RegOpenKeyExA(root_key, sub_key.c_str(), 0, KEY_READ, &hKey); + if (lResult != ERROR_SUCCESS) return result; + + DWORD dwType = REG_EXPAND_SZ; + DWORD dwSize = 0; + lResult = RegQueryValueExA(hKey, value_name.c_str(), NULL, &dwType, NULL, &dwSize); + if (lResult == ERROR_SUCCESS && dwSize > 0) { + std::vector buffer(dwSize); + lResult = RegQueryValueExA(hKey, value_name.c_str(), NULL, &dwType, (LPBYTE)buffer.data(), &dwSize); + if (lResult == ERROR_SUCCESS) { + result = std::string(buffer.data(), dwSize - 1); + } + } + + RegCloseKey(hKey); + return result; +} + +static LONG set_registry_key(HKEY root_key, const std::string& sub_key, const std::string& value_name, const std::string& value) +{ + HKEY hKey; + LONG lResult = RegOpenKeyExA(root_key, sub_key.c_str(), 0, KEY_WRITE, &hKey); + if (lResult != ERROR_SUCCESS) return lResult; + + lResult = RegSetValueExA(hKey, value_name.c_str(), 0, REG_EXPAND_SZ, + (const BYTE*)value.c_str(), value.length() + 1); + + RegCloseKey(hKey); + return lResult; +} + +static LONG delete_registry_key(HKEY root_key, const std::string& sub_key, const std::string& value_name) +{ + HKEY hKey; + LONG lResult = RegOpenKeyExA(root_key, sub_key.c_str(), 0, KEY_WRITE, &hKey); + if (lResult != ERROR_SUCCESS) return lResult; + + lResult = RegDeleteValueA(hKey, value_name.c_str()); + + RegCloseKey(hKey); + return lResult; +} + +static void notify_environment_change() +{ + SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, + (LPARAM)"Environment", SMTO_ABORTIFHUNG, 5000, NULL); +} + +int set_windows_path(const fs::path& ffmpeg_vm_dir) +{ + std::string dir_str = (ffmpeg_vm_dir / "bin").string(); + std::string currentPath = get_registry_value(HKEY_CURRENT_USER, "Environment", "Path"); + + LONG lResult = set_registry_key(HKEY_CURRENT_USER, "Environment", "FFMPEGVM_PATH", ffmpeg_vm_dir.string()); + if (lResult == ERROR_SUCCESS) { + notify_environment_change(); + } + + if (currentPath.find(dir_str) != std::string::npos) { + std::cout << "ffmpeg-vm already in PATH" << std::endl; + return 0; + } + + std::string newPath = dir_str + ";" + currentPath; + lResult = set_registry_key(HKEY_CURRENT_USER, "Environment", "Path", newPath); + if (lResult != ERROR_SUCCESS) { + std::cerr << "Error: Could not set PATH value" << std::endl; + return 1; + } + + notify_environment_change(); + return 0; +} + +int delete_windows_path(const fs::path& ffmpeg_vm_dir) +{ + std::string dir_str = (ffmpeg_vm_dir / "bin").string(); + std::string currentPath = get_registry_value(HKEY_CURRENT_USER, "Environment", "Path"); + + LONG lResult = delete_registry_key(HKEY_CURRENT_USER, "Environment", "FFMPEGVM_PATH"); + if (lResult == ERROR_SUCCESS) { + notify_environment_change(); + } + + size_t pos = currentPath.find(dir_str); + if (pos == std::string::npos) { + std::cout << "ffmpeg-vm not found in PATH" << std::endl; + return 0; + } + + std::string newPath = currentPath; + newPath.erase(pos, dir_str.length() + 1); + + lResult = set_registry_key(HKEY_CURRENT_USER, "Environment", "Path", newPath); + if (lResult != ERROR_SUCCESS) { + std::cerr << "Error: Could not set PATH value" << std::endl; + return 1; + } + + notify_environment_change(); + return 0; +} + +int os_setup_env(std::string version, std::filesystem::path ffmpeg_vm_dir, const char* home) +{ + int status = set_windows_path(ffmpeg_vm_dir); + + std::ofstream ffmpeg_version(ffmpeg_vm_dir / "VERSION"); + + if (!ffmpeg_version.is_open()) { + std::cerr << "Error: Could not open " << (ffmpeg_vm_dir / "VERSION") << " for writing." << std::endl; + return 5; + } + + ffmpeg_version << version << "\n"; + ffmpeg_version.close(); + + return status; +} + +int os_remove_env(std::filesystem::path ffmpeg_vm_dir, const char* home) +{ + return delete_windows_path(ffmpeg_vm_dir); +} + +#endif diff --git a/src/environment.cc b/src/environment.cc index 1e254e2..065cb77 100644 --- a/src/environment.cc +++ b/src/environment.cc @@ -16,89 +16,6 @@ namespace fs = std::filesystem; #define HOME "HOME" #endif -#ifdef _WIN32 -#include - -int update_windows_path(const fs::path& ffmpeg_vm_dir, bool add) { - HKEY hKey; - LONG lResult; - DWORD dwType = REG_EXPAND_SZ; - DWORD dwSize = 0; - std::string currentPath; - - // Open environment key - lResult = RegOpenKeyExA(HKEY_CURRENT_USER, "Environment", 0, KEY_READ | KEY_WRITE, &hKey); - if (lResult != ERROR_SUCCESS) { - std::cerr << "Error: Could not open environment registry key" << std::endl; - return 1; - } - - // Get current PATH value - lResult = RegQueryValueExA(hKey, "Path", NULL, &dwType, NULL, &dwSize); - if (lResult == ERROR_SUCCESS) { - std::vector buffer(dwSize); - lResult = RegQueryValueExA(hKey, "Path", NULL, &dwType, (LPBYTE)buffer.data(), &dwSize); - if (lResult == ERROR_SUCCESS) { - currentPath = std::string(buffer.data(), dwSize - 1); // Remove null terminator - } - } - - // Modify PATH - std::string newPath; - std::string dir_str = (ffmpeg_vm_dir / "bin").string(); - - if (add) { - lResult = RegSetValueExA(hKey, "FFMPEGVM_PATH", 0, REG_EXPAND_SZ, - (const BYTE*)ffmpeg_vm_dir.string().c_str(), ffmpeg_vm_dir.string().length() + 1); - if(lResult == ERROR_SUCCESS) - { - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, - (LPARAM)"Environment", SMTO_ABORTIFHUNG, 5000, NULL); - } - - if (currentPath.find(dir_str) != std::string::npos) { - std::cout << "ffmpeg-vm already in PATH" << std::endl; - RegCloseKey(hKey); - return 0; - } - newPath = dir_str + ";" + currentPath; - } else { - lResult = RegDeleteValueA(hKey, "FFMPEGVM_PATH"); - if(lResult == ERROR_SUCCESS) - { - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, - (LPARAM)"Environment", SMTO_ABORTIFHUNG, 5000, NULL); - } - - size_t pos = currentPath.find(dir_str); - if (pos == std::string::npos) { - std::cout << "ffmpeg-vm not found in PATH" << std::endl; - RegCloseKey(hKey); - return 0; - } - newPath = currentPath; - newPath.erase(pos, dir_str.length() + 1); // +1 to remove the semicolon - } - - // Set new PATH value - lResult = RegSetValueExA(hKey, "Path", 0, REG_EXPAND_SZ, - (const BYTE*)newPath.c_str(), newPath.length() + 1); - if (lResult != ERROR_SUCCESS) { - std::cerr << "Error: Could not set PATH value" << std::endl; - RegCloseKey(hKey); - return 1; - } - - RegCloseKey(hKey); - - // Notify system about environment change - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, - (LPARAM)"Environment", SMTO_ABORTIFHUNG, 5000, NULL); - - return 0; -} -#endif - fs::path get_ffmpeg_vm_dir() { const char* user_ffmpeg_path = getenv("FFMPEGVM_PATH"); @@ -139,63 +56,7 @@ int setup_env(std::string version) } } -#ifdef _WIN32 - int status = update_windows_path(ffmpeg_vm_dir, true); - - std::ofstream ffmpeg_version(ffmpeg_vm_dir / "VERSION"); - - if (!ffmpeg_version.is_open()) { - std::cerr << "Error: Could not open " << (ffmpeg_vm_dir / "VERSION") << " for writing." << std::endl; - return 5; - } - - ffmpeg_version << version << "\n"; - ffmpeg_version.close(); - - return status; -#else - fs::path bashrc_path = fs::path(home) / ".bashrc"; - std::ifstream bashrc_in(bashrc_path); - if (!bashrc_in.is_open()) { - std::cerr << "Error: Could not open " << bashrc_path << std::endl; - return 3; - } - - std::string content((std::istreambuf_iterator(bashrc_in)), std::istreambuf_iterator()); - bashrc_in.close(); - - const std::string start_marker = "# --- ffmpeg-vm start ---"; - const std::string end_marker = "# --- ffmpeg-vm end ---"; - const std::string new_section = start_marker - + "\nexport PATH=\"" + (ffmpeg_vm_dir / "bin").string() + ":$PATH\"\n" - + "export FFMPEGVM_PATH=\"" + ffmpeg_vm_dir.string() + "\"\n" - + end_marker; - - if (content.find(start_marker) != std::string::npos) { - std::cout << "ffmpeg-vm section already exists in .bashrc" << std::endl; - return 4; - } - - std::ofstream bashrc_out(bashrc_path, std::ios_base::app); - if (!bashrc_out.is_open()) { - std::cerr << "Error: Could not open " << bashrc_path << " for writing." << std::endl; - return 5; - } - bashrc_out << "\n" << new_section << "\n"; - bashrc_out.close(); - - std::ofstream ffmpeg_version(ffmpeg_vm_dir / "VERSION"); - - if (!ffmpeg_version.is_open()) { - std::cerr << "Error: Could not open " << (ffmpeg_vm_dir / "VERSION") << " for writing." << std::endl; - return 5; - } - - ffmpeg_version << version << "\n"; - ffmpeg_version.close(); - - return 0; -#endif + return os_setup_env(version, ffmpeg_vm_dir, home); } int remove_env() @@ -215,51 +76,5 @@ int remove_env() } } -#ifdef _WIN32 - return update_windows_path(ffmpeg_vm_dir, false); -#else - fs::path bashrc_path = fs::path(home) / ".bashrc"; - std::ifstream bashrc_in(bashrc_path); - if (!bashrc_in.is_open()) { - std::cerr << "Error: Could not open " << bashrc_path << std::endl; - return 2; - } - - std::string content((std::istreambuf_iterator(bashrc_in)), std::istreambuf_iterator()); - bashrc_in.close(); - - const std::string start_marker = "# --- ffmpeg-vm start ---"; - const std::string end_marker = "# --- ffmpeg-vm end ---"; - - size_t start_pos = content.find(start_marker); - if (start_pos == std::string::npos) { - std::cout << "No ffmpeg-vm section found in .bashrc" << std::endl; - return 4; - } - size_t end_pos = content.find(end_marker, start_pos); - if (end_pos == std::string::npos) { - std::cout << "No end marker found after start marker in .bashrc" << std::endl; - return 5; - } - end_pos += end_marker.length(); - - // Handle newline characters to avoid extra empty lines - if (end_pos < content.length() && content[end_pos] == '\n') { - end_pos++; - } else if (start_pos > 0 && content[start_pos - 1] == '\n') { - start_pos--; - } - - content.erase(start_pos - 1, end_pos - start_pos + 1); /* -1 and +1 to include the \n */ - - std::ofstream bashrc_out(bashrc_path); - if (!bashrc_out.is_open()) { - std::cerr << "Error: Could not open " << bashrc_path << " for writing." << std::endl; - return 6; - } - bashrc_out << content; - bashrc_out.close(); - - return 0; -#endif + return os_remove_env(ffmpeg_vm_dir, home); } diff --git a/src/request.cc b/src/request.cc index ac915ef..a2e0b87 100644 --- a/src/request.cc +++ b/src/request.cc @@ -1,5 +1,6 @@ #include "request.hh" #include "ui_elements.hh" +#include "curl_tools.hh" #include #include @@ -10,12 +11,9 @@ #include #include -#include - #include -#include -#include +#include "compress.hh" #include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive @@ -51,8 +49,10 @@ static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::stri return totalSize; } +// https://github.com/dryark/minibrew_deploy/blob/main/curlprog.m#L31 float last_progress = 0; -static int ProgressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + +static int DownloadCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { PROGRESSDATA* data = reinterpret_cast(clientp); ftxui::Element* display_slider = data->display_slider; @@ -79,55 +79,11 @@ static int ProgressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, vector get_ffmpeg_versions() { const char* custom_url = getenv("FFMPEGVM_URL"); - CURL *curl; - CURLcode res; string response; - - curl = curl_easy_init(); - if (!curl) { - cerr << "Failed to initialize curl" << endl; - return {}; - } - - curl_easy_setopt(curl, CURLOPT_URL, custom_url != NULL ? custom_url : FFMPEGVM_URL); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - - res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; - - // If error is SSL CA cert issue, retry insecure - if (res == CURLE_PEER_FAILED_VERIFICATION || - res == CURLE_SSL_CACERT || - res == CURLE_SSL_CACERT_BADFILE) { - - cerr << "Retrying without SSL verification (insecure!)..." << endl; - - // Disable verification - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - - response.clear(); // reset buffer - res = curl_easy_perform(curl); - - if (res != CURLE_OK) { - cerr << "Second attempt failed: " << curl_easy_strerror(res) << endl; - curl_easy_cleanup(curl); - return {}; - } - } else { - curl_easy_cleanup(curl); - return {}; - } - } - - curl_easy_cleanup(curl); + int status = quick_curl_request(custom_url != NULL ? custom_url : FFMPEGVM_URL, &response); // Error on request - if (response.empty()) + if (status != 0 || response.empty()) return {}; nlohmann::json json = nlohmann::json::parse(response); @@ -164,244 +120,69 @@ vector get_ffmpeg_versions() return list; } -string download_file(string url, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen) +string display_download_file(string url, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen) { - // TODO: Add download progress: https://github.com/dryark/minibrew_deploy/blob/main/curlprog.m#L31 - CURL *curl; - CURLcode res; + CURL *curl = init_curl_request(url); string response; - curl = curl_easy_init(); - if (!curl) { - cerr << "Failed to initialize curl" << endl; - return ""; - } - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + assert(display_slider != NULL && screen != NULL); // Use this function with a display - if(display_slider != NULL && screen != NULL) - { - PROGRESSDATA data; + PROGRESSDATA data; - data.display_slider = display_slider; - data.screen = screen; + data.display_slider = display_slider; + data.screen = screen; - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); - curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &data); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); // Enable download progress - } + set_progress_callback_curl(curl, &data, DownloadCallback); - res = curl_easy_perform(curl); - - last_progress = 0; + CURLcode res = launch_curl_request_result(curl, &response); - if (res != CURLE_OK) { - cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; + if(is_curl_cert_error(res)) + { + response.clear(); - // If error is SSL CA cert issue, retry insecure - if (res == CURLE_PEER_FAILED_VERIFICATION || - res == CURLE_SSL_CACERT || - res == CURLE_SSL_CACERT_BADFILE) { + set_unsecure_curl(curl); + res = launch_curl_request_result(curl, &response); + } - cerr << "Retrying without SSL verification (insecure!)..." << endl; + // After downloading reset window to 0 + last_progress = 0; - // Disable verification - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + destroy_curl(curl); - response.clear(); // reset buffer - res = curl_easy_perform(curl); + if(res != CURLE_OK) + { + cout << "curl_request: Error on request: " << curl_easy_strerror(res) << endl; - if (res != CURLE_OK) { - cerr << "Second attempt failed: " << curl_easy_strerror(res) << endl; - curl_easy_cleanup(curl); - return ""; - } - } else { - curl_easy_cleanup(curl); - return ""; - } + return ""; } - curl_easy_cleanup(curl); return response; } -int extract(const string &filedata, const fs::path &destination_dir, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen) +static void ExtractCallback(void *callback_data, size_t processed_entries, size_t total_entries) { - struct archive *archiv; - struct archive_entry *entry; - int result; - - archiv = archive_read_new(); - archive_read_support_filter_all(archiv); // Support for gzip, bzip2, xz, etc. - archive_read_support_format_all(archiv); // Support for tar, zip, 7zip, etc. - - // Load from memory - result = archive_read_open_memory(archiv, filedata.data(), filedata.size()); - - if (result != ARCHIVE_OK) - { - cerr << "Error opening archive from memory: " << archive_error_string(archiv) << endl; - archive_read_free(archiv); - return 1; - } + PROGRESSDATA* data = reinterpret_cast(callback_data); - // Number of chars to remove at the start of the path - size_t ffmpeg_entry_path_lenght = 0; - float prev_progress = 0; - size_t total_entries = 0; - size_t processed_entries = 0; - // Read all entry to know how many files are - while (archive_read_next_header(archiv, &entry) == ARCHIVE_OK) { - total_entries++; - archive_read_data_skip(archiv); // Saltar los datos para solo contar - } - - // Restart the read - archive_read_free(archiv); - archiv = archive_read_new(); - archive_read_support_filter_all(archiv); - archive_read_support_format_all(archiv); - result = archive_read_open_memory(archiv, filedata.data(), filedata.size()); - - if (result != ARCHIVE_OK) - { - cerr << "Error reopening archive from memory: " << archive_error_string(archiv) << endl; - archive_read_free(archiv); - return 1; - } + float progress = static_cast(processed_entries) / static_cast(total_entries); - // Read every entry (file/dir) inside the compressed file - while (archive_read_next_header(archiv, &entry) == ARCHIVE_OK) + // Reduce the amount of updates/s + if (progress - last_progress > 0.05) { - if (total_entries > 0 && display_slider != NULL && screen != NULL) - { - processed_entries++; - - float progress = static_cast(processed_entries) / static_cast(total_entries); - - // Reduce the amount of updates/s - if (progress - prev_progress > 0.05) - { - *display_slider = ftxui::text(generate_slider(progress > 0.95 ? 1.0f : progress)); - screen->PostEvent(ftxui::Event::Custom); - } - } - - fs::path entry_path = fs::path(archive_entry_pathname(entry)); - - if (ffmpeg_entry_path_lenght != 0) - { - string temp = entry_path.string(); - - temp.erase(0, ffmpeg_entry_path_lenght); - - entry_path = fs::path(temp); - } - - const fs::path full_dest_path = destination_dir / entry_path; - - /* - The first entry is the root folder - because we want all in ffmpeg-vm/... and not in ffmpeg-vm/something/..., we check this path to remove - */ - if (ffmpeg_entry_path_lenght == 0 && entry_path.string().rfind("ffmpeg") == 0) // Check if starts with ffmpeg - { - ffmpeg_entry_path_lenght = entry_path.string().length(); - continue; - } - - // Make sure that the folder exist - if (full_dest_path.has_parent_path()) - { - fs::create_directories(full_dest_path.parent_path()); - } - - // If is only a directory, create the dir - if (archive_entry_filetype(entry) == AE_IFDIR) - { - fs::create_directories(full_dest_path); - - continue; - } - -#ifndef _WIN32 - // If is only a symlink, create the symlink - if (archive_entry_filetype(entry) == AE_IFLNK) - { - const char* link_target_cstr = archive_entry_symlink(entry); - - if (link_target_cstr) { - // fs::create_symlink can fail if the link exist - std::error_code ec; - fs::remove(full_dest_path, ec); // Remove to prevent errors - fs::create_symlink(fs::path(link_target_cstr), full_dest_path); - } else { - cerr << "Warning: could not read symlink target for " << full_dest_path << endl; - } - - continue; - } -#endif - - // if it's a file, write it to disk - ofstream outfile(full_dest_path, ios::binary); - - if (!outfile) - { - cerr << "Error: Could not open file for writing: " << full_dest_path << endl; - archive_read_close(archiv); - archive_read_free(archiv); - return 2; - } - - const void *buff; - size_t size; - la_int64_t offset; - - // Read the blocks and write it - while ((result = archive_read_data_block(archiv, &buff, &size, &offset)) == ARCHIVE_OK) - { - outfile.write(static_cast(buff), size); - } - - if (result != ARCHIVE_EOF) - { - cerr << "Error reading data block: " << archive_error_string(archiv) << endl; - archive_read_close(archiv); - archive_read_free(archiv); - return 3; - } - -#ifndef _WIN32 - // Restore permissions - if (archive_entry_filetype(entry) != AE_IFLNK) { - try { - fs::permissions(full_dest_path, static_cast(archive_entry_perm(entry)), fs::perm_options::replace); - } catch (const exception& e) { - cerr << "Warning: could not set permissions for " << full_dest_path << ". " << e.what() << endl; - } - } -#endif + *data->display_slider = ftxui::text(generate_slider(progress > 0.95 ? 1.0f : progress)); + data->screen->PostEvent(ftxui::Event::Custom); } +} - // Check if error on readling last header - result = archive_read_close(archiv); +int display_extract(const string &filedata, const fs::path &destination_dir, ftxui::Element* display_slider, ftxui::ScreenInteractive* screen) +{ + last_progress = 0; - if (result != ARCHIVE_OK) - { - cerr << "Error closing archive: " << archive_error_string(archiv) << endl; - archive_read_free(archiv); - return 4; - } + PROGRESSDATA data; - archive_read_free(archiv); + data.display_slider = display_slider; + data.screen = screen; - return 0; + return extract_from_memory_to_folder(filedata, destination_dir, &data, ExtractCallback); } diff --git a/src/tui.cc b/src/tui.cc index cd7f9c1..4f83881 100644 --- a/src/tui.cc +++ b/src/tui.cc @@ -107,13 +107,13 @@ int run_tui() setup_env(version.version); - const std::string fdata = download_file(version.url, &display_slider, &download_screen); + const std::string fdata = display_download_file(version.url, &display_slider, &download_screen); display_text = text(center_text("Extracting files...")); display_slider = text(generate_slider(0.0f)); download_screen.PostEvent(ftxui::Event::Custom); - extract(fdata, downloaddir, &display_slider, &download_screen); + display_extract(fdata, downloaddir, &display_slider, &download_screen); display_text = text(center_text("Done!")); display_slider = text(generate_slider(1.0f)); @@ -157,7 +157,7 @@ int run_tui() container, [&] { - return window(text("ffmpeg-version-manager v0.1.6"), + return window(text("ffmpeg-version-manager v0.1.7"), hbox({ menus_component->Render() | borderEmpty | size(WIDTH, EQUAL, 15), separator(), @@ -168,4 +168,4 @@ int run_tui() screen.Loop(renderer); return 0; -} \ No newline at end of file +} diff --git a/src/ui_elements.cc b/src/ui_elements.cc index 4a21820..4fc364e 100644 --- a/src/ui_elements.cc +++ b/src/ui_elements.cc @@ -80,4 +80,4 @@ std::string center_text(std::string str) } return cent; -} \ No newline at end of file +}