--- /dev/null
+*.layout
+*~
+*.[oa]
+bin/*
+obj/*
+*.1
+*.gz
+Makefile
+CMakeCache.txt
+CMakeFiles/
+cmake_install.cmake
+build/
+*.cbp
--- /dev/null
+cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR)
+project (lgogdownloader LANGUAGES C CXX VERSION 3.7)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
+set(LINK_LIBCRYPTO 0)
+
+# Disable search for boost-cmake
+# Fixes building with Boost >= 1.70.0
+set(Boost_NO_BOOST_CMAKE ON)
+
+option(USE_QT_GUI "Build with Qt GUI login support" OFF)
+if(USE_QT_GUI)
+ add_definitions(-DUSE_QT_GUI_LOGIN=1)
+ set(CMAKE_AUTOMOC ON)
+ set(CMAKE_AUTOUIC ON)
+endif(USE_QT_GUI)
+
+find_program(READELF readelf DOC "Location of the readelf program")
+find_program(GREP grep DOC "Location of the grep program")
+find_package(Boost
+ REQUIRED
+ system
+ filesystem
+ regex
+ program_options
+ date_time
+ iostreams
+ )
+find_package(CURL 7.32.0 REQUIRED)
+if(CURL_FOUND)
+ execute_process(
+ COMMAND ${READELF} -d ${CURL_LIBRARIES}
+ COMMAND ${GREP} -q "libssl\\|libcrypto"
+ RESULT_VARIABLE READELF_RESULT_VAR
+ )
+ if(READELF_RESULT_VAR EQUAL 0)
+ add_definitions(-DSSL_THREAD_SETUP_OPENSSL=1)
+ find_package(Libcrypto REQUIRED)
+ set(LINK_LIBCRYPTO 1)
+ endif(READELF_RESULT_VAR EQUAL 0)
+endif(CURL_FOUND)
+
+find_package(Jsoncpp REQUIRED)
+find_package(Htmlcxx REQUIRED)
+find_package(Tinyxml2 REQUIRED)
+find_package(Rhash REQUIRED)
+find_package(Threads REQUIRED)
+find_package(ZLIB REQUIRED)
+
+file(GLOB SRC_FILES
+ main.cpp
+ src/website.cpp
+ src/downloader.cpp
+ src/progressbar.cpp
+ src/util.cpp
+ src/blacklist.cpp
+ src/gamefile.cpp
+ src/gamedetails.cpp
+ src/galaxyapi.cpp
+ src/ziputil.cpp
+ )
+
+if(USE_QT_GUI)
+ find_package(Qt5Widgets CONFIG REQUIRED)
+ find_package(Qt5WebEngineWidgets CONFIG REQUIRED)
+
+ file(GLOB QT_GUI_SRC_FILES
+ src/gui_login.cpp
+ )
+ list(APPEND SRC_FILES ${QT_GUI_SRC_FILES})
+endif(USE_QT_GUI)
+
+
+set(GIT_CHECKOUT FALSE)
+if(EXISTS ${PROJECT_SOURCE_DIR}/.git)
+ if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+ find_package(Git)
+ if(GIT_FOUND)
+ set(GIT_CHECKOUT TRUE)
+ else(GIT_FOUND)
+ message(WARNING "Git executable not found")
+ endif(GIT_FOUND)
+ else(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+ message(STATUS "Shallow Git clone detected, not attempting to retrieve version info")
+ endif(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow)
+endif(EXISTS ${PROJECT_SOURCE_DIR}/.git)
+
+if(GIT_CHECKOUT)
+ execute_process(COMMAND ${GIT_EXECUTABLE} diff --shortstat
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ OUTPUT_VARIABLE GIT_SHORTSTAT
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+ execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ OUTPUT_VARIABLE GIT_REV_PARSE
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+ if(GIT_SHORTSTAT)
+ set(GIT_DIRTY ON)
+ endif(GIT_SHORTSTAT)
+
+ if(GIT_DIRTY)
+ set(PROJECT_VERSION_MINOR ${PROJECT_VERSION_MINOR}M)
+ endif(GIT_DIRTY)
+
+ set(PROJECT_VERSION_PATCH ${GIT_REV_PARSE})
+ set(PROJECT_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH})
+endif(GIT_CHECKOUT)
+
+set(VERSION_NUMBER ${PROJECT_VERSION})
+set(VERSION_STRING "LGOGDownloader ${VERSION_NUMBER}")
+set(DEFAULT_USER_AGENT "LGOGDownloader/${VERSION_NUMBER} (${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_PROCESSOR})")
+
+add_definitions(-D_FILE_OFFSET_BITS=64 -DVERSION_NUMBER="${VERSION_NUMBER}" -DVERSION_STRING="${VERSION_STRING}" -DDEFAULT_USER_AGENT="${DEFAULT_USER_AGENT}")
+
+add_executable (${PROJECT_NAME} ${SRC_FILES})
+
+
+target_include_directories(${PROJECT_NAME}
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include
+ PRIVATE ${Boost_INCLUDE_DIRS}
+ PRIVATE ${CURL_INCLUDE_DIRS}
+ PRIVATE ${OAuth_INCLUDE_DIRS}
+ PRIVATE ${Jsoncpp_INCLUDE_DIRS}
+ PRIVATE ${Htmlcxx_INCLUDE_DIRS}
+ PRIVATE ${Tinyxml2_INCLUDE_DIRS}
+ PRIVATE ${Rhash_INCLUDE_DIRS}
+ PRIVATE ${ZLIB_INCLUDE_DIRS}
+ )
+
+target_link_libraries(${PROJECT_NAME}
+ PRIVATE ${Boost_LIBRARIES}
+ PRIVATE ${CURL_LIBRARIES}
+ PRIVATE ${OAuth_LIBRARIES}
+ PRIVATE ${Jsoncpp_LIBRARIES}
+ PRIVATE ${Htmlcxx_LIBRARIES}
+ PRIVATE ${Tinyxml2_LIBRARIES}
+ PRIVATE ${Rhash_LIBRARIES}
+ PRIVATE ${CMAKE_THREAD_LIBS_INIT}
+ PRIVATE ${ZLIB_LIBRARIES}
+ )
+
+# Check if libatomic is needed in order to use std::atomic, and add
+# it to the list of JavaScriptCore libraries.
+file(WRITE ${CMAKE_BINARY_DIR}/test_atomic.cpp
+ "#include <atomic>\n"
+ "int main() { std::atomic<int64_t> i(0); i++; return 0; }\n")
+try_compile(ATOMIC_BUILD_SUCCEEDED ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/test_atomic.cpp)
+if (NOT ATOMIC_BUILD_SUCCEEDED)
+ target_link_libraries(${PROJECT_NAME}
+ PRIVATE -latomic
+ )
+endif ()
+file(REMOVE ${CMAKE_BINARY_DIR}/test_atomic.cpp)
+
+if(LINK_LIBCRYPTO EQUAL 1)
+ target_link_libraries(${PROJECT_NAME}
+ PRIVATE ${Libcrypto_LIBRARIES}
+ )
+endif(LINK_LIBCRYPTO EQUAL 1)
+
+if(USE_QT_GUI)
+ target_link_libraries(${PROJECT_NAME}
+ PRIVATE Qt5::Widgets
+ PRIVATE Qt5::WebEngineWidgets
+ )
+endif(USE_QT_GUI)
+
+if(MSVC)
+ # Force to always compile with W4
+ if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
+ string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ else()
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
+ endif()
+elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ # Update if necessary
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra -Wno-long-long -fexceptions")
+endif()
+
+set(INSTALL_BIN_DIR bin CACHE PATH "Installation directory for executables")
+set(INSTALL_SHARE_DIR share CACHE PATH "Installation directory for resource files")
+
+install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX} DESTINATION ${INSTALL_BIN_DIR})
+add_subdirectory(man)
--- /dev/null
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
+
--- /dev/null
+# LGOGDownloader
+
+This repository contains the code of LGOGDownloader which is unoffcial open source downloader for [GOG.com](https://www.gog.com/).
+It uses the same API as GOG Galaxy which doesn't have Linux support at the moment.
+
+## Dependencies
+
+* [libcurl](https://curl.haxx.se/libcurl/) >= 7.32.0
+* [librhash](https://github.com/rhash/RHash)
+* [jsoncpp](https://github.com/open-source-parsers/jsoncpp)
+* [htmlcxx](http://htmlcxx.sourceforge.net/)
+* [tinyxml2](https://github.com/leethomason/tinyxml2)
+* [boost](http://www.boost.org/) (regex, date-time, system, filesystem, program-options, iostreams)
+* [libcrypto](https://www.openssl.org/) if libcurl is built with OpenSSL
+* [zlib](https://www.zlib.net/)
+* [qtwebengine](https://www.qt.io/) if built with -DUSE_QT_GUI=ON
+
+## Make dependencies
+* [cmake](https://cmake.org/) >= 3.0.0
+* [help2man](https://www.gnu.org/software/help2man/help2man.html) (optional, man page generation)
+* [grep](https://www.gnu.org/software/grep/)
+* [binutils](https://www.gnu.org/software/binutils/) (readelf)
+
+### Debian/Ubuntu
+
+ # apt install build-essential libcurl4-openssl-dev libboost-regex-dev \
+ libjsoncpp-dev librhash-dev libtinyxml2-dev libhtmlcxx-dev \
+ libboost-system-dev libboost-filesystem-dev libboost-program-options-dev \
+ libboost-date-time-dev libboost-iostreams-dev help2man cmake libssl-dev \
+ pkg-config zlib1g-dev qtwebengine5-dev
+
+## Build and install
+
+ $ mkdir build
+ $ cd build
+ $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DUSE_QT_GUI=ON
+ $ make
+ # sudo make install
+
+## Usage examples
+
+- **Login**
+
+ lgogdownloader --login
+
+- **Listing games and details for specific games**
+
+ lgogdownloader --list
+ lgogdownloader --list-details --game witcher
+
+
+- **Downloading files**
+
+ lgogdownloader --download
+ lgogdownloader --download --game stardew_valley --exclude extras
+ lgogdownloader --download --threads 6 --platform linux --language en+de,fr
+ lgogdownloader --download-file tyrian_2000/9543
+
+- **Repairing files**
+
+ lgogdownloader --repair --game beneath_a_steel_sky
+ lgogdownloader --repair --download --game "^a"
+
+- **Using Galaxy API for listing and installing game builds**
+
+ lgogdownloader --galaxy-platform windows --galaxy-show-builds stardew_valley
+ lgogdownloader --galaxy-platform windows --galaxy-install stardew_valley/0
+ lgogdownloader --galaxy-platform windows --galaxy-install beneath_a_steel_sky/0 --galaxy-no-dependencies
+
+- **See man page or help text for more**
+
+ lgogdownloader --help
+ man lgogdownloader
+
+## Links
+- [LGOGDownloader website](https://sites.google.com/site/gogdownloader/)
+- [GOG forum thread](https://www.gog.com/forum/general/lgogdownloader_gogdownloader_for_linux)
+- [LGOGDownloader @ AUR](https://aur.archlinux.org/packages/lgogdownloader/)
+- [LGOGDownloader @ AUR (git version)](https://aur.archlinux.org/packages/lgogdownloader-git/)
+- [LGOGDownloader @ Debian](https://tracker.debian.org/lgogdownloader)
+- [LGOGDownloader @ Ubuntu](https://launchpad.net/ubuntu/+source/lgogdownloader)
+
+[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PT95NXVLQU6WG&source=url)
--- /dev/null
+# - Try to find htmlcxx
+#
+# Once done this will define
+# Htmlcxx_FOUND - System has htmlcxx
+# Htmlcxx_INCLUDE_DIRS - The htmlcxx include directories
+# Htmlcxx_LIBRARIES - The libraries needed to use htmlcxx
+
+find_package(PkgConfig)
+pkg_check_modules(PC_HTMLCXX REQUIRED htmlcxx)
+
+find_path(HTMLCXX_INCLUDE_DIR
+ NAMES
+ css/parser.h
+ html/tree.h
+ HINTS
+ ${PC_HTMLCXX_INCLUDEDIR}
+ ${PC_HTMLCXX_INCLUDE_DIRS}
+ PATH_SUFFIXES
+ htmlcxx
+ PATHS
+ ${PC_HTMLCXX_INCLUDE_DIRS}
+ )
+
+find_library(HTMLCXX_LIBRARY_HTMLCXX htmlcxx
+ HINTS
+ ${PC_HTMLCXX_LIBDIR}
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ PATHS
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ )
+
+find_library(HTMLCXX_LIBRARY_CSS_PARSER css_parser
+ HINTS
+ ${PC_HTMLCXX_LIBDIR}
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ PATHS
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ )
+
+find_library(HTMLCXX_LIBRARY_CSS_PARSER_PP css_parser_pp
+ HINTS
+ ${PC_HTMLCXX_LIBDIR}
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ PATHS
+ ${PC_HTMLCXX_LIBRARY_DIRS}
+ )
+
+mark_as_advanced(HTMLCXX_INCLUDE_DIR HTMLCXX_LIBRARY_HTMLCXX HTMLCXX_LIBRARY_CSS_PARSER HTMLCXX_LIBRARY_CSS_PARSER_PP)
+
+if(PC_HTMLCXX_FOUND)
+ set(Htmlcxx_FOUND ON)
+ set(Htmlcxx_INCLUDE_DIRS ${HTMLCXX_INCLUDE_DIR})
+ set(Htmlcxx_LIBRARIES ${HTMLCXX_LIBRARY_HTMLCXX} ${HTMLCXX_LIBRARY_CSS_PARSER} ${HTMLCXX_LIBRARY_CSS_PARSER_PP})
+endif(PC_HTMLCXX_FOUND)
--- /dev/null
+# - Try to find Jsoncpp
+#
+# Once done, this will define
+# Jsoncpp_FOUND - system has Jsoncpp
+# Jsoncpp_INCLUDE_DIRS - the Jsoncpp include directories
+# Jsoncpp_LIBRARIES - link these to use Jsoncpp
+
+find_package(PkgConfig)
+pkg_check_modules(PC_JSONCPP REQUIRED jsoncpp)
+
+find_path(JSONCPP_INCLUDE_DIR
+ NAMES
+ json/allocator.h
+ HINTS
+ ${PC_JSONCPP_INCLUDEDIR}
+ ${PC_JSONCPP_INCLUDEDIRS}
+ PATH_SUFFIXES
+ jsoncpp
+ PATHS
+ ${PC_JSONCPP_INCLUDE_DIRS}
+ )
+
+find_library(JSONCPP_LIBRARY jsoncpp
+ PATHS
+ ${PC_JSONCPP_LIBRARY_DIRS}
+ )
+
+mark_as_advanced(JSONCPP_INCLUDE_DIR JSONCPP_LIBRARY)
+
+if(PC_JSONCPP_FOUND)
+ set(Jsoncpp_FOUND ON)
+ set(Jsoncpp_INCLUDE_DIRS ${JSONCPP_INCLUDE_DIR})
+ set(Jsoncpp_LIBRARIES ${JSONCPP_LIBRARY})
+endif(PC_JSONCPP_FOUND)
--- /dev/null
+# - Try to find libcrypto
+#
+# Once done this will define
+# Libcrypto_FOUND - System has libcrypto
+# Libcrypto_INCLUDE_DIRS - The libcrypto include directories
+# Libcrypto_LIBRARIES - The libraries needed to use libcrypto
+
+find_package(PkgConfig)
+pkg_check_modules(PC_LIBCRYPTO REQUIRED libcrypto)
+
+find_path(LIBCRYPTO_INCLUDE_DIR openssl/crypto.h
+ HINTS ${PC_LIBCRYPTO_INCLUDEDIR}
+ ${PC_LIBCRYPTO_INCLUDE_DIRS}
+ )
+
+find_library(LIBCRYPTO_LIBRARY NAMES crypto
+ HINTS ${PC_LIBCRYPTO_LIBDIR}
+ ${PC_LIBCRYPTO_LIBRARY_DIRS}
+ )
+
+mark_as_advanced(LIBCRYPTO_INCLUDE_DIR LIBCRYPTO_LIBRARY)
+
+if(PC_LIBCRYPTO_FOUND)
+ set(Libcrypto_FOUND ON)
+ set(Libcrypto_INCLUDE_DIRS ${LIBCRYPTO_INCLUDE_DIR})
+ set(Libcrypto_LIBRARIES ${LIBCRYPTO_LIBRARY})
+endif(PC_LIBCRYPTO_FOUND)
--- /dev/null
+# - Try to find rhash
+#
+# Once done this will define
+# Rhash_FOUND - System has rhash
+# Rhash_INCLUDE_DIRS - The rhash include directories
+# Rhash_LIBRARIES - The libraries needed to use rhash
+
+find_path(RHASH_INCLUDE_DIR rhash.h)
+find_library(RHASH_LIBRARY rhash)
+
+mark_as_advanced(RHASH_INCLUDE_DIR RHASH_LIBRARY)
+
+if(RHASH_LIBRARY AND RHASH_INCLUDE_DIR)
+ set(Rhash_FOUND ON)
+ set(Rhash_LIBRARIES ${RHASH_LIBRARY})
+ set(Rhash_INCLUDE_DIRS ${RHASH_INCLUDE_DIR})
+else()
+ set(Rhash_FOUND OFF)
+ if(Rhash_FIND_REQUIRED)
+ message(FATAL_ERROR "Could not find rhash")
+ endif(Rhash_FIND_REQUIRED)
+endif(RHASH_LIBRARY AND RHASH_INCLUDE_DIR)
--- /dev/null
+# - Try to find tinyxml2
+#
+# Once done this will define
+# Tinyxml2_FOUND - System has tinyxml2
+# Tinyxml2_INCLUDE_DIRS - The tinyxml2 include directories
+# Tinyxml2_LIBRARIES - The libraries needed to use tinyxml
+
+find_package(PkgConfig)
+pkg_check_modules(PC_TINYXML2 tinyxml2)
+
+find_path(TINYXML2_INCLUDE_DIR tinyxml2.h
+ HINTS
+ ${PC_TINYXML2_INCLUDEDIR}
+ ${PC_TINYXML2_INCLUDE_DIRS}
+ PATHS
+ ${PC_TINYXML2_INCLUDE_DIRS}
+ )
+
+find_library(TINYXML2_LIBRARY tinyxml2
+ HINTS
+ ${PC_TINYXML2_LIBDIR}
+ ${PC_TINYXML2_LIBRARY_DIRS}
+ PATHS
+ ${PC_TINYXML2_LIBRARY_DIRS}
+ )
+
+mark_as_advanced(TINYXML2_INCLUDE_DIR TINYXML2_LIBRARY)
+
+if(TINYXML2_INCLUDE_DIR)
+ set(Tinyxml2_FOUND ON)
+ set(Tinyxml2_INCLUDE_DIRS ${TINYXML2_INCLUDE_DIR})
+ set(Tinyxml2_LIBRARIES ${TINYXML2_LIBRARY})
+endif(TINYXML2_INCLUDE_DIR)
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef BLACKLIST_H__
+#define BLACKLIST_H__
+
+#include <boost/regex.hpp>
+#include <string>
+#include <vector>
+
+class Config;
+class gameFile;
+
+class BlacklistItem {
+ public:
+ unsigned int linenr; // where the blacklist item is defined in blacklist.txt
+ unsigned int flags;
+ std::string source; // source representation of the item
+ boost::regex regex;
+};
+
+class Blacklist
+{
+ public:
+ Blacklist() {};
+
+ void initialize(const std::vector<std::string>& lines);
+ bool isBlacklisted(const std::string& path);
+ bool isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory = "");
+
+ std::vector<BlacklistItem>::size_type size() const { return blacklist_.size(); }
+ bool empty() { return blacklist_.empty(); }
+ private:
+ std::vector<BlacklistItem> blacklist_;
+};
+
+#endif // BLACKLIST_H_
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef CONFIG_H__
+#define CONFIG_H__
+
+#include <iostream>
+#include <curl/curl.h>
+#include <json/json.h>
+#include <mutex>
+#include <ctime>
+
+#include "blacklist.h"
+
+struct DirectoryConfig
+{
+ bool bSubDirectories;
+ std::string sDirectory;
+ std::string sGameSubdir;
+ std::string sInstallersSubdir;
+ std::string sExtrasSubdir;
+ std::string sPatchesSubdir;
+ std::string sLanguagePackSubdir;
+ std::string sDLCSubdir;
+ std::string sGalaxyInstallSubdir;
+};
+
+struct DownloadConfig
+{
+ unsigned int iInstallerPlatform;
+ unsigned int iInstallerLanguage;
+ unsigned int iGalaxyCDN;
+ std::vector<unsigned int> vPlatformPriority;
+ std::vector<unsigned int> vLanguagePriority;
+ std::vector<unsigned int> vGalaxyCDNPriority;
+ unsigned int iInclude;
+ unsigned int iGalaxyPlatform;
+ unsigned int iGalaxyLanguage;
+ unsigned int iGalaxyArch;
+
+ bool bRemoteXML;
+ bool bSaveChangelogs;
+ bool bSaveSerials;
+ bool bAutomaticXMLCreation;
+
+ bool bInstallers;
+ bool bExtras;
+ bool bPatches;
+ bool bLanguagePacks;
+ bool bDLC;
+
+ bool bIgnoreDLCCount;
+ bool bDuplicateHandler;
+ bool bGalaxyDependencies;
+ bool bGalaxyDeleteOrphans;
+};
+
+struct gameSpecificConfig
+{
+ DownloadConfig dlConf;
+ DirectoryConfig dirConf;
+};
+
+class GalaxyConfig
+{
+ public:
+ bool isExpired()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ bool bExpired = true; // assume that token is expired
+ intmax_t time_now = time(NULL);
+ if (this->token_json.isMember("expires_at"))
+ bExpired = (time_now > this->token_json["expires_at"].asLargestInt());
+ return bExpired;
+ }
+
+ std::string getAccessToken()
+ {
+ std:: string access_token;
+ std::unique_lock<std::mutex> lock(m);
+ if (this->token_json.isMember("access_token"))
+ access_token = this->token_json["access_token"].asString();
+ return access_token;
+ }
+
+ std::string getRefreshToken()
+ {
+ std::string refresh_token;
+ std::unique_lock<std::mutex> lock(m);
+ if (this->token_json.isMember("refresh_token"))
+ refresh_token = this->token_json["refresh_token"].asString();
+ return refresh_token;
+ }
+
+ Json::Value getJSON()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return this->token_json;
+ }
+
+ void setJSON(Json::Value json)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ if (!json.isMember("expires_at"))
+ {
+ intmax_t time_now = time(NULL);
+ Json::Value::LargestInt expires_in = 3600;
+ if (json.isMember("expires_in"))
+ if (!json["expires_in"].isNull())
+ expires_in = json["expires_in"].asLargestInt();
+
+ Json::Value::LargestInt expires_at = time_now + expires_in;
+ json["expires_at"] = expires_at;
+ }
+ this->token_json = json;
+ }
+
+ void setFilepath(const std::string& path)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ this->filepath = path;
+ }
+
+ std::string getFilepath()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return this->filepath;
+ }
+
+ std::string getClientId()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return this->client_id;
+ }
+
+ std::string getClientSecret()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return this->client_secret;
+ }
+
+ std::string getRedirectUri()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return this->redirect_uri;
+ }
+
+ GalaxyConfig() = default;
+
+ GalaxyConfig(const GalaxyConfig& other)
+ {
+ std::lock_guard<std::mutex> guard(other.m);
+ client_id = other.client_id;
+ client_secret = other.client_secret;
+ redirect_uri = other.redirect_uri;
+ filepath = other.filepath;
+ token_json = other.token_json;
+ }
+
+ GalaxyConfig& operator= (GalaxyConfig& other)
+ {
+ if(&other == this)
+ return *this;
+
+ std::unique_lock<std::mutex> lock1(m, std::defer_lock);
+ std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
+ std::lock(lock1, lock2);
+ client_id = other.client_id;
+ client_secret = other.client_secret;
+ redirect_uri = other.redirect_uri;
+ filepath = other.filepath;
+ token_json = other.token_json;
+ return *this;
+ }
+ protected:
+ private:
+ std::string client_id = "46899977096215655";
+ std::string client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9";
+ std::string redirect_uri = "https://embed.gog.com/on_login_success?origin=client";
+ std::string filepath;
+ Json::Value token_json;
+ mutable std::mutex m;
+};
+
+struct CurlConfig
+{
+ bool bVerifyPeer;
+ bool bVerbose;
+ std::string sCACertPath;
+ std::string sCookiePath;
+ std::string sUserAgent;
+ long int iTimeout;
+ curl_off_t iDownloadRate;
+ long int iLowSpeedTimeout;
+ long int iLowSpeedTimeoutRate;
+};
+
+class Config
+{
+ public:
+ Config() {};
+ virtual ~Config() {};
+
+ // Booleans
+ bool bLogin;
+ bool bSaveConfig;
+ bool bResetConfig;
+
+ bool bDownload;
+ bool bRepair;
+ bool bUpdated;
+ bool bList;
+ bool bListDetails;
+ bool bCheckStatus;
+ bool bShowWishlist;
+ bool bNotifications;
+ bool bIncludeHiddenProducts;
+
+ bool bVerbose;
+ bool bUnicode; // use Unicode in console output
+ bool bColor; // use colors
+ bool bReport;
+ bool bRespectUmask;
+ bool bPlatformDetection;
+#ifdef USE_QT_GUI_LOGIN
+ bool bEnableLoginGUI;
+#endif
+
+ // Cache
+ bool bUseCache;
+ bool bUpdateCache;
+ int iCacheValid;
+
+ // Download with file id options
+ std::string sFileIdString;
+ std::string sOutputFilename;
+
+ // Curl
+ CurlConfig curlConf;
+
+ // Download
+ DownloadConfig dlConf;
+
+ // Directories
+ DirectoryConfig dirConf;
+ std::string sCacheDirectory;
+ std::string sXMLDirectory;
+ std::string sConfigDirectory;
+
+ // File paths
+ std::string sConfigFilePath;
+ std::string sBlacklistFilePath;
+ std::string sIgnorelistFilePath;
+ std::string sGameHasDLCListFilePath;
+ std::string sReportFilePath;
+
+ std::string sXMLFile;
+
+ // Regex
+ std::string sGameRegex;
+ std::string sOrphanRegex;
+ std::string sIgnoreDLCCountRegex;
+
+ // Priorities
+ std::string sPlatformPriority;
+ std::string sLanguagePriority;
+
+ // General strings
+ std::string sVersionString;
+ std::string sVersionNumber;
+ std::string sEmail;
+ std::string sPassword;
+
+ // Lists
+ Blacklist blacklist;
+ Blacklist ignorelist;
+ Blacklist gamehasdlc;
+ std::string sGameHasDLCList;
+
+ // Integers
+ int iRetries;
+ unsigned int iThreads;
+ unsigned int iInfoThreads;
+ int iWait;
+ size_t iChunkSize;
+ int iProgressInterval;
+};
+
+#endif // CONFIG_H__
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef DOWNLOADER_H
+#define DOWNLOADER_H
+
+#if __GNUC__
+# if !(__x86_64__ || __ppc64__ || __LP64__)
+# ifndef _LARGEFILE_SOURCE
+# define _LARGEFILE_SOURCE
+# endif
+# ifndef _LARGEFILE64_SOURCE
+# define _LARGEFILE64_SOURCE
+# endif
+# if !defined(_FILE_OFFSET_BITS) || (_FILE_OFFSET_BITS == 32)
+# define _FILE_OFFSET_BITS 64
+# endif
+# endif
+#endif
+
+#include "config.h"
+#include "progressbar.h"
+#include "website.h"
+#include "threadsafequeue.h"
+#include "galaxyapi.h"
+#include "globals.h"
+
+#include <curl/curl.h>
+#include <json/json.h>
+#include <ctime>
+#include <fstream>
+#include <deque>
+
+class Timer
+{
+ public:
+ Timer() { this->reset(); };
+ void reset() { gettimeofday(&(this->last_update), NULL); };
+ double getTimeBetweenUpdates()
+ { // Returns time elapsed between updates in milliseconds
+ struct timeval time_now;
+ gettimeofday(&time_now, NULL);
+ double time_between = ( (time_now.tv_sec+(time_now.tv_usec/1000000.0))*1000.0 - (this->last_update.tv_sec+(this->last_update.tv_usec/1000000.0))*1000.0 );
+ return time_between;
+ };
+ ~Timer() {};
+ private:
+ struct timeval last_update;
+};
+
+struct xferInfo
+{
+ unsigned int tid;
+ CURL* curlhandle;
+ Timer timer;
+ std::deque< std::pair<time_t, uintmax_t> > TimeAndSize;
+ curl_off_t offset;
+};
+
+struct ChunkMemoryStruct
+{
+ char *memory;
+ curl_off_t size;
+};
+
+typedef struct
+{
+ std::string filepath;
+ off_t comp_size;
+ off_t uncomp_size;
+ off_t start_offset_zip;
+ off_t start_offset_mojosetup;
+ off_t end_offset;
+ uint16_t file_attributes;
+ uint32_t crc32;
+ time_t timestamp;
+
+ std::string installer_url;
+
+ // For split file handling
+ bool isSplitFile = false;
+ std::string splitFileBasePath;
+ std::string splitFilePartExt;
+ off_t splitFileStartOffset;
+ off_t splitFileEndOffset;
+} zipFileEntry;
+
+typedef std::map<std::string,std::vector<zipFileEntry>> splitFilesMap;
+
+class Downloader
+{
+ public:
+ Downloader();
+ virtual ~Downloader();
+ bool isLoggedIn();
+ int init();
+ int login();
+ int listGames();
+ void checkNotifications();
+ void clearUpdateNotifications();
+ void repair();
+ void download();
+ void checkOrphans();
+ void checkStatus();
+ void updateCache();
+ int downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath);
+ void showWishlist();
+ CURL* curlhandle;
+ Timer timer;
+ ProgressBar* progressbar;
+ std::deque< std::pair<time_t, uintmax_t> > TimeAndSize;
+ void saveGalaxyJSON();
+
+ void galaxyInstallGame(const std::string& product_id, int build_index = -1, const unsigned int& iGalaxyArch = GlobalConstants::ARCH_X64);
+ void galaxyInstallGameById(const std::string& product_id, int build_index = -1, const unsigned int& iGalaxyArch = GlobalConstants::ARCH_X64);
+ void galaxyShowBuilds(const std::string& product_id, int build_index = -1);
+ void galaxyShowBuildsById(const std::string& product_id, int build_index = -1);
+ protected:
+ private:
+ CURLcode downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
+ int repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data = std::string(), const std::string& gamename = std::string());
+ int getGameDetails();
+ void getGameList();
+ uintmax_t getResumePosition();
+ CURLcode beginDownload();
+ std::string getResponse(const std::string& url);
+ std::string getLocalFileHash(const std::string& filepath, const std::string& gamename = std::string());
+ std::string getRemoteFileHash(const gameFile& gf);
+ int loadGameDetailsCache();
+ int saveGameDetailsCache();
+ std::vector<gameDetails> getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level = 0);
+ static std::string getSerialsFromJSON(const Json::Value& json);
+ void saveSerials(const std::string& serials, const std::string& filepath);
+ static std::string getChangelogFromJSON(const Json::Value& json);
+ void saveChangelog(const std::string& changelog, const std::string& filepath);
+ static void processDownloadQueue(Config conf, const unsigned int& tid);
+ static int progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+ template <typename T> void printProgress(const ThreadSafeQueue<T>& download_queue);
+ static void getGameDetailsThread(Config config, const unsigned int& tid);
+
+ static int progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+ static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+ static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream);
+ static size_t readData(void *ptr, size_t size, size_t nmemb, FILE *stream);
+
+ std::vector<std::string> galaxyGetOrphanedFiles(const std::vector<galaxyDepotItem>& items, const std::string& install_path);
+ static void processGalaxyDownloadQueue(const std::string& install_path, Config conf, const unsigned int& tid);
+ void galaxyInstallGame_MojoSetupHack(const std::string& product_id);
+ void galaxyInstallGame_MojoSetupHack_CombineSplitFiles(const splitFilesMap& mSplitFiles, const bool& bAppendtoFirst = false);
+ static void processGalaxyDownloadQueue_MojoSetupHack(Config conf, const unsigned int& tid);
+ int mojoSetupGetFileVector(const gameFile& gf, std::vector<zipFileEntry>& vFiles);
+ std::string getGalaxyInstallDirectory(galaxyAPI *galaxyHandle, const Json::Value& manifest);
+ bool galaxySelectProductIdHelper(const std::string& product_id, std::string& selected_product);
+ std::vector<galaxyDepotItem> galaxyGetDepotItemVectorFromJson(const Json::Value& json, const unsigned int& iGalaxyArch = GlobalConstants::ARCH_X64);
+
+ Website *gogWebsite;
+ galaxyAPI *gogGalaxy;
+ std::vector<gameItem> gameItems;
+ std::vector<gameDetails> games;
+
+ off_t resume_position;
+ int retries;
+ std::ofstream report_ofs;
+};
+
+#endif // DOWNLOADER_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef DOWNLOADINFO_H
+#define DOWNLOADINFO_H
+
+#include <curl/curl.h>
+#include <mutex>
+
+const unsigned int DLSTATUS_NOTSTARTED = 0;
+const unsigned int DLSTATUS_STARTING = 1 << 0;
+const unsigned int DLSTATUS_RUNNING = 1 << 1;
+const unsigned int DLSTATUS_FINISHED = 1 << 2;
+
+struct progressInfo
+{
+ curl_off_t dlnow;
+ curl_off_t dltotal;
+ double rate;
+ double rate_avg;
+};
+
+class DownloadInfo
+{
+ public:
+ void setFilename(const std::string& filename_)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ filename = filename_;
+ }
+
+ std::string getFilename()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return filename;
+ }
+
+ void setStatus(const unsigned int& status_)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ status = status_;
+ }
+
+ unsigned int getStatus()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return status;
+ }
+
+ void setProgressInfo(const progressInfo& info)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ progress_info = info;
+ }
+
+ progressInfo getProgressInfo()
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return progress_info;
+ }
+
+ DownloadInfo()=default;
+
+ DownloadInfo(const DownloadInfo& other)
+ {
+ std::lock_guard<std::mutex> guard(other.m);
+ filename = other.filename;
+ status = other.status;
+ progress_info = other.progress_info;
+ }
+
+ DownloadInfo& operator= (DownloadInfo& other)
+ {
+ if(&other == this)
+ return *this;
+
+ std::unique_lock<std::mutex> lock1(m, std::defer_lock);
+ std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
+ std::lock(lock1, lock2);
+ filename = other.filename;
+ status = other.status;
+ progress_info = other.progress_info;
+ return *this;
+ }
+ private:
+ std::string filename;
+ unsigned int status;
+ progressInfo progress_info;
+ mutable std::mutex m;
+};
+
+#endif // DOWNLOADINFO_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GALAXYAPI_H
+#define GALAXYAPI_H
+
+#include "globalconstants.h"
+#include "globals.h"
+#include "config.h"
+#include "util.h"
+#include "gamedetails.h"
+
+#include <iostream>
+#include <vector>
+#include <cstring>
+#include <curl/curl.h>
+#include <sys/time.h>
+
+struct galaxyDepotItemChunk
+{
+ std::string md5_compressed;
+ std::string md5_uncompressed;
+ uintmax_t size_compressed;
+ uintmax_t size_uncompressed;
+ uintmax_t offset_compressed;
+ uintmax_t offset_uncompressed;
+};
+
+struct galaxyDepotItem
+{
+ std::string path;
+ std::vector<galaxyDepotItemChunk> chunks;
+ uintmax_t totalSizeCompressed;
+ uintmax_t totalSizeUncompressed;
+ std::string md5;
+ std::string product_id;
+ bool isDependency = false;
+};
+
+class galaxyAPI
+{
+ public:
+ galaxyAPI(CurlConfig& conf);
+ virtual ~galaxyAPI();
+ int init();
+ bool isTokenExpired();
+ bool refreshLogin();
+ Json::Value getProductBuilds(const std::string& product_id, const std::string& platform = "windows", const std::string& generation = "2");
+ Json::Value getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id = "repository", const std::string& platform = "windows");
+ Json::Value getManifestV1(const std::string& manifest_url);
+ Json::Value getManifestV2(std::string manifest_hash, const bool& is_dependency = false);
+ Json::Value getSecureLink(const std::string& product_id, const std::string& path);
+ Json::Value getDependencyLink(const std::string& path);
+ std::string getResponse(const std::string& url, const bool& zlib_decompress = false);
+ Json::Value getResponseJson(const std::string& url, const bool& zlib_decompress = false);
+ std::string hashToGalaxyPath(const std::string& hash);
+ std::vector<galaxyDepotItem> getDepotItemsVector(const std::string& hash, const bool& is_dependency = false);
+ Json::Value getProductInfo(const std::string& product_id);
+ gameDetails productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf);
+ Json::Value getUserData();
+ Json::Value getDependenciesJson();
+ std::vector<galaxyDepotItem> getFilteredDepotItemsVectorFromJson(const Json::Value& depot_json, const std::string& galaxy_language, const std::string& galaxy_arch, const bool& is_dependency = false);
+ std::string getPathFromDownlinkUrl(const std::string& downlink_url, const std::string& gamename);
+ protected:
+ private:
+ CurlConfig curlConf;
+ static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+ CURL* curlhandle;
+ std::vector<gameFile> installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+ std::vector<gameFile> patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+ std::vector<gameFile> languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf);
+ std::vector<gameFile> extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json);
+ std::vector<gameFile> fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type = GFTYPE_INSTALLER, const unsigned int& platform = (GlobalConstants::PLATFORM_WINDOWS | GlobalConstants::PLATFORM_LINUX), const unsigned int& lang = GlobalConstants::LANGUAGE_EN, const bool& useDuplicateHandler = false);
+};
+
+#endif // GALAXYAPI_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GAMEDETAILS_H
+#define GAMEDETAILS_H
+
+#include "globalconstants.h"
+#include "globals.h"
+#include "gamefile.h"
+#include "config.h"
+#include "util.h"
+
+#include <iostream>
+#include <vector>
+#include <json/json.h>
+
+class gameDetails
+{
+ public:
+ gameDetails();
+ std::vector<gameFile> extras;
+ std::vector<gameFile> installers;
+ std::vector<gameFile> patches;
+ std::vector<gameFile> languagepacks;
+ std::vector<gameDetails> dlcs;
+ std::string gamename;
+ std::string product_id;
+ std::string title;
+ std::string icon;
+ std::string serials;
+ std::string changelog;
+ void filterWithPriorities(const gameSpecificConfig& config);
+ void makeFilepaths(const DirectoryConfig& config);
+ std::string getSerialsFilepath();
+ std::string getChangelogFilepath();
+ Json::Value getDetailsAsJson();
+ std::vector<gameFile> getGameFileVector();
+ std::vector<gameFile> getGameFileVectorFiltered(const unsigned int& iType);
+ virtual ~gameDetails();
+ protected:
+ void filterListWithPriorities(std::vector<gameFile>& list, const gameSpecificConfig& config);
+ private:
+ std::string serialsFilepath;
+ std::string changelogFilepath;
+};
+
+#endif // GAMEDETAILS_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GAMEFILE_H
+#define GAMEFILE_H
+
+#include "globalconstants.h"
+#include "globals.h"
+
+#include <iostream>
+#include <vector>
+#include <json/json.h>
+
+// Game file types
+const unsigned int GFTYPE_INSTALLER = 1 << 0;
+const unsigned int GFTYPE_EXTRA = 1 << 1;
+const unsigned int GFTYPE_PATCH = 1 << 2;
+const unsigned int GFTYPE_LANGPACK = 1 << 3;
+const unsigned int GFTYPE_DLC = 1 << 4;
+
+class gameFile
+{
+ public:
+ gameFile();
+ int updated;
+ std::string gamename;
+ std::string id;
+ std::string name;
+ std::string path;
+ std::string size;
+ std::string galaxy_downlink_json_url;
+ unsigned int platform;
+ unsigned int language;
+ unsigned int type;
+ int score;
+ int silent;
+ void setFilepath(const std::string& path);
+ std::string getFilepath();
+ Json::Value getAsJson();
+ virtual ~gameFile();
+ protected:
+ private:
+ std::string filepath;
+};
+
+#endif // GAMEFILE_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GLOBALCONSTANTS_H_INCLUDED
+#define GLOBALCONSTANTS_H_INCLUDED
+
+#include <iostream>
+#include <vector>
+
+namespace GlobalConstants
+{
+ const int GAMEDETAILS_CACHE_VERSION = 3;
+ const int ZLIB_WINDOW_SIZE = 15;
+
+ struct optionsStruct {const unsigned int id; const std::string code; const std::string str; const std::string regexp;};
+ const std::string PROTOCOL_PREFIX = "gogdownloader://";
+
+ // Language constants
+ const unsigned int LANGUAGE_EN = 1 << 0;
+ const unsigned int LANGUAGE_DE = 1 << 1;
+ const unsigned int LANGUAGE_FR = 1 << 2;
+ const unsigned int LANGUAGE_PL = 1 << 3;
+ const unsigned int LANGUAGE_RU = 1 << 4;
+ const unsigned int LANGUAGE_CN = 1 << 5;
+ const unsigned int LANGUAGE_CZ = 1 << 6;
+ const unsigned int LANGUAGE_ES = 1 << 7;
+ const unsigned int LANGUAGE_HU = 1 << 8;
+ const unsigned int LANGUAGE_IT = 1 << 9;
+ const unsigned int LANGUAGE_JP = 1 << 10;
+ const unsigned int LANGUAGE_TR = 1 << 11;
+ const unsigned int LANGUAGE_PT = 1 << 12;
+ const unsigned int LANGUAGE_KO = 1 << 13;
+ const unsigned int LANGUAGE_NL = 1 << 14;
+ const unsigned int LANGUAGE_SV = 1 << 15;
+ const unsigned int LANGUAGE_NO = 1 << 16;
+ const unsigned int LANGUAGE_DA = 1 << 17;
+ const unsigned int LANGUAGE_FI = 1 << 18;
+ const unsigned int LANGUAGE_PT_BR = 1 << 19;
+ const unsigned int LANGUAGE_SK = 1 << 20;
+ const unsigned int LANGUAGE_BL = 1 << 21;
+ const unsigned int LANGUAGE_UK = 1 << 22;
+ const unsigned int LANGUAGE_ES_419 = 1 << 23;
+ const unsigned int LANGUAGE_AR = 1 << 24;
+ const unsigned int LANGUAGE_RO = 1 << 25;
+ const unsigned int LANGUAGE_HE = 1 << 26;
+
+ const std::vector<optionsStruct> LANGUAGES =
+ {
+ { LANGUAGE_EN, "en", "English", "en|eng|english|en[_-]US" },
+ { LANGUAGE_DE, "de", "German", "de|deu|ger|german|de[_-]DE" },
+ { LANGUAGE_FR, "fr", "French", "fr|fra|fre|french|fr[_-]FR" },
+ { LANGUAGE_PL, "pl", "Polish", "pl|pol|polish|pl[_-]PL" },
+ { LANGUAGE_RU, "ru", "Russian", "ru|rus|russian|ru[_-]RU" },
+ { LANGUAGE_CN, "cn", "Chinese", "cn|zh|zho|chi|chinese|zh[_-]CN" },
+ { LANGUAGE_CZ, "cz", "Czech", "cz|cs|ces|cze|czech|cs[_-]CZ" },
+ { LANGUAGE_ES, "es", "Spanish", "es|spa|spanish|es[_-]ES" },
+ { LANGUAGE_HU, "hu", "Hungarian", "hu|hun|hungarian|hu[_-]HU" },
+ { LANGUAGE_IT, "it", "Italian", "it|ita|italian|it[_-]IT" },
+ { LANGUAGE_JP, "jp", "Japanese", "jp|ja|jpn|japanese|ja[_-]JP" },
+ { LANGUAGE_TR, "tr", "Turkish", "tr|tur|turkish|tr[_-]TR" },
+ { LANGUAGE_PT, "pt", "Portuguese", "pt|por|portuguese|pt[_-]PT" },
+ { LANGUAGE_KO, "ko", "Korean", "ko|kor|korean|ko[_-]KR" },
+ { LANGUAGE_NL, "nl", "Dutch", "nl|nld|dut|dutch|nl[_-]NL" },
+ { LANGUAGE_SV, "sv", "Swedish", "sv|swe|swedish|sv[_-]SE" },
+ { LANGUAGE_NO, "no", "Norwegian", "no|nor|norwegian|nb[_-]no|nn[_-]NO" },
+ { LANGUAGE_DA, "da", "Danish", "da|dan|danish|da[_-]DK" },
+ { LANGUAGE_FI, "fi", "Finnish", "fi|fin|finnish|fi[_-]FI" },
+ { LANGUAGE_PT_BR, "br", "Brazilian Portuguese", "br|pt_br|pt-br|ptbr|brazilian_portuguese" },
+ { LANGUAGE_SK, "sk", "Slovak", "sk|slk|slo|slovak|sk[_-]SK" },
+ { LANGUAGE_BL, "bl", "Bulgarian", "bl|bg|bul|bulgarian|bg[_-]BG" },
+ { LANGUAGE_UK, "uk", "Ukrainian", "uk|ukr|ukrainian|uk[_-]UA" },
+ { LANGUAGE_ES_419, "es_mx", "Spanish (Latin American)", "es_mx|es-mx|esmx|es-419|spanish_latin_american" },
+ { LANGUAGE_AR, "ar", "Arabic", "ar|ara|arabic|ar[_-][A-Z]{2}" },
+ { LANGUAGE_RO, "ro", "Romanian", "ro|ron|rum|romanian|ro[_-][RM]O" },
+ { LANGUAGE_HE, "he", "Hebrew", "he|heb|hebrew|he[_-]IL" }
+ };
+
+ // Platform constants
+ const unsigned int PLATFORM_WINDOWS = 1 << 0;
+ const unsigned int PLATFORM_MAC = 1 << 1;
+ const unsigned int PLATFORM_LINUX = 1 << 2;
+
+ const std::vector<optionsStruct> PLATFORMS =
+ {
+ { PLATFORM_WINDOWS, "win", "Windows" , "w|win|windows" },
+ { PLATFORM_MAC, "mac", "Mac" , "m|mac|osx" },
+ { PLATFORM_LINUX, "linux", "Linux" , "l|lin|linux" }
+ };
+
+ // Galaxy platform arch
+ const unsigned int ARCH_X86 = 1 << 0;
+ const unsigned int ARCH_X64 = 1 << 1;
+
+ const std::vector<optionsStruct> GALAXY_ARCHS =
+ {
+ { ARCH_X86, "32", "32-bit", "32|x86|32bit|32-bit" },
+ { ARCH_X64, "64", "64-bit", "64|x64|64bit|64-bit" }
+ };
+
+ // Galaxy CDNs
+ const unsigned int CDN_EDGECAST = 1 << 0;
+ const unsigned int CDN_HIGHWINDS = 1 << 1;
+ const unsigned int CDN_GOG = 1 << 2;
+
+ const std::vector<optionsStruct> GALAXY_CDNS =
+ {
+ { CDN_EDGECAST, "edgecast", "Edgecast", "ec|edgecast" },
+ { CDN_HIGHWINDS, "high_winds", "Highwinds", "hw|highwinds|high_winds" },
+ { CDN_GOG, "gog_cdn", "GOG", "gog|gog_cdn" }
+ };
+}
+
+#endif // GLOBALCONSTANTS_H_INCLUDED
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GLOBALS_H_INCLUDED
+#define GLOBALS_H_INCLUDED
+
+#include "config.h"
+#include <iostream>
+#include <vector>
+
+namespace Globals
+{
+ extern GalaxyConfig galaxyConf;
+ extern Config globalConfig;
+}
+
+#endif // GLOBALS_H_INCLUDED
+
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef GUI_LOGIN_H
+#define GUI_LOGIN_H
+
+#include "config.h"
+#include "util.h"
+#include "globals.h"
+
+#include <QObject>
+#include <QWebEngineCookieStore>
+#include <iostream>
+#include <vector>
+
+class GuiLogin : public QObject
+{
+ Q_OBJECT
+
+ public:
+ GuiLogin();
+ virtual ~GuiLogin();
+
+ void Login();
+ void Login(const std::string& username, const std::string& password);
+ std::string getCode();
+ std::vector<std::string> getCookies();
+
+ private:
+ QWebEngineCookieStore *cookiestore;
+ std::vector<std::string> cookies;
+ std::string auth_code;
+ std::string login_username;
+ std::string login_password;
+
+ public slots:
+ void loadFinished(bool success);
+ void cookieAdded(const QNetworkCookie &cookie);
+};
+
+#endif // GUI_LOGIN_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef MESSAGE_H
+#define MESSAGE_H
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+const unsigned int MSGTYPE_INFO = 1 << 0;
+const unsigned int MSGTYPE_WARNING = 1 << 1;
+const unsigned int MSGTYPE_ERROR = 1 << 2;
+const unsigned int MSGTYPE_SUCCESS = 1 << 3;
+
+class Message
+{
+ public:
+ Message() = default;
+ Message(std::string msg, const unsigned int& type = MSGTYPE_INFO, const std::string& prefix = std::string())
+ {
+ prefix_ = prefix;
+ msg_ = msg;
+ type_ = type;
+ timestamp_ = boost::posix_time::second_clock::local_time();
+ }
+
+ void setMessage(const std::string& msg)
+ {
+ msg_ = msg;
+ }
+
+ void setType(const unsigned int& type)
+ {
+ type_ = type;
+ }
+
+ void setTimestamp(const boost::posix_time::ptime& timestamp)
+ {
+ timestamp_ = timestamp;
+ }
+
+ void setPrefix(const std::string& prefix)
+ {
+ prefix_ = prefix;
+ }
+
+ std::string getMessage()
+ {
+ return msg_;
+ }
+
+ unsigned int getType()
+ {
+ return type_;
+ }
+
+ boost::posix_time::ptime getTimestamp()
+ {
+ return timestamp_;
+ }
+
+ std::string getTimestampString()
+ {
+ return boost::posix_time::to_simple_string(timestamp_);
+ }
+
+ std::string getPrefix()
+ {
+ return prefix_;
+ }
+
+ std::string getFormattedString(const bool& bColor = true, const bool& bPrefix = true)
+ {
+ std::string str;
+ std::string color_value = "\033[39m"; // Default foreground color
+ std::string color_reset = "\033[0m";
+
+ if (type_ == MSGTYPE_INFO)
+ color_value = "\033[39m"; // Default foreground color
+ else if (type_ == MSGTYPE_WARNING)
+ color_value = "\033[33m"; // Yellow
+ else if (type_ == MSGTYPE_ERROR)
+ color_value = "\033[31m"; // Red
+ else if (type_ == MSGTYPE_SUCCESS)
+ color_value = "\033[32m"; // Green
+
+ str = msg_;
+ if (!prefix_.empty() && bPrefix)
+ str = prefix_ + " " + str;
+
+ str = getTimestampString() + " " + str;
+
+ if (bColor)
+ str = color_value + str + color_reset;
+
+ return str;
+ }
+
+ private:
+ std::string msg_;
+ boost::posix_time::ptime timestamp_;
+ unsigned int type_;
+ std::string prefix_;
+};
+
+#endif // MESSAGE_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef PROGRESSBAR_H
+#define PROGRESSBAR_H
+
+#include <iostream>
+#include <vector>
+
+class ProgressBar
+{
+ public:
+ ProgressBar(bool bUnicode, bool bColor);
+ virtual ~ProgressBar();
+ void draw(unsigned int length, double fraction);
+ std::string createBarString(unsigned int length, double fraction);
+ protected:
+ private:
+ std::vector<std::string> const m_bar_chars;
+ std::string const m_left_border;
+ std::string const m_right_border;
+ std::string const m_simple_left_border;
+ std::string const m_simple_right_border;
+ std::string const m_simple_empty_fill;
+ std::string const m_simple_bar_char;
+ std::string const m_bar_color;
+ std::string const m_border_color;
+ std::string const COLOR_RESET;
+ bool m_use_unicode;
+ bool m_use_color;
+};
+
+#endif // PROGRESSBAR_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef SSL_THREAD_SETUP_H
+#define SSL_THREAD_SETUP_H
+
+#include <thread>
+#include <mutex>
+
+#if SSL_THREAD_SETUP_OPENSSL == 1
+ #include <openssl/crypto.h>
+
+ static std::mutex* ssl_mutex_array;
+
+ void thread_locking_callback(int mode, int n, const char* file, int line)
+ {
+ // suppress -Wunused-parameter messages by casting these variables to void
+ (void) file;
+ (void) line;
+
+ if(mode & CRYPTO_LOCK)
+ ssl_mutex_array[n].lock();
+ else
+ ssl_mutex_array[n].unlock();
+ }
+
+ unsigned long thread_id_callback()
+ {
+ return (unsigned long)std::hash<std::thread::id>() (std::this_thread::get_id());
+ }
+
+ int ssl_thread_setup()
+ {
+ ssl_mutex_array = new std::mutex[CRYPTO_num_locks()];
+ if(!ssl_mutex_array)
+ return 0;
+ else
+ {
+ CRYPTO_set_id_callback(thread_id_callback);
+ CRYPTO_set_locking_callback(thread_locking_callback);
+ }
+ return 1;
+ }
+
+ int ssl_thread_cleanup()
+ {
+ if(!ssl_mutex_array)
+ return 0;
+
+ CRYPTO_set_id_callback(NULL);
+ CRYPTO_set_locking_callback(NULL);
+ delete[] ssl_mutex_array;
+ ssl_mutex_array = NULL;
+ return 1;
+ }
+#else
+ #define ssl_thread_setup()
+ #define ssl_thread_cleanup()
+#endif
+
+#endif // SSL_THREAD_SETUP_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef THREADSAFEQUEUE_H
+#define THREADSAFEQUEUE_H
+
+#include <queue>
+#include <mutex>
+#include <condition_variable>
+
+template<typename T>
+class ThreadSafeQueue
+{
+ public:
+ void push(const T& item)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ q.push(item);
+ lock.unlock();
+ cvar.notify_one();
+ }
+
+ bool empty() const
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return q.empty();
+ }
+
+ typename std::queue<T>::size_type size() const
+ {
+ std::unique_lock<std::mutex> lock(m);
+ return q.size();
+ }
+
+ bool try_pop(T& item)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ if(q.empty())
+ return false;
+
+ item = q.front();
+ q.pop();
+ return true;
+ }
+
+ void wait_and_pop(T& item)
+ {
+ std::unique_lock<std::mutex> lock(m);
+ while(q.empty())
+ cvar.wait(lock);
+
+ item = q.front();
+ q.pop();
+ }
+
+ ThreadSafeQueue() = default;
+
+ ThreadSafeQueue(const ThreadSafeQueue& other)
+ {
+ std::lock_guard<std::mutex> guard(other.m);
+ q = other.q;
+ }
+
+ ThreadSafeQueue& operator= (ThreadSafeQueue& other)
+ {
+ if(&other == this)
+ return *this;
+
+ std::unique_lock<std::mutex> lock1(m, std::defer_lock);
+ std::unique_lock<std::mutex> lock2(other.m, std::defer_lock);
+ std::lock(lock1, lock2);
+ q = other.q;
+ return *this;
+ }
+ private:
+ std::queue<T> q;
+ mutable std::mutex m;
+ std::condition_variable cvar;
+};
+
+#endif // THREADSAFEQUEUE_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include "globalconstants.h"
+#include "config.h"
+#include "globals.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cerrno>
+#include <iostream>
+#include <sstream>
+#include <memory>
+#include <rhash.h>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+#include <json/json.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+struct gameItem
+{
+ std::string name;
+ std::string id;
+ std::vector<std::string> dlcnames;
+ Json::Value gamedetailsjson;
+ int updates = 0;
+};
+
+struct wishlistItem
+{
+ std::string title;
+ unsigned int platform;
+ std::vector<std::string> tags;
+ time_t release_date_time;
+ std::string currency;
+ std::string price;
+ std::string discount_percent;
+ std::string discount;
+ std::string store_credit;
+ std::string url;
+ bool bIsBonusStoreCreditIncluded;
+ bool bIsDiscounted;
+};
+
+namespace Util
+{
+ std::string makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory = "", const unsigned int& platformId = 0, const std::string& dlcname = "");
+ std::string makeRelativeFilepath(const std::string& path, const std::string& gamename, std::string subdirectory = "");
+ std::string getFileHash(const std::string& filename, unsigned hash_id);
+ std::string getFileHashRange(const std::string& filepath, unsigned hash_id, off_t range_start = 0, off_t range_end = 0);
+ std::string getChunkHash(unsigned char* chunk, uintmax_t chunk_size, unsigned hash_id);
+ int createXML(std::string filepath, uintmax_t chunk_size, std::string xml_dir = std::string());
+ int getGameSpecificConfig(std::string gamename, gameSpecificConfig* conf, std::string directory = std::string());
+ int replaceString(std::string& str, const std::string& to_replace, const std::string& replace_with);
+ void filepathReplaceReservedStrings(std::string& str, const std::string& gamename, const unsigned int& platformId = 0, const std::string& dlcname = "");
+ void setFilePermissions(const boost::filesystem::path& path, const boost::filesystem::perms& permissions);
+ int getTerminalWidth();
+ void getDownloaderUrlsFromJSON(const Json::Value &root, std::vector<std::string> &urls);
+ std::vector<std::string> getDLCNamesFromJSON(const Json::Value &root);
+ std::string getHomeDir();
+ std::string getConfigHome();
+ std::string getCacheHome();
+ std::vector<std::string> tokenize(const std::string& str, const std::string& separator = ",");
+ unsigned int getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options, const bool& bAllowStringToIntConversion = true);
+ std::string getOptionNameString(const unsigned int& value, const std::vector<GlobalConstants::optionsStruct>& options);
+ void parseOptionString(const std::string &option_string, std::vector<unsigned int> &priority, unsigned int &type, const std::vector<GlobalConstants::optionsStruct>& options);
+ std::string getLocalFileHash(const std::string& xml_dir, const std::string& filepath, const std::string& gamename = std::string());
+ void shortenStringToTerminalWidth(std::string& str);
+ std::string getJsonUIntValueAsString(const Json::Value& json);
+ std::string getStrippedString(std::string str);
+ std::string makeEtaString(const unsigned long long& iBytesRemaining, const double& dlRate);
+ std::string makeEtaString(const boost::posix_time::time_duration& duration);
+
+ template<typename ... Args> std::string formattedString(const std::string& format, Args ... args)
+ {
+ std::size_t sz = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // +1 for null terminator
+ std::unique_ptr<char[]> buf(new char[sz]);
+ std::snprintf(buf.get(), sz, format.c_str(), args ...);
+ return std::string(buf.get(), buf.get() + sz - 1); // -1 because we don't want the null terminator
+ }
+}
+
+#endif // UTIL_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef WEBSITE_H
+#define WEBSITE_H
+
+#include "config.h"
+#include "util.h"
+#include "globals.h"
+#include <curl/curl.h>
+#include <json/json.h>
+#include <fstream>
+
+class Website
+{
+ public:
+ Website();
+ int Login(const std::string& email, const std::string& password);
+ std::string getResponse(const std::string& url);
+ Json::Value getGameDetailsJSON(const std::string& gameid);
+ std::vector<gameItem> getGames();
+ std::vector<wishlistItem> getWishlistItems();
+ bool IsLoggedIn();
+ virtual ~Website();
+ protected:
+ private:
+ static size_t writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp);
+ CURL* curlhandle;
+ bool IsloggedInSimple();
+ bool IsLoggedInComplex(const std::string& email);
+ int retries;
+};
+
+#endif // WEBSITE_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#ifndef ZIPUTIL_H
+#define ZIPUTIL_H
+
+#include <cstdint>
+#include <ctime>
+#include <string>
+#include <iostream>
+#include <boost/filesystem.hpp>
+
+#define ZIP_LOCAL_HEADER_SIGNATURE 0x04034b50
+#define ZIP_CD_HEADER_SIGNATURE 0x02014b50
+#define ZIP_EOCD_HEADER_SIGNATURE 0x06054b50
+#define ZIP_EOCD_HEADER_SIGNATURE64 0x06064b50
+#define ZIP_EXTENSION_ZIP64 0x0001
+#define ZIP_EXTENDED_TIMESTAMP 0x5455
+#define ZIP_INFOZIP_UNIX_NEW 0x7875
+
+typedef struct
+{
+ uint32_t header = 0;
+ uint16_t disk = 0;
+ uint16_t cd_start_disk = 0;
+ uint16_t cd_records = 0;
+ uint16_t total_cd_records = 0;
+ uint32_t cd_size = 0;
+ uint32_t cd_start_offset = 0;
+ uint16_t comment_length = 0;
+
+ std::string comment;
+} zipEOCD;
+
+typedef struct
+{
+ uint32_t header = 0;
+ uint64_t directory_record_size = 0;
+ uint16_t version_made_by = 0;
+ uint16_t version_needed = 0;
+ uint32_t cd = 0;
+ uint32_t cd_start = 0;
+ uint64_t cd_total_disk = 0;
+ uint64_t cd_total = 0;
+ uint64_t cd_size = 0;
+ uint64_t cd_offset = 0;
+
+ std::string comment;
+} zip64EOCD;
+
+typedef struct
+{
+ uint32_t header = 0;
+ uint16_t version_made_by = 0;
+ uint16_t version_needed = 0;
+ uint16_t flag = 0;
+ uint16_t compression_method = 0;
+ uint16_t mod_date = 0;
+ uint16_t mod_time = 0;
+ uint32_t crc32 = 0;
+ uint64_t comp_size = 0;
+ uint64_t uncomp_size = 0;
+ uint16_t filename_length = 0;
+ uint16_t extra_length = 0;
+ uint16_t comment_length = 0;
+ uint32_t disk_num = 0;
+ uint16_t internal_file_attr = 0;
+ uint32_t external_file_attr = 0;
+ uint64_t disk_offset = 0;
+
+ std::string filename;
+ std::string extra;
+ std::string comment;
+ time_t timestamp = 0;
+ bool isLocalCDEntry = false;
+} zipCDEntry;
+
+namespace ZipUtil
+{
+ off_t getMojoSetupScriptSize(std::stringstream *stream);
+ off_t getMojoSetupInstallerSize(std::stringstream *stream);
+
+ struct tm date_time_to_tm(uint64_t date, uint64_t time);
+ bool isValidDate(struct tm timeinfo);
+
+ uint64_t readValue(std::istream *stream, uint32_t len);
+ uint64_t readUInt64(std::istream *stream);
+ uint32_t readUInt32(std::istream *stream);
+ uint16_t readUInt16(std::istream *stream);
+ uint8_t readUInt8(std::istream *stream);
+
+ off_t getZipEOCDOffsetSignature(std::istream *stream, const uint32_t& signature);
+ off_t getZipEOCDOffset(std::istream *stream);
+ off_t getZip64EOCDOffset(std::istream *stream);
+
+ zipEOCD readZipEOCDStruct(std::istream *stream, const off_t& eocd_start_pos = 0);
+ zip64EOCD readZip64EOCDStruct(std::istream *stream, const off_t& eocd_start_pos = 0);
+ zipCDEntry readZipCDEntry(std::istream *stream);
+
+ int extractFile(const std::string& input_file_path, const std::string& output_file_path);
+ int extractStream(std::istream* input_stream, std::ostream* output_stream);
+ boost::filesystem::perms getBoostFilePermission(const uint16_t& attributes);
+ bool isSymlink(const uint16_t& attributes);
+}
+
+#endif // ZIPUTIL_H
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "downloader.h"
+#include "config.h"
+#include "util.h"
+#include "globalconstants.h"
+#include "ssl_thread_setup.h"
+#include "galaxyapi.h"
+#include "globals.h"
+
+#include <fstream>
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+namespace bpo = boost::program_options;
+Config Globals::globalConfig;
+
+template<typename T> void set_vm_value(std::map<std::string, bpo::variable_value>& vm, const std::string& option, const T& value)
+{
+ vm[option].value() = boost::any(value);
+}
+
+int main(int argc, char *argv[])
+{
+ rhash_library_init();
+
+ // Constants for option selection with include/exclude
+ /* TODO: Add options to give better control for user
+ For example: option to select base game and DLC installers separately,
+ this requires some changes to Downloader class to implement */
+ const unsigned int OPTION_INSTALLERS = 1 << 0;
+ const unsigned int OPTION_EXTRAS = 1 << 1;
+ const unsigned int OPTION_PATCHES = 1 << 2;
+ const unsigned int OPTION_LANGPACKS = 1 << 3;
+ const unsigned int OPTION_DLCS = 1 << 4;
+
+ const std::vector<GlobalConstants::optionsStruct> INCLUDE_OPTIONS =
+ {
+ { OPTION_INSTALLERS, "i", "Installers", "i|installers" },
+ { OPTION_EXTRAS, "e", "Extras", "e|extras" },
+ { OPTION_PATCHES, "p", "Patches", "p|patches" },
+ { OPTION_LANGPACKS, "l", "Language packs", "l|languagepacks|langpacks" },
+ { OPTION_DLCS, "d", "DLCs", "d|dlc|dlcs" }
+ };
+
+ Globals::globalConfig.sVersionString = VERSION_STRING;
+ Globals::globalConfig.sVersionNumber = VERSION_NUMBER;
+ Globals::globalConfig.curlConf.sUserAgent = DEFAULT_USER_AGENT;
+
+ Globals::globalConfig.sCacheDirectory = Util::getCacheHome() + "/lgogdownloader";
+ Globals::globalConfig.sXMLDirectory = Globals::globalConfig.sCacheDirectory + "/xml";
+
+ Globals::globalConfig.sConfigDirectory = Util::getConfigHome() + "/lgogdownloader";
+ Globals::globalConfig.curlConf.sCookiePath = Globals::globalConfig.sConfigDirectory + "/cookies.txt";
+ Globals::globalConfig.sConfigFilePath = Globals::globalConfig.sConfigDirectory + "/config.cfg";
+ Globals::globalConfig.sBlacklistFilePath = Globals::globalConfig.sConfigDirectory + "/blacklist.txt";
+ Globals::globalConfig.sIgnorelistFilePath = Globals::globalConfig.sConfigDirectory + "/ignorelist.txt";
+ Globals::globalConfig.sGameHasDLCListFilePath = Globals::globalConfig.sConfigDirectory + "/game_has_dlc.txt";
+
+ Globals::galaxyConf.setFilepath(Globals::globalConfig.sConfigDirectory + "/galaxy_tokens.json");
+
+ std::string priority_help_text = "Set priority by separating values with \",\"\nCombine values by separating with \"+\"";
+ // Create help text for --platform option
+ std::string platform_text = "Select which installers are downloaded\n";
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ {
+ platform_text += GlobalConstants::PLATFORMS[i].str + " = " + GlobalConstants::PLATFORMS[i].regexp + "\n";
+ }
+ platform_text += "All = all";
+ platform_text += "\n\n" + priority_help_text;
+ platform_text += "\nExample: Linux if available otherwise Windows and Mac: l,w+m";
+
+ // Create help text for --galaxy-platform option
+ std::string galaxy_platform_text = "Select platform\n";
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ {
+ galaxy_platform_text += GlobalConstants::PLATFORMS[i].str + " = " + GlobalConstants::PLATFORMS[i].regexp + "\n";
+ }
+
+ // Create help text for --language option
+ std::string language_text = "Select which language installers are downloaded\n";
+ for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+ {
+ language_text += GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "\n";
+ }
+ language_text += "All = all";
+ language_text += "\n\n" + priority_help_text;
+ language_text += "\nExample: German if available otherwise English and French: de,en+fr";
+
+ // Create help text for --galaxy-language option
+ std::string galaxy_language_text = "Select language\n";
+ for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+ {
+ galaxy_language_text += GlobalConstants::LANGUAGES[i].str + " = " + GlobalConstants::LANGUAGES[i].regexp + "\n";
+ }
+
+ // Create help text for --galaxy-arch option
+ std::string galaxy_arch_text = "Select architecture\n";
+ for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i)
+ {
+ galaxy_arch_text += GlobalConstants::GALAXY_ARCHS[i].str + " = " + GlobalConstants::GALAXY_ARCHS[i].regexp + "\n";
+ }
+
+ // Create help text for --subdir-galaxy-install option
+ std::string galaxy_install_subdir_text = "Set subdirectory for galaxy install\n"
+ "\nTemplates:\n"
+ "- %install_dir% = Installation directory from Galaxy API response\n"
+ "- %gamename% = Game name\n"
+ "- %title% = Title of the game\n"
+ "- %product_id% = Product id of the game\n"
+ "- %install_dir_stripped% = %install_dir% with some characters stripped\n"
+ "- %title_stripped% = %title% with some characters stripped\n"
+ "\n\"stripped\" means that every character that doesn't match the following list is removed:\n"
+ "> alphanumeric\n"
+ "> space\n"
+ "> - _ . ( ) [ ] { }";
+
+ // Create help text for --galaxy-cdn-priority option
+ std::string galaxy_cdn_priority_text = "Set priority for used CDNs\n";
+ for (unsigned int i = 0; i < GlobalConstants::GALAXY_CDNS.size(); ++i)
+ {
+ galaxy_cdn_priority_text += GlobalConstants::GALAXY_CDNS[i].str + " = " + GlobalConstants::GALAXY_CDNS[i].regexp + "\n";
+ }
+ galaxy_cdn_priority_text += "\n" + priority_help_text;
+
+ // Create help text for --check-orphans
+ std::string orphans_regex_default = ".*\\.(zip|exe|bin|dmg|old|deb|tar\\.gz|pkg|sh)$"; // Limit to files with these extensions (".old" is for renamed older version files)
+ std::string check_orphans_text = "Check for orphaned files (files found on local filesystem that are not found on GOG servers). Sets regular expression filter (Perl syntax) for files to check. If no argument is given then the regex defaults to '" + orphans_regex_default + "'";
+
+ // Help text for subdir options
+ std::string subdir_help_text = "\nTemplates:\n- %platform%\n- %gamename%\n- %dlcname%";
+
+ // Help text for include and exclude options
+ std::string include_options_text;
+ for (unsigned int i = 0; i < INCLUDE_OPTIONS.size(); ++i)
+ {
+ include_options_text += INCLUDE_OPTIONS[i].str + " = " + INCLUDE_OPTIONS[i].regexp + "\n";
+ }
+ include_options_text += "Separate with \",\" to use multiple values";
+
+ std::string galaxy_product_id_install;
+ std::string galaxy_product_id_show_builds;
+
+ std::vector<std::string> vFileIdStrings;
+ std::vector<std::string> unrecognized_options_cfg;
+ std::vector<std::string> unrecognized_options_cli;
+ bpo::variables_map vm;
+ bpo::options_description options_cli_all("Options");
+ bpo::options_description options_cli_no_cfg;
+ bpo::options_description options_cli_no_cfg_hidden;
+ bpo::options_description options_cli_all_include_hidden;
+ bpo::options_description options_cli_experimental("Experimental");
+ bpo::options_description options_cli_cfg;
+ bpo::options_description options_cfg_only;
+ bpo::options_description options_cfg_all("Configuration");
+ bool bClearUpdateNotifications = false;
+ try
+ {
+ bool bInsecure = false;
+ bool bNoColor = false;
+ bool bNoUnicode = false;
+ bool bNoDuplicateHandler = false;
+ bool bNoRemoteXML = false;
+ bool bNoSubDirectories = false;
+ bool bNoPlatformDetection = false;
+ bool bNoGalaxyDependencies = false;
+ std::string sInstallerPlatform;
+ std::string sInstallerLanguage;
+ std::string sIncludeOptions;
+ std::string sExcludeOptions;
+ std::string sGalaxyPlatform;
+ std::string sGalaxyLanguage;
+ std::string sGalaxyArch;
+ std::string sGalaxyCDN;
+ Globals::globalConfig.bReport = false;
+ // Commandline options (no config file)
+ options_cli_no_cfg.add_options()
+ ("help,h", "Print help message")
+ ("version", "Print version information")
+ ("login", bpo::value<bool>(&Globals::globalConfig.bLogin)->zero_tokens()->default_value(false), "Login")
+ ("list", bpo::value<bool>(&Globals::globalConfig.bList)->zero_tokens()->default_value(false), "List games")
+ ("list-details", bpo::value<bool>(&Globals::globalConfig.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info")
+ ("download", bpo::value<bool>(&Globals::globalConfig.bDownload)->zero_tokens()->default_value(false), "Download")
+ ("repair", bpo::value<bool>(&Globals::globalConfig.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)")
+ ("game", bpo::value<std::string>(&Globals::globalConfig.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)")
+ ("create-xml", bpo::value<std::string>(&Globals::globalConfig.sXMLFile)->implicit_value("automatic"), "Create GOG XML for file\n\"automatic\" to enable automatic XML creation")
+ ("notifications", bpo::value<bool>(&Globals::globalConfig.bNotifications)->zero_tokens()->default_value(false), "Check notifications")
+ ("updated", bpo::value<bool>(&Globals::globalConfig.bUpdated)->zero_tokens()->default_value(false), "List/download only games with update flag set")
+ ("clear-update-flags", bpo::value<bool>(&bClearUpdateNotifications)->zero_tokens()->default_value(false), "Clear update notification flags")
+ ("check-orphans", bpo::value<std::string>(&Globals::globalConfig.sOrphanRegex)->implicit_value(""), check_orphans_text.c_str())
+ ("status", bpo::value<bool>(&Globals::globalConfig.bCheckStatus)->zero_tokens()->default_value(false), "Show status of files\n\nOutput format:\nstatuscode gamename filename filesize filehash\n\nStatus codes:\nOK - File is OK\nND - File is not downloaded\nMD5 - MD5 mismatch, different version\nFS - File size mismatch, incomplete download")
+ ("save-config", bpo::value<bool>(&Globals::globalConfig.bSaveConfig)->zero_tokens()->default_value(false), "Create config file with current settings")
+ ("reset-config", bpo::value<bool>(&Globals::globalConfig.bResetConfig)->zero_tokens()->default_value(false), "Reset config settings to default")
+ ("report", bpo::value<std::string>(&Globals::globalConfig.sReportFilePath)->implicit_value("lgogdownloader-report.log"), "Save report of downloaded/repaired files to specified file\nDefault filename: lgogdownloader-report.log")
+ ("update-cache", bpo::value<bool>(&Globals::globalConfig.bUpdateCache)->zero_tokens()->default_value(false), "Update game details cache")
+ ("no-platform-detection", bpo::value<bool>(&bNoPlatformDetection)->zero_tokens()->default_value(false), "Don't try to detect supported platforms from game shelf.\nSkips the initial fast platform detection and detects the supported platforms from game details which is slower but more accurate.\nUseful in case platform identifier is missing for some games in the game shelf.\nUsing --platform with --list doesn't work with this option.")
+ ("download-file", bpo::value<std::string>(&Globals::globalConfig.sFileIdString)->default_value(""), "Download files using fileid\n\nFormat:\n\"gamename/fileid\"\nor: \"gogdownloader://gamename/fileid\"\n\nMultiple files:\n\"gamename1/fileid1,gamename2/fileid2\"\nor: \"gogdownloader://gamename1/fileid1,gamename2/fileid2\"\n\nThis option ignores all subdir options. The files are downloaded to directory specified with --directory option.")
+ ("output-file,o", bpo::value<std::string>(&Globals::globalConfig.sOutputFilename)->default_value(""), "Set filename of file downloaded with --download-file.")
+ ("wishlist", bpo::value<bool>(&Globals::globalConfig.bShowWishlist)->zero_tokens()->default_value(false), "Show wishlist")
+ ("cacert", bpo::value<std::string>(&Globals::globalConfig.curlConf.sCACertPath)->default_value(""), "Path to CA certificate bundle in PEM format")
+ ("respect-umask", bpo::value<bool>(&Globals::globalConfig.bRespectUmask)->zero_tokens()->default_value(false), "Do not adjust permissions of sensitive files")
+ ("user-agent", bpo::value<std::string>(&Globals::globalConfig.curlConf.sUserAgent)->default_value(DEFAULT_USER_AGENT), "Set user agent")
+#ifdef USE_QT_GUI_LOGIN
+ ("enable-login-gui", bpo::value<bool>(&Globals::globalConfig.bEnableLoginGUI)->zero_tokens()->default_value(false), "Enable login GUI when encountering reCAPTCHA on login form")
+#endif
+ ;
+ // Commandline options (config file)
+ options_cli_cfg.add_options()
+ ("directory", bpo::value<std::string>(&Globals::globalConfig.dirConf.sDirectory)->default_value("."), "Set download directory")
+ ("limit-rate", bpo::value<curl_off_t>(&Globals::globalConfig.curlConf.iDownloadRate)->default_value(0), "Limit download rate to value in kB\n0 = unlimited")
+ ("xml-directory", bpo::value<std::string>(&Globals::globalConfig.sXMLDirectory), "Set directory for GOG XML files")
+ ("chunk-size", bpo::value<size_t>(&Globals::globalConfig.iChunkSize)->default_value(10), "Chunk size (in MB) when creating XML")
+ ("platform", bpo::value<std::string>(&sInstallerPlatform)->default_value("w+l"), platform_text.c_str())
+ ("language", bpo::value<std::string>(&sInstallerLanguage)->default_value("en"), language_text.c_str())
+ ("no-remote-xml", bpo::value<bool>(&bNoRemoteXML)->zero_tokens()->default_value(false), "Don't use remote XML for repair")
+ ("no-unicode", bpo::value<bool>(&bNoUnicode)->zero_tokens()->default_value(false), "Don't use Unicode in the progress bar")
+ ("no-color", bpo::value<bool>(&bNoColor)->zero_tokens()->default_value(false), "Don't use coloring in the progress bar or status messages")
+ ("no-duplicate-handling", bpo::value<bool>(&bNoDuplicateHandler)->zero_tokens()->default_value(false), "Don't use duplicate handler for installers\nDuplicate installers from different languages are handled separately")
+ ("no-subdirectories", bpo::value<bool>(&bNoSubDirectories)->zero_tokens()->default_value(false), "Don't create subdirectories for extras, patches and language packs")
+ ("verbose", bpo::value<bool>(&Globals::globalConfig.bVerbose)->zero_tokens()->default_value(false), "Print lots of information")
+ ("curl-verbose", bpo::value<bool>(&Globals::globalConfig.curlConf.bVerbose)->zero_tokens()->default_value(false), "Set libcurl to verbose mode")
+ ("insecure", bpo::value<bool>(&bInsecure)->zero_tokens()->default_value(false), "Don't verify authenticity of SSL certificates")
+ ("timeout", bpo::value<long int>(&Globals::globalConfig.curlConf.iTimeout)->default_value(10), "Set timeout for connection\nMaximum time in seconds that connection phase is allowed to take")
+ ("retries", bpo::value<int>(&Globals::globalConfig.iRetries)->default_value(3), "Set maximum number of retries on failed download")
+ ("wait", bpo::value<int>(&Globals::globalConfig.iWait)->default_value(0), "Time to wait between requests (milliseconds)")
+ ("subdir-installers", bpo::value<std::string>(&Globals::globalConfig.dirConf.sInstallersSubdir)->default_value(""), ("Set subdirectory for installers" + subdir_help_text).c_str())
+ ("subdir-extras", bpo::value<std::string>(&Globals::globalConfig.dirConf.sExtrasSubdir)->default_value("extras"), ("Set subdirectory for extras" + subdir_help_text).c_str())
+ ("subdir-patches", bpo::value<std::string>(&Globals::globalConfig.dirConf.sPatchesSubdir)->default_value("patches"), ("Set subdirectory for patches" + subdir_help_text).c_str())
+ ("subdir-language-packs", bpo::value<std::string>(&Globals::globalConfig.dirConf.sLanguagePackSubdir)->default_value("languagepacks"), ("Set subdirectory for language packs" + subdir_help_text).c_str())
+ ("subdir-dlc", bpo::value<std::string>(&Globals::globalConfig.dirConf.sDLCSubdir)->default_value("dlc/%dlcname%"), ("Set subdirectory for dlc" + subdir_help_text).c_str())
+ ("subdir-game", bpo::value<std::string>(&Globals::globalConfig.dirConf.sGameSubdir)->default_value("%gamename%"), ("Set subdirectory for game" + subdir_help_text).c_str())
+ ("use-cache", bpo::value<bool>(&Globals::globalConfig.bUseCache)->zero_tokens()->default_value(false), ("Use game details cache"))
+ ("cache-valid", bpo::value<int>(&Globals::globalConfig.iCacheValid)->default_value(2880), ("Set how long cached game details are valid (in minutes)\nDefault: 2880 minutes (48 hours)"))
+ ("save-serials", bpo::value<bool>(&Globals::globalConfig.dlConf.bSaveSerials)->zero_tokens()->default_value(false), "Save serial numbers when downloading")
+ ("ignore-dlc-count", bpo::value<std::string>(&Globals::globalConfig.sIgnoreDLCCountRegex)->implicit_value(".*"), "Set regular expression filter for games to ignore DLC count information\nIgnoring DLC count information helps in situations where the account page doesn't provide accurate information about DLCs")
+ ("include", bpo::value<std::string>(&sIncludeOptions)->default_value("all"), ("Select what to download/list/repair\n" + include_options_text).c_str())
+ ("exclude", bpo::value<std::string>(&sExcludeOptions)->default_value(""), ("Select what not to download/list/repair\n" + include_options_text).c_str())
+ ("automatic-xml-creation", bpo::value<bool>(&Globals::globalConfig.dlConf.bAutomaticXMLCreation)->zero_tokens()->default_value(false), "Automatically create XML data after download has completed")
+ ("save-changelogs", bpo::value<bool>(&Globals::globalConfig.dlConf.bSaveChangelogs)->zero_tokens()->default_value(false), "Save changelogs when downloading")
+ ("threads", bpo::value<unsigned int>(&Globals::globalConfig.iThreads)->default_value(4), "Number of download threads")
+ ("info-threads", bpo::value<unsigned int>(&Globals::globalConfig.iInfoThreads)->default_value(4), "Number of threads for getting product info")
+ ("dlc-list", bpo::value<std::string>(&Globals::globalConfig.sGameHasDLCList)->default_value("https://raw.githubusercontent.com/Sude-/lgogdownloader-lists/master/game_has_dlc.txt"), "Set URL for list of games that have DLC")
+ ("progress-interval", bpo::value<int>(&Globals::globalConfig.iProgressInterval)->default_value(100), "Set interval for progress bar update (milliseconds)\nValue must be between 1 and 10000")
+ ("lowspeed-timeout", bpo::value<long int>(&Globals::globalConfig.curlConf.iLowSpeedTimeout)->default_value(30), "Set time in number seconds that the transfer speed should be below the rate set with --lowspeed-rate for it to considered too slow and aborted")
+ ("lowspeed-rate", bpo::value<long int>(&Globals::globalConfig.curlConf.iLowSpeedTimeoutRate)->default_value(200), "Set average transfer speed in bytes per second that the transfer should be below during time specified with --lowspeed-timeout for it to be considered too slow and aborted")
+ ("include-hidden-products", bpo::value<bool>(&Globals::globalConfig.bIncludeHiddenProducts)->zero_tokens()->default_value(false), "Include games that have been set hidden in account page")
+ ;
+
+ options_cli_no_cfg_hidden.add_options()
+ ("login-email", bpo::value<std::string>(&Globals::globalConfig.sEmail)->default_value(""), "login email")
+ ("login-password", bpo::value<std::string>(&Globals::globalConfig.sPassword)->default_value(""), "login password")
+ ;
+
+ options_cli_experimental.add_options()
+ ("galaxy-install", bpo::value<std::string>(&galaxy_product_id_install)->default_value(""), "Install game using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
+ ("galaxy-show-builds", bpo::value<std::string>(&galaxy_product_id_show_builds)->default_value(""), "Show game builds using product id [product_id/build_index] or gamename regex [gamename/build_id]\nBuild index is used to select a build and defaults to 0 if not specified.\n\nExample: 12345/2 selects build 2 for product 12345")
+ ("galaxy-platform", bpo::value<std::string>(&sGalaxyPlatform)->default_value("w"), galaxy_platform_text.c_str())
+ ("galaxy-language", bpo::value<std::string>(&sGalaxyLanguage)->default_value("en"), galaxy_language_text.c_str())
+ ("galaxy-arch", bpo::value<std::string>(&sGalaxyArch)->default_value("x64"), galaxy_arch_text.c_str())
+ ("galaxy-no-dependencies", bpo::value<bool>(&bNoGalaxyDependencies)->zero_tokens()->default_value(false), "Don't download dependencies during --galaxy-install")
+ ("subdir-galaxy-install", bpo::value<std::string>(&Globals::globalConfig.dirConf.sGalaxyInstallSubdir)->default_value("%install_dir%"), galaxy_install_subdir_text.c_str())
+ ("galaxy-cdn-priority", bpo::value<std::string>(&sGalaxyCDN)->default_value("edgecast,highwinds,gog_cdn"), galaxy_cdn_priority_text.c_str())
+ ("galaxy-delete-orphans", bpo::value<bool>(&Globals::globalConfig.dlConf.bGalaxyDeleteOrphans)->zero_tokens()->default_value(false), "Delete orphaned files during --galaxy-install")
+ ;
+
+ options_cli_all.add(options_cli_no_cfg).add(options_cli_cfg).add(options_cli_experimental);
+ options_cfg_all.add(options_cfg_only).add(options_cli_cfg);
+ options_cli_all_include_hidden.add(options_cli_all).add(options_cli_no_cfg_hidden);
+
+ bpo::parsed_options parsed = bpo::parse_command_line(argc, argv, options_cli_all_include_hidden);
+ bpo::store(parsed, vm);
+ unrecognized_options_cli = bpo::collect_unrecognized(parsed.options, bpo::include_positional);
+ bpo::notify(vm);
+
+ if (vm.count("help"))
+ {
+ std::cout << Globals::globalConfig.sVersionString << std::endl
+ << options_cli_all << std::endl;
+ return 0;
+ }
+
+ if (vm.count("version"))
+ {
+ std::cout << VERSION_STRING << std::endl;
+ return 0;
+ }
+
+ // Create lgogdownloader directories
+ boost::filesystem::path path = Globals::globalConfig.sXMLDirectory;
+ if (!boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ return 1;
+ }
+ }
+
+ path = Globals::globalConfig.sConfigDirectory;
+ if (!boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ return 1;
+ }
+ }
+
+ path = Globals::globalConfig.sCacheDirectory;
+ if (!boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ return 1;
+ }
+ }
+
+ if (boost::filesystem::exists(Globals::globalConfig.sConfigFilePath))
+ {
+ std::ifstream ifs(Globals::globalConfig.sConfigFilePath.c_str());
+ if (!ifs)
+ {
+ std::cerr << "Could not open config file: " << Globals::globalConfig.sConfigFilePath << std::endl;
+ return 1;
+ }
+ else
+ {
+ bpo::parsed_options parsed = bpo::parse_config_file(ifs, options_cfg_all, true);
+ bpo::store(parsed, vm);
+ bpo::notify(vm);
+ ifs.close();
+ unrecognized_options_cfg = bpo::collect_unrecognized(parsed.options, bpo::include_positional);
+ }
+ }
+ if (boost::filesystem::exists(Globals::globalConfig.sBlacklistFilePath))
+ {
+ std::ifstream ifs(Globals::globalConfig.sBlacklistFilePath.c_str());
+ if (!ifs)
+ {
+ std::cerr << "Could not open blacklist file: " << Globals::globalConfig.sBlacklistFilePath << std::endl;
+ return 1;
+ }
+ else
+ {
+ std::string line;
+ std::vector<std::string> lines;
+ while (!ifs.eof())
+ {
+ std::getline(ifs, line);
+ lines.push_back(std::move(line));
+ }
+ Globals::globalConfig.blacklist.initialize(lines);
+ }
+ }
+
+ if (boost::filesystem::exists(Globals::globalConfig.sIgnorelistFilePath))
+ {
+ std::ifstream ifs(Globals::globalConfig.sIgnorelistFilePath.c_str());
+ if (!ifs)
+ {
+ std::cerr << "Could not open ignorelist file: " << Globals::globalConfig.sIgnorelistFilePath << std::endl;
+ return 1;
+ }
+ else
+ {
+ std::string line;
+ std::vector<std::string> lines;
+ while (!ifs.eof())
+ {
+ std::getline(ifs, line);
+ lines.push_back(std::move(line));
+ }
+ Globals::globalConfig.ignorelist.initialize(lines);
+ }
+ }
+
+ if (Globals::globalConfig.sIgnoreDLCCountRegex.empty())
+ {
+ if (boost::filesystem::exists(Globals::globalConfig.sGameHasDLCListFilePath))
+ {
+ std::ifstream ifs(Globals::globalConfig.sGameHasDLCListFilePath.c_str());
+ if (!ifs)
+ {
+ std::cerr << "Could not open list of games that have dlc: " << Globals::globalConfig.sGameHasDLCListFilePath << std::endl;
+ return 1;
+ }
+ else
+ {
+ std::string line;
+ std::vector<std::string> lines;
+ while (!ifs.eof())
+ {
+ std::getline(ifs, line);
+ lines.push_back(std::move(line));
+ }
+ Globals::globalConfig.gamehasdlc.initialize(lines);
+ }
+ }
+ }
+
+ if (Globals::globalConfig.bLogin)
+ {
+ std::string login_conf = Globals::globalConfig.sConfigDirectory + "/login.txt";
+ if (boost::filesystem::exists(login_conf))
+ {
+ std::ifstream ifs(login_conf);
+ if (!ifs)
+ {
+ std::cerr << "Could not open login conf: " << login_conf << std::endl;
+ return 1;
+ }
+ else
+ {
+ std::string line;
+ std::vector<std::string> lines;
+ while (!ifs.eof())
+ {
+ std::getline(ifs, line);
+ lines.push_back(std::move(line));
+ }
+ Globals::globalConfig.sEmail = lines[0];
+ Globals::globalConfig.sPassword = lines[1];
+ }
+ }
+ }
+
+ if (vm.count("chunk-size"))
+ Globals::globalConfig.iChunkSize <<= 20; // Convert chunk size from bytes to megabytes
+
+ if (vm.count("limit-rate"))
+ Globals::globalConfig.curlConf.iDownloadRate <<= 10; // Convert download rate from bytes to kilobytes
+
+ if (vm.count("check-orphans"))
+ if (Globals::globalConfig.sOrphanRegex.empty())
+ Globals::globalConfig.sOrphanRegex = orphans_regex_default;
+
+ if (vm.count("report"))
+ Globals::globalConfig.bReport = true;
+
+ if (Globals::globalConfig.iWait > 0)
+ Globals::globalConfig.iWait *= 1000;
+
+ if (Globals::globalConfig.iProgressInterval < 1)
+ Globals::globalConfig.iProgressInterval = 1;
+ else if (Globals::globalConfig.iProgressInterval > 10000)
+ Globals::globalConfig.iProgressInterval = 10000;
+
+ if (Globals::globalConfig.iThreads < 1)
+ {
+ Globals::globalConfig.iThreads = 1;
+ set_vm_value(vm, "threads", Globals::globalConfig.iThreads);
+ }
+
+ Globals::globalConfig.curlConf.bVerifyPeer = !bInsecure;
+ Globals::globalConfig.bColor = !bNoColor;
+ Globals::globalConfig.bUnicode = !bNoUnicode;
+ Globals::globalConfig.dlConf.bDuplicateHandler = !bNoDuplicateHandler;
+ Globals::globalConfig.dlConf.bRemoteXML = !bNoRemoteXML;
+ Globals::globalConfig.dirConf.bSubDirectories = !bNoSubDirectories;
+ Globals::globalConfig.bPlatformDetection = !bNoPlatformDetection;
+ Globals::globalConfig.dlConf.bGalaxyDependencies = !bNoGalaxyDependencies;
+
+ for (auto i = unrecognized_options_cli.begin(); i != unrecognized_options_cli.end(); ++i)
+ if (i->compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0)
+ Globals::globalConfig.sFileIdString = *i;
+
+ if (!Globals::globalConfig.sFileIdString.empty())
+ {
+ if (Globals::globalConfig.sFileIdString.compare(0, GlobalConstants::PROTOCOL_PREFIX.length(), GlobalConstants::PROTOCOL_PREFIX) == 0)
+ {
+ Globals::globalConfig.sFileIdString.replace(0, GlobalConstants::PROTOCOL_PREFIX.length(), "");
+ }
+ vFileIdStrings = Util::tokenize(Globals::globalConfig.sFileIdString, ",");
+ }
+
+ if (!Globals::globalConfig.sOutputFilename.empty() && vFileIdStrings.size() > 1)
+ {
+ std::cerr << "Cannot specify an output file name when downloading multiple files." << std::endl;
+ return 1;
+ }
+
+ if (Globals::globalConfig.sXMLFile == "automatic")
+ Globals::globalConfig.dlConf.bAutomaticXMLCreation = true;
+
+ Util::parseOptionString(sInstallerLanguage, Globals::globalConfig.dlConf.vLanguagePriority, Globals::globalConfig.dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES);
+ Util::parseOptionString(sInstallerPlatform, Globals::globalConfig.dlConf.vPlatformPriority, Globals::globalConfig.dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS);
+
+ Globals::globalConfig.dlConf.iGalaxyPlatform = Util::getOptionValue(sGalaxyPlatform, GlobalConstants::PLATFORMS);
+ Globals::globalConfig.dlConf.iGalaxyLanguage = Util::getOptionValue(sGalaxyLanguage, GlobalConstants::LANGUAGES);
+ Globals::globalConfig.dlConf.iGalaxyArch = Util::getOptionValue(sGalaxyArch, GlobalConstants::GALAXY_ARCHS, false);
+
+ if (Globals::globalConfig.dlConf.iGalaxyArch == 0 || Globals::globalConfig.dlConf.iGalaxyArch == Util::getOptionValue("all", GlobalConstants::GALAXY_ARCHS, false))
+ Globals::globalConfig.dlConf.iGalaxyArch = GlobalConstants::ARCH_X64;
+
+ Util::parseOptionString(sGalaxyCDN, Globals::globalConfig.dlConf.vGalaxyCDNPriority, Globals::globalConfig.dlConf.iGalaxyCDN, GlobalConstants::GALAXY_CDNS);
+
+ unsigned int include_value = 0;
+ unsigned int exclude_value = 0;
+ std::vector<std::string> vInclude = Util::tokenize(sIncludeOptions, ",");
+ std::vector<std::string> vExclude = Util::tokenize(sExcludeOptions, ",");
+ for (std::vector<std::string>::iterator it = vInclude.begin(); it != vInclude.end(); it++)
+ {
+ include_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS);
+ }
+ for (std::vector<std::string>::iterator it = vExclude.begin(); it != vExclude.end(); it++)
+ {
+ exclude_value |= Util::getOptionValue(*it, INCLUDE_OPTIONS);
+ }
+ Globals::globalConfig.dlConf.iInclude = include_value & ~exclude_value;
+
+ // Assign values
+ // TODO: Use config.iInclude in Downloader class directly and get rid of this value assignment
+ Globals::globalConfig.dlConf.bInstallers = (Globals::globalConfig.dlConf.iInclude & OPTION_INSTALLERS);
+ Globals::globalConfig.dlConf.bExtras = (Globals::globalConfig.dlConf.iInclude & OPTION_EXTRAS);
+ Globals::globalConfig.dlConf.bPatches = (Globals::globalConfig.dlConf.iInclude & OPTION_PATCHES);
+ Globals::globalConfig.dlConf.bLanguagePacks = (Globals::globalConfig.dlConf.iInclude & OPTION_LANGPACKS);
+ Globals::globalConfig.dlConf.bDLC = (Globals::globalConfig.dlConf.iInclude & OPTION_DLCS);
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ return 1;
+ }
+ catch (...)
+ {
+ std::cerr << "Exception of unknown type!" << std::endl;
+ return 1;
+ }
+
+ if (Globals::globalConfig.dlConf.iInstallerPlatform < GlobalConstants::PLATFORMS[0].id || Globals::globalConfig.dlConf.iInstallerPlatform > Util::getOptionValue("all", GlobalConstants::PLATFORMS))
+ {
+ std::cerr << "Invalid value for --platform" << std::endl;
+ return 1;
+ }
+
+ if (Globals::globalConfig.dlConf.iInstallerLanguage < GlobalConstants::LANGUAGES[0].id || Globals::globalConfig.dlConf.iInstallerLanguage > Util::getOptionValue("all", GlobalConstants::LANGUAGES))
+ {
+ std::cerr << "Invalid value for --language" << std::endl;
+ return 1;
+ }
+
+ if (!Globals::globalConfig.sXMLDirectory.empty())
+ {
+ // Make sure that xml directory doesn't have trailing slash
+ if (Globals::globalConfig.sXMLDirectory.at(Globals::globalConfig.sXMLDirectory.length()-1)=='/')
+ Globals::globalConfig.sXMLDirectory.assign(Globals::globalConfig.sXMLDirectory.begin(), Globals::globalConfig.sXMLDirectory.end()-1);
+ }
+
+ // Create GOG XML for a file
+ if (!Globals::globalConfig.sXMLFile.empty() && (Globals::globalConfig.sXMLFile != "automatic"))
+ {
+ Util::createXML(Globals::globalConfig.sXMLFile, Globals::globalConfig.iChunkSize, Globals::globalConfig.sXMLDirectory);
+ return 0;
+ }
+
+ // Make sure that directory has trailing slash
+ if (!Globals::globalConfig.dirConf.sDirectory.empty())
+ {
+ if (Globals::globalConfig.dirConf.sDirectory.at(Globals::globalConfig.dirConf.sDirectory.length()-1)!='/')
+ Globals::globalConfig.dirConf.sDirectory += "/";
+ }
+ else
+ {
+ Globals::globalConfig.dirConf.sDirectory = "./"; // Directory wasn't specified, use current directory
+ }
+
+ // CA certificate bundle
+ if (Globals::globalConfig.curlConf.sCACertPath.empty())
+ {
+ // Use CURL_CA_BUNDLE environment variable for CA certificate path if it is set
+ char *ca_bundle = getenv("CURL_CA_BUNDLE");
+ if (ca_bundle)
+ Globals::globalConfig.curlConf.sCACertPath = (std::string)ca_bundle;
+ }
+
+ if (!unrecognized_options_cfg.empty() && (!Globals::globalConfig.bSaveConfig || !Globals::globalConfig.bResetConfig))
+ {
+ std::cerr << "Unrecognized options in " << Globals::globalConfig.sConfigFilePath << std::endl;
+ for (unsigned int i = 0; i < unrecognized_options_cfg.size(); i+=2)
+ {
+ std::cerr << unrecognized_options_cfg[i] << " = " << unrecognized_options_cfg[i+1] << std::endl;
+ }
+ std::cerr << std::endl;
+ }
+
+ // Init curl globally
+ ssl_thread_setup();
+ curl_global_init(CURL_GLOBAL_ALL);
+
+ Downloader downloader;
+
+ int iLoginTries = 0;
+ bool bLoginOK = false;
+
+ // Login because --login, --login-api or --login-website was used
+ if (Globals::globalConfig.bLogin)
+ bLoginOK = downloader.login();
+
+ bool bIsLoggedin = downloader.isLoggedIn();
+
+ // Login because we are not logged in
+ while (iLoginTries++ < Globals::globalConfig.iRetries && !bIsLoggedin)
+ {
+ bLoginOK = downloader.login();
+ if (bLoginOK)
+ {
+ bIsLoggedin = downloader.isLoggedIn();
+ }
+ }
+
+ // Login failed, cleanup
+ if (!bLoginOK && !bIsLoggedin)
+ {
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 1;
+ }
+
+ // Make sure that config file and cookie file are only readable/writable by owner
+ if (!Globals::globalConfig.bRespectUmask)
+ {
+ Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+ Util::setFilePermissions(Globals::globalConfig.curlConf.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+ Util::setFilePermissions(Globals::galaxyConf.getFilepath(), boost::filesystem::owner_read | boost::filesystem::owner_write);
+ }
+
+ if (Globals::globalConfig.bSaveConfig || bLoginOK)
+ {
+ std::ofstream ofs(Globals::globalConfig.sConfigFilePath.c_str());
+ if (ofs)
+ {
+ std::cerr << "Saving config: " << Globals::globalConfig.sConfigFilePath << std::endl;
+ for (bpo::variables_map::iterator it = vm.begin(); it != vm.end(); ++it)
+ {
+ std::string option = it->first;
+ std::string option_value_string;
+ const bpo::variable_value& option_value = it->second;
+
+ try
+ {
+ if (options_cfg_all.find(option, false).long_name() == option)
+ {
+ if (!option_value.empty())
+ {
+ const std::type_info& type = option_value.value().type() ;
+ if ( type == typeid(std::string) )
+ option_value_string = option_value.as<std::string>();
+ else if ( type == typeid(int) )
+ option_value_string = std::to_string(option_value.as<int>());
+ else if ( type == typeid(size_t) )
+ option_value_string = std::to_string(option_value.as<size_t>());
+ else if ( type == typeid(unsigned int) )
+ option_value_string = std::to_string(option_value.as<unsigned int>());
+ else if ( type == typeid(long int) )
+ option_value_string = std::to_string(option_value.as<long int>());
+ else if ( type == typeid(bool) )
+ {
+ if (option_value.as<bool>() == true)
+ option_value_string = "true";
+ else
+ option_value_string = "false";
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ continue;
+ }
+
+ ofs << option << " = " << option_value_string << std::endl;
+ }
+ ofs.close();
+ if (!Globals::globalConfig.bRespectUmask)
+ Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+ if (Globals::globalConfig.bSaveConfig)
+ {
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 0;
+ }
+ }
+ else
+ {
+ std::cerr << "Failed to create config: " << Globals::globalConfig.sConfigFilePath << std::endl;
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 1;
+ }
+ }
+ else if (Globals::globalConfig.bResetConfig)
+ {
+ std::ofstream ofs(Globals::globalConfig.sConfigFilePath.c_str());
+ if (ofs)
+ {
+ ofs.close();
+ if (!Globals::globalConfig.bRespectUmask)
+ Util::setFilePermissions(Globals::globalConfig.sConfigFilePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 0;
+ }
+ else
+ {
+ std::cerr << "Failed to create config: " << Globals::globalConfig.sConfigFilePath << std::endl;
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 1;
+ }
+ }
+
+ bool bInitOK = downloader.init();
+ if (!bInitOK)
+ {
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+ return 1;
+ }
+
+ int res = 0;
+
+ if (Globals::globalConfig.bShowWishlist)
+ downloader.showWishlist();
+ else if (Globals::globalConfig.bUpdateCache)
+ downloader.updateCache();
+ else if (Globals::globalConfig.bNotifications)
+ downloader.checkNotifications();
+ else if (bClearUpdateNotifications)
+ downloader.clearUpdateNotifications();
+ else if (!vFileIdStrings.empty())
+ {
+ for (std::vector<std::string>::iterator it = vFileIdStrings.begin(); it != vFileIdStrings.end(); it++)
+ {
+ res |= downloader.downloadFileWithId(*it, Globals::globalConfig.sOutputFilename) ? 1 : 0;
+ }
+ }
+ else if (Globals::globalConfig.bRepair) // Repair file
+ downloader.repair();
+ else if (Globals::globalConfig.bDownload) // Download games
+ downloader.download();
+ else if (Globals::globalConfig.bListDetails || Globals::globalConfig.bList) // Detailed list of games/extras
+ res = downloader.listGames();
+ else if (!Globals::globalConfig.sOrphanRegex.empty()) // Check for orphaned files if regex for orphans is set
+ downloader.checkOrphans();
+ else if (Globals::globalConfig.bCheckStatus)
+ downloader.checkStatus();
+ else if (!galaxy_product_id_show_builds.empty())
+ {
+ int build_index = -1;
+ std::vector<std::string> tokens = Util::tokenize(galaxy_product_id_show_builds, "/");
+ std::string product_id = tokens[0];
+ if (tokens.size() == 2)
+ {
+ build_index = std::stoi(tokens[1]);
+ }
+ downloader.galaxyShowBuilds(product_id, build_index);
+ }
+ else if (!galaxy_product_id_install.empty())
+ {
+ int build_index = -1;
+ std::vector<std::string> tokens = Util::tokenize(galaxy_product_id_install, "/");
+ std::string product_id = tokens[0];
+ if (tokens.size() == 2)
+ {
+ build_index = std::stoi(tokens[1]);
+ }
+ downloader.galaxyInstallGame(product_id, build_index, Globals::globalConfig.dlConf.iGalaxyArch);
+ }
+ else
+ {
+ if (!Globals::globalConfig.bLogin)
+ {
+ // Show help message
+ std::cerr << Globals::globalConfig.sVersionString << std::endl
+ << options_cli_all << std::endl;
+ }
+ }
+
+ // Orphan check was called at the same time as download. Perform it after download has finished
+ if (!Globals::globalConfig.sOrphanRegex.empty() && Globals::globalConfig.bDownload)
+ downloader.checkOrphans();
+
+ curl_global_cleanup();
+ ssl_thread_cleanup();
+
+ return res;
+}
--- /dev/null
+find_program(HELP2MAN help2man DOC "Location of the help2man program")
+find_program(GZIP gzip DOC "Location of the gzip program")
+mark_as_advanced(HELP2MAN)
+mark_as_advanced(GZIP)
+
+include(GNUInstallDirs)
+
+if(HELP2MAN AND GZIP)
+ set(H2M_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.supplemental.groff")
+ set(MAN_PAGE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.1")
+ set(MAN_FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.1.gz")
+ add_custom_command(
+ OUTPUT ${MAN_FILE}
+ COMMAND ${HELP2MAN} -N -i ${H2M_FILE} -o ${MAN_PAGE} "\"${PROJECT_BINARY_DIR}/${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX}\""
+ COMMAND ${GZIP} -f -9 ${MAN_PAGE}
+ MAIN_DEPENDENCY ${H2M_FILE}
+ COMMENT "Building man page"
+ VERBATIM
+ )
+ add_custom_target(manpage ALL DEPENDS ${MAN_FILE} ${PROJECT_NAME})
+ install(FILES ${MAN_FILE} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
+else(HELP2MAN AND GZIP)
+ message("WARNING: One of the following is missing: help2man, gzip; man page will not be generated")
+endif(HELP2MAN AND GZIP)
--- /dev/null
+[synopsis]
+.B lgogdownloader
+[\fIOPTION\fP]...
+
+[description]
+An open-source GOG.com downloader for Linux users which uses the same API as GOG Galaxy.
+.PP
+LGOGDownloader can download purchased games, query GOG.com to see if game files have changed, as well as downloading extras such as artwork and manuals. It is capable of downloading language-specific installers for games where they exist.
+
+/--update-check/
+.nf
+/--no-installers/
+.fi
+
+/Status codes:/
+.nf
+
+[blacklist]
+.fi
+Allows user to specify individual files that should not be downloaded or mentioned as orphans.
+.sp 1
+Each line in the file specifies one blacklist expression, except for empty lines and lines starting with #.
+First few characters specify blacklist item type and flags.
+So far, only regular expression (perl variant) are supported, so each line must start with "Rp" characters.
+After a space comes the expression itself. Expressions are matched against file path relative to what was specified as \fI--directory\fP.
+
+\fIExample black list\fP
+.br
+# used to store manually downloaded mods/patches/maps/, don't mention it as orphans
+.br
+Rp ^[^/]*/manual/.*
+.br
+# included with every *divinity game, once is enough
+.br
+Rp beyond_divinity/extras/bd_ladymageknight\.zip
+.br
+Rp divinity_2_developers_cut/extras/divinity_2_ladymageknight\.zip
+.sp
+# extra 6GB is A LOT of space if you don't actually plan to mod your game
+.br
+Rp the_witcher_2/extras/the_witcher_2_redkit\.zip
+.br
+Rp the_witcher_2/extras/extras_pack_3_hu_pl_ru_tr_zh_\.zip
+.br
+Rp the_witcher_2/extras/extras_pack_2_fr_it_jp_\.zip
+
+[files]
+.fi
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/\fP
+Storage for configuration files and cookies
+.br
+If \fB$XDG_CONFIG_HOME\fP is not set, it will use \fI$HOME/.config/lgogdownloader/\fP.
+
+.TP
+\fI$XDG_CACHE_HOME/lgogdownloader/xml/\fP
+Storage for XML files
+.br
+If \fB$XDG_CACHE_HOME\fP is not set, it will use \fI$HOME/.cache/lgogdownloader/xml/\fP.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/blacklist.txt\fP
+Allows user to specify individual files that should not be downloaded.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/ignorelist.txt\fP
+Allows user to specify individual files that should not be mentioned
+as orphans. The file has the same format and interpretation as a
+blacklist.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/game_has_dlc.txt\fP
+Allows user to specify which games have dlc and should have their DLC count
+information ignored. The file has the same format and interpretation as a
+blacklist.
+.br
+It doesn't have to exist, but if it does exist, it must be readable to lgogdownloader.
+.br
+If the file exists lgogdownloader uses it instead of list specified with
+\fB--dlc-list\fP option
+
+.TP
+\fI$XDG_CONFIG_HOME/lgogdownloader/gamespecific/gamename.conf\fP
+JSON formatted file. Sets game specific settings for \fBgamename\fP.
+.br
+Allowed settings are \fBlanguage\fP, \fBplatform\fP, \fBdlc\fP, \fBignore-dlc-count\fP \fBsubdirectories\fP, \fBdirectory\fP, \fBsubdir-game\fP, \fBsubdir-installers\fP, \fBsubdir-extras\fP, \fBsubdir-patches\fP, \fBsubdir-language-packs\fP and \fBsubdir-dlc\fP.
+.br
+The \fBdlc\fP option is limited to disabling DLC for specific game. It can't enable DLC listing/downloading if \fB--no-dlc\fP option is used.
+.br
+Must be in the following format:
+.br
+{
+ "language" : <string>,
+ "platform" : <string>,
+ "dlc" : <bool>,
+ "ignore-dlc-count" : <bool>,
+ "subdirectories" : <bool>,
+ "directory" : <string>,
+ "subdir-game" : <string>,
+ "subdir-installers" : <string>,
+ "subdir-extras" : <string>,
+ "subdir-patches" : <string>,
+ "subdir-language-packs" : <string>,
+ "subdir-dlc" : <string>
+.br
+}
+
+[priorities]
+Separating values with "," when using \fBlanguage\fP and \fBplatform\fP switches enables a priority-based mode: only the first matching one will be downloaded.
+.PP
+For example, setting \fBlanguage\fP to \fBfr+en\fP means both French and English will be downloaded (if available) for all games. Setting \fBlanguage\fP to \fBfr,en\fP means that the French version (and only that one) will be downloaded if available, and if not, the English version will be downloaded.
+.PP
+You're allowed to "stack" codes in the priority string if needed. If you set \fBlanguage\fP to \fBes+fr,en\fP it means it'll download both Spanish (es) and French (fr) versions if they are available, and the English (en) one only if none of French and Spanish are available.
+
+[availability]
+The latest version of this distribution is available from \fIhttps://github.com/Sude-/lgogdownloader\fP
+
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "blacklist.h"
+#include "config.h"
+#include "util.h"
+
+#include <iostream>
+#include <utility>
+
+enum {
+ BLFLAG_RX = 1 << 0,
+ BLFLAG_PERL = 1 << 1
+};
+
+void Blacklist::initialize(const std::vector<std::string>& lines) {
+ int linenr = 1;
+ for (auto it = lines.begin(); it != lines.end(); ++it, ++linenr) {
+ BlacklistItem item;
+ const std::string& s = *it;
+
+ if (s.length() == 0 || s[0] == '#')
+ continue;
+
+ std::size_t i;
+ for (i = 0; i < s.length() && s[i] != '\x20'; ++i) {
+ switch (s[i]) {
+ case 'R':
+ item.flags |= BLFLAG_RX;
+ break;
+ case 'p':
+ item.flags |= BLFLAG_PERL;
+ break;
+ default:
+ std::cout << "unknown flag '" << s[i] << "' in blacklist line " << linenr << std::endl;
+ break;
+ }
+ }
+ ++i;
+ if (i == s.length()) {
+ std::cout << "empty expression in blacklist line " << linenr << std::endl;
+ continue;
+ }
+ if (item.flags & BLFLAG_RX) {
+ boost::regex::flag_type rx_flags = boost::regex::normal;
+
+ // we only support perl-like syntax for now, which is boost default (normal). Add further flag processing
+ // here if that changes.
+
+ rx_flags |= boost::regex::nosubs;
+
+ item.linenr = linenr;
+ item.source.assign(s.substr(i).c_str());
+ item.regex.assign(item.source, rx_flags);
+ blacklist_.push_back(std::move(item));
+ } else {
+ std::cout << "unknown expression type in blacklist line " << linenr << std::endl;
+ }
+ }
+}
+
+bool Blacklist::isBlacklisted(const std::string& path) {
+ for (auto it = blacklist_.begin(); it != blacklist_.end(); ++it) {
+ const BlacklistItem& item = *it;
+ if (item.flags & BLFLAG_RX && boost::regex_search(path, item.regex))
+ return true;
+ }
+ return false;
+}
+
+bool Blacklist::isBlacklisted(const std::string& path, const std::string& gamename, std::string subdirectory)
+{
+ std::string filepath = Util::makeRelativeFilepath(path, gamename, subdirectory);
+ return isBlacklisted(filepath);
+}
+
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "downloader.h"
+#include "util.h"
+#include "globals.h"
+#include "downloadinfo.h"
+#include "message.h"
+#include "ziputil.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <ctime>
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+#include <fstream>
+#include <iomanip>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <tinyxml2.h>
+#include <json/json.h>
+#include <htmlcxx/html/ParserDom.h>
+#include <htmlcxx/html/Uri.h>
+#include <termios.h>
+#include <algorithm>
+#include <thread>
+#include <mutex>
+#include <atomic>
+
+#include <boost/iostreams/filtering_streambuf.hpp>
+#include <boost/iostreams/copy.hpp>
+#include <boost/iostreams/filter/zlib.hpp>
+#include <boost/iostreams/device/back_inserter.hpp>
+
+namespace bptime = boost::posix_time;
+
+std::vector<DownloadInfo> vDownloadInfo;
+ThreadSafeQueue<gameFile> dlQueue;
+ThreadSafeQueue<Message> msgQueue;
+ThreadSafeQueue<gameFile> createXMLQueue;
+ThreadSafeQueue<gameItem> gameItemQueue;
+ThreadSafeQueue<gameDetails> gameDetailsQueue;
+ThreadSafeQueue<galaxyDepotItem> dlQueueGalaxy;
+ThreadSafeQueue<zipFileEntry> dlQueueGalaxy_MojoSetupHack;
+std::mutex mtx_create_directories; // Mutex for creating directories in Downloader::processDownloadQueue
+std::atomic<unsigned long long> iTotalRemainingBytes(0);
+
+static curl_off_t WriteChunkMemoryCallback(void *contents, curl_off_t size, curl_off_t nmemb, void *userp)
+{
+ curl_off_t realsize = size * nmemb;
+ struct ChunkMemoryStruct *mem = (struct ChunkMemoryStruct *)userp;
+
+ mem->memory = (char *) realloc(mem->memory, mem->size + realsize + 1);
+ if(mem->memory == NULL)
+ {
+ std::cout << "Not enough memory (realloc returned NULL)" << std::endl;
+ return 0;
+ }
+
+ memcpy(&(mem->memory[mem->size]), contents, realsize);
+ mem->size += realsize;
+ mem->memory[mem->size] = 0;
+
+ return realsize;
+}
+
+Downloader::Downloader()
+{
+ if (Globals::globalConfig.bLogin)
+ {
+ if (boost::filesystem::exists(Globals::globalConfig.curlConf.sCookiePath))
+ if (!boost::filesystem::remove(Globals::globalConfig.curlConf.sCookiePath))
+ std::cerr << "Failed to delete " << Globals::globalConfig.curlConf.sCookiePath << std::endl;
+ if (boost::filesystem::exists(Globals::galaxyConf.getFilepath()))
+ if (!boost::filesystem::remove(Globals::galaxyConf.getFilepath()))
+ std::cerr << "Failed to delete " << Globals::galaxyConf.getFilepath() << std::endl;
+ }
+
+ this->resume_position = 0;
+ this->retries = 0;
+
+ // Initialize curl and set curl options
+ curlhandle = curl_easy_init();
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.curlConf.sUserAgent.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout);
+ curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, Globals::globalConfig.curlConf.bVerifyPeer);
+ curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, Globals::globalConfig.curlConf.bVerbose);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(curlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+ curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, Globals::globalConfig.curlConf.iDownloadRate);
+ curl_easy_setopt(curlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_XFERINFODATA, this);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, Globals::globalConfig.curlConf.iLowSpeedTimeout);
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, Globals::globalConfig.curlConf.iLowSpeedTimeoutRate);
+
+ if (!Globals::globalConfig.curlConf.sCACertPath.empty())
+ curl_easy_setopt(curlhandle, CURLOPT_CAINFO, Globals::globalConfig.curlConf.sCACertPath.c_str());
+
+ // Create new GOG website handle
+ gogWebsite = new Website();
+
+ progressbar = new ProgressBar(Globals::globalConfig.bUnicode, Globals::globalConfig.bColor);
+
+ if (boost::filesystem::exists(Globals::galaxyConf.getFilepath()))
+ {
+ std::ifstream ifs(Globals::galaxyConf.getFilepath(), std::ifstream::binary);
+ Json::Value json;
+ try {
+ ifs >> json;
+ if (!json.isMember("expires_at"))
+ {
+ std::time_t last_modified = boost::filesystem::last_write_time(Globals::galaxyConf.getFilepath());
+ Json::Value::LargestInt expires_in = json["expires_in"].asLargestInt();
+ json["expires_at"] = expires_in + last_modified;
+ }
+
+ Globals::galaxyConf.setJSON(json);
+ } catch (const Json::Exception& exc) {
+ std::cerr << "Failed to parse " << Globals::galaxyConf.getFilepath() << std::endl;
+ std::cerr << exc.what() << std::endl;
+ }
+
+ if (ifs)
+ ifs.close();
+ }
+
+ gogGalaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+}
+
+Downloader::~Downloader()
+{
+ if (Globals::globalConfig.bReport)
+ if (this->report_ofs)
+ this->report_ofs.close();
+
+ if (!gogGalaxy->isTokenExpired())
+ this->saveGalaxyJSON();
+
+ delete progressbar;
+ delete gogGalaxy;
+ delete gogWebsite;
+ curl_easy_cleanup(curlhandle);
+ // Make sure that cookie file is only readable/writable by owner
+ if (!Globals::globalConfig.bRespectUmask)
+ {
+ Util::setFilePermissions(Globals::globalConfig.curlConf.sCookiePath, boost::filesystem::owner_read | boost::filesystem::owner_write);
+ }
+}
+
+/* Login check
+ returns false if not logged in
+ returns true if logged in
+*/
+bool Downloader::isLoggedIn()
+{
+ bool bIsLoggedIn = false;
+ Globals::globalConfig.bLogin= false;
+
+ bool bWebsiteIsLoggedIn = gogWebsite->IsLoggedIn();
+ if (!bWebsiteIsLoggedIn)
+ Globals::globalConfig.bLogin = true;
+
+ bool bGalaxyIsLoggedIn = !gogGalaxy->isTokenExpired();
+ if (!bGalaxyIsLoggedIn)
+ {
+ if (gogGalaxy->refreshLogin())
+ bGalaxyIsLoggedIn = true;
+ else
+ Globals::globalConfig.bLogin = true;
+ }
+
+ if (bWebsiteIsLoggedIn && bGalaxyIsLoggedIn)
+ bIsLoggedIn = true;
+
+ return bIsLoggedIn;
+}
+
+/* Initialize the downloader
+ returns 0 if failed
+ returns 1 if successful
+*/
+int Downloader::init()
+{
+ if (!Globals::globalConfig.sGameHasDLCList.empty())
+ {
+ if (Globals::globalConfig.gamehasdlc.empty())
+ {
+ std::string game_has_dlc_list = this->getResponse(Globals::globalConfig.sGameHasDLCList);
+ if (!game_has_dlc_list.empty())
+ Globals::globalConfig.gamehasdlc.initialize(Util::tokenize(game_has_dlc_list, "\n"));
+ }
+ }
+
+ if (!gogGalaxy->init())
+ {
+ if (gogGalaxy->refreshLogin())
+ {
+ this->saveGalaxyJSON();
+ }
+ else
+ return 0;
+ }
+
+ if (!Globals::galaxyConf.getJSON().empty())
+ {
+ if (Globals::galaxyConf.isExpired())
+ {
+ // Access token has expired, refresh
+ if (gogGalaxy->refreshLogin())
+ {
+ this->saveGalaxyJSON();
+ }
+ }
+ }
+
+ if (Globals::globalConfig.bReport && (Globals::globalConfig.bDownload || Globals::globalConfig.bRepair))
+ {
+ this->report_ofs.open(Globals::globalConfig.sReportFilePath);
+ if (!this->report_ofs)
+ {
+ Globals::globalConfig.bReport = false;
+ std::cerr << "Failed to create " << Globals::globalConfig.sReportFilePath << std::endl;
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+/* Login
+ returns 0 if login fails
+ returns 1 if successful
+*/
+int Downloader::login()
+{
+ std::string email;
+ std::string password;
+
+ if (!Globals::globalConfig.sEmail.empty() && !Globals::globalConfig.sPassword.empty())
+ {
+ email = Globals::globalConfig.sEmail;
+ password = Globals::globalConfig.sPassword;
+ }
+ else
+ {
+ if (!isatty(STDIN_FILENO)) {
+ std::cerr << "Unable to read email and password" << std::endl;
+ return 0;
+ }
+ std::cerr << "Email: ";
+ std::getline(std::cin,email);
+
+ std::cerr << "Password: ";
+ struct termios termios_old, termios_new;
+ tcgetattr(STDIN_FILENO, &termios_old); // Get current terminal attributes
+ termios_new = termios_old;
+ termios_new.c_lflag &= ~ECHO; // Set ECHO off
+ tcsetattr(STDIN_FILENO, TCSANOW, &termios_new); // Set terminal attributes
+ std::getline(std::cin, password);
+ tcsetattr(STDIN_FILENO, TCSANOW, &termios_old); // Restore old terminal attributes
+ std::cerr << std::endl;
+ }
+
+ if (email.empty() || password.empty())
+ {
+ std::cerr << "Email and/or password empty" << std::endl;
+ return 0;
+ }
+ else
+ {
+ // Login to website and Galaxy API
+ if (Globals::globalConfig.bLogin)
+ {
+ // Delete old cookies
+ if (boost::filesystem::exists(Globals::globalConfig.curlConf.sCookiePath))
+ if (!boost::filesystem::remove(Globals::globalConfig.curlConf.sCookiePath))
+ std::cerr << "Failed to delete " << Globals::globalConfig.curlConf.sCookiePath << std::endl;
+
+ int iWebsiteLoginResult = gogWebsite->Login(email, password);
+ if (iWebsiteLoginResult < 1)
+ {
+ std::cerr << "HTTP: Login failed" << std::endl;
+ return 0;
+ }
+ else
+ {
+ std::cerr << "HTTP: Login successful" << std::endl;
+ }
+
+ if (iWebsiteLoginResult < 2)
+ {
+ std::cerr << "Galaxy: Login failed" << std::endl;
+ return 0;
+ }
+ else
+ {
+ std::cerr << "Galaxy: Login successful" << std::endl;
+
+ if (!Globals::galaxyConf.getJSON().empty())
+ {
+ this->saveGalaxyJSON();
+ }
+ }
+ }
+ }
+ return 0;
+}
+
+void Downloader::checkNotifications()
+{
+ Json::Value userData = gogGalaxy->getUserData();
+
+ if (userData.empty())
+ {
+ std::cout << "Empty JSON response" << std::endl;
+ return;
+ }
+
+ if (!userData.isMember("updates"))
+ {
+ std::cout << "Invalid JSON response" << std::endl;
+ return;
+ }
+
+ std::cout << "New forum replies: " << userData["updates"]["messages"].asInt() << std::endl;
+ std::cout << "Updated games: " << userData["updates"]["products"].asInt() << std::endl;
+ std::cout << "Unread chat messages: " << userData["updates"]["unreadChatMessages"].asInt() << std::endl;
+ std::cout << "Pending friend requests: " << userData["updates"]["pendingFriendRequests"].asInt() << std::endl;
+}
+
+void Downloader::clearUpdateNotifications()
+{
+ Json::Value userData = gogGalaxy->getUserData();
+ if (userData.empty())
+ {
+ return;
+ }
+
+ if (!userData.isMember("updates"))
+ {
+ return;
+ }
+
+ if (userData["updates"]["products"].asInt() < 1)
+ {
+ std::cout << "No updates" << std::endl;
+ return;
+ }
+
+ Globals::globalConfig.bUpdated = true;
+ this->getGameList();
+
+ for (unsigned int i = 0; i < gameItems.size(); ++i)
+ {
+ // Getting game details should remove the update flag
+ std::cerr << "\033[KClearing update flags " << i+1 << " / " << gameItems.size() << "\r" << std::flush;
+ Json::Value details = gogWebsite->getGameDetailsJSON(gameItems[i].id);
+ }
+ std::cerr << std::endl;
+}
+
+void Downloader::getGameList()
+{
+ gameItems = gogWebsite->getGames();
+}
+
+/* Get detailed info about the games
+ returns 0 if successful
+ returns 1 if fails
+*/
+int Downloader::getGameDetails()
+{
+ // Set default game specific directory options to values from config
+ DirectoryConfig dirConfDefault = Globals::globalConfig.dirConf;
+
+ if (Globals::globalConfig.bUseCache && !Globals::globalConfig.bUpdateCache)
+ {
+ int result = this->loadGameDetailsCache();
+ if (result == 0)
+ {
+ for (unsigned int i = 0; i < this->games.size(); ++i)
+ {
+ gameSpecificConfig conf;
+ conf.dirConf = dirConfDefault;
+ Util::getGameSpecificConfig(games[i].gamename, &conf);
+ this->games[i].makeFilepaths(conf.dirConf);
+ }
+ return 0;
+ }
+ else
+ {
+ if (result == 1)
+ {
+ std::cerr << "Cache doesn't exist." << std::endl;
+ std::cerr << "Create cache with --update-cache" << std::endl;
+ }
+ else if (result == 3)
+ {
+ std::cerr << "Cache is too old." << std::endl;
+ std::cerr << "Update cache with --update-cache or use bigger --cache-valid" << std::endl;
+ }
+ else if (result == 5)
+ {
+ std::cerr << "Cache version doesn't match current version." << std::endl;
+ std::cerr << "Update cache with --update-cache" << std::endl;
+ }
+ return 1;
+ }
+ }
+
+ if (gameItems.empty())
+ this->getGameList();
+
+ if (!gameItems.empty())
+ {
+ for (unsigned int i = 0; i < gameItems.size(); ++i)
+ {
+ gameItemQueue.push(gameItems[i]);
+ }
+
+ // Create threads
+ unsigned int threads = std::min(Globals::globalConfig.iInfoThreads, static_cast<unsigned int>(gameItemQueue.size()));
+ std::vector<std::thread> vThreads;
+ for (unsigned int i = 0; i < threads; ++i)
+ {
+ DownloadInfo dlInfo;
+ dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+ vDownloadInfo.push_back(dlInfo);
+ vThreads.push_back(std::thread(Downloader::getGameDetailsThread, Globals::globalConfig, i));
+ }
+
+ unsigned int dl_status = DLSTATUS_NOTSTARTED;
+ while (dl_status != DLSTATUS_FINISHED)
+ {
+ dl_status = DLSTATUS_NOTSTARTED;
+
+ // Print progress information once per 100ms
+ std::this_thread::sleep_for(std::chrono::milliseconds(Globals::globalConfig.iProgressInterval));
+ std::cerr << "\033[J\r" << std::flush; // Clear screen from the current line down to the bottom of the screen
+
+ // Print messages from message queue first
+ Message msg;
+ while (msgQueue.try_pop(msg))
+ {
+ std::cerr << msg.getFormattedString(Globals::globalConfig.bColor, true) << std::endl;
+ if (Globals::globalConfig.bReport)
+ {
+ this->report_ofs << msg.getTimestampString() << ": " << msg.getMessage() << std::endl;
+ }
+ }
+
+ for (unsigned int i = 0; i < vDownloadInfo.size(); ++i)
+ {
+ unsigned int status = vDownloadInfo[i].getStatus();
+ dl_status |= status;
+ }
+
+ std::cerr << "Getting game info " << (gameItems.size() - gameItemQueue.size()) << " / " << gameItems.size() << std::endl;
+
+ if (dl_status != DLSTATUS_FINISHED)
+ {
+ std::cerr << "\033[1A\r" << std::flush; // Move cursor up by 1 row
+ }
+ }
+
+ // Join threads
+ for (unsigned int i = 0; i < vThreads.size(); ++i)
+ vThreads[i].join();
+
+ vThreads.clear();
+ vDownloadInfo.clear();
+
+ gameDetails details;
+ while (gameDetailsQueue.try_pop(details))
+ {
+ this->games.push_back(details);
+ }
+ std::sort(this->games.begin(), this->games.end(), [](const gameDetails& i, const gameDetails& j) -> bool { return i.gamename < j.gamename; });
+ }
+
+ return 0;
+}
+
+int Downloader::listGames()
+{
+ if (Globals::globalConfig.bListDetails) // Detailed list
+ {
+ if (this->games.empty()) {
+ int res = this->getGameDetails();
+ if (res > 0)
+ return res;
+ }
+
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ std::cout << "gamename: " << games[i].gamename << std::endl
+ << "product id: " << games[i].product_id << std::endl
+ << "title: " << games[i].title << std::endl
+ << "icon: " << games[i].icon << std::endl;
+ if (!games[i].serials.empty())
+ std::cout << "serials:" << std::endl << games[i].serials << std::endl;
+
+ // List installers
+ if (Globals::globalConfig.dlConf.bInstallers && !games[i].installers.empty())
+ {
+ std::cout << "installers: " << std::endl;
+ for (unsigned int j = 0; j < games[i].installers.size(); ++j)
+ {
+ std::string filepath = games[i].installers[j].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES);
+
+ std::cout << "\tid: " << games[i].installers[j].id << std::endl
+ << "\tname: " << games[i].installers[j].name << std::endl
+ << "\tpath: " << games[i].installers[j].path << std::endl
+ << "\tsize: " << games[i].installers[j].size << std::endl
+ << "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl
+ << "\tlanguage: " << languages << std::endl
+ << std::endl;
+ }
+ }
+ // List extras
+ if (Globals::globalConfig.dlConf.bExtras && !games[i].extras.empty())
+ {
+ std::cout << "extras: " << std::endl;
+ for (unsigned int j = 0; j < games[i].extras.size(); ++j)
+ {
+ std::string filepath = games[i].extras[j].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tid: " << games[i].extras[j].id << std::endl
+ << "\tname: " << games[i].extras[j].name << std::endl
+ << "\tpath: " << games[i].extras[j].path << std::endl
+ << "\tsize: " << games[i].extras[j].size << std::endl
+ << std::endl;
+ }
+ }
+ // List patches
+ if (Globals::globalConfig.dlConf.bPatches && !games[i].patches.empty())
+ {
+ std::cout << "patches: " << std::endl;
+ for (unsigned int j = 0; j < games[i].patches.size(); ++j)
+ {
+ std::string filepath = games[i].patches[j].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::string languages = Util::getOptionNameString(games[i].patches[j].language, GlobalConstants::LANGUAGES);
+
+ std::cout << "\tid: " << games[i].patches[j].id << std::endl
+ << "\tname: " << games[i].patches[j].name << std::endl
+ << "\tpath: " << games[i].patches[j].path << std::endl
+ << "\tsize: " << games[i].patches[j].size << std::endl
+ << "\tupdated: " << (games[i].patches[j].updated ? "True" : "False") << std::endl
+ << "\tlanguage: " << languages << std::endl
+ << std::endl;
+ }
+ }
+ // List language packs
+ if (Globals::globalConfig.dlConf.bLanguagePacks && !games[i].languagepacks.empty())
+ {
+ std::cout << "language packs: " << std::endl;
+ for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j)
+ {
+ std::string filepath = games[i].languagepacks[j].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tid: " << games[i].languagepacks[j].id << std::endl
+ << "\tname: " << games[i].languagepacks[j].name << std::endl
+ << "\tpath: " << games[i].languagepacks[j].path << std::endl
+ << "\tsize: " << games[i].languagepacks[j].size << std::endl
+ << std::endl;
+ }
+ }
+ if (Globals::globalConfig.dlConf.bDLC && !games[i].dlcs.empty())
+ {
+ std::cout << "DLCs: " << std::endl;
+ for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
+ {
+ if (!games[i].dlcs[j].serials.empty())
+ {
+ std::cout << "\tDLC gamename: " << games[i].dlcs[j].gamename << std::endl
+ << "\tserials:" << games[i].dlcs[j].serials << std::endl;
+ }
+
+ for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k)
+ {
+ std::string filepath = games[i].dlcs[j].installers[k].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+ << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
+ << "\tid: " << games[i].dlcs[j].installers[k].id << std::endl
+ << "\tname: " << games[i].dlcs[j].installers[k].name << std::endl
+ << "\tpath: " << games[i].dlcs[j].installers[k].path << std::endl
+ << "\tsize: " << games[i].dlcs[j].installers[k].size << std::endl
+ << "\tupdated: " << (games[i].dlcs[j].installers[k].updated ? "True" : "False") << std::endl
+ << std::endl;
+ }
+ for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k)
+ {
+ std::string filepath = games[i].dlcs[j].patches[k].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+ << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
+ << "\tid: " << games[i].dlcs[j].patches[k].id << std::endl
+ << "\tname: " << games[i].dlcs[j].patches[k].name << std::endl
+ << "\tpath: " << games[i].dlcs[j].patches[k].path << std::endl
+ << "\tsize: " << games[i].dlcs[j].patches[k].size << std::endl
+ << std::endl;
+ }
+ for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k)
+ {
+ std::string filepath = games[i].dlcs[j].extras[k].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+ << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
+ << "\tid: " << games[i].dlcs[j].extras[k].id << std::endl
+ << "\tname: " << games[i].dlcs[j].extras[k].name << std::endl
+ << "\tpath: " << games[i].dlcs[j].extras[k].path << std::endl
+ << "\tsize: " << games[i].dlcs[j].extras[k].size << std::endl
+ << std::endl;
+ }
+ for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k)
+ {
+ std::string filepath = games[i].dlcs[j].languagepacks[k].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl
+ << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl
+ << "\tid: " << games[i].dlcs[j].languagepacks[k].id << std::endl
+ << "\tname: " << games[i].dlcs[j].languagepacks[k].name << std::endl
+ << "\tpath: " << games[i].dlcs[j].languagepacks[k].path << std::endl
+ << "\tsize: " << games[i].dlcs[j].languagepacks[k].size << std::endl
+ << std::endl;
+ }
+ }
+ }
+ }
+ }
+ else
+ { // List game names
+ if (gameItems.empty())
+ this->getGameList();
+
+ for (unsigned int i = 0; i < gameItems.size(); ++i)
+ {
+ std::string gamename = gameItems[i].name;
+ if (gameItems[i].updates > 0)
+ {
+ gamename += " [" + std::to_string(gameItems[i].updates) + "]";
+ if (Globals::globalConfig.bColor)
+ gamename = "\033[32m" + gamename + "\033[0m";
+ }
+ std::cout << gamename << std::endl;
+ for (unsigned int j = 0; j < gameItems[i].dlcnames.size(); ++j)
+ std::cout << "+> " << gameItems[i].dlcnames[j] << std::endl;
+ }
+ }
+
+ return 0;
+}
+
+void Downloader::repair()
+{
+ if (this->games.empty())
+ this->getGameDetails();
+
+ // Create a vector containing all game files
+ std::vector<gameFile> vGameFiles;
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ std::vector<gameFile> vec = games[i].getGameFileVector();
+ vGameFiles.insert(std::end(vGameFiles), std::begin(vec), std::end(vec));
+ }
+
+ for (unsigned int i = 0; i < vGameFiles.size(); ++i)
+ {
+ gameSpecificConfig conf;
+ conf.dlConf = Globals::globalConfig.dlConf;
+ conf.dirConf = Globals::globalConfig.dirConf;
+
+ unsigned int type = vGameFiles[i].type;
+ if (!conf.dlConf.bDLC && (type & GFTYPE_DLC))
+ continue;
+ if (!conf.dlConf.bInstallers && (type & GFTYPE_INSTALLER))
+ continue;
+ if (!conf.dlConf.bExtras && (type & GFTYPE_EXTRA))
+ continue;
+ if (!conf.dlConf.bPatches && (type & GFTYPE_PATCH))
+ continue;
+ if (!conf.dlConf.bLanguagePacks && (type & GFTYPE_LANGPACK))
+ continue;
+
+ std::string filepath = vGameFiles[i].getFilepath();
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped blacklisted file " << filepath << std::endl;
+ continue;
+ }
+
+ // Refresh Galaxy login if token is expired
+ if (gogGalaxy->isTokenExpired())
+ {
+ if (!gogGalaxy->refreshLogin())
+ {
+ std::cerr << "Galaxy API failed to refresh login" << std::endl;
+ break;
+ }
+ }
+
+ Json::Value downlinkJson = gogGalaxy->getResponseJson(vGameFiles[i].galaxy_downlink_json_url);
+
+ if (downlinkJson.empty())
+ {
+ std::cerr << "Empty JSON response, skipping file" << std::endl;
+ continue;
+ }
+
+ if (!downlinkJson.isMember("downlink"))
+ {
+ std::cerr << "Invalid JSON response, skipping file" << std::endl;
+ continue;
+ }
+
+ std::string xml_url;
+ if (downlinkJson.isMember("checksum"))
+ if (!downlinkJson["checksum"].empty())
+ xml_url = downlinkJson["checksum"].asString();
+
+ // Get XML data
+ std::string XML = "";
+ if (conf.dlConf.bRemoteXML && !xml_url.empty())
+ XML = gogGalaxy->getResponse(xml_url);
+
+ // Repair
+ bool bUseLocalXML = !conf.dlConf.bRemoteXML;
+
+ // Use local XML data for extras
+ if (XML.empty() && (type & GFTYPE_EXTRA))
+ bUseLocalXML = true;
+
+ if (!XML.empty() || bUseLocalXML)
+ {
+ std::string url = downlinkJson["downlink"].asString();
+
+ std::cout << "Repairing file " << filepath << std::endl;
+ this->repairFile(url, filepath, XML, vGameFiles[i].gamename);
+ std::cout << std::endl;
+ }
+ }
+}
+
+void Downloader::download()
+{
+ if (this->games.empty())
+ this->getGameDetails();
+
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ gameSpecificConfig conf;
+ conf.dlConf = Globals::globalConfig.dlConf;
+ conf.dirConf = Globals::globalConfig.dirConf;
+
+ if (conf.dlConf.bSaveSerials && !games[i].serials.empty())
+ {
+ std::string filepath = games[i].getSerialsFilepath();
+ this->saveSerials(games[i].serials, filepath);
+ }
+
+ if (conf.dlConf.bSaveChangelogs && !games[i].changelog.empty())
+ {
+ std::string filepath = games[i].getChangelogFilepath();
+ this->saveChangelog(games[i].changelog, filepath);
+ }
+
+ if (conf.dlConf.bDLC && !games[i].dlcs.empty())
+ {
+ for (unsigned int j = 0; j < games[i].dlcs.size(); ++j)
+ {
+ if (conf.dlConf.bSaveSerials && !games[i].dlcs[j].serials.empty())
+ {
+ std::string filepath = games[i].dlcs[j].getSerialsFilepath();
+ this->saveSerials(games[i].dlcs[j].serials, filepath);
+ }
+ if (conf.dlConf.bSaveChangelogs && !games[i].dlcs[j].changelog.empty())
+ {
+ std::string filepath = games[i].dlcs[j].getChangelogFilepath();
+ this->saveChangelog(games[i].dlcs[j].changelog, filepath);
+ }
+ }
+ }
+
+ auto vFiles = games[i].getGameFileVectorFiltered(conf.dlConf.iInclude);
+ for (auto gf : vFiles)
+ {
+ dlQueue.push(gf);
+ unsigned long long filesize = 0;
+ try
+ {
+ filesize = std::stoll(gf.size);
+ }
+ catch (std::invalid_argument& e)
+ {
+ filesize = 0;
+ }
+ iTotalRemainingBytes.fetch_add(filesize);
+ }
+
+ }
+
+ if (!dlQueue.empty())
+ {
+ // Limit thread count to number of items in download queue
+ unsigned int iThreads = std::min(Globals::globalConfig.iThreads, static_cast<unsigned int>(dlQueue.size()));
+
+ // Create download threads
+ std::vector<std::thread> vThreads;
+ for (unsigned int i = 0; i < iThreads; ++i)
+ {
+ DownloadInfo dlInfo;
+ dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+ vDownloadInfo.push_back(dlInfo);
+ vThreads.push_back(std::thread(Downloader::processDownloadQueue, Globals::globalConfig, i));
+ }
+
+ this->printProgress(dlQueue);
+
+ // Join threads
+ for (unsigned int i = 0; i < vThreads.size(); ++i)
+ vThreads[i].join();
+
+ vThreads.clear();
+ vDownloadInfo.clear();
+ }
+
+ // Create xml data for all files in the queue
+ if (!createXMLQueue.empty())
+ {
+ std::cout << "Starting XML creation" << std::endl;
+ gameFile gf;
+ while (createXMLQueue.try_pop(gf))
+ {
+ std::string xml_directory = Globals::globalConfig.sXMLDirectory + "/" + gf.gamename;
+ Util::createXML(gf.getFilepath(), Globals::globalConfig.iChunkSize, xml_directory);
+ }
+ }
+}
+
+// Download a file, resume if possible
+CURLcode Downloader::downloadFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename)
+{
+ CURLcode res = CURLE_RECV_ERROR; // assume network error
+ bool bResume = false;
+ FILE *outfile;
+ off_t offset=0;
+
+ // Get directory from filepath
+ boost::filesystem::path pathname = filepath;
+ pathname = boost::filesystem::absolute(pathname, boost::filesystem::current_path());
+ std::string directory = pathname.parent_path().string();
+ std::string filenameXML = pathname.filename().string() + ".xml";
+ std::string xml_directory;
+ if (!gamename.empty())
+ xml_directory = Globals::globalConfig.sXMLDirectory + "/" + gamename;
+ else
+ xml_directory = Globals::globalConfig.sXMLDirectory;
+
+ // Using local XML data for version check before resuming
+ boost::filesystem::path local_xml_file;
+ local_xml_file = xml_directory + "/" + filenameXML;
+
+ bool bSameVersion = true; // assume same version
+ bool bLocalXMLExists = boost::filesystem::exists(local_xml_file); // This is additional check to see if remote xml should be saved to speed up future version checks
+
+ if (!xml_data.empty())
+ {
+ std::string localHash = this->getLocalFileHash(filepath, gamename);
+ // Do version check if local hash exists
+ if (!localHash.empty())
+ {
+ tinyxml2::XMLDocument remote_xml;
+ remote_xml.Parse(xml_data.c_str());
+ tinyxml2::XMLElement *fileElemRemote = remote_xml.FirstChildElement("file");
+ if (fileElemRemote)
+ {
+ std::string remoteHash = fileElemRemote->Attribute("md5");
+ if (remoteHash != localHash)
+ bSameVersion = false;
+ }
+ }
+ }
+
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = directory;
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ std::cerr << path << " is not directory" << std::endl;
+ return res;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ return res;
+ }
+ }
+
+ // Check if file exists
+ if ((outfile=fopen(filepath.c_str(), "r"))!=NULL)
+ {
+ if (bSameVersion)
+ {
+ // Check if file is complete so we can skip it instead of resuming
+ if (!xml_data.empty())
+ {
+ off_t filesize_xml;
+ off_t filesize_local = boost::filesystem::file_size(filepath);
+
+ tinyxml2::XMLDocument remote_xml;
+ remote_xml.Parse(xml_data.c_str());
+ tinyxml2::XMLElement *fileElem = remote_xml.FirstChildElement("file");
+ if (fileElem)
+ {
+ std::string total_size = fileElem->Attribute("total_size");
+ try
+ {
+ filesize_xml = std::stoull(total_size);
+ }
+ catch (std::invalid_argument& e)
+ {
+ filesize_xml = 0;
+ }
+ if (filesize_local == filesize_xml)
+ {
+ std::cout << "Skipping complete file: " + filepath << std::endl;
+ fclose(outfile);
+
+ // Save remote XML
+ if (!xml_data.empty())
+ {
+ if ((bLocalXMLExists && (!bSameVersion || Globals::globalConfig.bRepair)) || !bLocalXMLExists)
+ {
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = xml_directory;
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ std::cerr << path << " is not directory" << std::endl;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ }
+ }
+ std::ofstream ofs(local_xml_file.string().c_str());
+ if (ofs)
+ {
+ ofs << xml_data;
+ ofs.close();
+ }
+ else
+ {
+ std::cerr << "Can't create " << local_xml_file.string() << std::endl;
+ }
+ }
+ }
+
+ res = CURLE_OK;
+ return res;
+ }
+ }
+ }
+
+ // File exists, resume
+ if ((outfile = freopen(filepath.c_str(), "r+", outfile))!=NULL )
+ {
+ bResume = true;
+ fseek(outfile, 0, SEEK_END);
+ // use ftello to support large files on 32 bit platforms
+ offset = ftello(outfile);
+ curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM_LARGE, offset);
+ this->resume_position = offset;
+ }
+ else
+ {
+ std::cerr << "Failed to reopen " << filepath << std::endl;
+ return res;
+ }
+ }
+ else
+ { // File exists but is not the same version
+ fclose(outfile);
+ std::cerr << "Remote file is different, renaming local file" << std::endl;
+ std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+ boost::filesystem::path new_name = filepath + date_old; // Rename old file by appending date and ".old" to filename
+ boost::system::error_code ec;
+ boost::filesystem::rename(pathname, new_name, ec); // Rename the file
+ if (ec)
+ {
+ std::cerr << "Failed to rename " << filepath << " to " << new_name.string() << std::endl;
+ std::cerr << "Skipping file" << std::endl;
+ return res;
+ }
+ else
+ {
+ // Create new file
+ if ((outfile=fopen(filepath.c_str(), "w"))!=NULL)
+ {
+ curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, 0); // start downloading from the beginning of file
+ this->resume_position = 0;
+ }
+ else
+ {
+ std::cerr << "Failed to create " << filepath << std::endl;
+ return res;
+ }
+ }
+ }
+ }
+ else
+ {
+ // File doesn't exist, create new file
+ if ((outfile=fopen(filepath.c_str(), "w"))!=NULL)
+ {
+ curl_easy_setopt(curlhandle, CURLOPT_RESUME_FROM, 0); // start downloading from the beginning of file
+ this->resume_position = 0;
+ }
+ else
+ {
+ std::cerr << "Failed to create " << filepath << std::endl;
+ return res;
+ }
+ }
+
+ // Save remote XML
+ if (!xml_data.empty())
+ {
+ if ((bLocalXMLExists && (!bSameVersion || Globals::globalConfig.bRepair)) || !bLocalXMLExists)
+ {
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = xml_directory;
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ std::cerr << path << " is not directory" << std::endl;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ }
+ }
+ std::ofstream ofs(local_xml_file.string().c_str());
+ if (ofs)
+ {
+ ofs << xml_data;
+ ofs.close();
+ }
+ else
+ {
+ std::cerr << "Can't create " << local_xml_file.string() << std::endl;
+ }
+ }
+ }
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
+ curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L);
+ res = this->beginDownload();
+
+ fclose(outfile);
+
+ // Download failed and was not a resume attempt so delete the file
+ if (res != CURLE_OK && res != CURLE_PARTIAL_FILE && !bResume && res != CURLE_OPERATION_TIMEDOUT)
+ {
+ boost::filesystem::path path = filepath;
+ if (boost::filesystem::exists(path))
+ if (!boost::filesystem::remove(path))
+ std::cerr << "Failed to delete " << path << std::endl;
+ }
+
+ if (Globals::globalConfig.bReport)
+ {
+ std::string status = static_cast<std::string>(curl_easy_strerror(res));
+ if (bResume && res == CURLE_RANGE_ERROR) // CURLE_RANGE_ERROR on resume attempts is not an error that user needs to know about
+ status = "No error";
+ std::string report_line = "Downloaded [" + status + "] " + filepath;
+ this->report_ofs << report_line << std::endl;
+ }
+
+ // Retry partially downloaded file
+ // Retry if we aborted the transfer due to low speed limit
+ if ((res == CURLE_PARTIAL_FILE || res == CURLE_OPERATION_TIMEDOUT || res == CURLE_RECV_ERROR) && (this->retries < Globals::globalConfig.iRetries) )
+ {
+ this->retries++;
+
+ std::cerr << std::endl << "Retry " << this->retries << "/" << Globals::globalConfig.iRetries;
+ if (res == CURLE_PARTIAL_FILE)
+ std::cerr << " (partial download)";
+ else if (res == CURLE_OPERATION_TIMEDOUT)
+ std::cerr << " (timeout)";
+ else if (res == CURLE_RECV_ERROR)
+ std::cerr << " (failed receiving network data)";
+ std::cerr << std::endl;
+
+ res = this->downloadFile(url, filepath, xml_data, gamename);
+ }
+ else
+ {
+ this->retries = 0; // Reset retries counter
+ // Set timestamp for downloaded file to same value as file on server
+ long filetime = -1;
+ CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime);
+ if (result == CURLE_OK && filetime >= 0)
+ {
+ std::time_t timestamp = (std::time_t)filetime;
+ boost::filesystem::last_write_time(filepath, timestamp);
+ }
+ }
+ curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L);
+
+ return res;
+}
+
+// Repair file
+int Downloader::repairFile(const std::string& url, const std::string& filepath, const std::string& xml_data, const std::string& gamename)
+{
+ int res = 0;
+ FILE *outfile;
+ off_t offset=0, from_offset, to_offset, filesize;
+ std::string filehash;
+ int chunks;
+ std::vector<off_t> chunk_from, chunk_to;
+ std::vector<std::string> chunk_hash;
+ bool bParsingFailed = false;
+
+ // Get filename
+ boost::filesystem::path pathname = filepath;
+ std::string filename = pathname.filename().string();
+ std::string xml_directory;
+ if (!gamename.empty())
+ xml_directory = Globals::globalConfig.sXMLDirectory + "/" + gamename;
+ else
+ xml_directory = Globals::globalConfig.sXMLDirectory;
+ std::string xml_file = xml_directory + "/" + filename + ".xml";
+ bool bFileExists = boost::filesystem::exists(pathname);
+ bool bLocalXMLExists = boost::filesystem::exists(xml_file);
+
+ tinyxml2::XMLDocument xml;
+ if (!xml_data.empty()) // Parse remote XML data
+ {
+ std::cout << "XML: Using remote file" << std::endl;
+ xml.Parse(xml_data.c_str());
+ }
+ else
+ { // Parse local XML data
+ std::cout << "XML: Using local file" << std::endl;
+ if (!bLocalXMLExists)
+ std::cout << "XML: File doesn't exist (" << xml_file << ")" << std::endl;
+ xml.LoadFile(xml_file.c_str());
+ }
+
+ // Check if file node exists in XML data
+ tinyxml2::XMLElement *fileElem = xml.FirstChildElement("file");
+ if (!fileElem)
+ { // File node doesn't exist
+ std::cout << "XML: Parsing failed / not valid XML" << std::endl;
+ if (Globals::globalConfig.bDownload)
+ bParsingFailed = true;
+ else
+ return res;
+ }
+ else
+ { // File node exists --> valid XML
+ std::cout << "XML: Valid XML" << std::endl;
+ filename = fileElem->Attribute("name");
+ filehash = fileElem->Attribute("md5");
+ std::stringstream(fileElem->Attribute("chunks")) >> chunks;
+ std::stringstream(fileElem->Attribute("total_size")) >> filesize;
+
+ //Iterate through all chunk nodes
+ tinyxml2::XMLElement *chunkElem = fileElem->FirstChildElement("chunk");
+ while (chunkElem)
+ {
+ std::stringstream(chunkElem->Attribute("from")) >> from_offset;
+ std::stringstream(chunkElem->Attribute("to")) >> to_offset;
+ chunk_from.push_back(from_offset);
+ chunk_to.push_back(to_offset);
+ chunk_hash.push_back(chunkElem->GetText());
+ chunkElem = chunkElem->NextSiblingElement("chunk");
+ }
+
+ std::cout << "XML: Parsing finished" << std::endl << std::endl
+ << filename << std::endl
+ << "\tMD5:\t" << filehash << std::endl
+ << "\tChunks:\t" << chunks << std::endl
+ << "\tSize:\t" << filesize << " bytes" << std::endl << std::endl;
+ }
+
+ // No local XML file and parsing failed.
+ if (bParsingFailed && !bLocalXMLExists)
+ {
+ if (Globals::globalConfig.bDownload)
+ {
+ std::cout << "Downloading: " << filepath << std::endl;
+ CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+ std::cout << std::endl;
+ long int response_code = 0;
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ }
+ if (
+ /* File doesn't exist so only accept if everything was OK */
+ (!bFileExists && result == CURLE_OK) ||
+ /* File exists so also accept CURLE_RANGE_ERROR and response code 416 */
+ (bFileExists && (result == CURLE_OK || result == CURLE_RANGE_ERROR || response_code == 416))
+ )
+ {
+ bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data
+
+ if (Globals::globalConfig.dlConf.bAutomaticXMLCreation && !bLocalXMLExists)
+ {
+ std::cout << "Starting automatic XML creation" << std::endl;
+ Util::createXML(filepath, Globals::globalConfig.iChunkSize, xml_directory);
+ }
+ res = 1;
+ }
+ }
+ else
+ {
+ std::cout << "Can't repair file." << std::endl;
+ }
+ return res;
+ }
+
+ // Check if file exists
+ if (bFileExists)
+ {
+ // File exists
+ if ((outfile = fopen(filepath.c_str(), "r+"))!=NULL )
+ {
+ fseek(outfile, 0, SEEK_END);
+ // use ftello to support large files on 32 bit platforms
+ offset = ftello(outfile);
+ }
+ else
+ {
+ std::cout << "Failed to open " << filepath << std::endl;
+ return res;
+ }
+ }
+ else
+ {
+ std::cout << "File doesn't exist " << filepath << std::endl;
+ if (Globals::globalConfig.bDownload)
+ {
+ std::cout << "Downloading: " << filepath << std::endl;
+ CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+ std::cout << std::endl;
+ if (result == CURLE_OK)
+ {
+ if (Globals::globalConfig.dlConf.bAutomaticXMLCreation && bParsingFailed)
+ {
+ std::cout << "Starting automatic XML creation" << std::endl;
+ Util::createXML(filepath, Globals::globalConfig.iChunkSize, xml_directory);
+ }
+ res = 1;
+ }
+ }
+ return res;
+ }
+
+ // check if file sizes match
+ if (offset != filesize)
+ {
+ std::cout << "Filesizes don't match" << std::endl
+ << "Incomplete download or different version" << std::endl;
+ fclose(outfile);
+ if (Globals::globalConfig.bDownload)
+ {
+ std::cout << "Redownloading file" << std::endl;
+
+ std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+ boost::filesystem::path new_name = filepath + date_old; // Rename old file by appending date and ".old" to filename
+ std::cout << "Renaming old file to " << new_name.string() << std::endl;
+ boost::system::error_code ec;
+ boost::filesystem::rename(pathname, new_name, ec); // Rename the file
+ if (ec)
+ {
+ std::cout << "Failed to rename " << filepath << " to " << new_name.string() << std::endl;
+ std::cout << "Skipping file" << std::endl;
+ res = 0;
+ }
+ else
+ {
+ if (bLocalXMLExists)
+ {
+ std::cout << "Deleting old XML data" << std::endl;
+ boost::filesystem::remove(xml_file, ec); // Delete old XML data
+ if (ec)
+ {
+ std::cout << "Failed to delete " << xml_file << std::endl;
+ }
+ }
+
+ CURLcode result = this->downloadFile(url, filepath, xml_data, gamename);
+ std::cout << std::endl;
+ if (result == CURLE_OK)
+ {
+ bLocalXMLExists = boost::filesystem::exists(xml_file); // Check to see if downloadFile saved XML data
+ if (!bLocalXMLExists)
+ {
+ std::cout << "Starting automatic XML creation" << std::endl;
+ Util::createXML(filepath, Globals::globalConfig.iChunkSize, xml_directory);
+ }
+ res = 1;
+ }
+ else
+ {
+ res = 0;
+ }
+ }
+ }
+ return res;
+ }
+
+ // Check all chunks
+ int iChunksRepaired = 0;
+ int iChunkRetryCount = 0;
+ int iChunkRetryLimit = 3;
+ bool bChunkRetryLimitReached = false;
+ for (int i=0; i<chunks; i++)
+ {
+ off_t chunk_begin = chunk_from.at(i);
+ off_t chunk_end = chunk_to.at(i);
+ off_t size=0, chunk_size = chunk_end - chunk_begin + 1;
+ std::string range = std::to_string(chunk_begin) + "-" + std::to_string(chunk_end); // Download range string for curl
+
+ std::cout << "\033[0K\rChunk " << i << " (" << chunk_size << " bytes): ";
+ // use fseeko to support large files on 32 bit platforms
+ fseeko(outfile, chunk_begin, SEEK_SET);
+ unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+ if (chunk == NULL)
+ {
+ std::cout << "Memory error" << std::endl;
+ fclose(outfile);
+ return res;
+ }
+ size = fread(chunk, 1, chunk_size, outfile);
+ if (size != chunk_size)
+ {
+ std::cout << "Read error" << std::endl;
+ free(chunk);
+ fclose(outfile);
+ return res;
+ }
+ std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+ if (hash != chunk_hash.at(i))
+ {
+ if (bChunkRetryLimitReached)
+ {
+ std::cout << "Failed - chunk retry limit reached\r" << std::flush;
+ free(chunk);
+ res = 0;
+ break;
+ }
+
+ if (iChunkRetryCount < 1)
+ std::cout << "Failed - downloading chunk" << std::endl;
+ else
+ std::cout << "Failed - retrying chunk download" << std::endl;
+ // use fseeko to support large files on 32 bit platforms
+ fseeko(outfile, chunk_begin, SEEK_SET);
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, outfile);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, range.c_str()); //download range
+ curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 1L);
+ this->beginDownload(); //begin chunk download
+ std::cout << std::endl;
+ if (Globals::globalConfig.bReport)
+ iChunksRepaired++;
+ i--; //verify downloaded chunk
+
+ iChunkRetryCount++;
+ if (iChunkRetryCount >= iChunkRetryLimit)
+ {
+ bChunkRetryLimitReached = true;
+ }
+ }
+ else
+ {
+ std::cout << "OK\r" << std::flush;
+ iChunkRetryCount = 0; // reset retry count
+ bChunkRetryLimitReached = false;
+ }
+ free(chunk);
+ res = 1;
+ }
+ std::cout << std::endl;
+ fclose(outfile);
+
+ if (Globals::globalConfig.bReport)
+ {
+ std::string report_line;
+ if (bChunkRetryLimitReached)
+ report_line = "Repair failed: " + filepath;
+ else
+ report_line = "Repaired [" + std::to_string(iChunksRepaired) + "/" + std::to_string(chunks) + "] " + filepath;
+ this->report_ofs << report_line << std::endl;
+ }
+
+ if (bChunkRetryLimitReached)
+ return res;
+
+ // Set timestamp for downloaded file to same value as file on server
+ long filetime = -1;
+ CURLcode result = curl_easy_getinfo(curlhandle, CURLINFO_FILETIME, &filetime);
+ if (result == CURLE_OK && filetime >= 0)
+ {
+ std::time_t timestamp = (std::time_t)filetime;
+ boost::filesystem::last_write_time(filepath, timestamp);
+ }
+ curl_easy_setopt(curlhandle, CURLOPT_FILETIME, 0L);
+
+ return res;
+}
+
+CURLcode Downloader::beginDownload()
+{
+ this->TimeAndSize.clear();
+ this->timer.reset();
+ CURLcode result = curl_easy_perform(curlhandle);
+ this->resume_position = 0;
+ return result;
+}
+
+std::string Downloader::getResponse(const std::string& url)
+{
+ std::ostringstream memory;
+ std::string response;
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+
+ CURLcode result;
+ do
+ {
+ if (Globals::globalConfig.iWait > 0)
+ usleep(Globals::globalConfig.iWait); // Delay the request by specified time
+ result = curl_easy_perform(curlhandle);
+ response = memory.str();
+ memory.str(std::string());
+ }
+ while ((result != CURLE_OK) && response.empty() && (this->retries++ < Globals::globalConfig.iRetries));
+ this->retries = 0; // reset retries counter
+
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 0);
+
+ if (result != CURLE_OK)
+ {
+ std::cout << curl_easy_strerror(result) << std::endl;
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ long int response_code = 0;
+ result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ std::cout << "HTTP ERROR: ";
+ if (result == CURLE_OK)
+ std::cout << response_code << " (" << url << ")" << std::endl;
+ else
+ std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
+ }
+ }
+
+ return response;
+}
+
+int Downloader::progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+ // unused so lets prevent warnings and be more pedantic
+ (void) ulnow;
+ (void) ultotal;
+
+ // on entry: dltotal - how much remains to download till the end of the file (bytes)
+ // dlnow - how much was downloaded from the start of the program (bytes)
+ int bar_length = 26;
+ int min_bar_length = 5;
+ Downloader* downloader = static_cast<Downloader*>(clientp);
+
+ double rate; // average download speed in B/s
+ // trying to get rate and setting to NaN if it fails
+ if (CURLE_OK != curl_easy_getinfo(downloader->curlhandle, CURLINFO_SPEED_DOWNLOAD, &rate))
+ rate = std::numeric_limits<double>::quiet_NaN();
+
+ // (Shmerl): this flag is needed to catch the case before anything was downloaded on resume,
+ // and there is no way to calculate the fraction, so we set to 0 (otherwise it'd be 1).
+ // This is to prevent the progress bar from jumping to 100% and then to lower value.
+ // It's visually better to jump from 0% to higher one.
+ bool starting = ((0 == dlnow) && (0 == dltotal));
+
+ // (Shmerl): DEBUG: strange thing - when resuming a file which is already downloaded, dlnow is correctly 0.0
+ // but dltotal is 389.0! This messes things up in the progress bar not showing the very last bar as full.
+ // enable this debug line to test the problem:
+ //
+ // printf("\r\033[0K dlnow: %0.2f, dltotal: %0.2f\r", dlnow, dltotal); fflush(stdout); return 0;
+ //
+ // For now making a quirky workaround and setting dltotal to 0.0 in that case.
+ // It's probably better to find a real fix.
+ if ((0 == dlnow) && (389 == dltotal)) dltotal = 0;
+
+ // setting full dlwnow and dltotal
+ curl_off_t offset = static_cast<curl_off_t>(downloader->getResumePosition());
+ if (offset>0)
+ {
+ dlnow += offset;
+ dltotal += offset;
+ }
+
+ // Update progress bar every 100ms
+ if (downloader->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal)
+ {
+ downloader->timer.reset();
+ int iTermWidth = Util::getTerminalWidth();
+
+ // 10 second average download speed
+ // Don't use static value of 10 seconds because update interval depends on when and how often progress callback is called
+ downloader->TimeAndSize.push_back(std::make_pair(time(NULL), static_cast<uintmax_t>(dlnow)));
+ if (downloader->TimeAndSize.size() > 100) // 100 * 100ms = 10s
+ {
+ downloader->TimeAndSize.pop_front();
+ time_t time_first = downloader->TimeAndSize.front().first;
+ uintmax_t size_first = downloader->TimeAndSize.front().second;
+ time_t time_last = downloader->TimeAndSize.back().first;
+ uintmax_t size_last = downloader->TimeAndSize.back().second;
+ rate = (size_last - size_first) / static_cast<double>((time_last - time_first));
+ }
+
+ std::string etastring = Util::makeEtaString((dltotal - dlnow), rate);
+
+ // Create progressbar
+ double fraction = starting ? 0.0 : static_cast<double>(dlnow) / static_cast<double>(dltotal);
+
+ std::cout << Util::formattedString("\033[0K\r%3.0f%% ", fraction * 100);
+
+ // Download rate unit conversion
+ std::string rate_unit;
+ if (rate > 1048576) // 1 MB
+ {
+ rate /= 1048576;
+ rate_unit = "MB/s";
+ }
+ else
+ {
+ rate /= 1024;
+ rate_unit = "kB/s";
+ }
+ std::string status_text = Util::formattedString(" %0.2f/%0.2fMB @ %0.2f%s ETA: %s\r", static_cast<double>(dlnow)/1024/1024, static_cast<double>(dltotal)/1024/1024, rate, rate_unit.c_str(), etastring.c_str());
+ int status_text_length = status_text.length() + 6;
+
+ if ((status_text_length + bar_length) > iTermWidth)
+ bar_length -= (status_text_length + bar_length) - iTermWidth;
+
+ // Don't draw progressbar if length is less than min_bar_length
+ if (bar_length >= min_bar_length)
+ downloader->progressbar->draw(bar_length, fraction);
+
+ std::cout << status_text << std::flush;
+ }
+
+ return 0;
+}
+
+size_t Downloader::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) {
+ std::ostringstream *stream = (std::ostringstream*)userp;
+ size_t count = size * nmemb;
+ stream->write(ptr, count);
+ return count;
+}
+
+size_t Downloader::writeData(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ return fwrite(ptr, size, nmemb, stream);
+}
+
+size_t Downloader::readData(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ return fread(ptr, size, nmemb, stream);
+}
+
+uintmax_t Downloader::getResumePosition()
+{
+ return this->resume_position;
+}
+
+std::string Downloader::getSerialsFromJSON(const Json::Value& json)
+{
+ std::ostringstream serials;
+
+ if (!json.isMember("cdKey"))
+ return std::string();
+
+ std::string cdkey = json["cdKey"].asString();
+
+ if (cdkey.empty())
+ return std::string();
+
+ if (cdkey.find("<span>") == std::string::npos)
+ {
+ boost::regex expression("<br\\h*/?>");
+ std::string text = boost::regex_replace(cdkey, expression, "\n");
+ serials << text << std::endl;
+ }
+ else
+ {
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(cdkey);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ for (; it != end; ++it)
+ {
+ std::string tag_text;
+ if (it->tagName() == "span")
+ {
+ for (unsigned int j = 0; j < dom.number_of_children(it); ++j)
+ {
+ tree<htmlcxx::HTML::Node>::iterator span_it = dom.child(it, j);
+ if (!span_it->isTag() && !span_it->isComment())
+ tag_text = span_it->text();
+ }
+ }
+
+ if (!tag_text.empty())
+ {
+ boost::regex expression("^\\h+|\\h+$");
+ std::string text = boost::regex_replace(tag_text, expression, "");
+ if (!text.empty())
+ serials << text << std::endl;
+ }
+ }
+ }
+
+ return serials.str();
+}
+
+std::string Downloader::getChangelogFromJSON(const Json::Value& json)
+{
+ std::string changelog;
+ std::string title = "Changelog";
+
+ if (!json.isMember("changelog"))
+ return std::string();
+
+ changelog = json["changelog"].asString();
+
+ if (changelog.empty())
+ return std::string();
+
+ if (json.isMember("title"))
+ title = title + ": " + json["title"].asString();
+
+ changelog = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>" + title + "</title>\n</head>\n<body>" + changelog + "</body>\n</html>";
+
+ return changelog;
+}
+
+// Linear search. Good thing computers are fast and lists are small.
+static int isPresent(std::vector<gameFile>& list, const boost::filesystem::path& path, Blacklist& blacklist)
+{
+ if(blacklist.isBlacklisted(path.native()))
+ return false;
+ for (unsigned int k = 0; k < list.size(); ++k)
+ if (list[k].getFilepath() == path.native())
+ return true;
+ return false;
+}
+
+void Downloader::checkOrphans()
+{
+ // Always check everything when checking for orphaned files
+ Globals::globalConfig.dlConf.bInstallers = true;
+ Globals::globalConfig.dlConf.bExtras = true;
+ Globals::globalConfig.dlConf.bPatches = true;
+ Globals::globalConfig.dlConf.bLanguagePacks = true;
+ Globals::globalConfig.dlConf.bDLC = true;
+ Globals::globalConfig.dlConf.iInstallerLanguage = Util::getOptionValue("all", GlobalConstants::LANGUAGES);
+ Globals::globalConfig.dlConf.iInstallerPlatform = Util::getOptionValue("all", GlobalConstants::PLATFORMS);
+ Globals::globalConfig.dlConf.vLanguagePriority.clear();
+ Globals::globalConfig.dlConf.vPlatformPriority.clear();
+ Config config = Globals::globalConfig;
+
+ // Checking orphans after download.
+ // Game details have already been retrieved but possibly filtered.
+ // Therefore we need to clear game details and get them again.
+ if (config.bDownload)
+ {
+ this->gameItems.clear();
+ this->games.clear();
+ }
+
+ if (this->games.empty())
+ this->getGameDetails();
+
+ std::vector<std::string> orphans;
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ std::cerr << "Checking for orphaned files " << i+1 << " / " << games.size() << "\r" << std::flush;
+ std::vector<boost::filesystem::path> filepath_vector;
+
+ try
+ {
+ std::vector<boost::filesystem::path> paths;
+ std::vector<unsigned int> platformIds;
+ platformIds.push_back(0);
+ for (unsigned int j = 0; j < GlobalConstants::PLATFORMS.size(); ++j)
+ {
+ platformIds.push_back(GlobalConstants::PLATFORMS[j].id);
+ }
+ for (unsigned int j = 0; j < platformIds.size(); ++j)
+ {
+ std::string directory = config.dirConf.sDirectory + "/" + config.dirConf.sGameSubdir + "/";
+ Util::filepathReplaceReservedStrings(directory, games[i].gamename, platformIds[j]);
+ boost::filesystem::path path (directory);
+ if (boost::filesystem::exists(path))
+ {
+ bool bDuplicate = false;
+ for (unsigned int k = 0; k < paths.size(); ++k)
+ {
+ if (path == paths[k])
+ {
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (!bDuplicate)
+ paths.push_back(path);
+ }
+ }
+
+ for (unsigned int j = 0; j < paths.size(); ++j)
+ {
+ std::size_t pathlen = config.dirConf.sDirectory.length();
+ if (boost::filesystem::exists(paths[j]))
+ {
+ if (boost::filesystem::is_directory(paths[j]))
+ {
+ // Recursively iterate over files in directory
+ boost::filesystem::recursive_directory_iterator end_iter;
+ boost::filesystem::recursive_directory_iterator dir_iter(paths[j]);
+ while (dir_iter != end_iter)
+ {
+ if (boost::filesystem::is_regular_file(dir_iter->status()))
+ {
+ std::string filepath = dir_iter->path().string();
+ if (config.ignorelist.isBlacklisted(filepath.substr(pathlen))) {
+ if (config.bVerbose)
+ std::cerr << "skipped ignorelisted file " << filepath << std::endl;
+ } else {
+ boost::regex expression(config.sOrphanRegex); // Limit to files matching the regex
+ boost::match_results<std::string::const_iterator> what;
+ if (boost::regex_search(filepath, what, expression))
+ filepath_vector.push_back(dir_iter->path());
+ }
+ }
+ dir_iter++;
+ }
+ }
+ }
+ else
+ std::cerr << paths[j] << " does not exist" << std::endl;
+ }
+ }
+ catch (const boost::filesystem::filesystem_error& ex)
+ {
+ std::cout << ex.what() << std::endl;
+ }
+
+ if (!filepath_vector.empty())
+ {
+ for (unsigned int j = 0; j < filepath_vector.size(); ++j)
+ {
+ bool bFoundFile = isPresent(games[i].installers, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].extras, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].patches, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].languagepacks, filepath_vector[j], config.blacklist);
+
+ if (!bFoundFile)
+ { // Check dlcs
+ for (unsigned int k = 0; k < games[i].dlcs.size(); ++k)
+ {
+ bFoundFile = isPresent(games[i].dlcs[k].installers, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].dlcs[k].extras, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].dlcs[k].patches, filepath_vector[j], config.blacklist)
+ || isPresent(games[i].dlcs[k].languagepacks, filepath_vector[j], config.blacklist);
+ if(bFoundFile)
+ break;
+ }
+ }
+ if (!bFoundFile)
+ orphans.push_back(filepath_vector[j].string());
+ }
+ }
+ }
+ std::cout << std::endl;
+
+ if (!orphans.empty())
+ {
+ for (unsigned int i = 0; i < orphans.size(); ++i)
+ {
+ std::cout << orphans[i] << std::endl;
+ }
+ }
+ else
+ {
+ std::cout << "No orphaned files" << std::endl;
+ }
+
+ return;
+}
+
+// Check status of files
+void Downloader::checkStatus()
+{
+ if (this->games.empty())
+ this->getGameDetails();
+
+ // Create a vector containing all game files
+ std::vector<gameFile> vGameFiles;
+ for (unsigned int i = 0; i < games.size(); ++i)
+ {
+ std::vector<gameFile> vec = games[i].getGameFileVector();
+ vGameFiles.insert(std::end(vGameFiles), std::begin(vec), std::end(vec));
+ }
+
+ for (unsigned int i = 0; i < vGameFiles.size(); ++i)
+ {
+ unsigned int type = vGameFiles[i].type;
+ if (!Globals::globalConfig.dlConf.bDLC && (type & GFTYPE_DLC))
+ continue;
+ if (!Globals::globalConfig.dlConf.bInstallers && (type & GFTYPE_INSTALLER))
+ continue;
+ if (!Globals::globalConfig.dlConf.bExtras && (type & GFTYPE_EXTRA))
+ continue;
+ if (!Globals::globalConfig.dlConf.bPatches && (type & GFTYPE_PATCH))
+ continue;
+ if (!Globals::globalConfig.dlConf.bLanguagePacks && (type & GFTYPE_LANGPACK))
+ continue;
+
+ boost::filesystem::path filepath = vGameFiles[i].getFilepath();
+
+ if (Globals::globalConfig.blacklist.isBlacklisted(filepath.native()))
+ continue;
+
+ std::string gamename = vGameFiles[i].gamename;
+
+ if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+ {
+ std::string remoteHash;
+ bool bHashOK = true; // assume hash OK
+ uintmax_t filesize = boost::filesystem::file_size(filepath);
+
+ // GOG only provides xml data for installers, patches and language packs
+ if (type & (GFTYPE_INSTALLER | GFTYPE_PATCH | GFTYPE_LANGPACK))
+ remoteHash = this->getRemoteFileHash(vGameFiles[i]);
+ std::string localHash = this->getLocalFileHash(filepath.string(), gamename);
+
+ if (!remoteHash.empty())
+ {
+ if (remoteHash != localHash)
+ bHashOK = false;
+ else
+ {
+ // Check for incomplete file by comparing the filesizes
+ // Remote hash was saved but download was incomplete and therefore getLocalFileHash returned the same as getRemoteFileHash
+ uintmax_t filesize_xml = 0;
+ boost::filesystem::path path = filepath;
+ boost::filesystem::path local_xml_file;
+ if (!gamename.empty())
+ local_xml_file = Globals::globalConfig.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml";
+ else
+ local_xml_file = Globals::globalConfig.sXMLDirectory + "/" + path.filename().string() + ".xml";
+
+ if (boost::filesystem::exists(local_xml_file))
+ {
+ tinyxml2::XMLDocument local_xml;
+ local_xml.LoadFile(local_xml_file.string().c_str());
+ tinyxml2::XMLElement *fileElemLocal = local_xml.FirstChildElement("file");
+ if (fileElemLocal)
+ {
+ std::string filesize_xml_str = fileElemLocal->Attribute("total_size");
+ filesize_xml = std::stoull(filesize_xml_str);
+ }
+ }
+
+ if (filesize_xml > 0 && filesize_xml != filesize)
+ {
+ localHash = Util::getFileHash(path.string(), RHASH_MD5);
+ std::cout << "FS " << gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl;
+ continue;
+ }
+ }
+ }
+ std::cout << (bHashOK ? "OK " : "MD5 ") << gamename << " " << filepath.filename().string() << " " << filesize << " " << localHash << std::endl;
+ }
+ else
+ {
+ std::cout << "ND " << gamename << " " << filepath.filename().string() << std::endl;
+ }
+ }
+
+ return;
+}
+
+std::string Downloader::getLocalFileHash(const std::string& filepath, const std::string& gamename)
+{
+ std::string localHash;
+ boost::filesystem::path path = filepath;
+ boost::filesystem::path local_xml_file;
+ if (!gamename.empty())
+ local_xml_file = Globals::globalConfig.sXMLDirectory + "/" + gamename + "/" + path.filename().string() + ".xml";
+ else
+ local_xml_file = Globals::globalConfig.sXMLDirectory + "/" + path.filename().string() + ".xml";
+
+ if (Globals::globalConfig.dlConf.bAutomaticXMLCreation && !boost::filesystem::exists(local_xml_file) && boost::filesystem::exists(path))
+ {
+ std::string xml_directory = Globals::globalConfig.sXMLDirectory + "/" + gamename;
+ Util::createXML(filepath, Globals::globalConfig.iChunkSize, xml_directory);
+ }
+
+ localHash = Util::getLocalFileHash(Globals::globalConfig.sXMLDirectory, filepath, gamename);
+
+ return localHash;
+}
+
+std::string Downloader::getRemoteFileHash(const gameFile& gf)
+{
+ std::string remoteHash;
+
+ // Refresh Galaxy login if token is expired
+ if (gogGalaxy->isTokenExpired())
+ {
+ if (!gogGalaxy->refreshLogin())
+ {
+ std::cerr << "Galaxy API failed to refresh login" << std::endl;
+ return remoteHash;
+ }
+ }
+
+ // Get downlink JSON from Galaxy API
+ Json::Value downlinkJson = gogGalaxy->getResponseJson(gf.galaxy_downlink_json_url);
+
+ if (downlinkJson.empty())
+ {
+ std::cerr << "Empty JSON response" << std::endl;
+ return remoteHash;
+ }
+
+ std::string xml_url;
+ if (downlinkJson.isMember("checksum"))
+ if (!downlinkJson["checksum"].empty())
+ xml_url = downlinkJson["checksum"].asString();
+
+ // Get XML data
+ std::string xml;
+ if (!xml_url.empty())
+ xml = gogGalaxy->getResponse(xml_url);
+
+ if (!xml.empty())
+ {
+ tinyxml2::XMLDocument remote_xml;
+ remote_xml.Parse(xml.c_str());
+ tinyxml2::XMLElement *fileElemRemote = remote_xml.FirstChildElement("file");
+ if (fileElemRemote)
+ {
+ remoteHash = fileElemRemote->Attribute("md5");
+ }
+ }
+
+ return remoteHash;
+}
+
+/* Load game details from cache file
+ returns 0 if successful
+ returns 1 if cache file doesn't exist
+ returns 2 if JSON parsing failed
+ returns 3 if cache is too old
+ returns 4 if JSON doesn't contain "games" node
+ returns 5 if cache version doesn't match
+*/
+int Downloader::loadGameDetailsCache()
+{
+ int res = 0;
+ std::string cachepath = Globals::globalConfig.sCacheDirectory + "/gamedetails.json";
+
+ // Make sure file exists
+ boost::filesystem::path path = cachepath;
+ if (!boost::filesystem::exists(path)) {
+ return res = 1;
+ }
+
+ bptime::ptime now = bptime::second_clock::local_time();
+ bptime::ptime cachedate;
+
+ std::ifstream json(cachepath, std::ifstream::binary);
+ Json::Value root;
+ try {
+ json >> root;
+ } catch (const Json::Exception& exc) {
+ std::cout << "Failed to parse cache" << std::endl;
+ std::cout << exc.what() << std::endl;
+ return 2;
+ }
+
+ if (root.isMember("date"))
+ {
+ cachedate = bptime::from_iso_string(root["date"].asString());
+ if ((now - cachedate) > bptime::minutes(Globals::globalConfig.iCacheValid))
+ {
+ // cache is too old
+ return 3;
+ }
+ }
+
+ int iCacheVersion = 0;
+ if (root.isMember("gamedetails-cache-version"))
+ iCacheVersion = root["gamedetails-cache-version"].asInt();
+
+ if (iCacheVersion != GlobalConstants::GAMEDETAILS_CACHE_VERSION)
+ {
+ return 5;
+ }
+
+ if (root.isMember("games"))
+ {
+ this->games = getGameDetailsFromJsonNode(root["games"]);
+ return 0;
+ }
+
+ return 4;
+}
+/* Save game details to cache file
+ returns 0 if successful
+ returns 1 if fails
+*/
+int Downloader::saveGameDetailsCache()
+{
+ int res = 0;
+
+ // Don't try to save cache if we don't have any game details
+ if (this->games.empty())
+ {
+ return 1;
+ }
+
+ std::string cachepath = Globals::globalConfig.sCacheDirectory + "/gamedetails.json";
+
+ Json::Value json;
+
+ json["gamedetails-cache-version"] = GlobalConstants::GAMEDETAILS_CACHE_VERSION;
+ json["version-string"] = Globals::globalConfig.sVersionString;
+ json["version-number"] = Globals::globalConfig.sVersionNumber;
+ json["date"] = bptime::to_iso_string(bptime::second_clock::local_time());
+
+ for (unsigned int i = 0; i < this->games.size(); ++i)
+ json["games"].append(this->games[i].getDetailsAsJson());
+
+ std::ofstream ofs(cachepath);
+ if (!ofs)
+ {
+ res = 1;
+ }
+ else
+ {
+ ofs << json << std::endl;
+ ofs.close();
+ }
+ return res;
+}
+
+std::vector<gameDetails> Downloader::getGameDetailsFromJsonNode(Json::Value root, const int& recursion_level)
+{
+ std::vector<gameDetails> details;
+
+ // If root node is not array and we use root.size() it will return the number of nodes --> limit to 1 "array" node to make sure it is handled properly
+ for (unsigned int i = 0; i < (root.isArray() ? root.size() : 1); ++i)
+ {
+ Json::Value gameDetailsNode = (root.isArray() ? root[i] : root); // This json node can be array or non-array so take that into account
+ gameDetails game;
+ game.gamename = gameDetailsNode["gamename"].asString();
+
+ // DLCs are handled as part of the game so make sure that filtering is done with base game name
+ if (recursion_level == 0) // recursion level is 0 when handling base game
+ {
+ boost::regex expression(Globals::globalConfig.sGameRegex);
+ boost::match_results<std::string::const_iterator> what;
+ if (!boost::regex_search(game.gamename, what, expression)) // Check if name matches the specified regex
+ continue;
+ }
+ game.title = gameDetailsNode["title"].asString();
+ game.icon = gameDetailsNode["icon"].asString();
+ game.serials = gameDetailsNode["serials"].asString();
+ game.changelog = gameDetailsNode["changelog"].asString();
+ game.product_id = gameDetailsNode["product_id"].asString();
+
+ // Make a vector of valid node names to make things easier
+ std::vector<std::string> nodes;
+ nodes.push_back("extras");
+ nodes.push_back("installers");
+ nodes.push_back("patches");
+ nodes.push_back("languagepacks");
+ nodes.push_back("dlcs");
+
+ gameSpecificConfig conf;
+ conf.dlConf = Globals::globalConfig.dlConf;
+ if (Util::getGameSpecificConfig(game.gamename, &conf) > 0)
+ std::cerr << game.gamename << " - Language: " << conf.dlConf.iInstallerLanguage << ", Platform: " << conf.dlConf.iInstallerPlatform << ", DLC: " << (conf.dlConf.bDLC ? "true" : "false") << std::endl;
+
+ for (unsigned int j = 0; j < nodes.size(); ++j)
+ {
+ std::string nodeName = nodes[j];
+ if (gameDetailsNode.isMember(nodeName))
+ {
+ Json::Value fileDetailsNodeVector = gameDetailsNode[nodeName];
+ for (unsigned int index = 0; index < fileDetailsNodeVector.size(); ++index)
+ {
+ Json::Value fileDetailsNode = fileDetailsNodeVector[index];
+ gameFile fileDetails;
+
+ if (nodeName != "dlcs")
+ {
+ fileDetails.updated = fileDetailsNode["updated"].asInt();
+ fileDetails.id = fileDetailsNode["id"].asString();
+ fileDetails.name = fileDetailsNode["name"].asString();
+ fileDetails.path = fileDetailsNode["path"].asString();
+ fileDetails.size = fileDetailsNode["size"].asString();
+ fileDetails.platform = fileDetailsNode["platform"].asUInt();
+ fileDetails.language = fileDetailsNode["language"].asUInt();
+ fileDetails.silent = fileDetailsNode["silent"].asInt();
+ fileDetails.gamename = fileDetailsNode["gamename"].asString();
+ fileDetails.type = fileDetailsNode["type"].asUInt();
+ fileDetails.galaxy_downlink_json_url = fileDetailsNode["galaxy_downlink_json_url"].asString();
+
+ if (nodeName != "extras" && !(fileDetails.platform & conf.dlConf.iInstallerPlatform))
+ continue;
+ if (nodeName != "extras" && !(fileDetails.language & conf.dlConf.iInstallerLanguage))
+ continue;
+ }
+
+ if (nodeName == "extras" && conf.dlConf.bExtras)
+ game.extras.push_back(fileDetails);
+ else if (nodeName == "installers" && conf.dlConf.bInstallers)
+ game.installers.push_back(fileDetails);
+ else if (nodeName == "patches" && conf.dlConf.bPatches)
+ game.patches.push_back(fileDetails);
+ else if (nodeName == "languagepacks" && conf.dlConf.bLanguagePacks)
+ game.languagepacks.push_back(fileDetails);
+ else if (nodeName == "dlcs" && conf.dlConf.bDLC)
+ {
+ std::vector<gameDetails> dlcs = this->getGameDetailsFromJsonNode(fileDetailsNode, recursion_level + 1);
+ game.dlcs.insert(game.dlcs.end(), dlcs.begin(), dlcs.end());
+ }
+ }
+ }
+ }
+ if (!game.extras.empty() || !game.installers.empty() || !game.patches.empty() || !game.languagepacks.empty() || !game.dlcs.empty())
+ {
+ game.filterWithPriorities(conf);
+ details.push_back(game);
+ }
+ }
+ return details;
+}
+
+void Downloader::updateCache()
+{
+ // Make sure that all details get cached
+ Globals::globalConfig.dlConf.bExtras = true;
+ Globals::globalConfig.dlConf.bInstallers = true;
+ Globals::globalConfig.dlConf.bPatches = true;
+ Globals::globalConfig.dlConf.bLanguagePacks = true;
+ Globals::globalConfig.dlConf.bDLC = true;
+ Globals::globalConfig.sGameRegex = ".*";
+ Globals::globalConfig.dlConf.iInstallerLanguage = Util::getOptionValue("all", GlobalConstants::LANGUAGES);
+ Globals::globalConfig.dlConf.iInstallerPlatform = Util::getOptionValue("all", GlobalConstants::PLATFORMS);
+ Globals::globalConfig.dlConf.vLanguagePriority.clear();
+ Globals::globalConfig.dlConf.vPlatformPriority.clear();
+ Globals::globalConfig.sIgnoreDLCCountRegex = ".*"; // Ignore DLC count for all games because GOG doesn't report DLC count correctly
+
+ this->getGameList();
+ this->getGameDetails();
+ if (this->saveGameDetailsCache())
+ std::cout << "Failed to save cache" << std::endl;
+
+ return;
+}
+
+// Save serials to file
+void Downloader::saveSerials(const std::string& serials, const std::string& filepath)
+{
+ bool bFileExists = boost::filesystem::exists(filepath);
+
+ if (bFileExists)
+ return;
+
+ // Get directory from filepath
+ boost::filesystem::path pathname = filepath;
+ std::string directory = pathname.parent_path().string();
+
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = directory;
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ std::cout << path << " is not directory" << std::endl;
+ return;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cout << "Failed to create directory: " << path << std::endl;
+ return;
+ }
+ }
+
+ std::ofstream ofs(filepath);
+ if (ofs)
+ {
+ std::cout << "Saving serials: " << filepath << std::endl;
+ ofs << serials;
+ ofs.close();
+ }
+ else
+ {
+ std::cout << "Failed to create file: " << filepath << std::endl;
+ }
+
+ return;
+}
+
+// Save changelog to file
+void Downloader::saveChangelog(const std::string& changelog, const std::string& filepath)
+{
+ // Get directory from filepath
+ boost::filesystem::path pathname = filepath;
+ std::string directory = pathname.parent_path().string();
+
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = directory;
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ std::cout << path << " is not directory" << std::endl;
+ return;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ std::cout << "Failed to create directory: " << path << std::endl;
+ return;
+ }
+ }
+
+ std::ofstream ofs(filepath);
+ if (ofs)
+ {
+ std::cout << "Saving changelog: " << filepath << std::endl;
+ ofs << changelog;
+ ofs.close();
+ }
+ else
+ {
+ std::cout << "Failed to create file: " << filepath << std::endl;
+ }
+
+ return;
+}
+
+int Downloader::downloadFileWithId(const std::string& fileid_string, const std::string& output_filepath)
+{
+ if (gogGalaxy->isTokenExpired())
+ {
+ if (!gogGalaxy->refreshLogin())
+ {
+ std::cerr << "Galaxy API failed to refresh login" << std::endl;
+ return 1;
+ }
+ }
+
+ DownloadConfig dlConf = Globals::globalConfig.dlConf;
+ dlConf.bInstallers = true;
+ dlConf.bExtras = true;
+ dlConf.bPatches = true;
+ dlConf.bLanguagePacks = true;
+ dlConf.bDLC = true;
+ dlConf.bDuplicateHandler = false; // Disable duplicate handler
+
+ int res = 1;
+ CURLcode result = CURLE_RECV_ERROR; // assume network error
+
+ size_t pos = fileid_string.find("/");
+ if (pos == std::string::npos)
+ {
+ std::cerr << "Invalid file id " << fileid_string << ": could not find separator \"/\"" << std::endl;
+ }
+ else if (!output_filepath.empty() && boost::filesystem::is_directory(output_filepath))
+ {
+ std::cerr << "Failed to create the file " << output_filepath << ": Is a directory" << std::endl;
+ }
+ else
+ {
+ std::string gamename, fileid, url;
+ gamename.assign(fileid_string.begin(), fileid_string.begin()+pos);
+ fileid.assign(fileid_string.begin()+pos+1, fileid_string.end());
+
+ std::string product_id;
+ bool bSelectOK = this->galaxySelectProductIdHelper(gamename, product_id);
+
+ if (!bSelectOK || product_id.empty())
+ {
+ std::cerr << "Failed to get numerical product id" << std::endl;
+ return 1;
+ }
+
+ Json::Value productInfo = gogGalaxy->getProductInfo(product_id);
+ if (productInfo.empty())
+ {
+ std::cerr << "Failed to get product info" << std::endl;
+ return 1;
+ }
+
+ gameDetails gd = gogGalaxy->productInfoJsonToGameDetails(productInfo, dlConf);
+
+ auto vFiles = gd.getGameFileVector();
+ gameFile gf;
+ bool bFoundMatchingFile = false;
+ for (auto f : vFiles)
+ {
+ if (f.id == fileid)
+ {
+ gf = f;
+ bFoundMatchingFile = true;
+ break;
+ }
+ }
+
+ if (!bFoundMatchingFile)
+ {
+ std::cerr << "Failed to find file info (product id: " << product_id << " / file id: " << fileid << ")" << std::endl;
+ return 1;
+ }
+
+ Json::Value downlinkJson = gogGalaxy->getResponseJson(gf.galaxy_downlink_json_url);
+
+ if (downlinkJson.empty())
+ {
+ std::cerr << "Empty JSON response" << std::endl;
+ return 1;
+ }
+
+ if (downlinkJson.isMember("downlink"))
+ {
+ url = downlinkJson["downlink"].asString();
+ }
+ else
+ {
+ std::cerr << "Invalid JSON response" << std::endl;
+ return 1;
+ }
+
+ std::string xml_url;
+ if (downlinkJson.isMember("checksum"))
+ {
+ if (!downlinkJson["checksum"].empty())
+ xml_url = downlinkJson["checksum"].asString();
+ }
+
+ // Get XML data
+ std::string xml_data;
+ if (!xml_url.empty())
+ {
+ xml_data = gogGalaxy->getResponse(xml_url);
+ if (xml_data.empty())
+ {
+ std::cerr << "Failed to get XML data" << std::endl;
+ }
+ }
+
+ std::string filename, filepath;
+ filename = gogGalaxy->getPathFromDownlinkUrl(url, gf.gamename);
+ if (output_filepath.empty())
+ filepath = Util::makeFilepath(Globals::globalConfig.dirConf.sDirectory, filename, gf.gamename);
+ else
+ filepath = output_filepath;
+ std::cout << "Downloading: " << filepath << std::endl;
+ result = this->downloadFile(url, filepath, xml_data, gf.gamename);
+ std::cout << std::endl;
+ }
+
+ if (result == CURLE_OK)
+ res = 0;
+
+ return res;
+}
+
+void Downloader::showWishlist()
+{
+ std::vector<wishlistItem> wishlistItems = gogWebsite->getWishlistItems();
+ for (unsigned int i = 0; i < wishlistItems.size(); ++i)
+ {
+ wishlistItem item = wishlistItems[i];
+ std::string platforms_text = Util::getOptionNameString(item.platform, GlobalConstants::PLATFORMS);
+ std::string tags_text;
+ for (unsigned int j = 0; j < item.tags.size(); ++j)
+ {
+ tags_text += (tags_text.empty() ? "" : ", ")+item.tags[j];
+ }
+ if (!tags_text.empty())
+ tags_text = "[" + tags_text + "]";
+
+ std::string price_text = item.price;
+ if (item.bIsDiscounted)
+ price_text += " (-" + item.discount_percent + " | -" + item.discount + ")";
+
+ std::cout << item.title;
+ if (!tags_text.empty())
+ std::cout << " " << tags_text;
+ std::cout << std::endl;
+ std::cout << "\t" << item.url << std::endl;
+ if (item.platform != 0)
+ std::cout << "\tPlatforms: " << platforms_text << std::endl;
+ if (item.release_date_time != 0)
+ std::cout << "\tRelease date: " << bptime::to_simple_string(bptime::from_time_t(item.release_date_time)) << std::endl;
+ std::cout << "\tPrice: " << price_text << std::endl;
+ if (item.bIsBonusStoreCreditIncluded)
+ std::cout << "\tStore credit: " << item.store_credit << std::endl;
+ std::cout << std::endl;
+ }
+
+ return;
+}
+
+void Downloader::processDownloadQueue(Config conf, const unsigned int& tid)
+{
+ std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+ galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+ if (!galaxy->init())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ delete galaxy;
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ return;
+ }
+ }
+
+ CURL* dlhandle = curl_easy_init();
+ curl_easy_setopt(dlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.curlConf.sUserAgent.c_str());
+ curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1);
+
+ curl_easy_setopt(dlhandle, CURLOPT_CONNECTTIMEOUT, conf.curlConf.iTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(dlhandle, CURLOPT_SSL_VERIFYPEER, conf.curlConf.bVerifyPeer);
+ curl_easy_setopt(dlhandle, CURLOPT_VERBOSE, conf.curlConf.bVerbose);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+ curl_easy_setopt(dlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, conf.curlConf.iDownloadRate);
+ curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_TIME, conf.curlConf.iLowSpeedTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_LIMIT, conf.curlConf.iLowSpeedTimeoutRate);
+
+ if (!conf.curlConf.sCACertPath.empty())
+ curl_easy_setopt(dlhandle, CURLOPT_CAINFO, conf.curlConf.sCACertPath.c_str());
+
+ xferInfo xferinfo;
+ xferinfo.tid = tid;
+ xferinfo.curlhandle = dlhandle;
+
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
+
+ gameFile gf;
+ while (dlQueue.try_pop(gf))
+ {
+ CURLcode result = CURLE_RECV_ERROR; // assume network error
+ int iRetryCount = 0;
+ off_t iResumePosition = 0;
+
+ vDownloadInfo[tid].setStatus(DLSTATUS_STARTING);
+
+ unsigned long long filesize = 0;
+ try
+ {
+ filesize = std::stoll(gf.size);
+ }
+ catch (std::invalid_argument& e)
+ {
+ filesize = 0;
+ }
+ iTotalRemainingBytes.fetch_sub(filesize);
+
+ // Get directory from filepath
+ boost::filesystem::path filepath = gf.getFilepath();
+ filepath = boost::filesystem::absolute(filepath, boost::filesystem::current_path());
+ boost::filesystem::path directory = filepath.parent_path();
+
+ // Skip blacklisted files
+ if (conf.blacklist.isBlacklisted(filepath.string()))
+ {
+ msgQueue.push(Message("Blacklisted file: " + filepath.string(), MSGTYPE_INFO, msg_prefix));
+ continue;
+ }
+
+ std::string filenameXML = filepath.filename().string() + ".xml";
+ std::string xml_directory = conf.sXMLDirectory + "/" + gf.gamename;
+ boost::filesystem::path local_xml_file = xml_directory + "/" + filenameXML;
+
+ vDownloadInfo[tid].setFilename(filepath.filename().string());
+ msgQueue.push(Message("Begin download: " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
+
+ // Check that directory exists and create subdirectories
+ mtx_create_directories.lock(); // Use mutex to avoid possible race conditions
+ if (boost::filesystem::exists(directory))
+ {
+ if (!boost::filesystem::is_directory(directory))
+ {
+ mtx_create_directories.unlock();
+ msgQueue.push(Message(directory.string() + " is not directory, skipping file (" + filepath.filename().string() + ")", MSGTYPE_WARNING, msg_prefix));
+ continue;
+ }
+ else
+ {
+ mtx_create_directories.unlock();
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(directory))
+ {
+ mtx_create_directories.unlock();
+ msgQueue.push(Message("Failed to create directory (" + directory.string() + "), skipping file (" + filepath.filename().string() + ")", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ else
+ {
+ mtx_create_directories.unlock();
+ }
+ }
+
+ bool bSameVersion = true; // assume same version
+ bool bLocalXMLExists = boost::filesystem::exists(local_xml_file); // This is additional check to see if remote xml should be saved to speed up future version checks
+
+ // Refresh Galaxy login if token is expired
+ if (galaxy->isTokenExpired())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ delete galaxy;
+ return;
+ }
+ }
+
+ // Get downlink JSON from Galaxy API
+ Json::Value downlinkJson = galaxy->getResponseJson(gf.galaxy_downlink_json_url);
+
+ if (downlinkJson.empty())
+ {
+ msgQueue.push(Message("Empty JSON response, skipping file", MSGTYPE_WARNING, msg_prefix));
+ continue;
+ }
+
+ if (!downlinkJson.isMember("downlink"))
+ {
+ msgQueue.push(Message("Invalid JSON response, skipping file", MSGTYPE_WARNING, msg_prefix));
+ continue;
+ }
+
+ std::string xml;
+ if (gf.type & (GFTYPE_INSTALLER | GFTYPE_PATCH) && conf.dlConf.bRemoteXML)
+ {
+ std::string xml_url;
+ if (downlinkJson.isMember("checksum"))
+ if (!downlinkJson["checksum"].empty())
+ xml_url = downlinkJson["checksum"].asString();
+
+ // Get XML data
+ if (conf.dlConf.bRemoteXML && !xml_url.empty())
+ xml = galaxy->getResponse(xml_url);
+
+ if (!xml.empty())
+ {
+ std::string localHash = Util::getLocalFileHash(conf.sXMLDirectory, filepath.string(), gf.gamename);
+ // Do version check if local hash exists
+ if (!localHash.empty())
+ {
+ tinyxml2::XMLDocument remote_xml;
+ remote_xml.Parse(xml.c_str());
+ tinyxml2::XMLElement *fileElem = remote_xml.FirstChildElement("file");
+ if (fileElem)
+ {
+ std::string remoteHash = fileElem->Attribute("md5");
+ if (remoteHash != localHash)
+ bSameVersion = false;
+ }
+ }
+ }
+ }
+
+ bool bIsComplete = false;
+ bool bResume = false;
+ if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+ {
+ if (bSameVersion)
+ {
+ bResume = true;
+
+ // Check if file is complete so we can skip it instead of resuming
+ if (!xml.empty())
+ {
+ off_t filesize_xml;
+ off_t filesize_local = boost::filesystem::file_size(filepath);
+
+ tinyxml2::XMLDocument remote_xml;
+ remote_xml.Parse(xml.c_str());
+ tinyxml2::XMLElement *fileElem = remote_xml.FirstChildElement("file");
+ if (fileElem)
+ {
+ std::string total_size = fileElem->Attribute("total_size");
+ try
+ {
+ filesize_xml = std::stoull(total_size);
+ }
+ catch (std::invalid_argument& e)
+ {
+ filesize_xml = 0;
+ }
+ if (filesize_local == filesize_xml)
+ {
+ msgQueue.push(Message("Skipping complete file: " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
+ bIsComplete = true; // Set to true so we can skip after saving xml data
+ }
+ }
+ }
+ }
+ else
+ {
+ msgQueue.push(Message("Remote file is different, renaming local file", MSGTYPE_INFO, msg_prefix));
+ std::string date_old = "." + bptime::to_iso_string(bptime::second_clock::local_time()) + ".old";
+ boost::filesystem::path new_name = filepath.string() + date_old; // Rename old file by appending date and ".old" to filename
+ boost::system::error_code ec;
+ boost::filesystem::rename(filepath, new_name, ec); // Rename the file
+ if (ec)
+ {
+ msgQueue.push(Message("Failed to rename " + filepath.string() + " to " + new_name.string() + " - Skipping file", MSGTYPE_WARNING, msg_prefix));
+ continue;
+ }
+ }
+ }
+
+ // Save remote XML
+ if (!xml.empty())
+ {
+ if ((bLocalXMLExists && !bSameVersion) || !bLocalXMLExists)
+ {
+ // Check that directory exists and create subdirectories
+ boost::filesystem::path path = xml_directory;
+ mtx_create_directories.lock(); // Use mutex to avoid race conditions
+ if (boost::filesystem::exists(path))
+ {
+ if (!boost::filesystem::is_directory(path))
+ {
+ msgQueue.push(Message(path.string() + " is not directory", MSGTYPE_WARNING, msg_prefix));
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(path))
+ {
+ msgQueue.push(Message("Failed to create directory: " + path.string(), MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ mtx_create_directories.unlock();
+ std::ofstream ofs(local_xml_file.string().c_str());
+ if (ofs)
+ {
+ ofs << xml;
+ ofs.close();
+ }
+ else
+ {
+ msgQueue.push(Message("Can't create " + local_xml_file.string(), MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ }
+
+ // File was complete and we have saved xml data so we can skip it
+ if (bIsComplete)
+ continue;
+
+ std::string url = downlinkJson["downlink"].asString();
+ curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
+ long int response_code = 0;
+ bool bShouldRetry = false;
+ do
+ {
+ if (conf.iWait > 0)
+ usleep(conf.iWait); // Wait before continuing
+
+ response_code = 0; // Make sure that response code is reset
+
+ if (iRetryCount != 0)
+ msgQueue.push(Message("Retry " + std::to_string(iRetryCount) + "/" + std::to_string(conf.iRetries) + ": " + filepath.filename().string(), MSGTYPE_INFO, msg_prefix));
+
+ FILE* outfile;
+ // File exists, resume
+ if (bResume)
+ {
+ iResumePosition = boost::filesystem::file_size(filepath);
+ if ((outfile=fopen(filepath.string().c_str(), "r+"))!=NULL)
+ {
+ fseek(outfile, 0, SEEK_END);
+ curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, iResumePosition);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+ }
+ else
+ {
+ msgQueue.push(Message("Failed to open " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
+ break;
+ }
+ }
+ else // File doesn't exist, create new file
+ {
+ if ((outfile=fopen(filepath.string().c_str(), "w"))!=NULL)
+ {
+ curl_easy_setopt(dlhandle, CURLOPT_RESUME_FROM_LARGE, 0); // start downloading from the beginning of file
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+ }
+ else
+ {
+ msgQueue.push(Message("Failed to create " + filepath.string(), MSGTYPE_ERROR, msg_prefix));
+ break;
+ }
+ }
+
+ xferinfo.offset = iResumePosition;
+ xferinfo.timer.reset();
+ xferinfo.TimeAndSize.clear();
+ result = curl_easy_perform(dlhandle);
+ fclose(outfile);
+
+ switch (result)
+ {
+ // Retry on these errors
+ case CURLE_PARTIAL_FILE:
+ case CURLE_OPERATION_TIMEDOUT:
+ case CURLE_RECV_ERROR:
+ bShouldRetry = true;
+ break;
+ // Retry on CURLE_HTTP_RETURNED_ERROR if response code is not "416 Range Not Satisfiable"
+ case CURLE_HTTP_RETURNED_ERROR:
+ curl_easy_getinfo(dlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ if (response_code == 416)
+ bShouldRetry = false;
+ else
+ bShouldRetry = true;
+ break;
+ default:
+ bShouldRetry = false;
+ break;
+ }
+
+ if (bShouldRetry)
+ {
+ iRetryCount++;
+ if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+ bResume = true;
+ }
+
+ } while (bShouldRetry && (iRetryCount <= conf.iRetries));
+
+ if (result == CURLE_OK || result == CURLE_RANGE_ERROR || (result == CURLE_HTTP_RETURNED_ERROR && response_code == 416))
+ {
+ // Set timestamp for downloaded file to same value as file on server
+ long filetime = -1;
+ CURLcode res = curl_easy_getinfo(dlhandle, CURLINFO_FILETIME, &filetime);
+ if (res == CURLE_OK && filetime >= 0)
+ {
+ std::time_t timestamp = (std::time_t)filetime;
+ boost::filesystem::last_write_time(filepath, timestamp);
+ }
+
+ // Average download speed
+ std::ostringstream dlrate_avg;
+ std::string rate_unit;
+ progressInfo progress_info = vDownloadInfo[tid].getProgressInfo();
+ if (progress_info.rate_avg > 1048576) // 1 MB
+ {
+ progress_info.rate_avg /= 1048576;
+ rate_unit = "MB/s";
+ }
+ else
+ {
+ progress_info.rate_avg /= 1024;
+ rate_unit = "kB/s";
+ }
+ dlrate_avg << std::setprecision(2) << std::fixed << progress_info.rate_avg << rate_unit;
+
+ msgQueue.push(Message("Download complete: " + filepath.filename().string() + " (@ " + dlrate_avg.str() + ")", MSGTYPE_SUCCESS, msg_prefix));
+ }
+ else
+ {
+ std::string msg = "Download complete (" + static_cast<std::string>(curl_easy_strerror(result));
+ if (response_code > 0)
+ msg += " (" + std::to_string(response_code) + ")";
+ msg += "): " + filepath.filename().string();
+ msgQueue.push(Message(msg, MSGTYPE_WARNING, msg_prefix));
+
+ // Delete the file if download failed and was not a resume attempt or the result is zero length file
+ if (boost::filesystem::exists(filepath) && boost::filesystem::is_regular_file(filepath))
+ {
+ if ((result != CURLE_PARTIAL_FILE && !bResume && result != CURLE_OPERATION_TIMEDOUT) || boost::filesystem::file_size(filepath) == 0)
+ {
+ if (!boost::filesystem::remove(filepath))
+ msgQueue.push(Message("Failed to delete " + filepath.filename().string(), MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ }
+
+ // Automatic xml creation
+ if (conf.dlConf.bAutomaticXMLCreation)
+ {
+ if (result == CURLE_OK)
+ {
+ if ((gf.type & GFTYPE_EXTRA) || (conf.dlConf.bRemoteXML && !bLocalXMLExists && xml.empty()))
+ createXMLQueue.push(gf);
+ }
+ }
+ }
+
+ curl_easy_cleanup(dlhandle);
+ delete galaxy;
+
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ msgQueue.push(Message("Finished all tasks", MSGTYPE_INFO, msg_prefix));
+
+ return;
+}
+
+int Downloader::progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+ // unused so lets prevent warnings and be more pedantic
+ (void) ulnow;
+ (void) ultotal;
+
+ xferInfo* xferinfo = static_cast<xferInfo*>(clientp);
+
+ // Update progress info every 100ms
+ if (xferinfo->timer.getTimeBetweenUpdates()>=100 || dlnow == dltotal)
+ {
+ xferinfo->timer.reset();
+ progressInfo info;
+ info.dlnow = dlnow;
+ info.dltotal = dltotal;
+
+ // trying to get rate and setting to NaN if it fails
+ if (CURLE_OK != curl_easy_getinfo(xferinfo->curlhandle, CURLINFO_SPEED_DOWNLOAD, &info.rate_avg))
+ info.rate_avg = std::numeric_limits<double>::quiet_NaN();
+
+ // setting full dlwnow and dltotal
+ if (xferinfo->offset > 0)
+ {
+ info.dlnow += xferinfo->offset;
+ info.dltotal += xferinfo->offset;
+ }
+
+ // 10 second average download speed
+ // Don't use static value of 10 seconds because update interval depends on when and how often progress callback is called
+ xferinfo->TimeAndSize.push_back(std::make_pair(time(NULL), static_cast<uintmax_t>(info.dlnow)));
+ if (xferinfo->TimeAndSize.size() > 100) // 100 * 100ms = 10s
+ {
+ xferinfo->TimeAndSize.pop_front();
+ time_t time_first = xferinfo->TimeAndSize.front().first;
+ uintmax_t size_first = xferinfo->TimeAndSize.front().second;
+ time_t time_last = xferinfo->TimeAndSize.back().first;
+ uintmax_t size_last = xferinfo->TimeAndSize.back().second;
+ info.rate = (size_last - size_first) / static_cast<double>((time_last - time_first));
+ }
+ else
+ {
+ info.rate = info.rate_avg;
+ }
+
+ vDownloadInfo[xferinfo->tid].setProgressInfo(info);
+ vDownloadInfo[xferinfo->tid].setStatus(DLSTATUS_RUNNING);
+ }
+
+ return 0;
+}
+
+template <typename T> void Downloader::printProgress(const ThreadSafeQueue<T>& download_queue)
+{
+ // Print progress information until all threads have finished their tasks
+ ProgressBar bar(Globals::globalConfig.bUnicode, Globals::globalConfig.bColor);
+ unsigned int dl_status = DLSTATUS_NOTSTARTED;
+ while (dl_status != DLSTATUS_FINISHED)
+ {
+ dl_status = DLSTATUS_NOTSTARTED;
+
+ // Print progress information once per 100ms
+ std::this_thread::sleep_for(std::chrono::milliseconds(Globals::globalConfig.iProgressInterval));
+ std::cout << "\033[J\r" << std::flush; // Clear screen from the current line down to the bottom of the screen
+
+ // Print messages from message queue first
+ Message msg;
+ while (msgQueue.try_pop(msg))
+ {
+ std::cout << msg.getFormattedString(Globals::globalConfig.bColor, true) << std::endl;
+ if (Globals::globalConfig.bReport)
+ {
+ this->report_ofs << msg.getTimestampString() << ": " << msg.getMessage() << std::endl;
+ }
+ }
+
+ int iTermWidth = Util::getTerminalWidth();
+ double total_rate = 0;
+ bptime::time_duration eta_total_seconds;
+
+ // Create progress info text for all download threads
+ std::vector<std::string> vProgressText;
+ for (unsigned int i = 0; i < vDownloadInfo.size(); ++i)
+ {
+ std::string progress_text;
+ int bar_length = 26;
+ int min_bar_length = 5;
+
+ unsigned int status = vDownloadInfo[i].getStatus();
+ dl_status |= status;
+
+ if (status == DLSTATUS_FINISHED)
+ {
+ vProgressText.push_back("#" + std::to_string(i) + ": Finished");
+ continue;
+ }
+
+ std::string filename = vDownloadInfo[i].getFilename();
+ progressInfo progress_info = vDownloadInfo[i].getProgressInfo();
+ total_rate += progress_info.rate;
+
+ bool starting = ((0 == progress_info.dlnow) && (0 == progress_info.dltotal));
+ double fraction = starting ? 0.0 : static_cast<double>(progress_info.dlnow) / static_cast<double>(progress_info.dltotal);
+
+ std::string progress_percentage_text = Util::formattedString("%3.0f%% ", fraction * 100);
+ int progress_percentage_text_length = progress_percentage_text.length() + 1;
+
+ bptime::time_duration eta(bptime::seconds((long)((progress_info.dltotal - progress_info.dlnow) / progress_info.rate)));
+ eta_total_seconds += eta;
+ std::string etastring = Util::makeEtaString(eta);
+
+ std::string rate_unit;
+ if (progress_info.rate > 1048576) // 1 MB
+ {
+ progress_info.rate /= 1048576;
+ rate_unit = "MB/s";
+ }
+ else
+ {
+ progress_info.rate /= 1024;
+ rate_unit = "kB/s";
+ }
+
+ std::string progress_status_text = Util::formattedString(" %0.2f/%0.2fMB @ %0.2f%s ETA: %s", static_cast<double>(progress_info.dlnow)/1024/1024, static_cast<double>(progress_info.dltotal)/1024/1024, progress_info.rate, rate_unit.c_str(), etastring.c_str());
+ int status_text_length = progress_status_text.length() + 1;
+
+ if ((status_text_length + progress_percentage_text_length + bar_length) > iTermWidth)
+ bar_length -= (status_text_length + progress_percentage_text_length + bar_length) - iTermWidth;
+
+ // Don't draw progressbar if length is less than min_bar_length
+ std::string progress_bar_text;
+ if (bar_length >= min_bar_length)
+ progress_bar_text = bar.createBarString(bar_length, fraction);
+
+ progress_text = progress_percentage_text + progress_bar_text + progress_status_text;
+ std::string filename_text = "#" + std::to_string(i) + " " + filename;
+ Util::shortenStringToTerminalWidth(filename_text);
+
+ vProgressText.push_back(filename_text);
+ vProgressText.push_back(progress_text);
+ }
+
+ // Total download speed and number of remaining tasks in download queue
+ if (dl_status != DLSTATUS_FINISHED)
+ {
+ unsigned long long total_remaining = iTotalRemainingBytes.load();
+ std::string total_eta_str;
+ if (total_remaining > 0)
+ {
+ bptime::time_duration eta(bptime::seconds((long)(total_remaining / total_rate)));
+ eta += eta_total_seconds;
+ std::string eta_str = Util::makeEtaString(eta);
+
+ double total_remaining_double = static_cast<double>(total_remaining)/1048576;
+ std::string total_remaining_unit = "MB";
+ std::vector<std::string> units = { "GB", "TB", "PB" };
+
+ if (total_remaining_double > 1024)
+ {
+ for (const auto& unit : units)
+ {
+ total_remaining_double /= 1024;
+ total_remaining_unit = unit;
+
+ if (total_remaining_double < 1024)
+ break;
+ }
+ }
+
+ total_eta_str = Util::formattedString(" (%0.2f%s) ETA: %s", total_remaining_double, total_remaining_unit.c_str(), eta_str.c_str());
+ }
+
+ std::ostringstream ss;
+ if (Globals::globalConfig.iThreads > 1)
+ {
+ std::string rate_unit;
+ if (total_rate > 1048576) // 1 MB
+ {
+ total_rate /= 1048576;
+ rate_unit = "MB/s";
+ }
+ else
+ {
+ total_rate /= 1024;
+ rate_unit = "kB/s";
+ }
+ ss << "Total: " << std::setprecision(2) << std::fixed << total_rate << rate_unit << " | ";
+ }
+ ss << "Remaining: " << download_queue.size();
+
+ if (!total_eta_str.empty())
+ ss << total_eta_str;
+
+ vProgressText.push_back(ss.str());
+ }
+
+ // Print progress info
+ for (unsigned int i = 0; i < vProgressText.size(); ++i)
+ {
+ std::cout << vProgressText[i] << std::endl;
+ }
+
+ // Move cursor up by vProgressText.size() rows
+ if (dl_status != DLSTATUS_FINISHED)
+ {
+ std::cout << "\033[" << vProgressText.size() << "A\r" << std::flush;
+ }
+ }
+}
+
+void Downloader::getGameDetailsThread(Config config, const unsigned int& tid)
+{
+ std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+ galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+ if (!galaxy->init())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ delete galaxy;
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ return;
+ }
+ }
+
+ // Create new GOG website handle
+ Website* website = new Website();
+ if (!website->IsLoggedIn())
+ {
+ delete galaxy;
+ delete website;
+ msgQueue.push(Message("Website not logged in", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ return;
+ }
+
+ // Set default game specific directory options to values from config
+ DirectoryConfig dirConfDefault;
+ dirConfDefault = config.dirConf;
+
+ gameItem game_item;
+ while (gameItemQueue.try_pop(game_item))
+ {
+ gameDetails game;
+
+ gameSpecificConfig conf;
+ conf.dlConf = config.dlConf;
+ conf.dirConf = dirConfDefault;
+ conf.dlConf.bIgnoreDLCCount = false;
+
+ if (!config.bUpdateCache) // Disable game specific config files for cache update
+ {
+ int iOptionsOverridden = Util::getGameSpecificConfig(game_item.name, &conf);
+ if (iOptionsOverridden > 0)
+ {
+ std::ostringstream ss;
+ ss << game_item.name << " - " << iOptionsOverridden << " options overridden with game specific options" << std::endl;
+ if (config.bVerbose)
+ {
+ if (conf.dlConf.bIgnoreDLCCount)
+ ss << "\tIgnore DLC count" << std::endl;
+ if (conf.dlConf.bDLC != config.dlConf.bDLC)
+ ss << "\tDLC: " << (conf.dlConf.bDLC ? "true" : "false") << std::endl;
+ if (conf.dlConf.iInstallerLanguage != config.dlConf.iInstallerLanguage)
+ ss << "\tLanguage: " << Util::getOptionNameString(conf.dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES) << std::endl;
+ if (conf.dlConf.vLanguagePriority != config.dlConf.vLanguagePriority)
+ {
+ ss << "\tLanguage priority:" << std::endl;
+ for (unsigned int j = 0; j < conf.dlConf.vLanguagePriority.size(); ++j)
+ {
+ ss << "\t " << j << ": " << Util::getOptionNameString(conf.dlConf.vLanguagePriority[j], GlobalConstants::LANGUAGES) << std::endl;
+ }
+ }
+ if (conf.dlConf.iInstallerPlatform != config.dlConf.iInstallerPlatform)
+ ss << "\tPlatform: " << Util::getOptionNameString(conf.dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS) << std::endl;
+ if (conf.dlConf.vPlatformPriority != config.dlConf.vPlatformPriority)
+ {
+ ss << "\tPlatform priority:" << std::endl;
+ for (unsigned int j = 0; j < conf.dlConf.vPlatformPriority.size(); ++j)
+ {
+ ss << "\t " << j << ": " << Util::getOptionNameString(conf.dlConf.vPlatformPriority[j], GlobalConstants::PLATFORMS) << std::endl;
+ }
+ }
+ }
+ msgQueue.push(Message(ss.str(), MSGTYPE_INFO, msg_prefix));
+ }
+ }
+
+ // Refresh Galaxy login if token is expired
+ if (galaxy->isTokenExpired())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ break;
+ }
+ }
+
+ Json::Value product_info = galaxy->getProductInfo(game_item.id);
+ game = galaxy->productInfoJsonToGameDetails(product_info, conf.dlConf);
+ game.filterWithPriorities(conf);
+
+ Json::Value gameDetailsJSON;
+
+ if (!game_item.gamedetailsjson.empty())
+ gameDetailsJSON = game_item.gamedetailsjson;
+
+ if (conf.dlConf.bSaveSerials && game.serials.empty())
+ {
+ if (gameDetailsJSON.empty())
+ gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+ game.serials = Downloader::getSerialsFromJSON(gameDetailsJSON);
+ }
+
+ if (conf.dlConf.bSaveChangelogs && game.changelog.empty())
+ {
+ if (gameDetailsJSON.empty())
+ gameDetailsJSON = website->getGameDetailsJSON(game_item.id);
+ game.changelog = Downloader::getChangelogFromJSON(gameDetailsJSON);
+ }
+
+ game.makeFilepaths(conf.dirConf);
+ gameDetailsQueue.push(game);
+ }
+
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ delete galaxy;
+ delete website;
+
+ return;
+}
+
+void Downloader::saveGalaxyJSON()
+{
+ if (!Globals::galaxyConf.getJSON().empty())
+ {
+ std::ofstream ofs(Globals::galaxyConf.getFilepath());
+ if (!ofs)
+ {
+ std::cerr << "Failed to write " << Globals::galaxyConf.getFilepath() << std::endl;
+ }
+ else
+ {
+ ofs << Globals::galaxyConf.getJSON() << std::endl;
+ ofs.close();
+ }
+ if (!Globals::globalConfig.bRespectUmask)
+ Util::setFilePermissions(Globals::galaxyConf.getFilepath(), boost::filesystem::owner_read | boost::filesystem::owner_write);
+ }
+}
+
+bool Downloader::galaxySelectProductIdHelper(const std::string& product_id, std::string& selected_product)
+{
+ selected_product = product_id;
+
+ // Check to see if product_id is id or gamename
+ boost::regex expression("^[0-9]+$");
+ boost::match_results<std::string::const_iterator> what;
+ if (!boost::regex_search(product_id, what, expression))
+ {
+ Globals::globalConfig.sGameRegex = product_id;
+ this->getGameList();
+ if (this->gameItems.empty())
+ {
+ std::cerr << "Didn't match any products" << std::endl;
+ return false;
+ }
+
+ if (this->gameItems.size() == 1)
+ {
+ selected_product = this->gameItems[0].id;
+ }
+ else
+ {
+ std::cout << "Select product:" << std::endl;
+ for (unsigned int i = 0; i < this->gameItems.size(); ++i)
+ std::cout << i << ": " << this->gameItems[i].name << std::endl;
+
+ if (!isatty(STDIN_FILENO)) {
+ std::cerr << "Unable to read selection" << std::endl;
+ return false;
+ }
+
+ int iSelect = -1;
+ int iSelectMax = this->gameItems.size();
+ while (iSelect < 0 || iSelect >= iSelectMax)
+ {
+ std::cerr << "> ";
+ std::string selection;
+
+ std::getline(std::cin, selection);
+ try
+ {
+ iSelect = std::stoi(selection);
+ }
+ catch(std::invalid_argument& e)
+ {
+ std::cerr << e.what() << std::endl;
+ }
+ }
+ selected_product = this->gameItems[iSelect].id;
+ }
+ }
+ return true;
+}
+
+std::vector<galaxyDepotItem> Downloader::galaxyGetDepotItemVectorFromJson(const Json::Value& json, const unsigned int& iGalaxyArch)
+{
+ std::string product_id = json["baseProductId"].asString();
+
+ std::string sLanguageRegex = "en|eng|english|en[_-]US";
+ unsigned int iLanguage = Globals::globalConfig.dlConf.iGalaxyLanguage;
+ for (unsigned int i = 0; i < GlobalConstants::LANGUAGES.size(); ++i)
+ {
+ if (GlobalConstants::LANGUAGES[i].id == iLanguage)
+ {
+ sLanguageRegex = GlobalConstants::LANGUAGES[i].regexp;
+ break;
+ }
+ }
+
+ std::string sGalaxyArch = "64";
+ for (unsigned int i = 0; i < GlobalConstants::GALAXY_ARCHS.size(); ++i)
+ {
+ if (GlobalConstants::GALAXY_ARCHS[i].id == iGalaxyArch)
+ {
+ sGalaxyArch = GlobalConstants::GALAXY_ARCHS[i].code;
+ break;
+ }
+ }
+
+ std::vector<galaxyDepotItem> items;
+ for (unsigned int i = 0; i < json["depots"].size(); ++i)
+ {
+ std::vector<galaxyDepotItem> vec = gogGalaxy->getFilteredDepotItemsVectorFromJson(json["depots"][i], sLanguageRegex, sGalaxyArch);
+
+ if (!vec.empty())
+ items.insert(std::end(items), std::begin(vec), std::end(vec));
+ }
+
+ // Add dependency ids to vector
+ std::vector<std::string> dependencies;
+ if (json.isMember("dependencies") && Globals::globalConfig.dlConf.bGalaxyDependencies)
+ {
+ for (unsigned int i = 0; i < json["dependencies"].size(); ++i)
+ {
+ dependencies.push_back(json["dependencies"][i].asString());
+ }
+ }
+
+ // Add dependencies to items vector
+ if (!dependencies.empty())
+ {
+ Json::Value dependenciesJson = gogGalaxy->getDependenciesJson();
+ if (!dependenciesJson.empty() && dependenciesJson.isMember("depots"))
+ {
+ for (unsigned int i = 0; i < dependenciesJson["depots"].size(); ++i)
+ {
+ std::string dependencyId = dependenciesJson["depots"][i]["dependencyId"].asString();
+ if (std::any_of(dependencies.begin(), dependencies.end(), [dependencyId](std::string dependency){return dependency == dependencyId;}))
+ {
+ std::vector<galaxyDepotItem> vec = gogGalaxy->getFilteredDepotItemsVectorFromJson(dependenciesJson["depots"][i], sLanguageRegex, sGalaxyArch, true);
+
+ if (!vec.empty())
+ items.insert(std::end(items), std::begin(vec), std::end(vec));
+ }
+ }
+ }
+ }
+
+ // Set product id for items
+ for (auto it = items.begin(); it != items.end(); ++it)
+ {
+ if (it->product_id.empty())
+ {
+ it->product_id = product_id;
+ }
+ }
+
+ return items;
+}
+
+void Downloader::galaxyInstallGame(const std::string& product_id, int build_index, const unsigned int& iGalaxyArch)
+{
+ std::string id;
+ if(this->galaxySelectProductIdHelper(product_id, id))
+ {
+ if (!id.empty())
+ this->galaxyInstallGameById(id, build_index, iGalaxyArch);
+ }
+}
+
+void Downloader::galaxyInstallGameById(const std::string& product_id, int build_index, const unsigned int& iGalaxyArch)
+{
+ if (build_index < 0)
+ build_index = 0;
+
+ std::string sPlatform;
+ unsigned int iPlatform = Globals::globalConfig.dlConf.iGalaxyPlatform;
+ if (iPlatform == GlobalConstants::PLATFORM_LINUX)
+ sPlatform = "linux";
+ else if (iPlatform == GlobalConstants::PLATFORM_MAC)
+ sPlatform = "osx";
+ else
+ sPlatform = "windows";
+
+ Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform);
+
+ // JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support
+ if (json.empty() && iPlatform == GlobalConstants::PLATFORM_LINUX)
+ {
+ std::cout << "Galaxy API doesn't have Linux support" << std::endl;
+
+ // Galaxy install hack for Linux
+ std::cout << "Trying to use installers as repository" << std::endl;
+ this->galaxyInstallGame_MojoSetupHack(product_id);
+
+ return;
+ }
+
+ if (json["items"][build_index]["generation"].asInt() != 2)
+ {
+ std::cout << "Only generation 2 builds are supported currently" << std::endl;
+ return;
+ }
+
+ std::string link = json["items"][build_index]["link"].asString();
+ std::string buildHash;
+ buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end());
+
+ // Save builds json to another variable for later use
+ Json::Value json_builds = json;
+
+ json = gogGalaxy->getManifestV2(buildHash);
+ std::string game_title = json["products"][0]["name"].asString();
+ std::string install_directory;
+
+ if (Globals::globalConfig.dirConf.bSubDirectories)
+ {
+ install_directory = this->getGalaxyInstallDirectory(gogGalaxy, json);
+ }
+
+ std::string install_path = Globals::globalConfig.dirConf.sDirectory + install_directory;
+
+ std::vector<galaxyDepotItem> items = this->galaxyGetDepotItemVectorFromJson(json, iGalaxyArch);
+
+ // Check for differences between previously installed build and new build
+ std::vector<galaxyDepotItem> items_old;
+
+ std::string info_path = install_path + "/goggame-" + product_id + ".info";
+ std::string old_build_id;
+ int old_build_index = -1;
+ if (boost::filesystem::exists(info_path))
+ {
+ std::ifstream info_file_stream(info_path, std::ifstream::binary);
+ Json::Value info_json;
+ try {
+ info_file_stream >> info_json;
+ old_build_id = info_json["buildId"].asString();
+ }
+ catch (const Json::Exception& exc)
+ {
+ std::cout << "Failed to parse " << info_path << std::endl;
+ std::cout << exc.what() << std::endl;
+ return;
+ }
+
+ if (!old_build_id.empty())
+ {
+ for (unsigned int i = 0; i < json_builds["items"].size(); ++i)
+ {
+ std::string build_id = json_builds["items"][i]["build_id"].asString();
+ if (build_id == old_build_id)
+ {
+ old_build_index = i;
+ break;
+ }
+ }
+ }
+ }
+
+ // Check for deleted files between builds
+ if (old_build_index >= 0 && old_build_index != build_index)
+ {
+ std::string link = json_builds["items"][old_build_index]["link"].asString();
+ std::string buildHash_old;
+ buildHash_old.assign(link.begin()+link.find_last_of("/")+1, link.end());
+
+ Json::Value json_old = gogGalaxy->getManifestV2(buildHash_old);
+ items_old = this->galaxyGetDepotItemVectorFromJson(json_old, iGalaxyArch);
+ }
+
+ std::vector<std::string> deleted_filepaths;
+ if (!items_old.empty())
+ {
+ for (auto old_item: items_old)
+ {
+ bool isDeleted = true;
+ for (auto item: items)
+ {
+ if (old_item.path == item.path)
+ {
+ isDeleted = false;
+ break;
+ }
+ }
+ if (isDeleted)
+ deleted_filepaths.push_back(old_item.path);
+ }
+ }
+
+ // Delete old files
+ if (!deleted_filepaths.empty())
+ {
+ for (auto path : deleted_filepaths)
+ {
+ std::string filepath = install_path + "/" + path;
+ std::cout << "Deleting " << filepath << std::endl;
+ if (boost::filesystem::exists(filepath))
+ if (!boost::filesystem::remove(filepath))
+ std::cerr << "Failed to delete " << filepath << std::endl;
+ }
+ }
+
+ uintmax_t totalSize = 0;
+ for (unsigned int i = 0; i < items.size(); ++i)
+ {
+ if (Globals::globalConfig.bVerbose)
+ {
+ std::cout << items[i].path << std::endl;
+ std::cout << "\tChunks: " << items[i].chunks.size() << std::endl;
+ std::cout << "\tmd5: " << items[i].md5 << std::endl;
+ }
+ totalSize += items[i].totalSizeUncompressed;
+ iTotalRemainingBytes.fetch_add(items[i].totalSizeCompressed);
+ dlQueueGalaxy.push(items[i]);
+ }
+
+ double totalSizeMB = static_cast<double>(totalSize)/1024/1024;
+ std::cout << game_title << std::endl;
+ std::cout << "Files: " << items.size() << std::endl;
+ std::cout << "Total size installed: " << totalSizeMB << " MB" << std::endl;
+
+ // Limit thread count to number of items in download queue
+ unsigned int iThreads = std::min(Globals::globalConfig.iThreads, static_cast<unsigned int>(dlQueueGalaxy.size()));
+
+ // Create download threads
+ std::vector<std::thread> vThreads;
+ for (unsigned int i = 0; i < iThreads; ++i)
+ {
+ DownloadInfo dlInfo;
+ dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+ vDownloadInfo.push_back(dlInfo);
+ vThreads.push_back(std::thread(Downloader::processGalaxyDownloadQueue, install_path, Globals::globalConfig, i));
+ }
+
+ this->printProgress(dlQueueGalaxy);
+
+ // Join threads
+ for (unsigned int i = 0; i < vThreads.size(); ++i)
+ vThreads[i].join();
+
+ vThreads.clear();
+ vDownloadInfo.clear();
+
+ std::cout << "Checking for orphaned files" << std::endl;
+ std::vector<std::string> orphans = this->galaxyGetOrphanedFiles(items, install_path);
+ std::cout << "\t" << orphans.size() << " orphaned files" << std::endl;
+ for (unsigned int i = 0; i < orphans.size(); ++i)
+ {
+ if (Globals::globalConfig.dlConf.bGalaxyDeleteOrphans)
+ {
+ std::string filepath = orphans[i];
+ std::cout << "Deleting " << filepath << std::endl;
+ if (boost::filesystem::exists(filepath))
+ if (!boost::filesystem::remove(filepath))
+ std::cerr << "Failed to delete " << filepath << std::endl;
+ }
+ else
+ std::cout << "\t" << orphans[i] << std::endl;
+ }
+}
+
+void Downloader::processGalaxyDownloadQueue(const std::string& install_path, Config conf, const unsigned int& tid)
+{
+ std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+ galaxyAPI* galaxy = new galaxyAPI(Globals::globalConfig.curlConf);
+ if (!galaxy->init())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ delete galaxy;
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ return;
+ }
+ }
+
+ CURL* dlhandle = curl_easy_init();
+ curl_easy_setopt(dlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.curlConf.sUserAgent.c_str());
+ curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1);
+
+ curl_easy_setopt(dlhandle, CURLOPT_CONNECTTIMEOUT, conf.curlConf.iTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(dlhandle, CURLOPT_SSL_VERIFYPEER, conf.curlConf.bVerifyPeer);
+ curl_easy_setopt(dlhandle, CURLOPT_VERBOSE, conf.curlConf.bVerbose);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+ curl_easy_setopt(dlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, conf.curlConf.iDownloadRate);
+ curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_TIME, conf.curlConf.iLowSpeedTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_LIMIT, conf.curlConf.iLowSpeedTimeoutRate);
+
+ if (!conf.curlConf.sCACertPath.empty())
+ curl_easy_setopt(dlhandle, CURLOPT_CAINFO, conf.curlConf.sCACertPath.c_str());
+
+ xferInfo xferinfo;
+ xferinfo.tid = tid;
+ xferinfo.curlhandle = dlhandle;
+
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
+
+ galaxyDepotItem item;
+ while (dlQueueGalaxy.try_pop(item))
+ {
+ vDownloadInfo[tid].setStatus(DLSTATUS_STARTING);
+ iTotalRemainingBytes.fetch_sub(item.totalSizeCompressed);
+
+ boost::filesystem::path path = install_path + "/" + item.path;
+
+ // Check that directory exists and create it
+ boost::filesystem::path directory = path.parent_path();
+ mtx_create_directories.lock(); // Use mutex to avoid possible race conditions
+ if (boost::filesystem::exists(directory))
+ {
+ if (!boost::filesystem::is_directory(directory))
+ {
+ msgQueue.push(Message(directory.string() + " is not directory", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ delete galaxy;
+ mtx_create_directories.unlock();
+ return;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(directory))
+ {
+ msgQueue.push(Message("Failed to create directory: " + directory.string(), MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ delete galaxy;
+ mtx_create_directories.unlock();
+ return;
+ }
+ }
+ mtx_create_directories.unlock();
+
+ vDownloadInfo[tid].setFilename(path.string());
+
+ unsigned int start_chunk = 0;
+ if (boost::filesystem::exists(path))
+ {
+ if (conf.bVerbose)
+ msgQueue.push(Message("File already exists: " + path.string(), MSGTYPE_INFO, msg_prefix));
+
+ unsigned int resume_chunk = 0;
+ uintmax_t filesize = boost::filesystem::file_size(path);
+ if (filesize == item.totalSizeUncompressed)
+ {
+ // File is same size
+ if (Util::getFileHash(path.string(), RHASH_MD5) == item.md5)
+ {
+ msgQueue.push(Message(path.string() + ": OK", MSGTYPE_SUCCESS, msg_prefix));
+ continue;
+ }
+ else
+ {
+ msgQueue.push(Message(path.string() + ": MD5 mismatch", MSGTYPE_WARNING, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+ else if (filesize > item.totalSizeUncompressed)
+ {
+ // File is bigger than on server, delete old file and start from beginning
+ msgQueue.push(Message(path.string() + ": File is bigger than expected. Deleting old file and starting from beginning", MSGTYPE_INFO, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ else
+ {
+ // File is smaller than on server, resume
+ for (unsigned int j = 0; j < item.chunks.size(); ++j)
+ {
+ if (item.chunks[j].offset_uncompressed == filesize)
+ {
+ resume_chunk = j;
+ break;
+ }
+ }
+
+ if (resume_chunk > 0)
+ {
+ msgQueue.push(Message(path.string() + ": Resume from chunk " + std::to_string(resume_chunk), MSGTYPE_INFO, msg_prefix));
+ // Get chunk hash for previous chunk
+ FILE* f = fopen(path.string().c_str(), "r");
+ if (!f)
+ {
+ msgQueue.push(Message(path.string() + ": Failed to open", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+
+ unsigned int previous_chunk = resume_chunk - 1;
+ uintmax_t chunk_size = item.chunks[previous_chunk].size_uncompressed;
+ // use fseeko to support large files on 32 bit platforms
+ fseeko(f, item.chunks[previous_chunk].offset_uncompressed, SEEK_SET);
+ unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+ if (chunk == NULL)
+ {
+ msgQueue.push(Message(path.string() + ": Memory error - Chunk " + std::to_string(resume_chunk), MSGTYPE_ERROR, msg_prefix));
+ fclose(f);
+ continue;
+ }
+
+ uintmax_t fread_size = fread(chunk, 1, chunk_size, f);
+ fclose(f);
+
+ if (fread_size != chunk_size)
+ {
+ msgQueue.push(Message(path.string() + ": Read error - Chunk " + std::to_string(resume_chunk), MSGTYPE_ERROR, msg_prefix));
+ free(chunk);
+ continue;
+ }
+ std::string chunk_hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+ free(chunk);
+
+ if (chunk_hash == item.chunks[previous_chunk].md5_uncompressed)
+ {
+ // Hash for previous chunk matches, resume at this position
+ start_chunk = resume_chunk;
+ }
+ else
+ {
+ // Hash for previous chunk is different, delete old file and start from beginning
+ msgQueue.push(Message(path.string() + ": Chunk hash is different. Deleting old file and starting from beginning.", MSGTYPE_WARNING, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+ else
+ {
+ msgQueue.push(Message(path.string() + ": Failed to find valid resume position. Deleting old file and starting from beginning.", MSGTYPE_WARNING, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+ }
+
+ bool bChunkFailure = false;
+ std::time_t timestamp = -1;
+ for (unsigned int j = start_chunk; j < item.chunks.size(); ++j)
+ {
+ ChunkMemoryStruct chunk;
+ chunk.memory = (char *) malloc(1);
+ chunk.size = 0;
+
+ // Refresh Galaxy login if token is expired
+ if (galaxy->isTokenExpired())
+ {
+ if (!galaxy->refreshLogin())
+ {
+ msgQueue.push(Message("Galaxy API failed to refresh login", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ free(chunk.memory);
+ delete galaxy;
+ return;
+ }
+ }
+
+ Json::Value json;
+ if (item.isDependency)
+ json = galaxy->getDependencyLink(galaxy->hashToGalaxyPath(item.chunks[j].md5_compressed));
+ else
+ json = galaxy->getSecureLink(item.product_id, galaxy->hashToGalaxyPath(item.chunks[j].md5_compressed));
+
+ if (json.empty())
+ {
+ bChunkFailure = true;
+ std::string error_message = path.string() + ": Empty JSON response (product: " + item.product_id + ", chunk #"+ std::to_string(j) + ": " + item.chunks[j].md5_compressed + ")";
+ msgQueue.push(Message(error_message, MSGTYPE_ERROR, msg_prefix));
+ free(chunk.memory);
+ break;
+ }
+
+ // Handle priority of CDNs
+ struct urlPriority
+ {
+ std::string url;
+ int priority;
+ };
+
+ // Build a vector of all urls and their priority score
+ std::vector<urlPriority> cdnUrls;
+ for (unsigned int k = 0; k < json["urls"].size(); ++k)
+ {
+ std::string endpoint_name = json["urls"][k]["endpoint_name"].asString();
+
+ unsigned int score = conf.dlConf.vGalaxyCDNPriority.size();
+ unsigned int cdn = Util::getOptionValue(endpoint_name, GlobalConstants::GALAXY_CDNS, false);
+ for (unsigned int idx = 0; idx < score; ++idx)
+ {
+ if (cdn & conf.dlConf.vGalaxyCDNPriority[idx])
+ {
+ score = idx;
+ break;
+ }
+ }
+
+ // Couldn't find a match when assigning score
+ if (score == conf.dlConf.vGalaxyCDNPriority.size())
+ {
+ // Add index value to score
+ // This way unknown CDNs have priority based on the order they appear in json
+ score += k;
+ }
+
+ // Build url according to url_format
+ std::string link_base_url = json["urls"][k]["parameters"]["base_url"].asString();
+ std::string link_path = json["urls"][k]["parameters"]["path"].asString();
+ std::string link_token = json["urls"][k]["parameters"]["token"].asString();
+
+ std::string url = json["urls"][k]["url_format"].asString();
+
+ while(Util::replaceString(url, "{base_url}", link_base_url));
+ while(Util::replaceString(url, "{path}", link_path));
+ while(Util::replaceString(url, "{token}", link_token));
+
+ // Highwinds specific
+ std::string link_hw_l= json["urls"][k]["parameters"]["l"].asString();
+ std::string link_hw_source = json["urls"][k]["parameters"]["source"].asString();
+ std::string link_hw_ttl = json["urls"][k]["parameters"]["ttl"].asString();
+ std::string link_hw_gog_token = json["urls"][k]["parameters"]["gog_token"].asString();
+
+ while(Util::replaceString(url, "{l}", link_hw_l));
+ while(Util::replaceString(url, "{source}", link_hw_source));
+ while(Util::replaceString(url, "{ttl}", link_hw_ttl));
+ while(Util::replaceString(url, "{gog_token}", link_hw_gog_token));
+
+ urlPriority cdnurl;
+ cdnurl.url = url;
+ cdnurl.priority = score;
+ cdnUrls.push_back(cdnurl);
+ }
+
+ if (cdnUrls.empty())
+ {
+ bChunkFailure = true;
+ msgQueue.push(Message(path.string() + ": Failed to get download url", MSGTYPE_ERROR, msg_prefix));
+ free(chunk.memory);
+ break;
+ }
+
+ // Sort urls by priority (lowest score first)
+ std::sort(cdnUrls.begin(), cdnUrls.end(),
+ [](urlPriority a, urlPriority b)
+ {
+ return (a.priority < b.priority);
+ }
+ );
+
+ // Select url with lowest priority score
+ std::string url = cdnUrls[0].url;
+
+ curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, WriteChunkMemoryCallback);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, &chunk);
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
+ curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
+
+ std::string filepath_and_chunk = path.string() + " (chunk " + std::to_string(j + 1) + "/" + std::to_string(item.chunks.size()) + ")";
+ vDownloadInfo[tid].setFilename(filepath_and_chunk);
+
+ if (Globals::globalConfig.iWait > 0)
+ usleep(Globals::globalConfig.iWait); // Delay the request by specified time
+
+ xferinfo.offset = 0;
+ xferinfo.timer.reset();
+ xferinfo.TimeAndSize.clear();
+
+ CURLcode result = curl_easy_perform(dlhandle);
+
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 0L);
+
+ if (result != CURLE_OK)
+ {
+ msgQueue.push(Message(std::string(curl_easy_strerror(result)), MSGTYPE_ERROR, msg_prefix));
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ long int response_code = 0;
+ result = curl_easy_getinfo(dlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ if (result == CURLE_OK)
+ msgQueue.push(Message("HTTP ERROR: " + std::to_string(response_code) + " (" + url + ")", MSGTYPE_ERROR, msg_prefix));
+ else
+ msgQueue.push(Message("HTTP ERROR: failed to get error code: " + std::string(curl_easy_strerror(result)) + " (" + url + ")", MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ else
+ {
+ // Get timestamp for downloaded file
+ long filetime = -1;
+ result = curl_easy_getinfo(dlhandle, CURLINFO_FILETIME, &filetime);
+ if (result == CURLE_OK && filetime >= 0)
+ timestamp = (std::time_t)filetime;
+ }
+
+ std::ofstream ofs(path.string(), std::ofstream::out | std::ofstream::binary | std::ofstream::app);
+ if (ofs)
+ {
+ boost::iostreams::filtering_streambuf<boost::iostreams::output> output;
+ output.push(boost::iostreams::zlib_decompressor(GlobalConstants::ZLIB_WINDOW_SIZE));
+ output.push(ofs);
+ boost::iostreams::write(output, chunk.memory, chunk.size);
+ }
+ if (ofs)
+ ofs.close();
+
+ free(chunk.memory);
+ }
+
+ if (bChunkFailure)
+ {
+ msgQueue.push(Message(path.string() + ": Chunk failure, skipping file", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+
+ // Set timestamp for downloaded file to same value as file on server
+ if (boost::filesystem::exists(path) && timestamp >= 0)
+ boost::filesystem::last_write_time(path, timestamp);
+
+ msgQueue.push(Message("Download complete: " + path.string(), MSGTYPE_SUCCESS, msg_prefix));
+ }
+
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ delete galaxy;
+ curl_easy_cleanup(dlhandle);
+
+ return;
+}
+
+void Downloader::galaxyShowBuilds(const std::string& product_id, int build_index)
+{
+ std::string id;
+ if(this->galaxySelectProductIdHelper(product_id, id))
+ {
+ if (!id.empty())
+ this->galaxyShowBuildsById(id, build_index);
+ }
+}
+
+void Downloader::galaxyShowBuildsById(const std::string& product_id, int build_index)
+{
+ std::string sPlatform;
+ unsigned int iPlatform = Globals::globalConfig.dlConf.iGalaxyPlatform;
+ if (iPlatform == GlobalConstants::PLATFORM_LINUX)
+ sPlatform = "linux";
+ else if (iPlatform == GlobalConstants::PLATFORM_MAC)
+ sPlatform = "osx";
+ else
+ sPlatform = "windows";
+
+ Json::Value json = gogGalaxy->getProductBuilds(product_id, sPlatform);
+
+ // JSON is empty and platform is Linux. Most likely cause is that Galaxy API doesn't have Linux support
+ if (json.empty() && iPlatform == GlobalConstants::PLATFORM_LINUX)
+ {
+ std::cout << "Galaxy API doesn't have Linux support" << std::endl;
+
+ std::cout << "Checking for installers that can be used as repository" << std::endl;
+ DownloadConfig dlConf = Globals::globalConfig.dlConf;
+ dlConf.bInstallers = true;
+ dlConf.bExtras = false;
+ dlConf.bLanguagePacks = false;
+ dlConf.bPatches = false;
+ dlConf.bDLC = true;
+ dlConf.iInstallerPlatform = dlConf.iGalaxyPlatform;
+ dlConf.iInstallerLanguage = dlConf.iGalaxyLanguage;
+
+ Json::Value product_info = gogGalaxy->getProductInfo(product_id);
+ gameDetails game = gogGalaxy->productInfoJsonToGameDetails(product_info, dlConf);
+
+ std::vector<gameFile> vInstallers;
+ if (!game.installers.empty())
+ {
+ vInstallers.push_back(game.installers[0]);
+ for (unsigned int i = 0; i < game.dlcs.size(); ++i)
+ {
+ if (!game.dlcs[i].installers.empty())
+ vInstallers.push_back(game.dlcs[i].installers[0]);
+ }
+ }
+
+ if (vInstallers.empty())
+ {
+ std::cout << "No installers found" << std::endl;
+ }
+ else
+ {
+ std::cout << "Using these installers" << std::endl;
+ for (unsigned int i = 0; i < vInstallers.size(); ++i)
+ std::cout << "\t" << vInstallers[i].gamename << "/" << vInstallers[i].id << std::endl;
+ }
+
+ return;
+ }
+
+ if (build_index < 0)
+ {
+ for (unsigned int i = 0; i < json["items"].size(); ++i)
+ {
+ std::cout << i << ": " << "Version " << json["items"][i]["version_name"].asString() << " - " << json["items"][i]["date_published"].asString() << " (Gen " << json["items"][i]["generation"].asInt() << ")" << std::endl;
+ }
+ return;
+ }
+
+ std::string link = json["items"][build_index]["link"].asString();
+
+ if (json["items"][build_index]["generation"].asInt() == 1)
+ {
+ json = gogGalaxy->getManifestV1(link);
+ }
+ else if (json["items"][build_index]["generation"].asInt() == 2)
+ {
+ std::string buildHash;
+ buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end());
+ json = gogGalaxy->getManifestV2(buildHash);
+ }
+ else
+ {
+ std::cout << "Only generation 1 and 2 builds are supported currently" << std::endl;
+ return;
+ }
+
+ std::cout << json << std::endl;
+
+ return;
+}
+
+std::vector<std::string> Downloader::galaxyGetOrphanedFiles(const std::vector<galaxyDepotItem>& items, const std::string& install_path)
+{
+ std::vector<std::string> orphans;
+ std::vector<std::string> item_paths;
+ for (unsigned int i = 0; i < items.size(); ++i)
+ item_paths.push_back(install_path + "/" + items[i].path);
+
+ std::vector<boost::filesystem::path> filepath_vector;
+ try
+ {
+ std::size_t pathlen = Globals::globalConfig.dirConf.sDirectory.length();
+ if (boost::filesystem::exists(install_path))
+ {
+ if (boost::filesystem::is_directory(install_path))
+ {
+ // Recursively iterate over files in directory
+ boost::filesystem::recursive_directory_iterator end_iter;
+ boost::filesystem::recursive_directory_iterator dir_iter(install_path);
+ while (dir_iter != end_iter)
+ {
+ if (boost::filesystem::is_regular_file(dir_iter->status()))
+ {
+ std::string filepath = dir_iter->path().string();
+ if (Globals::globalConfig.ignorelist.isBlacklisted(filepath.substr(pathlen)))
+ {
+ if (Globals::globalConfig.bVerbose)
+ std::cerr << "skipped ignorelisted file " << filepath << std::endl;
+ }
+ else
+ {
+ filepath_vector.push_back(dir_iter->path());
+ }
+ }
+ dir_iter++;
+ }
+ }
+ }
+ else
+ std::cerr << install_path << " does not exist" << std::endl;
+ }
+ catch (const boost::filesystem::filesystem_error& ex)
+ {
+ std::cout << ex.what() << std::endl;
+ }
+
+ std::sort(item_paths.begin(), item_paths.end());
+ std::sort(filepath_vector.begin(), filepath_vector.end());
+
+ if (!filepath_vector.empty())
+ {
+ for (unsigned int i = 0; i < filepath_vector.size(); ++i)
+ {
+ bool bFileIsOrphaned = true;
+ for (std::vector<std::string>::iterator it = item_paths.begin(); it != item_paths.end(); it++)
+ {
+ boost::filesystem::path item_path = *it;
+ boost::filesystem::path file_path = filepath_vector[i].native();
+
+ if (item_path == file_path)
+ {
+ bFileIsOrphaned = false;
+ item_paths.erase(it);
+ break;
+ }
+ }
+
+ if (bFileIsOrphaned)
+ orphans.push_back(filepath_vector[i].string());
+ }
+ }
+
+ return orphans;
+}
+
+void Downloader::galaxyInstallGame_MojoSetupHack(const std::string& product_id)
+{
+ DownloadConfig dlConf = Globals::globalConfig.dlConf;
+ dlConf.bInstallers = true;
+ dlConf.bExtras = false;
+ dlConf.bLanguagePacks = false;
+ dlConf.bPatches = false;
+ dlConf.bDLC = true;
+ dlConf.iInstallerPlatform = dlConf.iGalaxyPlatform;
+ dlConf.iInstallerLanguage = dlConf.iGalaxyLanguage;
+
+ Json::Value product_info = gogGalaxy->getProductInfo(product_id);
+ gameDetails game = gogGalaxy->productInfoJsonToGameDetails(product_info, dlConf);
+
+ std::vector<gameFile> vInstallers;
+ if (!game.installers.empty())
+ {
+ vInstallers.push_back(game.installers[0]);
+ for (unsigned int i = 0; i < game.dlcs.size(); ++i)
+ {
+ if (!game.dlcs[i].installers.empty())
+ vInstallers.push_back(game.dlcs[i].installers[0]);
+ }
+ }
+
+ if (!vInstallers.empty())
+ {
+ std::vector<zipFileEntry> zipFileEntries;
+ for (unsigned int i = 0; i < vInstallers.size(); ++i)
+ {
+ std::vector<zipFileEntry> vFiles;
+ std::cout << "Getting file list for " << vInstallers[i].gamename << "/" << vInstallers[i].id << std::endl;
+ if (this->mojoSetupGetFileVector(vInstallers[i], vFiles))
+ {
+ std::cerr << "Failed to get file list" << std::endl;
+ return;
+ }
+ else
+ {
+ zipFileEntries.insert(std::end(zipFileEntries), std::begin(vFiles), std::end(vFiles));
+ }
+ }
+
+ std::string install_directory;
+
+ if (Globals::globalConfig.dirConf.bSubDirectories)
+ {
+ Json::Value windows_builds = gogGalaxy->getProductBuilds(product_id, "windows");
+ if (!windows_builds.empty())
+ {
+ std::string link = windows_builds["items"][0]["link"].asString();
+ std::string buildHash;
+ buildHash.assign(link.begin()+link.find_last_of("/")+1, link.end());
+
+ Json::Value manifest = gogGalaxy->getManifestV2(buildHash);
+ if (!manifest.empty())
+ {
+ install_directory = this->getGalaxyInstallDirectory(gogGalaxy, manifest);
+ }
+ }
+ }
+
+ std::string install_path = Globals::globalConfig.dirConf.sDirectory + "/" + install_directory + "/";
+ std::vector<zipFileEntry> vZipDirectories;
+ std::vector<zipFileEntry> vZipFiles;
+ std::vector<zipFileEntry> vZipFilesSymlink;
+ std::vector<zipFileEntry> vZipFilesSplit;
+
+ // Determine if installer contains split files and get list of base file paths
+ std::vector<std::string> vSplitFileBasePaths;
+ for (const auto& zfe : zipFileEntries)
+ {
+ std::string noarch = "data/noarch/";
+ std::string split_files = noarch + "support/split_files";
+ if (zfe.filepath.find(split_files) != std::string::npos)
+ {
+ std::cout << "Getting info about split files" << std::endl;
+ std::string url = zfe.installer_url;
+ std::string dlrange = std::to_string(zfe.start_offset_mojosetup) + "-" + std::to_string(zfe.end_offset);
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+
+ std::stringstream splitfiles_compressed;
+ std::stringstream splitfiles_uncompressed;
+
+ CURLcode result = CURLE_RECV_ERROR;
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &splitfiles_compressed);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, dlrange.c_str());
+ result = curl_easy_perform(curlhandle);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, NULL);
+
+ if (result == CURLE_OK)
+ {
+ if (ZipUtil::extractStream(&splitfiles_compressed, &splitfiles_uncompressed) == 0)
+ {
+ std::string path;
+ while (std::getline(splitfiles_uncompressed, path))
+ {
+ // Replace the leading "./" in base file path with install path
+ Util::replaceString(path, "./", install_path);
+ while (Util::replaceString(path, "//", "/")); // Replace any double slashes with single slash
+ vSplitFileBasePaths.push_back(path);
+ }
+ }
+ }
+ }
+ }
+
+ bool bContainsSplitFiles = !vSplitFileBasePaths.empty();
+
+ for (std::uintmax_t i = 0; i < zipFileEntries.size(); ++i)
+ {
+ // Ignore all files and directories that are not in "data/noarch/" directory
+ std::string noarch = "data/noarch/";
+ if (zipFileEntries[i].filepath.find(noarch) == std::string::npos || zipFileEntries[i].filepath == noarch)
+ continue;
+
+ zipFileEntry zfe = zipFileEntries[i];
+ Util::replaceString(zfe.filepath, noarch, install_path);
+ while (Util::replaceString(zfe.filepath, "//", "/")); // Replace any double slashes with single slash
+
+ if (zfe.filepath.at(zfe.filepath.length()-1) == '/')
+ vZipDirectories.push_back(zfe);
+ else if (ZipUtil::isSymlink(zfe.file_attributes))
+ vZipFilesSymlink.push_back(zfe);
+ else
+ {
+ // Check for split files
+ if (bContainsSplitFiles)
+ {
+ boost::regex expression("^(.*)(\\.split\\d+)$");
+ boost::match_results<std::string::const_iterator> what;
+ if (boost::regex_search(zfe.filepath, what, expression))
+ {
+ std::string basePath = what[1];
+ std::string partExt = what[2];
+
+ // Check against list of base file paths read from "data/noarch/support/split_files"
+ if (
+ std::any_of(
+ vSplitFileBasePaths.begin(),
+ vSplitFileBasePaths.end(),
+ [basePath](const std::string& path)
+ {
+ return path == basePath;
+ }
+ )
+ )
+ {
+ zfe.isSplitFile = true;
+ zfe.splitFileBasePath = basePath;
+ zfe.splitFilePartExt = partExt;
+ }
+ }
+
+ if (zfe.isSplitFile)
+ vZipFilesSplit.push_back(zfe);
+ else
+ vZipFiles.push_back(zfe);
+ }
+ else
+ {
+ vZipFiles.push_back(zfe);
+ }
+ }
+ }
+
+ // Create directories
+ for (std::uintmax_t i = 0; i < vZipDirectories.size(); ++i)
+ {
+ if (!boost::filesystem::exists(vZipDirectories[i].filepath))
+ {
+ if (!boost::filesystem::create_directories(vZipDirectories[i].filepath))
+ {
+ std::cerr << "Failed to create directory " << vZipDirectories[i].filepath << std::endl;
+ return;
+ }
+ }
+ }
+
+ // Set start and end offsets for split files
+ // Create map of split files for combining them later
+ splitFilesMap mSplitFiles;
+ if (!vZipFilesSplit.empty())
+ {
+ std::sort(vZipFilesSplit.begin(), vZipFilesSplit.end(), [](const zipFileEntry& i, const zipFileEntry& j) -> bool { return i.filepath < j.filepath; });
+
+ std::string prevBasePath = "";
+ off_t prevEndOffset = 0;
+ for (auto& zfe : vZipFilesSplit)
+ {
+ if (zfe.splitFileBasePath == prevBasePath)
+ zfe.splitFileStartOffset = prevEndOffset;
+ else
+ zfe.splitFileStartOffset = 0;
+
+ zfe.splitFileEndOffset = zfe.splitFileStartOffset + zfe.uncomp_size;
+
+ prevBasePath = zfe.splitFileBasePath;
+ prevEndOffset = zfe.splitFileEndOffset;
+
+ if (mSplitFiles.count(zfe.splitFileBasePath) > 0)
+ {
+ mSplitFiles[zfe.splitFileBasePath].push_back(zfe);
+ }
+ else
+ {
+ std::vector<zipFileEntry> vec;
+ vec.push_back(zfe);
+ mSplitFiles[zfe.splitFileBasePath] = vec;
+ }
+ }
+
+ vZipFiles.insert(std::end(vZipFiles), std::begin(vZipFilesSplit), std::end(vZipFilesSplit));
+ }
+
+ // Add files to download queue
+ for (std::uintmax_t i = 0; i < vZipFiles.size(); ++i)
+ {
+ dlQueueGalaxy_MojoSetupHack.push(vZipFiles[i]);
+ iTotalRemainingBytes.fetch_add(vZipFiles[i].comp_size);
+ }
+
+ // Add symlinks to download queue
+ for (std::uintmax_t i = 0; i < vZipFilesSymlink.size(); ++i)
+ {
+ dlQueueGalaxy_MojoSetupHack.push(vZipFilesSymlink[i]);
+ iTotalRemainingBytes.fetch_add(vZipFilesSymlink[i].comp_size);
+ }
+
+ // Limit thread count to number of items in download queue
+ unsigned int iThreads = std::min(Globals::globalConfig.iThreads, static_cast<unsigned int>(dlQueueGalaxy_MojoSetupHack.size()));
+
+ // Create download threads
+ std::vector<std::thread> vThreads;
+ for (unsigned int i = 0; i < iThreads; ++i)
+ {
+ DownloadInfo dlInfo;
+ dlInfo.setStatus(DLSTATUS_NOTSTARTED);
+ vDownloadInfo.push_back(dlInfo);
+ vThreads.push_back(std::thread(Downloader::processGalaxyDownloadQueue_MojoSetupHack, Globals::globalConfig, i));
+ }
+
+ this->printProgress(dlQueueGalaxy_MojoSetupHack);
+
+ // Join threads
+ for (unsigned int i = 0; i < vThreads.size(); ++i)
+ vThreads[i].join();
+
+ vThreads.clear();
+ vDownloadInfo.clear();
+
+ // Combine split files
+ if (!mSplitFiles.empty())
+ {
+ this->galaxyInstallGame_MojoSetupHack_CombineSplitFiles(mSplitFiles, true);
+ }
+ }
+ else
+ {
+ std::cout << "No installers found" << std::endl;
+ }
+}
+
+void Downloader::galaxyInstallGame_MojoSetupHack_CombineSplitFiles(const splitFilesMap& mSplitFiles, const bool& bAppendToFirst)
+{
+ for (const auto& baseFile : mSplitFiles)
+ {
+ // Check that all parts exist
+ bool bAllPartsExist = true;
+ for (const auto& splitFile : baseFile.second)
+ {
+ if (!boost::filesystem::exists(splitFile.filepath))
+ {
+ bAllPartsExist = false;
+ break;
+ }
+ }
+
+ bool bBaseFileExists = boost::filesystem::exists(baseFile.first);
+
+ if (!bAllPartsExist)
+ {
+ if (bBaseFileExists)
+ {
+ // Base file exist and we're missing parts.
+ // This should mean that we already have complete file.
+ // So we can safely skip this file without informing the user
+ continue;
+ }
+ else
+ {
+ // Base file doesn't exist and we're missing parts. Print message about it before skipping file.
+ std::cout << baseFile.first << " is missing parts. Skipping this file." << std::endl;
+ continue;
+ }
+ }
+
+ // Delete base file if it already exists
+ if (bBaseFileExists)
+ {
+ std::cout << baseFile.first << " already exists. Deleting old file." << std::endl;
+ if (!boost::filesystem::remove(baseFile.first))
+ {
+ std::cout << baseFile.first << ": Failed to delete" << std::endl;
+ continue;
+ }
+ }
+
+ std::cout << "Beginning to combine " << baseFile.first << std::endl;
+ std::ofstream ofs;
+
+ // Create base file for appending if we aren't appending to first part
+ if (!bAppendToFirst)
+ {
+ ofs.open(baseFile.first, std::ios_base::binary | std::ios_base::app);
+ if (!ofs.is_open())
+ {
+ std::cout << "Failed to create " << baseFile.first << std::endl;
+ continue;
+ }
+ }
+
+ for (const auto& splitFile : baseFile.second)
+ {
+ std::cout << "\t" << splitFile.filepath << std::endl;
+
+ // Append to first file is set and current file is first in vector.
+ // Open file for appending and continue to next file
+ if (bAppendToFirst && (&splitFile == &baseFile.second.front()))
+ {
+ ofs.open(splitFile.filepath, std::ios_base::binary | std::ios_base::app);
+ if (!ofs.is_open())
+ {
+ std::cout << "Failed to open " << splitFile.filepath << std::endl;
+ break;
+ }
+ continue;
+ }
+
+ std::ifstream ifs(splitFile.filepath, std::ios_base::binary);
+ if (!ifs)
+ {
+ std::cout << "Failed to open " << splitFile.filepath << ". Deleting incomplete file." << std::endl;
+
+ ofs.close();
+ if (!boost::filesystem::remove(baseFile.first))
+ {
+ std::cout << baseFile.first << ": Failed to delete" << std::endl;
+ }
+ break;
+ }
+
+ ofs << ifs.rdbuf();
+ ifs.close();
+
+ // Delete split file
+ if (!boost::filesystem::remove(splitFile.filepath))
+ {
+ std::cout << splitFile.filepath << ": Failed to delete" << std::endl;
+ }
+ }
+
+ if (ofs)
+ ofs.close();
+
+ // Appending to first file so we must rename it
+ if (bAppendToFirst)
+ {
+ boost::filesystem::path splitFilePath = baseFile.second.front().filepath;
+ boost::filesystem::path baseFilePath = baseFile.first;
+
+ boost::system::error_code ec;
+ boost::filesystem::rename(splitFilePath, baseFilePath, ec);
+ if (ec)
+ {
+ std::cout << "Failed to rename " << splitFilePath.string() << "to " << baseFilePath.string();
+ }
+ }
+ }
+
+ return;
+}
+
+void Downloader::processGalaxyDownloadQueue_MojoSetupHack(Config conf, const unsigned int& tid)
+{
+ std::string msg_prefix = "[Thread #" + std::to_string(tid) + "]";
+
+ CURL* dlhandle = curl_easy_init();
+ curl_easy_setopt(dlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(dlhandle, CURLOPT_USERAGENT, conf.curlConf.sUserAgent.c_str());
+ curl_easy_setopt(dlhandle, CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(dlhandle, CURLOPT_NOSIGNAL, 1);
+
+ curl_easy_setopt(dlhandle, CURLOPT_CONNECTTIMEOUT, conf.curlConf.iTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(dlhandle, CURLOPT_SSL_VERIFYPEER, conf.curlConf.bVerifyPeer);
+ curl_easy_setopt(dlhandle, CURLOPT_VERBOSE, conf.curlConf.bVerbose);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+ curl_easy_setopt(dlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, conf.curlConf.iDownloadRate);
+ curl_easy_setopt(dlhandle, CURLOPT_FILETIME, 1L);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_TIME, conf.curlConf.iLowSpeedTimeout);
+ curl_easy_setopt(dlhandle, CURLOPT_LOW_SPEED_LIMIT, conf.curlConf.iLowSpeedTimeoutRate);
+
+ if (!conf.curlConf.sCACertPath.empty())
+ curl_easy_setopt(dlhandle, CURLOPT_CAINFO, conf.curlConf.sCACertPath.c_str());
+
+ xferInfo xferinfo;
+ xferinfo.tid = tid;
+ xferinfo.curlhandle = dlhandle;
+
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFOFUNCTION, Downloader::progressCallbackForThread);
+ curl_easy_setopt(dlhandle, CURLOPT_XFERINFODATA, &xferinfo);
+
+ zipFileEntry zfe;
+ while (dlQueueGalaxy_MojoSetupHack.try_pop(zfe))
+ {
+ vDownloadInfo[tid].setStatus(DLSTATUS_STARTING);
+ iTotalRemainingBytes.fetch_sub(zfe.comp_size);
+
+ boost::filesystem::path path = zfe.filepath;
+ boost::filesystem::path path_tmp = zfe.filepath + ".lgogdltmp";
+
+ // Check that directory exists and create it
+ boost::filesystem::path directory = path.parent_path();
+ mtx_create_directories.lock(); // Use mutex to avoid possible race conditions
+ if (boost::filesystem::exists(directory))
+ {
+ if (!boost::filesystem::is_directory(directory))
+ {
+ msgQueue.push(Message(directory.string() + " is not directory", MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ mtx_create_directories.unlock();
+ return;
+ }
+ }
+ else
+ {
+ if (!boost::filesystem::create_directories(directory))
+ {
+ msgQueue.push(Message("Failed to create directory: " + directory.string(), MSGTYPE_ERROR, msg_prefix));
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ mtx_create_directories.unlock();
+ return;
+ }
+ }
+ mtx_create_directories.unlock();
+
+ vDownloadInfo[tid].setFilename(path.string());
+
+ if (ZipUtil::isSymlink(zfe.file_attributes))
+ {
+ if (boost::filesystem::is_symlink(path))
+ {
+ msgQueue.push(Message("Symlink already exists: " + path.string(), MSGTYPE_INFO, msg_prefix));
+ continue;
+ }
+ }
+ else
+ {
+ if (zfe.isSplitFile)
+ {
+ if (boost::filesystem::exists(zfe.splitFileBasePath))
+ {
+ msgQueue.push(Message(path.string() + ": Complete file (" + zfe.splitFileBasePath + ") of split file exists. Checking if it is same version.", MSGTYPE_INFO, msg_prefix));
+
+ std::string crc32 = Util::getFileHashRange(zfe.splitFileBasePath, RHASH_CRC32, zfe.splitFileStartOffset, zfe.splitFileEndOffset);
+ if (crc32 == Util::formattedString("%08x", zfe.crc32))
+ {
+ msgQueue.push(Message(path.string() + ": Complete file (" + zfe.splitFileBasePath + ") of split file is same version. Skipping file.", MSGTYPE_INFO, msg_prefix));
+ continue;
+ }
+ else
+ {
+ msgQueue.push(Message(path.string() + ": Complete file (" + zfe.splitFileBasePath + ") of split file is different version. Continuing to download file.", MSGTYPE_INFO, msg_prefix));
+ }
+ }
+ }
+
+ if (boost::filesystem::exists(path))
+ {
+ if (conf.bVerbose)
+ msgQueue.push(Message("File already exists: " + path.string(), MSGTYPE_INFO, msg_prefix));
+
+ off_t filesize = static_cast<off_t>(boost::filesystem::file_size(path));
+ if (filesize == zfe.uncomp_size)
+ {
+ // File is same size
+ if (Util::getFileHash(path.string(), RHASH_CRC32) == Util::formattedString("%08x", zfe.crc32))
+ {
+ msgQueue.push(Message(path.string() + ": OK", MSGTYPE_SUCCESS, msg_prefix));
+ continue;
+ }
+ else
+ {
+ msgQueue.push(Message(path.string() + ": CRC32 mismatch. Deleting old file.", MSGTYPE_WARNING, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+ else
+ {
+ // File size mismatch
+ msgQueue.push(Message(path.string() + ": File size mismatch. Deleting old file.", MSGTYPE_INFO, msg_prefix));
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+ }
+
+ off_t resume_from = 0;
+ if (boost::filesystem::exists(path_tmp))
+ {
+ off_t filesize = static_cast<off_t>(boost::filesystem::file_size(path_tmp));
+ if (filesize < zfe.comp_size)
+ {
+ // Continue
+ resume_from = filesize;
+ }
+ else
+ {
+ // Delete old file
+ msgQueue.push(Message(path_tmp.string() + ": Deleting old file.", MSGTYPE_INFO, msg_prefix));
+ if (!boost::filesystem::remove(path_tmp))
+ {
+ msgQueue.push(Message(path_tmp.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+
+ std::string url = zfe.installer_url;
+ std::string dlrange = std::to_string(zfe.start_offset_mojosetup) + "-" + std::to_string(zfe.end_offset);
+ curl_easy_setopt(dlhandle, CURLOPT_URL, url.c_str());
+ if (ZipUtil::isSymlink(zfe.file_attributes))
+ {
+ // Symlink
+ std::stringstream symlink_compressed;
+ std::stringstream symlink_uncompressed;
+ std::string link_target;
+
+ CURLcode result = CURLE_RECV_ERROR;
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, &symlink_compressed);
+ curl_easy_setopt(dlhandle, CURLOPT_RANGE, dlrange.c_str());
+
+ vDownloadInfo[tid].setFilename(path.string());
+
+ if (conf.iWait > 0)
+ usleep(conf.iWait); // Delay the request by specified time
+
+ xferinfo.offset = 0;
+ xferinfo.timer.reset();
+ xferinfo.TimeAndSize.clear();
+
+ result = curl_easy_perform(dlhandle);
+
+ if (result != CURLE_OK)
+ {
+ symlink_compressed.str(std::string());
+ msgQueue.push(Message(path.string() + ": Failed to download", MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+
+ int res = ZipUtil::extractStream(&symlink_compressed, &symlink_uncompressed);
+ symlink_compressed.str(std::string());
+
+ if (res != 0)
+ {
+ std::string msg = "Extraction failed (";
+ switch (res)
+ {
+ case 1:
+ msg += "invalid input stream";
+ break;
+ case 2:
+ msg += "unsupported compression method";
+ break;
+ case 3:
+ msg += "invalid output stream";
+ break;
+ case 4:
+ msg += "zlib error";
+ break;
+ default:
+ msg += "unknown error";
+ break;
+ }
+ msg += ")";
+
+ msgQueue.push(Message(msg + " " + path.string(), MSGTYPE_ERROR, msg_prefix));
+ symlink_uncompressed.str(std::string());
+ continue;
+ }
+
+ link_target = symlink_uncompressed.str();
+ symlink_uncompressed.str(std::string());
+
+ if (!link_target.empty())
+ {
+ if (!boost::filesystem::exists(path))
+ {
+ if (conf.bVerbose)
+ msgQueue.push(Message(path.string() + ": Creating symlink to " + link_target, MSGTYPE_INFO, msg_prefix));
+ boost::filesystem::create_symlink(link_target, path);
+ }
+ }
+ }
+ else
+ {
+ // Download file
+ CURLcode result = CURLE_RECV_ERROR;
+
+ off_t max_size_memory = 5 << 20; // 5MB
+ if (zfe.comp_size < max_size_memory) // Handle small files in memory
+ {
+ std::ofstream ofs(path.string(), std::ofstream::out | std::ofstream::binary);
+ if (!ofs)
+ {
+ msgQueue.push(Message("Failed to create " + path_tmp.string(), MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+
+ std::stringstream data_compressed;
+ vDownloadInfo[tid].setFilename(path.string());
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, &data_compressed);
+ curl_easy_setopt(dlhandle, CURLOPT_RANGE, dlrange.c_str());
+
+ xferinfo.offset = 0;
+ xferinfo.timer.reset();
+ xferinfo.TimeAndSize.clear();
+
+ result = curl_easy_perform(dlhandle);
+
+ if (result != CURLE_OK)
+ {
+ data_compressed.str(std::string());
+ ofs.close();
+ msgQueue.push(Message(path.string() + ": Failed to download", MSGTYPE_ERROR, msg_prefix));
+ if (boost::filesystem::exists(path) && boost::filesystem::is_regular_file(path))
+ {
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ continue;
+ }
+
+ int res = ZipUtil::extractStream(&data_compressed, &ofs);
+ data_compressed.str(std::string());
+ ofs.close();
+
+ if (res != 0)
+ {
+ std::string msg = "Extraction failed (";
+ switch (res)
+ {
+ case 1:
+ msg += "invalid input stream";
+ break;
+ case 2:
+ msg += "unsupported compression method";
+ break;
+ case 3:
+ msg += "invalid output stream";
+ break;
+ case 4:
+ msg += "zlib error";
+ break;
+ default:
+ msg += "unknown error";
+ break;
+ }
+ msg += ")";
+
+ msgQueue.push(Message(msg + " " + path.string(), MSGTYPE_ERROR, msg_prefix));
+ data_compressed.str(std::string());
+ if (boost::filesystem::exists(path) && boost::filesystem::is_regular_file(path))
+ {
+ if (!boost::filesystem::remove(path))
+ {
+ msgQueue.push(Message(path.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ continue;
+ }
+
+ if (boost::filesystem::exists(path))
+ {
+ // Set file permission
+ boost::filesystem::perms permissions = ZipUtil::getBoostFilePermission(zfe.file_attributes);
+ Util::setFilePermissions(path, permissions);
+
+ // Set timestamp
+ if (zfe.timestamp > 0)
+ boost::filesystem::last_write_time(path, zfe.timestamp);
+ }
+ }
+ else // Use temporary file for bigger files
+ {
+ vDownloadInfo[tid].setFilename(path_tmp.string());
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEFUNCTION, Downloader::writeData);
+ curl_easy_setopt(dlhandle, CURLOPT_READFUNCTION, Downloader::readData);
+
+ int iRetryCount = 0;
+ do
+ {
+ if (iRetryCount != 0)
+ msgQueue.push(Message("Retry " + std::to_string(iRetryCount) + "/" + std::to_string(conf.iRetries) + ": " + path_tmp.filename().string(), MSGTYPE_INFO, msg_prefix));
+
+
+ FILE* outfile;
+ // File exists, resume
+ if (resume_from > 0)
+ {
+ if ((outfile=fopen(path_tmp.string().c_str(), "r+"))!=NULL)
+ {
+ fseek(outfile, 0, SEEK_END);
+ dlrange = std::to_string(zfe.start_offset_mojosetup + resume_from) + "-" + std::to_string(zfe.end_offset);
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+ curl_easy_setopt(dlhandle, CURLOPT_RANGE, dlrange.c_str());
+ }
+ else
+ {
+ msgQueue.push(Message("Failed to open " + path_tmp.string(), MSGTYPE_ERROR, msg_prefix));
+ break;
+ }
+ }
+ else // File doesn't exist, create new file
+ {
+ if ((outfile=fopen(path_tmp.string().c_str(), "w"))!=NULL)
+ {
+ curl_easy_setopt(dlhandle, CURLOPT_WRITEDATA, outfile);
+ curl_easy_setopt(dlhandle, CURLOPT_RANGE, dlrange.c_str());
+ }
+ else
+ {
+ msgQueue.push(Message("Failed to create " + path_tmp.string(), MSGTYPE_ERROR, msg_prefix));
+ break;
+ }
+ }
+
+ if (conf.iWait > 0)
+ usleep(conf.iWait); // Delay the request by specified time
+
+ xferinfo.offset = 0;
+ xferinfo.timer.reset();
+ xferinfo.TimeAndSize.clear();
+ result = curl_easy_perform(dlhandle);
+ fclose(outfile);
+
+ if (result == CURLE_PARTIAL_FILE || result == CURLE_OPERATION_TIMEDOUT || result == CURLE_RECV_ERROR)
+ {
+ iRetryCount++;
+ if (boost::filesystem::exists(path_tmp) && boost::filesystem::is_regular_file(path_tmp))
+ resume_from = static_cast<off_t>(boost::filesystem::file_size(path_tmp));
+ }
+
+ } while ((result == CURLE_PARTIAL_FILE || result == CURLE_OPERATION_TIMEDOUT || result == CURLE_RECV_ERROR) && (iRetryCount <= conf.iRetries));
+
+ if (result == CURLE_OK)
+ {
+ // Extract file
+ int res = ZipUtil::extractFile(path_tmp.string(), path.string());
+ if (res != 0)
+ {
+ std::string msg = "Extraction failed (";
+ switch (res)
+ {
+ case 1:
+ msg += "failed to open input file";
+ break;
+ case 2:
+ msg += "unsupported compression method";
+ break;
+ case 3:
+ msg += "failed to create output file";
+ break;
+ case 4:
+ msg += "zlib error";
+ break;
+ default:
+ msg += "unknown error";
+ break;
+ }
+ msg += ")";
+
+ msgQueue.push(Message(msg + " " + path_tmp.string(), MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ else
+ {
+ if (boost::filesystem::exists(path_tmp) && boost::filesystem::is_regular_file(path_tmp))
+ {
+ if (!boost::filesystem::remove(path_tmp))
+ {
+ msgQueue.push(Message(path_tmp.string() + ": Failed to delete", MSGTYPE_ERROR, msg_prefix));
+ }
+ }
+ }
+
+ // Set file permission
+ boost::filesystem::perms permissions = ZipUtil::getBoostFilePermission(zfe.file_attributes);
+ if (boost::filesystem::exists(path))
+ Util::setFilePermissions(path, permissions);
+ }
+ else
+ {
+ msgQueue.push(Message("Download failed " + path_tmp.string(), MSGTYPE_ERROR, msg_prefix));
+ continue;
+ }
+ }
+ }
+
+ msgQueue.push(Message("Download complete: " + path.string(), MSGTYPE_SUCCESS, msg_prefix));
+ }
+
+ vDownloadInfo[tid].setStatus(DLSTATUS_FINISHED);
+ curl_easy_cleanup(dlhandle);
+
+ return;
+}
+
+int Downloader::mojoSetupGetFileVector(const gameFile& gf, std::vector<zipFileEntry>& vFiles)
+{
+ Json::Value downlinkJson = gogGalaxy->getResponseJson(gf.galaxy_downlink_json_url);
+
+ if (downlinkJson.empty())
+ {
+ std::cerr << "Empty JSON response" << std::endl;
+ return 1;
+ }
+
+ if (!downlinkJson.isMember("downlink"))
+ {
+ std::cerr << "Invalid JSON response" << std::endl;
+ return 1;
+ }
+
+ std::string xml_url;
+ if (downlinkJson.isMember("checksum"))
+ {
+ if (!downlinkJson["checksum"].empty())
+ xml_url = downlinkJson["checksum"].asString();
+ }
+ else
+ {
+ std::cerr << "Invalid JSON response. Response doesn't contain XML url." << std::endl;
+ return 1;
+ }
+
+
+ // Get XML data
+ std::string xml_data = gogGalaxy->getResponse(xml_url);
+ if (xml_data.empty())
+ {
+ std::cerr << "Failed to get XML data" << std::endl;
+ return 1;
+ }
+
+
+ std::uintmax_t file_size = 0;
+ tinyxml2::XMLDocument xml;
+ xml.Parse(xml_data.c_str());
+ tinyxml2::XMLElement *fileElem = xml.FirstChildElement("file");
+
+ if (!fileElem)
+ {
+ std::cerr << "Failed to parse XML data" << std::endl;
+ return 1;
+ }
+ else
+ {
+ std::string total_size = fileElem->Attribute("total_size");
+ try
+ {
+ file_size = std::stoull(total_size);
+ }
+ catch (std::invalid_argument& e)
+ {
+ file_size = 0;
+ }
+
+ if (file_size == 0)
+ {
+ std::cerr << "Failed to get file size" << std::endl;
+ return 1;
+ }
+ }
+
+ std::string installer_url = downlinkJson["downlink"].asString();
+ if (installer_url.empty())
+ {
+ std::cerr << "Failed to get installer url" << std::endl;
+ return 1;
+ }
+
+ off_t head_size = 100 << 10; // 100 kB
+ off_t tail_size = 200 << 10; // 200 kB
+ std::string head_range = "0-" + std::to_string(head_size);
+ std::string tail_range = std::to_string(file_size - tail_size) + "-" + std::to_string(file_size);
+
+ CURLcode result;
+
+ // Get head
+ std::stringstream head;
+ curl_easy_setopt(curlhandle, CURLOPT_URL, installer_url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &head);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, head_range.c_str());
+ result = curl_easy_perform(curlhandle);
+
+ if (result != CURLE_OK)
+ {
+ std::cerr << "Failed to download data" << std::endl;
+ return 1;
+ }
+
+ // Get zip start offset in MojoSetup installer
+ off_t mojosetup_zip_offset = 0;
+ off_t mojosetup_script_size = ZipUtil::getMojoSetupScriptSize(&head);
+ head.seekg(0, head.beg);
+ off_t mojosetup_installer_size = ZipUtil::getMojoSetupInstallerSize(&head);
+ head.str(std::string());
+
+ if (mojosetup_script_size == -1 || mojosetup_installer_size == -1)
+ {
+ std::cerr << "Failed to get Zip offset" << std::endl;
+ return 1;
+ }
+ else
+ {
+ mojosetup_zip_offset = mojosetup_script_size + mojosetup_installer_size;
+ }
+
+ // Get tail
+ std::stringstream tail;
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &tail);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, tail_range.c_str());
+ result = curl_easy_perform(curlhandle);
+
+ if (result != CURLE_OK)
+ {
+ std::cerr << "Failed to download data" << std::endl;
+ return 1;
+ }
+
+ off_t offset_zip_eocd = ZipUtil::getZipEOCDOffset(&tail);
+ off_t offset_zip64_eocd = ZipUtil::getZip64EOCDOffset(&tail);
+
+ if (offset_zip_eocd < 0)
+ {
+ std::cerr << "Failed to find Zip EOCD offset" << std::endl;
+ return 1;
+ }
+
+ zipEOCD eocd = ZipUtil::readZipEOCDStruct(&tail, offset_zip_eocd);
+
+ uint64_t cd_offset = eocd.cd_start_offset;
+ uint64_t cd_total = eocd.total_cd_records;
+
+ if (offset_zip64_eocd >= 0)
+ {
+ zip64EOCD eocd64 = ZipUtil::readZip64EOCDStruct(&tail, offset_zip64_eocd);
+ if (cd_offset == UINT32_MAX)
+ cd_offset = eocd64.cd_offset;
+
+ if (cd_total == UINT16_MAX)
+ cd_total = eocd64.cd_total;
+ }
+
+ off_t cd_offset_in_stream = 0;
+ off_t mojosetup_cd_offset = mojosetup_zip_offset + cd_offset;
+ off_t cd_offset_from_file_end = file_size - mojosetup_cd_offset;
+
+ if (cd_offset_from_file_end > tail_size)
+ {
+ tail.str(std::string());
+ tail_range = std::to_string(mojosetup_cd_offset) + "-" + std::to_string(file_size);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &tail);
+ curl_easy_setopt(curlhandle, CURLOPT_RANGE, tail_range.c_str());
+ result = curl_easy_perform(curlhandle);
+
+ if (result != CURLE_OK)
+ {
+ std::cerr << "Failed to download data" << std::endl;
+ return 1;
+ }
+ }
+ else
+ {
+ cd_offset_in_stream = tail_size - cd_offset_from_file_end;
+ }
+
+ tail.seekg(cd_offset_in_stream, tail.beg);
+ uint32_t signature = ZipUtil::readUInt32(&tail);
+ if (signature != ZIP_CD_HEADER_SIGNATURE)
+ {
+ std::cerr << "Failed to find Zip Central Directory" << std::endl;
+ return 1;
+ }
+
+
+ // Read file entries from Zip Central Directory
+ tail.seekg(cd_offset_in_stream, tail.beg);
+ for (std::uint64_t i = 0; i < cd_total; ++i)
+ {
+ zipCDEntry cd;
+ cd = ZipUtil::readZipCDEntry(&tail);
+
+ zipFileEntry zfe;
+ zfe.filepath = cd.filename;
+ zfe.comp_size = cd.comp_size;
+ zfe.uncomp_size = cd.uncomp_size;
+ zfe.start_offset_zip = cd.disk_offset;
+ zfe.start_offset_mojosetup = zfe.start_offset_zip + mojosetup_zip_offset;
+ zfe.file_attributes = cd.external_file_attr >> 16;
+ zfe.crc32 = cd.crc32;
+ zfe.timestamp = cd.timestamp;
+ zfe.installer_url = installer_url;
+
+ vFiles.push_back(zfe);
+ }
+ tail.str(std::string());
+
+ // Set end offset for all entries
+ vFiles[vFiles.size() - 1].end_offset = mojosetup_cd_offset - 1;
+ for (std::uintmax_t i = 0; i < (vFiles.size() - 1); i++)
+ {
+ vFiles[i].end_offset = vFiles[i+1].start_offset_mojosetup - 1;
+ }
+
+ return 0;
+}
+
+std::string Downloader::getGalaxyInstallDirectory(galaxyAPI *galaxyHandle, const Json::Value& manifest)
+{
+ std::string install_directory = Globals::globalConfig.dirConf.sGalaxyInstallSubdir;
+ std::string product_id = manifest["baseProductId"].asString();
+
+ // Templates for installation subdir
+ std::map<std::string, std::string> templates;
+ templates["%install_dir%"] = manifest["installDirectory"].asString();
+ templates["%product_id%"] = product_id;
+
+ std::vector<std::string> templates_need_info =
+ {
+ "%gamename%",
+ "%title%",
+ "%title_stripped%"
+ };
+
+ if (std::any_of(templates_need_info.begin(), templates_need_info.end(), [install_directory](std::string template_dir){return template_dir == install_directory;}))
+ {
+ Json::Value productInfo = galaxyHandle->getProductInfo(product_id);
+ std::string gamename = productInfo["slug"].asString();
+ std::string title = productInfo["title"].asString();
+
+ if (!gamename.empty())
+ templates["%gamename%"] = productInfo["slug"].asString();
+ if (!title.empty())
+ templates["%title%"] = productInfo["title"].asString();
+ }
+
+ if (templates.count("%install_dir%"))
+ {
+ templates["%install_dir_stripped%"] = Util::getStrippedString(templates["%install_dir%"]);
+ }
+
+ if (templates.count("%title%"))
+ {
+ templates["%title_stripped%"] = Util::getStrippedString(templates["%title%"]);;
+ }
+
+ if (templates.count(install_directory))
+ {
+ install_directory = templates[install_directory];
+ }
+
+ return install_directory;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "galaxyapi.h"
+
+#include <boost/iostreams/filtering_streambuf.hpp>
+#include <boost/iostreams/copy.hpp>
+#include <boost/iostreams/filter/zlib.hpp>
+#include <boost/iostreams/device/back_inserter.hpp>
+#include <sstream>
+
+GalaxyConfig Globals::galaxyConf;
+
+size_t galaxyAPI::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) {
+ std::ostringstream *stream = (std::ostringstream*)userp;
+ std::streamsize count = (std::streamsize) size * nmemb;
+ stream->write(ptr, count);
+ return count;
+}
+
+galaxyAPI::galaxyAPI(CurlConfig& conf)
+{
+ this->curlConf = conf;
+
+ curlhandle = curl_easy_init();
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_PROGRESSDATA, this);
+ curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, curlConf.iTimeout);
+ curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, curlConf.sCookiePath.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, curlConf.sCookiePath.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, curlConf.bVerifyPeer);
+ curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, curlConf.bVerbose);
+ curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, curlConf.iDownloadRate);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, curlConf.iLowSpeedTimeout);
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, curlConf.iLowSpeedTimeoutRate);
+
+ if (!curlConf.sCACertPath.empty())
+ curl_easy_setopt(curlhandle, CURLOPT_CAINFO, curlConf.sCACertPath.c_str());
+}
+
+galaxyAPI::~galaxyAPI()
+{
+ curl_easy_cleanup(curlhandle);
+}
+
+/* Initialize the API
+ returns 0 if failed
+ returns 1 if successful
+*/
+int galaxyAPI::init()
+{
+ int res = 0;
+
+ if (!this->isTokenExpired())
+ {
+ res = 1;
+ }
+ else
+ res = 0;
+
+ return res;
+}
+
+bool galaxyAPI::refreshLogin()
+{
+ std::string refresh_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId()
+ + "&client_secret=" + Globals::galaxyConf.getClientSecret()
+ + "&grant_type=refresh_token"
+ + "&refresh_token=" + Globals::galaxyConf.getRefreshToken();
+
+ Json::Value token_json = this->getResponseJson(refresh_url);
+
+ if (token_json.empty())
+ return false;
+
+ Globals::galaxyConf.setJSON(token_json);
+
+ return true;
+}
+
+bool galaxyAPI::isTokenExpired()
+{
+ bool res = false;
+
+ if (Globals::galaxyConf.isExpired())
+ res = true;
+
+ return res;
+}
+
+std::string galaxyAPI::getResponse(const std::string& url, const bool& zlib_decompress)
+{
+ std::ostringstream memory;
+
+ struct curl_slist *header = NULL;
+
+ std::string access_token;
+ if (!Globals::galaxyConf.isExpired())
+ access_token = Globals::galaxyConf.getAccessToken();
+ if (!access_token.empty())
+ {
+ std::string bearer = "Authorization: Bearer " + access_token;
+ header = curl_slist_append(header, bearer.c_str());
+ }
+ curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, header);
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, galaxyAPI::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+
+ int retries = 0;
+ CURLcode result;
+ std::string response;
+ do
+ {
+ if (Globals::globalConfig.iWait > 0)
+ usleep(Globals::globalConfig.iWait); // Delay the request by specified time
+ result = curl_easy_perform(curlhandle);
+ response = memory.str();
+ memory.str(std::string());
+ }
+ while ((result != CURLE_OK) && response.empty() && (retries++ < Globals::globalConfig.iRetries));
+
+ curl_easy_setopt(curlhandle, CURLOPT_HTTPHEADER, NULL);
+ curl_slist_free_all(header);
+
+ if (zlib_decompress)
+ {
+ std::string response_decompressed;
+ boost::iostreams::filtering_streambuf<boost::iostreams::input> in;
+ in.push(boost::iostreams::zlib_decompressor(GlobalConstants::ZLIB_WINDOW_SIZE));
+ in.push(boost::make_iterator_range(response));
+ boost::iostreams::copy(in, boost::iostreams::back_inserter(response_decompressed));
+ response = response_decompressed;
+ }
+
+ return response;
+}
+
+Json::Value galaxyAPI::getResponseJson(const std::string& url, const bool& zlib_decompress)
+{
+ std::istringstream response(this->getResponse(url, zlib_decompress));
+ Json::Value json;
+
+ if (!response.str().empty())
+ {
+ try
+ {
+ response >> json;
+ }
+ catch(const Json::Exception& exc)
+ {
+ // Failed to parse json response
+ }
+ }
+
+ return json;
+}
+
+Json::Value galaxyAPI::getProductBuilds(const std::string& product_id, const std::string& platform, const std::string& generation)
+{
+ std::string url = "https://content-system.gog.com/products/" + product_id + "/os/" + platform + "/builds?generation=" + generation;
+
+ return this->getResponseJson(url);
+}
+
+Json::Value galaxyAPI::getManifestV1(const std::string& product_id, const std::string& build_id, const std::string& manifest_id, const std::string& platform)
+{
+ std::string url = "https://cdn.gog.com/content-system/v1/manifests/" + product_id + "/" + platform + "/" + build_id + "/" + manifest_id + ".json";
+
+ return this->getManifestV1(url);
+}
+
+Json::Value galaxyAPI::getManifestV1(const std::string& manifest_url)
+{
+ return this->getResponseJson(manifest_url);
+}
+
+Json::Value galaxyAPI::getManifestV2(std::string manifest_hash, const bool& is_dependency)
+{
+ if (!manifest_hash.empty() && manifest_hash.find("/") == std::string::npos)
+ manifest_hash = this->hashToGalaxyPath(manifest_hash);
+
+ std::string url;
+ if (is_dependency)
+ url = "https://cdn.gog.com/content-system/v2/dependencies/meta/" + manifest_hash;
+ else
+ url = "https://cdn.gog.com/content-system/v2/meta/" + manifest_hash;
+
+ return this->getResponseJson(url, true);
+}
+
+Json::Value galaxyAPI::getSecureLink(const std::string& product_id, const std::string& path)
+{
+ std::string url = "https://content-system.gog.com/products/" + product_id + "/secure_link?generation=2&path=" + path + "&_version=2";
+
+ return this->getResponseJson(url);
+}
+
+Json::Value galaxyAPI::getDependencyLink(const std::string& path)
+{
+ std::string url = "https://content-system.gog.com/open_link?generation=2&_version=2&path=/dependencies/store/" + path;
+
+ return this->getResponseJson(url);
+}
+
+
+std::string galaxyAPI::hashToGalaxyPath(const std::string& hash)
+{
+ std::string galaxy_path = hash;
+ if (galaxy_path.find("/") == std::string::npos)
+ galaxy_path.assign(hash.begin(), hash.begin()+2).append("/").append(hash.begin()+2, hash.begin()+4).append("/").append(hash);
+
+ return galaxy_path;
+}
+
+std::vector<galaxyDepotItem> galaxyAPI::getDepotItemsVector(const std::string& hash, const bool& is_dependency)
+{
+ Json::Value json = this->getManifestV2(hash, is_dependency);
+
+ std::vector<galaxyDepotItem> items;
+
+ for (unsigned int i = 0; i < json["depot"]["items"].size(); ++i)
+ {
+ if (!json["depot"]["items"][i]["chunks"].empty())
+ {
+ galaxyDepotItem item;
+ item.totalSizeCompressed = 0;
+ item.totalSizeUncompressed = 0;
+ item.path = json["depot"]["items"][i]["path"].asString();
+ item.isDependency = is_dependency;
+
+ while (Util::replaceString(item.path, "\\", "/"));
+ for (unsigned int j = 0; j < json["depot"]["items"][i]["chunks"].size(); ++j)
+ {
+ galaxyDepotItemChunk chunk;
+ chunk.md5_compressed = json["depot"]["items"][i]["chunks"][j]["compressedMd5"].asString();
+ chunk.md5_uncompressed = json["depot"]["items"][i]["chunks"][j]["md5"].asString();
+ chunk.size_compressed = json["depot"]["items"][i]["chunks"][j]["compressedSize"].asLargestUInt();
+ chunk.size_uncompressed = json["depot"]["items"][i]["chunks"][j]["size"].asLargestUInt();
+
+ chunk.offset_compressed = item.totalSizeCompressed;
+ chunk.offset_uncompressed = item.totalSizeUncompressed;
+
+ item.totalSizeCompressed += chunk.size_compressed;
+ item.totalSizeUncompressed += chunk.size_uncompressed;
+ item.chunks.push_back(chunk);
+ }
+
+ if (json["depot"]["items"][i].isMember("md5"))
+ item.md5 = json["depot"]["items"][i]["md5"].asString();
+ else if (json["depot"]["items"][i]["chunks"].size() == 1)
+ item.md5 = json["depot"]["items"][i]["chunks"][0]["md5"].asString();
+
+ items.push_back(item);
+ }
+ }
+
+ return items;
+}
+
+Json::Value galaxyAPI::getProductInfo(const std::string& product_id)
+{
+ std::string url = "https://api.gog.com/products/" + product_id + "?expand=downloads,expanded_dlcs,description,screenshots,videos,related_products,changelog&locale=en-US";
+
+ return this->getResponseJson(url);
+}
+
+gameDetails galaxyAPI::productInfoJsonToGameDetails(const Json::Value& json, const DownloadConfig& dlConf)
+{
+ gameDetails gamedetails;
+
+ gamedetails.gamename = json["slug"].asString();
+ gamedetails.product_id = json["id"].asString();
+ gamedetails.title = json["title"].asString();
+ gamedetails.icon = "https:" + json["images"]["icon"].asString();
+
+ if (json.isMember("changelog"))
+ gamedetails.changelog = json["changelog"].asString();
+
+ if (dlConf.bInstallers)
+ {
+ gamedetails.installers = this->installerJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["installers"], dlConf);
+ }
+
+ if (dlConf.bExtras)
+ {
+ gamedetails.extras = this->extraJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["bonus_content"]);
+ }
+
+ if (dlConf.bPatches)
+ {
+ gamedetails.patches = this->patchJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["patches"], dlConf);
+ }
+
+ if (dlConf.bLanguagePacks)
+ {
+ gamedetails.languagepacks = this->languagepackJsonNodeToGameFileVector(gamedetails.gamename, json["downloads"]["language_packs"], dlConf);
+ }
+
+ if (dlConf.bDLC)
+ {
+ if (json.isMember("expanded_dlcs"))
+ {
+ for (unsigned int i = 0; i < json["expanded_dlcs"].size(); ++i)
+ {
+ gameDetails dlc_gamedetails = this->productInfoJsonToGameDetails(json["expanded_dlcs"][i], dlConf);
+
+ // Add DLC type to all DLC files
+ for (unsigned int j = 0; j < dlc_gamedetails.installers.size(); ++j)
+ dlc_gamedetails.installers[j].type |= GFTYPE_DLC;
+ for (unsigned int j = 0; j < dlc_gamedetails.extras.size(); ++j)
+ dlc_gamedetails.extras[j].type |= GFTYPE_DLC;
+ for (unsigned int j = 0; j < dlc_gamedetails.patches.size(); ++j)
+ dlc_gamedetails.patches[j].type |= GFTYPE_DLC;
+ for (unsigned int j = 0; j < dlc_gamedetails.languagepacks.size(); ++j)
+ dlc_gamedetails.languagepacks[j].type |= GFTYPE_DLC;
+
+ // Add DLC only if it has any files
+ if (!dlc_gamedetails.installers.empty() || !dlc_gamedetails.extras.empty() || !dlc_gamedetails.patches.empty() || !dlc_gamedetails.languagepacks.empty())
+ gamedetails.dlcs.push_back(dlc_gamedetails);
+ }
+ }
+ }
+
+ return gamedetails;
+}
+
+std::vector<gameFile> galaxyAPI::installerJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+ return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_INSTALLER, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::patchJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+ return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_PATCH, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::languagepackJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const DownloadConfig& dlConf)
+{
+ return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_LANGPACK, dlConf.iInstallerPlatform, dlConf.iInstallerLanguage, dlConf.bDuplicateHandler);
+}
+
+std::vector<gameFile> galaxyAPI::extraJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json)
+{
+ return this->fileJsonNodeToGameFileVector(gamename, json, GFTYPE_EXTRA);
+}
+
+std::vector<gameFile> galaxyAPI::fileJsonNodeToGameFileVector(const std::string& gamename, const Json::Value& json, const unsigned int& type, const unsigned int& platform, const unsigned int& lang, const bool& useDuplicateHandler)
+{
+ std::vector<gameFile> gamefiles;
+ unsigned int iInfoNodes = json.size();
+ for (unsigned int i = 0; i < iInfoNodes; ++i)
+ {
+ Json::Value infoNode = json[i];
+ unsigned int iFiles = infoNode["files"].size();
+ std::string name = infoNode["name"].asString();
+
+ unsigned int iPlatform = GlobalConstants::PLATFORM_WINDOWS;
+ unsigned int iLanguage = GlobalConstants::LANGUAGE_EN;
+ if (!(type & GFTYPE_EXTRA))
+ {
+ iPlatform = Util::getOptionValue(infoNode["os"].asString(), GlobalConstants::PLATFORMS);
+ iLanguage = Util::getOptionValue(infoNode["language"].asString(), GlobalConstants::LANGUAGES);
+
+ if (!(iPlatform & platform))
+ continue;
+
+ if (!(iLanguage & lang))
+ continue;
+ }
+
+ for (unsigned int j = 0; j < iFiles; ++j)
+ {
+ Json::Value fileNode = infoNode["files"][j];
+ std::string downlink = fileNode["downlink"].asString();
+
+ Json::Value downlinkJson = this->getResponseJson(downlink);
+ if (downlinkJson.empty())
+ continue;
+
+ std::string downlink_url = downlinkJson["downlink"].asString();
+ std::string path = this->getPathFromDownlinkUrl(downlink_url, gamename);
+
+ gameFile gf;
+ gf.gamename = gamename;
+ gf.type = type;
+ gf.id = fileNode["id"].asString();
+ gf.name = name;
+ gf.path = path;
+ gf.size = Util::getJsonUIntValueAsString(fileNode["size"]);
+ gf.updated = 0; // assume not updated
+ gf.galaxy_downlink_json_url = downlink;
+
+ if (!(type & GFTYPE_EXTRA))
+ {
+ gf.platform = iPlatform;
+ gf.language = iLanguage;
+
+ if (useDuplicateHandler)
+ {
+ bool bDuplicate = false;
+ for (unsigned int k = 0; k < gamefiles.size(); ++k)
+ {
+ if (gamefiles[k].path == gf.path)
+ {
+ gamefiles[k].language |= gf.language; // Add language code to installer
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (bDuplicate)
+ continue;
+ }
+ }
+
+ gamefiles.push_back(gf);
+ }
+ }
+
+ return gamefiles;
+}
+
+Json::Value galaxyAPI::getUserData()
+{
+ std::string url = "https://embed.gog.com/userData.json";
+
+ return this->getResponseJson(url);
+}
+
+Json::Value galaxyAPI::getDependenciesJson()
+{
+ std::string url = "https://content-system.gog.com/dependencies/repository?generation=2";
+ Json::Value dependencies;
+ Json::Value repository = this->getResponseJson(url);
+
+ if (!repository.empty())
+ {
+ if (repository.isMember("repository_manifest"))
+ {
+ std::string manifest_url = repository["repository_manifest"].asString();
+ dependencies = this->getResponseJson(manifest_url, true);
+ }
+ }
+
+ return dependencies;
+}
+
+std::vector<galaxyDepotItem> galaxyAPI::getFilteredDepotItemsVectorFromJson(const Json::Value& depot_json, const std::string& galaxy_language, const std::string& galaxy_arch, const bool& is_dependency)
+{
+ std::vector<galaxyDepotItem> items;
+
+ bool bSelectedLanguage = false;
+ bool bSelectedArch = false;
+ boost::regex language_re("^(" + galaxy_language + ")$", boost::regex::perl | boost::regex::icase);
+ boost::match_results<std::string::const_iterator> what;
+ for (unsigned int j = 0; j < depot_json["languages"].size(); ++j)
+ {
+ std::string language = depot_json["languages"][j].asString();
+ if (language == "*" || boost::regex_search(language, what, language_re))
+ bSelectedLanguage = true;
+ }
+
+ if (depot_json.isMember("osBitness"))
+ {
+ for (unsigned int j = 0; j < depot_json["osBitness"].size(); ++j)
+ {
+ std::string osBitness = depot_json["osBitness"][j].asString();
+ if (osBitness == "*" || osBitness == galaxy_arch)
+ bSelectedArch = true;
+ }
+ }
+ else
+ {
+ // No osBitness found, assume that we want this depot
+ bSelectedArch = true;
+ }
+
+ if (bSelectedLanguage && bSelectedArch)
+ {
+ std::string depotHash = depot_json["manifest"].asString();
+ std::string depot_product_id = depot_json["productId"].asString();
+
+ items = this->getDepotItemsVector(depotHash, is_dependency);
+
+ // Set product id for items
+ if (!depot_product_id.empty())
+ {
+ for (auto it = items.begin(); it != items.end(); ++it)
+ {
+ it->product_id = depot_product_id;
+ }
+ }
+ }
+
+ return items;
+}
+
+std::string galaxyAPI::getPathFromDownlinkUrl(const std::string& downlink_url, const std::string& gamename)
+{
+ std::string path;
+ std::string downlink_url_unescaped = (std::string)curl_easy_unescape(curlhandle, downlink_url.c_str(), downlink_url.size(), NULL);
+
+ // GOG has changed the url formatting few times between 2 different formats.
+ // Try to get proper file name in both cases.
+ size_t filename_end_pos;
+ if (downlink_url_unescaped.find("?path=") != std::string::npos)
+ {
+ size_t token_pos = downlink_url_unescaped.find("&token=");
+ size_t access_token_pos = downlink_url_unescaped.find("&access_token=");
+ if ((token_pos != std::string::npos) && (access_token_pos != std::string::npos))
+ {
+ filename_end_pos = std::min(token_pos, access_token_pos);
+ }
+ else
+ {
+ filename_end_pos = downlink_url_unescaped.find_first_of("&");
+ }
+ }
+ else
+ filename_end_pos = downlink_url_unescaped.find_first_of("?");
+
+ if (downlink_url_unescaped.find("/" + gamename + "/") != std::string::npos)
+ {
+ path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find("/" + gamename + "/"), downlink_url_unescaped.begin()+filename_end_pos);
+ }
+ else
+ {
+ path.assign(downlink_url_unescaped.begin()+downlink_url_unescaped.find_last_of("/")+1, downlink_url_unescaped.begin()+filename_end_pos);
+ path = "/" + gamename + "/" + path;
+ }
+
+ // Workaround for filename issue caused by different (currently unknown) url formatting scheme
+ // https://github.com/Sude-/lgogdownloader/issues/126
+ if (path.find("?") != std::string::npos)
+ {
+ if (path.find_last_of("?") > path.find_last_of("/"))
+ {
+ path.assign(path.begin(), path.begin()+path.find_last_of("?"));
+ }
+ }
+
+ return path;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "gamedetails.h"
+
+gameDetails::gameDetails()
+{
+ //ctor
+}
+
+gameDetails::~gameDetails()
+{
+ //dtor
+}
+
+void gameDetails::filterWithPriorities(const gameSpecificConfig& config)
+{
+ if (config.dlConf.vPlatformPriority.empty() && config.dlConf.vLanguagePriority.empty())
+ return;
+
+ filterListWithPriorities(installers, config);
+ filterListWithPriorities(patches, config);
+ filterListWithPriorities(languagepacks, config);
+ for (unsigned int i = 0; i < dlcs.size(); ++i)
+ {
+ filterListWithPriorities(dlcs[i].installers, config);
+ filterListWithPriorities(dlcs[i].patches, config);
+ filterListWithPriorities(dlcs[i].languagepacks, config);
+ }
+}
+
+void gameDetails::filterListWithPriorities(std::vector<gameFile>& list, const gameSpecificConfig& config)
+{
+ /*
+ Compute the score of each item - we use a scoring mechanism and we keep all ties
+ Like if someone asked French then English and Linux then Windows, but there are
+ only Windows French, Windows English and Linux English versions, we'll get the
+ Windows French and Linux English ones.
+ Score is inverted: lower is better.
+ */
+ int bestscore = -1;
+
+ for (std::vector<gameFile>::iterator fileDetails = list.begin(); fileDetails != list.end(); fileDetails++)
+ {
+ fileDetails->score = 0;
+ if (!config.dlConf.vPlatformPriority.empty())
+ {
+ for (size_t i = 0; i != config.dlConf.vPlatformPriority.size(); i++)
+ if (fileDetails->platform & config.dlConf.vPlatformPriority[i])
+ {
+ fileDetails->score += i;
+ break;
+ }
+ }
+ if (!config.dlConf.vLanguagePriority.empty())
+ {
+ for (size_t i = 0; i != config.dlConf.vLanguagePriority.size(); i++)
+ if (fileDetails->language & config.dlConf.vLanguagePriority[i])
+ {
+ fileDetails->score += i;
+ break;
+ }
+ }
+ if ((fileDetails->score < bestscore) or (bestscore < 0))
+ bestscore = fileDetails->score;
+ }
+
+ for (std::vector<gameFile>::iterator fileDetails = list.begin(); fileDetails != list.end(); )
+ {
+ if (fileDetails->score > bestscore)
+ fileDetails = list.erase(fileDetails);
+ else
+ fileDetails++;
+ }
+}
+
+void gameDetails::makeFilepaths(const DirectoryConfig& config)
+{
+ std::string filepath;
+ std::string directory = config.sDirectory + "/" + config.sGameSubdir + "/";
+ std::string subdir;
+ this->serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0);
+ this->changelogFilepath = Util::makeFilepath(directory, "changelog_" + gamename + ".html", this->gamename, subdir, 0);
+
+ for (unsigned int i = 0; i < this->installers.size(); ++i)
+ {
+ subdir = config.bSubDirectories ? config.sInstallersSubdir : "";
+ filepath = Util::makeFilepath(directory, this->installers[i].path, this->gamename, subdir, this->installers[i].platform);
+ this->installers[i].setFilepath(filepath);
+ }
+
+ for (unsigned int i = 0; i < this->extras.size(); ++i)
+ {
+ subdir = config.bSubDirectories ? config.sExtrasSubdir : "";
+ filepath = Util::makeFilepath(directory, this->extras[i].path, this->gamename, subdir, 0);
+ this->extras[i].setFilepath(filepath);
+ }
+
+ for (unsigned int i = 0; i < this->patches.size(); ++i)
+ {
+ subdir = config.bSubDirectories ? config.sPatchesSubdir : "";
+ filepath = Util::makeFilepath(directory, this->patches[i].path, this->gamename, subdir, this->patches[i].platform);
+ this->patches[i].setFilepath(filepath);
+ }
+
+ for (unsigned int i = 0; i < this->languagepacks.size(); ++i)
+ {
+ subdir = config.bSubDirectories ? config.sLanguagePackSubdir : "";
+ filepath = Util::makeFilepath(directory, this->languagepacks[i].path, this->gamename, subdir, 0);
+ this->languagepacks[i].setFilepath(filepath);
+ }
+
+ for (unsigned int i = 0; i < this->dlcs.size(); ++i)
+ {
+ subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : "";
+ this->dlcs[i].serialsFilepath = Util::makeFilepath(directory, "serials.txt", this->gamename, subdir, 0);
+ this->dlcs[i].changelogFilepath = Util::makeFilepath(directory, "changelog_" + this->dlcs[i].gamename + ".html", this->gamename, subdir, 0);
+ for (unsigned int j = 0; j < this->dlcs[i].installers.size(); ++j)
+ {
+ subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sInstallersSubdir : "";
+ filepath = Util::makeFilepath(directory, this->dlcs[i].installers[j].path, this->gamename, subdir, this->dlcs[i].installers[j].platform, this->dlcs[i].gamename);
+ this->dlcs[i].installers[j].setFilepath(filepath);
+ }
+
+ for (unsigned int j = 0; j < this->dlcs[i].patches.size(); ++j)
+ {
+ subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sPatchesSubdir : "";
+ filepath = Util::makeFilepath(directory, this->dlcs[i].patches[j].path, this->gamename, subdir, this->dlcs[i].patches[j].platform, this->dlcs[i].gamename);
+ this->dlcs[i].patches[j].setFilepath(filepath);
+ }
+
+ for (unsigned int j = 0; j < this->dlcs[i].extras.size(); ++j)
+ {
+ subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sExtrasSubdir : "";
+ filepath = Util::makeFilepath(directory, this->dlcs[i].extras[j].path, this->gamename, subdir, 0, this->dlcs[i].gamename);
+ this->dlcs[i].extras[j].setFilepath(filepath);
+ }
+
+ for (unsigned int j = 0; j < this->dlcs[i].languagepacks.size(); ++j)
+ {
+ subdir = config.bSubDirectories ? config.sDLCSubdir + "/" + config.sLanguagePackSubdir : "";
+ filepath = Util::makeFilepath(directory, this->dlcs[i].languagepacks[j].path, this->gamename, subdir, 0, this->dlcs[i].gamename);
+ this->dlcs[i].languagepacks[j].setFilepath(filepath);
+ }
+ }
+}
+
+Json::Value gameDetails::getDetailsAsJson()
+{
+ Json::Value json;
+
+ json["gamename"] = this->gamename;
+ json["product_id"] = this->product_id;
+ json["title"] = this->title;
+ json["icon"] = this->icon;
+ json["serials"] = this->serials;
+ json["changelog"] = this->changelog;
+
+ for (unsigned int i = 0; i < this->extras.size(); ++i)
+ json["extras"].append(this->extras[i].getAsJson());
+ for (unsigned int i = 0; i < this->installers.size(); ++i)
+ json["installers"].append(this->installers[i].getAsJson());
+ for (unsigned int i = 0; i < this->patches.size(); ++i)
+ json["patches"].append(this->patches[i].getAsJson());
+ for (unsigned int i = 0; i < this->languagepacks.size(); ++i)
+ json["languagepacks"].append(this->languagepacks[i].getAsJson());
+
+ if (!this->dlcs.empty())
+ {
+ for (unsigned int i = 0; i < this->dlcs.size(); ++i)
+ {
+ json["dlcs"].append(this->dlcs[i].getDetailsAsJson());
+ }
+ }
+
+ return json;
+}
+
+std::string gameDetails::getSerialsFilepath()
+{
+ return this->serialsFilepath;
+}
+
+std::string gameDetails::getChangelogFilepath()
+{
+ return this->changelogFilepath;
+}
+
+// Return vector containing all game files
+std::vector<gameFile> gameDetails::getGameFileVector()
+{
+ std::vector<gameFile> vGameFiles;
+
+ vGameFiles.insert(std::end(vGameFiles), std::begin(installers), std::end(installers));
+ vGameFiles.insert(std::end(vGameFiles), std::begin(patches), std::end(patches));
+ vGameFiles.insert(std::end(vGameFiles), std::begin(extras), std::end(extras));
+ vGameFiles.insert(std::end(vGameFiles), std::begin(languagepacks), std::end(languagepacks));
+
+ if (!dlcs.empty())
+ {
+ for (unsigned int i = 0; i < dlcs.size(); ++i)
+ {
+ std::vector<gameFile> vGameFilesDLC = dlcs[i].getGameFileVector();
+ vGameFiles.insert(std::end(vGameFiles), std::begin(vGameFilesDLC), std::end(vGameFilesDLC));
+ }
+ }
+
+ return vGameFiles;
+}
+
+// Return vector containing all game files matching download filters
+std::vector<gameFile> gameDetails::getGameFileVectorFiltered(const unsigned int& iType)
+{
+ std::vector<gameFile> vGameFiles = this->getGameFileVector();
+
+ std::remove_if(
+ vGameFiles.begin(),
+ vGameFiles.end(),
+ [iType](gameFile gf)
+ {
+ bool bRemove = false;
+ if (gf.type & iType)
+ {
+ // Remove if DLC but DLCs not enabled
+ if ( !((iType & GFTYPE_DLC) & iType) )
+ bRemove = true;
+ }
+ else
+ {
+ bRemove = true;
+ }
+ return bRemove;
+ }
+ );
+
+ return vGameFiles;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "gamefile.h"
+
+gameFile::gameFile()
+{
+ this->platform = GlobalConstants::PLATFORM_WINDOWS;
+ this->language = GlobalConstants::LANGUAGE_EN;
+ this->silent = 0;
+ this->type = 0;
+}
+
+gameFile::~gameFile()
+{
+ //dtor
+}
+
+void gameFile::setFilepath(const std::string& path)
+{
+ this->filepath = path;
+}
+
+std::string gameFile::getFilepath()
+{
+ return this->filepath;
+}
+
+Json::Value gameFile::getAsJson()
+{
+ Json::Value json;
+
+ json["updated"] = this->updated;
+ json["id"] = this->id;
+ json["name"] = this->name;
+ json["path"] = this->path;
+ json["size"] = this->size;
+ json["platform"] = this->platform;
+ json["language"] = this->language;
+ json["silent"] = this->silent;
+ json["gamename"] = this->gamename;
+ json["type"] = this->type;
+ json["galaxy_downlink_json_url"] = this->galaxy_downlink_json_url;
+
+ return json;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "gui_login.h"
+
+#include <QApplication>
+#include <QtWidgets/QWidget>
+#include <QtWebEngineWidgets/QWebEngineView>
+#include <QLayout>
+#include <QWebEngineProfile>
+
+GuiLogin::GuiLogin()
+{
+ // constructor
+}
+
+GuiLogin::~GuiLogin()
+{
+ // destructor
+}
+
+void GuiLogin::loadFinished(bool success)
+{
+ QWebEngineView *view = qobject_cast<QWebEngineView*>(sender());
+ std::string url = view->page()->url().toString().toUtf8().constData();
+
+ // Autofill login form
+ if (success && url.find("https://login.gog.com/auth?client_id=") != std::string::npos)
+ {
+ if (!this->login_username.empty() && !this->login_password.empty())
+ {
+ std::string js_fill_username = "document.getElementById(\"login_username\").value = \"" + this->login_username + "\";";
+ std::string js_fill_password = "document.getElementById(\"login_password\").value = \"" + this->login_password + "\";";
+ std::string js = js_fill_username + js_fill_password;
+ view->page()->runJavaScript(QString::fromStdString(js));
+ }
+ }
+ // Get auth code
+ else if (success && url.find("https://embed.gog.com/on_login_success") != std::string::npos)
+ {
+ std::string find_str = "code=";
+ auto pos = url.find(find_str);
+ if (pos != std::string::npos)
+ {
+ pos += find_str.length();
+ std::string code;
+ code.assign(url.begin()+pos, url.end());
+ if (!code.empty())
+ {
+ this->auth_code = code;
+ QCoreApplication::exit();
+ }
+ }
+ }
+}
+
+void GuiLogin::cookieAdded(const QNetworkCookie& cookie)
+{
+ std::string raw_cookie = cookie.toRawForm().toStdString();
+ if (!raw_cookie.empty())
+ {
+ std::string set_cookie = "Set-Cookie: " + raw_cookie;
+ bool duplicate = false;
+ for (auto cookie : this->cookies)
+ {
+ if (set_cookie == cookie)
+ {
+ duplicate = true;
+ break;
+ }
+ }
+ if (!duplicate)
+ this->cookies.push_back(set_cookie);
+ }
+}
+
+void GuiLogin::Login()
+{
+ this->Login(std::string(), std::string());
+}
+
+void GuiLogin::Login(const std::string& username, const std::string& password)
+{
+ QByteArray redirect_uri = QUrl::toPercentEncoding(QString::fromStdString(Globals::galaxyConf.getRedirectUri()));
+ std::string auth_url = "https://auth.gog.com/auth?client_id=" + Globals::galaxyConf.getClientId() + "&redirect_uri=" + redirect_uri.toStdString() + "&response_type=code";
+ QUrl url = QString::fromStdString(auth_url);
+ this->login_username = username;
+ this->login_password = password;
+
+ std::vector<char> version_string(
+ Globals::globalConfig.sVersionString.c_str(),
+ Globals::globalConfig.sVersionString.c_str() + Globals::globalConfig.sVersionString.size() + 1
+ );
+
+ int argc = 1;
+ char *argv[] = {&version_string[0]};
+ QApplication app(argc, argv);
+
+ QWidget window;
+ QVBoxLayout *layout = new QVBoxLayout;
+ window.resize(440, 540);
+
+ QWebEngineView *webengine = new QWebEngineView(&window);
+ layout->addWidget(webengine);
+ QWebEngineProfile profile;
+ profile.setHttpUserAgent(QString::fromStdString(Globals::globalConfig.curlConf.sUserAgent));
+ QWebEnginePage page(&profile);
+ cookiestore = profile.cookieStore();
+
+ QObject::connect(
+ webengine, SIGNAL(loadFinished(bool)),
+ this, SLOT(loadFinished(bool))
+ );
+
+ QObject::connect(
+ this->cookiestore, SIGNAL(cookieAdded(const QNetworkCookie&)),
+ this, SLOT(cookieAdded(const QNetworkCookie&))
+ );
+
+ webengine->resize(window.frameSize());
+ webengine->setPage(&page);
+ webengine->setUrl(url);
+
+ window.setLayout(layout);
+ window.show();
+
+ app.exec();
+}
+
+std::string GuiLogin::getCode()
+{
+ return this->auth_code;
+}
+
+std::vector<std::string> GuiLogin::getCookies()
+{
+ return this->cookies;
+}
+
+#include "moc_gui_login.cpp"
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "progressbar.h"
+#include <cmath>
+#include <sstream>
+
+ProgressBar::ProgressBar(bool bUnicode, bool bColor)
+:
+ // Based on block characters.
+ // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Block_elements
+ // u8"\u2591" - you can try using this ("light shade") instead of space, but it looks worse,
+ // since partial bar has no shade behind it.
+ m_bar_chars
+ {
+ " ", // 0/8
+ u8"\u258F", // 1/8
+ u8"\u258E", // 2/8
+ u8"\u258D", // 3/8
+ u8"\u258C", // 4/8
+ u8"\u258B", // 5/8
+ u8"\u258A", // 6/8
+ u8"\u2589", // 7/8
+ u8"\u2588" /* 8/8 */
+ },
+ m_left_border(u8"\u2595"), // right 1/8th
+ m_right_border(u8"\u258F"), // left 1/8th
+ m_simple_left_border("["),
+ m_simple_right_border("]"),
+ m_simple_empty_fill(" "),
+ m_simple_bar_char("="),
+ // using vt100 escape sequences for colors... See http://ascii-table.com/ansi-escape-sequences.php
+ m_bar_color("\033[1;34m"),
+ m_border_color("\033[1;37m"),
+ COLOR_RESET("\033[0m"),
+ m_use_unicode(bUnicode),
+ m_use_color(bColor)
+{ }
+
+ProgressBar::~ProgressBar()
+{
+ //dtor
+}
+
+void ProgressBar::draw(unsigned int length, double fraction)
+{
+ std::cout << createBarString(length, fraction);
+}
+
+std::string ProgressBar::createBarString(unsigned int length, double fraction)
+{
+ std::ostringstream ss;
+ // validation
+ if (!std::isnormal(fraction) || (fraction < 0.0)) fraction = 0.0;
+ else if (fraction > 1.0) fraction = 1.0;
+
+ double bar_part = fraction * length;
+ double whole_bar_chars = std::floor(bar_part);
+ unsigned int whole_bar_chars_i = (unsigned int) whole_bar_chars;
+ // The bar uses symbols graded with 1/8
+ unsigned int partial_bar_char_index = (unsigned int) std::floor((bar_part - whole_bar_chars) * 8.0);
+
+ // left border
+ if (m_use_color) ss << m_border_color;
+ ss << (m_use_unicode ? m_left_border : m_simple_left_border);
+
+ // whole completed bars
+ if (m_use_color) ss << m_bar_color;
+ unsigned int i = 0;
+ for (; i < whole_bar_chars_i; i++)
+ {
+ ss << (m_use_unicode ? m_bar_chars[8] : m_simple_bar_char);
+ }
+
+ // partial completed bar
+ if (i < length) ss << (m_use_unicode ? m_bar_chars[partial_bar_char_index] : m_simple_empty_fill);
+
+ // whole unfinished bars
+ if (m_use_color) ss << COLOR_RESET;
+ for (i = whole_bar_chars_i + 1; i < length; i++)
+ { // first entry in m_bar_chars is assumed to be the empty bar
+ ss << (m_use_unicode ? m_bar_chars[0] : m_simple_empty_fill);
+ }
+
+ // right border
+ if (m_use_color) ss << m_border_color;
+ ss << (m_use_unicode ? m_right_border : m_simple_right_border);
+ if (m_use_color) ss << COLOR_RESET;
+
+ return ss.str();
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "util.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <tinyxml2.h>
+#include <json/json.h>
+#include <fstream>
+#include <sys/ioctl.h>
+
+/*
+ Create filepath from specified directory and path
+ Remove the leading slash from path if needed
+ Use gamename as base directory if specified
+*/
+std::string Util::makeFilepath(const std::string& directory, const std::string& path, const std::string& gamename, std::string subdirectory, const unsigned int& platformId, const std::string& dlcname)
+{
+ std::string dir = directory + makeRelativeFilepath(path, gamename, subdirectory);
+ Util::filepathReplaceReservedStrings(dir, gamename, platformId, dlcname);
+ return dir;
+}
+
+/* Create filepath relative to download base directory specified in config.
+ */
+std::string Util::makeRelativeFilepath(const std::string& path, const std::string& gamename, std::string subdirectory)
+{
+ std::string filepath;
+
+ if (gamename.empty())
+ {
+ if (path.at(0)=='/')
+ {
+ std::string tmp_path = path.substr(1,path.length());
+ filepath = tmp_path;
+ }
+ else
+ {
+ filepath = path;
+ }
+ }
+ else
+ {
+ std::string filename = path.substr(path.find_last_of("/")+1, path.length());
+ if (!subdirectory.empty())
+ {
+ subdirectory = "/" + subdirectory;
+ }
+ filepath = subdirectory + "/" + filename;
+ }
+
+ return filepath;
+}
+
+std::string Util::getFileHash(const std::string& filename, unsigned hash_id)
+{
+ unsigned char digest[rhash_get_digest_size(hash_id)];
+ char result[rhash_get_hash_length(hash_id) + 1];
+
+ int i = rhash_file(hash_id, filename.c_str(), digest);
+ if (i < 0)
+ std::cerr << "LibRHash error: " << strerror(errno) << std::endl;
+ else
+ rhash_print_bytes(result, digest, rhash_get_digest_size(hash_id), RHPR_HEX);
+
+ return result;
+}
+
+std::string Util::getFileHashRange(const std::string& filepath, unsigned hash_id, off_t range_start, off_t range_end)
+{
+ char result[rhash_get_hash_length(hash_id) + 1];
+
+ if (!boost::filesystem::exists(filepath))
+ return result;
+
+ off_t filesize = boost::filesystem::file_size(filepath);
+
+ if (range_end == 0 || range_end > filesize)
+ range_end = filesize;
+
+ if (range_end < range_start)
+ {
+ off_t tmp = range_start;
+ range_start = range_end;
+ range_end = tmp;
+ }
+
+ off_t chunk_size = 10 << 20; // 10MB
+ off_t rangesize = range_end - range_start;
+ off_t remaining = rangesize % chunk_size;
+ int chunks = (remaining == 0) ? rangesize/chunk_size : (rangesize/chunk_size)+1;
+
+ rhash rhash_context;
+ rhash_context = rhash_init(hash_id);
+
+ FILE *infile = fopen(filepath.c_str(), "r");
+
+ for (int i = 0; i < chunks; i++)
+ {
+ off_t chunk_begin = range_start + i*chunk_size;
+ fseek(infile, chunk_begin, SEEK_SET);
+ if ((i == chunks-1) && (remaining != 0))
+ chunk_size = remaining;
+
+ unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+ if (chunk == NULL)
+ {
+ std::cerr << "Memory error" << std::endl;
+ fclose(infile);
+ return result;
+ }
+ off_t size = fread(chunk, 1, chunk_size, infile);
+ if (size != chunk_size)
+ {
+ std::cerr << "Read error" << std::endl;
+ free(chunk);
+ fclose(infile);
+ return result;
+ }
+
+ rhash_update(rhash_context, chunk, chunk_size);
+ free(chunk);
+ }
+ fclose(infile);
+
+ rhash_final(rhash_context, NULL);
+ rhash_print(result, rhash_context, hash_id, RHPR_HEX);
+ rhash_free(rhash_context);
+
+ return result;
+}
+
+std::string Util::getChunkHash(unsigned char *chunk, uintmax_t chunk_size, unsigned hash_id)
+{
+ unsigned char digest[rhash_get_digest_size(hash_id)];
+ char result[rhash_get_hash_length(hash_id) + 1];
+
+ int i = rhash_msg(hash_id, chunk, chunk_size, digest);
+ if (i < 0)
+ std::cerr << "LibRHash error: " << strerror(errno) << std::endl;
+ else
+ rhash_print_bytes(result, digest, rhash_get_digest_size(hash_id), RHPR_HEX);
+
+ return result;
+}
+
+// Create GOG XML
+int Util::createXML(std::string filepath, uintmax_t chunk_size, std::string xml_dir)
+{
+ int res = 0;
+ FILE *infile;
+ FILE *xmlfile;
+ uintmax_t filesize, size;
+ int chunks, i;
+
+ if (xml_dir.empty())
+ {
+ xml_dir = Util::getCacheHome() + "/lgogdownloader/xml";
+ }
+
+ // Make sure directory exists
+ boost::filesystem::path path = xml_dir;
+ if (!boost::filesystem::exists(path)) {
+ if (!boost::filesystem::create_directories(path)) {
+ std::cerr << "Failed to create directory: " << path << std::endl;
+ return res;
+ }
+ }
+
+ if ((infile=fopen(filepath.c_str(), "r"))!=NULL) {
+ //File exists
+ fseek(infile, 0, SEEK_END);
+ filesize = ftell(infile);
+ rewind(infile);
+ } else {
+ std::cerr << filepath << " doesn't exist" << std::endl;
+ return res;
+ }
+
+ // Get filename
+ boost::filesystem::path pathname = filepath;
+ std::string filename = pathname.filename().string();
+ std::string filenameXML = xml_dir + "/" + filename + ".xml";
+
+ std::cout << filename << std::endl;
+ //Determine number of chunks
+ int remaining = filesize % chunk_size;
+ chunks = (remaining == 0) ? filesize/chunk_size : (filesize/chunk_size)+1;
+ std::cout << "Filesize: " << filesize << " bytes" << std::endl
+ << "Chunks: " << chunks << std::endl
+ << "Chunk size: " << (chunk_size >> 20) << " MB" << std::endl;
+
+ tinyxml2::XMLDocument xml;
+ tinyxml2::XMLElement *fileElem = xml.NewElement("file");
+ fileElem->SetAttribute("name", filename.c_str());
+ fileElem->SetAttribute("chunks", chunks);
+ fileElem->SetAttribute("total_size", std::to_string(filesize).c_str());
+
+ std::cout << "Getting MD5 for chunks" << std::endl;
+
+ rhash rhash_context;
+ rhash_context = rhash_init(RHASH_MD5);
+ if(!rhash_context)
+ {
+ std::cerr << "error: couldn't initialize rhash context" << std::endl;
+ fclose(infile);
+ return res;
+ }
+ char rhash_result[rhash_get_hash_length(RHASH_MD5) + 1];
+
+ for (i = 0; i < chunks; i++) {
+ uintmax_t range_begin = i*chunk_size;
+ fseek(infile, range_begin, SEEK_SET);
+ if ((i == chunks-1) && (remaining != 0))
+ chunk_size = remaining;
+ uintmax_t range_end = range_begin + chunk_size - 1;
+ unsigned char *chunk = (unsigned char *) malloc(chunk_size * sizeof(unsigned char *));
+ if (chunk == NULL)
+ {
+ std::cerr << "Memory error" << std::endl;
+ fclose(infile);
+ return res;
+ }
+ size = fread(chunk, 1, chunk_size, infile);
+ if (size != chunk_size)
+ {
+ std::cerr << "Read error" << std::endl;
+ free(chunk);
+ fclose(infile);
+ return res;
+ }
+
+ std::string hash = Util::getChunkHash(chunk, chunk_size, RHASH_MD5);
+ rhash_update(rhash_context, chunk, chunk_size); // Update hash for the whole file
+
+ free(chunk);
+
+ tinyxml2::XMLElement *chunkElem = xml.NewElement("chunk");
+ chunkElem->SetAttribute("id", i);
+ chunkElem->SetAttribute("from", std::to_string(range_begin).c_str());
+ chunkElem->SetAttribute("to", std::to_string(range_end).c_str());
+ chunkElem->SetAttribute("method", "md5");
+ tinyxml2::XMLText *text = xml.NewText(hash.c_str());
+ chunkElem->LinkEndChild(text);
+ fileElem->LinkEndChild(chunkElem);
+
+ std::cout << "Chunks hashed " << (i+1) << " / " << chunks << "\r" << std::flush;
+ }
+ fclose(infile);
+
+ rhash_final(rhash_context, NULL);
+ rhash_print(rhash_result, rhash_context, RHASH_MD5, RHPR_HEX);
+ rhash_free(rhash_context);
+
+ std::cout << std::endl << "MD5: " << rhash_result << std::endl;
+ fileElem->SetAttribute("md5", rhash_result);
+
+ xml.LinkEndChild(fileElem);
+
+ std::cout << "Writing XML: " << filenameXML << std::endl;
+ if ((xmlfile=fopen(filenameXML.c_str(), "w"))!=NULL) {
+ tinyxml2::XMLPrinter printer(xmlfile);
+ xml.Print(&printer);
+ fclose(xmlfile);
+ res = 1;
+ } else {
+ std::cerr << "Can't create " << filenameXML << std::endl;
+ return res;
+ }
+
+ return res;
+}
+
+/*
+ Overrides global settings with game specific settings
+ returns 0 if fails
+ returns number of changed settings if succesful
+*/
+int Util::getGameSpecificConfig(std::string gamename, gameSpecificConfig* conf, std::string directory)
+{
+ int res = 0;
+
+ if (directory.empty())
+ {
+ directory = Util::getConfigHome() + "/lgogdownloader/gamespecific";
+ }
+
+ std::string filepath = directory + "/" + gamename + ".conf";
+
+ // Make sure file exists
+ boost::filesystem::path path = filepath;
+ if (!boost::filesystem::exists(path)) {
+ return res;
+ }
+
+ std::ifstream json(filepath, std::ifstream::binary);
+ Json::Value root;
+ try {
+ json >> root;
+ } catch (const Json::Exception& exc) {
+ std::cerr << "Failed to parse game specific config " << filepath << std::endl;
+ std::cerr << exc.what() << std::endl;
+ return res;
+ }
+
+ if (root.isMember("language"))
+ {
+ if (root["language"].isInt())
+ conf->dlConf.iInstallerLanguage = root["language"].asUInt();
+ else
+ {
+ Util::parseOptionString(root["language"].asString(), conf->dlConf.vLanguagePriority, conf->dlConf.iInstallerLanguage, GlobalConstants::LANGUAGES);
+ }
+ res++;
+ }
+ if (root.isMember("platform"))
+ {
+ if (root["platform"].isInt())
+ conf->dlConf.iInstallerPlatform = root["platform"].asUInt();
+ else
+ {
+ Util::parseOptionString(root["platform"].asString(), conf->dlConf.vPlatformPriority, conf->dlConf.iInstallerPlatform, GlobalConstants::PLATFORMS);
+ }
+ res++;
+ }
+ if (root.isMember("dlc"))
+ {
+ conf->dlConf.bDLC = root["dlc"].asBool();
+ res++;
+ }
+ if (root.isMember("ignore-dlc-count"))
+ {
+ conf->dlConf.bIgnoreDLCCount = root["ignore-dlc-count"].asBool();
+ res++;
+ }
+ if (root.isMember("subdirectories"))
+ {
+ conf->dirConf.bSubDirectories = root["subdirectories"].asBool();
+ res++;
+ }
+ if (root.isMember("directory"))
+ {
+ conf->dirConf.sDirectory = root["directory"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-game"))
+ {
+ conf->dirConf.sGameSubdir = root["subdir-game"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-installers"))
+ {
+ conf->dirConf.sInstallersSubdir = root["subdir-installers"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-extras"))
+ {
+ conf->dirConf.sExtrasSubdir = root["subdir-extras"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-patches"))
+ {
+ conf->dirConf.sPatchesSubdir = root["subdir-patches"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-language-packs"))
+ {
+ conf->dirConf.sLanguagePackSubdir = root["subdir-language-packs"].asString();
+ res++;
+ }
+ if (root.isMember("subdir-dlc"))
+ {
+ conf->dirConf.sDLCSubdir = root["subdir-dlc"].asString();
+ res++;
+ }
+ return res;
+}
+
+int Util::replaceString(std::string& str, const std::string& to_replace, const std::string& replace_with)
+{
+ size_t pos = str.find(to_replace);
+ if (pos == std::string::npos)
+ {
+ return 0;
+ }
+ str.replace(str.begin()+pos, str.begin()+pos+to_replace.length(), replace_with);
+ return 1;
+}
+
+void Util::filepathReplaceReservedStrings(std::string& str, const std::string& gamename, const unsigned int& platformId, const std::string& dlcname)
+{
+ std::string platform;
+ for (unsigned int i = 0; i < GlobalConstants::PLATFORMS.size(); ++i)
+ {
+ if ((platformId & GlobalConstants::PLATFORMS[i].id) == GlobalConstants::PLATFORMS[i].id)
+ {
+ platform = boost::algorithm::to_lower_copy(GlobalConstants::PLATFORMS[i].str);
+ break;
+ }
+ }
+ if (platform.empty())
+ {
+ if (str.find("%gamename%/%platform%") != std::string::npos)
+ platform = "";
+ else
+ platform = "no_platform";
+ }
+
+ while (Util::replaceString(str, "%gamename%", gamename));
+ while (Util::replaceString(str, "%dlcname%", dlcname));
+ while (Util::replaceString(str, "%platform%", platform));
+ while (Util::replaceString(str, "//", "/")); // Replace any double slashes with single slash
+}
+
+void Util::setFilePermissions(const boost::filesystem::path& path, const boost::filesystem::perms& permissions)
+{
+ if (boost::filesystem::exists(path))
+ {
+ if (boost::filesystem::is_regular_file(path))
+ {
+ boost::filesystem::file_status s = boost::filesystem::status(path);
+ if (s.permissions() != permissions)
+ {
+ boost::system::error_code ec;
+ boost::filesystem::permissions(path, permissions, ec);
+ if (ec)
+ {
+ std::cerr << "Failed to set file permissions for " << path.string() << std::endl;
+ }
+ }
+ }
+ }
+}
+
+int Util::getTerminalWidth()
+{
+ int width;
+ if(isatty(STDOUT_FILENO))
+ {
+ struct winsize w;
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
+ width = static_cast<int>(w.ws_col);
+ }
+ else
+ width = 10000;//Something sufficiently big
+ return width;
+}
+
+
+void Util::getDownloaderUrlsFromJSON(const Json::Value &root, std::vector<std::string> &urls)
+{
+ if(root.size() > 0) {
+ for(Json::ValueConstIterator it = root.begin() ; it != root.end() ; ++it)
+ {
+ if (it.key() == "downloaderUrl")
+ {
+ Json::Value url = *it;
+ urls.push_back(url.asString());
+ }
+ else
+ getDownloaderUrlsFromJSON(*it, urls);
+ }
+ }
+ return;
+}
+
+std::vector<std::string> Util::getDLCNamesFromJSON(const Json::Value &root)
+{
+ std::vector<std::string> urls, dlcnames;
+ getDownloaderUrlsFromJSON(root, urls);
+
+ for (unsigned int i = 0; i < urls.size(); ++i)
+ {
+ std::string gamename;
+ if (urls[i].find(GlobalConstants::PROTOCOL_PREFIX) == std::string::npos)
+ continue;
+
+ gamename.assign(urls[i].begin()+urls[i].find(GlobalConstants::PROTOCOL_PREFIX)+GlobalConstants::PROTOCOL_PREFIX.length(), urls[i].begin()+urls[i].find_last_of("/"));
+ bool bDuplicate = false;
+ for (unsigned int j = 0; j < dlcnames.size(); ++j)
+ {
+ if (gamename == dlcnames[j])
+ {
+ bDuplicate = true;
+ break;
+ }
+ }
+ if (!bDuplicate)
+ dlcnames.push_back(gamename);
+ }
+ return dlcnames;
+}
+
+std::string Util::getHomeDir()
+{
+ return (std::string)getenv("HOME");
+}
+
+std::string Util::getConfigHome()
+{
+ std::string configHome;
+ char *xdgconfig = getenv("XDG_CONFIG_HOME");
+ if (xdgconfig)
+ configHome = (std::string)xdgconfig;
+ else
+ configHome = Util::getHomeDir() + "/.config";
+ return configHome;
+}
+
+std::string Util::getCacheHome()
+{
+ std::string cacheHome;
+ char *xdgcache = getenv("XDG_CACHE_HOME");
+ if (xdgcache)
+ cacheHome = (std::string)xdgcache;
+ else
+ cacheHome = Util::getHomeDir() + "/.cache";
+ return cacheHome;
+}
+
+std::vector<std::string> Util::tokenize(const std::string& str, const std::string& separator)
+{
+ std::vector<std::string> tokens;
+ std::string token;
+ size_t idx = 0, found;
+ while ((found = str.find(separator, idx)) != std::string::npos)
+ {
+ token = str.substr(idx, found - idx);
+ if (!token.empty())
+ tokens.push_back(token);
+ idx = found + separator.length();
+ }
+ token = str.substr(idx);
+ if (!token.empty())
+ tokens.push_back(token);
+
+ return tokens;
+}
+
+unsigned int Util::getOptionValue(const std::string& str, const std::vector<GlobalConstants::optionsStruct>& options, const bool& bAllowStringToIntConversion)
+{
+ unsigned int value = 0;
+ boost::regex expression("^[+-]?\\d+$", boost::regex::perl);
+ boost::match_results<std::string::const_iterator> what;
+ if (str == "all")
+ {
+ value = (1 << options.size()) - 1;
+ }
+ else if (boost::regex_search(str, what, expression) && bAllowStringToIntConversion)
+ {
+ value = std::stoi(str);
+ }
+ else
+ {
+ for (unsigned int i = 0; i < options.size(); ++i)
+ {
+ if (!options[i].regexp.empty())
+ {
+ boost::regex expr("^(" + options[i].regexp + ")$", boost::regex::perl | boost::regex::icase);
+ if (boost::regex_search(str, what, expr))
+ {
+ value = options[i].id;
+ break;
+ }
+ }
+ else if (str == options[i].code)
+ {
+ value = options[i].id;
+ break;
+ }
+ }
+ }
+ return value;
+}
+
+std::string Util::getOptionNameString(const unsigned int& value, const std::vector<GlobalConstants::optionsStruct>& options)
+{
+ std::string str;
+ for (unsigned int i = 0; i < options.size(); ++i)
+ {
+ if (value & options[i].id)
+ str += (str.empty() ? "" : ", ")+options[i].str;
+ }
+ return str;
+}
+
+// Parse the options string
+void Util::parseOptionString(const std::string &option_string, std::vector<unsigned int> &priority, unsigned int &type, const std::vector<GlobalConstants::optionsStruct>& options)
+{
+ type = 0;
+ priority.clear();
+ std::vector<std::string> tokens_priority = Util::tokenize(option_string, ",");
+ for (std::vector<std::string>::iterator it_priority = tokens_priority.begin(); it_priority != tokens_priority.end(); it_priority++)
+ {
+ unsigned int value = 0;
+ std::vector<std::string> tokens_value = Util::tokenize(*it_priority, "+");
+ for (std::vector<std::string>::iterator it_value = tokens_value.begin(); it_value != tokens_value.end(); it_value++)
+ {
+ value |= Util::getOptionValue(*it_value, options);
+ }
+ priority.push_back(value);
+ type |= value;
+ }
+}
+
+std::string Util::getLocalFileHash(const std::string& xml_dir, const std::string& filepath, const std::string& gamename)
+{
+ std::string localHash;
+ boost::filesystem::path path = filepath;
+ boost::filesystem::path local_xml_file;
+ if (!gamename.empty())
+ local_xml_file = xml_dir + "/" + gamename + "/" + path.filename().string() + ".xml";
+ else
+ local_xml_file = xml_dir + "/" + path.filename().string() + ".xml";
+
+ if (boost::filesystem::exists(local_xml_file))
+ {
+ tinyxml2::XMLDocument local_xml;
+ local_xml.LoadFile(local_xml_file.string().c_str());
+ tinyxml2::XMLElement *fileElem = local_xml.FirstChildElement("file");
+
+ if (fileElem)
+ {
+ localHash = fileElem->Attribute("md5");
+ }
+ }
+ else if (boost::filesystem::exists(path) && boost::filesystem::is_regular_file(path))
+ {
+ localHash = Util::getFileHash(path.string(), RHASH_MD5);
+ }
+
+ return localHash;
+}
+
+void Util::shortenStringToTerminalWidth(std::string& str)
+{
+ int iStrLen = static_cast<int>(str.length());
+ int iTermWidth = Util::getTerminalWidth();
+ if (iStrLen >= iTermWidth)
+ {
+ size_t chars_to_remove = (iStrLen - iTermWidth) + 4;
+ size_t middle = iStrLen / 2;
+ size_t pos1 = middle - (chars_to_remove / 2);
+ size_t pos2 = middle + (chars_to_remove / 2);
+ str.replace(str.begin()+pos1, str.begin()+pos2, "...");
+ }
+}
+
+std::string Util::getJsonUIntValueAsString(const Json::Value& json_value)
+{
+ std::string value;
+ try
+ {
+ value = json_value.asString();
+ }
+ catch (...)
+ {
+ try
+ {
+ uintmax_t value_uint = json_value.asLargestUInt();
+ value = std::to_string(value_uint);
+ }
+ catch (...)
+ {
+ value = "";
+ }
+ }
+
+ return value;
+}
+
+std::string Util::getStrippedString(std::string str)
+{
+ str.erase(
+ std::remove_if(str.begin(), str.end(),
+ [](unsigned char c)
+ {
+ bool bIsValid = false;
+ bIsValid = (std::isspace(c) && std::isprint(c)) || std::isalnum(c);
+ std::vector<unsigned char> validChars = { '-', '_', '.', '(', ')', '[', ']', '{', '}' };
+ if (std::any_of(validChars.begin(), validChars.end(), [c](unsigned char x){return x == c;}))
+ {
+ bIsValid = true;
+ }
+ return !bIsValid;
+ }
+ ),
+ str.end()
+ );
+ return str;
+}
+
+std::string Util::makeEtaString(const unsigned long long& iBytesRemaining, const double& dlRate)
+{
+ boost::posix_time::time_duration duration(boost::posix_time::seconds((long)(iBytesRemaining / dlRate)));
+
+ return Util::makeEtaString(duration);
+}
+
+std::string Util::makeEtaString(const boost::posix_time::time_duration& duration)
+{
+ std::string etastr;
+ std::stringstream eta_ss;
+
+ if (duration.hours() > 23)
+ {
+ eta_ss << duration.hours() / 24 << "d " <<
+ std::setfill('0') << std::setw(2) << duration.hours() % 24 << "h " <<
+ std::setfill('0') << std::setw(2) << duration.minutes() << "m " <<
+ std::setfill('0') << std::setw(2) << duration.seconds() << "s";
+ }
+ else if (duration.hours() > 0)
+ {
+ eta_ss << duration.hours() << "h " <<
+ std::setfill('0') << std::setw(2) << duration.minutes() << "m " <<
+ std::setfill('0') << std::setw(2) << duration.seconds() << "s";
+ }
+ else if (duration.minutes() > 0)
+ {
+ eta_ss << duration.minutes() << "m " <<
+ std::setfill('0') << std::setw(2) << duration.seconds() << "s";
+ }
+ else
+ {
+ eta_ss << duration.seconds() << "s";
+ }
+ etastr = eta_ss.str();
+
+ return etastr;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "website.h"
+#include "globalconstants.h"
+
+#include <htmlcxx/html/ParserDom.h>
+#include <boost/algorithm/string/case_conv.hpp>
+
+#ifdef USE_QT_GUI_LOGIN
+ #include "gui_login.h"
+#endif
+
+Website::Website()
+{
+ this->retries = 0;
+
+ curlhandle = curl_easy_init();
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_USERAGENT, Globals::globalConfig.curlConf.sUserAgent.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_NOSIGNAL, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_CONNECTTIMEOUT, Globals::globalConfig.curlConf.iTimeout);
+ curl_easy_setopt(curlhandle, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIEFILE, Globals::globalConfig.curlConf.sCookiePath.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIEJAR, Globals::globalConfig.curlConf.sCookiePath.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_SSL_VERIFYPEER, Globals::globalConfig.curlConf.bVerifyPeer);
+ curl_easy_setopt(curlhandle, CURLOPT_VERBOSE, Globals::globalConfig.curlConf.bVerbose);
+ curl_easy_setopt(curlhandle, CURLOPT_MAX_RECV_SPEED_LARGE, Globals::globalConfig.curlConf.iDownloadRate);
+
+ // Assume that we have connection error and abort transfer with CURLE_OPERATION_TIMEDOUT if download speed is less than 200 B/s for 30 seconds
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_TIME, 30);
+ curl_easy_setopt(curlhandle, CURLOPT_LOW_SPEED_LIMIT, 200);
+
+ if (!Globals::globalConfig.curlConf.sCACertPath.empty())
+ curl_easy_setopt(curlhandle, CURLOPT_CAINFO, Globals::globalConfig.curlConf.sCACertPath.c_str());
+}
+
+Website::~Website()
+{
+ curl_easy_cleanup(curlhandle);
+}
+
+size_t Website::writeMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp)
+{
+ std::ostringstream *stream = (std::ostringstream*)userp;
+ size_t count = size * nmemb;
+ stream->write(ptr, count);
+ return count;
+}
+
+std::string Website::getResponse(const std::string& url)
+{
+ std::ostringstream memory;
+ std::string response;
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+
+ CURLcode result;
+ do
+ {
+ if (Globals::globalConfig.iWait > 0)
+ usleep(Globals::globalConfig.iWait); // Delay the request by specified time
+ result = curl_easy_perform(curlhandle);
+ response = memory.str();
+ memory.str(std::string());
+ }
+ while ((result != CURLE_OK) && response.empty() && (this->retries++ < Globals::globalConfig.iRetries));
+ this->retries = 0; // reset retries counter
+
+ if (result != CURLE_OK)
+ {
+ std::cout << curl_easy_strerror(result) << std::endl;
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ long int response_code = 0;
+ result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ std::cout << "HTTP ERROR: ";
+ if (result == CURLE_OK)
+ std::cout << response_code << " (" << url << ")" << std::endl;
+ else
+ std::cout << "failed to get error code: " << curl_easy_strerror(result) << " (" << url << ")" << std::endl;
+ }
+ else if (result == CURLE_SSL_CACERT)
+ {
+ std::cout << "Try using CA certificate bundle from cURL: https://curl.haxx.se/ca/cacert.pem" << std::endl;
+ std::cout << "Use --cacert to set the path for CA certificate bundle" << std::endl;
+ }
+ }
+
+ return response;
+}
+
+Json::Value Website::getGameDetailsJSON(const std::string& gameid)
+{
+ std::string gameDataUrl = "https://www.gog.com/account/gameDetails/" + gameid + ".json";
+ std::string json = this->getResponse(gameDataUrl);
+
+ // Parse JSON
+ Json::Value root;
+ std::istringstream json_stream(json);
+
+ try {
+ json_stream >> root;
+ } catch(const Json::Exception& exc) {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << json << std::endl;
+ #endif
+ std::cout << exc.what();
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getGameDetailsJSON)" << std::endl << root << std::endl;
+ #endif
+
+ return root;
+}
+
+// Get list of games from account page
+std::vector<gameItem> Website::getGames()
+{
+ std::vector<gameItem> games;
+ Json::Value root;
+ int i = 1;
+ bool bAllPagesParsed = false;
+ int iUpdated = Globals::globalConfig.bUpdated ? 1 : 0;
+ int iHidden = 0;
+
+ do
+ {
+ std::string response = this->getResponse("https://www.gog.com/account/getFilteredProducts?hiddenFlag=" + std::to_string(iHidden) + "&isUpdated=" + std::to_string(iUpdated) + "&mediaType=1&sortBy=title&system=&page=" + std::to_string(i));
+ std::istringstream json_stream(response);
+
+ try {
+ // Parse JSON
+ json_stream >> root;
+ } catch (const Json::Exception& exc) {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << response << std::endl;
+ #endif
+ std::cout << exc.what();
+ if (!response.empty())
+ {
+ if(response[0] != '{')
+ {
+ // Response was not JSON. Assume that cookies have expired.
+ std::cerr << "Response was not JSON. Cookies have most likely expired. Try --login first." << std::endl;
+ }
+ }
+ exit(1);
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getGames)" << std::endl << root << std::endl;
+ #endif
+ if (root["page"].asInt() == root["totalPages"].asInt() || root["totalPages"].asInt() == 0)
+ bAllPagesParsed = true;
+
+ // Make the next loop handle hidden products
+ if (Globals::globalConfig.bIncludeHiddenProducts && bAllPagesParsed && iHidden == 0)
+ {
+ bAllPagesParsed = false;
+ iHidden = 1;
+ i = 0; // Set to 0 because we increment it at the end of the loop
+ }
+
+ if (root["products"].isArray())
+ {
+ for (unsigned int i = 0; i < root["products"].size(); ++i)
+ {
+ std::cerr << "\033[KGetting game names " << "(" << root["page"].asInt() << "/" << root["totalPages"].asInt() << ") " << i+1 << " / " << root["products"].size() << "\r" << std::flush;
+ Json::Value product = root["products"][i];
+ gameItem game;
+ game.name = product["slug"].asString();
+ game.id = product["id"].isInt() ? std::to_string(product["id"].asInt()) : product["id"].asString();
+
+ if (product.isMember("updates"))
+ {
+ if (product["updates"].isNull())
+ {
+ /* In some cases the value can be null.
+ * For example when user owns a dlc but not the base game
+ * https://github.com/Sude-/lgogdownloader/issues/101
+ * Assume that there are no updates in this case */
+ game.updates = 0;
+ }
+ else if (product["updates"].isInt())
+ game.updates = product["updates"].asInt();
+ else
+ {
+ try
+ {
+ game.updates = std::stoi(product["updates"].asString());
+ }
+ catch (std::invalid_argument& e)
+ {
+ game.updates = 0; // Assume no updates
+ }
+ }
+ }
+
+ unsigned int platform = 0;
+ if (product["worksOn"]["Windows"].asBool())
+ platform |= GlobalConstants::PLATFORM_WINDOWS;
+ if (product["worksOn"]["Mac"].asBool())
+ platform |= GlobalConstants::PLATFORM_MAC;
+ if (product["worksOn"]["Linux"].asBool())
+ platform |= GlobalConstants::PLATFORM_LINUX;
+
+ // Skip if platform doesn't match
+ if (Globals::globalConfig.bPlatformDetection && !(platform & Globals::globalConfig.dlConf.iInstallerPlatform))
+ continue;
+
+ // Filter the game list
+ if (!Globals::globalConfig.sGameRegex.empty())
+ {
+ boost::regex expression(Globals::globalConfig.sGameRegex);
+ boost::match_results<std::string::const_iterator> what;
+ if (!boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
+ continue;
+ }
+
+ if (Globals::globalConfig.dlConf.bDLC)
+ {
+ int dlcCount = product["dlcCount"].asInt();
+
+ bool bDownloadDLCInfo = (dlcCount != 0);
+
+ if (!bDownloadDLCInfo && !Globals::globalConfig.sIgnoreDLCCountRegex.empty())
+ {
+ boost::regex expression(Globals::globalConfig.sIgnoreDLCCountRegex);
+ boost::match_results<std::string::const_iterator> what;
+ if (boost::regex_search(game.name, what, expression)) // Check if name matches the specified regex
+ {
+ bDownloadDLCInfo = true;
+ }
+ }
+
+ if (!bDownloadDLCInfo && !Globals::globalConfig.gamehasdlc.empty())
+ {
+ if (Globals::globalConfig.gamehasdlc.isBlacklisted(game.name))
+ bDownloadDLCInfo = true;
+ }
+
+ // Check game specific config
+ if (!Globals::globalConfig.bUpdateCache) // Disable game specific config files for cache update
+ {
+ gameSpecificConfig conf;
+ conf.dlConf.bIgnoreDLCCount = bDownloadDLCInfo;
+ Util::getGameSpecificConfig(game.name, &conf);
+ bDownloadDLCInfo = conf.dlConf.bIgnoreDLCCount;
+ }
+
+ if (bDownloadDLCInfo && !Globals::globalConfig.sGameRegex.empty())
+ {
+ // don't download unnecessary info if user is only interested in a subset of his account
+ boost::regex expression(Globals::globalConfig.sGameRegex);
+ boost::match_results<std::string::const_iterator> what;
+ if (!boost::regex_search(game.name, what, expression))
+ {
+ bDownloadDLCInfo = false;
+ }
+ }
+
+ if (bDownloadDLCInfo)
+ {
+ game.gamedetailsjson = this->getGameDetailsJSON(game.id);
+ if (!game.gamedetailsjson.empty())
+ game.dlcnames = Util::getDLCNamesFromJSON(game.gamedetailsjson["dlcs"]);
+ }
+ }
+ games.push_back(game);
+ }
+ }
+ i++;
+ } while (!bAllPagesParsed);
+ std::cerr << std::endl;
+
+ if (Globals::globalConfig.bIncludeHiddenProducts)
+ {
+ std::sort(games.begin(), games.end(), [](const gameItem& i, const gameItem& j) -> bool { return i.name < j.name; });
+ }
+
+ return games;
+}
+
+// Login to GOG website
+int Website::Login(const std::string& email, const std::string& password)
+{
+ int res = 0;
+ std::string postdata;
+ std::ostringstream memory;
+ std::string token;
+ std::string tagname_username = "login[username]";
+ std::string tagname_password = "login[password]";
+ std::string tagname_login = "login[login]";
+ std::string tagname_token;
+ std::string auth_url = "https://auth.gog.com/auth?client_id=" + Globals::galaxyConf.getClientId() + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size()) + "&response_type=code&layout=default&brand=gog";
+ std::string auth_code;
+ bool bRecaptcha = false;
+
+ std::string login_form_html = this->getResponse(auth_url);
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::Login)" << std::endl;
+ std::cerr << login_form_html << std::endl;
+ #endif
+ if (login_form_html.find("google.com/recaptcha") != std::string::npos)
+ {
+ bRecaptcha = true;
+ #ifndef USE_QT_GUI_LOGIN
+ std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl
+ << "Try to login later or compile LGOGDownloader with -DUSE_QT_GUI=ON" << std::endl;
+ return res = 0;
+ #else
+ if (!Globals::globalConfig.bEnableLoginGUI)
+ {
+ std::cout << "Login form contains reCAPTCHA but GUI login is disabled." << std::endl
+ << "Enable GUI login with --enable-login-gui or try to login later." << std::endl;
+ return res = 0;
+ }
+ GuiLogin gl;
+ gl.Login(email, password);
+
+ auto cookies = gl.getCookies();
+ for (auto cookie : cookies)
+ {
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, cookie.c_str());
+ }
+ auth_code = gl.getCode();
+ #endif
+ }
+
+ if (bRecaptcha)
+ {
+ // This should never be reached but do additional check here just in case
+ #ifndef USE_QT_GUI_LOGIN
+ return res = 0;
+ #endif
+ }
+ else
+ {
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
+ tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator login_it_end = login_dom.end();
+ for (; login_it != login_it_end; ++login_it)
+ {
+ if (login_it->tagName()=="input")
+ {
+ login_it->parseAttributes();
+ if (login_it->attribute("id").second == "login__token")
+ {
+ token = login_it->attribute("value").second; // login token
+ tagname_token = login_it->attribute("name").second;
+ }
+ }
+ }
+
+ if (token.empty())
+ {
+ std::cout << "Failed to get login token" << std::endl;
+ return res = 0;
+ }
+
+ //Create postdata - escape characters in email/password to support special characters
+ postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size())
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size())
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "="
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size());
+ curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check");
+ curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+ // Don't follow to redirect location because we need to check it for two step authorization.
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+ CURLcode result = curl_easy_perform(curlhandle);
+ memory.str(std::string());
+
+ if (result != CURLE_OK)
+ {
+ // Expected to hit maximum amount of redirects so don't print error on it
+ if (result != CURLE_TOO_MANY_REDIRECTS)
+ std::cout << curl_easy_strerror(result) << std::endl;
+ }
+
+ // Get redirect url
+ char *redirect_url;
+ curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+
+ // Handle two step authorization
+ if (std::string(redirect_url).find("two_step") != std::string::npos)
+ {
+ std::string security_code;
+ std::string tagname_two_step_send = "second_step_authentication[send]";
+ std::string tagname_two_step_auth_letter_1 = "second_step_authentication[token][letter_1]";
+ std::string tagname_two_step_auth_letter_2 = "second_step_authentication[token][letter_2]";
+ std::string tagname_two_step_auth_letter_3 = "second_step_authentication[token][letter_3]";
+ std::string tagname_two_step_auth_letter_4 = "second_step_authentication[token][letter_4]";
+ std::string tagname_two_step_token;
+ std::string token_two_step;
+ std::string two_step_html = this->getResponse(redirect_url);
+ redirect_url = NULL;
+
+ tree<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
+ tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator two_step_it_end = two_step_dom.end();
+ for (; two_step_it != two_step_it_end; ++two_step_it)
+ {
+ if (two_step_it->tagName()=="input")
+ {
+ two_step_it->parseAttributes();
+ if (two_step_it->attribute("id").second == "second_step_authentication__token")
+ {
+ token_two_step = two_step_it->attribute("value").second; // two step token
+ tagname_two_step_token = two_step_it->attribute("name").second;
+ }
+ }
+ }
+
+ std::cerr << "Security code: ";
+ std::getline(std::cin,security_code);
+ if (security_code.size() != 4)
+ {
+ std::cerr << "Security code must be 4 characters long" << std::endl;
+ exit(1);
+ }
+
+ postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0]
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1]
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2]
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3]
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "="
+ + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size());
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step");
+ curl_easy_setopt(curlhandle, CURLOPT_POST, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0);
+ curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+ // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first.
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+ result = curl_easy_perform(curlhandle);
+ memory.str(std::string());
+ curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+ }
+
+ if (!std::string(redirect_url).empty())
+ {
+ long response_code;
+ do
+ {
+ curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
+ result = curl_easy_perform(curlhandle);
+ memory.str(std::string());
+
+ result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ if ((response_code / 100) == 3)
+ curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
+
+ std::string redir_url = std::string(redirect_url);
+ boost::regex re(".*code=(.*?)([\?&].*|$)", boost::regex_constants::icase);
+ boost::match_results<std::string::const_iterator> what;
+ if (boost::regex_search(redir_url, what, re))
+ {
+ auth_code = what[1];
+ if (!auth_code.empty())
+ break;
+ }
+ } while (result == CURLE_OK && (response_code / 100) == 3);
+ }
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
+ curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1);
+ curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1);
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ result = curl_easy_perform(curlhandle);
+
+ if (result != CURLE_OK)
+ {
+ std::cout << curl_easy_strerror(result) << std::endl;
+ }
+ }
+
+ if (this->IsLoggedInComplex(email))
+ {
+ res = 1; // Login was successful
+ }
+ else
+ {
+ if (this->IsloggedInSimple())
+ res = 1; // Login was successful
+ }
+
+ if (res == 1 && !auth_code.empty())
+ {
+ std::string token_url = "https://auth.gog.com/token?client_id=" + Globals::galaxyConf.getClientId()
+ + "&client_secret=" + Globals::galaxyConf.getClientSecret()
+ + "&grant_type=authorization_code&code=" + auth_code
+ + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size());
+
+ std::string json = this->getResponse(token_url);
+ if (!json.empty())
+ {
+ Json::Value token_json;
+ std::istringstream json_stream(json);
+ try {
+ json_stream >> token_json;
+
+ Globals::galaxyConf.setJSON(token_json);
+ res = 2;
+ } catch (const Json::Exception& exc) {
+ std::cerr << "Failed to parse json" << std::endl << json << std::endl;
+ std::cerr << exc.what() << std::endl;
+ }
+ }
+ }
+
+ if (res >= 1)
+ curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, "FLUSH"); // Write all known cookies to the file specified by CURLOPT_COOKIEJAR
+
+ return res;
+}
+
+bool Website::IsLoggedIn()
+{
+ return this->IsloggedInSimple();
+}
+
+/* Complex login check. Check login by checking email address on the account settings page.
+ returns true if we are logged in
+ returns false if we are not logged in
+*/
+bool Website::IsLoggedInComplex(const std::string& email)
+{
+ bool bIsLoggedIn = false;
+ std::string html = this->getResponse("https://www.gog.com/account/settings/security");
+ std::string email_lowercase = boost::algorithm::to_lower_copy(email); // boost::algorithm::to_lower does in-place modification but "email" is read-only so we need to make a copy of it
+
+ htmlcxx::HTML::ParserDom parser;
+ tree<htmlcxx::HTML::Node> dom = parser.parseTree(html);
+ tree<htmlcxx::HTML::Node>::iterator it = dom.begin();
+ tree<htmlcxx::HTML::Node>::iterator end = dom.end();
+ dom = parser.parseTree(html);
+ it = dom.begin();
+ end = dom.end();
+ for (; it != end; ++it)
+ {
+ if (it->tagName()=="strong")
+ {
+ it->parseAttributes();
+ if (it->attribute("class").second == "settings-item__value settings-item__section")
+ {
+ for (unsigned int i = 0; i < dom.number_of_children(it); ++i)
+ {
+ tree<htmlcxx::HTML::Node>::iterator tag_it = dom.child(it, i);
+ if (!tag_it->isTag() && !tag_it->isComment())
+ {
+ std::string tag_text = boost::algorithm::to_lower_copy(tag_it->text());
+ if (tag_text == email_lowercase)
+ {
+ bIsLoggedIn = true; // We are logged in
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (bIsLoggedIn) // We are logged in so no need to go through the remaining tags
+ break;
+ }
+
+ return bIsLoggedIn;
+}
+
+/* Simple login check. Check login by trying to get account page. If response code isn't 200 then login failed.
+ returns true if we are logged in
+ returns false if we are not logged in
+*/
+bool Website::IsloggedInSimple()
+{
+ bool bIsLoggedIn = false;
+ std::ostringstream memory;
+ std::string url = "https://www.gog.com/account";
+ long int response_code = 0;
+
+ curl_easy_setopt(curlhandle, CURLOPT_URL, url.c_str());
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback);
+ curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory);
+ curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1);
+ curl_easy_perform(curlhandle);
+ memory.str(std::string());
+
+ curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
+ curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1);
+ if (response_code == 200)
+ bIsLoggedIn = true; // We are logged in
+
+ return bIsLoggedIn;
+}
+
+std::vector<wishlistItem> Website::getWishlistItems()
+{
+ Json::Value root;
+ Json::CharReaderBuilder builder;
+ int i = 1;
+ bool bAllPagesParsed = false;
+ std::vector<wishlistItem> wishlistItems;
+
+ do
+ {
+ std::string response(this->getResponse("https://www.gog.com/account/wishlist/search?hasHiddenProducts=false&hiddenFlag=0&isUpdated=0&mediaType=0&sortBy=title&system=&page=" + std::to_string(i)));
+ std::istringstream response_stream(response);
+
+ try {
+ // Parse JSON
+ response_stream >> root;
+ } catch(const Json::Exception& exc) {
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << response << std::endl;
+ #endif
+ std::cout << exc.what();
+ exit(1);
+ }
+ #ifdef DEBUG
+ std::cerr << "DEBUG INFO (Website::getWishlistItems)" << std::endl << root << std::endl;
+ #endif
+ if (root["page"].asInt() >= root["totalPages"].asInt())
+ bAllPagesParsed = true;
+ if (root["products"].isArray())
+ {
+ for (unsigned int i = 0; i < root["products"].size(); ++i)
+ {
+ wishlistItem item;
+ Json::Value product = root["products"][i];
+
+ item.platform = 0;
+ std::string platforms_text;
+ bool bIsMovie = product["isMovie"].asBool();
+ if (!bIsMovie)
+ {
+ if (product["worksOn"]["Windows"].asBool())
+ item.platform |= GlobalConstants::PLATFORM_WINDOWS;
+ if (product["worksOn"]["Mac"].asBool())
+ item.platform |= GlobalConstants::PLATFORM_MAC;
+ if (product["worksOn"]["Linux"].asBool())
+ item.platform |= GlobalConstants::PLATFORM_LINUX;
+
+ // Skip if platform doesn't match
+ if (Globals::globalConfig.bPlatformDetection && !(item.platform & Globals::globalConfig.dlConf.iInstallerPlatform))
+ continue;
+ }
+
+ if (product["isComingSoon"].asBool())
+ item.tags.push_back("Coming soon");
+ if (product["isDiscounted"].asBool())
+ item.tags.push_back("Discount");
+ if (bIsMovie)
+ item.tags.push_back("Movie");
+
+ item.release_date_time = 0;
+ if (product.isMember("releaseDate") && product["isComingSoon"].asBool())
+ {
+ if (!product["releaseDate"].empty())
+ {
+ if (product["releaseDate"].isInt())
+ {
+ item.release_date_time = product["releaseDate"].asInt();
+ }
+ else
+ {
+ std::string release_date_time_string = product["releaseDate"].asString();
+ if (!release_date_time_string.empty())
+ {
+ try
+ {
+ item.release_date_time = std::stoi(release_date_time_string);
+ }
+ catch (std::invalid_argument& e)
+ {
+ item.release_date_time = 0;
+ }
+ }
+ }
+ }
+ }
+
+ item.currency = product["price"]["symbol"].asString();
+ item.price = product["price"]["finalAmount"].isDouble() ? std::to_string(product["price"]["finalAmount"].asDouble()) + item.currency : product["price"]["finalAmount"].asString() + item.currency;
+ item.discount_percent = product["price"]["discountPercentage"].isInt() ? std::to_string(product["price"]["discountPercentage"].asInt()) + "%" : product["price"]["discountPercentage"].asString() + "%";
+ item.discount = product["price"]["discountDifference"].isDouble() ? std::to_string(product["price"]["discountDifference"].asDouble()) + item.currency : product["price"]["discountDifference"].asString() + item.currency;
+ item.store_credit = product["price"]["bonusStoreCreditAmount"].isDouble() ? std::to_string(product["price"]["bonusStoreCreditAmount"].asDouble()) + item.currency : product["price"]["bonusStoreCreditAmount"].asString() + item.currency;
+
+ item.url = product["url"].asString();
+ if (item.url.find("/game/") == 0)
+ item.url = "https://www.gog.com" + item.url;
+ else if (item.url.find("/movie/") == 0)
+ item.url = "https://www.gog.com" + item.url;
+
+ item.title = product["title"].asString();
+ item.bIsBonusStoreCreditIncluded = product["price"]["isBonusStoreCreditIncluded"].asBool();
+ item.bIsDiscounted = product["isDiscounted"].asBool();
+
+ wishlistItems.push_back(item);
+ }
+ }
+ i++;
+ } while (!bAllPagesParsed);
+
+ return wishlistItems;
+}
--- /dev/null
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://www.wtfpl.net/ for more details. */
+
+#include "ziputil.h"
+
+#include <sstream>
+#include <fstream>
+#include <sys/stat.h>
+#include <boost/iostreams/filtering_streambuf.hpp>
+#include <boost/iostreams/copy.hpp>
+#include <boost/iostreams/filter/zlib.hpp>
+#include <boost/regex.hpp>
+
+off_t ZipUtil::getMojoSetupScriptSize(std::stringstream *stream)
+{
+ off_t mojosetup_script_size = -1;
+ int script_lines = 0;
+ boost::regex re("offset=`head -n (\\d+?) \"\\$0\"", boost::regex::perl | boost::regex::icase);
+ boost::match_results<std::string::const_iterator> what;
+
+ if (boost::regex_search(stream->str(), what, re))
+ {
+ script_lines = std::stoi(what[1]);
+ }
+
+ std::string script;
+ for (int i = 0; i < script_lines; ++i)
+ {
+ std::string line;
+ std::getline(*stream, line);
+ script += line + "\n";
+ }
+ mojosetup_script_size = script.size();
+
+ return mojosetup_script_size;
+}
+
+off_t ZipUtil::getMojoSetupInstallerSize(std::stringstream *stream)
+{
+ off_t mojosetup_installer_size = -1;
+ boost::regex re("filesizes=\"(\\d+?)\"", boost::regex::perl | boost::regex::icase);
+ boost::match_results<std::string::const_iterator> what;
+
+ if (boost::regex_search(stream->str(), what, re))
+ {
+ mojosetup_installer_size = std::stoll(what[1]);
+ }
+
+ return mojosetup_installer_size;
+}
+
+struct tm ZipUtil::date_time_to_tm(uint64_t date, uint64_t time)
+{
+ /* DOS date time format
+ * Y|Y|Y|Y|Y|Y|Y|M| |M|M|M|D|D|D|D|D| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s
+ *
+ * second is divided by 2
+ * month starts at 1
+ * https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247%28v=vs.85%29.aspx */
+
+ uint64_t local_time_base_year = 1900;
+ uint64_t dos_time_base_year = 1980;
+
+ struct tm timeinfo;
+ timeinfo.tm_year = (uint16_t)(((date & 0xFE00) >> 9) - local_time_base_year + dos_time_base_year);
+ timeinfo.tm_mon = (uint16_t)(((date & 0x1E0) >> 5) - 1);
+ timeinfo.tm_mday = (uint16_t)(date & 0x1F);
+ timeinfo.tm_hour = (uint16_t)((time & 0xF800) >> 11);
+ timeinfo.tm_min = (uint16_t)((time & 0x7E0) >> 5);
+ timeinfo.tm_sec = (uint16_t)(2 * (time & 0x1F));
+ timeinfo.tm_isdst = -1;
+
+ return timeinfo;
+}
+
+bool ZipUtil::isValidDate(struct tm timeinfo)
+{
+ if (!(timeinfo.tm_year >= 0 && timeinfo.tm_year <= 207))
+ return false;
+ if (!(timeinfo.tm_mon >= 0 && timeinfo.tm_mon <= 11))
+ return false;
+ if (!(timeinfo.tm_mday >= 1 && timeinfo.tm_mday <= 31))
+ return false;
+ if (!(timeinfo.tm_hour >= 0 && timeinfo.tm_hour <= 23))
+ return false;
+ if (!(timeinfo.tm_min >= 0 && timeinfo.tm_min <= 59))
+ return false;
+ if (!(timeinfo.tm_sec >= 0 && timeinfo.tm_sec <= 59))
+ return false;
+
+ return true;
+}
+
+uint64_t ZipUtil::readValue(std::istream *stream, uint32_t len)
+{
+ uint64_t value = 0;
+
+ for (uint32_t i = 0; i < len; i++)
+ {
+ value |= ((uint64_t)(stream->get() & 0xFF)) << (i * 8);
+ }
+
+ return value;
+}
+
+uint64_t ZipUtil::readUInt64(std::istream *stream)
+{
+ uint64_t value = (uint64_t)readValue(stream, sizeof(uint64_t));
+ return value;
+}
+
+uint32_t ZipUtil::readUInt32(std::istream *stream)
+{
+ uint32_t value = (uint32_t)readValue(stream, sizeof(uint32_t));
+ return value;
+}
+
+uint16_t ZipUtil::readUInt16(std::istream *stream)
+{
+ uint16_t value = (uint16_t)readValue(stream, sizeof(uint16_t));
+ return value;
+}
+
+uint8_t ZipUtil::readUInt8(std::istream *stream)
+{
+ uint8_t value = (uint8_t)readValue(stream, sizeof(uint8_t));
+ return value;
+}
+
+off_t ZipUtil::getZipEOCDOffsetSignature(std::istream *stream, const uint32_t& signature)
+{
+ off_t offset = -1;
+ stream->seekg(0, stream->end);
+ off_t stream_length = stream->tellg();
+
+ for (off_t i = 4; i <= stream_length; i++)
+ {
+ off_t pos = stream_length - i;
+ stream->seekg(pos, stream->beg);
+ if (readUInt32(stream) == signature)
+ {
+ offset = stream->tellg();
+ offset -= 4;
+ break;
+ }
+ }
+
+ return offset;
+}
+
+off_t ZipUtil::getZipEOCDOffset(std::istream *stream)
+{
+ return getZipEOCDOffsetSignature(stream, ZIP_EOCD_HEADER_SIGNATURE);
+}
+
+off_t ZipUtil::getZip64EOCDOffset(std::istream *stream)
+{
+ return getZipEOCDOffsetSignature(stream, ZIP_EOCD_HEADER_SIGNATURE64);
+}
+
+zipEOCD ZipUtil::readZipEOCDStruct(std::istream *stream, const off_t& eocd_start_pos)
+{
+ zipEOCD eocd;
+
+ stream->seekg(eocd_start_pos, stream->beg);
+
+ // end of central dir signature <4 bytes>
+ eocd.header = readUInt32(stream);
+
+ // number of this disk <2 bytes>
+ eocd.disk = readUInt16(stream); // Number of this disk
+
+ // number of the disk with the start of the central directory <2 bytes>
+ eocd.cd_start_disk = readUInt16(stream);
+
+ // total number of entries in the central directory on this disk <2 bytes>
+ eocd.cd_records = readUInt16(stream);
+
+ // total number of entries in the central directory <2 bytes>
+ eocd.total_cd_records = readUInt16(stream);
+
+ // size of the central directory <4 bytes>
+ eocd.cd_size = readUInt32(stream);
+
+ // offset of start of central directory with respect to the starting disk number <4 bytes>
+ eocd.cd_start_offset = readUInt32(stream);
+
+ // .ZIP file comment length <2 bytes>
+ eocd.comment_length = readUInt16(stream);
+
+ // .ZIP file comment <variable size>
+ if (eocd.comment_length > 0)
+ {
+ char *buf = new char[eocd.comment_length + 1];
+ stream->read(buf, eocd.comment_length);
+ eocd.comment = std::string(buf, eocd.comment_length);
+ delete[] buf;
+ }
+
+ return eocd;
+}
+
+zip64EOCD ZipUtil::readZip64EOCDStruct(std::istream *stream, const off_t& eocd_start_pos)
+{
+ zip64EOCD eocd;
+
+ stream->seekg(eocd_start_pos, stream->beg);
+
+ // zip64 end of central dir signature <4 bytes>
+ eocd.header = readUInt32(stream);
+
+ // size of zip64 end of central directory record <8 bytes>
+ eocd.directory_record_size = readUInt64(stream);
+ /* The value stored into the "size of zip64 end of central
+ * directory record" should be the size of the remaining
+ * record and should not include the leading 12 bytes.
+ *
+ * Size = SizeOfFixedFields + SizeOfVariableData - 12 */
+
+ // version made by <2 bytes>
+ eocd.version_made_by = readUInt16(stream);
+
+ // version needed to extract <2 bytes>
+ eocd.version_needed = readUInt16(stream);
+
+ // number of this disk <4 bytes>
+ eocd.cd = readUInt32(stream);
+
+ // number of the disk with the start of the central directory <8 bytes>
+ eocd.cd_start = readUInt32(stream);
+
+ // total number of entries in the central directory on this disk <8 bytes>
+ eocd.cd_total_disk = readUInt64(stream);
+
+ // total number of entries in the central directory <8 bytes>
+ eocd.cd_total = readUInt64(stream);
+
+ // size of the central directory <8 bytes>
+ eocd.cd_size = readUInt64(stream);
+
+ // offset of start of central directory with respect to the starting disk number <8 bytes>
+ eocd.cd_offset = readUInt64(stream);
+
+ // zip64 extensible data sector <variable size>
+ // This is data is not needed for our purposes so we just ignore this data
+
+ return eocd;
+}
+
+zipCDEntry ZipUtil::readZipCDEntry(std::istream *stream)
+{
+ zipCDEntry cd;
+ char *buf;
+
+ // file header signature <4 bytes>
+ cd.header = readUInt32(stream);
+
+ cd.isLocalCDEntry = (cd.header == ZIP_LOCAL_HEADER_SIGNATURE);
+
+ if (!cd.isLocalCDEntry)
+ {
+ // version made by <2 bytes>
+ cd.version_made_by = readUInt16(stream);
+ }
+ // version needed to extract <2 bytes>
+ cd.version_needed = readUInt16(stream);
+
+ // general purpose bit flag <2 bytes>
+ cd.flag = readUInt16(stream);
+
+ // compression method <2 bytes>
+ cd.compression_method = readUInt16(stream);
+
+ // last mod file time <2 bytes>
+ cd.mod_time = readUInt16(stream);
+
+ // last mod file date <2 bytes>
+ cd.mod_date = readUInt16(stream);
+
+ // crc-32 <4 bytes>
+ cd.crc32 = readUInt32(stream);
+
+ // compressed size <4 bytes>
+ cd.comp_size = readUInt32(stream);
+
+ // uncompressed size <4 bytes>
+ cd.uncomp_size = readUInt32(stream);
+
+ // file name length <2 bytes>
+ cd.filename_length = readUInt16(stream);
+
+ // extra field length <2 bytes>
+ cd.extra_length = readUInt16(stream);
+
+ if (!cd.isLocalCDEntry)
+ {
+ // file comment length <2 bytes>
+ cd.comment_length = readUInt16(stream);
+
+ // disk number start <2 bytes>
+ cd.disk_num = readUInt16(stream);
+
+ // internal file attributes <2 bytes>
+ cd.internal_file_attr = readUInt16(stream);
+
+ // external file attributes <4 bytes>
+ cd.external_file_attr = readUInt32(stream);
+
+ // relative offset of local header <4 bytes>
+ cd.disk_offset = readUInt32(stream);
+ }
+
+ // file name <variable size>
+ buf = new char[cd.filename_length + 1];
+ stream->read(buf, cd.filename_length);
+ cd.filename = std::string(buf, cd.filename_length);
+ delete[] buf;
+
+ // extra field <variable size>
+ buf = new char[cd.extra_length + 1];
+ stream->read(buf, cd.extra_length);
+ cd.extra = std::string(buf, cd.extra_length);
+ delete[] buf;
+ std::stringstream extra_stream(cd.extra);
+
+ cd.timestamp = 0;
+ struct tm timeinfo = date_time_to_tm(cd.mod_date, cd.mod_time);
+ if (isValidDate(timeinfo))
+ cd.timestamp = mktime(&timeinfo);
+
+ // Read extra fields
+ off_t i = 0;
+ while (i < cd.extra_length)
+ {
+ /* Extra field
+ * <2 bytes> signature
+ * <2 bytes> size of extra field data
+ * <variable> extra field data */
+
+ uint16_t header_id = readUInt16(&extra_stream);
+ uint16_t extra_data_size = readUInt16(&extra_stream);
+
+ if (header_id == ZIP_EXTENSION_ZIP64)
+ {
+ /* Zip64 Extended Information Extra Field
+ * <8 bytes> size of uncompressed file
+ * <8 bytes> size of compressed data
+ * <8 bytes> offset of local header record
+ * <4 bytes> number of the disk on which this file starts
+ *
+ * The fields only appear if the corresponding Local or Central
+ * directory record field is set to UINT16_MAX or UINT32_MAX */
+
+ if (cd.uncomp_size == UINT32_MAX)
+ cd.uncomp_size = readUInt64(&extra_stream);
+ if (cd.comp_size == UINT32_MAX)
+ cd.comp_size = readUInt64(&extra_stream);
+ if (cd.disk_offset == UINT32_MAX)
+ cd.disk_offset = readUInt64(&extra_stream);
+ if (cd.disk_num == UINT16_MAX)
+ cd.disk_num = readUInt32(&extra_stream);
+ }
+ else if (header_id == ZIP_EXTENDED_TIMESTAMP)
+ {
+ /* Extended Timestamp Extra Field
+ *
+ * Local header version
+ * <1 byte> info bits
+ * <4 bytes> modification time
+ * <4 bytes> access time
+ * <4 bytes> creation time
+ *
+ * Central header version
+ * <1 byte> info bits
+ * <4 bytes> modification time
+ *
+ * The lower three info bits in both headers indicate
+ * which timestamps are present in the local extra field
+ * bit 0 if set, modification time is present
+ * bit 1 if set, access time is present
+ * bit 2 if set, creation time is present
+ * bits 3-7 reserved for additional timestamps; not set
+ *
+ * If info bits indicate that modification time is present
+ * in the local header field, it must be present in the
+ * central header field.
+ * Those times that are present will appear in the order
+ * indicated, but any combination of times may be omitted. */
+
+ uint32_t modification_time = 0;
+ uint32_t access_time = 0;
+ uint32_t creation_time = 0;
+
+ uint8_t flags = readUInt8(&extra_stream);
+
+ if (flags & 0x1) // modification time is present
+ {
+ modification_time = readUInt32(&extra_stream);
+ cd.timestamp = modification_time;
+ }
+
+ if (cd.isLocalCDEntry)
+ {
+ if (flags & 0x2) // access time is present
+ {
+ access_time = readUInt32(&extra_stream);
+ }
+
+ if (flags & 0x4) // creation time is present
+ {
+ creation_time = readUInt32(&extra_stream);
+ }
+ }
+
+ // access time and creation time are unused currently
+ // suppress -Wunused-but-set-variable messages by casting these variables to void
+ (void) access_time;
+ (void) creation_time;
+ }
+ else if (header_id == ZIP_INFOZIP_UNIX_NEW)
+ {
+ /* Info-ZIP New Unix Extra Field
+ * <1 byte> version
+ * <1 byte> size of uid
+ * <variable> uid
+ * <1 byte> size of gid
+ * <variable> gid
+ *
+ * Currently Version is set to the number 1. If there is a need
+ * to change this field, the version will be incremented.
+ * UID and GID entries are stored in standard little endian format */
+
+ uint8_t version = readUInt8(&extra_stream);
+ if (version == 1)
+ {
+ uint64_t uid = 0;
+ uint64_t gid = 0;
+
+ uint8_t uid_size = readUInt8(&extra_stream);
+ for (uint8_t i = 0; i < uid_size; i++)
+ {
+ uid |= ((uint64_t)extra_stream.get()) << (i * 8);
+ }
+
+ uint8_t gid_size = readUInt8(&extra_stream);
+ for (uint8_t i = 0; i < gid_size; i++)
+ {
+ gid |= ((uint64_t)extra_stream.get()) << (i * 8);
+ }
+ }
+ else
+ {
+ // Unknown version
+ // Skip the rest of this field
+ extra_stream.seekg(extra_data_size - 1, extra_stream.cur);
+ }
+ }
+ else
+ {
+ // Skip over unknown/unimplemented extra field
+ extra_stream.seekg(extra_data_size, extra_stream.cur);
+ }
+ i += 4 + extra_data_size;
+ }
+
+ // file comment <variable size>
+ buf = new char[cd.comment_length + 1];
+ stream->read(buf, cd.comment_length);
+ cd.comment = std::string(buf, cd.comment_length);
+ delete[] buf;
+
+ return cd;
+}
+
+/* Extract file
+ returns 0 if successful
+ returns 1 if input file could not be opened
+ returns 2 if compression method is unsupported
+ returns 3 if output file could not be created
+ returns 4 if zlib error
+*/
+int ZipUtil::extractFile(const std::string& input_file_path, const std::string& output_file_path)
+{
+ std::ifstream input_file(input_file_path, std::ifstream::in | std::ifstream::binary);
+
+ if (!input_file)
+ {
+ // Could not open input file
+ return 1;
+ }
+
+ // Read header
+ zipCDEntry cd = readZipCDEntry(&input_file);
+
+ if (!(cd.compression_method == boost::iostreams::zlib::deflated || cd.compression_method == boost::iostreams::zlib::no_compression))
+ {
+ // Unsupported compression method
+ return 2;
+ }
+
+ boost::iostreams::zlib_params p;
+ p.window_bits = 15;
+ p.noheader = true; // zlib header and trailing adler-32 checksum is omitted
+
+ std::ofstream output_file(output_file_path, std::ofstream::out | std::ofstream::binary);
+ if (!output_file)
+ {
+ // Failed to create output file
+ return 3;
+ }
+
+ // Uncompress
+ boost::iostreams::filtering_streambuf<boost::iostreams::input> in;
+
+ if (cd.compression_method == boost::iostreams::zlib::deflated)
+ in.push(boost::iostreams::zlib_decompressor(p));
+
+ in.push(input_file);
+ try
+ {
+ boost::iostreams::copy(in, output_file);
+ }
+ catch(boost::iostreams::zlib_error & e)
+ {
+ // zlib error
+ return 4;
+ }
+
+ input_file.close();
+ output_file.close();
+
+ if (cd.timestamp > 0)
+ boost::filesystem::last_write_time(output_file_path, cd.timestamp);
+
+ return 0;
+}
+
+/* Extract stream to stream
+ returns 0 if successful
+ returns 1 if input stream is not valid
+ returns 2 if compression method is unsupported
+ returns 3 if output stream is not valid
+ returns 4 if zlib error
+*/
+int ZipUtil::extractStream(std::istream* input_stream, std::ostream* output_stream)
+{
+ if (!input_stream)
+ {
+ // Input stream not valid
+ return 1;
+ }
+
+ // Read header
+ zipCDEntry cd = readZipCDEntry(input_stream);
+
+ if (!(cd.compression_method == boost::iostreams::zlib::deflated || cd.compression_method == boost::iostreams::zlib::no_compression))
+ {
+ // Unsupported compression method
+ return 2;
+ }
+
+ boost::iostreams::zlib_params p;
+ p.window_bits = 15;
+ p.noheader = true; // zlib header and trailing adler-32 checksum is omitted
+
+ if (!output_stream)
+ {
+ // Output stream not valid
+ return 3;
+ }
+
+ // Uncompress
+ boost::iostreams::filtering_streambuf<boost::iostreams::input> in;
+
+ if (cd.compression_method == boost::iostreams::zlib::deflated)
+ in.push(boost::iostreams::zlib_decompressor(p));
+
+ in.push(*input_stream);
+ try
+ {
+ boost::iostreams::copy(in, *output_stream);
+ }
+ catch(boost::iostreams::zlib_error & e)
+ {
+ // zlib error
+ return 4;
+ }
+
+ return 0;
+}
+
+boost::filesystem::perms ZipUtil::getBoostFilePermission(const uint16_t& attributes)
+{
+ boost::filesystem::perms perms = boost::filesystem::no_perms;
+ if (attributes & S_IRUSR)
+ perms |= boost::filesystem::owner_read;
+ if (attributes & S_IWUSR)
+ perms |= boost::filesystem::owner_write;
+ if (attributes & S_IXUSR)
+ perms |= boost::filesystem::owner_exe;
+
+ if (attributes & S_IRGRP)
+ perms |= boost::filesystem::group_read;
+ if (attributes & S_IWGRP)
+ perms |= boost::filesystem::group_write;
+ if (attributes & S_IXGRP)
+ perms |= boost::filesystem::group_exe;
+
+ if (attributes & S_IROTH)
+ perms |= boost::filesystem::others_read;
+ if (attributes & S_IWOTH)
+ perms |= boost::filesystem::others_write;
+ if (attributes & S_IXOTH)
+ perms |= boost::filesystem::others_exe;
+
+ return perms;
+}
+
+bool ZipUtil::isSymlink(const uint16_t& attributes)
+{
+ bool bSymlink = ((attributes & S_IFMT) == S_IFLNK);
+ return bSymlink;
+}