// 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 #include #include // EINVAL #include /* INT_MAX */ #include // size_t #include #include #include #include #include #include #include #include #include #include #include #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{}; tor->use_metainfo_from_file(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 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 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{ 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{ { "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::min(); constexpr auto MaxQueuePosition = std::numeric_limits::max(); #ifdef TR_ENABLE_ASSERTS [[nodiscard]] bool torrents_are_sorted_by_queue_position(std::vector 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(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(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(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(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 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. session->queue_session_thread(tr_torrentVerify, this); } else { completion_.set_has_all(); date_done_ = date_added_; recheck_completeness(); if (start_when_stable_) { start(false, {}); } else if (is_running()) { stop_soon(); } } } 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{}; 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 = 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::find_file(tr_file_index_t file_index) const { using namespace location_helpers; auto paths = std::array{}; 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{}; 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 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(*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 = get_metadata_percent(); 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(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) { 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 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(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(); } // --- 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 }; } // --- void tr_torrent::set_file_priorities(tr_file_index_t const* files, tr_file_index_t file_count, tr_priority_t priority) { if (std::any_of( files, files + file_count, [this, priority](tr_file_index_t file) { return priority != file_priorities_.file_priority(file); })) { file_priorities_.set(files, file_count, priority); priority_changed_.emit(this, files, file_count, priority); set_dirty(); } } // --- 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{}; 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 for (auto [file, file_end] = fpm_.file_span_for_piece(piece); 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, '/')) { return false; } auto const newpath = tr_strv_contains(oldpath, '/') ? 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{}; 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, '/')) { if (oldpath_len >= std::size(subpath)) { name = newname; } else { name = fmt::format("{: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("{:s}/{:s}"sv, tmp, newname); } else { name = fmt::format("{: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 = 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 const& tr_torrent::ResumeHelper::file_mtimes() const noexcept { return tor_.file_mtimes_; }