diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e43703..489e6d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,6 @@ set(HEADER_FILES alpm/packagefinder.h ) set(SRC_FILES - main.cpp alpm/manager.cpp alpm/package.cpp alpm/utilities.cpp @@ -44,6 +43,21 @@ set(SRC_FILES network/userrepository.cpp network/networkaccessmanager.cpp ) +set(CLI_HEADER_FILES +) +set(CLI_SRC_FILES + cli/main.cpp +) +set(GUI_HEADER_FILES + gui/mainwindow.h + gui/webpage.h + gui/webviewprovider.h +) +set(GUI_SRC_FILES + gui/main.cpp + gui/mainwindow.cpp + gui/webpage.cpp +) set(WEB_FILES web/3rdparty/bootstrap/css/bootstrap-theme.min.css web/3rdparty/bootstrap/css/bootstrap.min.css @@ -71,13 +85,13 @@ set(WEB_FILES # meta data set(META_PROJECT_NAME repoindex) -set(META_APP_NAME "Repository browser") +set(META_APP_NAME "Repository Browser") set(META_APP_AUTHOR "Martchus") set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}") -set(META_APP_DESCRIPTION "Arch Linux repository browser") +set(META_APP_DESCRIPTION "Repository browser for Arch Linux") set(META_VERSION_MAJOR 0) set(META_VERSION_MINOR 2) -set(META_VERSION_PATCH 1) +set(META_VERSION_PATCH 2) # stringification of meta data set(META_PROJECT_NAME_STR "\"${META_PROJECT_NAME}\"") @@ -119,6 +133,9 @@ if(MINGW) enable_language(RC) endif(MINGW) +# read cached variables +set(WEBVIEW_PROVIDER "auto" CACHE STRING "specifies the webview provider: auto, webkit or webengine") + # check required Qt 5 modules find_package(Qt5Core REQUIRED) find_package(Qt5Concurrent REQUIRED) @@ -126,6 +143,36 @@ find_package(Qt5Network REQUIRED) find_package(Qt5WebSockets REQUIRED) find_package(KF5Archive REQUIRED) +# select Qt module providing webview (either Qt WebKit or Qt WebEngine) +if(${WEBVIEW_PROVIDER} STREQUAL "none") + set(WEBVIEW_PROVIDER OFF) + message(STATUS "Webview disabled, not building GUI.") +elseif(${WEBVIEW_PROVIDER} STREQUAL "auto") + find_package(Qt5WebEngineWidgets) + if(Qt5WebEngineWidgets_FOUND) + set(WEBVIEW_PROVIDER Qt5::WebEngineWidgets) + set(WEBVIEW_DEFINITION -DREPOINDEX_USE_WEBENGINE) + message(STATUS "No webview provider explicitely specified, defaulting to Qt WebEngine.") + else() + find_package(Qt5WebKitWidgets REQUIRED) + set(WEBVIEW_PROVIDER Qt5::WebKitWidgets) + message(STATUS "No webview provider explicitely specified, defaulting to Qt WebKit.") + endif() +else() + if(${WEBVIEW_PROVIDER} STREQUAL "webkit") + find_package(Qt5WebKitWidgets REQUIRED) + set(WEBVIEW_PROVIDER Qt5::WebKitWidgets) + message(STATUS "Using Qt WebKit as webview provider.") + elseif(${WEBVIEW_PROVIDER} STREQUAL "webengine") + find_package(Qt5WebEngineWidgets REQUIRED) + set(WEBVIEW_PROVIDER Qt5::WebEngineWidgets) + set(WEBVIEW_DEFINITION -DREPOINDEX_USE_WEBENGINE) + message(STATUS "Using Qt WebEngine as webview provider.") + else() + message(FATAL_ERROR "The specified webview provider '${WEBVIEW_PROVIDER}' is unknown.") + endif() +endif() + # enable moc set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -134,18 +181,33 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) add_definitions( -D_GLIBCXX_USE_CXX11_ABI=0 -DCMAKE_BUILD + ${WEBVIEW_DEFINITION} ) if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions(-DDEBUG_BUILD) message(STATUS "Debug build enabled.") endif() - # executable and linking -add_executable(${META_PROJECT_NAME} ${HEADER_FILES} ${SRC_FILES} ${WEB_FILES} ${RES_FILES}) -target_link_libraries(${META_PROJECT_NAME} c++utilities Qt5::Core Qt5::Concurrent Qt5::Network Qt5::WebSockets KF5::Archive) +add_library(${META_PROJECT_NAME}-lib SHARED ${HEADER_FILES} ${SRC_FILES} ${WEB_FILES}) +target_link_libraries(${META_PROJECT_NAME}-lib c++utilities Qt5::Core Qt5::Concurrent Qt5::Network Qt5::WebSockets KF5::Archive) +set_target_properties(${META_PROJECT_NAME}-lib PROPERTIES + VERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH} + SOVERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH} + CXX_STANDARD 11 + OUTPUT_NAME ${META_PROJECT_NAME} +) +add_executable(${META_PROJECT_NAME} ${CLI_HEADER_FILES} ${CLI_SRC_FILES}) +target_link_libraries(${META_PROJECT_NAME} c++utilities ${META_PROJECT_NAME}-lib Qt5::Core Qt5::Network Qt5::WebSockets) set_target_properties(${META_PROJECT_NAME} PROPERTIES CXX_STANDARD 11 ) +if(NOT ${WEBVIEW_PROVIDER} STREQUAL "none") + add_executable(${META_PROJECT_NAME}-gui ${GUI_HEADER_FILES} ${GUI_SRC_FILES}) + target_link_libraries(${META_PROJECT_NAME}-gui c++utilities qtutilities ${META_PROJECT_NAME}-lib Qt5::Core Qt5::Network Qt5::WebSockets ${WEBVIEW_PROVIDER}) + set_target_properties(${META_PROJECT_NAME}-gui PROPERTIES + CXX_STANDARD 11 + ) +endif() # add install target for web files / minimizing # -> don't minimize debug builds @@ -211,6 +273,11 @@ foreach(WEB_FILE ${WEB_FILES}) ) endif() endforeach() +install( + FILES resources/icons/hicolor/scalable/apps/${META_PROJECT_NAME}.svg + DESTINATION share/${META_PROJECT_NAME}/web/img + COMPONENT web +) # add target for minimizing if(HTML_MIN_FILES) add_custom_target(htmlmin ALL DEPENDS ${HTML_MIN_FILES}) @@ -220,10 +287,32 @@ if(JS_MIN_FILES) endif() # add install target +foreach(HEADER_FILE ${HEADER_FILES}) + get_filename_component(HEADER_DIR ${HEADER_FILE} DIRECTORY) + install( + FILES ${HEADER_FILE} + DESTINATION include/${META_PROJECT_NAME}/${HEADER_DIR} + COMPONENT header + ) +endforeach() install(TARGETS ${META_PROJECT_NAME} RUNTIME DESTINATION bin COMPONENT binary ) +if(NOT ${WEBVIEW_PROVIDER} STREQUAL "none") + install(TARGETS ${META_PROJECT_NAME}-gui + RUNTIME DESTINATION bin + COMPONENT binary-gui + ) +endif() +install(TARGETS ${META_PROJECT_NAME}-lib + RUNTIME DESTINATION bin + COMPONENT binary + LIBRARY DESTINATION lib + COMPONENT binary + ARCHIVE DESTINATION lib + COMPONENT binary +) install(FILES resources/systemd/${META_PROJECT_NAME}.service DESTINATION lib/systemd/system COMPONENT service @@ -232,18 +321,45 @@ install(FILES resources/settings/${META_PROJECT_NAME}.conf.js DESTINATION share/${META_PROJECT_NAME}/skel COMPONENT config ) +install(FILES resources/icons/hicolor/scalable/apps/${META_PROJECT_NAME}.svg + DESTINATION share/icons/hicolor/scalable/apps + COMPONENT desktop +) +install(FILES resources/desktop/applications/${META_PROJECT_NAME}.desktop + DESTINATION share/applications + COMPONENT desktop +) if(NOT TARGET install-binary) add_custom_target(install-binary DEPENDS ${META_PROJECT_NAME} COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=binary -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" ) endif() +if((NOT ${WEBVIEW_PROVIDER} STREQUAL "none") AND (NOT TARGET install-binary-gui)) + set(GUI_INSTALL_TARGET "install-binary-gui") + add_custom_target(install-binary-gui + DEPENDS ${META_PROJECT_NAME} + COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=binary-gui -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" + ) +endif() +if(NOT TARGET install-header) + add_custom_target(install-header + DEPENDS ${META_PROJECT_NAME} + COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=header -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" + ) +endif() if(NOT TARGET install-service) add_custom_target(install-service DEPENDS ${META_PROJECT_NAME} COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=service -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" ) endif() +if(NOT TARGET install-desktop) + add_custom_target(install-desktop + DEPENDS ${META_PROJECT_NAME} + COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=desktop -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" + ) +endif() if(NOT TARGET install-config) add_custom_target(install-config DEPENDS ${META_PROJECT_NAME} @@ -258,7 +374,7 @@ if(NOT TARGET install-web) endif() if(NOT TARGET install-mingw-w64) add_custom_target(install-mingw-w64 - DEPENDS install-binary + DEPENDS install-binary ${GUI_INSTALL_TARGET} install-header ) endif() if(NOT TARGET install-binary-strip) diff --git a/alpm/alpmdatabase.cpp b/alpm/alpmdatabase.cpp index de5a0af..b4ccb0a 100644 --- a/alpm/alpmdatabase.cpp +++ b/alpm/alpmdatabase.cpp @@ -22,6 +22,7 @@ #include using namespace std; +using namespace ChronoUtilities; namespace RepoIndex { @@ -37,25 +38,31 @@ using namespace Utilities; class LoadPackage { public: - LoadPackage(AlpmDatabase *database, PackageOrigin origin) : + LoadPackage(AlpmDatabase *database, PackageOrigin origin, DateTime descriptionsLastModified) : m_db(database), - m_origin(origin) + m_origin(origin), + m_descriptionsLastModified(descriptionsLastModified) {} void operator()(const QPair > &description) { - m_db->addPackageFromDescription(description.first, description.second, m_origin); + m_db->addPackageFromDescription(description.first, description.second, m_origin, m_descriptionsLastModified); } private: AlpmDatabase *const m_db; const PackageOrigin m_origin; + const DateTime m_descriptionsLastModified; }; -DatabaseError AlpmDatabase::loadDescriptions(QList > > &descriptions) +DatabaseError AlpmDatabase::loadDescriptions(QList > > &descriptions, ChronoUtilities::DateTime *lastModified) { QFileInfo pathInfo(databasePath()); if(pathInfo.isDir()) { + if(lastModified) { + // just use current date here since this is usually the local db + *lastModified = DateTime::gmtNow(); + } static const QStringList relevantFiles = QStringList() << QStringLiteral("desc") << QStringLiteral("files"); QDir dbDir(databasePath()); QStringList pkgDirNames = dbDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); @@ -83,6 +90,9 @@ DatabaseError AlpmDatabase::loadDescriptions(QListloadDescriptions(m_descriptions)) == DatabaseError::NoError) { - m_future = QtConcurrent::map(m_descriptions, LoadPackage(repository, origin)); + if((m_error = repository->loadDescriptions(m_descriptions, &m_descriptionsLastModified)) == DatabaseError::NoError) { + m_future = QtConcurrent::map(m_descriptions, LoadPackage(repository, origin, m_descriptionsLastModified)); } } @@ -157,9 +167,6 @@ AlpmPackageLoader *AlpmDatabase::internalInit() origin = PackageOrigin::SyncDb; } - // wipe current packages - wipePackages(); - // initialization of packages is done concurrently via AlpmPackageLoader return new AlpmPackageLoader(this, origin); @@ -211,17 +218,19 @@ QNetworkRequest AlpmDatabase::filesDatabaseRequest() * - Does nothing if there is not at least one server URL available. * - Status messages are printed via cerr. */ -void AlpmDatabase::downloadDatabase(const QString &targetDir, bool filesDatabase) +bool AlpmDatabase::downloadDatabase(const QString &targetDir, bool filesDatabase) { QWriteLocker locker(lock()); if(serverUrls().isEmpty()) { - return; // no server URLs available + return false; // no server URLs available } + addBusyFlag(); cerr << shchar << "Downloading " << (filesDatabase ? "files" : "regular") << " database for [" << name().toLocal8Bit().data() << "] from mirror " << serverUrls().front().toLocal8Bit().data() << " ..." << endl; QNetworkReply *reply = networkAccessManager().get(filesDatabase ? filesDatabaseRequest() : regularDatabaseRequest()); reply->setProperty("filesDatabase", filesDatabase); m_downloadTargetDir = targetDir.isEmpty() ? QString(QChar('.')) : targetDir; connect(reply, &QNetworkReply::finished, this, &AlpmDatabase::databaseDownloadFinished); + return true; } /*! @@ -234,10 +243,8 @@ void AlpmDatabase::downloadDatabase(const QString &targetDir, bool filesDatabase */ void AlpmDatabase::refresh(const QString &targetDir) { - if(serverUrls().isEmpty()) { + if(!downloadDatabase(targetDir, true)) { init(); - } else { - downloadDatabase(targetDir, true); } } @@ -252,6 +259,7 @@ std::unique_ptr AlpmDatabase::emptyPackage() */ void AlpmDatabase::databaseDownloadFinished() { + removeBusyFlag(); auto *reply = static_cast(sender()); reply->deleteLater(); bool filesDatabase = reply->property("filesDatabase").toBool(); diff --git a/alpm/alpmdatabase.h b/alpm/alpmdatabase.h index ad8dfcd..eb6c00d 100644 --- a/alpm/alpmdatabase.h +++ b/alpm/alpmdatabase.h @@ -38,6 +38,7 @@ private: AlpmDatabase *const m_db; DatabaseError m_error; QList > > m_descriptions; + ChronoUtilities::DateTime m_descriptionsLastModified; }; /*! @@ -73,7 +74,7 @@ public: void setDatabasePath(const QString &dbPath); // updating/refreshing - void downloadDatabase(const QString &targetDir, bool filesDatabase = true); + bool downloadDatabase(const QString &targetDir, bool filesDatabase = true); void refresh(const QString &targetDir); protected: @@ -83,7 +84,7 @@ private slots: void databaseDownloadFinished(); private: - DatabaseError loadDescriptions(QList > > &descriptions); + DatabaseError loadDescriptions(QList > > &descriptions, ChronoUtilities::DateTime *lastModified = nullptr); QNetworkRequest regularDatabaseRequest(); QNetworkRequest filesDatabaseRequest(); diff --git a/alpm/config.cpp b/alpm/config.cpp index 957c64d..1a1ade8 100644 --- a/alpm/config.cpp +++ b/alpm/config.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -40,6 +41,7 @@ ConfigArgs::ConfigArgs(ArgumentParser &parser) : rootdirArg("root-dir", "r", "specifies the root directory (default is /)"), dbpathArg("db-path", "d", "specifies the pacman database path (default is /var/lib/pacman)"), pacmanConfArg("pacman-conf", "p", "specifies the path of the pacman config file (default is /etc/pacman.conf"), + reposFromPacmanConfEnabled("repos-from-pacman-conf", string(), "enables repositories from the pacman config file"), websocketAddrArg("addr", string(), "specifies the listening address for the websocket server, default is 127.0.0.1"), websocketPortArg("port", string(), "specifies the listening port for the websocket server, default is 1234"), certFileArg("cert-file", string(), "specifies the SSL certificate"), @@ -84,6 +86,7 @@ ConfigArgs::ConfigArgs(ArgumentParser &parser) : pacmanConfArg.setCombinable(true); pacmanConfArg.setValueNames(pathValueName); pacmanConfArg.setRequiredValueCount(1); + reposFromPacmanConfEnabled.setCombinable(true); websocketAddrArg.setCombinable(true); websocketAddrArg.setValueNames({"IP address"}); websocketAddrArg.setRequiredValueCount(1); @@ -127,7 +130,7 @@ ConfigArgs::ConfigArgs(ArgumentParser &parser) : shSyntaxArg.setCombinable(true); repoArg.setRequiredValueCount(1); repoArg.setValueNames({"repo name"}); - serverArg.setSecondaryArguments({&rootdirArg, &dbpathArg, &pacmanConfArg, &certFileArg, &keyFileArg, &websocketAddrArg, &websocketPortArg, &insecureArg, &aurArg, &shSyntaxArg}); + serverArg.setSecondaryArguments({&rootdirArg, &dbpathArg, &pacmanConfArg, &reposFromPacmanConfEnabled, &certFileArg, &keyFileArg, &websocketAddrArg, &websocketPortArg, &insecureArg, &aurArg, &shSyntaxArg}); upgradeLookupArg.setSecondaryArguments({&shSyntaxArg}); buildOrderArg.setSecondaryArguments({&aurArg, &addSourceOnlyDepsArg, &requireSourcesArg, &verboseArg, &shSyntaxArg}); mingwBundleArg.setSecondaryArguments({&targetDirArg, &targetNameArg, &targetFormatArg, &iconThemesArg, &defaultIconThemeArg, &extraPackagesArg}); @@ -137,7 +140,7 @@ ConfigArgs::ConfigArgs(ArgumentParser &parser) : storageDirArg.setCombinable(true); storageDirArg.setRequiredValueCount(1); storageDirArg.setValueNames(pathValueName); - parser.setMainArguments({&buildOrderArg, &upgradeLookupArg, &serverArg, &mingwBundleArg, &repoindexConfArg, &repoindexConfArg, &cacheDirArg, &helpArg}); + parser.setMainArguments({&buildOrderArg, &upgradeLookupArg, &serverArg, &mingwBundleArg, &repoindexConfArg, &repoindexConfArg, &cacheDirArg, &storageDirArg, &helpArg}); } /*! @@ -157,6 +160,7 @@ Config::Config() : m_websocketServerListeningAddr(QHostAddress::LocalHost), m_websocketServerListeningPort(1234), m_serverInsecure(false), + m_serverCloseable(true), m_localEnabled(true), m_reposFromPacmanConfEnabled(false), m_aurEnabled(true), @@ -280,8 +284,8 @@ void Config::loadFromConfigFile(const ConfigArgs &args) loadFromConfigFile(QString::fromLocal8Bit(args.repoindexConfArg.values().front().data())); return; } else { - for(const auto &defaultPath : {QStringLiteral("./repoindex.conf"), QStringLiteral("/etc/repoindex.conf")}) { - if(QFile::exists(defaultPath)) { + for(const auto &defaultPath : {QStringLiteral("./repoindex.conf"), QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/repoindex.conf"), QStringLiteral("/etc/repoindex.conf")}) { + if(!defaultPath.isEmpty() && QFile::exists(defaultPath)) { loadFromConfigFile(defaultPath); return; } @@ -322,6 +326,18 @@ void Config::loadFromArgs(const ConfigArgs &args) if(args.storageDirArg.isPresent()) { m_storageDir = QString::fromLocal8Bit(args.storageDirArg.values().front().data()); } + if(args.reposFromPacmanConfEnabled.isPresent()) { + m_reposFromPacmanConfEnabled = true; + } +} + +/*! + * \brief Ensures that the server is only accessable from the local machine. + */ +void Config::loadLocalOnlySetup() +{ + m_websocketServerListeningAddr = QHostAddress::LocalHost; + m_serverInsecure = true; } RepoEntry::RepoEntry() : diff --git a/alpm/config.h b/alpm/config.h index ea0e0d5..612e290 100644 --- a/alpm/config.h +++ b/alpm/config.h @@ -33,6 +33,7 @@ public: ApplicationUtilities::Argument rootdirArg; ApplicationUtilities::Argument dbpathArg; ApplicationUtilities::Argument pacmanConfArg; + ApplicationUtilities::Argument reposFromPacmanConfEnabled; ApplicationUtilities::Argument websocketAddrArg; ApplicationUtilities::Argument websocketPortArg; ApplicationUtilities::Argument certFileArg; @@ -143,6 +144,8 @@ public: const QString &serverCertFile() const; const QString &serverKeyFile() const; bool serverInsecure() const; + bool isServerCloseable() const; + void setServerCloseable(bool closeable); bool isLocalDatabaseEnabled() const; bool areReposFromPacmanConfEnabled() const; const QList &repoEntries() const; @@ -154,6 +157,7 @@ public: void loadFromConfigFile(const QString &args); void loadFromConfigFile(const ConfigArgs &args); void loadFromArgs(const ConfigArgs &args); + void loadLocalOnlySetup(); private: QString m_alpmRootDir; @@ -167,6 +171,7 @@ private: QString m_serverCertFile; QString m_serverKeyFile; bool m_serverInsecure; + bool m_serverCloseable; QList m_repoEntries; bool m_localEnabled; @@ -226,6 +231,16 @@ inline bool Config::serverInsecure() const return m_serverInsecure; } +inline bool Config::isServerCloseable() const +{ + return m_serverCloseable; +} + +inline void Config::setServerCloseable(bool closeable) +{ + m_serverCloseable = closeable; +} + inline bool Config::isLocalDatabaseEnabled() const { return m_localEnabled; diff --git a/alpm/manager.cpp b/alpm/manager.cpp index 10af3e3..41d0215 100644 --- a/alpm/manager.cpp +++ b/alpm/manager.cpp @@ -523,9 +523,6 @@ void Manager::initAlpmDataBases() delete loader; } } - for(auto &syncDbEntry : m_syncDbMap) { - syncDbEntry.second->updateGroups(); - } if(m_config.isVerbose() || m_config.runServer()) { cerr << "DONE" << endl; } @@ -547,6 +544,10 @@ void Manager::computeRequiredBy(Repository *repo) } else { relevantDbs.reserve(m_syncDbs.size()); for(auto &syncDb : m_syncDbs) { + if(syncDb->isBusy()) { + syncDb->asSoonAsPossible(bind(&Manager::computeRequiredBy, this, repo)); + return; + } relevantDbs << syncDb.get(); } } @@ -590,6 +591,7 @@ void Manager::writeCache() // could iterate through all repos and check isCachingUseful() but // currently its just the AUR which is needed to be cached if(userRepository()) { + QDir().mkpath(config().cacheDir()); QFile file(config().cacheDir() % QChar('/') % userRepository()->name() % QStringLiteral(".cache")); if(file.open(QFileDevice::WriteOnly)) { QDataStream stream(&file); @@ -690,13 +692,17 @@ void Manager::setAutoUpdateEnabled(bool enabled) void Manager::updateAlpmDatabases() { if(localDatabase()) { + QReadLocker locker(localDatabase()->lock()); if(localDatabase()->hasOutdatedPackages()) { + locker.unlock(); localDatabase()->init(); } } - for(auto &syncDbEntry : m_syncDbMap) { - if(syncDbEntry.second->hasOutdatedPackages()) { - syncDbEntry.second->refresh(m_config.storageDir() + QStringLiteral("/sync")); + for(auto &syncDb : m_syncDbs) { + QReadLocker locker(syncDb->lock()); + if(syncDb->hasOutdatedPackages()) { + locker.unlock(); + syncDb->refresh(m_config.storageDir() + QStringLiteral("/sync")); } } } diff --git a/alpm/package.cpp b/alpm/package.cpp index d6c9f88..0b4eaf7 100644 --- a/alpm/package.cpp +++ b/alpm/package.cpp @@ -34,7 +34,6 @@ namespace RepoIndex { Package::Package(const QString &name, Repository *repository) : m_origin(PackageOrigin::Unknown), m_repository(repository), - m_timeStamp(DateTime::now()), m_hasGeneralInfo(false), m_hasAllGeneralInfo(false), m_name(name), diff --git a/alpm/package.h b/alpm/package.h index 8ce5839..e85ac00 100644 --- a/alpm/package.h +++ b/alpm/package.h @@ -229,6 +229,7 @@ public: PackageOrigin origin() const; Repository *repository() const; ChronoUtilities::DateTime timeStamp() const; + void setTimeStamp(ChronoUtilities::DateTime timeStamp); bool hasGeneralInfo() const; bool hasAllGeneralInfo() const; const QString &name() const; @@ -427,6 +428,14 @@ inline ChronoUtilities::DateTime Package::timeStamp() const return m_timeStamp; } +/*! + * \brief Sets the package's timestamp. + */ +inline void Package::setTimeStamp(ChronoUtilities::DateTime timeStamp) +{ + m_timeStamp = timeStamp; +} + /*! * \brief Returns whether general information is available for the package. */ diff --git a/alpm/repository.cpp b/alpm/repository.cpp index eaee9d7..dfb18b6 100644 --- a/alpm/repository.cpp +++ b/alpm/repository.cpp @@ -73,6 +73,12 @@ const QStringList Repository::packageNames() const return names; } +/*! + * \brief Updates the groups. + * + * This method is automatically after initialization, so there is usually no need + * to call this method manually. + */ void Repository::updateGroups() { m_groups.clear(); @@ -101,9 +107,12 @@ PackageLoader *Repository::init() { addBusyFlag(); QWriteLocker locker(lock()); + // wipe current packages + wipePackages(); if(PackageLoader *loader = internalInit()) { if(loader->future().isRunning()) { auto watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, this, &Repository::updateGroups); connect(watcher, &QFutureWatcher::finished, this, &Repository::removeBusyFlag); connect(watcher, &QFutureWatcher::finished, this, &Repository::initialized); connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater); @@ -111,20 +120,30 @@ PackageLoader *Repository::init() } return loader; } else { + updateGroups(); + removeBusyFlag(); return nullptr; } } void Repository::initAsSoonAsPossible() +{ + asSoonAsPossible(bind(&Repository::init, this)); +} + +/*! + * \brief Performs the specified \a operation as soon as possible. + */ +void Repository::asSoonAsPossible(std::function operation) { if(isBusy()) { auto connection = make_shared(); - *connection = connect(this, &Repository::available, [connection, this] { + *connection = connect(this, &Repository::available, [connection, operation] { disconnect(*connection); - init(); + operation(); }); } else { - init(); + operation(); } } @@ -283,30 +302,6 @@ QList Repository::packageByFilter(std::function &relevantRepos) - { - m_blockedRepos.reserve(relevantRepos.size()); - for(Repository *repo : relevantRepos) { - if(repo->lock()->tryLockForWrite()) { - m_blockedRepos << repo; - } - } - } - - ~Blocker() - { - for(Repository *repo : m_blockedRepos) { - repo->lock()->unlock(); - } - } - -private: - QList m_blockedRepos; -}; - class ComputeRequired { public: @@ -555,7 +550,7 @@ void Repository::restoreFromCacheStream(QDataStream &in, bool skipOutdated) quint32 packageCount; in >> packageCount; bool good = true; - const auto now = DateTime::now(); + const auto now = DateTime::gmtNow(); for(quint32 i = 0; i < packageCount && good && in.status() == QDataStream::Ok; ++i) { if(auto package = emptyPackage()) { package->restoreFromCacheStream(in); @@ -632,7 +627,7 @@ void Repository::cleanOutdatedPackages() if(maxPackageAge().isInfinity()) { return; } - auto now = DateTime::now(); + auto now = DateTime::gmtNow(); for(auto i = m_packages.begin(); i != m_packages.end(); ) { const Package &pkg = *i->second; if((now - pkg.timeStamp()) > maxPackageAge()) { @@ -652,10 +647,9 @@ bool Repository::hasOutdatedPackages() if(maxPackageAge().isInfinity()) { return false; } - auto now = DateTime::now(); - for(auto i = m_packages.begin(); i != m_packages.end(); ) { - const Package &pkg = *i->second; - if((now - pkg.timeStamp()) > maxPackageAge()) { + auto now = DateTime::gmtNow(); + for(const auto &pkgEntry : m_packages) { + if((now - pkgEntry.second->timeStamp()) > maxPackageAge()) { return true; } } @@ -866,7 +860,7 @@ void Repository::parseDescriptions(const QList &descriptions, QStrin * \returns Returns the added/updated packages. In the case of a split package more then * one package is returned. */ -QList Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo) +QList Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo, ChronoUtilities::DateTime timeStamp) { // define states enum { @@ -965,6 +959,7 @@ QList Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo) pkg = emptyPackage(); } currentPackage = pkg.get(); + currentPackage->setTimeStamp(timeStamp); packageInfo.clear(); } // add field to ... @@ -1007,7 +1002,7 @@ QList Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo) * - If \a name is empty and the description doesn't provide a name either, the package can not be added. * \returns Returns the added/updated package or nullptr if no package could be added. */ -Package *Repository::addPackageFromDescription(QString name, const QList &descriptions, PackageOrigin origin) +Package *Repository::addPackageFromDescription(QString name, const QList &descriptions, PackageOrigin origin, DateTime timeStamp) { // parse fields QList > fields; @@ -1021,6 +1016,7 @@ Package *Repository::addPackageFromDescription(QString name, const QListsetTimeStamp(timeStamp); pkgRawPtr->putDescription(name, fields, origin); { QWriteLocker locker(&m_lock); diff --git a/alpm/repository.h b/alpm/repository.h index 1c9ae78..f290047 100644 --- a/alpm/repository.h +++ b/alpm/repository.h @@ -212,16 +212,16 @@ public: bool isPackageOnly() const; std::map > &groups(); const std::map > &groups() const; - void updateGroups(); + Q_SLOT void updateGroups(); const QStringList &serverUrls() const; QStringList &serverUrls(); SignatureLevel sigLevel() const; void setSigLevel(SignatureLevel sigLevel); // gathering data -public: - PackageLoader *init(); + Q_SLOT PackageLoader *init(); void initAsSoonAsPossible(); + void asSoonAsPossible(std::function operation); virtual PackageLoader *internalInit(); virtual PackageDetailAvailability requestsRequired(PackageDetail packageDetail = PackageDetail::Basics) const; virtual SuggestionsReply *requestSuggestions(const QString &phrase); @@ -273,8 +273,8 @@ public: // parsing src/pkg info static void parsePkgInfo(const QByteArray &pkgInfo, QString &name, QList > packageInfo); static void parseDescriptions(const QList &descriptions, QString &name, QList > &fields); - QList addPackagesFromSrcInfo(const QByteArray &srcInfo); - Package *addPackageFromDescription(QString name, const QList &descriptions, PackageOrigin origin); + QList addPackagesFromSrcInfo(const QByteArray &srcInfo, ChronoUtilities::DateTime timeStamp); + Package *addPackageFromDescription(QString name, const QList &descriptions, PackageOrigin origin, ChronoUtilities::DateTime timeStamp); // thread synchronization QReadWriteLock *lock() const; @@ -576,6 +576,7 @@ inline void Repository::setMaxPackageAge(ChronoUtilities::TimeSpan maxPackageAge inline void Repository::wipePackages() { m_packages.clear(); + m_groups.clear(); } /*! diff --git a/main.cpp b/cli/main.cpp similarity index 97% rename from main.cpp rename to cli/main.cpp index 68611c9..af41ade 100644 --- a/main.cpp +++ b/cli/main.cpp @@ -15,8 +15,6 @@ #include #include -#include - using namespace std; using namespace ApplicationUtilities; using namespace RepoIndex; diff --git a/gui/main.cpp b/gui/main.cpp new file mode 100644 index 0000000..52c0e0b --- /dev/null +++ b/gui/main.cpp @@ -0,0 +1,106 @@ +#include "./alpm/manager.h" +#include "./alpm/utilities.h" +#include "./alpm/config.h" + +#include "./network/server.h" + +#include "./gui/mainwindow.h" + +#include "resources/config.h" + +#include +#include + +#include +#include +#include + +#include + +#include + +using namespace std; +using namespace ApplicationUtilities; +using namespace RepoIndex; + +int main(int argc, char *argv[]) +{ + // setup the argument parser + ArgumentParser parser; + SET_APPLICATION_INFO; + SET_QT_APPLICATION_INFO; + QT_CONFIG_ARGUMENTS qtConfigArgs; + ConfigArgs configArgs(parser); + parser.setIgnoreUnknownArguments(false); + Argument webdirArg("web-dir", string(), "specifies the directory of the web files"); + webdirArg.setCombinable(true); + webdirArg.setRequiredValueCount(1); + webdirArg.setValueNames({"path"}); + for(Argument *arg : initializer_list{&configArgs.rootdirArg, &configArgs.dbpathArg, &configArgs.pacmanConfArg, &configArgs.reposFromPacmanConfEnabled, &configArgs.aurArg}) { + qtConfigArgs.qtWidgetsGuiArg().addSecondaryArgument(arg); + } + parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &configArgs.repoindexConfArg, &configArgs.repoindexConfArg, &webdirArg, &configArgs.cacheDirArg, &configArgs.storageDirArg, &configArgs.helpArg}); + // parse command line arguments + try { + parser.parseArgs(argc, argv); + } catch (const Failure &e) { + cerr << shchar << "Unable to parse arguments: " << e.what() << endl; + return 2; + } + try { + // load configuration + Config config; + config.loadFromConfigFile(configArgs); + config.loadFromArgs(configArgs); + config.loadLocalOnlySetup(); + config.setServerCloseable(false); + + if(qtConfigArgs.qtWidgetsGuiArg().isPresent()) { + // configure Qt + qtConfigArgs.applySettings(); + + // find directory with web files + QString webdir; + if(webdirArg.isPresent()) { + webdir = QString::fromLocal8Bit(webdirArg.values().front().data()); + } else { + webdir = QStringLiteral("/usr/share/" PROJECT_NAME "/web"); + } + + // create app + QApplication application(argc, argv); + MainWindow mainWindow(webdir); + mainWindow.show(); + + // setup manager + Manager manager(config); + cerr << shchar << "Loading databases ..." << endl; + if(config.areReposFromPacmanConfEnabled()) { + manager.addDataBasesFromPacmanConfig(); + } + manager.addDatabasesFromRepoIndexConfig(); + manager.initAlpmDataBases(); + cerr << shchar << "Restoring cache ... "; + manager.restoreCache(); + cerr << shchar << "DONE" << endl; + + // setup the server + Server server(manager, manager.config()); + manager.setAutoCacheMaintenanceEnabled(true); + manager.setAutoUpdateEnabled(true); + + // run Qt event loop + return application.exec(); + + } else if(!configArgs.helpArg.isPresent()) { + if(useShSyntax) { + cerr << "export REPOINDEX_ERROR='No command line arguments specified. See --help for available commands.'" << endl; + } else { + cerr << "No command line arguments specified. See --help for available commands." << endl; + } + } + } catch (const exception &ex) { + Utilities::printError(ex); + return 1; + } +} diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp new file mode 100644 index 0000000..96ade1d --- /dev/null +++ b/gui/mainwindow.cpp @@ -0,0 +1,98 @@ +#include "./mainwindow.h" +#include "./webpage.h" + +#include "resources/config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RepoIndex { + +/*! + * \brief Constructs a new main window. + */ +MainWindow::MainWindow(const QString &webdir) +{ + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(), QCoreApplication::applicationName()); + setWindowTitle(QStringLiteral(APP_NAME)); + setWindowIcon(QIcon::fromTheme(QStringLiteral(PROJECT_NAME))); + m_webView.setPage(new WebPage(&m_webView)); + QUrl url(QStringLiteral("file://") % webdir % QStringLiteral("/index.html")); + url.setFragment(settings.value(QStringLiteral("fragment"), QStringLiteral("packages")).toString()); + m_webView.setUrl(url); + m_webView.setContextMenuPolicy(Qt::CustomContextMenu); + connect(&m_webView, &QWidget::customContextMenuRequested, this, &MainWindow::showInfoWebViewContextMenu); + setCentralWidget(&m_webView); + restoreGeometry(settings.value(QStringLiteral("geometry")).toByteArray()); +} + +bool MainWindow::event(QEvent *event) +{ + switch(event->type()) { + case QEvent::KeyRelease: { + QKeyEvent *keyEvent = static_cast(event); + switch(keyEvent->key()) { + case Qt::Key_Left: + case Qt::Key_Back: + m_webView.back(); + return true; + case Qt::Key_Right: + case Qt::Key_Forward: + m_webView.forward(); + return true; + default: + ; + } + } case QEvent::MouseButtonPress: { + QMouseEvent *mouseEvent = static_cast(event); + switch(mouseEvent->button()) { + case Qt::BackButton: + m_webView.back(); + return true; + case Qt::ForwardButton: + m_webView.forward(); + return true; + default: + ; + } + break; + } case QEvent::Close: { + // save settings + QSettings settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(), QCoreApplication::applicationName()); + settings.setValue(QStringLiteral("geometry"), saveGeometry()); + settings.setValue(QStringLiteral("fragment"), m_webView.url().fragment()); + break; + } default: + ; + } + return QMainWindow::event(event); +} + +/*! + * \brief Shows the context menu for the info web view. + */ +void MainWindow::showInfoWebViewContextMenu(const QPoint &) +{ + QAction copyAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), nullptr); + copyAction.setDisabled(m_webView.selectedText().isEmpty()); + connect(©Action, &QAction::triggered, this, &MainWindow::copyInfoWebViewSelection); + QMenu menu; + menu.addAction(©Action); + menu.exec(QCursor::pos()); +} + +/*! + * \brief Copies the current selection of the info web view. + */ +void MainWindow::copyInfoWebViewSelection() +{ + QGuiApplication::clipboard()->setText(m_webView.selectedText()); +} + +} diff --git a/gui/mainwindow.h b/gui/mainwindow.h new file mode 100644 index 0000000..ec4b0a0 --- /dev/null +++ b/gui/mainwindow.h @@ -0,0 +1,34 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "./webviewprovider.h" + +#include +#ifdef REPOINDEX_USE_WEBENGINE +# include +#else +# include +#endif + +namespace RepoIndex { + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + MainWindow(const QString &webdir); + +protected: + bool event(QEvent *event); + +private slots: + void showInfoWebViewContextMenu(const QPoint &); + void copyInfoWebViewSelection(); + +private: + WEB_VIEW_PROVIDER m_webView; +}; + +} + +#endif // MAINWINDOW_H diff --git a/gui/webpage.cpp b/gui/webpage.cpp new file mode 100644 index 0000000..d5ff409 --- /dev/null +++ b/gui/webpage.cpp @@ -0,0 +1,51 @@ +#include "./webpage.h" + +#include "resources/config.h" + +#include +#ifdef REPOINDEX_USE_WEBENGINE +# include +# include +#else +# include +# include +#endif + +namespace RepoIndex { + +WebPage::WebPage(WEB_VIEW_PROVIDER *view) : + WEB_PAGE_PROVIDER(view), + m_view(view) +{ +#ifdef REPOINDEX_USE_WEBENGINE + settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); +#else + settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); +#endif + if(!m_view) { + // delegate to external browser if no view is assigned + connect(this, &WebPage::urlChanged, this, &WebPage::delegateToExternalBrowser); + m_view = new WEB_VIEW_PROVIDER; + m_view->setPage(this); + } +} + +WEB_PAGE_PROVIDER *WebPage::createWindow(QWebEnginePage::WebWindowType type) +{ + return new WebPage; +} + +void WebPage::delegateToExternalBrowser(const QUrl &url) +{ + openUrlExternal(url); + // this page and the associated view are useless + m_view->deleteLater(); + deleteLater(); +} + +void WebPage::openUrlExternal(const QUrl &url) +{ + QDesktopServices::openUrl(url); +} + +} diff --git a/gui/webpage.h b/gui/webpage.h new file mode 100644 index 0000000..49f1168 --- /dev/null +++ b/gui/webpage.h @@ -0,0 +1,37 @@ +#ifndef WEBPAGE_H +#define WEBPAGE_H + +#include "./webviewprovider.h" + +#ifdef REPOINDEX_USE_WEBENGINE +# include +#else +# include +#endif + +QT_FORWARD_DECLARE_CLASS(WEB_VIEW_PROVIDER) + +namespace RepoIndex { + +class WebPage : public WEB_PAGE_PROVIDER +{ + Q_OBJECT +public: + WebPage(WEB_VIEW_PROVIDER *view = nullptr); + +public slots: + void openUrlExternal(const QUrl &url); + +protected: + WEB_PAGE_PROVIDER *createWindow(WebWindowType type); + +private slots: + void delegateToExternalBrowser(const QUrl &url); + +private: + WEB_VIEW_PROVIDER *m_view; +}; + +} + +#endif // WEBPAGE_H diff --git a/gui/webviewprovider.h b/gui/webviewprovider.h new file mode 100644 index 0000000..9bd3e95 --- /dev/null +++ b/gui/webviewprovider.h @@ -0,0 +1,9 @@ +#ifndef WEB_VIEW_PROVIDER +#ifdef REPOINDEX_USE_WEBENGINE +# define WEB_VIEW_PROVIDER QWebEngineView +# define WEB_PAGE_PROVIDER QWebEnginePage +#else +# define WEB_VIEW_PROVIDER QWebView +# define WEB_PAGE_PROVIDER QWebPage +#endif +#endif diff --git a/network/connection.cpp b/network/connection.cpp index 975ed28..53f61bd 100644 --- a/network/connection.cpp +++ b/network/connection.cpp @@ -132,7 +132,7 @@ void Connection::handleCmd(const QJsonObject &obj) const auto what = obj.value(QStringLiteral("w")).toString(); const auto id = obj.value(QStringLiteral("id")); if(what == QLatin1String("stop")) { - if(m_socket->peerAddress().isLoopback()) { + if(m_manager.config().isServerCloseable() && m_socket->peerAddress().isLoopback()) { cerr << shchar << "Info: Server stopped via web interface." << endl; QCoreApplication::quit(); } else { diff --git a/network/userrepository.cpp b/network/userrepository.cpp index 6812caa..4a4da3f 100644 --- a/network/userrepository.cpp +++ b/network/userrepository.cpp @@ -55,6 +55,7 @@ void AurPackageReply::processData(QNetworkReply *reply) QJsonParseError error; const auto doc = QJsonDocument::fromJson(reply->readAll(), &error); if(error.error == QJsonParseError::NoError) { + auto now = DateTime::gmtNow(); QWriteLocker locker(m_repo->lock()); auto &packages = m_repo->packages(); for(const auto &result : doc.object().value(QStringLiteral("results")).toArray()) { @@ -67,6 +68,7 @@ void AurPackageReply::processData(QNetworkReply *reply) } else { package = make_unique(obj, static_cast(m_userRepo)); } + package->setTimeStamp(now); } } } else { @@ -113,7 +115,7 @@ void AurFullPackageReply::processData(QNetworkReply *reply) if(srcInfoEntry && srcInfoEntry->isFile()) { const auto srcInfo = static_cast(srcInfoEntry)->data(); QWriteLocker locker(m_userRepo->lock()); - const auto packages = m_userRepo->addPackagesFromSrcInfo(srcInfo); + const auto packages = m_userRepo->addPackagesFromSrcInfo(srcInfo, DateTime::gmtNow()); // TODO: error handling for(const auto &entryName : baseDir->entries()) { if(entryName != QLatin1String(".SRCINFO")) { diff --git a/resources/desktop/applications/repoindex.desktop b/resources/desktop/applications/repoindex.desktop new file mode 100755 index 0000000..adc6548 --- /dev/null +++ b/resources/desktop/applications/repoindex.desktop @@ -0,0 +1,9 @@ +#!/usr/bin/env xdg-open +[Desktop Entry] +Name=Repository Browser +Comment=An Arch Linux repository browser. +Exec=sh -c "repoindex-gui --repos-from-pacman-conf --cache-dir \\"\\$HOME/.cache/Martchus/Repository Browser\\" --storage-dir \\"\\$HOME/.config/Martchus/Repository Browser\\"" +Icon=repoindex +Terminal=false +Type=Application +Categories=Utility; diff --git a/resources/icons/hicolor/scalable/apps/repoindex.svg b/resources/icons/hicolor/scalable/apps/repoindex.svg new file mode 100644 index 0000000..aad35d2 --- /dev/null +++ b/resources/icons/hicolor/scalable/apps/repoindex.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/settings/repoindex.conf.js b/resources/settings/repoindex.conf.js index d9dc3bb..279f993 100644 --- a/resources/settings/repoindex.conf.js +++ b/resources/settings/repoindex.conf.js @@ -21,10 +21,12 @@ }, "repos": { + "localEnabled": true, "fromPacmanConfig": true, - "add": [ + "//add": [ {"name": "examplerepo", + "maxAge": 3600 "dataBaseFile": "path/to/database/file", "sourcesDir": "path/to/local/source/dir", "packagesDir": "path/to/local/pkg/dir", diff --git a/testing/repoindex.conf.js b/testing/repoindex.conf.js index 6f393fb..463646e 100644 --- a/testing/repoindex.conf.js +++ b/testing/repoindex.conf.js @@ -30,7 +30,8 @@ "upgradeSources": ["aur"], "server": [ "https://localhost/repo/arch/$repo/os/$arch" - ] + ], + "maxAge": 3600 }, {"name": "local", "maxAge": 3600}, diff --git a/web/index.html b/web/index.html index c03a31a..634e903 100644 --- a/web/index.html +++ b/web/index.html @@ -229,6 +229,7 @@