Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple libtorrent sessions #226

Merged
merged 10 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ add_library(
src/cmdargs.cpp
src/config.cpp
src/logger.cpp
src/session.cpp
src/sessions.cpp
src/uri.cpp
src/utils/eta.cpp
src/utils/secretkey.cpp
Expand All @@ -124,6 +124,7 @@ add_library(
src/data/migrations/0006_clientdata.cpp
src/data/migrations/0007_removesessionsettings.cpp
src/data/migrations/0008_plugins.cpp
src/data/migrations/0009_multisessions.cpp
src/data/models/addtorrentparams.cpp
src/data/models/users.cpp
src/data/statement.cpp
Expand Down Expand Up @@ -161,10 +162,11 @@ add_library(
src/methods/plugins/pluginsreload.cpp
src/methods/plugins/pluginsuninstall.cpp
src/methods/plugins/pluginsupdate.cpp
src/methods/sessions/sessionslist.cpp
src/methods/sessions/sessionspause.cpp
src/methods/sessions/sessionsresume.cpp
src/methods/sessions/sessionssettingslist.cpp
src/methods/presetslist.cpp
src/methods/sessionpause.cpp
src/methods/sessionresume.cpp
src/methods/sessionsettingslist.cpp
src/methods/sysversions.cpp
src/methods/torrentsadd.cpp
src/methods/torrentsfileslist.cpp
Expand Down Expand Up @@ -236,7 +238,6 @@ target_link_libraries(

add_executable(
${PROJECT_NAME}_tests
tests/inmemorysession.cpp
tests/main.cpp
tests/query/pql.cpp
tests/utils/string.cpp
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,23 @@ and _extensible_.

### Features

* [User-defined workflows](https://porla.org/scripting/getting_started).
* Support for both BitTorrent v1 and v2.
* Based on libtorrent with support for both BitTorrent v1 and v2.
* Lua API for writing [plugins and workflows](https://porla.org/plugins/getting_started).
* Supports multiple, distinct sessions with different settings.
* Embedded query language to find torrents. Fast.
* [HTTP API](https://porla.org/api/auth) with JWT auth.
* Modern web UI.

#### Workflows
#### Plugins and workflows

Workflows can be used to automate and integrate Porla with all types of
applications and services, such as Discord and [ntfy.sh](https://ntfy.sh). They
are inspired by GitHub Actions.
The Lua API can be used to automate and integrate Porla with all types of
applications and services, such as Discord and [ntfy.sh](https://ntfy.sh).

#### Multiple sessions

You can easily set up multiple sessions to separate your public torrents from
your private torrents, for example. Each session binds to its own network
device and port pair, and can have custom rate limits, queueing rules, etc.

#### The Porla query language (PQL)

Expand Down
2 changes: 1 addition & 1 deletion html/src/components/SettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function SettingsForm(props: SettingsFormProps) {
}

export default function SettingsDrawer(props: SettingsDrawerProps) {
const { data, error, mutate } = useRPC<ISettingsList>("session.settings.list", {
const { data, error, mutate } = useRPC<ISettingsList>("sessions.settings.list", {
keys: [
"active_checking",
"active_downloads",
Expand Down
20 changes: 19 additions & 1 deletion html/src/components/TorrentsListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, CircularProgress, CircularProgressLabel, Flex, Grid, GridItem, HStack, Icon, IconButton, Link, Menu, MenuButton, MenuDivider, MenuGroup, MenuItem, MenuList, Text, textDecoration, useColorMode } from "@chakra-ui/react";
import { Badge, Box, CircularProgress, CircularProgressLabel, Flex, Grid, GridItem, HStack, Icon, IconButton, Link, Menu, MenuButton, MenuDivider, MenuGroup, MenuItem, MenuList, Text, textDecoration, useColorMode } from "@chakra-ui/react";
import { filesize } from "filesize";
import { IconType } from "react-icons/lib";
import { MdCheck, MdDelete, MdDriveFileMove, MdFileCopy, MdFolder, MdFolderOpen, MdLabel, MdOutlineMoreVert, MdOutlineReport, MdPause, MdPlayArrow, MdSchedule, MdSearch, MdTag, MdUpload, MdViewList } from "react-icons/md";
Expand All @@ -7,6 +7,18 @@ import { Torrent } from "../types"
import useNinja from "../contexts/ninja";
import useTorrentsFilter from "../contexts/TorrentsFilterContext";

function hashCode(str: string) {
let hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return hash;
}

function pickColor(str: string) {
return `hsl(${hashCode(str) % 360}, 100%, 80%)`;
}

type TorrentsListItemProps = {
index: number;
isDeleting: (torrent: Torrent) => boolean;
Expand Down Expand Up @@ -207,6 +219,12 @@ export default function TorrentsListItem(props: TorrentsListItemProps) {
fontSize={"sm"}
spacing={3}
>
<Badge
color={pickColor(props.torrent.session)}
>
{props.torrent.session}
</Badge>

<KeyValue
key={"savepath"}
icon={MdFolderOpen}
Expand Down
1 change: 1 addition & 0 deletions html/src/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type Torrent = {
progress: number;
queue_position: number;
save_path: string;
session: string;
ratio: number;
size: number;
state: number;
Expand Down
87 changes: 43 additions & 44 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
};

auto cfg = std::unique_ptr<Config>(new Config());
cfg->http_auth_enabled = true;
cfg->session_settings = lt::default_settings();
cfg->http_auth_enabled = true;
cfg->sessions.insert({ "default", lt::default_settings() });

// Check default locations for a config file.
for (auto const& path : config_file_search_paths)
Expand Down Expand Up @@ -94,9 +94,9 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
if (auto val = std::getenv("PORLA_SECRET_KEY")) cfg->secret_key = val;
if (auto val = std::getenv("PORLA_SESSION_SETTINGS_BASE"))
{
if (strcmp("default", val) == 0) cfg->session_settings = lt::default_settings();
if (strcmp("high_performance_seed", val) == 0) cfg->session_settings = lt::high_performance_seed();
if (strcmp("min_memory_usage", val) == 0) cfg->session_settings = lt::min_memory_usage();
if (strcmp("default", val) == 0) cfg->sessions.at("default") = lt::default_settings();
if (strcmp("high_performance_seed", val) == 0) cfg->sessions.at("default") = lt::high_performance_seed();
if (strcmp("min_memory_usage", val) == 0) cfg->sessions.at("default") = lt::min_memory_usage();
}
if (auto val = std::getenv("PORLA_SODIUM_MEMLIMIT"))
{
Expand Down Expand Up @@ -153,9 +153,9 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map

if (auto val = config_file_tbl["session_settings"]["base"].value<std::string>())
{
if (*val == "default") cfg->session_settings = lt::default_settings();
if (*val == "high_performance_seed") cfg->session_settings = lt::high_performance_seed();
if (*val == "min_memory_usage") cfg->session_settings = lt::min_memory_usage();
if (*val == "default") cfg->sessions.at("default") = lt::default_settings();
if (*val == "high_performance_seed") cfg->sessions.at("default") = lt::high_performance_seed();
if (*val == "min_memory_usage") cfg->sessions.at("default") = lt::min_memory_usage();
}

if (auto val = config_file_tbl["db"].value<std::string>())
Expand Down Expand Up @@ -206,12 +206,12 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map

if (!listen_val.empty())
{
cfg->session_settings.set_str(lt::settings_pack::listen_interfaces, listen_val.substr(1));
cfg->sessions.at("default").set_str(lt::settings_pack::listen_interfaces, listen_val.substr(1));
}

if (!outbound_val.empty())
{
cfg->session_settings.set_str(lt::settings_pack::outgoing_interfaces, outbound_val.substr(1));
cfg->sessions.at("default").set_str(lt::settings_pack::outgoing_interfaces, outbound_val.substr(1));
}
}

Expand Down Expand Up @@ -267,6 +267,9 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
if (auto val = value_tbl["save_path"].value<std::string>())
p.save_path = *val;

if (auto val = value_tbl["session"].value<std::string>())
p.session = *val;

if (auto val = value_tbl["storage_mode"].value<std::string>())
{
if (strcmp(val->c_str(), "allocate") == 0) p.storage_mode = lt::storage_mode_allocate;
Expand Down Expand Up @@ -296,63 +299,56 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
BOOST_LOG_TRIVIAL(info) << "Configuring session proxy";

if (const auto val = (*proxy_tbl)["host"].value<std::string>())
cfg->session_settings.set_str(lt::settings_pack::proxy_hostname, *val);
cfg->sessions.at("default").set_str(lt::settings_pack::proxy_hostname, *val);

if (const auto val = (*proxy_tbl)["port"].value<int>())
cfg->session_settings.set_int(lt::settings_pack::proxy_port, *val);
cfg->sessions.at("default").set_int(lt::settings_pack::proxy_port, *val);

if (const auto val = (*proxy_tbl)["type"].value<std::string>())
{
if (strcmp(val->c_str(), "socks4") == 0 || strcmp(val->c_str(), "SOCKS4") == 0)
cfg->session_settings.set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks4);
cfg->sessions.at("default").set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks4);

if (strcmp(val->c_str(), "socks5") == 0 || strcmp(val->c_str(), "SOCKS5") == 0)
cfg->session_settings.set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks5);
cfg->sessions.at("default").set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks5);
}

if (const auto val = (*proxy_tbl)["hostnames"].value<bool>())
cfg->session_settings.set_bool(lt::settings_pack::proxy_hostnames, *val);
cfg->sessions.at("default").set_bool(lt::settings_pack::proxy_hostnames, *val);

if (const auto val = (*proxy_tbl)["peer_connections"].value<bool>())
cfg->session_settings.set_bool(lt::settings_pack::proxy_peer_connections, *val);
cfg->sessions.at("default").set_bool(lt::settings_pack::proxy_peer_connections, *val);

if (const auto val = (*proxy_tbl)["tracker_connections"].value<bool>())
cfg->session_settings.set_bool(lt::settings_pack::proxy_tracker_connections, *val);
cfg->sessions.at("default").set_bool(lt::settings_pack::proxy_tracker_connections, *val);
}

if (auto val = config_file_tbl["secret_key"].value<std::string>())
cfg->secret_key = *val;

if (auto val = config_file_tbl["session_settings"]["extensions"].as_array())
{
std::vector<lt_plugin> extensions;
if (auto session_settings_tbl = config_file_tbl["session_settings"].as_table())
ApplySettings(*session_settings_tbl, cfg->sessions.at("default"));

for (auto const& item : *val)
// Load all sessions
if (const auto* sessions_tbl = config_file_tbl["sessions"].as_table())
{
for (const auto& [key, value] : *sessions_tbl)
{
if (auto const item_value = item.value<std::string>())
{
if (*item_value == "smart_ban")
extensions.emplace_back(&lt::create_smart_ban_plugin);
BOOST_LOG_TRIVIAL(debug) << "Loading configuration for session " << key;

if (*item_value == "ut_metadata")
extensions.emplace_back(&lt::create_ut_metadata_plugin);
cfg->sessions.insert({ key.data(), lt::default_settings() });

if (*item_value == "ut_pex")
extensions.emplace_back(&lt::create_ut_pex_plugin);
}
else
if (const auto* value_tbl = value.as_table())
{
BOOST_LOG_TRIVIAL(warning)
<< "Item in session_extension array is not a string (" << item.type() << ")";
if (const auto settings_tbl = (*value_tbl)["settings"].as_table())
{
BOOST_LOG_TRIVIAL(debug) << "Applying session settings for " << key;
ApplySettings(*settings_tbl, cfg->sessions.at(key.data()));
}
}
}

cfg->session_extensions = extensions;
}

if (auto session_settings_tbl = config_file_tbl["session_settings"].as_table())
ApplySettings(*session_settings_tbl, cfg->session_settings);

if (auto val = config_file_tbl["state_dir"].value<std::string>())
cfg->state_dir = *val;

Expand Down Expand Up @@ -409,9 +405,9 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
{
auto val = cmd["session-settings-base"].as<std::string>();

if (val == "default") cfg->session_settings = lt::default_settings();
if (val == "high_performance_seed") cfg->session_settings = lt::high_performance_seed();
if (val == "min_memory_usage") cfg->session_settings = lt::min_memory_usage();
if (val == "default") cfg->sessions.at("default") = lt::default_settings();
if (val == "high_performance_seed") cfg->sessions.at("default") = lt::high_performance_seed();
if (val == "min_memory_usage") cfg->sessions.at("default") = lt::min_memory_usage();
}
if (cmd.count("state-dir")) cfg->state_dir = cmd["state-dir"].as<std::string>();
if (cmd.count("timer-dht-stats")) cfg->timer_dht_stats = cmd["timer-dht-stats"].as<int>();
Expand Down Expand Up @@ -456,9 +452,12 @@ std::unique_ptr<Config> Config::Load(const boost::program_options::variables_map
| lt::alert::storage_notification
| lt::alert::tracker_notification;

cfg->session_settings.set_int(lt::settings_pack::alert_mask, alerts);
cfg->session_settings.set_str(lt::settings_pack::peer_fingerprint, lt::generate_fingerprint("PO", 0, 1));
cfg->session_settings.set_str(lt::settings_pack::user_agent, "porla/1.0");
for (auto& [ _, settings ] : cfg->sessions)
{
settings.set_int(lt::settings_pack::alert_mask, alerts);
settings.set_str(lt::settings_pack::peer_fingerprint, lt::generate_fingerprint("PO", 0, 1));
settings.set_str(lt::settings_pack::user_agent, "porla/1.0");
}

// If we get here without having a secret key, we must generate one. Also log a warning because
// if the secret key changes, JWT's will not work if restarting.
Expand Down
45 changes: 22 additions & 23 deletions src/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,33 @@ namespace porla
std::optional<int> max_connections;
std::optional<int> max_uploads;
std::optional<std::string> save_path;
std::optional<std::string> session;
std::optional<libtorrent::storage_mode_t> storage_mode;
std::unordered_set<std::string> tags;
std::optional<int> upload_limit;
};

std::optional<std::string> config_file;
toml::table config_tbl;
sqlite3* db;
std::optional<std::string> db_file;
std::optional<bool> http_auth_enabled;
std::optional<std::string> http_base_path;
std::optional<std::string> http_host;
std::optional<bool> http_metrics_enabled;
std::optional<uint16_t> http_port;
std::optional<bool> http_webui_enabled;

std::optional<bool> plugins_allow_git;
std::optional<fs::path> plugins_install_dir;
std::map<std::string, Preset> presets;
std::string secret_key;
std::optional<std::vector<lt_plugin>> session_extensions;
libtorrent::settings_pack session_settings;
std::optional<int> sodium_memlimit;
std::optional<fs::path> state_dir;
std::optional<int> timer_dht_stats;
std::optional<int> timer_session_stats;
std::optional<int> timer_torrent_updates;
std::optional<fs::path> workflow_dir;
std::optional<std::string> config_file;
toml::table config_tbl;
sqlite3* db;
std::optional<std::string> db_file;
std::optional<bool> http_auth_enabled;
std::optional<std::string> http_base_path;
std::optional<std::string> http_host;
std::optional<bool> http_metrics_enabled;
std::optional<uint16_t> http_port;
std::optional<bool> http_webui_enabled;
std::optional<bool> plugins_allow_git;
std::optional<fs::path> plugins_install_dir;
std::map<std::string, Preset> presets;
std::string secret_key;
std::map<std::string, lt::settings_pack> sessions;
std::optional<int> sodium_memlimit;
std::optional<fs::path> state_dir;
std::optional<int> timer_dht_stats;
std::optional<int> timer_session_stats;
std::optional<int> timer_torrent_updates;
std::optional<fs::path> workflow_dir;

static std::unique_ptr<Config> Load(const boost::program_options::variables_map& cmd);

Expand Down
4 changes: 3 additions & 1 deletion src/data/migrate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "migrations/0006_clientdata.hpp"
#include "migrations/0007_removesessionsettings.hpp"
#include "migrations/0008_plugins.hpp"
#include "migrations/0009_multisessions.hpp"
#include "statement.hpp"

int GetUserVersion(sqlite3* db)
Expand Down Expand Up @@ -47,7 +48,8 @@ bool porla::Data::Migrate(sqlite3* db)
&porla::Data::Migrations::TorrentsMetadata::Migrate,
&porla::Data::Migrations::ClientData::Migrate,
&porla::Data::Migrations::RemoveSessionSettings::Migrate,
&porla::Data::Migrations::Plugins::Migrate
&porla::Data::Migrations::Plugins::Migrate,
&porla::Data::Migrations::MultiSessions::Migrate
};

int user_version = GetUserVersion(db);
Expand Down
19 changes: 19 additions & 0 deletions src/data/migrations/0009_multisessions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "0009_multisessions.hpp"

#include <boost/log/trivial.hpp>

using porla::Data::Migrations::MultiSessions;

int MultiSessions::Migrate(sqlite3* db)
{
BOOST_LOG_TRIVIAL(info) << "Adding 'session_id' column to table 'addtorrentparams'";

int res = sqlite3_exec(
db,
"ALTER TABLE addtorrentparams ADD COLUMN session_id TEXT NOT NULL DEFAULT 'default'",
nullptr,
nullptr,
nullptr);

return res;
}
Loading