├── GitVersion.yml ├── client ├── src │ ├── assets │ │ └── brand │ │ │ ├── icon.svg │ │ │ ├── logo-white.svg │ │ │ └── logo.svg │ ├── scss │ │ ├── pages │ │ │ ├── _all.scss │ │ │ ├── _add-torrent.scss │ │ │ ├── _about.scss │ │ │ └── _settings.scss │ │ ├── components │ │ │ ├── _keyframes.scss │ │ │ ├── _main.scss │ │ │ ├── _layout.scss │ │ │ ├── _scroll-bar.scss │ │ │ ├── _footer.scss │ │ │ ├── _all.scss │ │ │ ├── _tree.scss │ │ │ ├── _dialog.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _typography.scss │ │ │ ├── _progress.scss │ │ │ ├── _tabs.scss │ │ │ ├── _button.scss │ │ │ ├── _table.scss │ │ │ ├── _torrent-file.scss │ │ │ ├── _form.scss │ │ │ ├── _status-bar.scss │ │ │ ├── _toast.scss │ │ │ ├── _header.scss │ │ │ └── _torrent-list.scss │ │ ├── style.scss │ │ └── _utilities.scss │ ├── pages │ │ ├── settings │ │ │ ├── connection │ │ │ │ ├── Layout.vue │ │ │ │ ├── AddListenInterface.vue │ │ │ │ └── Index.vue │ │ │ ├── Common.vue │ │ │ ├── Downloads.vue │ │ │ ├── Layout.vue │ │ │ ├── Profiles.vue │ │ │ └── Proxy.vue │ │ ├── Error.vue │ │ ├── Home.vue │ │ ├── AddMagnetLink.vue │ │ ├── AddTorrent.vue │ │ └── About.vue │ ├── store │ │ ├── index.js │ │ ├── modules │ │ │ ├── session.js │ │ │ └── torrents.js │ │ └── plugins │ │ │ └── ws.js │ ├── App.vue │ ├── main.js │ ├── plugins │ │ └── rpc.js │ ├── components │ │ ├── Toast.vue │ │ ├── Header.vue │ │ ├── StatusBar.vue │ │ └── TorrentList.vue │ └── router │ │ └── index.js ├── public │ ├── favicon.ico │ └── index.html ├── babel.config.js ├── vue.config.js ├── .gitignore ├── webpack.config.js ├── README.md └── package.json ├── vetur.config.js ├── .gitignore ├── .gitmodules ├── .dockerignore ├── src ├── data │ ├── sqliteexception.hpp │ ├── migrations │ │ └── 0001_initialsetup.hpp │ ├── transaction.hpp │ ├── models │ │ ├── config.hpp │ │ ├── listeninterface.hpp │ │ ├── config.cpp │ │ ├── settingspack.hpp │ │ ├── profile.hpp │ │ ├── proxy.hpp │ │ ├── listeninterface.cpp │ │ └── profile.cpp │ ├── datareader.hpp │ ├── statement.hpp │ ├── transaction.cpp │ └── statement.cpp ├── database.hpp ├── log.hpp ├── rpc │ ├── configget.hpp │ ├── configset.hpp │ ├── proxygetall.hpp │ ├── settingspacklist.hpp │ ├── profilesgetall.hpp │ ├── profilesgetactive.hpp │ ├── settingspackcreate.hpp │ ├── settingspackgetbyid.hpp │ ├── listeninterfacesgetall.hpp │ ├── proxygetall.cpp │ ├── sessionaddtorrent.hpp │ ├── profilesgetactive.cpp │ ├── torrentspause.hpp │ ├── sessionaddmagnetlink.hpp │ ├── sessionremovetorrent.hpp │ ├── torrentsresume.hpp │ ├── listeninterfacesgetall.cpp │ ├── profilesgetall.cpp │ ├── proxycreate.hpp │ ├── profilesupdate.hpp │ ├── listeninterfacescreate.hpp │ ├── listeninterfacesremove.hpp │ ├── settingspackupdate.hpp │ ├── configset.cpp │ ├── settingspackcreate.cpp │ ├── sessionremovetorrent.cpp │ ├── configget.cpp │ ├── command.hpp │ ├── listeninterfacesremove.cpp │ ├── listeninterfacescreate.cpp │ ├── settingspacklist.cpp │ ├── sessionaddmagnetlink.cpp │ ├── profilesupdate.cpp │ ├── torrentspause.cpp │ ├── torrentsresume.cpp │ ├── proxycreate.cpp │ ├── sessionaddtorrent.cpp │ ├── settingspackgetbyid.cpp │ └── settingspackupdate.cpp ├── tsdb │ ├── timeseriesdatabase.hpp │ ├── prometheus.hpp │ ├── influxdb.hpp │ └── prometheus.cpp ├── http │ ├── handlers │ │ ├── websockethandler.hpp │ │ └── jsonrpchandler.hpp │ ├── httprequesthandler.hpp │ ├── httplistener.hpp │ ├── mimetype.hpp │ ├── httplistener.cpp │ └── httpsession.hpp ├── json │ ├── listeninterface.hpp │ ├── torrentstatus.hpp │ ├── infohash.hpp │ ├── profile.hpp │ └── proxy.hpp ├── log.cpp ├── options.hpp ├── sessionmanager.hpp ├── database.cpp ├── main.cpp └── options.cpp ├── vendor └── vcpkg-overlays │ ├── triplets │ └── x64-linux-release.cmake │ └── ports │ └── libtorrent │ ├── vcpkg.json │ └── portfile.cmake ├── vcpkg.json ├── tests ├── helpers.hpp ├── mocks.hpp ├── main.cpp ├── data │ └── models.cpp └── rpc │ ├── configset.cpp │ ├── configget.cpp │ ├── torrentspause.cpp │ └── torrentsresume.cpp ├── Dockerfile ├── .github └── workflows │ └── ci.yml ├── README.md └── CMakeLists.txt /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: Mainline 2 | -------------------------------------------------------------------------------- /client/src/assets/brand/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vetur.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | './client' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vscode/ 4 | build/ 5 | cmake-build-*/ 6 | vendor/sqlite* 7 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/picotorrent/server/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/vcpkg"] 2 | path = vendor/vcpkg 3 | url = https://github.com/microsoft/vcpkg 4 | -------------------------------------------------------------------------------- /client/src/scss/pages/_all.scss: -------------------------------------------------------------------------------- 1 | // Pages 2 | @import "about"; 3 | @import "settings"; 4 | @import "add-torrent"; -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build/ 3 | client/dist 4 | client/node_modules 5 | cmake-build-* 6 | vendor/vcpkg/buildtrees 7 | vendor/vcpkg/downloads 8 | vendor/vcpkg/packages 9 | vendor/vcpkg/vcpkg 10 | -------------------------------------------------------------------------------- /src/data/sqliteexception.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::Data 6 | { 7 | class SQLiteException : public std::exception 8 | { 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/database.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server 6 | { 7 | class Database 8 | { 9 | public: 10 | static bool Migrate(sqlite3* db); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/pages/settings/connection/Layout.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /vendor/vcpkg-overlays/triplets/x64-linux-release.cmake: -------------------------------------------------------------------------------- 1 | set(VCPKG_TARGET_ARCHITECTURE x64) 2 | set(VCPKG_CRT_LINKAGE dynamic) 3 | set(VCPKG_LIBRARY_LINKAGE static) 4 | 5 | set(VCPKG_CMAKE_SYSTEM_NAME Linux) 6 | 7 | set(VCPKG_BUILD_TYPE release) 8 | -------------------------------------------------------------------------------- /src/data/migrations/0001_initialsetup.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::Data::Migrations 6 | { 7 | struct InitialSetup 8 | { 9 | static int Migrate(sqlite3* db); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server 6 | { 7 | class Log 8 | { 9 | public: 10 | static void Setup(boost::log::trivial::severity_level level); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/scss/pages/_add-torrent.scss: -------------------------------------------------------------------------------- 1 | // Page: Add Torrent 2 | main.add-torrent { 3 | .content { 4 | display: block; 5 | width: 100%; 6 | max-width: 450px; 7 | } 8 | .group { 9 | .item { 10 | flex: 1; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /client/src/pages/Error.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/scss/components/_keyframes.scss: -------------------------------------------------------------------------------- 1 | // Keyframes 2 | @keyframes fade-in { 3 | from { 4 | opacity: 0; 5 | } 6 | to { 7 | opacity: 1; 8 | } 9 | } 10 | @keyframes fade-out { 11 | from { 12 | opacity: 1; 13 | } 14 | to { 15 | opacity: 0; 16 | } 17 | } -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '^/api': { 5 | target: 'http://localhost:1337', 6 | ws: true 7 | } 8 | } 9 | }, 10 | css: { 11 | loaderOptions: { 12 | sass: { 13 | implementation: require('sass') 14 | } 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /client/src/scss/components/_main.scss: -------------------------------------------------------------------------------- 1 | // Main 2 | main { 3 | flex : 1; 4 | display : flex; 5 | flex-direction: column; 6 | overflow-y : auto; 7 | position : relative; 8 | animation : fade-in 0.35s forwards; 9 | &:not(.home) { 10 | .sidebar, 11 | .content { 12 | padding: 1rem; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/data/transaction.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::Data 6 | { 7 | struct Transaction 8 | { 9 | Transaction(sqlite3* db); 10 | ~Transaction(); 11 | 12 | void Commit(); 13 | void Rollback(); 14 | 15 | private: 16 | sqlite3* m_db; 17 | bool m_did_act; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/rpc/configget.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ConfigGetCommand : public Command 10 | { 11 | public: 12 | ConfigGetCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/configset.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ConfigSetCommand : public Command 10 | { 11 | public: 12 | ConfigSetCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/proxygetall.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "command.hpp" 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ProxyGetAllCommand : public Command 10 | { 11 | public: 12 | ProxyGetAllCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/settingspacklist.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class SettingsPackList : public Command 10 | { 11 | public: 12 | SettingsPackList(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/profilesgetall.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ProfilesGetAllCommand : public Command 10 | { 11 | public: 12 | ProfilesGetAllCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", 3 | "name": "picotorrent", 4 | "version-string": "1", 5 | "dependencies": [ 6 | "boost-beast", 7 | "boost-log", 8 | "boost-program-options", 9 | "boost-system", 10 | "catch2", 11 | "gtest", 12 | "libtorrent", 13 | "nlohmann-json", 14 | "sqlite3" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.s[ac]ss$/i, 6 | use: [ 7 | // Creates `style` nodes from JS strings 8 | "style-loader", 9 | // Translates CSS into CommonJS 10 | "css-loader", 11 | // Compiles Sass to CSS 12 | "sass-loader", 13 | ], 14 | }, 15 | ], 16 | }, 17 | }; -------------------------------------------------------------------------------- /src/rpc/profilesgetactive.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ProfilesGetActiveCommand : public Command 10 | { 11 | public: 12 | ProfilesGetActiveCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/settingspackcreate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class SettingsPackCreateCommand : public Command 10 | { 11 | public: 12 | SettingsPackCreateCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/settingspackgetbyid.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class SettingsPackGetByIdCommand : public Command 10 | { 11 | public: 12 | SettingsPackGetByIdCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/data/models/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace pt::Server::Data::Models 9 | { 10 | class Config 11 | { 12 | public: 13 | static nlohmann::json Get(sqlite3* db, const std::string_view& key); 14 | static void Set(sqlite3* db, const std::string_view& key, const nlohmann::json& value); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/assets/brand/logo-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import session from './modules/session' 5 | import torrents from './modules/torrents' 6 | import createWebSocketPlugin from './plugins/ws' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | session, 13 | torrents 14 | }, 15 | plugins: [ createWebSocketPlugin() ] 16 | }); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacesgetall.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::RPC 8 | { 9 | class ListenInterfacesGetAllCommand : public Command 10 | { 11 | public: 12 | ListenInterfacesGetAllCommand(sqlite3* db); 13 | nlohmann::json Execute(const nlohmann::json&) override; 14 | 15 | private: 16 | sqlite3* m_db; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/scss/components/_layout.scss: -------------------------------------------------------------------------------- 1 | // Layout 2 | // Row 3 | .row { 4 | display : flex; 5 | flex-wrap: wrap; 6 | &.cols-3 { 7 | flex : 1 1 33.333%; 8 | max-width: 33.333%; 9 | } 10 | &.cols-2 { 11 | flex : 1 1 50%; 12 | max-width: 50%; 13 | } 14 | &.cols-1 { 15 | flex : 1 1 100%; 16 | max-width: 100%; 17 | } 18 | } 19 | 20 | .content { 21 | animation : fade-in 0.35s forwards; 22 | } -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # PicoTorrent Server Web UI 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /src/tsdb/timeseriesdatabase.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace pt::Server::TSDB 9 | { 10 | struct TimeSeriesDatabase 11 | { 12 | virtual ~TimeSeriesDatabase() = default; 13 | 14 | virtual void WriteMetrics( 15 | std::map const& metrics, 16 | std::chrono::milliseconds timestamp) = 0; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/scss/components/_scroll-bar.scss: -------------------------------------------------------------------------------- 1 | 2 | // Scrollbar 3 | ::-webkit-scrollbar { 4 | width : 10px; 5 | height: 10px; 6 | } 7 | 8 | ::-webkit-scrollbar-thumb { 9 | background : var(--scrollbar-thumb-bg); 10 | border-radius: 999px; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb:hover{ 14 | background: var(--scrollbar-thumb-hover-bg); 15 | } 16 | 17 | ::-webkit-scrollbar-track{ 18 | background : var(--scrollbar-track-bg); 19 | border-radius: 0px; 20 | } 21 | -------------------------------------------------------------------------------- /src/data/datareader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | static std::optional pt_sqlite3_column_std_string(sqlite3_stmt* stmt, int columnId) 9 | { 10 | auto data = sqlite3_column_text(stmt, columnId); 11 | if (data == nullptr) { return std::nullopt; } 12 | return std::string( 13 | reinterpret_cast(data), 14 | sqlite3_column_bytes(stmt, columnId)); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/scss/components/_footer.scss: -------------------------------------------------------------------------------- 1 | 2 | // Footer 3 | footer { 4 | margin-top: auto; 5 | padding: .5rem; 6 | border-top: var(--separator); 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | // Info badge 11 | .info { 12 | display: flex; 13 | align-items: center; 14 | .badge { 15 | border-radius: 3px; 16 | padding: 1px 4px 1px 3px; 17 | margin-left: .5rem; 18 | margin-bottom: -2px; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/rpc/proxygetall.cpp: -------------------------------------------------------------------------------- 1 | #include "proxygetall.hpp" 2 | 3 | #include "../data/models/proxy.hpp" 4 | #include "../json/proxy.hpp" 5 | 6 | namespace lt = libtorrent; 7 | using json = nlohmann::json; 8 | using pt::Server::Data::Models::Proxy; 9 | using pt::Server::RPC::ProxyGetAllCommand; 10 | 11 | ProxyGetAllCommand::ProxyGetAllCommand(sqlite3* db) 12 | : m_db(db) 13 | { 14 | } 15 | 16 | json ProxyGetAllCommand::Execute(const json& params) 17 | { 18 | return Ok(Proxy::GetAll(m_db)); 19 | } 20 | -------------------------------------------------------------------------------- /tests/helpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace pt 7 | { 8 | static libtorrent::info_hash_t InfoHashFromString(const std::string_view& str) 9 | { 10 | if (str.size() == 40) 11 | { 12 | lt::sha1_hash hash; 13 | lt::aux::from_hex({ str.data(), 40 }, hash.data()); 14 | return lt::info_hash_t(hash); 15 | } 16 | 17 | return lt::info_hash_t(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/store/modules/session.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const state = { 4 | stats: {} 5 | }; 6 | 7 | const getters = { 8 | all: state => state.stats, 9 | byId: state => id => { 10 | return state.stats[id] 11 | } 12 | } 13 | 14 | const mutations = { 15 | UPDATE_STATS(state, stats) { 16 | for (const key in stats) { 17 | Vue.set(state.stats, key, stats[key]); 18 | } 19 | } 20 | } 21 | 22 | export default { 23 | namespaced: true, 24 | state, 25 | getters, 26 | mutations 27 | } 28 | -------------------------------------------------------------------------------- /client/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /src/rpc/sessionaddtorrent.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class SessionAddTorrentCommand : public Command 13 | { 14 | public: 15 | SessionAddTorrentCommand(std::shared_ptr); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | std::shared_ptr m_session; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/profilesgetactive.cpp: -------------------------------------------------------------------------------- 1 | #include "profilesgetactive.hpp" 2 | 3 | #include 4 | 5 | #include "../data/models/profile.hpp" 6 | #include "../json/profile.hpp" 7 | 8 | using json = nlohmann::json; 9 | using pt::Server::Data::Models::Profile; 10 | using pt::Server::RPC::ProfilesGetActiveCommand; 11 | 12 | ProfilesGetActiveCommand::ProfilesGetActiveCommand(sqlite3* db) 13 | : m_db(db) 14 | { 15 | } 16 | 17 | json ProfilesGetActiveCommand::Execute(const json& params) 18 | { 19 | return Ok(Profile::GetActive(m_db)); 20 | } 21 | -------------------------------------------------------------------------------- /src/rpc/torrentspause.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class ITorrentHandleFinder; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class TorrentsPauseCommand : public Command 13 | { 14 | public: 15 | TorrentsPauseCommand(std::shared_ptr); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | std::shared_ptr m_finder; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/sessionaddmagnetlink.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class SessionAddMagnetLinkCommand : public Command 13 | { 14 | public: 15 | SessionAddMagnetLinkCommand(std::shared_ptr); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | std::shared_ptr m_session; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/sessionremovetorrent.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class SessionRemoveTorrentCommand : public Command 13 | { 14 | public: 15 | SessionRemoveTorrentCommand(std::shared_ptr); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | std::shared_ptr m_session; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/torrentsresume.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class ITorrentHandleFinder; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class TorrentsResumeCommand : public Command 13 | { 14 | public: 15 | TorrentsResumeCommand(std::shared_ptr); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | std::shared_ptr m_finder; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /tests/mocks.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../src/sessionmanager.hpp" 6 | 7 | class MockTorrentHandleFinder : public pt::Server::ITorrentHandleFinder 8 | { 9 | public: 10 | MOCK_METHOD(std::shared_ptr, Find, (const libtorrent::info_hash_t& hash)); 11 | }; 12 | 13 | class MockTorrentHandleActor : public pt::Server::ITorrentHandleActor 14 | { 15 | public: 16 | MOCK_METHOD(bool, IsValid, ()); 17 | MOCK_METHOD(void, Pause, ()); 18 | MOCK_METHOD(void, Resume, ()); 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacesgetall.cpp: -------------------------------------------------------------------------------- 1 | #include "listeninterfacesgetall.hpp" 2 | 3 | #include "../data/models/listeninterface.hpp" 4 | #include "../json/listeninterface.hpp" 5 | 6 | using json = nlohmann::json; 7 | using pt::Server::Data::Models::ListenInterface; 8 | using pt::Server::RPC::ListenInterfacesGetAllCommand; 9 | 10 | ListenInterfacesGetAllCommand::ListenInterfacesGetAllCommand(sqlite3* db) 11 | : m_db(db) 12 | { 13 | } 14 | 15 | json ListenInterfacesGetAllCommand::Execute(const json& params) 16 | { 17 | return Ok(ListenInterface::GetAll(m_db)); 18 | } 19 | -------------------------------------------------------------------------------- /client/src/scss/components/_all.scss: -------------------------------------------------------------------------------- 1 | // Components 2 | // Structure 3 | @import "header"; 4 | @import "main"; 5 | @import "footer"; 6 | // UI 7 | @import "layout"; 8 | @import "typography"; 9 | @import "tooltip"; 10 | @import "torrent-list"; 11 | @import "torrent-file"; 12 | @import "status-bar"; 13 | @import "scroll-bar"; 14 | // Form, Inputs, Buttons 15 | @import "form"; 16 | @import "button"; 17 | @import "progress"; 18 | // Content 19 | @import "dialog"; 20 | @import "table"; 21 | @import "tabs"; 22 | @import "tree"; 23 | @import "toast"; 24 | // Other 25 | @import "keyframes"; -------------------------------------------------------------------------------- /src/rpc/profilesgetall.cpp: -------------------------------------------------------------------------------- 1 | #include "profilesgetall.hpp" 2 | 3 | #include 4 | 5 | #include "../data/datareader.hpp" 6 | #include "../data/models/profile.hpp" 7 | #include "../json/profile.hpp" 8 | 9 | using json = nlohmann::json; 10 | using pt::Server::Data::Models::Profile; 11 | using pt::Server::RPC::ProfilesGetAllCommand; 12 | 13 | ProfilesGetAllCommand::ProfilesGetAllCommand(sqlite3* db) 14 | : m_db(db) 15 | { 16 | } 17 | 18 | json ProfilesGetAllCommand::Execute(const json& params) 19 | { 20 | return Ok(Profile::GetAll(m_db)); 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/proxycreate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class ProxyCreateCommand : public Command 13 | { 14 | public: 15 | ProxyCreateCommand(sqlite3* db, std::shared_ptr session); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | sqlite3* m_db; 20 | std::shared_ptr m_session; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class Env : public ::testing::Environment 6 | { 7 | public: 8 | ~Env() override = default; 9 | void SetUp() override 10 | { 11 | boost::log::core::get()->set_filter(boost::log::trivial::severity > boost::log::trivial::fatal); 12 | } 13 | }; 14 | 15 | int main(int argc, char **argv) 16 | { 17 | ::testing::InitGoogleTest(&argc, argv); 18 | ::testing::AddGlobalTestEnvironment(new Env()); 19 | return RUN_ALL_TESTS(); 20 | } 21 | -------------------------------------------------------------------------------- /src/rpc/profilesupdate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class ProfilesUpdateCommand : public Command 13 | { 14 | public: 15 | ProfilesUpdateCommand(sqlite3* db, std::shared_ptr session); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | sqlite3* m_db; 20 | std::shared_ptr m_session; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/http/handlers/websockethandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../httprequesthandler.hpp" 4 | 5 | namespace pt::Server 6 | { 7 | class SessionManager; 8 | } 9 | 10 | namespace pt::Server::Http::Handlers 11 | { 12 | class WebSocketHandler : public HttpRequestHandler 13 | { 14 | public: 15 | explicit WebSocketHandler(std::shared_ptr sm); 16 | 17 | void Execute(std::shared_ptr context) override; 18 | 19 | private: 20 | std::shared_ptr m_sm; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacescreate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class ListenInterfacesCreateCommand : public Command 13 | { 14 | public: 15 | ListenInterfacesCreateCommand(sqlite3* db, std::shared_ptr session); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | sqlite3* m_db; 20 | std::shared_ptr m_session; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacesremove.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "command.hpp" 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class ListenInterfacesRemoveCommand : public Command 13 | { 14 | public: 15 | ListenInterfacesRemoveCommand(sqlite3* db, std::shared_ptr session); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | sqlite3* m_db; 20 | std::shared_ptr m_session; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PicoTorrent 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/rpc/settingspackupdate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.hpp" 4 | 5 | #include 6 | #include 7 | 8 | namespace pt::Server { class SessionManager; } 9 | 10 | namespace pt::Server::RPC 11 | { 12 | class SettingsPackUpdateCommand : public Command 13 | { 14 | public: 15 | SettingsPackUpdateCommand(sqlite3* db, std::shared_ptr const& session); 16 | nlohmann::json Execute(const nlohmann::json&) override; 17 | 18 | private: 19 | sqlite3* m_db; 20 | std::shared_ptr m_session; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/json/listeninterface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../data/models/listeninterface.hpp" 6 | 7 | using nlohmann::json; 8 | 9 | namespace pt::Server::Data::Models 10 | { 11 | static void to_json(json& j, const std::shared_ptr& li) 12 | { 13 | j = { 14 | { "id", li->Id() }, 15 | { "host", li->Host() }, 16 | { "port", li->Port() }, 17 | { "is_local", li->IsLocal() }, 18 | { "is_a", li->IsOutgoing() }, 19 | { "is_ssl", li->IsSsl() } 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/rpc/configset.cpp: -------------------------------------------------------------------------------- 1 | #include "configset.hpp" 2 | 3 | #include "../data/models/config.hpp" 4 | 5 | using json = nlohmann::json; 6 | using pt::Server::Data::Models::Config; 7 | using pt::Server::RPC::ConfigSetCommand; 8 | 9 | ConfigSetCommand::ConfigSetCommand(sqlite3* db) 10 | : m_db(db) 11 | { 12 | } 13 | 14 | json ConfigSetCommand::Execute(const json& params) 15 | { 16 | if (!params.is_object()) 17 | { 18 | return Error(-1, "params not an object"); 19 | } 20 | 21 | for (const auto& [key,value] : params.items()) 22 | { 23 | Config::Set(m_db, key, value); 24 | } 25 | 26 | return Ok(); 27 | } 28 | -------------------------------------------------------------------------------- /client/src/assets/brand/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/http/handlers/jsonrpchandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../httprequesthandler.hpp" 6 | #include "../../rpc/command.hpp" 7 | #include "../../sessionmanager.hpp" 8 | 9 | namespace pt::Server::Http::Handlers 10 | { 11 | class JsonRpcHandler : public HttpRequestHandler 12 | { 13 | public: 14 | explicit JsonRpcHandler(sqlite3* db, const std::shared_ptr& sm); 15 | 16 | void Execute(std::shared_ptr context) override; 17 | 18 | private: 19 | std::map> m_commands; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/http/httprequesthandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::Http 6 | { 7 | class HttpRequestHandler 8 | { 9 | public: 10 | class Context : public std::enable_shared_from_this 11 | { 12 | public: 13 | virtual boost::beast::http::request& Request() = 0; 14 | virtual boost::beast::tcp_stream& Stream() = 0; 15 | 16 | virtual void Write(boost::beast::http::response res) = 0; 17 | }; 18 | 19 | virtual void Execute(std::shared_ptr context) = 0; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /client/src/scss/components/_tree.scss: -------------------------------------------------------------------------------- 1 | // Tree 2 | .tree { 3 | width: 100%; 4 | .folder { 5 | width: 100%; 6 | margin-bottom: 0.25rem; 7 | .root { 8 | background: transparent; 9 | border: 0; 10 | color: var(--body-color-highlight); 11 | outline: 0; 12 | padding: 0; 13 | } 14 | .item { 15 | display: flex; 16 | align-items: center; 17 | flex: 1; 18 | padding: 0.25rem 0; 19 | & > div { 20 | margin: 0 0.5rem; 21 | &:first-child { 22 | margin-left: 1rem; 23 | width: 200px; 24 | height: 30px; 25 | overflow-x: auto; 26 | } 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import prettyBytes from 'pretty-bytes' 4 | 5 | import App from './App.vue' 6 | import RpcPlugin from './plugins/rpc' 7 | import router from './router' 8 | import store from './store' 9 | 10 | Vue.config.productionTip = false 11 | Vue.use(VueRouter) 12 | Vue.use(RpcPlugin) 13 | 14 | Vue.filter('fileSize', (val) => { 15 | if (!val) { return '-'; } 16 | return prettyBytes(val) 17 | }); 18 | 19 | Vue.filter('speed', (val) => { 20 | if (!val || val === 0) { 21 | return '-'; 22 | } 23 | return `${prettyBytes(val)}/s`; 24 | }); 25 | 26 | new Vue({ 27 | render: h => h(App), 28 | router, 29 | store 30 | }).$mount('#app') 31 | -------------------------------------------------------------------------------- /src/rpc/settingspackcreate.cpp: -------------------------------------------------------------------------------- 1 | #include "settingspackcreate.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "../data/datareader.hpp" 7 | #include "../data/models/settingspack.hpp" 8 | #include "../sessionmanager.hpp" 9 | 10 | namespace lt = libtorrent; 11 | using json = nlohmann::json; 12 | using pt::Server::Data::SettingsPack; 13 | using pt::Server::RPC::SettingsPackCreateCommand; 14 | using pt::Server::SessionManager; 15 | 16 | SettingsPackCreateCommand::SettingsPackCreateCommand(sqlite3* db) 17 | : m_db(db) 18 | { 19 | } 20 | 21 | json SettingsPackCreateCommand::Execute(const json& params) 22 | { 23 | return Error(-1, "not implemented"); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/plugins/rpc.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | async function makeRequest(method, params) { 4 | const response = await axios.post('/api/jsonrpc', { 5 | jsonrpc: '2.0', 6 | method, 7 | params 8 | }); 9 | 10 | return response.data.result; 11 | } 12 | 13 | export default { 14 | install (Vue) { 15 | Vue.prototype.$rpc = async function rpc(method, args) { 16 | if (Array.isArray(method)) { 17 | // Array is [ method, args ] 18 | const result = []; 19 | 20 | for (const exec of method) { 21 | result.push(await makeRequest(exec[0], exec[1] || [])); 22 | } 23 | 24 | return result; 25 | } 26 | 27 | return await makeRequest(method, args || []); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/scss/pages/_about.scss: -------------------------------------------------------------------------------- 1 | // Page: About 2 | main.about { 3 | // Table: Components 4 | .components { 5 | max-width: 600px; 6 | } 7 | // Social Links 8 | .social { 9 | display : flex; 10 | align-items: center; 11 | margin-bottom: 1rem; 12 | a { 13 | display : flex; 14 | align-items : center; 15 | text-decoration: none; 16 | margin-right : 0.75rem; 17 | transition : all 0.35s ease; 18 | &:last-child { 19 | margin-right: 0; 20 | } 21 | .icon { 22 | width: 16px; 23 | height: 16px; 24 | object-fit: cover; 25 | object-position: center; 26 | path { 27 | fill: #777; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/json/torrentstatus.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "infohash.hpp" 7 | 8 | using nlohmann::json; 9 | 10 | namespace libtorrent 11 | { 12 | static void to_json(json& j, const libtorrent::torrent_status& ts) 13 | { 14 | j = json { 15 | { "info_hash", ts.info_hashes }, 16 | { "name", ts.name }, 17 | { "progress", ts.progress }, 18 | { "save_path", ts.save_path }, 19 | { "total_wanted", ts.total_wanted }, 20 | { "state", ts.state }, 21 | { "dl", ts.download_payload_rate }, 22 | { "ul", ts.upload_payload_rate } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/tsdb/prometheus.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../http/httprequesthandler.hpp" 4 | #include "timeseriesdatabase.hpp" 5 | 6 | #include 7 | 8 | namespace pt::Server::TSDB 9 | { 10 | class Prometheus : public Http::HttpRequestHandler, public TimeSeriesDatabase 11 | { 12 | public: 13 | ~Prometheus() override; 14 | 15 | void Execute(std::shared_ptr context) override; 16 | 17 | void WriteMetrics( 18 | std::map const& metrics, 19 | std::chrono::milliseconds timestamp) override; 20 | 21 | private: 22 | std::optional> m_metrics; 23 | std::optional m_timestamp; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/rpc/sessionremovetorrent.cpp: -------------------------------------------------------------------------------- 1 | #include "sessionremovetorrent.hpp" 2 | 3 | #include 4 | 5 | #include "../json/infohash.hpp" 6 | #include "../sessionmanager.hpp" 7 | 8 | namespace lt = libtorrent; 9 | using json = nlohmann::json; 10 | using pt::Server::SessionManager; 11 | using pt::Server::RPC::SessionRemoveTorrentCommand; 12 | 13 | SessionRemoveTorrentCommand::SessionRemoveTorrentCommand(std::shared_ptr session) 14 | : m_session(session) 15 | { 16 | } 17 | 18 | json SessionRemoveTorrentCommand::Execute(const json& j) 19 | { 20 | if (j.is_array()) 21 | { 22 | for (lt::info_hash_t const& hash : j.get>()) 23 | { 24 | m_session->RemoveTorrent(hash); 25 | } 26 | } 27 | 28 | return Ok(); 29 | } 30 | -------------------------------------------------------------------------------- /src/json/infohash.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using nlohmann::json; 10 | 11 | namespace libtorrent 12 | { 13 | static void from_json(const json& j, libtorrent::info_hash_t& ih) 14 | { 15 | auto str = j.get(); 16 | 17 | if (str.size() == 40) 18 | { 19 | lt::sha1_hash hash; 20 | lt::aux::from_hex({ str.c_str(), 40 }, hash.data()); 21 | ih = lt::info_hash_t(hash); 22 | } 23 | } 24 | 25 | static void to_json(json& j, const libtorrent::info_hash_t& ih) 26 | { 27 | std::stringstream ss; 28 | if (ih.has_v2()) { ss << ih.v2; } 29 | else { ss << ih.v1; } 30 | 31 | j = ss.str(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/rpc/configget.cpp: -------------------------------------------------------------------------------- 1 | #include "configget.hpp" 2 | 3 | #include "../data/models/config.hpp" 4 | 5 | using json = nlohmann::json; 6 | using pt::Server::Data::Models::Config; 7 | using pt::Server::RPC::ConfigGetCommand; 8 | 9 | ConfigGetCommand::ConfigGetCommand(sqlite3* db) 10 | : m_db(db) 11 | { 12 | } 13 | 14 | json ConfigGetCommand::Execute(const json& params) 15 | { 16 | if (params.is_array()) 17 | { 18 | json result = json::object(); 19 | 20 | for (const auto& val : params.get>()) 21 | { 22 | result[val] = Config::Get(m_db, val); 23 | } 24 | 25 | return Ok(result); 26 | } 27 | else if (params.is_string()) 28 | { 29 | return Ok(Config::Get(m_db, params.get())); 30 | } 31 | 32 | return Error(-1, "no config keys specified"); 33 | } 34 | -------------------------------------------------------------------------------- /src/json/profile.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../data/models/profile.hpp" 6 | 7 | using nlohmann::json; 8 | 9 | namespace pt::Server::Data::Models 10 | { 11 | static void to_json(json& j, const std::shared_ptr& p) 12 | { 13 | j = { 14 | { "id", p->Id() }, 15 | { "name", p->Name() }, 16 | { "is_active", p->IsActive() }, 17 | { "description", p->Description() }, 18 | { "proxy_id", p->ProxyId() ? json(p->ProxyId().value()) : json() }, 19 | { "proxy_name", p->ProxyName() ? json(p->ProxyName().value()) : json() }, 20 | { "settings_pack_id", p->SettingsPackId(), }, 21 | { "settings_pack_name", p->SettingsPackName() } 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rpc/command.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::RPC 6 | { 7 | class Command 8 | { 9 | public: 10 | virtual ~Command() {} 11 | virtual nlohmann::json Execute(const nlohmann::json&) = 0; 12 | 13 | nlohmann::json Error(int code, std::string const& message, nlohmann::json data = {}) 14 | { 15 | return { 16 | { 17 | "error", 18 | { 19 | { "code", code }, 20 | { "message", message }, 21 | { "data", data } 22 | } 23 | } 24 | }; 25 | } 26 | 27 | nlohmann::json Ok(nlohmann::json result = {}) 28 | { 29 | return { 30 | { "result", result } 31 | }; 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /client/src/scss/components/_dialog.scss: -------------------------------------------------------------------------------- 1 | // Dialog 2 | .dialog { 3 | width : 100%; 4 | height : 100%; 5 | display : flex; 6 | flex-direction : column; 7 | align-items : center; 8 | justify-content: center; 9 | padding : 1rem; 10 | // Dialog: Content 11 | .content { 12 | padding : 2rem 1.5rem; 13 | background : rgba($dark, 0.25); 14 | border-radius: .75rem; 15 | .title { 16 | font-weight: 500; 17 | margin-bottom: 1.5rem; 18 | font-size: 1.25rem; 19 | } 20 | .icon { 21 | display : block; 22 | font-size : 4rem; 23 | line-height : 0; 24 | margin-bottom: 2rem; 25 | text-align : center; 26 | color : var(--primary); 27 | } 28 | } 29 | // Dialog: Error 30 | &.error { 31 | text-align: center; 32 | .content .icon { 33 | color: var(--red); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build js client 2 | FROM node:16 AS client-build-env 3 | WORKDIR /app 4 | COPY ./client/package*.json ./ 5 | RUN npm install 6 | COPY ./client . 7 | RUN npm run build 8 | 9 | # build server 10 | FROM debian:bookworm-slim AS build-env 11 | WORKDIR /app 12 | COPY . . 13 | RUN apt-get update \ 14 | && apt-get install -y build-essential cmake zip unzip curl git ninja-build pkg-config \ 15 | && mkdir build \ 16 | && cd build \ 17 | && cmake -DCMAKE_BUILD_TYPE=Release -DVCPKG_TARGET_TRIPLET=x64-linux-release -G Ninja .. \ 18 | && ninja \ 19 | && ./PicoTorrentTests 20 | 21 | # production layer 22 | FROM debian:bookworm-slim 23 | WORKDIR /app 24 | RUN mkdir client 25 | COPY --from=client-build-env /app/dist /app/client 26 | COPY --from=build-env /app/build/PicoTorrentServer /app 27 | ENV PICOTORRENT_HTTP_HOST=0.0.0.0 28 | ENV PICOTORRENT_WEBROOT_PATH=/app/client 29 | ENTRYPOINT [ "./PicoTorrentServer" ] 30 | -------------------------------------------------------------------------------- /client/src/components/Toast.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/scss/components/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: relative; 3 | .title { 4 | display: none; 5 | z-index : 9999; 6 | &.show { 7 | display: block; 8 | } 9 | } 10 | &:hover { 11 | .title { 12 | position : absolute !important; 13 | display : block !important; 14 | left : 50%; 15 | bottom : 0; 16 | margin : 0 !important; 17 | padding : 0.35rem 0.65rem !important; 18 | color : var(--body-highlight-color) !important; 19 | font-weight : 500 !important; 20 | background : rgba(50, 50, 50, 0.95) !important; 21 | border-radius : 0.25rem; 22 | backdrop-filter: blur(5px); 23 | box-shadow : 0 0.25rem 0.5rem -0.15rem rgba(0, 0, 0, 0.5); 24 | transform : translate(-50%, 100%); 25 | text-align : center; 26 | line-height : initial; 27 | animation : fade-in .5s forwards; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /client/src/scss/components/_typography.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color : var(--link-color); 3 | transition: all 0.35s ease; 4 | &:hover { 5 | color: var(--link-hover-color); 6 | } 7 | } 8 | 9 | p { 10 | font-size : .85rem; 11 | margin-bottom: 1rem; 12 | &.lead { 13 | font-size: 1rem; 14 | } 15 | } 16 | 17 | // Headings 18 | h1, h2, h3, h4, h5, h6 { 19 | margin : 0 0 1rem; 20 | line-height: 1.3; 21 | font-weight: 300; 22 | color : var(--body-color-highlight); 23 | } 24 | 25 | h1 { 26 | margin-top: 0; 27 | font-size: 2rem; 28 | } 29 | 30 | h2 { 31 | font-size: 1.75rem; 32 | } 33 | 34 | h3 { 35 | font-size: 1.5rem; 36 | } 37 | 38 | h4 { 39 | font-size: 1.25rem; 40 | } 41 | 42 | h5 { 43 | font-size: 1rem; 44 | } 45 | 46 | small, .small { 47 | font-size: 0.85rem; 48 | } 49 | 50 | a, p, h1, h2, h3, h4, h5, small, .small { 51 | &.medium { 52 | font-weight: 500 !important; 53 | } 54 | 55 | &.bold { 56 | font-weight: 600 !important; 57 | } 58 | } -------------------------------------------------------------------------------- /client/src/pages/settings/Common.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /src/data/statement.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace pt::Server::Data 11 | { 12 | class Statement 13 | { 14 | public: 15 | struct Row 16 | { 17 | friend class Statement; 18 | 19 | std::vector GetBlob(int col) const; 20 | bool GetBool(int col) const; 21 | int GetInt32(int col) const; 22 | std::string GetStdString(int col) const; 23 | bool IsNull(int col) const; 24 | 25 | private: 26 | Row(sqlite3_stmt* stmt); 27 | sqlite3_stmt* m_stmt; 28 | }; 29 | 30 | static void ForEach( 31 | sqlite3* db, 32 | std::string const& sql, 33 | std::function const& cb, 34 | std::function bind = {}); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/scss/components/_progress.scss: -------------------------------------------------------------------------------- 1 | // Progress bar 2 | progress { 3 | display : flex; 4 | background-color: var(--progress-bg); 5 | height : var(--progress-height); 6 | border-radius : var(--progress-border-radius); 7 | overflow : hidden; 8 | min-width: 100px; 9 | position: relative; 10 | font-size: 80%; 11 | font-weight: bold; 12 | line-height: 0; 13 | &[value] { 14 | appearance: none; 15 | border: none; 16 | width: 100%; 17 | } 18 | &::-webkit-progress-bar { 19 | background: transparent; 20 | } 21 | &::-webkit-progress-value { 22 | display : flex; 23 | flex-direction : column; 24 | align-items : center; 25 | justify-content: center; 26 | overflow : hidden; 27 | text-align : center; 28 | white-space : nowrap; 29 | line-height : 0; 30 | color : var(--progress-bar-color); 31 | background : var(--progress-bar-bg); 32 | border-radius : var(--progress-border-radius); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/data/models.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/database.hpp" 5 | #include "../../src/data/models/listeninterface.hpp" 6 | 7 | using pt::Server::Database; 8 | using pt::Server::Data::Models::ListenInterface; 9 | 10 | class ListenInterfaceTests : public testing::Test 11 | { 12 | protected: 13 | void SetUp() override 14 | { 15 | sqlite3_open(":memory:", &db); 16 | EXPECT_TRUE(pt::Server::Database::Migrate(db)); 17 | } 18 | 19 | void TearDown() override 20 | { 21 | sqlite3_close(db); 22 | } 23 | 24 | sqlite3* db; 25 | }; 26 | 27 | TEST_F(ListenInterfaceTests, Create) 28 | { 29 | ListenInterface::Create(db, "127.0.0.1", 6881, true, true, true); 30 | auto all = ListenInterface::GetAll(db); 31 | 32 | EXPECT_EQ(all.back()->Host(), "127.0.0.1"); 33 | } 34 | 35 | TEST_F(ListenInterfaceTests, GetAll) 36 | { 37 | auto all = ListenInterface::GetAll(db); 38 | EXPECT_EQ(all[0]->Host(), "0.0.0.0"); 39 | EXPECT_EQ(all[1]->Host(), "[::]"); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/store/plugins/ws.js: -------------------------------------------------------------------------------- 1 | export default function createWebSocketPlugin() { 2 | return store => { 3 | const scheme = location.protocol === 'http:' 4 | ? 'ws:' 5 | : 'wss:'; 6 | 7 | const ws = new WebSocket(`${scheme}//${location.host}/api/ws`); 8 | 9 | ws.onmessage = (ev) => { 10 | const data = JSON.parse(ev.data); 11 | 12 | switch (data.type) { 13 | case 'init': 14 | store.commit('torrents/ADD_TORRENTS', data.torrents); 15 | break; 16 | 17 | case 'session.stats': 18 | store.commit('session/UPDATE_STATS', data.stats); 19 | break; 20 | 21 | case 'torrent.added': 22 | store.commit('torrents/ADD_TORRENT', data.torrent); 23 | break; 24 | 25 | case 'torrent.removed': 26 | store.commit('torrents/REMOVE_BY_ID', data.info_hash); 27 | break; 28 | 29 | case 'torrent.updated': 30 | store.commit('torrents/UPDATE_TORRENTS', data.torrents); 31 | break; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/data/models/listeninterface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace pt::Server::Data::Models 10 | { 11 | class ListenInterface 12 | { 13 | public: 14 | static std::shared_ptr Create( 15 | sqlite3* db, 16 | std::string const& host, 17 | int port, 18 | bool isLocal, 19 | bool isOutgoing, 20 | bool isSsl); 21 | 22 | static std::vector> GetAll(sqlite3* db); 23 | 24 | int Id() { return m_id; } 25 | std::string Host() { return m_host; } 26 | int Port() { return m_port; } 27 | bool IsLocal() { return m_isLocal; } 28 | bool IsOutgoing() { return m_isOutgoing; } 29 | bool IsSsl() { return m_isSsl; } 30 | 31 | private: 32 | int m_id; 33 | std::string m_host; 34 | int m_port; 35 | bool m_isLocal; 36 | bool m_isOutgoing; 37 | bool m_isSsl; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/json/proxy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../data/models/proxy.hpp" 6 | 7 | using nlohmann::json; 8 | 9 | namespace pt::Server::Data::Models 10 | { 11 | static void to_json(json& j, const std::shared_ptr& proxy) 12 | { 13 | j = { 14 | { "id", proxy->Id() }, 15 | { "name", proxy->Name() }, 16 | { "type", proxy->Type() }, 17 | { "hostname", proxy->Hostname() }, 18 | { "port", proxy->Port() }, 19 | { "username", proxy->Username() ? json(proxy->Username().value()) : json() }, 20 | { "password", proxy->Password() ? json("********") : json() }, 21 | { "proxy_hostnames", proxy->ProxyHostnames() }, 22 | { "proxy_peer_connections", proxy->ProxyPeerConnections() }, 23 | { "proxy_tracker_connections", proxy->ProxyTrackerConnections() }, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/scss/components/_tabs.scss: -------------------------------------------------------------------------------- 1 | // Tabs 2 | .tabs { 3 | .menu { 4 | display : flex; 5 | width : 100%; 6 | border-top : var(--separator); 7 | border-bottom: var(--separator); 8 | .item { 9 | padding : 0.75rem 1rem; 10 | background : transparent; 11 | color : var(--body-color); 12 | font-size : 0.85rem; 13 | border : 0; 14 | border-radius: 0; 15 | outline : 0; 16 | transition : all 0.35s ease; 17 | position : relative; 18 | cursor : pointer; 19 | &.active { 20 | &::after { 21 | content: ""; 22 | position: absolute; 23 | bottom: 4px; 24 | left: 1rem; 25 | right: 1rem; 26 | border-bottom: 2px solid var(--primary); 27 | animation: fade-in 0.5s forwards; 28 | } 29 | } 30 | &:hover { 31 | background: rgba(255, 255, 255, 0.025); 32 | } 33 | } 34 | } 35 | .content { 36 | .tab { 37 | &:not(.show) { 38 | display: none; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/http/httplistener.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace pt::Server::Http 9 | { 10 | class HttpRequestHandler; 11 | 12 | class HttpListener : public std::enable_shared_from_this 13 | { 14 | public: 15 | HttpListener( 16 | boost::asio::io_context& ioc, 17 | const boost::asio::ip::tcp::endpoint& endpoint, 18 | std::shared_ptr docroot); 19 | 20 | void AddHandler( 21 | const std::string& method, 22 | const std::string& path, 23 | const std::shared_ptr& handler); 24 | 25 | void Run(); 26 | 27 | private: 28 | void BeginAccept(); 29 | void EndAccept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket); 30 | 31 | boost::asio::io_context& m_io; 32 | boost::asio::ip::tcp::acceptor m_acceptor; 33 | std::shared_ptr m_docroot; 34 | std::shared_ptr, std::shared_ptr>> m_handlers; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/data/models/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.hpp" 2 | 3 | #include "../statement.hpp" 4 | 5 | using json = nlohmann::json; 6 | using pt::Server::Data::Models::Config; 7 | using pt::Server::Data::Statement; 8 | 9 | json Config::Get(sqlite3* db, const std::string_view &key) 10 | { 11 | json result = {}; 12 | 13 | Statement::ForEach( 14 | db, 15 | "SELECT value FROM config WHERE key = $1", 16 | [&result](const auto& row) 17 | { 18 | result = json::parse(row.GetStdString(0)); 19 | }, 20 | [&key](auto stmt) 21 | { 22 | sqlite3_bind_text(stmt, 1, key.data(), static_cast(key.size()), SQLITE_TRANSIENT); 23 | }); 24 | 25 | return result; 26 | } 27 | 28 | void Config::Set(sqlite3* db, const std::string_view& key, const json& value) 29 | { 30 | Statement::ForEach( 31 | db, 32 | "REPLACE INTO config (key, value) VALUES ($1, $2)", 33 | [](const auto &) 34 | {}, 35 | [&key, &value](auto stmt) 36 | { 37 | sqlite3_bind_text(stmt, 1, key.data(), -1, SQLITE_TRANSIENT); 38 | sqlite3_bind_text(stmt, 2, value.dump().c_str(), -1, SQLITE_TRANSIENT); 39 | }); 40 | } -------------------------------------------------------------------------------- /src/tsdb/influxdb.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "timeseriesdatabase.hpp" 4 | 5 | #include 6 | 7 | namespace pt::Server::TSDB 8 | { 9 | class InfluxDb : public TimeSeriesDatabase 10 | { 11 | public: 12 | InfluxDb( 13 | boost::asio::io_context& io, 14 | const std::string& host, 15 | int16_t port, 16 | const std::string& org, 17 | const std::string& bucket, 18 | const std::string& token); 19 | 20 | ~InfluxDb() override; 21 | 22 | void WriteMetrics( 23 | std::map const& metrics, 24 | std::chrono::milliseconds timestamp) override; 25 | 26 | private: 27 | class HttpClient; 28 | 29 | struct Metrics 30 | { 31 | std::map data; 32 | std::chrono::milliseconds timestamp; 33 | }; 34 | 35 | void Send(boost::system::error_code ec); 36 | 37 | boost::asio::io_context& m_io; 38 | boost::asio::deadline_timer m_timer; 39 | std::shared_ptr m_httpClient; 40 | std::vector m_current; 41 | std::vector m_inflight; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /client/src/scss/components/_button.scss: -------------------------------------------------------------------------------- 1 | button { 2 | display: inline-flex; 3 | align-items: center; 4 | cursor : pointer; 5 | outline : 0; 6 | border : 0; 7 | padding : 0.5rem 1rem; 8 | background: var(--primary); 9 | color : var(--white); 10 | transition: all 0.35s ease; 11 | &.icon { 12 | .bi { 13 | margin-top: -2px; 14 | margin-right: 0.5rem; 15 | } 16 | } 17 | &:hover { 18 | background: hsla(var(--primary-hsl), var(--alpha-hover)); 19 | } 20 | &:focus { 21 | box-shadow: 0 0 0 3px hsla(var(--primary-hsl), var(--alpha-outline)); 22 | } 23 | &.link { 24 | padding-left: 0.5rem; 25 | padding-right: 0.5rem; 26 | background: transparent; 27 | color: var(--body-color); 28 | &:hover { 29 | color: var(--body-color-highlight); 30 | text-decoration: underline; 31 | } 32 | } 33 | @each $name, $color in $colors { 34 | &.#{$name} { 35 | background: var(--#{$name}); 36 | color: color-yiq($color); 37 | &:hover { 38 | background: hsla(var(--#{$name}-hsl), var(--alpha-hover)); 39 | } 40 | &:focus { 41 | box-shadow: 0 0 0 3px hsla(var(--#{$name}-hsl), var(--alpha-outline)); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /client/src/scss/components/_table.scss: -------------------------------------------------------------------------------- 1 | // Table 2 | table { 3 | border-collapse: collapse; 4 | width: 100%; 5 | vertical-align: middle; 6 | tr { 7 | border-bottom: 1px solid var(--separator-color); 8 | } 9 | th, 10 | td { 11 | padding: 0.20rem 0.55rem; 12 | text-align: left; 13 | &:first-child { 14 | padding-left: 0.75rem; 15 | } 16 | &:last-child { 17 | padding-right: 0.75rem; 18 | } 19 | &.actions { 20 | text-align : right; 21 | justify-content: flex-end; 22 | } 23 | } 24 | th { 25 | padding-top : 0.50rem; 26 | padding-bottom: 0.50rem; 27 | text-transform: uppercase; 28 | font-size : 85%; 29 | font-weight : bolder; 30 | user-select : none; 31 | &.actions { 32 | visibility: hidden; 33 | } 34 | } 35 | tbody { 36 | tr { 37 | font-size: 0.70rem; 38 | } 39 | } 40 | } 41 | 42 | // 43 | .table-control { 44 | max-width: 600px; 45 | width: 100%; 46 | margin-bottom: 1rem; 47 | border: var(--separator); 48 | border-radius: .25rem; 49 | table tbody { 50 | tr:last-child { 51 | border: 0; 52 | } 53 | td.actions { 54 | button { 55 | padding: 0.35rem; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /vendor/vcpkg-overlays/ports/libtorrent/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libtorrent", 3 | "version": "2.0.4", 4 | "port-version": 1, 5 | "maintainers": "Arvid Norberg ", 6 | "description": "An efficient feature complete C++ BitTorrent implementation", 7 | "homepage": "https://libtorrent.org", 8 | "documentation": "https://libtorrent.org/reference.html", 9 | "supports": "!uwp & !(windows & arm)", 10 | "dependencies": [ 11 | "boost-asio", 12 | "boost-chrono", 13 | "boost-config", 14 | "boost-crc", 15 | "boost-date-time", 16 | "boost-iterator", 17 | "boost-logic", 18 | "boost-multi-index", 19 | "boost-multiprecision", 20 | "boost-pool", 21 | "boost-random", 22 | "boost-scope-exit", 23 | "boost-system", 24 | "boost-variant", 25 | "openssl" 26 | ], 27 | "features": { 28 | "deprfun": { 29 | "description": "build with deprecated functions enabled" 30 | }, 31 | "examples": { 32 | "description": "build the examples in the examples directory" 33 | }, 34 | "test": { 35 | "description": "build the libtorrent tests" 36 | }, 37 | "tools": { 38 | "description": "build the tools in the tools directory" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@picotorrent/server-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "bootstrap-icons": "^1.3.0", 13 | "core-js": "^3.6.5", 14 | "pretty-bytes": "^5.5.0", 15 | "vue": "^2.6.11", 16 | "vue-router": "^3.5.1", 17 | "vuex": "^3.6.2" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-eslint": "~4.5.0", 22 | "@vue/cli-service": "~4.5.0", 23 | "babel-eslint": "^10.1.0", 24 | "eslint": "^6.7.2", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "sass": "^1.32.6", 27 | "sass-loader": "^8.0.2", 28 | "vue-template-compiler": "^2.6.11" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "eslint:recommended" 38 | ], 39 | "parserOptions": { 40 | "parser": "babel-eslint" 41 | }, 42 | "rules": {} 43 | }, 44 | "browserslist": [ 45 | "> 1%", 46 | "last 2 versions", 47 | "not dead" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacesremove.cpp: -------------------------------------------------------------------------------- 1 | #include "listeninterfacesremove.hpp" 2 | 3 | #include 4 | 5 | #include "../data/datareader.hpp" 6 | #include "../sessionmanager.hpp" 7 | 8 | using json = nlohmann::json; 9 | using pt::Server::RPC::ListenInterfacesRemoveCommand; 10 | using pt::Server::SessionManager; 11 | 12 | ListenInterfacesRemoveCommand::ListenInterfacesRemoveCommand(sqlite3* db, std::shared_ptr session) 13 | : m_db(db), 14 | m_session(session) 15 | { 16 | } 17 | 18 | json ListenInterfacesRemoveCommand::Execute(const json& params) 19 | { 20 | if (params.is_array()) 21 | { 22 | sqlite3_stmt* stmt; 23 | sqlite3_prepare_v2( 24 | m_db, 25 | "DELETE FROM listen_interfaces WHERE id = $1", 26 | -1, 27 | &stmt, 28 | nullptr); 29 | 30 | for (auto const& p : params) 31 | { 32 | int id = p.get(); 33 | sqlite3_bind_int(stmt, 1, id); 34 | sqlite3_step(stmt); 35 | sqlite3_reset(stmt); 36 | } 37 | 38 | sqlite3_finalize(stmt); 39 | 40 | // Reload session settings to reload listen interfaces 41 | m_session->ReloadSettings(); 42 | } 43 | 44 | return Ok(true); 45 | } 46 | -------------------------------------------------------------------------------- /src/rpc/listeninterfacescreate.cpp: -------------------------------------------------------------------------------- 1 | #include "listeninterfacescreate.hpp" 2 | 3 | #include 4 | 5 | #include "../data/datareader.hpp" 6 | #include "../data/models/listeninterface.hpp" 7 | #include "../sessionmanager.hpp" 8 | 9 | using json = nlohmann::json; 10 | using pt::Server::Data::Models::ListenInterface; 11 | using pt::Server::RPC::ListenInterfacesCreateCommand; 12 | using pt::Server::SessionManager; 13 | 14 | ListenInterfacesCreateCommand::ListenInterfacesCreateCommand(sqlite3* db, std::shared_ptr session) 15 | : m_db(db), 16 | m_session(session) 17 | { 18 | } 19 | 20 | json ListenInterfacesCreateCommand::Execute(const json& params) 21 | { 22 | if (params.is_object()) 23 | { 24 | auto listenInterface = ListenInterface::Create( 25 | m_db, 26 | params["host"].get(), 27 | params["port"].get(), 28 | params["is_local"].get(), 29 | params["is_outgoing"].get(), 30 | params["is_ssl"].get()); 31 | 32 | // Reload session settings to reload listen interfaces 33 | m_session->ReloadSettings(); 34 | 35 | return Ok({ {"id", listenInterface->Id() } }); 36 | } 37 | 38 | return Error(1, "params not an object"); 39 | } 40 | -------------------------------------------------------------------------------- /src/rpc/settingspacklist.cpp: -------------------------------------------------------------------------------- 1 | #include "settingspacklist.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "../data/datareader.hpp" 7 | #include "../data/models/settingspack.hpp" 8 | #include "../sessionmanager.hpp" 9 | 10 | namespace lt = libtorrent; 11 | using json = nlohmann::json; 12 | using pt::Server::Data::SettingsPack; 13 | using pt::Server::RPC::SettingsPackList; 14 | using pt::Server::SessionManager; 15 | 16 | SettingsPackList::SettingsPackList(sqlite3* db) 17 | : m_db(db) 18 | { 19 | } 20 | 21 | json SettingsPackList::Execute(const json& params) 22 | { 23 | sqlite3_stmt* stmt; 24 | sqlite3_prepare_v2(m_db, "SELECT id,name,description FROM settings_pack ORDER BY name ASC", -1, &stmt, nullptr); 25 | 26 | json result; 27 | 28 | int res = SQLITE_ERROR; 29 | 30 | while ((res = sqlite3_step(stmt)) == SQLITE_ROW) 31 | { 32 | result.push_back( 33 | { 34 | { "id", sqlite3_column_int(stmt, 0) }, 35 | { "name", pt_sqlite3_column_std_string(stmt, 1).value() }, 36 | { "description", pt_sqlite3_column_std_string(stmt, 2).value() } 37 | }); 38 | } 39 | 40 | sqlite3_finalize(stmt); 41 | 42 | return Ok(result); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/pages/settings/Downloads.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 48 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | #include "log.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using pt::Server::Log; 11 | 12 | namespace expr = boost::log::expressions; 13 | 14 | void Log::Setup(boost::log::trivial::severity_level level) 15 | { 16 | auto psink = boost::log::add_console_log( 17 | std::cout, 18 | boost::log::keywords::format = expr::stream 19 | << expr::format_date_time("TimeStamp", "%Y-%m-%d %H:%M:%S.%f") << " " 20 | << "[" << expr::attr("Uptime") << "] " 21 | << "[" << expr::attr("ThreadID") << "] " 22 | << std::setw(7) << boost::log::trivial::severity << ": " 23 | << expr::message); 24 | 25 | psink->locked_backend()->auto_flush(true); 26 | 27 | boost::log::add_common_attributes(); 28 | boost::log::core::get()->add_global_attribute("Uptime", boost::log::attributes::timer()); 29 | boost::log::core::get()->set_filter(boost::log::trivial::severity >= level); 30 | } 31 | -------------------------------------------------------------------------------- /client/src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "root"; 3 | @import "utilities"; 4 | @import "components/all"; 5 | @import "pages/all"; 6 | 7 | *, ::after, ::before { 8 | box-sizing: border-box; 9 | } 10 | 11 | // HTML 12 | html { 13 | margin : 0; 14 | padding: 0; 15 | height : 100%; 16 | } 17 | 18 | // Body 19 | body { 20 | display : flex; 21 | flex-direction : column; 22 | height : 100%; 23 | margin : 0; 24 | font-family : var(--font-family-base); 25 | font-size : var(--font-size-base); 26 | font-weight : var(--font-weight-base); 27 | line-height : var(--line-height-base); 28 | color : var(--body-color); 29 | text-align : var(--body-text-align); 30 | background-color : var(--body-bg-color); 31 | background-image : if($ui-enable-bg, var(--body-bg-img), 'none'); 32 | background-repeat : no-repeat; 33 | background-size : auto 100%; 34 | background-position : center; 35 | -webkit-text-size-adjust : 100%; 36 | -webkit-tap-highlight-color: #000; 37 | } 38 | 39 | // Vue App 40 | #app { 41 | display : flex; 42 | flex-direction: column; 43 | height : 100%; 44 | flex : 1; 45 | } 46 | -------------------------------------------------------------------------------- /src/data/transaction.cpp: -------------------------------------------------------------------------------- 1 | #include "transaction.hpp" 2 | 3 | #include 4 | 5 | #include "sqliteexception.hpp" 6 | 7 | using pt::Server::Data::Transaction; 8 | 9 | Transaction::Transaction(sqlite3* db) 10 | : m_db(db), 11 | m_did_act(false) 12 | { 13 | int res = sqlite3_exec( 14 | m_db, 15 | "BEGIN TRANSACTION;", 16 | nullptr, 17 | nullptr, 18 | nullptr); 19 | 20 | if (res != SQLITE_OK) 21 | { 22 | throw SQLiteException(); 23 | } 24 | } 25 | 26 | Transaction::~Transaction() 27 | { 28 | if (!m_did_act) 29 | { 30 | BOOST_LOG_TRIVIAL(error) << "Transaction left in dangling state"; 31 | } 32 | } 33 | 34 | void Transaction::Commit() 35 | { 36 | int res = sqlite3_exec( 37 | m_db, 38 | "COMMIT TRANSACTION;", 39 | nullptr, 40 | nullptr, 41 | nullptr); 42 | 43 | if (res != SQLITE_OK) 44 | { 45 | throw SQLiteException(); 46 | } 47 | 48 | m_did_act = true; 49 | } 50 | 51 | void Transaction::Rollback() 52 | { 53 | int res = sqlite3_exec( 54 | m_db, 55 | "ROLLBACK TRANSACTION;", 56 | nullptr, 57 | nullptr, 58 | nullptr); 59 | 60 | if (res != SQLITE_OK) 61 | { 62 | throw SQLiteException(); 63 | } 64 | 65 | m_did_act = true; 66 | } 67 | -------------------------------------------------------------------------------- /src/tsdb/prometheus.cpp: -------------------------------------------------------------------------------- 1 | #include "prometheus.hpp" 2 | 3 | #include 4 | 5 | using pt::Server::TSDB::Prometheus; 6 | 7 | Prometheus::~Prometheus() = default; 8 | 9 | void Prometheus::Execute(std::shared_ptr context) 10 | { 11 | namespace http = boost::beast::http; 12 | 13 | auto const response = [context](std::string const& body) 14 | { 15 | http::response res{http::status::ok, context->Request().version()}; 16 | res.set(http::field::server, BOOST_BEAST_VERSION_STRING); 17 | res.set(http::field::content_type, "text/plain; version=0.0.4"); 18 | res.keep_alive(context->Request().keep_alive()); 19 | res.body() = body; 20 | res.prepare_payload(); 21 | return res; 22 | }; 23 | 24 | if (m_metrics.has_value() && m_timestamp.has_value()) 25 | { 26 | std::stringstream ss; 27 | 28 | for (auto const& metric : m_metrics.value()) 29 | { 30 | ss << std::regex_replace(metric.first, std::regex("\\."), "_") << " " << metric.second << "\n"; 31 | } 32 | 33 | context->Write(response(ss.str())); 34 | } 35 | } 36 | 37 | void Prometheus::WriteMetrics(const std::map &metrics, std::chrono::milliseconds timestamp) 38 | { 39 | m_metrics = metrics; 40 | m_timestamp = timestamp; 41 | } 42 | -------------------------------------------------------------------------------- /src/rpc/sessionaddmagnetlink.cpp: -------------------------------------------------------------------------------- 1 | #include "sessionaddmagnetlink.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../json/infohash.hpp" 8 | #include "../sessionmanager.hpp" 9 | 10 | namespace lt = libtorrent; 11 | using json = nlohmann::json; 12 | using pt::Server::SessionManager; 13 | using pt::Server::RPC::SessionAddMagnetLinkCommand; 14 | 15 | SessionAddMagnetLinkCommand::SessionAddMagnetLinkCommand(std::shared_ptr session) 16 | : m_session(session) 17 | { 18 | } 19 | 20 | json SessionAddMagnetLinkCommand::Execute(const json& j) 21 | { 22 | if (!j.contains("magnet_uri")) 23 | { 24 | return Error(1, "Missing 'magnet_uri' field"); 25 | } 26 | 27 | if (!j.contains("save_path")) 28 | { 29 | return Error(1, "Missing 'save_path' field"); 30 | } 31 | 32 | lt::error_code ec; 33 | lt::add_torrent_params p = lt::parse_magnet_uri( 34 | j["magnet_uri"].get(), 35 | ec); 36 | 37 | if (ec) 38 | { 39 | BOOST_LOG_TRIVIAL(error) << "Failed to parse magnet uri: " << ec.message(); 40 | return Error(1, ec.message()); 41 | } 42 | 43 | p.save_path = j["save_path"].get(); 44 | 45 | return Ok({ 46 | { "info_hash", m_session->AddTorrent(p) } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/data/models/settingspack.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace pt::Server::Data 11 | { 12 | class SettingsPack 13 | { 14 | public: 15 | static std::shared_ptr Create( 16 | sqlite3* db, 17 | std::string_view const& name, 18 | std::string_view const& desc, 19 | libtorrent::settings_pack settings = libtorrent::default_settings()); 20 | 21 | static std::shared_ptr GetById(sqlite3* db, int id); 22 | 23 | static std::unordered_set& Names(); 24 | 25 | int Id() { return m_id; } 26 | std::string& Name() { return m_name; } 27 | std::string& Description() { return m_desc; } 28 | libtorrent::settings_pack& Settings() { return m_settings; } 29 | 30 | private: 31 | SettingsPack( 32 | int id, 33 | std::string_view const& name, 34 | std::string_view const& desc, 35 | libtorrent::settings_pack const& settings) 36 | : m_id(id), 37 | m_name(name), 38 | m_desc(desc), 39 | m_settings(settings) 40 | { 41 | } 42 | 43 | int m_id; 44 | std::string m_name; 45 | std::string m_desc; 46 | libtorrent::settings_pack m_settings; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /client/src/pages/settings/Layout.vue: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /client/src/pages/settings/Profiles.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: picotorrent/server 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | submodules: true 26 | 27 | - name: Install GitVersion 28 | uses: gittools/actions/gitversion/setup@v0.9.11 29 | with: 30 | versionSpec: '5.x' 31 | 32 | - name: Calculate version 33 | id: gitversion 34 | uses: gittools/actions/gitversion/execute@v0.9.11 35 | with: 36 | useConfigFile: true 37 | 38 | - name: Set up QEMU 39 | uses: docker/setup-qemu-action@v1 40 | 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v1 43 | 44 | - name: Login to the container registry 45 | uses: docker/login-action@v1 46 | with: 47 | registry: ${{ env.REGISTRY }} 48 | username: ${{ github.actor }} 49 | password: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Build and push 52 | uses: docker/build-push-action@v2 53 | with: 54 | context: . 55 | push: true 56 | tags: ghcr.io/picotorrent/server:${{ github.sha }},ghcr.io/picotorrent/server:${{ steps.gitversion.outputs.semVer }} 57 | -------------------------------------------------------------------------------- /src/rpc/profilesupdate.cpp: -------------------------------------------------------------------------------- 1 | #include "profilesupdate.hpp" 2 | 3 | #include 4 | 5 | #include "../data/datareader.hpp" 6 | #include "../data/models/profile.hpp" 7 | #include "../sessionmanager.hpp" 8 | 9 | using json = nlohmann::json; 10 | using pt::Server::Data::Models::Profile; 11 | using pt::Server::RPC::ProfilesUpdateCommand; 12 | using pt::Server::SessionManager; 13 | 14 | ProfilesUpdateCommand::ProfilesUpdateCommand(sqlite3* db, std::shared_ptr session) 15 | : m_db(db), 16 | m_session(session) 17 | { 18 | } 19 | 20 | json ProfilesUpdateCommand::Execute(const json& params) 21 | { 22 | if (!params.is_object()) 23 | { 24 | return Error(1, "params not an object"); 25 | } 26 | 27 | if (!params.contains("id")) 28 | { 29 | return Error(1, "missing 'id' key"); 30 | } 31 | 32 | auto profile = Profile::GetById(m_db, params["id"].get()); 33 | 34 | if (profile == nullptr) 35 | { 36 | return Error(1, "No profile found with given id"); 37 | } 38 | 39 | if (params.contains("proxy_id")) 40 | { 41 | if (params["proxy_id"].is_null()) 42 | { 43 | profile->ProxyId(std::nullopt); 44 | } 45 | else 46 | { 47 | profile->ProxyId(params["proxy_id"].get()); 48 | } 49 | } 50 | 51 | Profile::Update( 52 | m_db, 53 | profile); 54 | 55 | if (profile->IsActive()) 56 | { 57 | m_session->ReloadSettings(); 58 | } 59 | 60 | return Ok(true); 61 | } 62 | -------------------------------------------------------------------------------- /src/data/models/profile.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "../statement.hpp" 11 | 12 | namespace pt::Server::Data::Models 13 | { 14 | class Profile 15 | { 16 | public: 17 | static std::shared_ptr GetActive(sqlite3* db); 18 | static std::shared_ptr GetById(sqlite3* db, int id); 19 | static std::vector> GetAll(sqlite3* db); 20 | static void Update(sqlite3* db, std::shared_ptr const& profile); 21 | 22 | int Id() { return m_id; } 23 | std::string& Name() { return m_name; } 24 | bool IsActive() { return m_isActive; } 25 | std::string Description() { return m_desc; } 26 | 27 | std::optional ProxyId() { return m_proxyId; } 28 | void ProxyId(std::optional id) { m_proxyId = id; } 29 | 30 | std::optional ProxyName() { return m_proxyName; } 31 | int SettingsPackId() { return m_settingsPackId; } 32 | std::string& SettingsPackName() { return m_settingsPackName; } 33 | 34 | private: 35 | static std::shared_ptr Construct(Statement::Row const& row); 36 | 37 | int m_id; 38 | std::string m_name; 39 | std::string m_desc; 40 | bool m_isActive; 41 | std::optional m_proxyId; 42 | std::optional m_proxyName; 43 | int m_settingsPackId; 44 | std::string m_settingsPackName; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/store/modules/torrents.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const state = { 4 | torrents: {} 5 | }; 6 | 7 | const getters = { 8 | all: state => state.torrents, 9 | byId: state => id => { 10 | return state.torrents[id] 11 | }, 12 | count: state => Object.keys(state.torrents).length, 13 | 14 | dl_total: state => Object.values(state.torrents) 15 | .map(t => t.dl) 16 | .reduce((a, b) => a + b, 0), 17 | 18 | ul_total: state => Object.values(state.torrents) 19 | .map(t => t.ul) 20 | .reduce((a, b) => a + b, 0) 21 | } 22 | 23 | const actions = { 24 | addTorrent({ commit }, infoHash, torrent) { 25 | commit('ADD_TORRENT', infoHash, torrent); 26 | }, 27 | 28 | addMany({ commit }, torrents) { 29 | commit('ADD_TORRENTS', torrents); 30 | }, 31 | 32 | updateMany({ commit }, torrents) { 33 | commit('UPDATE_TORRENTS', torrents); 34 | } 35 | } 36 | 37 | const mutations = { 38 | ADD_TORRENT(state, torrent) { 39 | Vue.set(state.torrents, torrent.info_hash, torrent); 40 | }, 41 | 42 | ADD_TORRENTS(state, torrents) { 43 | for (const infoHash in torrents) { 44 | Vue.set(state.torrents, infoHash, torrents[infoHash]); 45 | } 46 | }, 47 | 48 | REMOVE_BY_ID(state, infoHash) { 49 | Vue.delete(state.torrents, infoHash); 50 | }, 51 | 52 | UPDATE_TORRENTS(state, torrents) { 53 | for (const infoHash in torrents) { 54 | Vue.set(state.torrents, infoHash, torrents[infoHash]); 55 | } 56 | } 57 | } 58 | 59 | export default { 60 | namespaced: true, 61 | state, 62 | getters, 63 | actions, 64 | mutations 65 | } 66 | -------------------------------------------------------------------------------- /src/rpc/torrentspause.cpp: -------------------------------------------------------------------------------- 1 | #include "torrentspause.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../json/infohash.hpp" 11 | #include "../sessionmanager.hpp" 12 | 13 | namespace lt = libtorrent; 14 | 15 | using json = nlohmann::json; 16 | using pt::Server::RPC::TorrentsPauseCommand; 17 | using pt::Server::ITorrentHandleFinder; 18 | 19 | TorrentsPauseCommand::TorrentsPauseCommand(std::shared_ptr finder) 20 | : m_finder(std::move(finder)) 21 | { 22 | } 23 | 24 | json TorrentsPauseCommand::Execute(const json& j) 25 | { 26 | auto pause = [this](const lt::info_hash_t& hash) 27 | { 28 | auto handle = m_finder->Find(hash); 29 | 30 | if (handle == nullptr) 31 | { 32 | BOOST_LOG_TRIVIAL(warning) << "Failed to find torrent with info hash " << hash; 33 | return; 34 | } 35 | 36 | if (!handle->IsValid()) 37 | { 38 | BOOST_LOG_TRIVIAL(warning) << "Found torrent handle which is not valid"; 39 | return; 40 | } 41 | 42 | handle->Pause(); 43 | }; 44 | 45 | if (j.is_array()) 46 | { 47 | for (lt::info_hash_t const& hash : j.get>()) 48 | { 49 | pause(hash); 50 | } 51 | 52 | return Ok(); 53 | } 54 | else if (j.is_string()) 55 | { 56 | pause(j.get()); 57 | return Ok(); 58 | } 59 | 60 | return Error(1, "'params' not a string or array of strings"); 61 | } 62 | -------------------------------------------------------------------------------- /src/rpc/torrentsresume.cpp: -------------------------------------------------------------------------------- 1 | #include "torrentsresume.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../json/infohash.hpp" 11 | #include "../sessionmanager.hpp" 12 | 13 | namespace lt = libtorrent; 14 | 15 | using json = nlohmann::json; 16 | using pt::Server::RPC::TorrentsResumeCommand; 17 | using pt::Server::ITorrentHandleFinder; 18 | 19 | TorrentsResumeCommand::TorrentsResumeCommand(std::shared_ptr finder) 20 | : m_finder(std::move(finder)) 21 | { 22 | } 23 | 24 | json TorrentsResumeCommand::Execute(const json& j) 25 | { 26 | auto resume = [this](const lt::info_hash_t& hash) 27 | { 28 | auto handle = m_finder->Find(hash); 29 | 30 | if (handle == nullptr) 31 | { 32 | BOOST_LOG_TRIVIAL(warning) << "Failed to find torrent with info hash " << hash; 33 | return; 34 | } 35 | 36 | if (!handle->IsValid()) 37 | { 38 | BOOST_LOG_TRIVIAL(warning) << "Found torrent handle which is not valid"; 39 | return; 40 | } 41 | 42 | handle->Resume(); 43 | }; 44 | 45 | if (j.is_array()) 46 | { 47 | for (lt::info_hash_t const& hash : j.get>()) 48 | { 49 | resume(hash); 50 | } 51 | 52 | return Ok(); 53 | } 54 | else if (j.is_string()) 55 | { 56 | resume(j.get()); 57 | return Ok(); 58 | } 59 | 60 | return Error(1, "'params' not a string or array of strings"); 61 | } 62 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import VueRouter from 'vue-router'; 2 | 3 | import About from '@/pages/About'; 4 | import AddMagnetLink from '@/pages/AddMagnetLink'; 5 | import AddTorrent from '@/pages/AddTorrent'; 6 | import Home from '@/pages/Home'; 7 | import SettingsCommon from '@/pages/settings/Common'; 8 | 9 | import SettingsConnectionLayout from '@/pages/settings/connection/Layout'; 10 | import SettingsConnectionIndex from '@/pages/settings/connection/Index'; 11 | import SettingsConnectionAddListenIntercace from '@/pages/settings/connection/AddListenInterface'; 12 | 13 | import SettingsDownloads from '@/pages/settings/Downloads'; 14 | import SettingsLayout from '@/pages/settings/Layout'; 15 | import SettingsProfiles from '@/pages/settings/Profiles'; 16 | import SettingsProxy from '@/pages/settings/Proxy'; 17 | 18 | export default new VueRouter({ 19 | routes: [ 20 | { path: '/', component: Home }, 21 | { path: '/about', component: About }, 22 | { path: '/add-magnet-link', component: AddMagnetLink }, 23 | { path: '/add-torrent', component: AddTorrent }, 24 | { 25 | path: '/settings', 26 | component: SettingsLayout, 27 | redirect: '/settings/common', 28 | children: [ 29 | { path: 'common', component: SettingsCommon }, 30 | { 31 | path: 'connection', 32 | component: SettingsConnectionLayout, 33 | children: [ 34 | { path: '', component: SettingsConnectionIndex }, 35 | { path: 'add-listen-interface', component: SettingsConnectionAddListenIntercace } 36 | ] 37 | }, 38 | { path: 'downloads', component: SettingsDownloads }, 39 | { path: 'profiles', component: SettingsProfiles }, 40 | { path: 'proxy', component: SettingsProxy } 41 | ] 42 | } 43 | ] 44 | }); 45 | -------------------------------------------------------------------------------- /src/data/statement.cpp: -------------------------------------------------------------------------------- 1 | #include "statement.hpp" 2 | 3 | #include 4 | 5 | #include "datareader.hpp" 6 | #include "sqliteexception.hpp" 7 | 8 | using pt::Server::Data::SQLiteException; 9 | using pt::Server::Data::Statement; 10 | 11 | Statement::Row::Row(sqlite3_stmt* stmt) 12 | : m_stmt(stmt) 13 | { 14 | } 15 | 16 | std::vector Statement::Row::GetBlob(int col) const 17 | { 18 | int len = sqlite3_column_bytes(m_stmt, col); 19 | const char* buf = static_cast(sqlite3_column_blob(m_stmt, col)); 20 | return std::vector(buf, buf + len); 21 | } 22 | 23 | bool Statement::Row::GetBool(int col) const 24 | { 25 | return sqlite3_column_int(m_stmt, col) == 1 ? true : false; 26 | } 27 | 28 | int Statement::Row::GetInt32(int col) const 29 | { 30 | return sqlite3_column_int(m_stmt, col); 31 | } 32 | 33 | std::string Statement::Row::GetStdString(int col) const 34 | { 35 | return pt_sqlite3_column_std_string(m_stmt, col).value(); 36 | } 37 | 38 | bool Statement::Row::IsNull(int col) const 39 | { 40 | return sqlite3_column_type(m_stmt, col) == SQLITE_NULL; 41 | } 42 | 43 | void Statement::ForEach( 44 | sqlite3* db, 45 | std::string const& sql, 46 | std::function const& cb, 47 | std::function bind) 48 | { 49 | sqlite3_stmt* stmt; 50 | int res = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); 51 | 52 | if (res != SQLITE_OK) 53 | { 54 | BOOST_LOG_TRIVIAL(error) << "Failed to prepare statement: " << sqlite3_errmsg(db); 55 | throw SQLiteException(); 56 | } 57 | 58 | if (bind) 59 | { 60 | bind(stmt); 61 | } 62 | 63 | while ((res = sqlite3_step(stmt)) == SQLITE_ROW) 64 | { 65 | Row r(stmt); 66 | cb(r); 67 | } 68 | 69 | sqlite3_finalize(stmt); 70 | } 71 | -------------------------------------------------------------------------------- /tests/rpc/configset.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/database.hpp" 5 | #include "../../src/data/statement.hpp" 6 | #include "../../src/rpc/configset.hpp" 7 | 8 | using json = nlohmann::json; 9 | using pt::Server::Data::Statement; 10 | using pt::Server::RPC::ConfigSetCommand; 11 | 12 | class ConfigSetCommandTests : public ::testing::Test 13 | { 14 | protected: 15 | void SetUp() override 16 | { 17 | sqlite3_open(":memory:", &db); 18 | EXPECT_TRUE(pt::Server::Database::Migrate(db)); 19 | 20 | cmd = std::make_unique(db); 21 | } 22 | 23 | void TearDown() override 24 | { 25 | sqlite3_close(db); 26 | } 27 | 28 | json GetValue(const std::string_view& key) 29 | { 30 | json v; 31 | Statement::ForEach( 32 | db, 33 | "SELECT value FROM config WHERE key = $1", 34 | [&v](const auto& row) 35 | { 36 | v = json::parse(row.GetStdString(0)); 37 | }, 38 | [&key](auto stmt) 39 | { 40 | sqlite3_bind_text(stmt, 1, key.data(), static_cast(key.size()), SQLITE_TRANSIENT); 41 | }); 42 | return v; 43 | } 44 | 45 | sqlite3* db; 46 | std::unique_ptr cmd; 47 | }; 48 | 49 | TEST_F(ConfigSetCommandTests, Execute_WithSingleKey) 50 | { 51 | EXPECT_EQ( 52 | cmd->Execute({{ "foo", "bar" }}), 53 | R"({"result": null})"_json); 54 | 55 | EXPECT_EQ(GetValue("foo"), "bar"); 56 | } 57 | 58 | TEST_F(ConfigSetCommandTests, Execute_WithMultipleKeys) 59 | { 60 | EXPECT_EQ( 61 | cmd->Execute({{ "foo", "bar" }, { "baz", 123 }}), 62 | R"({"result": null})"_json); 63 | 64 | EXPECT_EQ(GetValue("foo"), "bar"); 65 | EXPECT_EQ(GetValue("baz"), 123); 66 | } 67 | -------------------------------------------------------------------------------- /src/http/mimetype.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pt::Server::Http 6 | { 7 | static boost::beast::string_view MimeType(boost::beast::string_view path) 8 | { 9 | namespace beast = boost::beast; 10 | using beast::iequals; 11 | 12 | auto const ext = [&path] 13 | { 14 | auto const pos = path.rfind("."); 15 | if(pos == beast::string_view::npos) 16 | return beast::string_view{}; 17 | return path.substr(pos); 18 | }(); 19 | 20 | if(iequals(ext, ".htm")) return "text/html"; 21 | if(iequals(ext, ".html")) return "text/html"; 22 | if(iequals(ext, ".php")) return "text/html"; 23 | if(iequals(ext, ".css")) return "text/css"; 24 | if(iequals(ext, ".txt")) return "text/plain"; 25 | if(iequals(ext, ".js")) return "application/javascript"; 26 | if(iequals(ext, ".json")) return "application/json"; 27 | if(iequals(ext, ".xml")) return "application/xml"; 28 | if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; 29 | if(iequals(ext, ".flv")) return "video/x-flv"; 30 | if(iequals(ext, ".png")) return "image/png"; 31 | if(iequals(ext, ".jpe")) return "image/jpeg"; 32 | if(iequals(ext, ".jpeg")) return "image/jpeg"; 33 | if(iequals(ext, ".jpg")) return "image/jpeg"; 34 | if(iequals(ext, ".gif")) return "image/gif"; 35 | if(iequals(ext, ".bmp")) return "image/bmp"; 36 | if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; 37 | if(iequals(ext, ".tiff")) return "image/tiff"; 38 | if(iequals(ext, ".tif")) return "image/tiff"; 39 | if(iequals(ext, ".svg")) return "image/svg+xml"; 40 | if(iequals(ext, ".svgz")) return "image/svg+xml"; 41 | 42 | return "application/text"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/src/pages/AddMagnetLink.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 64 | -------------------------------------------------------------------------------- /client/src/pages/settings/connection/AddListenInterface.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 68 | -------------------------------------------------------------------------------- /client/src/pages/AddTorrent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 62 | -------------------------------------------------------------------------------- /src/options.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace pt::Server 11 | { 12 | struct Options 13 | { 14 | static std::shared_ptr Load(int argc, char* argv[]); 15 | 16 | std::filesystem::path DatabaseFilePath() { return m_databaseFilePath; } 17 | boost::log::trivial::severity_level LogLevel() { return m_logLevel; } 18 | std::string Host() { return m_host; } 19 | uint16_t Port() { return m_port; } 20 | std::shared_ptr WebRoot() { return m_webRoot; } 21 | 22 | bool PrometheusExporterEnabled() { return m_prometheusEnabled; } 23 | 24 | // InfluxDb options 25 | std::optional InfluxDbHost() { return m_influxHost; } 26 | std::optional InfluxDbPort() { return m_influxPort; } 27 | std::optional InfluxDbBucket() { return m_influxBucket; } 28 | std::optional InfluxDbOrganization() { return m_influxOrg; } 29 | std::optional InfluxDbToken() { return m_influxToken; } 30 | 31 | bool IsValidInfluxDbConfig() 32 | { 33 | return m_influxHost.has_value() 34 | && m_influxPort.has_value() 35 | && m_influxBucket.has_value() 36 | && m_influxOrg.has_value() 37 | && m_influxToken.has_value(); 38 | } 39 | 40 | private: 41 | std::filesystem::path m_databaseFilePath; 42 | boost::log::trivial::severity_level m_logLevel; 43 | std::string m_host; 44 | uint16_t m_port; 45 | std::shared_ptr m_webRoot; 46 | 47 | bool m_prometheusEnabled; 48 | 49 | std::optional m_influxHost; 50 | std::optional m_influxPort; 51 | std::optional m_influxBucket; 52 | std::optional m_influxOrg; 53 | std::optional m_influxToken; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/http/httplistener.cpp: -------------------------------------------------------------------------------- 1 | #include "httplistener.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "httpsession.hpp" 7 | 8 | using pt::Server::Http::HttpListener; 9 | using pt::Server::Http::HttpRequestHandler; 10 | 11 | HttpListener::HttpListener( 12 | boost::asio::io_context& ioc, 13 | const boost::asio::ip::tcp::endpoint& endpoint, 14 | std::shared_ptr docroot) 15 | : m_io(ioc) 16 | , m_acceptor(boost::asio::make_strand(ioc)) 17 | , m_docroot(std::move(docroot)) 18 | , m_handlers(std::make_shared, std::shared_ptr>>()) 19 | { 20 | m_acceptor.open(endpoint.protocol()); 21 | m_acceptor.set_option(boost::asio::socket_base::reuse_address(true)); 22 | m_acceptor.bind(endpoint); 23 | m_acceptor.listen(boost::asio::socket_base::max_listen_connections); 24 | } 25 | 26 | void HttpListener::AddHandler( 27 | const std::string& method, 28 | const std::string& path, 29 | const std::shared_ptr& handler) 30 | { 31 | m_handlers->insert({ { method, path }, handler }); 32 | } 33 | 34 | void HttpListener::Run() 35 | { 36 | boost::asio::dispatch( 37 | m_acceptor.get_executor(), 38 | boost::beast::bind_front_handler( 39 | &HttpListener::BeginAccept, 40 | shared_from_this())); 41 | } 42 | 43 | void HttpListener::BeginAccept() 44 | { 45 | m_acceptor.async_accept( 46 | boost::asio::make_strand(m_io), 47 | boost::beast::bind_front_handler( 48 | &HttpListener::EndAccept, 49 | shared_from_this())); 50 | } 51 | 52 | void HttpListener::EndAccept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket) 53 | { 54 | if(ec) 55 | { 56 | BOOST_LOG_TRIVIAL(error) << "Error when accepting HTTP client: " << ec.message(); 57 | } 58 | else 59 | { 60 | std::make_shared( 61 | std::move(socket), 62 | m_handlers, 63 | m_docroot)->Run(); 64 | } 65 | 66 | BeginAccept(); 67 | } 68 | -------------------------------------------------------------------------------- /src/data/models/proxy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "../statement.hpp" 12 | 13 | namespace pt::Server::Data::Models 14 | { 15 | class Proxy 16 | { 17 | public: 18 | static std::shared_ptr Create( 19 | sqlite3* db, 20 | std::string const& name, 21 | libtorrent::settings_pack::proxy_type_t type, 22 | std::string const& hostname, 23 | int port, 24 | std::optional const& username, 25 | std::optional const& password, 26 | bool proxyHostnames, 27 | bool proxyPeerConnections, 28 | bool proxyTrackerConnections); 29 | 30 | static std::vector> GetAll(sqlite3* db); 31 | static std::shared_ptr GetById(sqlite3* db, int proxyId); 32 | 33 | int Id() { return m_id; } 34 | std::string Name() { return m_name; } 35 | libtorrent::settings_pack::proxy_type_t Type() { return m_type; } 36 | std::string Hostname() { return m_hostname; } 37 | int Port() { return m_port; } 38 | std::optional Username() { return m_username; } 39 | std::optional Password() { return m_password; } 40 | bool ProxyHostnames() { return m_proxyHostnames; } 41 | bool ProxyPeerConnections() { return m_proxyPeerConnections; } 42 | bool ProxyTrackerConnections() { return m_proxyTrackerConnections; } 43 | 44 | private: 45 | static std::shared_ptr Construct(Statement::Row const& row); 46 | 47 | int m_id; 48 | std::string m_name; 49 | libtorrent::settings_pack::proxy_type_t m_type; 50 | std::string m_hostname; 51 | int m_port; 52 | std::optional m_username; 53 | std::optional m_password; 54 | bool m_proxyHostnames; 55 | bool m_proxyPeerConnections; 56 | bool m_proxyTrackerConnections; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /client/src/pages/About.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/models/listeninterface.cpp: -------------------------------------------------------------------------------- 1 | #include "listeninterface.hpp" 2 | 3 | #include "../datareader.hpp" 4 | #include "../sqliteexception.hpp" 5 | #include "../statement.hpp" 6 | 7 | using pt::Server::Data::Models::ListenInterface; 8 | using pt::Server::Data::SQLiteException; 9 | using pt::Server::Data::Statement; 10 | 11 | std::shared_ptr ListenInterface::Create(sqlite3* db, std::string const& host, int port, bool isLocal, bool isOutgoing, bool isSsl) 12 | { 13 | Statement::ForEach( 14 | db, 15 | "INSERT INTO listen_interfaces (host, port, is_local, is_outgoing, is_ssl) VALUES ($1, $2, $3, $4, $5);", 16 | [](auto const&){}, 17 | [&](sqlite3_stmt* stmt) 18 | { 19 | sqlite3_bind_text(stmt, 1, host.c_str(), static_cast(host.size()), SQLITE_TRANSIENT); 20 | sqlite3_bind_int(stmt, 2, port); 21 | sqlite3_bind_int(stmt, 3, isLocal ? 1 : 0); 22 | sqlite3_bind_int(stmt, 4, isOutgoing ? 1 : 0); 23 | sqlite3_bind_int(stmt, 5, isSsl ? 1 : 0); 24 | }); 25 | 26 | auto res = std::make_shared(); 27 | res->m_id = static_cast(sqlite3_last_insert_rowid(db)); 28 | res->m_host = host; 29 | res->m_port = port; 30 | res->m_isLocal = isLocal; 31 | res->m_isOutgoing = isOutgoing; 32 | res->m_isSsl = isSsl; 33 | 34 | return std::move(res); 35 | } 36 | 37 | std::vector> ListenInterface::GetAll(sqlite3* db) 38 | { 39 | std::vector> result; 40 | 41 | Statement::ForEach( 42 | db, 43 | "SELECT id,host,port,is_local,is_outgoing,is_ssl FROM listen_interfaces ORDER BY id ASC", 44 | [&](Statement::Row const& row) 45 | { 46 | auto listenInterface = std::make_shared(); 47 | listenInterface->m_id = row.GetInt32(0); 48 | listenInterface->m_host = row.GetStdString(1); 49 | listenInterface->m_port = row.GetInt32(2); 50 | listenInterface->m_isLocal = row.GetBool(3); 51 | listenInterface->m_isOutgoing = row.GetBool(4); 52 | listenInterface->m_isSsl = row.GetBool(5); 53 | result.push_back(listenInterface); 54 | }); 55 | 56 | return result; 57 | } 58 | -------------------------------------------------------------------------------- /client/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | -------------------------------------------------------------------------------- /src/rpc/proxycreate.cpp: -------------------------------------------------------------------------------- 1 | #include "proxycreate.hpp" 2 | 3 | #include 4 | 5 | #include "../data/datareader.hpp" 6 | #include "../data/models/proxy.hpp" 7 | #include "../sessionmanager.hpp" 8 | 9 | namespace lt = libtorrent; 10 | using json = nlohmann::json; 11 | using pt::Server::Data::Models::Proxy; 12 | using pt::Server::RPC::ProxyCreateCommand; 13 | using pt::Server::SessionManager; 14 | 15 | ProxyCreateCommand::ProxyCreateCommand(sqlite3* db, std::shared_ptr session) 16 | : m_db(db), 17 | m_session(session) 18 | { 19 | } 20 | 21 | json ProxyCreateCommand::Execute(const json& params) 22 | { 23 | if (params.is_object()) 24 | { 25 | std::string name = params["name"].get(); 26 | int numType = params["type"].get(); 27 | std::string hostname = params["hostname"].get(); 28 | int port = params["port"].get(); 29 | bool proxyHostnames = params["proxy_hostnames"].get(); 30 | bool proxyPeerConnections = params["proxy_peer_connections"].get(); 31 | bool proxyTrackerConnections = params["proxy_tracker_connections"].get(); 32 | 33 | std::optional username = std::nullopt; 34 | if (params.contains("username") 35 | && !params["username"].is_null()) 36 | { 37 | username = params["username"].get(); 38 | } 39 | 40 | std::optional password = std::nullopt; 41 | if (params.contains("password") 42 | && !params["username"].is_null()) 43 | { 44 | password = params["password"].get(); 45 | } 46 | 47 | if (numType < 1 || numType > 6) 48 | { 49 | return Error(1, "invalid proxy type - should be between 1-6"); 50 | } 51 | 52 | auto type = static_cast(numType); 53 | 54 | auto proxy = Proxy::Create( 55 | m_db, 56 | name, 57 | type, 58 | hostname, 59 | port, 60 | username, 61 | password, 62 | proxyHostnames, 63 | proxyPeerConnections, 64 | proxyTrackerConnections); 65 | 66 | return Ok({ {"id", proxy->Id() } }); 67 | } 68 | 69 | return Error(1, "params not an object"); 70 | } 71 | -------------------------------------------------------------------------------- /vendor/vcpkg-overlays/ports/libtorrent/portfile.cmake: -------------------------------------------------------------------------------- 1 | vcpkg_fail_port_install(ON_TARGET "uwp") 2 | 3 | if(VCPKG_TARGET_IS_WINDOWS) 4 | if(VCPKG_CRT_LINKAGE STREQUAL "static") 5 | set(_static_runtime ON) 6 | endif() 7 | endif() 8 | 9 | vcpkg_check_features( 10 | OUT_FEATURE_OPTIONS FEATURE_OPTIONS 11 | FEATURES 12 | deprfun deprecated-functions 13 | examples build_examples 14 | test build_tests 15 | tools build_tools 16 | ) 17 | 18 | vcpkg_from_github( 19 | OUT_SOURCE_PATH SOURCE_PATH 20 | REPO arvidn/libtorrent 21 | REF 3c20db6a1e0ed2d57401aa7fdf82c9c5a3cdc84d # v2.0.5 22 | SHA512 750baab503e36bda612b01c5cfee974eb98c65bdfc81831d814adcac87c8502c3550b0f5cc76ddd567ba2a38caa4cbb433da319e6a51c1918b6cba9a9ffa6504 23 | HEAD_REF RC_2_0 24 | ) 25 | 26 | vcpkg_from_github( 27 | OUT_SOURCE_PATH TRYSIGNAL_SOURCE_PATH 28 | REPO arvidn/try_signal 29 | REF 334fd139e2bb387017b42d36753a03935e3bca75 30 | SHA512 a25d439b2d979e975f9dd125a34072f70bfc7a08fab950e3829130742c05c584ae88d9f58fc0f1b4fa0b51df2c0e32c5b24c5828d53b121b4bc183a4c68d6a5a 31 | HEAD_REF master 32 | ) 33 | 34 | file( 35 | COPY 36 | ${TRYSIGNAL_SOURCE_PATH}/signal_error_code.cpp 37 | ${TRYSIGNAL_SOURCE_PATH}/signal_error_code.hpp 38 | ${TRYSIGNAL_SOURCE_PATH}/try_signal.cpp 39 | ${TRYSIGNAL_SOURCE_PATH}/try_signal.hpp 40 | ${TRYSIGNAL_SOURCE_PATH}/try_signal_mingw.hpp 41 | ${TRYSIGNAL_SOURCE_PATH}/try_signal_msvc.hpp 42 | ${TRYSIGNAL_SOURCE_PATH}/try_signal_posix.hpp 43 | DESTINATION ${SOURCE_PATH}/deps/try_signal) 44 | 45 | vcpkg_configure_cmake( 46 | SOURCE_PATH ${SOURCE_PATH} 47 | PREFER_NINJA 48 | OPTIONS 49 | ${FEATURE_OPTIONS} 50 | -Dstatic_runtime=${_static_runtime} 51 | ) 52 | 53 | vcpkg_install_cmake() 54 | 55 | vcpkg_fixup_cmake_targets(CONFIG_PATH lib/cmake/LibtorrentRasterbar TARGET_PATH share/LibtorrentRasterbar) 56 | 57 | # Handle copyright 58 | file(INSTALL ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright) 59 | 60 | # Do not duplicate include files 61 | file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include ${CURRENT_PACKAGES_DIR}/debug/share ${CURRENT_PACKAGES_DIR}/share/cmake) 62 | 63 | vcpkg_fixup_pkgconfig() 64 | -------------------------------------------------------------------------------- /client/src/scss/components/_torrent-file.scss: -------------------------------------------------------------------------------- 1 | 2 | // Torrent File 3 | .torrent-file { 4 | position : absolute; 5 | left : 0; 6 | top : 0; 7 | right : 0; 8 | bottom : 0; 9 | background: var(--backdrop-bg); 10 | padding : 1rem 0.75rem; 11 | overflow-y: auto; 12 | h2 { 13 | font-size: 1.5rem; 14 | @media (min-width: $breakpoint-sm) { 15 | font-size: 1.35rem; 16 | } 17 | } 18 | h3 { 19 | font-size: 1.25rem; 20 | @media (min-width: $breakpoint-sm) { 21 | font-size: 1.15rem; 22 | } 23 | } 24 | .status-bar { 25 | width : calc(100% + 1.5rem); 26 | margin-left : -0.75rem; 27 | margin-bottom: 1rem; 28 | border-top : var(--separator); 29 | .item { 30 | flex : 0 0 auto; 31 | border-bottom : 0; 32 | justify-content: flex-start; 33 | padding : 0.5rem 0.75rem; 34 | min-width : 100px; 35 | } 36 | } 37 | .row { 38 | margin : 0 -1rem 2rem; 39 | flex-wrap: wrap; 40 | .col { 41 | display : flex; 42 | flex : 1 1 100%; 43 | max-width : 100%; 44 | border-bottom: var(--separator); 45 | @media (min-width: $breakpoint-md) { 46 | flex : 1 1 50%; 47 | max-width: 50%; 48 | } 49 | @media (min-width: $breakpoint-lg) { 50 | flex : 1 1 33.333%; 51 | max-width: 33.333%; 52 | } 53 | @media (min-width: $breakpoint-xl) { 54 | flex : 1 1 25%; 55 | max-width: 25%; 56 | } 57 | .label, 58 | .value { 59 | padding : 0.35rem 1rem; 60 | word-wrap: break-word; 61 | @media (min-width: $breakpoint-md) { 62 | flex : 1 1 50%; 63 | max-width : 50%; 64 | max-height: 45px; 65 | } 66 | @media (min-width: $breakpoint-md) { 67 | max-height: 50px; 68 | } 69 | } 70 | .label { 71 | flex : 0 0 175px; 72 | width : 175px; 73 | font-size : 0.75rem; 74 | font-weight: 500; 75 | color : var(--body-color-highlight); 76 | overflow : auto; 77 | } 78 | .value { 79 | flex : 1 1 100%; 80 | font-size: 0.8rem; 81 | overflow : auto; 82 | } 83 | } 84 | } 85 | .progress { 86 | margin: 1rem 0 2rem; 87 | } 88 | .tabs { 89 | margin: 0 -0.75rem 1rem; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/rpc/configget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/database.hpp" 5 | #include "../../src/data/statement.hpp" 6 | #include "../../src/rpc/configget.hpp" 7 | 8 | using json = nlohmann::json; 9 | using pt::Server::Data::Statement; 10 | using pt::Server::RPC::ConfigGetCommand; 11 | 12 | class ConfigGetCommandTests : public ::testing::Test 13 | { 14 | protected: 15 | void SetUp() override 16 | { 17 | sqlite3_open(":memory:", &db); 18 | EXPECT_TRUE(pt::Server::Database::Migrate(db)); 19 | 20 | cmd = std::make_unique(db); 21 | } 22 | 23 | void TearDown() override 24 | { 25 | sqlite3_close(db); 26 | } 27 | 28 | void InsertValue(const std::string_view& key, const json& value) 29 | { 30 | Statement::ForEach( 31 | db, 32 | "REPLACE INTO config (key, value) VALUES ($1, $2)", 33 | [](auto const&){}, 34 | [&key, &value](auto stmt) 35 | { 36 | sqlite3_bind_text(stmt, 1, key.data(), -1, SQLITE_TRANSIENT); 37 | sqlite3_bind_text(stmt, 2, value.dump().c_str(), -1, SQLITE_TRANSIENT); 38 | }); 39 | } 40 | 41 | json ToResult(const json& value) 42 | { 43 | return { 44 | { "result", value } 45 | }; 46 | } 47 | 48 | sqlite3* db; 49 | std::unique_ptr cmd; 50 | }; 51 | 52 | TEST_F(ConfigGetCommandTests, Execute_WithNonExistingStringKey_ReturnsNullResult) 53 | { 54 | EXPECT_EQ(cmd->Execute("foo"), ToResult({})); 55 | } 56 | 57 | TEST_F(ConfigGetCommandTests, Execute_WithExistingKey_ReturnsValue) 58 | { 59 | InsertValue("foo", R"([ 1, 2, 3 ])"_json); 60 | EXPECT_EQ(cmd->Execute("foo"), ToResult({ 1, 2, 3 })); 61 | } 62 | 63 | TEST_F(ConfigGetCommandTests, Execute_WithMultipleExistingKeys_ReturnsValues) 64 | { 65 | InsertValue("foo", R"([ 1, 2, 3 ])"_json); 66 | InsertValue("bar", R"({ "baz": 1 })"_json); 67 | 68 | EXPECT_EQ( 69 | cmd->Execute({ "foo", "bar" }), 70 | ToResult(R"({ "foo": [1,2,3], "bar":{"baz":1}})"_json)); 71 | } 72 | 73 | TEST_F(ConfigGetCommandTests, Execute_WithOneExistingAndOneMissingKey_ReturnsMixedResult) 74 | { 75 | InsertValue("foo", R"([ 1, 2, 3 ])"_json); 76 | 77 | EXPECT_EQ( 78 | cmd->Execute({ "foo", "bar" }), 79 | ToResult(R"({ "foo": [1,2,3], "bar": null })"_json)); 80 | } 81 | -------------------------------------------------------------------------------- /src/sessionmanager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace pt::Server::TSDB 14 | { 15 | struct TimeSeriesDatabase; 16 | } 17 | 18 | namespace pt::Server 19 | { 20 | class ITorrentHandleActor 21 | { 22 | public: 23 | virtual ~ITorrentHandleActor() = default; 24 | 25 | virtual bool IsValid() = 0; 26 | virtual void Pause() = 0; 27 | virtual void Resume() = 0; 28 | }; 29 | 30 | class ITorrentHandleFinder 31 | { 32 | public: 33 | virtual std::shared_ptr Find(const libtorrent::info_hash_t& hash) = 0; 34 | }; 35 | 36 | class SessionManager : public ITorrentHandleFinder 37 | { 38 | public: 39 | static std::shared_ptr Load(boost::asio::io_context& io, sqlite3* db, std::shared_ptr tsdb); 40 | 41 | ~SessionManager(); 42 | 43 | // inherited 44 | std::shared_ptr Find(const libtorrent::info_hash_t& hash) override; 45 | 46 | libtorrent::info_hash_t AddTorrent(libtorrent::add_torrent_params& params); 47 | bool FindTorrent(libtorrent::info_hash_t const& hash, libtorrent::torrent_status& status); 48 | void ForEachTorrent(std::function const&); 49 | void ReloadSettings(); 50 | void RemoveTorrent(libtorrent::info_hash_t const& hash, bool removeFiles = false); 51 | std::shared_ptr Subscribe(std::function); 52 | 53 | SessionManager(boost::asio::io_context& io, sqlite3* db, std::unique_ptr session, std::shared_ptr tsdb); 54 | 55 | private: 56 | void Broadcast(nlohmann::json&); 57 | void LoadTorrents(); 58 | void ReadAlerts(); 59 | void PostUpdates(boost::system::error_code ec); 60 | 61 | boost::asio::io_context& m_io; 62 | boost::asio::deadline_timer m_timer; 63 | 64 | sqlite3* m_db; 65 | std::unique_ptr m_session; 66 | std::map m_torrents; 67 | std::vector>> m_subscribers; 68 | std::vector m_stats; 69 | std::shared_ptr m_tsdb; 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/database.cpp: -------------------------------------------------------------------------------- 1 | #include "database.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "data/migrations/0001_initialsetup.hpp" 10 | #include "data/transaction.hpp" 11 | 12 | using pt::Server::Database; 13 | 14 | static std::map> DatabaseMigrations = 15 | { 16 | { "0001_InitialSetup", &pt::Server::Data::Migrations::InitialSetup::Migrate }, 17 | }; 18 | 19 | bool CreateMigrationsTable(sqlite3* db) 20 | { 21 | int res = sqlite3_exec( 22 | db, 23 | "CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY);", 24 | nullptr, 25 | nullptr, 26 | nullptr); 27 | 28 | return res == SQLITE_OK; 29 | } 30 | 31 | bool MigrationExists(sqlite3* db, std::string const& id) 32 | { 33 | sqlite3_stmt* stmt; 34 | sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM migrations WHERE id = $1", -1, &stmt, nullptr); 35 | sqlite3_bind_text(stmt, 1, id.c_str(), static_cast(id.size()), SQLITE_TRANSIENT); 36 | sqlite3_step(stmt); 37 | int cnt = sqlite3_column_int(stmt, 0); 38 | sqlite3_finalize(stmt); 39 | return cnt >= 1; 40 | } 41 | 42 | bool Database::Migrate(sqlite3* db) 43 | { 44 | Data::Transaction tx(db); 45 | 46 | BOOST_LOG_TRIVIAL(debug) << "Creating migrations table"; 47 | 48 | if (!CreateMigrationsTable(db)) 49 | { 50 | BOOST_LOG_TRIVIAL(error) << "Failed to create migrations table"; 51 | tx.Rollback(); 52 | return false; 53 | } 54 | 55 | // Run migrations 56 | for (auto const& migration : DatabaseMigrations) 57 | { 58 | if (MigrationExists(db, migration.first)) 59 | { 60 | BOOST_LOG_TRIVIAL(debug) << "Migration '" << migration.first << "' exists - skipping..."; 61 | continue; 62 | } 63 | 64 | int res = migration.second(db); 65 | 66 | if (res != SQLITE_OK) 67 | { 68 | BOOST_LOG_TRIVIAL(error) << "Failed to apply migration: " << sqlite3_errmsg(db); 69 | tx.Rollback(); 70 | return false; 71 | } 72 | 73 | sqlite3_stmt* stmt; 74 | sqlite3_prepare_v2(db, "INSERT INTO migrations (id) VALUES ($1);", -1, &stmt, nullptr); 75 | sqlite3_bind_text(stmt, 1, migration.first.c_str(), -1, SQLITE_TRANSIENT); 76 | sqlite3_step(stmt); 77 | sqlite3_finalize(stmt); 78 | 79 | BOOST_LOG_TRIVIAL(info) << "Applied migration " << migration.first; 80 | } 81 | 82 | tx.Commit(); 83 | 84 | return true; 85 | } 86 | -------------------------------------------------------------------------------- /src/rpc/sessionaddtorrent.cpp: -------------------------------------------------------------------------------- 1 | #include "sessionaddtorrent.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../json/infohash.hpp" 9 | #include "../sessionmanager.hpp" 10 | 11 | static std::string Base64Decode(const std::string_view in) 12 | { 13 | // table from '+' to 'z' 14 | const uint8_t lookup[] = { 15 | 62, 255, 62, 255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 16 | 255, 0, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 18 | 255, 255, 255, 255, 63, 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 19 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; 20 | static_assert(sizeof(lookup) == 'z' - '+' + 1); 21 | 22 | std::string out; 23 | int val = 0, valb = -8; 24 | for (uint8_t c : in) { 25 | if (c < '+' || c > 'z') 26 | break; 27 | c -= '+'; 28 | if (lookup[c] >= 64) 29 | break; 30 | val = (val << 6) + lookup[c]; 31 | valb += 6; 32 | if (valb >= 0) { 33 | out.push_back(char((val >> valb) & 0xFF)); 34 | valb -= 8; 35 | } 36 | } 37 | return out; 38 | } 39 | 40 | namespace lt = libtorrent; 41 | using json = nlohmann::json; 42 | using pt::Server::SessionManager; 43 | using pt::Server::RPC::SessionAddTorrentCommand; 44 | 45 | SessionAddTorrentCommand::SessionAddTorrentCommand(std::shared_ptr session) 46 | : m_session(std::move(session)) 47 | { 48 | } 49 | 50 | json SessionAddTorrentCommand::Execute(const json& j) 51 | { 52 | if (!j.contains("data")) 53 | { 54 | return Error(1, "Missing 'data' field"); 55 | } 56 | 57 | if (!j.contains("save_path")) 58 | { 59 | return Error(1, "Missing 'save_path' field"); 60 | } 61 | 62 | std::string const& data = Base64Decode( 63 | j["data"].get()); 64 | 65 | lt::error_code ec; 66 | lt::bdecode_node node = lt::bdecode(data, ec); 67 | 68 | if (ec) 69 | { 70 | BOOST_LOG_TRIVIAL(error) << "Failed to bdecode torrent: " << ec.message(); 71 | return Error(1, ec.message()); 72 | } 73 | 74 | lt::add_torrent_params p; 75 | p.save_path = j["save_path"].get(); 76 | p.ti = std::make_shared(node); 77 | 78 | return Ok({ 79 | { "info_hash", m_session->AddTorrent(p) } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /client/src/components/StatusBar.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 84 | -------------------------------------------------------------------------------- /client/src/scss/components/_form.scss: -------------------------------------------------------------------------------- 1 | input, 2 | select, 3 | textarea { 4 | margin-bottom: 1rem; 5 | padding : 0.5rem 0.75rem; 6 | background : hsla(var(--input-bg), var(--input-bg-alpha)); 7 | color : var(--input-color); 8 | border : 0; 9 | border-radius: 0.25rem; 10 | outline : 0; 11 | transition : all 0.35s ease; 12 | min-width : 200px; 13 | &[type="checkbox"] { 14 | min-width: auto; 15 | } 16 | &:hover { 17 | color : var(--input-hover-color); 18 | background: hsla(var(--input-bg), var(--input-hover-bg-alpha)); 19 | } 20 | &:focus { 21 | box-shadow: 0 0 0 2px hsla(var(--state-hsl, var(--primary-hsl)), var(--alpha-outline)); 22 | } 23 | } 24 | 25 | select { 26 | option { 27 | color: #000; 28 | } 29 | } 30 | 31 | input[type="file"] { 32 | &::-webkit-file-upload-button { 33 | background: var(--primary); 34 | color : var(--white); 35 | border : 0; 36 | margin : -1rem 0.5rem -1rem -0.5rem; 37 | padding : 0.25rem 0.5rem; 38 | outline : 0; 39 | } 40 | } 41 | 42 | .check { 43 | display : flex; 44 | align-items : center; 45 | margin-bottom: 0.5rem; 46 | input[type="checkbox"] { 47 | width : 1.25em !important; 48 | height : 1.25em !important; 49 | margin : 0 0.5rem 0 0; 50 | padding : 0; 51 | vertical-align : middle; 52 | background-repeat : no-repeat; 53 | background-position : center; 54 | background-size : contain; 55 | -webkit-appearance : none; 56 | -moz-appearance : none; 57 | -webkit-print-color-adjust: exact; 58 | appearance : none; 59 | color-adjust : exact; 60 | border : 0; 61 | border-radius : 0.25rem; 62 | &:checked { 63 | background : var(--primary) url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); 64 | border-color: var(--primary); 65 | } 66 | } 67 | } 68 | 69 | .form { 70 | .group { 71 | display : flex; 72 | flex-wrap: wrap; 73 | margin : 0 -1rem; 74 | .item { 75 | flex: 1; 76 | @media (min-width: $breakpoint-sm) { 77 | flex: 0 1 auto; 78 | } 79 | label { 80 | font-weight : 500; 81 | margin-bottom: 0.75rem; 82 | } 83 | } 84 | } 85 | .item:not(.check) { 86 | display : flex; 87 | flex-direction: column; 88 | margin : 0 1rem; 89 | label { 90 | margin-bottom: 0.5rem; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PicoTorrent Server is a high performance BitTorrent server 2 | 3 | Built to handle tens of thousands of torrents, PicoTorrent Server is an 4 | excellent BitTorrent server. Perfect for a seedbox setup where performance 5 | and ease of use is key. 6 | 7 | ## Features 8 | 9 | * Full support for BitTorrent 2.0 ([BEP-52](http://bittorrent.org/beps/bep_0052.html)), 10 | v1, v2 and v1+v2 hybrid torrents. 11 | * A simple, clean, embedded web UI for management. 12 | * All data is stored in a SQLite database which makes backups trivial. 13 | * A Prometheus exporter for monitoring session statistics. 14 | 15 | ## Getting started 16 | 17 | PicoTorrent Server is distributed via Docker which makes it easy to run and 18 | distribute. 19 | 20 | ```sh 21 | docker run -p 8080:1337 ghcr.io/picotorrent/server:{VERSION} 22 | ``` 23 | 24 | ### Using `docker-compose` 25 | 26 | This is an example file with more advanced usage - the container is set to 27 | read-only and volumes are mapped in for data storage. 28 | 29 | ```yaml 30 | version: '3' 31 | 32 | services: 33 | picotorrent: 34 | image: ghcr.io/picotorrent/server:{VERSION} 35 | ports: 36 | - 8080:1337 37 | environment: 38 | PICOTORRENT_DB_FILE: /data/PicoTorrent.sqlite 39 | volumes: 40 | - /mnt/downloads:/downloads 41 | - /var/lib/picotorrent:/data 42 | ``` 43 | 44 | ### Configuration 45 | 46 | Use these environment variables to configure the behavior of PicoTorrent 47 | Server. 48 | 49 | * `PICOTORRENT_DB_FILE` - the path to a SQLite database to use for data storage. 50 | The file will be created if it does not exist. _Defaults to right next to the 51 | PicoTorrentServer binary_. 52 | * `PICOTORRENT_HTTP_HOST` - the IP address to listen on for the HTTP server. 53 | _Defaults to `127.0.0.1`_. 54 | * `PICOTORRENT_HTTP_PORT` - the port to use for the HTTP server. _Defaults to `1337`_. 55 | * `PICOTORRENT_WEBROOT_PATH` - the path to the web UI. If unset, no web UI will 56 | be served. _Defaults to `/app/client` in the Docker container which is where 57 | the bundled web UI is located_. 58 | 59 | #### Prometheus 60 | 61 | * `PICOTORRENT_PROMETHEUS_EXPORTER` - if this is set, the Prometheus exporter 62 | is enabled and will serve metrics on `/metrics`. _Defaults to unset_. 63 | 64 | ## Setting up for development 65 | 66 | Build PicoTorrent Server then run it. 67 | 68 | ```sh 69 | $ mkdir build && cd build 70 | $ cmake -G Ninja 71 | $ ninja 72 | $ ./PicoTorrentServer 73 | ``` 74 | 75 | Then start the Vue client. 76 | 77 | ```sh 78 | $ cd client 79 | $ npm i 80 | $ npm run serve 81 | ``` 82 | 83 | Open `http://localhost:8080` in your browser. All API requests are forwarded to 84 | the server process with the help of the Vue proxy. 85 | 86 | In production scenarios, the Vue client is hosted by PicoTorrent itself. 87 | -------------------------------------------------------------------------------- /src/rpc/settingspackgetbyid.cpp: -------------------------------------------------------------------------------- 1 | #include "settingspackgetbyid.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include "../data/datareader.hpp" 7 | #include "../data/models/settingspack.hpp" 8 | #include "../sessionmanager.hpp" 9 | 10 | namespace lt = libtorrent; 11 | using json = nlohmann::json; 12 | using pt::Server::Data::SettingsPack; 13 | using pt::Server::RPC::SettingsPackGetByIdCommand; 14 | using pt::Server::SessionManager; 15 | 16 | SettingsPackGetByIdCommand::SettingsPackGetByIdCommand(sqlite3* db) 17 | : m_db(db) 18 | { 19 | } 20 | 21 | json SettingsPackGetByIdCommand::Execute(const json& params) 22 | { 23 | sqlite3_stmt* stmt; 24 | sqlite3_prepare_v2(m_db, "SELECT sp.id,sp.description,sp.name, * FROM settings_pack sp WHERE id = $1", -1, &stmt, nullptr); 25 | sqlite3_bind_int(stmt, 1, params[0].get()); 26 | 27 | json result; 28 | result["settings"] = json::object(); 29 | 30 | switch (sqlite3_step(stmt)) 31 | { 32 | case SQLITE_ROW: 33 | { 34 | result["id"] = sqlite3_column_int(stmt, 0); 35 | result["description"] = pt_sqlite3_column_std_string(stmt, 1).value_or(""); 36 | result["name"] = pt_sqlite3_column_std_string(stmt, 2).value_or(""); 37 | 38 | for (int i = 0; i < sqlite3_column_count(stmt); i++) 39 | { 40 | std::string columnName = sqlite3_column_name(stmt, i); 41 | 42 | if (SettingsPack::Names().find(columnName) == SettingsPack::Names().end()) 43 | { 44 | continue; 45 | } 46 | 47 | int setting = lt::setting_by_name(columnName); 48 | 49 | if (setting == -1) 50 | { 51 | BOOST_LOG_TRIVIAL(warning) << "Unknown setting: " << columnName; 52 | } 53 | 54 | if (setting >= lt::settings_pack::string_type_base 55 | && setting < lt::settings_pack::max_string_setting_internal) 56 | { 57 | result["settings"][columnName] = pt_sqlite3_column_std_string(stmt, i).value_or(""); 58 | } 59 | 60 | if (setting >= lt::settings_pack::bool_type_base 61 | && setting < lt::settings_pack::max_bool_setting_internal) 62 | { 63 | result["settings"][columnName] = sqlite3_column_int(stmt, i) == 1 ? true : false; 64 | } 65 | 66 | if (setting >= lt::settings_pack::int_type_base 67 | && setting < lt::settings_pack::max_int_setting_internal) 68 | { 69 | result["settings"][columnName] = sqlite3_column_int(stmt, i); 70 | } 71 | } 72 | 73 | break; 74 | } 75 | case SQLITE_ERROR: 76 | { 77 | BOOST_LOG_TRIVIAL(error) << "Error when fetching settings pack: " << sqlite3_errmsg(m_db); 78 | break; 79 | } 80 | } 81 | 82 | sqlite3_finalize(stmt); 83 | 84 | return Ok(result); 85 | } 86 | -------------------------------------------------------------------------------- /client/src/scss/components/_status-bar.scss: -------------------------------------------------------------------------------- 1 | 2 | // Status bar 3 | .status-bar { 4 | display : flex; 5 | align-items: stretch; 6 | width : 100%; 7 | flex-wrap : wrap; 8 | @if $ui-enable-backdrop { 9 | border-bottom: var(--separator); 10 | } @else { 11 | box-shadow: 0 0.05rem 0.25rem hsla(0, 0%, 0%, 0.10); 12 | } 13 | .item { 14 | display : flex; 15 | flex : 0 0 auto; 16 | align-items : center; 17 | justify-content: center; 18 | padding : .60rem 1rem; 19 | font-size : .75rem; 20 | span { 21 | font-weight : 500; 22 | margin-right: .35rem; 23 | } 24 | @media (max-width: $breakpoint-md) { 25 | padding : .5rem; 26 | flex : 1; 27 | border-bottom: var(--separator); 28 | } 29 | @media (max-width: $breakpoint-md) { 30 | span { 31 | @include aria-ready(); 32 | } 33 | } 34 | &:first-child { 35 | margin-left: -2.5px; 36 | } 37 | &:last-child { 38 | border: 0; 39 | } 40 | .pt-icon { 41 | margin-right: .5rem; 42 | } 43 | .bi { 44 | font-size : 125%; 45 | line-height : 0; 46 | margin-right : .75rem; 47 | margin-bottom: -2px; 48 | transition : all 0.35s ease; 49 | } 50 | } 51 | 52 | .graphs { 53 | display : flex; 54 | flex : 0 1 auto; 55 | height : 100%; 56 | max-height : 40px; 57 | max-width : 100%; 58 | margin-left: auto; 59 | overflow : hidden; 60 | user-select: none; 61 | @media (max-width: $breakpoint-md) { 62 | order: 2; 63 | flex : 1 1 100%; 64 | } 65 | .both, 66 | .download, 67 | .upload { 68 | flex : 1 1 0; 69 | height : 100%; 70 | position: relative; 71 | @media (max-width: $breakpoint-md) { 72 | flex: 1 1 100%; 73 | } 74 | .label { 75 | display: flex; 76 | align-items: center; 77 | font-size : 10px; 78 | font-weight: bold; 79 | position : absolute; 80 | bottom : 0; 81 | left : 0; 82 | opacity : 0.80; 83 | padding: 5px 0; 84 | div { 85 | display: flex; 86 | align-items: center; 87 | .bi { 88 | line-height: 0; 89 | margin: 0 5px; 90 | } 91 | } 92 | } 93 | .dl { 94 | color: $primary; 95 | } 96 | .ul { 97 | color: $blue; 98 | } 99 | .graph { 100 | flex : 1 1 0; 101 | height : 100%; 102 | max-width: 300px; 103 | @media (max-width: $breakpoint-md) { 104 | flex : 1; 105 | max-width: 100%; 106 | } 107 | canvas { 108 | width: 100% !important; 109 | height: 100% !important; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/rpc/torrentspause.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../helpers.hpp" 5 | #include "../mocks.hpp" 6 | #include "../../src/json/infohash.hpp" 7 | #include "../../src/rpc/torrentspause.hpp" 8 | 9 | using pt::Server::RPC::TorrentsPauseCommand; 10 | 11 | class TorrentsPauseCommandTests : public ::testing::Test 12 | { 13 | protected: 14 | void SetUp() override 15 | { 16 | finder = std::make_shared(); 17 | cmd = std::make_unique(finder); 18 | } 19 | 20 | std::unique_ptr cmd; 21 | std::shared_ptr finder; 22 | }; 23 | 24 | TEST_F(TorrentsPauseCommandTests, Execute_WithInvalidParams_ReturnsError) 25 | { 26 | auto result = cmd->Execute(1); 27 | 28 | EXPECT_TRUE(result.contains("error")); 29 | EXPECT_EQ(result["error"]["code"], 1); 30 | } 31 | 32 | TEST_F(TorrentsPauseCommandTests, Execute_WithValidInfoHash_PausesTorrent) 33 | { 34 | lt::info_hash_t ih = pt::InfoHashFromString("0101010101010101010101010101010101010101"); 35 | 36 | auto handle = std::make_shared(); 37 | 38 | EXPECT_CALL(*finder, Find(ih)) 39 | .Times(1) 40 | .WillOnce(::testing::Return(handle)); 41 | 42 | EXPECT_CALL(*handle, IsValid()) 43 | .Times(1) 44 | .WillOnce(::testing::Return(true)); 45 | 46 | EXPECT_CALL(*handle, Pause()) 47 | .Times(1); 48 | 49 | auto result = cmd->Execute("0101010101010101010101010101010101010101"); 50 | 51 | EXPECT_TRUE(result.is_object()); 52 | } 53 | 54 | TEST_F(TorrentsPauseCommandTests, Execute_WithValidInfoHashArray_PausesTorrents) 55 | { 56 | struct F 57 | { 58 | lt::info_hash_t ih; 59 | std::shared_ptr handle; 60 | }; 61 | 62 | std::vector items; 63 | items.push_back({ pt::InfoHashFromString("0101010101010101010101010101010101010101"), std::make_shared() }); 64 | items.push_back({ pt::InfoHashFromString("0202020202020202020202020202020202020202"), std::make_shared() }); 65 | items.push_back({ pt::InfoHashFromString("0303030303030303030303030303030303030303"), std::make_shared() }); 66 | 67 | auto handle = std::make_shared(); 68 | 69 | for (auto const& itm : items) 70 | { 71 | EXPECT_CALL(*finder, Find(itm.ih)) 72 | .Times(1) 73 | .WillOnce(::testing::Return(itm.handle)); 74 | 75 | EXPECT_CALL(*itm.handle, IsValid()) 76 | .Times(1) 77 | .WillOnce(::testing::Return(true)); 78 | 79 | EXPECT_CALL(*itm.handle, Pause()) 80 | .Times(1); 81 | } 82 | 83 | auto result = cmd->Execute( 84 | { "0101010101010101010101010101010101010101", 85 | "0202020202020202020202020202020202020202", 86 | "0303030303030303030303030303030303030303" }); 87 | 88 | EXPECT_TRUE(result.is_object()); 89 | } 90 | -------------------------------------------------------------------------------- /tests/rpc/torrentsresume.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../helpers.hpp" 8 | #include "../mocks.hpp" 9 | #include "../../src/json/infohash.hpp" 10 | #include "../../src/rpc/torrentsresume.hpp" 11 | 12 | using pt::Server::RPC::TorrentsResumeCommand; 13 | 14 | class TorrentsResumeCommandTests : public ::testing::Test 15 | { 16 | protected: 17 | void SetUp() override 18 | { 19 | finder = std::make_shared(); 20 | cmd = std::make_unique(finder); 21 | } 22 | 23 | std::unique_ptr cmd; 24 | std::shared_ptr finder; 25 | }; 26 | 27 | TEST_F(TorrentsResumeCommandTests, Execute_WithInvalidParams_ReturnsError) 28 | { 29 | auto result = cmd->Execute(1); 30 | 31 | EXPECT_TRUE(result.contains("error")); 32 | EXPECT_EQ(result["error"]["code"], 1); 33 | } 34 | 35 | TEST_F(TorrentsResumeCommandTests, Execute_WithValidInfoHash_ResumesTorrent) 36 | { 37 | auto hash = pt::InfoHashFromString("7cf55428325617fdde910fe55b79ab72be937924"); 38 | auto handle = std::make_shared(); 39 | 40 | EXPECT_CALL(*finder, Find(hash)) 41 | .Times(1) 42 | .WillOnce(::testing::Return(handle)); 43 | 44 | EXPECT_CALL(*handle, IsValid()) 45 | .Times(1) 46 | .WillOnce(::testing::Return(true)); 47 | 48 | EXPECT_CALL(*handle, Resume()) 49 | .Times(1); 50 | 51 | auto result = cmd->Execute("7cf55428325617fdde910fe55b79ab72be937924"); 52 | 53 | EXPECT_TRUE(result.is_object()); 54 | } 55 | 56 | TEST_F(TorrentsResumeCommandTests, Execute_WithValidInfoHashArray_ResumesTorrents) 57 | { 58 | struct F 59 | { 60 | lt::info_hash_t ih; 61 | std::shared_ptr handle; 62 | }; 63 | 64 | std::vector items; 65 | items.push_back({ pt::InfoHashFromString("7cf55428325617fdde910fe55b79ab72be937924"), std::make_shared() }); 66 | items.push_back({ pt::InfoHashFromString("0202020202020202020202020202020202020202"), std::make_shared() }); 67 | items.push_back({ pt::InfoHashFromString("0303030303030303030303030303030303030303"), std::make_shared() }); 68 | 69 | auto handle = std::make_shared(); 70 | 71 | for (auto const& itm : items) 72 | { 73 | EXPECT_CALL(*finder, Find(itm.ih)) 74 | .Times(1) 75 | .WillOnce(::testing::Return(itm.handle)); 76 | 77 | EXPECT_CALL(*itm.handle, IsValid()) 78 | .Times(1) 79 | .WillOnce(::testing::Return(true)); 80 | 81 | EXPECT_CALL(*itm.handle, Resume()) 82 | .Times(1); 83 | } 84 | 85 | auto result = cmd->Execute( 86 | { "7cf55428325617fdde910fe55b79ab72be937924", 87 | "0202020202020202020202020202020202020202", 88 | "0303030303030303030303030303030303030303" }); 89 | 90 | EXPECT_TRUE(result.is_object()); 91 | } 92 | -------------------------------------------------------------------------------- /client/src/components/TorrentList.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 96 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18 FATAL_ERROR) 2 | 3 | cmake_policy(SET CMP0092 NEW) # don't add /W3 as default 4 | 5 | set(VCPKG_OVERLAY_PORTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/vcpkg-overlays/ports) 6 | set(VCPKG_OVERLAY_TRIPLETS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/vcpkg-overlays/triplets) 7 | set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/vcpkg/scripts/buildsystems/vcpkg.cmake CACHE STRING "Vcpkg toolchain file") 8 | 9 | project(PicoTorrentServer) 10 | 11 | set(CMAKE_CXX_STANDARD 20) 12 | 13 | find_package(Boost REQUIRED COMPONENTS log program_options system) 14 | find_package(GTest CONFIG REQUIRED) 15 | find_package(LibtorrentRasterbar CONFIG REQUIRED) 16 | find_package(nlohmann_json CONFIG REQUIRED) 17 | find_package(unofficial-sqlite3 CONFIG REQUIRED) 18 | 19 | add_library( 20 | PicoTorrentCore 21 | STATIC 22 | 23 | src/database.cpp 24 | src/log.cpp 25 | src/options.cpp 26 | src/sessionmanager.cpp 27 | 28 | # data 29 | src/data/migrations/0001_initialsetup.cpp 30 | src/data/statement.cpp 31 | src/data/transaction.cpp 32 | src/data/models/config.cpp 33 | src/data/models/listeninterface.cpp 34 | src/data/models/profile.cpp 35 | src/data/models/proxy.cpp 36 | src/data/models/settingspack.cpp 37 | src/data/models/settingspacknames.cpp 38 | 39 | # http 40 | src/http/httplistener.cpp 41 | src/http/httpsession.cpp 42 | 43 | # http handlers 44 | src/http/handlers/jsonrpchandler.cpp 45 | src/http/handlers/jsonrpchandler.hpp 46 | src/http/handlers/websockethandler.cpp 47 | src/http/handlers/websockethandler.hpp 48 | 49 | # rpc 50 | src/rpc/configget.cpp 51 | src/rpc/configset.cpp 52 | src/rpc/listeninterfacescreate.cpp 53 | src/rpc/listeninterfacesgetall.cpp 54 | src/rpc/listeninterfacesremove.cpp 55 | src/rpc/profilesgetactive.cpp 56 | src/rpc/profilesgetall.cpp 57 | src/rpc/profilesupdate.cpp 58 | src/rpc/proxycreate.cpp 59 | src/rpc/proxygetall.cpp 60 | src/rpc/sessionaddmagnetlink.cpp 61 | src/rpc/sessionaddtorrent.cpp 62 | src/rpc/sessionremovetorrent.cpp 63 | src/rpc/settingspackcreate.cpp 64 | src/rpc/settingspackgetbyid.cpp 65 | src/rpc/settingspackupdate.cpp 66 | src/rpc/settingspacklist.cpp 67 | src/rpc/torrentspause.cpp 68 | src/rpc/torrentsresume.cpp 69 | 70 | # tsdb 71 | src/tsdb/influxdb.cpp 72 | src/tsdb/influxdb.hpp 73 | src/tsdb/prometheus.cpp 74 | src/tsdb/prometheus.hpp 75 | ) 76 | 77 | 78 | target_link_libraries( 79 | PicoTorrentCore 80 | Boost::boost 81 | Boost::log 82 | Boost::program_options 83 | nlohmann_json::nlohmann_json 84 | LibtorrentRasterbar::torrent-rasterbar 85 | unofficial::sqlite3::sqlite3 86 | ) 87 | 88 | add_executable( 89 | PicoTorrentServer 90 | src/main.cpp 91 | ) 92 | 93 | target_link_libraries( 94 | PicoTorrentServer 95 | PicoTorrentCore 96 | ) 97 | 98 | add_executable( 99 | PicoTorrentTests 100 | tests/main.cpp 101 | tests/data/models.cpp 102 | tests/rpc/configget.cpp 103 | tests/rpc/configset.cpp 104 | tests/rpc/torrentspause.cpp 105 | tests/rpc/torrentsresume.cpp 106 | ) 107 | 108 | target_link_libraries( 109 | PicoTorrentTests 110 | PRIVATE 111 | PicoTorrentCore 112 | GTest::gmock GTest::gtest GTest::gmock_main 113 | ) 114 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "database.hpp" 7 | #include "log.hpp" 8 | #include "options.hpp" 9 | #include "sessionmanager.hpp" 10 | 11 | #include "http/handlers/jsonrpchandler.hpp" 12 | #include "http/handlers/websockethandler.hpp" 13 | #include "http/httplistener.hpp" 14 | #include "http/httprequesthandler.hpp" 15 | #include "tsdb/influxdb.hpp" 16 | #include "tsdb/prometheus.hpp" 17 | #include "tsdb/timeseriesdatabase.hpp" 18 | 19 | namespace fs = std::filesystem; 20 | namespace lt = libtorrent; 21 | 22 | using pt::Server::Database; 23 | using pt::Server::Http::Handlers::JsonRpcHandler; 24 | using pt::Server::Http::Handlers::WebSocketHandler; 25 | using pt::Server::Http::HttpListener; 26 | using pt::Server::Log; 27 | using pt::Server::Options; 28 | using pt::Server::SessionManager; 29 | 30 | void Run(sqlite3* db, std::shared_ptr const& options) 31 | { 32 | boost::asio::io_context io; 33 | boost::asio::signal_set signals(io, SIGINT, SIGTERM); 34 | signals.async_wait( 35 | [&io](boost::system::error_code const& ec, int signal) 36 | { 37 | io.stop(); 38 | }); 39 | 40 | std::shared_ptr tsdb = nullptr; 41 | 42 | auto http = std::make_shared( 43 | io, 44 | boost::asio::ip::tcp::endpoint 45 | { 46 | boost::asio::ip::make_address(options->Host()), 47 | options->Port() 48 | }, 49 | options->WebRoot()); 50 | 51 | if (options->IsValidInfluxDbConfig()) 52 | { 53 | BOOST_LOG_TRIVIAL(info) << "InfluxDb configuration seems legit. Configuring reporter..."; 54 | 55 | tsdb = std::make_shared( 56 | io, 57 | options->InfluxDbHost().value(), 58 | options->InfluxDbPort().value(), 59 | options->InfluxDbOrganization().value(), 60 | options->InfluxDbBucket().value(), 61 | options->InfluxDbToken().value()); 62 | } 63 | else if (options->PrometheusExporterEnabled()) 64 | { 65 | BOOST_LOG_TRIVIAL(info) << "Enabling Prometheus metrics exporter"; 66 | 67 | auto prometheus = std::make_shared(); 68 | http->AddHandler("GET", "/metrics", prometheus); 69 | tsdb = prometheus; 70 | } 71 | 72 | auto sm = SessionManager::Load(io, db, tsdb); 73 | 74 | http->AddHandler("POST", "/api/jsonrpc", std::make_shared(db, sm)); 75 | http->AddHandler("GET", "/api/ws", std::make_shared(sm)); 76 | http->Run(); 77 | 78 | io.run(); 79 | } 80 | 81 | int main(int argc, char* argv[]) 82 | { 83 | auto options = Options::Load(argc, argv); 84 | if (!options) { return 1; } 85 | 86 | Log::Setup(options->LogLevel()); 87 | 88 | BOOST_LOG_TRIVIAL(info) << "PicoTorrent Server starting up..."; 89 | BOOST_LOG_TRIVIAL(info) << "Opening database from " << options->DatabaseFilePath(); 90 | 91 | sqlite3* db = nullptr; 92 | sqlite3_open(options->DatabaseFilePath().c_str(), &db); 93 | 94 | if (!Database::Migrate(db)) 95 | { 96 | BOOST_LOG_TRIVIAL(error) << "Failed to migrate database, shutting down"; 97 | return 1; 98 | } 99 | 100 | Run(db, options); 101 | 102 | BOOST_LOG_TRIVIAL(info) << "Closing database..."; 103 | sqlite3_close(db); 104 | 105 | return 0; 106 | } 107 | -------------------------------------------------------------------------------- /src/data/models/profile.cpp: -------------------------------------------------------------------------------- 1 | #include "profile.hpp" 2 | 3 | #include 4 | 5 | #include "../datareader.hpp" 6 | #include "../sqliteexception.hpp" 7 | #include "../statement.hpp" 8 | 9 | using pt::Server::Data::Models::Profile; 10 | using pt::Server::Data::SQLiteException; 11 | using pt::Server::Data::Statement; 12 | 13 | std::shared_ptr Profile::GetActive(sqlite3* db) 14 | { 15 | std::shared_ptr result = nullptr; 16 | 17 | Statement::ForEach( 18 | db, 19 | "SELECT p.id, p.name, p.description, p.is_active, sp.id, sp.name, prx.id, prx.name " 20 | "FROM profiles p JOIN settings_pack sp ON p.settings_pack_id = sp.id " 21 | "LEFT JOIN proxy prx ON p.proxy_id = prx.id " 22 | "WHERE p.is_active = 1 " 23 | "LIMIT 1", 24 | [&](Statement::Row const& row) 25 | { 26 | result = Construct(row); 27 | }); 28 | 29 | return result; 30 | } 31 | 32 | std::vector> Profile::GetAll(sqlite3* db) 33 | { 34 | std::vector> result; 35 | 36 | Statement::ForEach( 37 | db, 38 | "SELECT p.id, p.name, p.description, p.is_active, sp.id, sp.name, prx.id, prx.name " 39 | "FROM profiles p JOIN settings_pack sp ON p.settings_pack_id = sp.id " 40 | "LEFT JOIN proxy prx ON p.proxy_id = prx.id " 41 | "ORDER BY p.id ASC", 42 | [&](Statement::Row const& row) 43 | { 44 | result.push_back(Construct(row)); 45 | }); 46 | 47 | return result; 48 | } 49 | 50 | std::shared_ptr Profile::GetById(sqlite3* db, int id) 51 | { 52 | std::shared_ptr result; 53 | 54 | Statement::ForEach( 55 | db, 56 | "SELECT p.id, p.name, p.description, p.is_active, sp.id, sp.name, prx.id, prx.name " 57 | "FROM profiles p JOIN settings_pack sp ON p.settings_pack_id = sp.id " 58 | "LEFT JOIN proxy prx ON p.proxy_id = prx.id " 59 | "WHERE p.id = $1", 60 | [&](Statement::Row const& row) 61 | { 62 | result = Construct(row); 63 | }, 64 | [&](sqlite3_stmt* stmt) 65 | { 66 | sqlite3_bind_int(stmt, 1, id); 67 | }); 68 | 69 | return result; 70 | } 71 | 72 | void Profile::Update(sqlite3* db, std::shared_ptr const& profile) 73 | { 74 | Statement::ForEach( 75 | db, 76 | "UPDATE profiles SET proxy_id = $1 WHERE id = $2", 77 | [](auto const&) {}, 78 | [&](sqlite3_stmt* stmt) 79 | { 80 | if (profile->ProxyId().has_value()) 81 | { 82 | sqlite3_bind_int(stmt, 1, profile->ProxyId().value()); 83 | } 84 | else 85 | { 86 | sqlite3_bind_null(stmt, 1); 87 | } 88 | 89 | sqlite3_bind_int(stmt, 2, profile->Id()); 90 | }); 91 | } 92 | 93 | std::shared_ptr Profile::Construct(Statement::Row const& row) 94 | { 95 | auto profile = std::make_shared(); 96 | profile->m_id = row.GetInt32(0); 97 | profile->m_name = row.GetStdString(1); 98 | profile->m_desc = row.GetStdString(2); 99 | profile->m_isActive = row.GetBool(3); 100 | profile->m_settingsPackId = row.GetInt32(4); 101 | profile->m_settingsPackName = row.GetStdString(5); 102 | 103 | if (!row.IsNull(6)) 104 | { 105 | profile->m_proxyId = row.GetInt32(6); 106 | } 107 | 108 | if (!row.IsNull(7)) 109 | { 110 | profile->m_proxyName = row.GetStdString(7); 111 | } 112 | 113 | return std::move(profile); 114 | } 115 | -------------------------------------------------------------------------------- /client/src/scss/components/_toast.scss: -------------------------------------------------------------------------------- 1 | .toast { 2 | background : var(--state-color,var(--toast-bg)); 3 | color : var(--toast-color); 4 | position : fixed; 5 | display : flex; 6 | flex-direction : column; 7 | bottom : $toast-offset; 8 | left : 50%; 9 | width : 100%; 10 | max-width : $toast-max-width; 11 | padding : 0; 12 | transform : translateX(-50%); 13 | backdrop-filter: blur(6px) saturate(1.5) brightness(1.5); 14 | border : $toast-border; 15 | box-shadow : var(--toast-box-shadow); 16 | border-radius : $toast-border-radius; 17 | z-index : $toast-zindex; 18 | user-select : none; 19 | animation : fade-in 0.75s forwards; 20 | .header { 21 | position : relative; 22 | display : flex; 23 | align-items : center; 24 | margin : $toast-header-margin; 25 | padding : $toast-header-padding; 26 | border-radius : $toast-header-border-radius; 27 | background : rgba($dark, 0.25); 28 | color : var(--body-color-highlight); 29 | min-height : 30px; 30 | font-weight : bold; 31 | line-height : 0; 32 | backdrop-filter: saturate(1) contrast(1.2); 33 | transition : all 0.35s ease; 34 | .icon { 35 | display : flex; 36 | align-items : center; 37 | justify-content: center; 38 | width : 20px; 39 | height : 20px; 40 | font-size : 1rem; 41 | line-height : 0; 42 | margin-right : 0.35rem; 43 | color : var(--bg-color); 44 | } 45 | .close { 46 | margin-left : auto; 47 | padding : .25rem; 48 | border-radius: 100%; 49 | background : transparent; 50 | color : var(--bg-color); 51 | opacity : 0.75; 52 | transition : all 0.35s ease; 53 | .icon { 54 | margin: 0; 55 | } 56 | &:hover { 57 | opacity: 1; 58 | } 59 | } 60 | .title { 61 | color: var(--bg-color-highlight); 62 | } 63 | } 64 | // Toast: Body 65 | .body { 66 | margin : 0; 67 | padding : $toast-body-padding; 68 | font-size: 115%; 69 | transition: all .35s ease; 70 | } 71 | // Toast: Modifiers 72 | &.is-center { 73 | top : 50%; 74 | left : 50%; 75 | right : initial; 76 | bottom : initial; 77 | transform: translate(-50%, -50%); 78 | } 79 | &.is-left { 80 | left : $toast-offset; 81 | transform: translateX(0); 82 | } 83 | &.is-right { 84 | left : initial; 85 | right : $toast-offset; 86 | transform: translateX(0); 87 | } 88 | &.is-top { 89 | top : $toast-offset; 90 | bottom : initial; 91 | transform: translate(-50%, 100%); 92 | &.is-left, 93 | &.is-right { 94 | transform: translate(0, 100%); 95 | } 96 | } 97 | &.primary { 98 | .header { 99 | color: lighten($primary, 50%); 100 | } 101 | .body { 102 | color: lighten($primary, 45%); 103 | } 104 | } 105 | // States 106 | // State: Sucess 107 | &.success { 108 | .header { 109 | color: lighten($green, 55%); 110 | } 111 | .body { 112 | color: lighten($green, 50%); 113 | } 114 | } 115 | // State: Error 116 | &.error { 117 | .header { 118 | color: lighten($red, 50%); 119 | } 120 | .body { 121 | color: lighten($red, 45%); 122 | } 123 | } 124 | // State: Warning 125 | &.warning { 126 | .header { 127 | color: darken($yellow, 47.5%); 128 | } 129 | .body { 130 | color: darken($yellow, 40%); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /client/src/scss/_utilities.scss: -------------------------------------------------------------------------------- 1 | // Utilities 2 | .mt-0 { 3 | margin-top: 0rem !important; 4 | } 5 | .mt-1 { 6 | margin-top: 1rem !important; 7 | } 8 | .mt-2 { 9 | margin-top: 2rem !important; 10 | } 11 | .mt-3 { 12 | margin-top: 3rem !important; 13 | } 14 | .mb-0 { 15 | margin-bottom: 0rem !important; 16 | } 17 | .mb-1 { 18 | margin-bottom: 1rem !important; 19 | } 20 | .mb-2 { 21 | margin-bottom: 2rem !important; 22 | } 23 | .mb-3 { 24 | margin-bottom: 3rem !important; 25 | } 26 | .mr-1 { 27 | margin-right: 1rem !important; 28 | } 29 | .mr-2 { 30 | margin-right: 2rem !important; 31 | } 32 | .mr-3 { 33 | margin-right: 3rem !important; 34 | } 35 | .ml-1 { 36 | margin-left: 1rem !important; 37 | } 38 | .ml-2 { 39 | margin-left: 2rem !important; 40 | } 41 | .ml-3 { 42 | margin-left: 3rem !important; 43 | } 44 | 45 | // Animations 46 | // Animation: Opening / Fade-in 47 | .is-opening, 48 | .fade-in { 49 | animation: fade-in 0.5s forwards; 50 | } 51 | // Animation: Closing / Fade-out 52 | .is-closing, 53 | .fade-out { 54 | animation: fade-out 0.5s forwards; 55 | } 56 | 57 | // States 58 | // State: Blue / Info / Seeding 59 | .is-blue, 60 | .info, 61 | .seeding { 62 | --state-color: var(--blue); 63 | --state-color-hover: hsla(var(--blue-hsl), var(--alpha-hover)); 64 | } 65 | // State: Primary / Downloading 66 | .is-primary, 67 | .downloading { 68 | --state-hsla: hsla(var(--primary-hsl), var(--alpha, 0.10)); 69 | --state-color: var(--primary); 70 | --state-color-hover: hsla(var(--primary-hsl), var(--alpha-hover)); 71 | } 72 | // State: Green / Success / Completed 73 | .is-green, 74 | .success, 75 | .completed { 76 | --state-color: var(--green); 77 | --state-color-hover: hsla(var(--green-hsl), var(--alpha-hover)); 78 | } 79 | // State: Yellow / Warning / Paused 80 | .is-yellow, 81 | .warning, 82 | .paused { 83 | --state-color: var(--yellow); 84 | --state-color-hover: hsla(var(--yellow-hsl), var(--alpha-hover)); 85 | } 86 | // State: Orange / Stopped 87 | .is-orange, 88 | .stopped { 89 | --state-color: var(--orange); 90 | --state-color-hover: hsla(var(--orange-hsl), var(--alpha-hover)); 91 | } 92 | // State: Red / Danger / Error 93 | .is-red, 94 | .danger, 95 | .error { 96 | --state-color: var(--red); 97 | --state-color-hover: hsla(var(--red-hsl), var(--alpha-hover)); 98 | } 99 | // State: Gray / Waiting 100 | .is-gray, 101 | .waiting { 102 | --state-color: var(--gray); 103 | --state-color-hover: hsla(var(--gray-hsl), var(--alpha-hover)); 104 | } 105 | 106 | // Badge 107 | .badge { 108 | background: var(--separator-color); 109 | color: var(--white); 110 | font-size: 70%; 111 | border-radius: 3px; 112 | padding: 0.25em 0.5em; 113 | margin-left: .5rem; 114 | margin-bottom: -2px; 115 | } 116 | 117 | // Backdrop 118 | .backdrop { 119 | @if $ui-enable-backdrop { 120 | background : var(--backdrop-bg); 121 | backdrop-filter: var(--backdrop-filter); 122 | } 123 | } 124 | 125 | // Hide 126 | .hide { 127 | display: none; 128 | } 129 | 130 | // Separator 131 | hr { 132 | border: 0; 133 | border-bottom: var(--separator); 134 | } 135 | 136 | .separator { 137 | display : block; 138 | height : 100%; 139 | border-right: var(--separator); 140 | @media (max-width: $breakpoint-sm) { 141 | display: none; 142 | } 143 | } 144 | 145 | // ARIA 146 | @mixin aria-ready() { 147 | position: absolute!important; 148 | width: 1px!important; 149 | height: 1px!important; 150 | padding: 0!important; 151 | margin: -1px!important; 152 | overflow: hidden!important; 153 | clip: rect(0,0,0,0)!important; 154 | white-space: nowrap!important; 155 | border: 0!important; 156 | } 157 | 158 | .aria, 159 | .aria-focusable:not(:focus):not(:focus-within) { 160 | @include aria-ready(); 161 | } 162 | 163 | .item-aria { 164 | position: relative; 165 | .item-label { 166 | position: absolute; 167 | top: 0; 168 | left: 0; 169 | right: 0; 170 | bottom: 0; 171 | font-size: 0; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /client/src/pages/settings/connection/Index.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 122 | -------------------------------------------------------------------------------- /src/options.cpp: -------------------------------------------------------------------------------- 1 | #include "options.hpp" 2 | 3 | #include 4 | 5 | namespace fs = std::filesystem; 6 | namespace po = boost::program_options; 7 | 8 | using pt::Server::Options; 9 | 10 | std::shared_ptr Options::Load(int argc, char* argv[]) 11 | { 12 | po::options_description desc("Allowed options"); 13 | desc.add_options() 14 | ("http-addr", po::value(), "set the http server address") 15 | ("http-port", po::value(), "set the http server port") 16 | ("log-level", po::value(), "set log level") 17 | // InfluxDb options 18 | ("influxdb-host", po::value(), "set the InfluxDb host") 19 | ("influxdb-port", po::value(), "set the InfluxDb port") 20 | ("influxdb-bucket", po::value(), "set the InfluxDb bucket") 21 | ("influxdb-org", po::value(), "set the InfluxDb organization") 22 | ("influxdb-token", po::value(), "set the InfluxDb auth. token") 23 | // Prometheus options 24 | ("prometheus-exporter", "enable the Prometheus metrics exporter") 25 | ; 26 | 27 | po::variables_map vm; 28 | po::store(po::parse_command_line(argc, argv, desc), vm); 29 | po::notify(vm); 30 | 31 | auto opts = std::make_shared(); 32 | opts->m_databaseFilePath = fs::path(argv[0]).parent_path() / "PicoTorrent.sqlite"; 33 | opts->m_host = "127.0.0.1"; 34 | opts->m_port = 1337; 35 | opts->m_logLevel = boost::log::trivial::info; 36 | opts->m_webRoot = nullptr; 37 | 38 | if (const char* dbPath = std::getenv("PICOTORRENT_DB_FILE")) 39 | { 40 | opts->m_databaseFilePath = fs::path(dbPath); 41 | } 42 | 43 | if (const char* httpHost = std::getenv("PICOTORRENT_HTTP_HOST")) 44 | { 45 | opts->m_host = httpHost; 46 | } 47 | 48 | if (const char* httpPort = std::getenv("PICOTORRENT_HTTP_PORT")) 49 | { 50 | opts->m_port = std::stoi(httpPort); 51 | } 52 | 53 | if (const char* webRoot = std::getenv("PICOTORRENT_WEBROOT_PATH")) 54 | { 55 | opts->m_webRoot = std::make_shared(webRoot); 56 | } 57 | 58 | if (vm.count("http-addr")) { opts->m_host = vm["http-addr"].as(); } 59 | if (vm.count("http-port")) { opts->m_port = vm["http-port"].as(); } 60 | 61 | if (vm.count("log-level")) 62 | { 63 | std::string level = vm["log-level"].as(); 64 | if (level == "trace") { opts->m_logLevel = boost::log::trivial::trace; } 65 | if (level == "debug") { opts->m_logLevel = boost::log::trivial::debug; } 66 | if (level == "info") { opts->m_logLevel = boost::log::trivial::info; } 67 | if (level == "warning") { opts->m_logLevel = boost::log::trivial::warning; } 68 | if (level == "error") { opts->m_logLevel = boost::log::trivial::error; } 69 | if (level == "fatal") { opts->m_logLevel = boost::log::trivial::fatal; } 70 | } 71 | 72 | // InfluxDb setup 73 | if (auto val = std::getenv("PICOTORRENT_INFLUXDB_HOST")) { opts->m_influxHost = val; } 74 | if (auto val = std::getenv("PICOTORRENT_INFLUXDB_PORT")) { opts->m_influxPort = std::stoi(val); } 75 | if (auto val = std::getenv("PICOTORRENT_INFLUXDB_BUCKET")) { opts->m_influxBucket = val; } 76 | if (auto val = std::getenv("PICOTORRENT_INFLUXDB_ORG")) { opts->m_influxOrg = val; } 77 | if (auto val = std::getenv("PICOTORRENT_INFLUXDB_TOKEN")) { opts->m_influxToken = val; } 78 | 79 | if (vm.count("influxdb-host")) { opts->m_influxHost = vm["influxdb-host"].as(); } 80 | if (vm.count("influxdb-port")) { opts->m_influxPort = vm["influxdb-port"].as(); } 81 | if (vm.count("influxdb-bucket")) { opts->m_influxBucket = vm["influxdb-bucket"].as(); } 82 | if (vm.count("influxdb-org")) { opts->m_influxOrg = vm["influxdb-org"].as(); } 83 | if (vm.count("influxdb-token")) { opts->m_influxToken = vm["influxdb-token"].as(); } 84 | 85 | if (std::getenv("PICOTORRENT_PROMETHEUS_EXPORTER")) { opts->m_prometheusEnabled = true; } 86 | if (vm.count("prometheus-exporter")) { opts->m_prometheusEnabled = true; } 87 | 88 | return opts; 89 | } 90 | -------------------------------------------------------------------------------- /client/src/scss/components/_header.scss: -------------------------------------------------------------------------------- 1 | 2 | // Header 3 | header { 4 | display : flex; 5 | width : 100%; 6 | align-items : stretch; 7 | background : var(--header-bg); 8 | flex-wrap : wrap; 9 | backdrop-filter: var(--backdrop-filter); 10 | animation : fade-in 0.35s forwards; 11 | z-index : 999; 12 | // Logo 13 | .logo { 14 | display : flex; 15 | align-items : center; 16 | justify-content: center; 17 | max-width : 35px; 18 | height : 100%; 19 | padding : 0; 20 | margin : 0 0.65rem; 21 | color : var(--header-color); 22 | transition : all 0.35s ease; 23 | svg, 24 | img { 25 | width : 22px; 26 | height : 22px; 27 | object-fit : contain; 28 | object-position: center; 29 | } 30 | &:hover { 31 | transform: scale(1.1); 32 | } 33 | } 34 | 35 | // Divisions 36 | .left, 37 | .center, 38 | .right { 39 | display: flex; 40 | flex : 1; 41 | } 42 | 43 | // Center 44 | .center { 45 | flex : 1 1 auto; 46 | justify-content: center; 47 | align-self : center; 48 | @media (max-width: $breakpoint-md) { 49 | order: 2; 50 | flex : 1 1 100%; 51 | } 52 | } 53 | 54 | // Right 55 | .right { 56 | justify-content: flex-end; 57 | } 58 | 59 | // Items 60 | .item { 61 | display : flex; 62 | align-items : center; 63 | padding : 1rem; 64 | text-decoration: none; 65 | color : var(--header-color); 66 | font-weight : 500; 67 | transition : all 0.35s ease; 68 | line-height : 0; 69 | background : transparent; 70 | border : 0; 71 | flex : 0 0 auto; 72 | font-size : 0.85rem; 73 | &.control { 74 | padding-left : 0.65rem; 75 | padding-right: 0.65rem; 76 | } 77 | &:hover { 78 | color : $white; 79 | background: rgba(255, 255, 255,0.15); 80 | } 81 | span { 82 | margin-top : -2px; 83 | margin-left: .5rem; 84 | display : none; 85 | @media (min-width: $breakpoint-sm) { 86 | display: block; 87 | } 88 | } 89 | &.icon { 90 | .title { 91 | min-width: 85px; 92 | } 93 | } 94 | &.tooltip { 95 | .title { 96 | @media (min-width: $breakpoint-sm) { 97 | display: none !important; 98 | } 99 | } 100 | 101 | } 102 | } 103 | 104 | // Icons 105 | .bi { 106 | font-size : 15px; 107 | line-height: 0; 108 | transition : all 0.35s ease; 109 | @media (min-width: 350px) { 110 | font-size : 20px; 111 | } 112 | } 113 | 114 | // Filter 115 | .filter { 116 | padding : 0 1rem; 117 | display : flex; 118 | align-items : center; 119 | background : var(--header-filter-bg); 120 | width : 100%; 121 | margin : 0.5rem; 122 | border-radius: var(--header-filter-border-radius); 123 | box-shadow : 0 0.25rem .5rem -.25rem rgba($black,0.25); 124 | @media (min-width: $breakpoint-md) { 125 | max-width: calc(100% - 100px); 126 | } 127 | @media (min-width: $breakpoint-md + 200px) { 128 | max-width: 400px; 129 | } 130 | @media (min-width: $breakpoint-lg) { 131 | max-width: 600px; 132 | } 133 | .bi { 134 | display : flex; 135 | align-items : center; 136 | color : var(--header-filter-color); 137 | margin-bottom: 0; 138 | } 139 | label { 140 | width : 0; 141 | height : 0; 142 | overflow: hidden; 143 | } 144 | input { 145 | display : block; 146 | width : 100%; 147 | min-width : auto; 148 | margin : 0; 149 | border : 0; 150 | outline : 0; 151 | background : transparent; 152 | transition : all 0.35s ease; 153 | padding-top : 0.50rem; 154 | padding-bottom: 0.50rem; 155 | color : var(--header-filter-color); 156 | box-shadow : none; 157 | &::placeholder { 158 | color: var(--header-filter-color); 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /client/src/scss/pages/_settings.scss: -------------------------------------------------------------------------------- 1 | // Page: Settings 2 | main.settings { 3 | flex-direction: column; 4 | @media (min-width: $breakpoint-sm) { 5 | flex-direction: row; 6 | } 7 | .sidebar { 8 | display : flex; 9 | flex-direction: column; 10 | padding : 1rem 1rem 0; 11 | flex : 0 0 auto; 12 | background : var(--sidebar-bg); 13 | @media (min-width: $breakpoint-sm) { 14 | padding : 1rem; 15 | flex : 1 1 175px; 16 | max-width: 200px; 17 | } 18 | .title { 19 | order : 1; 20 | margin : 1rem 0 0; 21 | display: none; 22 | @media (min-width: $breakpoint-sm) { 23 | display : block; 24 | font-size: 1.35rem; 25 | order : 0; 26 | margin : 0 0 1rem; 27 | } 28 | } 29 | .menu { 30 | display : flex; 31 | flex-direction : row; 32 | flex-wrap : wrap; 33 | justify-content: space-between; 34 | order : 0; 35 | @media (min-width: $breakpoint-sm) { 36 | flex-direction: column; 37 | order : 1; 38 | } 39 | .item { 40 | margin : 0 0.25rem; 41 | padding : 0.75rem 1rem; 42 | display : flex; 43 | align-items : center; 44 | justify-content: center; 45 | font-size : 110%; 46 | font-weight : 500; 47 | text-decoration: none; 48 | color : var(--body-color); 49 | flex : 1; 50 | border-radius : .25rem; 51 | position : relative; 52 | @media (min-width: $breakpoint-sm) { 53 | flex : 1; 54 | border-radius : 0rem; 55 | margin : 0 0 0.5rem; 56 | justify-content: flex-start; 57 | } 58 | &:first-child { 59 | margin-left: 0; 60 | } 61 | &:last-child { 62 | margin-right: 0; 63 | } 64 | &.active { 65 | color: var(--white); 66 | &::before { 67 | content: ""; 68 | position: absolute; 69 | top: inherit; 70 | left: 20%; 71 | right: 20%; 72 | bottom: 2px; 73 | height: 4px; 74 | background: var(--primary); 75 | border-radius: 1rem; 76 | animation: fade-in 0.5s forwards; 77 | } 78 | @media (min-width: $breakpoint-sm) { 79 | color: var(--body-color); 80 | &::before { 81 | top: 5px; 82 | left: 3px; 83 | bottom: 5px; 84 | right: inherit; 85 | height: inherit; 86 | width: 3px; 87 | border-bottom: 0; 88 | border-left: 4px solid var(--primary); 89 | } 90 | } 91 | } 92 | &:hover { 93 | background: var(--separator-color); 94 | border-radius: .25rem; 95 | } 96 | // Icons 97 | .bi { 98 | font-size: 145%; 99 | line-height: 0; 100 | transition: all 0.35s ease; 101 | @media (min-width: $breakpoint-sm) { 102 | margin-bottom: -2px; 103 | margin-right: 0.75rem; 104 | } 105 | } 106 | // Text 107 | span { 108 | display: none; 109 | @media (min-width: $breakpoint-sm) { 110 | display: inline; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | // Content 117 | .content { 118 | flex: 1 1 100%; 119 | @media (min-width: $breakpoint-sm) { 120 | flex: 1; 121 | } 122 | .title { 123 | margin: 0; 124 | font-size: 1.25rem; 125 | } 126 | hr { 127 | margin: 1.5rem 0; 128 | } 129 | h3 { 130 | margin-bottom: 1rem; 131 | padding-bottom: 1rem; 132 | border-bottom: var(--separator); 133 | } 134 | h4 { 135 | margin-top: 1rem; 136 | } 137 | h5 { 138 | font-weight: 500; 139 | margin: 1.75rem 0 1rem; 140 | &:first-child { 141 | margin-top: 0 !important; 142 | } 143 | } 144 | .sub-content { 145 | h5 { 146 | margin-top: 1.75rem !important; 147 | } 148 | } 149 | } 150 | .settings-common, 151 | .settings-downloads { 152 | .group { 153 | .item { 154 | flex: 1; 155 | max-width: 450px 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /client/src/pages/settings/Proxy.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 127 | -------------------------------------------------------------------------------- /src/rpc/settingspackupdate.cpp: -------------------------------------------------------------------------------- 1 | #include "settingspackupdate.hpp" 2 | 3 | #include 4 | 5 | #include "../data/transaction.hpp" 6 | #include "../data/models/profile.hpp" 7 | #include "../data/models/settingspack.hpp" 8 | #include "../sessionmanager.hpp" 9 | 10 | using json = nlohmann::json; 11 | using pt::Server::RPC::SettingsPackUpdateCommand; 12 | using pt::Server::SessionManager; 13 | 14 | SettingsPackUpdateCommand::SettingsPackUpdateCommand( 15 | sqlite3* db, 16 | std::shared_ptr const& session) 17 | : m_db(db), 18 | m_session(session) 19 | { 20 | } 21 | 22 | json SettingsPackUpdateCommand::Execute(const json& params) 23 | { 24 | Data::Transaction tx(m_db); 25 | 26 | try 27 | { 28 | int id = params[0]; 29 | bool updated_settings = false; 30 | 31 | if (params[1].contains("name")) 32 | { 33 | auto name = params[1]["name"].get(); 34 | 35 | sqlite3_stmt* stmt; 36 | sqlite3_prepare_v2(m_db, "UPDATE settings_pack SET name = $1 where id = $2", -1, &stmt, nullptr); 37 | sqlite3_bind_text(stmt, 1, name.c_str(), static_cast(name.size()), SQLITE_TRANSIENT); 38 | sqlite3_bind_int(stmt, 2, id); 39 | sqlite3_step(stmt); 40 | sqlite3_finalize(stmt); 41 | } 42 | 43 | if (params[1].contains("description")) 44 | { 45 | auto desc = params[1]["description"].get(); 46 | 47 | sqlite3_stmt* stmt; 48 | sqlite3_prepare_v2(m_db, "UPDATE settings_pack SET description = $1 where id = $2", -1, &stmt, nullptr); 49 | sqlite3_bind_text(stmt, 1, desc.c_str(), static_cast(desc.size()), SQLITE_TRANSIENT); 50 | sqlite3_bind_int(stmt, 2, id); 51 | sqlite3_step(stmt); 52 | sqlite3_finalize(stmt); 53 | } 54 | 55 | if (params[1].contains("settings")) 56 | { 57 | auto settings = params[1]["settings"]; 58 | 59 | for (auto& [key, value] : settings.items()) 60 | { 61 | if (Data::SettingsPack::Names().find(key) == Data::SettingsPack::Names().end()) 62 | { 63 | BOOST_LOG_TRIVIAL(warning) << "Unknown settings key: " << key; 64 | continue; 65 | } 66 | 67 | int setting = lt::setting_by_name(key); 68 | 69 | if (setting < 0) 70 | { 71 | BOOST_LOG_TRIVIAL(warning) << "Unknown libtorrent setting: " << key; 72 | continue; 73 | } 74 | 75 | std::string query = "UPDATE settings_pack SET " + key + " = $1 WHERE id = $2"; 76 | 77 | sqlite3_stmt* stmt; 78 | sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, nullptr); 79 | 80 | if (setting >= lt::settings_pack::string_type_base 81 | && setting < lt::settings_pack::max_string_setting_internal) 82 | { 83 | sqlite3_bind_text(stmt, 1, value.get().c_str(), -1, SQLITE_TRANSIENT); 84 | } 85 | else if (setting >= lt::settings_pack::int_type_base 86 | && setting < lt::settings_pack::max_int_setting_internal) 87 | { 88 | sqlite3_bind_int(stmt, 1, value); 89 | } 90 | else if (setting >= lt::settings_pack::bool_type_base 91 | && setting < lt::settings_pack::max_bool_setting_internal) 92 | { 93 | sqlite3_bind_int(stmt, 1, value.get() ? 1 : 0); 94 | } 95 | 96 | sqlite3_bind_int(stmt, 2, id); 97 | sqlite3_step(stmt); 98 | sqlite3_finalize(stmt); 99 | } 100 | 101 | updated_settings = true; 102 | } 103 | 104 | tx.Commit(); 105 | 106 | if (updated_settings) 107 | { 108 | auto activeProfile = Data::Models::Profile::GetActive(m_db); 109 | 110 | if (activeProfile != nullptr 111 | && activeProfile->SettingsPackId() == id) 112 | { 113 | BOOST_LOG_TRIVIAL(info) << "Settings for active profile changed - reloading session"; 114 | m_session->ReloadSettings(); 115 | } 116 | } 117 | } 118 | catch (std::exception const& ex) 119 | { 120 | tx.Rollback(); 121 | throw ex; 122 | } 123 | 124 | return Ok(true); 125 | } 126 | -------------------------------------------------------------------------------- /client/src/scss/components/_torrent-list.scss: -------------------------------------------------------------------------------- 1 | 2 | // Torrent List 3 | .torrent-list { 4 | display : block; 5 | width : 100%; 6 | height : var(--torrents-table-height); 7 | overflow-x : auto; 8 | -webkit-overflow-scrolling: touch; 9 | -ms-overflow-style : -ms-autohiding-scrollbar; 10 | table { 11 | min-width: 750px; 12 | // Table: Head and Cell 13 | th, 14 | td { 15 | // Torrent: Number 16 | &.id { 17 | width: 30px; 18 | } 19 | // Icons 20 | .bi { 21 | font-size : 125%; 22 | line-height : 0; 23 | margin-right: 0.50rem; 24 | display : var(--torrents-table-icons, 'inline'); 25 | &.bi-hourglass { 26 | font-size : 110%; 27 | margin-left: 1px; 28 | } 29 | } 30 | // Progress-bar 31 | progress, 32 | .progress { 33 | span { 34 | display: var(--torrents-table-progress-text, 'inline'); 35 | } 36 | } 37 | // Table: Check 38 | &.checkbox { 39 | width: 30px; 40 | .check, 41 | input { 42 | margin: 0; 43 | } 44 | } 45 | // Table: Actions 46 | &.actions { 47 | button { 48 | padding: 0.25rem; 49 | .bi { 50 | margin : 0; 51 | font-size: .85rem; 52 | } 53 | } 54 | } 55 | } 56 | // Table: Head 57 | th { 58 | font-size : 85%; 59 | letter-spacing: 1px; 60 | &.actions { 61 | font-size: 0; 62 | } 63 | } 64 | // Table: Row 65 | tr { 66 | border-bottom: 0; 67 | transition : all 0.35s ease; 68 | } 69 | // Table Body 70 | tbody tr { 71 | --alpha: 0.10; 72 | @media (max-width: $breakpoint-sm) { 73 | background: var(--state-hsla, transparent); 74 | } 75 | td { 76 | &.name { 77 | .icon-status { 78 | @media (min-width: $breakpoint-sm) { 79 | display: none; 80 | } 81 | } 82 | } 83 | .icon-status { 84 | display: inline-block; 85 | font-family: bootstrap-icons !important; 86 | font-style: normal; 87 | font-weight: normal !important; 88 | font-variant: normal; 89 | text-transform: none; 90 | line-height: 1; 91 | vertical-align: text-bottom; 92 | -webkit-font-smoothing: antialiased; 93 | -moz-osx-font-smoothing: grayscale; 94 | &::before { 95 | content: "\f4de"; 96 | } 97 | } 98 | } 99 | &:hover { 100 | --alpha: 0.15; 101 | background: var(--state-hsla, var(--separator-color)); 102 | } 103 | // Torrent status 104 | @each $status, $icon in $torrent-list-status { 105 | &.#{$status} .icon-status::before { 106 | content: "#{$icon}"; 107 | } 108 | } 109 | // For bigger than mobile 110 | @media (min-width: $breakpoint-sm) { 111 | color: var(--state-color); 112 | } 113 | } 114 | // Bar 115 | progress, 116 | .progress { 117 | &::-webkit-progress-value, 118 | .bar { 119 | background: var(--state-color, var(--primary)); 120 | } 121 | } 122 | // Mobile 123 | @media (max-width: $breakpoint-sm) { 124 | min-width: auto; 125 | thead { 126 | display: none; 127 | } 128 | tbody { 129 | display : flex; 130 | flex : 1 1 100%; 131 | flex-wrap : wrap; 132 | flex-direction: column; 133 | tr { 134 | display : flex; 135 | flex-wrap : wrap; 136 | padding : 0.5rem 0; 137 | border-bottom: var(--separator); 138 | td { 139 | display : flex; 140 | flex : 1 0 auto; 141 | align-items: center; 142 | // Torrent Number 143 | &.checkbox { 144 | display: none; 145 | } 146 | // Torrent Name 147 | &.name { 148 | padding-top: 0; 149 | order : 0; 150 | flex : 1 1 100%; 151 | font-size : 115%; 152 | font-weight: 600; 153 | border : 0; 154 | } 155 | // Torrent Prgoress 156 | &.progress { 157 | order : 10; 158 | flex : 1 1 100%; 159 | border: 0; 160 | progress, 161 | .progress { 162 | width : 100%; 163 | height: 10px; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src/http/httpsession.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace pt::Server { class SessionManager; } 17 | namespace pt::Server::RPC { class Command; } 18 | 19 | namespace pt::Server::Http 20 | { 21 | class HttpRequestHandler; 22 | 23 | class HttpSession : public std::enable_shared_from_this 24 | { 25 | class DefaultContext; 26 | 27 | class Queue 28 | { 29 | enum 30 | { 31 | // Maximum number of responses we will queue 32 | Limit = 8 33 | }; 34 | 35 | // The type-erased, saved work item 36 | struct Work 37 | { 38 | virtual ~Work() = default; 39 | virtual void operator()() = 0; 40 | }; 41 | 42 | HttpSession& m_self; 43 | std::vector> m_items; 44 | 45 | public: 46 | explicit Queue(HttpSession& self) 47 | : m_self(self) 48 | { 49 | static_assert(Limit > 0, "queue limit must be positive"); 50 | m_items.reserve(Limit); 51 | } 52 | 53 | // Returns `true` if we have reached the queue limit 54 | bool IsFull() const 55 | { 56 | return m_items.size() >= Limit; 57 | } 58 | 59 | // Called when a message finishes sending 60 | // Returns `true` if the caller should initiate a read 61 | bool OnWrite() 62 | { 63 | BOOST_ASSERT(!m_items.empty()); 64 | auto const wasFull = IsFull(); 65 | m_items.erase(m_items.begin()); 66 | if(! m_items.empty()) 67 | (*m_items.front())(); 68 | return wasFull; 69 | } 70 | 71 | template 72 | void operator()(boost::beast::http::message&& msg) 73 | { 74 | // This holds a work item 75 | struct WorkImpl : Work 76 | { 77 | HttpSession& m_self; 78 | boost::beast::http::message m_msg; 79 | 80 | WorkImpl( 81 | HttpSession& self, 82 | boost::beast::http::message&& msg) 83 | : m_self(self) 84 | , m_msg(std::move(msg)) 85 | { 86 | } 87 | 88 | void operator()() 89 | { 90 | boost::beast::http::async_write( 91 | m_self.m_stream, 92 | m_msg, 93 | boost::beast::bind_front_handler( 94 | &HttpSession::EndWrite, 95 | m_self.shared_from_this(), 96 | m_msg.need_eof())); 97 | } 98 | }; 99 | 100 | // Allocate and store the work 101 | m_items.push_back(std::make_unique(m_self, std::move(msg))); 102 | 103 | // If there was no previous work, start this one 104 | if(m_items.size() == 1) 105 | (*m_items.front())(); 106 | } 107 | }; 108 | 109 | public: 110 | HttpSession( 111 | boost::asio::ip::tcp::socket&& socket, 112 | std::shared_ptr, std::shared_ptr>> handlers, 113 | std::shared_ptr docroot); 114 | 115 | void Run(); 116 | 117 | private: 118 | void BeginRead(); 119 | void EndRead(boost::beast::error_code ec, std::size_t bytes_transferred); 120 | void EndWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred); 121 | void BeginClose(); 122 | 123 | boost::beast::tcp_stream m_stream; 124 | boost::beast::flat_buffer m_buffer; 125 | std::shared_ptr m_docroot; 126 | std::shared_ptr, std::shared_ptr>> m_handlers; 127 | 128 | Queue m_queue; 129 | 130 | // The parser is stored in an optional container so we can 131 | // construct it from scratch it at the beginning of each new message. 132 | boost::optional> m_parser; 133 | }; 134 | } 135 | --------------------------------------------------------------------------------