2696 lines
71 KiB
C++
2696 lines
71 KiB
C++
// This file Copyright © Mnemosyne LLC.
|
|
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
|
// or any future license endorsed by Mnemosyne LLC.
|
|
// License text can be found in the licenses/ folder.
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno> // EINVAL
|
|
#include <climits> /* INT_MAX */
|
|
#include <cstddef> // size_t
|
|
#include <ctime>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <fmt/chrono.h>
|
|
#include <fmt/core.h>
|
|
|
|
#include <small/map.hpp>
|
|
|
|
#include "libtransmission/transmission.h"
|
|
|
|
#include "libtransmission/announcer.h"
|
|
#include "libtransmission/bandwidth.h"
|
|
#include "libtransmission/completion.h"
|
|
#include "libtransmission/crypto-utils.h" // for tr_sha1()
|
|
#include "libtransmission/error.h"
|
|
#include "libtransmission/file.h"
|
|
#include "libtransmission/inout.h" // tr_ioTestPiece()
|
|
#include "libtransmission/log.h"
|
|
#include "libtransmission/magnet-metainfo.h"
|
|
#include "libtransmission/peer-common.h"
|
|
#include "libtransmission/peer-mgr.h"
|
|
#include "libtransmission/resume.h"
|
|
#include "libtransmission/session.h"
|
|
#include "libtransmission/subprocess.h"
|
|
#include "libtransmission/torrent-ctor.h"
|
|
#include "libtransmission/torrent-magnet.h"
|
|
#include "libtransmission/torrent-metainfo.h"
|
|
#include "libtransmission/torrent.h"
|
|
#include "libtransmission/tr-assert.h"
|
|
#include "libtransmission/tr-strbuf.h"
|
|
#include "libtransmission/utils.h"
|
|
#include "libtransmission/version.h"
|
|
#include "libtransmission/web-utils.h"
|
|
|
|
struct tr_ctor;
|
|
|
|
using namespace std::literals;
|
|
using namespace libtransmission::Values;
|
|
|
|
// ---
|
|
|
|
void tr_torrent::Error::set_tracker_warning(tr_interned_string announce_url, std::string_view errmsg)
|
|
{
|
|
announce_url_ = announce_url;
|
|
errmsg_.assign(errmsg);
|
|
error_type_ = TR_STAT_TRACKER_WARNING;
|
|
}
|
|
|
|
void tr_torrent::Error::set_tracker_error(tr_interned_string announce_url, std::string_view errmsg)
|
|
{
|
|
announce_url_ = announce_url;
|
|
errmsg_.assign(errmsg);
|
|
error_type_ = TR_STAT_TRACKER_ERROR;
|
|
}
|
|
|
|
void tr_torrent::Error::set_local_error(std::string_view errmsg)
|
|
{
|
|
announce_url_.clear();
|
|
errmsg_.assign(errmsg);
|
|
error_type_ = TR_STAT_LOCAL_ERROR;
|
|
}
|
|
|
|
void tr_torrent::Error::clear() noexcept
|
|
{
|
|
announce_url_.clear();
|
|
errmsg_.clear();
|
|
error_type_ = TR_STAT_OK;
|
|
}
|
|
|
|
void tr_torrent::Error::clear_if_tracker() noexcept
|
|
{
|
|
if (error_type_ == TR_STAT_TRACKER_WARNING || error_type_ == TR_STAT_TRACKER_ERROR)
|
|
{
|
|
clear();
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
char const* tr_torrentName(tr_torrent const* tor)
|
|
{
|
|
return tor != nullptr ? tor->name().c_str() : "";
|
|
}
|
|
|
|
tr_torrent_id_t tr_torrentId(tr_torrent const* tor)
|
|
{
|
|
return tor != nullptr ? tor->id() : -1;
|
|
}
|
|
|
|
tr_torrent* tr_torrentFindFromId(tr_session* session, tr_torrent_id_t id)
|
|
{
|
|
return session->torrents().get(id);
|
|
}
|
|
|
|
tr_torrent* tr_torrentFindFromMetainfo(tr_session* session, tr_torrent_metainfo const* metainfo)
|
|
{
|
|
if (session == nullptr || metainfo == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return session->torrents().get(metainfo->info_hash());
|
|
}
|
|
|
|
tr_torrent* tr_torrentFindFromMagnetLink(tr_session* session, char const* magnet_link)
|
|
{
|
|
return magnet_link == nullptr ? nullptr : session->torrents().get(magnet_link);
|
|
}
|
|
|
|
bool tr_torrentSetMetainfoFromFile(tr_torrent* tor, tr_torrent_metainfo const* metainfo, char const* filename)
|
|
{
|
|
if (tr_torrentHasMetadata(tor))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto error = tr_error{};
|
|
tr_torrentUseMetainfoFromFile(tor, metainfo, filename, &error);
|
|
if (error)
|
|
{
|
|
tor->error().set_local_error(fmt::format(
|
|
_("Couldn't use metainfo from '{path}' for '{magnet}': {error} ({error_code})"),
|
|
fmt::arg("path", filename),
|
|
fmt::arg("magnet", tor->magnet()),
|
|
fmt::arg("error", error.message()),
|
|
fmt::arg("error_code", error.code())));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---
|
|
|
|
namespace
|
|
{
|
|
bool did_files_disappear(tr_torrent* tor, std::optional<bool> has_any_local_data = {})
|
|
{
|
|
auto const has = has_any_local_data ? *has_any_local_data : tor->has_any_local_data();
|
|
return tor->has_total() > 0 && !has;
|
|
}
|
|
|
|
bool set_local_error_if_files_disappeared(tr_torrent* tor, std::optional<bool> has_any_local_data = {})
|
|
{
|
|
auto const files_disappeared = did_files_disappear(tor, has_any_local_data);
|
|
|
|
if (files_disappeared)
|
|
{
|
|
tr_logAddTraceTor(tor, "[LAZY] uh oh, the files disappeared");
|
|
tor->error().set_local_error(
|
|
_("No data found! Ensure your drives are connected or use \"Set Location\". "
|
|
"To re-download, use \"Verify Local Data\" and start the torrent afterwards."));
|
|
}
|
|
|
|
return files_disappeared;
|
|
}
|
|
|
|
/* returns true if the seed ratio applies --
|
|
* it applies if the torrent's a seed AND it has a seed ratio set */
|
|
bool tr_torrentGetSeedRatioBytes(tr_torrent const* tor, uint64_t* setme_left, uint64_t* setme_goal)
|
|
{
|
|
bool seed_ratio_applies = false;
|
|
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (auto const seed_ratio = tor->effective_seed_ratio(); seed_ratio)
|
|
{
|
|
auto const uploaded = tor->bytes_uploaded_.ever();
|
|
auto const baseline = tor->size_when_done();
|
|
auto const goal = baseline * *seed_ratio;
|
|
|
|
if (setme_left != nullptr)
|
|
{
|
|
*setme_left = goal > uploaded ? goal - uploaded : 0;
|
|
}
|
|
|
|
if (setme_goal != nullptr)
|
|
{
|
|
*setme_goal = goal;
|
|
}
|
|
|
|
seed_ratio_applies = tor->is_done();
|
|
}
|
|
|
|
return seed_ratio_applies;
|
|
}
|
|
|
|
bool tr_torrentIsSeedRatioDone(tr_torrent const* tor)
|
|
{
|
|
auto bytes_left = uint64_t{};
|
|
return tr_torrentGetSeedRatioBytes(tor, &bytes_left, nullptr) && bytes_left == 0;
|
|
}
|
|
} // namespace
|
|
|
|
// --- PER-TORRENT UL / DL SPEEDS
|
|
|
|
void tr_torrentSetSpeedLimit_KBps(tr_torrent* const tor, tr_direction const dir, size_t const limit_kbyps)
|
|
{
|
|
tor->set_speed_limit(dir, Speed{ limit_kbyps, Speed::Units::KByps });
|
|
}
|
|
|
|
size_t tr_torrentGetSpeedLimit_KBps(tr_torrent const* const tor, tr_direction const dir)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(tr_isDirection(dir));
|
|
|
|
return tor->speed_limit(dir).count(Speed::Units::KByps);
|
|
}
|
|
|
|
void tr_torrentUseSpeedLimit(tr_torrent* const tor, tr_direction const dir, bool const enabled)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(tr_isDirection(dir));
|
|
|
|
tor->use_speed_limit(dir, enabled);
|
|
}
|
|
|
|
bool tr_torrentUsesSpeedLimit(tr_torrent const* const tor, tr_direction const dir)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->uses_speed_limit(dir);
|
|
}
|
|
|
|
void tr_torrentUseSessionLimits(tr_torrent* const tor, bool const enabled)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (tor->bandwidth().honor_parent_limits(TR_UP, enabled) || tor->bandwidth().honor_parent_limits(TR_DOWN, enabled))
|
|
{
|
|
tor->set_dirty();
|
|
}
|
|
}
|
|
|
|
bool tr_torrentUsesSessionLimits(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->uses_session_limits();
|
|
}
|
|
|
|
// --- Download Ratio
|
|
|
|
void tr_torrentSetRatioMode(tr_torrent* const tor, tr_ratiolimit mode)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_seed_ratio_mode(mode);
|
|
}
|
|
|
|
tr_ratiolimit tr_torrentGetRatioMode(tr_torrent const* const tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->seed_ratio_mode();
|
|
}
|
|
|
|
void tr_torrentSetRatioLimit(tr_torrent* const tor, double desired_ratio)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_seed_ratio(desired_ratio);
|
|
}
|
|
|
|
double tr_torrentGetRatioLimit(tr_torrent const* const tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->seed_ratio();
|
|
}
|
|
|
|
bool tr_torrentGetSeedRatio(tr_torrent const* const tor, double* ratio)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
auto const val = tor->effective_seed_ratio();
|
|
|
|
if (ratio != nullptr && val)
|
|
{
|
|
*ratio = *val;
|
|
}
|
|
|
|
return val.has_value();
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrentSetIdleMode(tr_torrent* const tor, tr_idlelimit mode)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_idle_limit_mode(mode);
|
|
}
|
|
|
|
tr_idlelimit tr_torrentGetIdleMode(tr_torrent const* const tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->idle_limit_mode();
|
|
}
|
|
|
|
void tr_torrentSetIdleLimit(tr_torrent* const tor, uint16_t idle_minutes)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_idle_limit_minutes(idle_minutes);
|
|
}
|
|
|
|
uint16_t tr_torrentGetIdleLimit(tr_torrent const* const tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->idle_limit_minutes();
|
|
}
|
|
|
|
namespace
|
|
{
|
|
namespace script_helpers
|
|
{
|
|
[[nodiscard]] std::string build_labels_string(tr_torrent::labels_t const& labels)
|
|
{
|
|
auto buf = std::stringstream{};
|
|
|
|
for (auto it = std::begin(labels), end = std::end(labels); it != end;)
|
|
{
|
|
buf << it->sv();
|
|
|
|
if (++it != end)
|
|
{
|
|
buf << ',';
|
|
}
|
|
}
|
|
|
|
return buf.str();
|
|
}
|
|
|
|
[[nodiscard]] std::string buildTrackersString(tr_torrent const* tor)
|
|
{
|
|
auto buf = std::stringstream{};
|
|
|
|
for (size_t i = 0, n = tr_torrentTrackerCount(tor); i < n; ++i)
|
|
{
|
|
buf << tr_torrentTracker(tor, i).host_and_port;
|
|
|
|
if (i < n)
|
|
{
|
|
buf << ',';
|
|
}
|
|
}
|
|
|
|
return buf.str();
|
|
}
|
|
|
|
void torrentCallScript(tr_torrent const* tor, std::string const& script)
|
|
{
|
|
if (std::empty(script))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto torrent_dir = tr_pathbuf{ tor->current_dir() };
|
|
tr_sys_path_native_separators(std::data(torrent_dir));
|
|
|
|
auto const cmd = std::array<char const*, 2>{ script.c_str(), nullptr };
|
|
|
|
auto const id_str = std::to_string(tr_torrentId(tor));
|
|
auto const labels_str = build_labels_string(tor->labels());
|
|
auto const trackers_str = buildTrackersString(tor);
|
|
auto const bytes_downloaded_str = std::to_string(tor->bytes_downloaded_.ever());
|
|
auto const localtime_str = fmt::format("{:%a %b %d %T %Y%n}", fmt::localtime(tr_time()));
|
|
|
|
auto const env = std::map<std::string_view, std::string_view>{
|
|
{ "TR_APP_VERSION"sv, SHORT_VERSION_STRING },
|
|
{ "TR_TIME_LOCALTIME"sv, localtime_str },
|
|
{ "TR_TORRENT_BYTES_DOWNLOADED"sv, bytes_downloaded_str },
|
|
{ "TR_TORRENT_DIR"sv, torrent_dir.c_str() },
|
|
{ "TR_TORRENT_HASH"sv, tor->info_hash_string() },
|
|
{ "TR_TORRENT_ID"sv, id_str },
|
|
{ "TR_TORRENT_LABELS"sv, labels_str },
|
|
{ "TR_TORRENT_NAME"sv, tor->name() },
|
|
{ "TR_TORRENT_TRACKERS"sv, trackers_str },
|
|
};
|
|
|
|
tr_logAddInfoTor(tor, fmt::format(_("Calling script '{path}'"), fmt::arg("path", script)));
|
|
|
|
auto error = tr_error{};
|
|
if (!tr_spawn_async(std::data(cmd), env, TR_IF_WIN32("\\", "/"), &error))
|
|
{
|
|
tr_logAddWarnTor(
|
|
tor,
|
|
fmt::format(
|
|
_("Couldn't call script '{path}': {error} ({error_code})"),
|
|
fmt::arg("path", script),
|
|
fmt::arg("error", error.message()),
|
|
fmt::arg("error_code", error.code())));
|
|
}
|
|
}
|
|
} // namespace script_helpers
|
|
|
|
void callScriptIfEnabled(tr_torrent const* tor, TrScript type)
|
|
{
|
|
using namespace script_helpers;
|
|
|
|
auto const* session = tor->session;
|
|
|
|
if (tr_sessionIsScriptEnabled(session, type))
|
|
{
|
|
torrentCallScript(tor, session->script(type));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---
|
|
|
|
void tr_torrent::stop_if_seed_limit_reached()
|
|
{
|
|
if (!is_running() || is_stopping_ || !is_done())
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* if we're seeding and reach our seed ratio limit, stop the torrent */
|
|
if (tr_torrentIsSeedRatioDone(this))
|
|
{
|
|
tr_logAddInfoTor(this, _("Seed ratio reached; pausing torrent"));
|
|
stop_soon();
|
|
session->onRatioLimitHit(this);
|
|
}
|
|
/* if we're seeding and reach our inactivity limit, stop the torrent */
|
|
else if (auto const secs_left = idle_seconds_left(tr_time()); secs_left && *secs_left == 0U)
|
|
{
|
|
tr_logAddInfoTor(this, _("Seeding idle limit reached; pausing torrent"));
|
|
|
|
stop_soon();
|
|
finished_seeding_by_idle_ = true;
|
|
session->onIdleLimitHit(this);
|
|
}
|
|
|
|
if (is_stopping_)
|
|
{
|
|
callScriptIfEnabled(this, TR_SCRIPT_ON_TORRENT_DONE_SEEDING);
|
|
}
|
|
}
|
|
|
|
// --- Queue
|
|
|
|
namespace
|
|
{
|
|
namespace queue_helpers
|
|
{
|
|
constexpr auto MinQueuePosition = std::numeric_limits<size_t>::min();
|
|
constexpr auto MaxQueuePosition = std::numeric_limits<size_t>::max();
|
|
|
|
#ifdef TR_ENABLE_ASSERTS
|
|
[[nodiscard]] bool torrents_are_sorted_by_queue_position(std::vector<tr_torrent*> torrents)
|
|
{
|
|
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
|
|
|
|
for (size_t idx = 0, end_idx = std::size(torrents); idx < end_idx; ++idx)
|
|
{
|
|
if (torrents[idx]->queue_position() != idx)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
} // namespace queue_helpers
|
|
} // namespace
|
|
|
|
size_t tr_torrentGetQueuePosition(tr_torrent const* tor)
|
|
{
|
|
return tor->queue_position();
|
|
}
|
|
|
|
void tr_torrent::set_unique_queue_position(size_t const new_pos)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto current = size_t{};
|
|
auto const old_pos = queue_position_;
|
|
|
|
queue_position_ = MaxQueuePosition;
|
|
|
|
auto& torrents = session->torrents();
|
|
for (auto* const walk : torrents)
|
|
{
|
|
if ((old_pos < new_pos) && (old_pos <= walk->queue_position_) && (walk->queue_position_ <= new_pos))
|
|
{
|
|
--walk->queue_position_;
|
|
walk->mark_changed();
|
|
}
|
|
|
|
if ((old_pos > new_pos) && (new_pos <= walk->queue_position_) && (walk->queue_position_ < old_pos))
|
|
{
|
|
++walk->queue_position_;
|
|
walk->mark_changed();
|
|
}
|
|
|
|
current = std::max(current, walk->queue_position_ + 1U);
|
|
}
|
|
|
|
queue_position_ = std::min(new_pos, current);
|
|
mark_changed();
|
|
|
|
TR_ASSERT(torrents_are_sorted_by_queue_position(torrents.get_all()));
|
|
}
|
|
|
|
void tr_torrentSetQueuePosition(tr_torrent* tor, size_t queue_position)
|
|
{
|
|
tor->set_unique_queue_position(queue_position);
|
|
}
|
|
|
|
void tr_torrentsQueueMoveTop(tr_torrent* const* torrents_in, size_t torrent_count)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
|
|
std::sort(std::rbegin(torrents), std::rend(torrents), tr_torrent::CompareQueuePosition);
|
|
for (auto* const tor : torrents)
|
|
{
|
|
tor->set_unique_queue_position(MinQueuePosition);
|
|
}
|
|
}
|
|
|
|
void tr_torrentsQueueMoveUp(tr_torrent* const* torrents_in, size_t torrent_count)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
|
|
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
|
|
for (auto* const tor : torrents)
|
|
{
|
|
if (auto const pos = tor->queue_position(); pos > MinQueuePosition)
|
|
{
|
|
tor->set_unique_queue_position(pos - 1U);
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrentsQueueMoveDown(tr_torrent* const* torrents_in, size_t torrent_count)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
|
|
std::sort(std::rbegin(torrents), std::rend(torrents), tr_torrent::CompareQueuePosition);
|
|
for (auto* const tor : torrents)
|
|
{
|
|
if (auto const pos = tor->queue_position(); pos < MaxQueuePosition)
|
|
{
|
|
tor->set_unique_queue_position(pos + 1U);
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrentsQueueMoveBottom(tr_torrent* const* torrents_in, size_t torrent_count)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
|
|
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
|
|
for (auto* const tor : torrents)
|
|
{
|
|
tor->set_unique_queue_position(MaxQueuePosition);
|
|
}
|
|
}
|
|
|
|
// --- Start, Stop
|
|
|
|
namespace
|
|
{
|
|
namespace start_stop_helpers
|
|
{
|
|
bool torrentShouldQueue(tr_torrent const* const tor)
|
|
{
|
|
tr_direction const dir = tor->queue_direction();
|
|
|
|
return tor->session->count_queue_free_slots(dir) == 0;
|
|
}
|
|
|
|
bool removeTorrentFile(char const* filename, void* /*user_data*/, tr_error* error)
|
|
{
|
|
return tr_sys_path_remove(filename, error);
|
|
}
|
|
|
|
void removeTorrentInSessionThread(tr_torrent* tor, bool delete_flag, tr_fileFunc delete_func, void* user_data)
|
|
{
|
|
auto const lock = tor->unique_lock();
|
|
|
|
if (delete_flag && tor->has_metainfo())
|
|
{
|
|
// ensure the files are all closed and idle before moving
|
|
tor->session->close_torrent_files(tor->id());
|
|
tor->session->verify_remove(tor);
|
|
|
|
if (delete_func == nullptr)
|
|
{
|
|
delete_func = removeTorrentFile;
|
|
}
|
|
|
|
auto const delete_func_wrapper = [&delete_func, user_data](char const* filename)
|
|
{
|
|
delete_func(filename, user_data, nullptr);
|
|
};
|
|
|
|
tor->files().remove(tor->current_dir(), tor->name(), delete_func_wrapper);
|
|
}
|
|
|
|
tr_torrentFreeInSessionThread(tor);
|
|
}
|
|
|
|
void freeTorrent(tr_torrent* tor)
|
|
{
|
|
using namespace queue_helpers;
|
|
|
|
auto const lock = tor->unique_lock();
|
|
|
|
TR_ASSERT(!tor->is_running());
|
|
|
|
tr_session* session = tor->session;
|
|
|
|
tor->doomed_.emit(tor);
|
|
|
|
session->announcer_->removeTorrent(tor);
|
|
|
|
session->torrents().remove(tor, tr_time());
|
|
|
|
if (!session->isClosing())
|
|
{
|
|
// move the torrent being freed to the end of the queue so that
|
|
// all the torrents queued after it will move up one position
|
|
tor->set_unique_queue_position(queue_helpers::MaxQueuePosition);
|
|
}
|
|
|
|
delete tor;
|
|
}
|
|
} // namespace start_stop_helpers
|
|
} // namespace
|
|
|
|
// has_any_local_data is true or false if we know whether or not local data exists,
|
|
// or unset if we don't know and need to check for ourselves
|
|
void tr_torrent::start(bool bypass_queue, std::optional<bool> has_any_local_data)
|
|
{
|
|
using namespace start_stop_helpers;
|
|
|
|
auto const lock = unique_lock();
|
|
|
|
switch (activity())
|
|
{
|
|
case TR_STATUS_SEED:
|
|
case TR_STATUS_DOWNLOAD:
|
|
return; /* already started */
|
|
|
|
case TR_STATUS_SEED_WAIT:
|
|
case TR_STATUS_DOWNLOAD_WAIT:
|
|
if (!bypass_queue)
|
|
{
|
|
return; /* already queued */
|
|
}
|
|
|
|
break;
|
|
|
|
case TR_STATUS_CHECK:
|
|
case TR_STATUS_CHECK_WAIT:
|
|
/* verifying right now... wait until that's done so
|
|
* we'll know what completeness to use/announce */
|
|
return;
|
|
|
|
case TR_STATUS_STOPPED:
|
|
if (!bypass_queue && torrentShouldQueue(this))
|
|
{
|
|
set_is_queued();
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
/* don't allow the torrent to be started if the files disappeared */
|
|
if (set_local_error_if_files_disappeared(this, has_any_local_data))
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* allow finished torrents to be resumed */
|
|
if (tr_torrentIsSeedRatioDone(this))
|
|
{
|
|
tr_logAddInfoTor(this, _("Restarted manually -- disabling its seed ratio"));
|
|
set_seed_ratio_mode(TR_RATIOLIMIT_UNLIMITED);
|
|
}
|
|
|
|
is_running_ = true;
|
|
set_dirty();
|
|
session->run_in_session_thread([this]() { start_in_session_thread(); });
|
|
}
|
|
|
|
void tr_torrent::start_in_session_thread()
|
|
{
|
|
using namespace start_stop_helpers;
|
|
|
|
TR_ASSERT(session->am_in_session_thread());
|
|
auto const lock = unique_lock();
|
|
|
|
// We are after `torrentStart` and before announcing to trackers/peers,
|
|
// so now is the best time to create wanted empty files.
|
|
create_empty_files();
|
|
|
|
recheck_completeness();
|
|
set_is_queued(false);
|
|
|
|
time_t const now = tr_time();
|
|
|
|
is_running_ = true;
|
|
completeness_ = completion_.status();
|
|
date_started_ = now;
|
|
mark_changed();
|
|
error().clear();
|
|
finished_seeding_by_idle_ = false;
|
|
|
|
bytes_uploaded_.start_new_session();
|
|
bytes_downloaded_.start_new_session();
|
|
bytes_corrupt_.start_new_session();
|
|
set_dirty();
|
|
|
|
session->announcer_->startTorrent(this);
|
|
lpdAnnounceAt = now;
|
|
started_.emit(this);
|
|
}
|
|
|
|
void tr_torrent::stop_now()
|
|
{
|
|
TR_ASSERT(session->am_in_session_thread());
|
|
auto const lock = unique_lock();
|
|
|
|
is_running_ = false;
|
|
is_stopping_ = false;
|
|
mark_changed();
|
|
|
|
if (!session->isClosing())
|
|
{
|
|
tr_logAddInfoTor(this, _("Pausing torrent"));
|
|
}
|
|
|
|
session->verify_remove(this);
|
|
|
|
stopped_.emit(this);
|
|
session->announcer_->stopTorrent(this);
|
|
|
|
session->close_torrent_files(id());
|
|
|
|
if (!is_deleting_)
|
|
{
|
|
save_resume_file();
|
|
}
|
|
|
|
set_is_queued(false);
|
|
}
|
|
|
|
void tr_torrentStop(tr_torrent* tor)
|
|
{
|
|
if (!tr_isTorrent(tor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const lock = tor->unique_lock();
|
|
|
|
tor->start_when_stable_ = false;
|
|
tor->set_dirty();
|
|
tor->session->run_in_session_thread([tor]() { tor->stop_now(); });
|
|
}
|
|
|
|
void tr_torrentRemove(tr_torrent* tor, bool delete_flag, tr_fileFunc delete_func, void* user_data)
|
|
{
|
|
using namespace start_stop_helpers;
|
|
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->is_deleting_ = true;
|
|
|
|
tor->session->run_in_session_thread(removeTorrentInSessionThread, tor, delete_flag, delete_func, user_data);
|
|
}
|
|
|
|
void tr_torrentFreeInSessionThread(tr_torrent* tor)
|
|
{
|
|
using namespace start_stop_helpers;
|
|
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(tor->session != nullptr);
|
|
TR_ASSERT(tor->session->am_in_session_thread());
|
|
|
|
if (!tor->session->isClosing())
|
|
{
|
|
tr_logAddInfoTor(tor, _("Removing torrent"));
|
|
}
|
|
|
|
tor->stop_now();
|
|
|
|
if (tor->is_deleting_)
|
|
{
|
|
tr_torrent_metainfo::remove_file(tor->session->torrentDir(), tor->name(), tor->info_hash_string(), ".torrent"sv);
|
|
tr_torrent_metainfo::remove_file(tor->session->torrentDir(), tor->name(), tor->info_hash_string(), ".magnet"sv);
|
|
tr_torrent_metainfo::remove_file(tor->session->resumeDir(), tor->name(), tor->info_hash_string(), ".resume"sv);
|
|
}
|
|
|
|
freeTorrent(tor);
|
|
}
|
|
|
|
// ---
|
|
|
|
// Sniff out newly-added seeds so that they can skip the verify step
|
|
bool tr_torrent::is_new_torrent_a_seed()
|
|
{
|
|
if (!has_metainfo())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (tr_file_index_t i = 0, n = file_count(); i < n; ++i)
|
|
{
|
|
// it's not a new seed if a file is missing
|
|
auto const found = find_file(i);
|
|
if (!found)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// it's not a new seed if a file is partial
|
|
if (tr_strv_ends_with(found->filename(), tr_torrent_files::PartialFileSuffix))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// it's not a new seed if a file size is wrong
|
|
if (found->size != file_size(i))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// it's not a new seed if it was modified after it was added
|
|
if (found->last_modified_at >= date_added_)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// check the first piece
|
|
return ensure_piece_is_checked(0);
|
|
}
|
|
|
|
void tr_torrent::on_metainfo_updated()
|
|
{
|
|
completion_ = tr_completion{ this, &block_info() };
|
|
obfuscated_hash_ = tr_sha1::digest("req2"sv, info_hash());
|
|
fpm_ = tr_file_piece_map{ metainfo_ };
|
|
file_mtimes_.resize(file_count());
|
|
file_priorities_ = tr_file_priorities{ &fpm_ };
|
|
files_wanted_ = tr_files_wanted{ &fpm_ };
|
|
checked_pieces_ = tr_bitfield{ size_t(piece_count()) };
|
|
}
|
|
|
|
void tr_torrent::on_metainfo_completed()
|
|
{
|
|
// we can look for files now that we know what files are in the torrent
|
|
refresh_current_dir();
|
|
|
|
callScriptIfEnabled(this, TR_SCRIPT_ON_TORRENT_ADDED);
|
|
|
|
if (session->shouldFullyVerifyAddedTorrents() || !is_new_torrent_a_seed())
|
|
{
|
|
// Potentially, we are in `tr_torrent::init`,
|
|
// and we don't want any file created before `tr_torrent::start`
|
|
// so we Verify but we don't Create files.
|
|
tr_torrentVerify(this);
|
|
}
|
|
else
|
|
{
|
|
completion_.set_has_all();
|
|
date_done_ = date_added_;
|
|
recheck_completeness();
|
|
|
|
if (start_when_stable_)
|
|
{
|
|
start(false, {});
|
|
}
|
|
else if (is_running())
|
|
{
|
|
tr_torrentStop(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrent::init(tr_ctor const& ctor)
|
|
{
|
|
session = ctor.session();
|
|
TR_ASSERT(session != nullptr);
|
|
auto const lock = unique_lock();
|
|
|
|
queue_position_ = std::size(session->torrents());
|
|
|
|
on_metainfo_updated();
|
|
|
|
if (auto dir = ctor.download_dir(TR_FORCE); !std::empty(dir))
|
|
{
|
|
download_dir_ = dir;
|
|
}
|
|
else if (dir = ctor.download_dir(TR_FALLBACK); !std::empty(dir))
|
|
{
|
|
download_dir_ = dir;
|
|
}
|
|
|
|
if (tr_sessionIsIncompleteDirEnabled(session))
|
|
{
|
|
auto const& dir = ctor.incomplete_dir();
|
|
incomplete_dir_ = !std::empty(dir) ? dir : session->incompleteDir();
|
|
}
|
|
|
|
bandwidth().set_parent(&session->top_bandwidth_);
|
|
bandwidth().set_priority(ctor.bandwidth_priority());
|
|
error().clear();
|
|
finished_seeding_by_idle_ = false;
|
|
|
|
set_labels(ctor.labels());
|
|
|
|
session->addTorrent(this);
|
|
|
|
TR_ASSERT(bytes_downloaded_.during_this_session() == 0U);
|
|
TR_ASSERT(bytes_uploaded_.during_this_session() == 0);
|
|
|
|
mark_changed();
|
|
|
|
date_added_ = tr_time(); // this is a default that will be overwritten by the resume file
|
|
|
|
tr_resume::fields_t loaded = {};
|
|
|
|
{
|
|
// tr_resume::load() calls a lot of tr_torrentSetFoo() methods
|
|
// that set things as dirty, but... these settings being loaded are
|
|
// the same ones that would be saved back again, so don't let them
|
|
// affect the 'is dirty' flag.
|
|
auto const was_dirty = is_dirty();
|
|
auto resume_helper = ResumeHelper{ *this };
|
|
loaded = tr_resume::load(this, resume_helper, tr_resume::All, ctor);
|
|
set_dirty(was_dirty);
|
|
tr_torrent_metainfo::migrate_file(session->torrentDir(), name(), info_hash_string(), ".torrent"sv);
|
|
}
|
|
|
|
completeness_ = completion_.status();
|
|
|
|
ctor.init_torrent_priorities(*this);
|
|
ctor.init_torrent_wanted(*this);
|
|
|
|
refresh_current_dir();
|
|
|
|
if ((loaded & tr_resume::Speedlimit) == 0)
|
|
{
|
|
use_speed_limit(TR_UP, false);
|
|
set_speed_limit(TR_UP, session->speed_limit(TR_UP));
|
|
use_speed_limit(TR_DOWN, false);
|
|
set_speed_limit(TR_DOWN, session->speed_limit(TR_DOWN));
|
|
tr_torrentUseSessionLimits(this, true);
|
|
}
|
|
|
|
if ((loaded & tr_resume::Ratiolimit) == 0)
|
|
{
|
|
set_seed_ratio_mode(TR_RATIOLIMIT_GLOBAL);
|
|
set_seed_ratio(session->desiredRatio());
|
|
}
|
|
|
|
if ((loaded & tr_resume::Idlelimit) == 0)
|
|
{
|
|
set_idle_limit_mode(TR_IDLELIMIT_GLOBAL);
|
|
set_idle_limit_minutes(session->idleLimitMinutes());
|
|
}
|
|
|
|
auto has_any_local_data = std::optional<bool>{};
|
|
if ((loaded & tr_resume::Progress) != 0)
|
|
{
|
|
// if tr_resume::load() loaded progress info, then initCheckedPieces()
|
|
// has already looked for local data on the filesystem
|
|
has_any_local_data = std::any_of(
|
|
std::begin(file_mtimes_),
|
|
std::end(file_mtimes_),
|
|
[](auto mtime) { return mtime > 0; });
|
|
}
|
|
|
|
auto const filename = has_metainfo() ? torrent_file() : magnet_file();
|
|
|
|
// if we don't have a local .torrent or .magnet file already,
|
|
// assume the torrent is new
|
|
bool const is_new_torrent = !tr_sys_path_exists(filename);
|
|
|
|
if (is_new_torrent)
|
|
{
|
|
auto error = tr_error{};
|
|
|
|
if (has_metainfo()) // torrent file
|
|
{
|
|
ctor.save(filename, &error);
|
|
}
|
|
else // magnet link
|
|
{
|
|
auto const magnet_link = magnet();
|
|
tr_file_save(filename, magnet_link, &error);
|
|
}
|
|
|
|
if (error)
|
|
{
|
|
this->error().set_local_error(fmt::format(
|
|
_("Couldn't save '{path}': {error} ({error_code})"),
|
|
fmt::arg("path", filename),
|
|
fmt::arg("error", error.message()),
|
|
fmt::arg("error_code", error.code())));
|
|
}
|
|
}
|
|
|
|
torrent_announcer = session->announcer_->addTorrent(this, &tr_torrent::on_tracker_response);
|
|
|
|
if (auto const has_metainfo = this->has_metainfo(); is_new_torrent && has_metainfo)
|
|
{
|
|
on_metainfo_completed();
|
|
}
|
|
else if (start_when_stable_)
|
|
{
|
|
auto const bypass_queue = !has_metainfo; // to fetch metainfo from peers
|
|
start(bypass_queue, has_any_local_data);
|
|
}
|
|
else
|
|
{
|
|
set_local_error_if_files_disappeared(this, has_any_local_data);
|
|
}
|
|
}
|
|
|
|
void tr_torrent::set_metainfo(tr_torrent_metainfo tm)
|
|
{
|
|
TR_ASSERT(!has_metainfo());
|
|
metainfo_ = std::move(tm);
|
|
on_metainfo_updated();
|
|
|
|
got_metainfo_.emit(this);
|
|
session->onMetadataCompleted(this);
|
|
set_dirty();
|
|
mark_edited();
|
|
|
|
on_metainfo_completed();
|
|
this->on_announce_list_changed();
|
|
}
|
|
|
|
tr_torrent* tr_torrentNew(tr_ctor* ctor, tr_torrent** setme_duplicate_of)
|
|
{
|
|
TR_ASSERT(ctor != nullptr);
|
|
auto* const session = ctor->session();
|
|
TR_ASSERT(session != nullptr);
|
|
|
|
// is the metainfo valid?
|
|
auto metainfo = ctor->steal_metainfo();
|
|
if (std::empty(metainfo.info_hash_string()))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// is it a duplicate?
|
|
if (auto* const duplicate_of = session->torrents().get(metainfo.info_hash()); duplicate_of != nullptr)
|
|
{
|
|
if (setme_duplicate_of != nullptr)
|
|
{
|
|
*setme_duplicate_of = duplicate_of;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
auto* const tor = new tr_torrent{ std::move(metainfo) };
|
|
tor->verify_done_callback_ = ctor->steal_verify_done_callback();
|
|
tor->init(*ctor);
|
|
return tor;
|
|
}
|
|
|
|
// --- Location
|
|
|
|
void tr_torrent::set_location_in_session_thread(std::string_view const path, bool move_from_old_path, int volatile* setme_state)
|
|
{
|
|
TR_ASSERT(session->am_in_session_thread());
|
|
|
|
auto ok = bool{ true };
|
|
if (move_from_old_path)
|
|
{
|
|
if (setme_state != nullptr)
|
|
{
|
|
*setme_state = TR_LOC_MOVING;
|
|
}
|
|
|
|
// ensure the files are all closed and idle before moving
|
|
session->close_torrent_files(id());
|
|
session->verify_remove(this);
|
|
|
|
auto error = tr_error{};
|
|
ok = files().move(current_dir(), path, name(), &error);
|
|
if (error)
|
|
{
|
|
this->error().set_local_error(fmt::format(
|
|
_("Couldn't move '{old_path}' to '{path}': {error} ({error_code})"),
|
|
fmt::arg("old_path", current_dir()),
|
|
fmt::arg("path", path),
|
|
fmt::arg("error", error.message()),
|
|
fmt::arg("error_code", error.code())));
|
|
tr_torrentStop(this);
|
|
}
|
|
}
|
|
|
|
// tell the torrent where the files are
|
|
if (ok)
|
|
{
|
|
set_download_dir(path);
|
|
|
|
if (move_from_old_path)
|
|
{
|
|
incomplete_dir_.clear();
|
|
current_dir_ = download_dir();
|
|
}
|
|
}
|
|
|
|
if (setme_state != nullptr)
|
|
{
|
|
*setme_state = ok ? TR_LOC_DONE : TR_LOC_ERROR;
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
namespace location_helpers
|
|
{
|
|
size_t buildSearchPathArray(tr_torrent const* tor, std::string_view* paths)
|
|
{
|
|
auto* walk = paths;
|
|
|
|
if (auto const& path = tor->download_dir(); !std::empty(path))
|
|
{
|
|
*walk++ = path.sv();
|
|
}
|
|
|
|
if (auto const& path = tor->incomplete_dir(); !std::empty(path))
|
|
{
|
|
*walk++ = path.sv();
|
|
}
|
|
|
|
return walk - paths;
|
|
}
|
|
} // namespace location_helpers
|
|
} // namespace
|
|
|
|
void tr_torrent::set_location(std::string_view location, bool move_from_old_path, int volatile* setme_state)
|
|
{
|
|
if (setme_state != nullptr)
|
|
{
|
|
*setme_state = TR_LOC_MOVING;
|
|
}
|
|
|
|
session->run_in_session_thread([this, loc = std::string(location), move_from_old_path, setme_state]()
|
|
{ set_location_in_session_thread(loc, move_from_old_path, setme_state); });
|
|
}
|
|
|
|
void tr_torrentSetLocation(tr_torrent* tor, char const* location, bool move_from_old_path, int volatile* setme_state)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(location != nullptr);
|
|
TR_ASSERT(*location != '\0');
|
|
|
|
tor->set_location(location, move_from_old_path, setme_state);
|
|
}
|
|
|
|
std::optional<tr_torrent_files::FoundFile> tr_torrent::find_file(tr_file_index_t file_index) const
|
|
{
|
|
using namespace location_helpers;
|
|
|
|
auto paths = std::array<std::string_view, 4>{};
|
|
auto const n_paths = buildSearchPathArray(this, std::data(paths));
|
|
return files().find(file_index, std::data(paths), n_paths);
|
|
}
|
|
|
|
bool tr_torrent::has_any_local_data() const
|
|
{
|
|
using namespace location_helpers;
|
|
|
|
auto paths = std::array<std::string_view, 4>{};
|
|
auto const n_paths = buildSearchPathArray(this, std::data(paths));
|
|
return files().hasAnyLocalData(std::data(paths), n_paths);
|
|
}
|
|
|
|
void tr_torrentSetDownloadDir(tr_torrent* tor, char const* path)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (tor->download_dir_ != path)
|
|
{
|
|
tor->set_download_dir(path, true);
|
|
}
|
|
}
|
|
|
|
char const* tr_torrentGetDownloadDir(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->download_dir().c_str();
|
|
}
|
|
|
|
char const* tr_torrentGetCurrentDir(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->current_dir().c_str();
|
|
}
|
|
|
|
void tr_torrentChangeMyPort(tr_torrent* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (tor->is_running())
|
|
{
|
|
tr_announcerChangeMyPort(tor);
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
namespace
|
|
{
|
|
namespace manual_update_helpers
|
|
{
|
|
void torrentManualUpdateImpl(tr_torrent* const tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (tor->is_running())
|
|
{
|
|
tr_announcerManualAnnounce(tor);
|
|
}
|
|
}
|
|
} // namespace manual_update_helpers
|
|
} // namespace
|
|
|
|
void tr_torrentManualUpdate(tr_torrent* tor)
|
|
{
|
|
using namespace manual_update_helpers;
|
|
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->session->run_in_session_thread(torrentManualUpdateImpl, tor);
|
|
}
|
|
|
|
bool tr_torrentCanManualUpdate(tr_torrent const* tor)
|
|
{
|
|
return tr_isTorrent(tor) && tor->is_running() && tr_announcerCanManualAnnounce(tor);
|
|
}
|
|
|
|
// ---
|
|
|
|
tr_stat tr_torrent::stats() const
|
|
{
|
|
static auto constexpr IsStalled = [](tr_torrent const* const tor, std::optional<size_t> idle_secs)
|
|
{
|
|
return tor->session->queueStalledEnabled() && idle_secs > tor->session->queueStalledMinutes() * 60U;
|
|
};
|
|
|
|
auto const now_msec = tr_time_msec();
|
|
auto const now_sec = tr_time();
|
|
|
|
auto const swarm_stats = this->swarm != nullptr ? tr_swarmGetStats(this->swarm) : tr_swarm_stats{};
|
|
auto const activity = this->activity();
|
|
auto const idle_seconds = this->idle_seconds(now_sec);
|
|
|
|
auto stats = tr_stat{};
|
|
|
|
stats.id = this->id();
|
|
stats.activity = activity;
|
|
stats.error = this->error().error_type();
|
|
stats.queuePosition = queue_position();
|
|
stats.idleSecs = idle_seconds ? static_cast<time_t>(*idle_seconds) : -1;
|
|
stats.isStalled = IsStalled(this, idle_seconds);
|
|
stats.errorString = this->error().errmsg().c_str();
|
|
|
|
stats.peersConnected = swarm_stats.peer_count;
|
|
stats.peersSendingToUs = swarm_stats.active_peer_count[TR_DOWN];
|
|
stats.peersGettingFromUs = swarm_stats.active_peer_count[TR_UP];
|
|
stats.webseedsSendingToUs = swarm_stats.active_webseed_count;
|
|
|
|
for (int i = 0; i < TR_PEER_FROM__MAX; i++)
|
|
{
|
|
stats.peersFrom[i] = swarm_stats.peer_from_count[i];
|
|
stats.knownPeersFrom[i] = swarm_stats.known_peer_from_count[i];
|
|
}
|
|
|
|
auto const piece_upload_speed = bandwidth().get_piece_speed(now_msec, TR_UP);
|
|
stats.pieceUploadSpeed_KBps = piece_upload_speed.count(Speed::Units::KByps);
|
|
auto const piece_download_speed = bandwidth().get_piece_speed(now_msec, TR_DOWN);
|
|
stats.pieceDownloadSpeed_KBps = piece_download_speed.count(Speed::Units::KByps);
|
|
|
|
stats.percentComplete = this->completion_.percent_complete();
|
|
stats.metadataPercentComplete = tr_torrentGetMetadataPercent(this);
|
|
|
|
stats.percentDone = this->completion_.percent_done();
|
|
stats.leftUntilDone = this->completion_.left_until_done();
|
|
stats.sizeWhenDone = this->completion_.size_when_done();
|
|
|
|
auto const verify_progress = this->verify_progress();
|
|
stats.recheckProgress = verify_progress.value_or(0.0);
|
|
stats.activityDate = this->date_active_;
|
|
stats.addedDate = this->date_added_;
|
|
stats.doneDate = this->date_done_;
|
|
stats.editDate = this->date_edited_;
|
|
stats.startDate = this->date_started_;
|
|
stats.secondsSeeding = this->seconds_seeding(now_sec);
|
|
stats.secondsDownloading = this->seconds_downloading(now_sec);
|
|
|
|
stats.corruptEver = this->bytes_corrupt_.ever();
|
|
stats.downloadedEver = this->bytes_downloaded_.ever();
|
|
stats.uploadedEver = this->bytes_uploaded_.ever();
|
|
stats.haveValid = this->completion_.has_valid();
|
|
stats.haveUnchecked = this->has_total() - stats.haveValid;
|
|
stats.desiredAvailable = tr_peerMgrGetDesiredAvailable(this);
|
|
|
|
stats.ratio = tr_getRatio(stats.uploadedEver, this->size_when_done());
|
|
|
|
auto seed_ratio_bytes_left = uint64_t{};
|
|
auto seed_ratio_bytes_goal = uint64_t{};
|
|
bool const seed_ratio_applies = tr_torrentGetSeedRatioBytes(this, &seed_ratio_bytes_left, &seed_ratio_bytes_goal);
|
|
|
|
// eta, etaIdle
|
|
stats.eta = TR_ETA_NOT_AVAIL;
|
|
stats.etaIdle = TR_ETA_NOT_AVAIL;
|
|
if (activity == TR_STATUS_DOWNLOAD)
|
|
{
|
|
if (auto const eta_speed_byps = eta_speed_.update(now_msec, piece_download_speed).base_quantity(); eta_speed_byps == 0U)
|
|
{
|
|
stats.eta = TR_ETA_UNKNOWN;
|
|
}
|
|
else if (stats.leftUntilDone <= stats.desiredAvailable || webseed_count() >= 1U)
|
|
{
|
|
stats.eta = stats.leftUntilDone / eta_speed_byps;
|
|
}
|
|
}
|
|
else if (activity == TR_STATUS_SEED)
|
|
{
|
|
auto const eta_speed_byps = eta_speed_.update(now_msec, piece_upload_speed).base_quantity();
|
|
|
|
if (seed_ratio_applies)
|
|
{
|
|
stats.eta = eta_speed_byps == 0U ? static_cast<time_t>(TR_ETA_UNKNOWN) : seed_ratio_bytes_left / eta_speed_byps;
|
|
}
|
|
|
|
if (eta_speed_byps < 1U)
|
|
{
|
|
if (auto const secs_left = idle_seconds_left(now_sec); secs_left)
|
|
{
|
|
stats.etaIdle = *secs_left;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* stats.haveValid is here to make sure a torrent isn't marked 'finished'
|
|
* when the user hits "uncheck all" prior to starting the torrent... */
|
|
stats.finished = this->finished_seeding_by_idle_ ||
|
|
(seed_ratio_applies && seed_ratio_bytes_left == 0 && stats.haveValid != 0);
|
|
|
|
if (!seed_ratio_applies || stats.finished)
|
|
{
|
|
stats.seedRatioPercentDone = 1.0F;
|
|
}
|
|
else if (seed_ratio_bytes_goal == 0) /* impossible? safeguard for div by zero */
|
|
{
|
|
stats.seedRatioPercentDone = 0.0F;
|
|
}
|
|
else
|
|
{
|
|
stats.seedRatioPercentDone = float(seed_ratio_bytes_goal - seed_ratio_bytes_left) / seed_ratio_bytes_goal;
|
|
}
|
|
|
|
/* test some of the constraints */
|
|
TR_ASSERT(stats.sizeWhenDone <= this->total_size());
|
|
TR_ASSERT(stats.leftUntilDone <= stats.sizeWhenDone);
|
|
TR_ASSERT(stats.desiredAvailable <= stats.leftUntilDone);
|
|
return stats;
|
|
}
|
|
|
|
tr_stat const* tr_torrentStat(tr_torrent* const tor)
|
|
{
|
|
tor->stats_ = tor->stats();
|
|
return &tor->stats_;
|
|
}
|
|
|
|
// ---
|
|
|
|
tr_file_view tr_torrentFile(tr_torrent const* tor, tr_file_index_t file)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
auto const& subpath = tor->file_subpath(file);
|
|
auto const priority = tor->file_priorities_.file_priority(file);
|
|
auto const wanted = tor->files_wanted_.file_wanted(file);
|
|
auto const length = tor->file_size(file);
|
|
auto const [begin, end] = tor->piece_span_for_file(file);
|
|
|
|
if (tor->is_seed() || length == 0)
|
|
{
|
|
return { subpath.c_str(), length, length, 1.0, begin, end, priority, wanted };
|
|
}
|
|
|
|
auto const have = tor->completion_.count_has_bytes_in_span(tor->byte_span_for_file(file));
|
|
return { subpath.c_str(), have, length, have >= length ? 1.0 : have / double(length), begin, end, priority, wanted };
|
|
}
|
|
|
|
size_t tr_torrentFileCount(tr_torrent const* torrent)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(torrent));
|
|
|
|
return torrent->file_count();
|
|
}
|
|
|
|
tr_webseed_view tr_torrentWebseed(tr_torrent const* tor, size_t nth)
|
|
{
|
|
return tr_peerMgrWebseed(tor, nth);
|
|
}
|
|
|
|
size_t tr_torrentWebseedCount(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->webseed_count();
|
|
}
|
|
|
|
tr_tracker_view tr_torrentTracker(tr_torrent const* tor, size_t i)
|
|
{
|
|
return tr_announcerTracker(tor, i);
|
|
}
|
|
|
|
size_t tr_torrentTrackerCount(tr_torrent const* tor)
|
|
{
|
|
return tr_announcerTrackerCount(tor);
|
|
}
|
|
|
|
tr_torrent_view tr_torrentView(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
auto ret = tr_torrent_view{};
|
|
ret.name = tor->name().c_str();
|
|
ret.hash_string = tor->info_hash_string().c_str();
|
|
ret.comment = tor->comment().c_str();
|
|
ret.creator = tor->creator().c_str();
|
|
ret.source = tor->source().c_str();
|
|
ret.total_size = tor->total_size();
|
|
ret.date_created = tor->date_created();
|
|
ret.piece_size = tor->piece_size();
|
|
ret.n_pieces = tor->piece_count();
|
|
ret.is_private = tor->is_private();
|
|
ret.is_folder = tor->file_count() > 1 || (tor->file_count() == 1 && tr_strv_contains(tor->file_subpath(0), '/'));
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string tr_torrentFilename(tr_torrent const* tor)
|
|
{
|
|
return std::string{ tor->torrent_file() };
|
|
}
|
|
|
|
size_t tr_torrentFilenameToBuf(tr_torrent const* tor, char* buf, size_t buflen)
|
|
{
|
|
return tr_strv_to_buf(tr_torrentFilename(tor), buf, buflen);
|
|
}
|
|
|
|
// ---
|
|
|
|
tr_peer_stat* tr_torrentPeers(tr_torrent const* tor, size_t* peer_count)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tr_peerMgrPeerStats(tor, peer_count);
|
|
}
|
|
|
|
void tr_torrentPeersFree(tr_peer_stat* peer_stats, size_t /*peer_count*/)
|
|
{
|
|
delete[] peer_stats;
|
|
}
|
|
|
|
void tr_torrentAvailability(tr_torrent const* tor, int8_t* tab, int size)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
if (tab != nullptr && size > 0)
|
|
{
|
|
tr_peerMgrTorrentAvailability(tor, tab, size);
|
|
}
|
|
}
|
|
|
|
void tr_torrentAmountFinished(tr_torrent const* tor, float* tabs, int n_tabs)
|
|
{
|
|
return tor->amount_done_bins(tabs, n_tabs);
|
|
}
|
|
|
|
// --- Start/Stop Callback
|
|
|
|
void tr_torrentStart(tr_torrent* tor)
|
|
{
|
|
if (tr_isTorrent(tor))
|
|
{
|
|
tor->start_when_stable_ = true;
|
|
tor->start(false /*bypass_queue*/, {});
|
|
}
|
|
}
|
|
|
|
void tr_torrentStartNow(tr_torrent* tor)
|
|
{
|
|
if (tr_isTorrent(tor))
|
|
{
|
|
tor->start_when_stable_ = true;
|
|
tor->start(true /*bypass_queue*/, {});
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrentVerify(tr_torrent* tor)
|
|
{
|
|
tor->session->run_in_session_thread(
|
|
[tor]()
|
|
{
|
|
TR_ASSERT(tor->session->am_in_session_thread());
|
|
auto const lock = tor->unique_lock();
|
|
|
|
if (tor->is_deleting_)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tor->session->verify_remove(tor);
|
|
|
|
if (!tor->has_metainfo())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (tor->is_running())
|
|
{
|
|
tor->stop_now();
|
|
}
|
|
|
|
if (did_files_disappear(tor))
|
|
{
|
|
tor->error().set_local_error(
|
|
_("Paused torrent as no data was found! Ensure your drives are connected or use \"Set Location\", "
|
|
"then use \"Verify Local Data\" again. To re-download, start the torrent."));
|
|
tor->start_when_stable_ = false;
|
|
}
|
|
|
|
tor->session->verify_add(tor);
|
|
});
|
|
}
|
|
|
|
void tr_torrent::set_verify_state(VerifyState const state)
|
|
{
|
|
TR_ASSERT(state == VerifyState::None || state == VerifyState::Queued || state == VerifyState::Active);
|
|
|
|
verify_state_ = state;
|
|
verify_progress_ = {};
|
|
mark_changed();
|
|
}
|
|
|
|
tr_torrent_metainfo const& tr_torrent::VerifyMediator::metainfo() const
|
|
{
|
|
return tor_->metainfo_;
|
|
}
|
|
|
|
std::optional<std::string> tr_torrent::VerifyMediator::find_file(tr_file_index_t const file_index) const
|
|
{
|
|
if (auto const found = tor_->find_file(file_index); found)
|
|
{
|
|
return std::string{ found->filename().sv() };
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void tr_torrent::VerifyMediator::on_verify_queued()
|
|
{
|
|
tr_logAddTraceTor(tor_, "Queued for verification");
|
|
tor_->set_verify_state(VerifyState::Queued);
|
|
}
|
|
|
|
void tr_torrent::VerifyMediator::on_verify_started()
|
|
{
|
|
tr_logAddDebugTor(tor_, "Verifying torrent");
|
|
time_started_ = tr_time();
|
|
tor_->set_verify_state(VerifyState::Active);
|
|
}
|
|
|
|
void tr_torrent::VerifyMediator::on_piece_checked(tr_piece_index_t const piece, bool const has_piece)
|
|
{
|
|
auto const had_piece = tor_->has_piece(piece);
|
|
|
|
if (has_piece || had_piece)
|
|
{
|
|
tor_->set_has_piece(piece, has_piece);
|
|
tor_->set_dirty();
|
|
}
|
|
|
|
tor_->checked_pieces_.set(piece, true);
|
|
tor_->mark_changed();
|
|
tor_->verify_progress_ = std::clamp(static_cast<float>(piece + 1U) / tor_->metainfo_.piece_count(), 0.0F, 1.0F);
|
|
}
|
|
|
|
// (usually called from tr_verify_worker's thread)
|
|
void tr_torrent::VerifyMediator::on_verify_done(bool const aborted)
|
|
{
|
|
if (time_started_.has_value())
|
|
{
|
|
auto const total_size = tor_->total_size();
|
|
auto const duration_secs = tr_time() - *time_started_;
|
|
tr_logAddDebugTor(
|
|
tor_,
|
|
fmt::format(
|
|
"Verification is done. It took {} seconds to verify {} bytes ({} bytes per second)",
|
|
duration_secs,
|
|
total_size,
|
|
total_size / (1 + duration_secs)));
|
|
}
|
|
|
|
tor_->set_verify_state(VerifyState::None);
|
|
|
|
if (!aborted && !tor_->is_deleting_)
|
|
{
|
|
tor_->session->run_in_session_thread(
|
|
[tor = tor_]()
|
|
{
|
|
if (tor->is_deleting_)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tor->recheck_completeness();
|
|
|
|
if (tor->verify_done_callback_)
|
|
{
|
|
tor->verify_done_callback_(tor);
|
|
}
|
|
|
|
if (tor->start_when_stable_)
|
|
{
|
|
tor->start(false, !tor->checked_pieces_.has_none());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::save_resume_file()
|
|
{
|
|
if (!is_dirty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
set_dirty(false);
|
|
auto helper = ResumeHelper{ *this };
|
|
tr_resume::save(this, helper);
|
|
}
|
|
|
|
// --- Completeness
|
|
|
|
namespace
|
|
{
|
|
namespace completeness_helpers
|
|
{
|
|
[[nodiscard]] constexpr char const* get_completion_string(int type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case TR_PARTIAL_SEED:
|
|
/* Translators: this is a minor point that's safe to skip over, but FYI:
|
|
"Complete" and "Done" are specific, different terms in Transmission:
|
|
"Complete" means we've downloaded every file in the torrent.
|
|
"Done" means we're done downloading the files we wanted, but NOT all
|
|
that exist */
|
|
return "Done";
|
|
|
|
case TR_SEED:
|
|
return "Complete";
|
|
|
|
default:
|
|
return "Incomplete";
|
|
}
|
|
}
|
|
} // namespace completeness_helpers
|
|
} // namespace
|
|
|
|
void tr_torrent::create_empty_files() const
|
|
{
|
|
auto const base = current_dir();
|
|
TR_ASSERT(!std::empty(base));
|
|
if (!has_metainfo() || std::empty(base))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const file_count = this->file_count();
|
|
for (tr_file_index_t file_index = 0U; file_index < file_count; ++file_index)
|
|
{
|
|
if (file_size(file_index) != 0U || !file_is_wanted(file_index) || find_file(file_index))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// torrent contains a wanted zero-bytes file and that file isn't on disk yet.
|
|
// We attempt to create that file.
|
|
auto filename = tr_pathbuf{};
|
|
auto const& subpath = file_subpath(file_index);
|
|
filename.assign(base, '/', subpath);
|
|
|
|
// create subfolders, if any
|
|
auto dir = tr_pathbuf{ filename.sv() };
|
|
dir.popdir();
|
|
tr_sys_dir_create(dir, TR_SYS_DIR_CREATE_PARENTS, 0777);
|
|
|
|
// create the file
|
|
if (auto const fd = tr_sys_file_open(filename, TR_SYS_FILE_WRITE | TR_SYS_FILE_CREATE | TR_SYS_FILE_SEQUENTIAL, 0666);
|
|
fd != TR_BAD_SYS_FILE)
|
|
{
|
|
tr_sys_file_close(fd);
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrent::recheck_completeness()
|
|
{
|
|
using namespace completeness_helpers;
|
|
|
|
auto const lock = unique_lock();
|
|
|
|
needs_completeness_check_ = false;
|
|
|
|
if (auto const new_completeness = completion_.status(); completeness_ != new_completeness)
|
|
{
|
|
bool const recent_change = bytes_downloaded_.during_this_session() != 0U;
|
|
bool const was_running = is_running();
|
|
|
|
if (recent_change)
|
|
{
|
|
tr_logAddTraceTor(
|
|
this,
|
|
fmt::format(
|
|
"State changed from {} to {}",
|
|
get_completion_string(completeness_),
|
|
get_completion_string(new_completeness)));
|
|
}
|
|
|
|
completeness_ = new_completeness;
|
|
session->close_torrent_files(id());
|
|
|
|
if (is_done())
|
|
{
|
|
if (recent_change)
|
|
{
|
|
tr_announcerTorrentCompleted(this);
|
|
mark_changed();
|
|
date_done_ = tr_time();
|
|
}
|
|
|
|
if (current_dir() == incomplete_dir())
|
|
{
|
|
set_location(download_dir(), true, nullptr);
|
|
}
|
|
|
|
done_.emit(this, recent_change);
|
|
}
|
|
|
|
session->onTorrentCompletenessChanged(this, completeness_, was_running);
|
|
|
|
set_dirty();
|
|
|
|
if (is_done())
|
|
{
|
|
save_resume_file();
|
|
callScriptIfEnabled(this, TR_SCRIPT_ON_TORRENT_DONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- File DND
|
|
|
|
void tr_torrentSetFileDLs(tr_torrent* tor, tr_file_index_t const* files, tr_file_index_t n_files, bool wanted)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_files_wanted(files, n_files, wanted);
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::set_labels(labels_t const& new_labels)
|
|
{
|
|
auto const lock = unique_lock();
|
|
labels_.clear();
|
|
|
|
for (auto label : new_labels)
|
|
{
|
|
if (std::find(std::begin(labels_), std::end(labels_), label) == std::end(labels_))
|
|
{
|
|
labels_.push_back(label);
|
|
}
|
|
}
|
|
labels_.shrink_to_fit();
|
|
this->set_dirty();
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::set_bandwidth_group(std::string_view group_name) noexcept
|
|
{
|
|
group_name = tr_strv_strip(group_name);
|
|
|
|
auto const lock = this->unique_lock();
|
|
|
|
if (std::empty(group_name))
|
|
{
|
|
this->bandwidth_group_ = tr_interned_string{};
|
|
this->bandwidth().set_parent(&this->session->top_bandwidth_);
|
|
}
|
|
else
|
|
{
|
|
this->bandwidth_group_ = group_name;
|
|
this->bandwidth().set_parent(&this->session->getBandwidthGroup(group_name));
|
|
}
|
|
|
|
this->set_dirty();
|
|
}
|
|
|
|
// ---
|
|
|
|
tr_priority_t tr_torrentGetPriority(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->get_priority();
|
|
}
|
|
|
|
void tr_torrentSetPriority(tr_torrent* const tor, tr_priority_t const priority)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(tr_isPriority(priority));
|
|
|
|
if (tor->bandwidth().get_priority() != priority)
|
|
{
|
|
tor->bandwidth().set_priority(priority);
|
|
|
|
tor->set_dirty();
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrentSetPeerLimit(tr_torrent* tor, uint16_t max_connected_peers)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tor->set_peer_limit(max_connected_peers);
|
|
}
|
|
|
|
uint16_t tr_torrentGetPeerLimit(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
return tor->peer_limit();
|
|
}
|
|
|
|
// ---
|
|
|
|
bool tr_torrentReqIsValid(tr_torrent const* tor, tr_piece_index_t index, uint32_t offset, uint32_t length)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
int err = 0;
|
|
|
|
if (index >= tor->piece_count())
|
|
{
|
|
err = 1;
|
|
}
|
|
else if (length < 1)
|
|
{
|
|
err = 2;
|
|
}
|
|
else if (offset + length > tor->piece_size(index))
|
|
{
|
|
err = 3;
|
|
}
|
|
else if (length > tr_block_info::BlockSize)
|
|
{
|
|
err = 4;
|
|
}
|
|
else if (tor->piece_loc(index, offset, length).byte > tor->total_size())
|
|
{
|
|
err = 5;
|
|
}
|
|
|
|
if (err != 0)
|
|
{
|
|
tr_logAddTraceTor(tor, fmt::format("index {} offset {} length {} err {}", index, offset, length, err));
|
|
}
|
|
|
|
return err == 0;
|
|
}
|
|
|
|
tr_block_span_t tr_torrent::block_span_for_file(tr_file_index_t const file) const noexcept
|
|
{
|
|
auto const [begin_byte, end_byte] = byte_span_for_file(file);
|
|
|
|
auto const begin_block = byte_loc(begin_byte).block;
|
|
if (begin_byte >= end_byte) // 0-byte file
|
|
{
|
|
return { begin_block, begin_block + 1 };
|
|
}
|
|
|
|
auto const final_block = byte_loc(end_byte - 1).block;
|
|
auto const end_block = final_block + 1;
|
|
return { begin_block, end_block };
|
|
}
|
|
|
|
// ---
|
|
|
|
bool tr_torrent::check_piece(tr_piece_index_t const piece) const
|
|
{
|
|
auto const pass = tr_ioTestPiece(*this, piece);
|
|
tr_logAddTraceTor(this, fmt::format("[LAZY] tr_torrent.checkPiece tested piece {}, pass=={}", piece, pass));
|
|
return pass;
|
|
}
|
|
|
|
// ---
|
|
|
|
bool tr_torrent::set_announce_list(std::string_view announce_list_str)
|
|
{
|
|
auto ann = tr_announce_list{};
|
|
return ann.parse(announce_list_str) && set_announce_list(std::move(ann));
|
|
}
|
|
|
|
bool tr_torrent::set_announce_list(tr_announce_list announce_list)
|
|
{
|
|
auto const lock = unique_lock();
|
|
|
|
auto& tgt = metainfo_.announce_list();
|
|
|
|
tgt = std::move(announce_list);
|
|
|
|
// save the changes
|
|
auto save_error = tr_error{};
|
|
auto filename = std::string{};
|
|
if (has_metainfo())
|
|
{
|
|
filename = torrent_file();
|
|
tgt.save(filename, &save_error);
|
|
}
|
|
else
|
|
{
|
|
filename = magnet_file();
|
|
tr_file_save(filename, magnet(), &save_error);
|
|
}
|
|
|
|
on_announce_list_changed();
|
|
|
|
if (save_error.has_value())
|
|
{
|
|
error().set_local_error(fmt::format(
|
|
_("Couldn't save '{path}': {error} ({error_code})"),
|
|
fmt::arg("path", filename),
|
|
fmt::arg("error", save_error.message()),
|
|
fmt::arg("error_code", save_error.code())));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void tr_torrent::on_announce_list_changed()
|
|
{
|
|
// if we had a tracker-related error on this torrent,
|
|
// and that tracker's been removed,
|
|
// then clear the error
|
|
if (auto const& error_url = error_.announce_url(); !std::empty(error_url))
|
|
{
|
|
auto const& ann = metainfo().announce_list();
|
|
if (std::any_of(
|
|
std::begin(ann),
|
|
std::end(ann),
|
|
[error_url](auto const& tracker) { return tracker.announce == error_url; }))
|
|
{
|
|
error_.clear();
|
|
}
|
|
}
|
|
|
|
mark_edited();
|
|
|
|
session->announcer_->resetTorrent(this);
|
|
}
|
|
|
|
void tr_torrent::on_tracker_response(tr_tracker_event const* event)
|
|
{
|
|
switch (event->type)
|
|
{
|
|
case tr_tracker_event::Type::Peers:
|
|
tr_logAddTraceTor(this, fmt::format("Got {} peers from tracker", std::size(event->pex)));
|
|
tr_peerMgrAddPex(this, TR_PEER_FROM_TRACKER, std::data(event->pex), std::size(event->pex));
|
|
break;
|
|
|
|
case tr_tracker_event::Type::Counts:
|
|
if (is_private() && (event->leechers == 0))
|
|
{
|
|
swarm_is_all_seeds_.emit(this);
|
|
}
|
|
|
|
break;
|
|
|
|
case tr_tracker_event::Type::Warning:
|
|
tr_logAddWarnTor(
|
|
this,
|
|
fmt::format(
|
|
_("Tracker warning: '{warning}' ({url})"),
|
|
fmt::arg("warning", event->text),
|
|
fmt::arg("url", tr_urlTrackerLogName(event->announce_url))));
|
|
error_.set_tracker_warning(event->announce_url, event->text);
|
|
break;
|
|
|
|
case tr_tracker_event::Type::Error:
|
|
error_.set_tracker_error(event->announce_url, event->text);
|
|
break;
|
|
|
|
case tr_tracker_event::Type::ErrorClear:
|
|
error_.clear_if_tracker();
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool tr_torrentSetTrackerList(tr_torrent* tor, char const* text)
|
|
{
|
|
return text != nullptr && tor->set_announce_list(text);
|
|
}
|
|
|
|
std::string tr_torrentGetTrackerList(tr_torrent const* tor)
|
|
{
|
|
return tor->announce_list().to_string();
|
|
}
|
|
|
|
size_t tr_torrentGetTrackerListToBuf(tr_torrent const* tor, char* buf, size_t buflen)
|
|
{
|
|
return tr_strv_to_buf(tr_torrentGetTrackerList(tor), buf, buflen);
|
|
}
|
|
|
|
// ---
|
|
|
|
uint64_t tr_torrentGetBytesLeftToAllocate(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
uint64_t bytes_left = 0;
|
|
|
|
for (tr_file_index_t i = 0, n = tor->file_count(); i < n; ++i)
|
|
{
|
|
if (auto const wanted = tor->files_wanted_.file_wanted(i); !wanted)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const length = tor->file_size(i);
|
|
bytes_left += length;
|
|
|
|
auto const found = tor->find_file(i);
|
|
if (found)
|
|
{
|
|
bytes_left -= found->size;
|
|
}
|
|
}
|
|
|
|
return bytes_left;
|
|
}
|
|
|
|
// ---
|
|
|
|
std::string_view tr_torrent::primary_mime_type() const
|
|
{
|
|
// count up how many bytes there are for each mime-type in the torrent
|
|
// NB: get_mime_type_for_filename() always returns the same ptr for a
|
|
// mime_type, so its raw pointer can be used as a key.
|
|
auto size_per_mime_type = small::unordered_map<std::string_view, size_t, 256U>{};
|
|
for (tr_file_index_t i = 0, n = this->file_count(); i < n; ++i)
|
|
{
|
|
auto const mime_type = tr_get_mime_type_for_filename(this->file_subpath(i));
|
|
size_per_mime_type[mime_type] += this->file_size(i);
|
|
}
|
|
|
|
if (std::empty(size_per_mime_type))
|
|
{
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
// application/octet-stream is the default value for all other cases.
|
|
// An unknown file type should use this type.
|
|
auto constexpr Fallback = "application/octet-stream"sv;
|
|
return Fallback;
|
|
}
|
|
|
|
auto const it = std::max_element(
|
|
std::begin(size_per_mime_type),
|
|
std::end(size_per_mime_type),
|
|
[](auto const& a, auto const& b) { return a.second < b.second; });
|
|
return it->first;
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::on_file_completed(tr_file_index_t const file)
|
|
{
|
|
/* close the file so that we can reopen in read-only mode as needed */
|
|
session->close_torrent_file(*this, file);
|
|
|
|
/* now that the file is complete and closed, we can start watching its
|
|
* mtime timestamp for changes to know if we need to reverify pieces */
|
|
file_mtimes_[file] = tr_time();
|
|
|
|
/* if the torrent's current filename isn't the same as the one in the
|
|
* metadata -- for example, if it had the ".part" suffix appended to
|
|
* it until now -- then rename it to match the one in the metadata */
|
|
if (auto found = find_file(file); found)
|
|
{
|
|
if (auto const& file_subpath = this->file_subpath(file); file_subpath != found->subpath())
|
|
{
|
|
auto const& oldpath = found->filename();
|
|
auto const newpath = tr_pathbuf{ found->base(), '/', file_subpath };
|
|
auto error = tr_error{};
|
|
|
|
if (!tr_sys_path_rename(oldpath, newpath, &error))
|
|
{
|
|
tr_logAddErrorTor(
|
|
this,
|
|
fmt::format(
|
|
_("Couldn't move '{old_path}' to '{path}': {error} ({error_code})"),
|
|
fmt::arg("old_path", oldpath),
|
|
fmt::arg("path", newpath),
|
|
fmt::arg("error", error.message()),
|
|
fmt::arg("error_code", error.code())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrent::on_piece_completed(tr_piece_index_t const piece)
|
|
{
|
|
piece_completed_.emit(this, piece);
|
|
|
|
// bookkeeping
|
|
set_needs_completeness_check();
|
|
|
|
// if this piece completes any file, invoke the fileCompleted func for it
|
|
auto const [file_begin, file_end] = fpm_.file_span_for_piece(piece);
|
|
for (auto file = file_begin; file < file_end; ++file)
|
|
{
|
|
if (completion_.has_blocks(block_span_for_file(file)))
|
|
{
|
|
on_file_completed(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_torrent::on_piece_failed(tr_piece_index_t const piece)
|
|
{
|
|
tr_logAddDebugTor(this, fmt::format("Piece {}, which was just downloaded, failed its checksum test", piece));
|
|
|
|
auto const n = piece_size(piece);
|
|
bytes_corrupt_ += n;
|
|
bytes_downloaded_.reduce(n);
|
|
got_bad_piece_.emit(this, piece);
|
|
set_has_piece(piece, false);
|
|
}
|
|
|
|
void tr_torrent::on_block_received(tr_block_index_t const block)
|
|
{
|
|
TR_ASSERT(session->am_in_session_thread());
|
|
|
|
if (has_block(block))
|
|
{
|
|
tr_logAddDebugTor(this, "we have this block already...");
|
|
bytes_downloaded_.reduce(block_size(block));
|
|
return;
|
|
}
|
|
|
|
set_dirty();
|
|
|
|
completion_.add_block(block);
|
|
|
|
auto const block_loc = this->block_loc(block);
|
|
auto const first_piece = block_loc.piece;
|
|
auto const last_piece = byte_loc(block_loc.byte + block_size(block) - 1).piece;
|
|
for (auto piece = first_piece; piece <= last_piece; ++piece)
|
|
{
|
|
if (!has_piece(piece))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (check_piece(piece))
|
|
{
|
|
on_piece_completed(piece);
|
|
}
|
|
else
|
|
{
|
|
on_piece_failed(piece);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
std::string tr_torrentFindFile(tr_torrent const* tor, tr_file_index_t file_num)
|
|
{
|
|
auto const found = tor->find_file(file_num);
|
|
return std::string{ found ? found->filename().sv() : ""sv };
|
|
}
|
|
|
|
size_t tr_torrentFindFileToBuf(tr_torrent const* tor, tr_file_index_t file_num, char* buf, size_t buflen)
|
|
{
|
|
return tr_strv_to_buf(tr_torrentFindFile(tor, file_num), buf, buflen);
|
|
}
|
|
|
|
void tr_torrent::set_download_dir(std::string_view path, bool is_new_torrent)
|
|
{
|
|
download_dir_ = path;
|
|
mark_changed();
|
|
mark_edited();
|
|
set_dirty();
|
|
refresh_current_dir();
|
|
|
|
if (is_new_torrent)
|
|
{
|
|
if (session->shouldFullyVerifyAddedTorrents() || !is_new_torrent_a_seed())
|
|
{
|
|
tr_torrentVerify(this);
|
|
}
|
|
else
|
|
{
|
|
completion_.set_has_all();
|
|
date_done_ = date_added_;
|
|
recheck_completeness();
|
|
}
|
|
}
|
|
else if (error_.error_type() == TR_STAT_LOCAL_ERROR && !set_local_error_if_files_disappeared(this))
|
|
{
|
|
error_.clear();
|
|
}
|
|
}
|
|
|
|
// decide whether we should be looking for files in downloadDir or incompleteDir
|
|
void tr_torrent::refresh_current_dir()
|
|
{
|
|
auto dir = tr_interned_string{};
|
|
|
|
if (std::empty(incomplete_dir()))
|
|
{
|
|
dir = download_dir();
|
|
}
|
|
else if (!has_metainfo()) // no files to find
|
|
{
|
|
dir = incomplete_dir();
|
|
}
|
|
else
|
|
{
|
|
auto const found = find_file(0);
|
|
dir = found ? tr_interned_string{ found->base() } : incomplete_dir();
|
|
}
|
|
|
|
TR_ASSERT(!std::empty(dir));
|
|
TR_ASSERT(dir == download_dir() || dir == incomplete_dir());
|
|
|
|
current_dir_ = dir;
|
|
}
|
|
|
|
// --- RENAME
|
|
|
|
namespace
|
|
{
|
|
namespace rename_helpers
|
|
{
|
|
bool renameArgsAreValid(tr_torrent const* tor, std::string_view oldpath, std::string_view newname)
|
|
{
|
|
if (std::empty(oldpath) || std::empty(newname) || newname == "."sv || newname == ".."sv ||
|
|
tr_strv_contains(newname, TR_PATH_DELIMITER))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const newpath = tr_strv_contains(oldpath, TR_PATH_DELIMITER) ?
|
|
tr_pathbuf{ tr_sys_path_dirname(oldpath), '/', newname } :
|
|
tr_pathbuf{ newname };
|
|
|
|
if (newpath == oldpath)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
auto const newpath_as_dir = tr_pathbuf{ newpath, '/' };
|
|
auto const n_files = tor->file_count();
|
|
|
|
for (tr_file_index_t i = 0; i < n_files; ++i)
|
|
{
|
|
auto const& name = tor->file_subpath(i);
|
|
if (newpath == name || tr_strv_starts_with(name, newpath_as_dir))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
auto renameFindAffectedFiles(tr_torrent const* tor, std::string_view oldpath)
|
|
{
|
|
auto indices = std::vector<tr_file_index_t>{};
|
|
auto const oldpath_as_dir = tr_pathbuf{ oldpath, '/' };
|
|
auto const n_files = tor->file_count();
|
|
|
|
for (tr_file_index_t i = 0; i < n_files; ++i)
|
|
{
|
|
auto const& name = tor->file_subpath(i);
|
|
if (name == oldpath || tr_strv_starts_with(name, oldpath_as_dir))
|
|
{
|
|
indices.push_back(i);
|
|
}
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
|
|
int renamePath(tr_torrent const* tor, std::string_view oldpath, std::string_view newname)
|
|
{
|
|
int err = 0;
|
|
|
|
auto const base = tor->is_done() || std::empty(tor->incomplete_dir()) ? tor->download_dir() : tor->incomplete_dir();
|
|
|
|
auto src = tr_pathbuf{ base, '/', oldpath };
|
|
|
|
if (!tr_sys_path_exists(src)) /* check for it as a partial */
|
|
{
|
|
src += tr_torrent_files::PartialFileSuffix;
|
|
}
|
|
|
|
if (tr_sys_path_exists(src))
|
|
{
|
|
auto const parent = tr_sys_path_dirname(src);
|
|
auto const tgt = tr_strv_ends_with(src, tr_torrent_files::PartialFileSuffix) ?
|
|
tr_pathbuf{ parent, '/', newname, tr_torrent_files::PartialFileSuffix } :
|
|
tr_pathbuf{ parent, '/', newname };
|
|
|
|
auto tmp = errno;
|
|
bool const tgt_exists = tr_sys_path_exists(tgt);
|
|
errno = tmp;
|
|
|
|
if (!tgt_exists)
|
|
{
|
|
tmp = errno;
|
|
|
|
if (auto error = tr_error{}; !tr_sys_path_rename(src, tgt, &error))
|
|
{
|
|
err = error.code();
|
|
}
|
|
|
|
errno = tmp;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
void renameTorrentFileString(tr_torrent* tor, std::string_view oldpath, std::string_view newname, tr_file_index_t file_index)
|
|
{
|
|
auto name = std::string{};
|
|
auto const subpath = std::string_view{ tor->file_subpath(file_index) };
|
|
auto const oldpath_len = std::size(oldpath);
|
|
|
|
if (!tr_strv_contains(oldpath, TR_PATH_DELIMITER))
|
|
{
|
|
if (oldpath_len >= std::size(subpath))
|
|
{
|
|
name = newname;
|
|
}
|
|
else
|
|
{
|
|
name = fmt::format(FMT_STRING("{:s}/{:s}"sv), newname, subpath.substr(oldpath_len + 1));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto const tmp = tr_sys_path_dirname(oldpath);
|
|
|
|
if (std::empty(tmp))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (oldpath_len >= std::size(subpath))
|
|
{
|
|
name = fmt::format(FMT_STRING("{:s}/{:s}"sv), tmp, newname);
|
|
}
|
|
else
|
|
{
|
|
name = fmt::format(FMT_STRING("{:s}/{:s}/{:s}"sv), tmp, newname, subpath.substr(oldpath_len + 1));
|
|
}
|
|
}
|
|
|
|
if (subpath != name)
|
|
{
|
|
tor->set_file_subpath(file_index, name);
|
|
}
|
|
}
|
|
|
|
} // namespace rename_helpers
|
|
} // namespace
|
|
|
|
void tr_torrent::rename_path_in_session_thread(
|
|
std::string_view const oldpath,
|
|
std::string_view const newname,
|
|
tr_torrent_rename_done_func const callback,
|
|
void* const callback_user_data)
|
|
{
|
|
using namespace rename_helpers;
|
|
|
|
auto error = int{ 0 };
|
|
|
|
if (!renameArgsAreValid(this, oldpath, newname))
|
|
{
|
|
error = EINVAL;
|
|
}
|
|
else if (auto const file_indices = renameFindAffectedFiles(this, oldpath); std::empty(file_indices))
|
|
{
|
|
error = EINVAL;
|
|
}
|
|
else
|
|
{
|
|
error = renamePath(this, oldpath, newname);
|
|
|
|
if (error == 0)
|
|
{
|
|
/* update tr_info.files */
|
|
for (auto const& file_index : file_indices)
|
|
{
|
|
renameTorrentFileString(this, oldpath, newname, file_index);
|
|
}
|
|
|
|
/* update tr_info.name if user changed the toplevel */
|
|
if (std::size(file_indices) == file_count() && !tr_strv_contains(oldpath, '/'))
|
|
{
|
|
set_name(newname);
|
|
}
|
|
|
|
mark_edited();
|
|
set_dirty();
|
|
}
|
|
}
|
|
|
|
mark_changed();
|
|
|
|
if (callback != nullptr)
|
|
{
|
|
auto const szold = tr_pathbuf{ oldpath };
|
|
auto const sznew = tr_pathbuf{ newname };
|
|
(*callback)(this, szold.c_str(), sznew.c_str(), error, callback_user_data);
|
|
}
|
|
}
|
|
|
|
void tr_torrent::rename_path(
|
|
std::string_view oldpath,
|
|
std::string_view newname,
|
|
tr_torrent_rename_done_func callback,
|
|
void* callback_user_data)
|
|
{
|
|
this->session->run_in_session_thread(
|
|
[this, oldpath = std::string(oldpath), newname = std::string(newname), callback, callback_user_data]()
|
|
{ rename_path_in_session_thread(oldpath, newname, callback, callback_user_data); });
|
|
}
|
|
|
|
void tr_torrentRenamePath(
|
|
tr_torrent* tor,
|
|
char const* oldpath,
|
|
char const* newname,
|
|
tr_torrent_rename_done_func callback,
|
|
void* callback_user_data)
|
|
{
|
|
oldpath = oldpath != nullptr ? oldpath : "";
|
|
newname = newname != nullptr ? newname : "";
|
|
|
|
tor->rename_path(oldpath, newname, callback, callback_user_data);
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrentSetFilePriorities(
|
|
tr_torrent* tor,
|
|
tr_file_index_t const* files,
|
|
tr_file_index_t file_count,
|
|
tr_priority_t priority)
|
|
{
|
|
tor->set_file_priorities(files, file_count, priority);
|
|
}
|
|
|
|
bool tr_torrentHasMetadata(tr_torrent const* tor)
|
|
{
|
|
return tor->has_metainfo();
|
|
}
|
|
|
|
void tr_torrent::mark_edited()
|
|
{
|
|
this->date_edited_ = tr_time();
|
|
}
|
|
|
|
void tr_torrent::mark_changed()
|
|
{
|
|
this->bump_date_changed(tr_time());
|
|
}
|
|
|
|
[[nodiscard]] bool tr_torrent::ensure_piece_is_checked(tr_piece_index_t piece)
|
|
{
|
|
TR_ASSERT(piece < this->piece_count());
|
|
|
|
if (is_piece_checked(piece))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool const checked = check_piece(piece);
|
|
mark_changed();
|
|
set_dirty();
|
|
|
|
checked_pieces_.set(piece, checked);
|
|
return checked;
|
|
}
|
|
|
|
// --- RESUME HELPER
|
|
|
|
tr_bitfield const& tr_torrent::ResumeHelper::checked_pieces() const noexcept
|
|
{
|
|
return tor_.checked_pieces_;
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_checked_pieces(tr_bitfield const& checked, time_t const* mtimes /*fileCount()*/)
|
|
{
|
|
TR_ASSERT(std::size(checked) == tor_.piece_count());
|
|
tor_.checked_pieces_ = checked;
|
|
|
|
auto const n_files = tor_.file_count();
|
|
tor_.file_mtimes_.resize(n_files);
|
|
|
|
for (size_t file = 0; file < n_files; ++file)
|
|
{
|
|
auto const found = tor_.find_file(file);
|
|
auto const mtime = found ? found->last_modified_at : 0;
|
|
|
|
tor_.file_mtimes_[file] = mtime;
|
|
|
|
// if a file has changed, mark its pieces as unchecked
|
|
if (mtime == 0 || mtime != mtimes[file])
|
|
{
|
|
auto const [piece_begin, piece_end] = tor_.piece_span_for_file(file);
|
|
tor_.checked_pieces_.unset_span(piece_begin, piece_end);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
tr_bitfield const& tr_torrent::ResumeHelper::blocks() const noexcept
|
|
{
|
|
return tor_.completion_.blocks();
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_blocks(tr_bitfield blocks)
|
|
{
|
|
tor_.completion_.set_blocks(std::move(blocks));
|
|
}
|
|
|
|
// ---
|
|
|
|
time_t tr_torrent::ResumeHelper::date_active() const noexcept
|
|
{
|
|
return tor_.date_active_;
|
|
}
|
|
|
|
// ---
|
|
|
|
time_t tr_torrent::ResumeHelper::date_added() const noexcept
|
|
{
|
|
return tor_.date_added_;
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_date_added(time_t when) noexcept
|
|
{
|
|
tor_.date_added_ = when;
|
|
}
|
|
|
|
// ---
|
|
|
|
time_t tr_torrent::ResumeHelper::date_done() const noexcept
|
|
{
|
|
return tor_.date_done_;
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_date_done(time_t when) noexcept
|
|
{
|
|
tor_.date_done_ = when;
|
|
}
|
|
|
|
// ---
|
|
|
|
time_t tr_torrent::ResumeHelper::seconds_downloading(time_t now) const noexcept
|
|
{
|
|
return tor_.seconds_downloading(now);
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_seconds_downloading_before_current_start(time_t when) noexcept
|
|
{
|
|
tor_.seconds_downloading_before_current_start_ = when;
|
|
}
|
|
|
|
// ---
|
|
|
|
time_t tr_torrent::ResumeHelper::seconds_seeding(time_t now) const noexcept
|
|
{
|
|
return tor_.seconds_seeding(now);
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_seconds_seeding_before_current_start(time_t when) noexcept
|
|
{
|
|
tor_.seconds_seeding_before_current_start_ = when;
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::ResumeHelper::load_download_dir(std::string_view const dir) noexcept
|
|
{
|
|
bool const is_current_dir = tor_.current_dir_ == tor_.download_dir_;
|
|
tor_.download_dir_ = dir;
|
|
if (is_current_dir)
|
|
{
|
|
tor_.current_dir_ = tor_.download_dir_;
|
|
}
|
|
}
|
|
|
|
void tr_torrent::ResumeHelper::load_incomplete_dir(std::string_view const dir) noexcept
|
|
{
|
|
bool const is_current_dir = tor_.current_dir_ == tor_.incomplete_dir_;
|
|
tor_.incomplete_dir_ = dir;
|
|
if (is_current_dir)
|
|
{
|
|
tor_.current_dir_ = tor_.incomplete_dir_;
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
void tr_torrent::ResumeHelper::load_start_when_stable(bool const val) noexcept
|
|
{
|
|
tor_.start_when_stable_ = val;
|
|
}
|
|
|
|
bool tr_torrent::ResumeHelper::start_when_stable() const noexcept
|
|
{
|
|
return tor_.start_when_stable_;
|
|
}
|
|
|
|
// ---
|
|
|
|
std::vector<time_t> const& tr_torrent::ResumeHelper::file_mtimes() const noexcept
|
|
{
|
|
return tor_.file_mtimes_;
|
|
}
|