mirror of
https://github.com/transmission/transmission.git
synced 2025-12-24 12:28:52 +00:00
refactor: tr_metainfo_builder() (#3565)
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <glibmm.h>
|
||||
#include <glibmm/i18n.h>
|
||||
@@ -14,6 +15,8 @@
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
|
||||
#include <libtransmission/error.h>
|
||||
#include <libtransmission/makemeta.h>
|
||||
#include <libtransmission/utils.h> /* tr_formatter_mem_B() */
|
||||
|
||||
@@ -23,6 +26,8 @@
|
||||
#include "Session.h"
|
||||
#include "Utils.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -34,12 +39,18 @@ public:
|
||||
MakeProgressDialog(
|
||||
Gtk::Window& parent,
|
||||
tr_metainfo_builder& builder,
|
||||
std::string const& target,
|
||||
std::future<tr_error*> future,
|
||||
std::string_view target,
|
||||
Glib::RefPtr<Session> const& core);
|
||||
~MakeProgressDialog() override;
|
||||
|
||||
TR_DISABLE_COPY_MOVE(MakeProgressDialog)
|
||||
|
||||
[[nodiscard]] bool success() const
|
||||
{
|
||||
return success_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool onProgressDialogRefresh();
|
||||
void onProgressDialogResponse(int response);
|
||||
@@ -48,8 +59,10 @@ private:
|
||||
|
||||
private:
|
||||
tr_metainfo_builder& builder_;
|
||||
std::future<tr_error*> future_;
|
||||
std::string const target_;
|
||||
Glib::RefPtr<Session> const core_;
|
||||
bool success_ = false;
|
||||
|
||||
sigc::connection progress_tag_;
|
||||
Gtk::Label* progress_label_ = nullptr;
|
||||
@@ -80,9 +93,9 @@ private:
|
||||
|
||||
void updatePiecesLabel();
|
||||
|
||||
void setFilename(std::string const& filename);
|
||||
void setFilename(std::string_view filename);
|
||||
|
||||
void makeProgressDialog(std::string const& target);
|
||||
void makeProgressDialog(std::string_view target, std::future<tr_error*> future);
|
||||
void configurePieceSizeScale();
|
||||
void onPieceSizeUpdated();
|
||||
|
||||
@@ -104,57 +117,66 @@ private:
|
||||
Gtk::Entry* source_entry_ = nullptr;
|
||||
std::unique_ptr<MakeProgressDialog> progress_dialog_;
|
||||
Glib::RefPtr<Gtk::TextBuffer> announce_text_buffer_;
|
||||
std::unique_ptr<tr_metainfo_builder, void (*)(tr_metainfo_builder*)> builder_ = { nullptr, nullptr };
|
||||
std::optional<tr_metainfo_builder> builder_;
|
||||
};
|
||||
|
||||
bool MakeProgressDialog::onProgressDialogRefresh()
|
||||
{
|
||||
Glib::ustring str;
|
||||
double const fraction = builder_.pieceCount != 0 ? (double)builder_.pieceIndex / builder_.pieceCount : 0;
|
||||
auto const base = Glib::path_get_basename(builder_.top);
|
||||
auto const is_done = !future_.valid() || future_.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
|
||||
/* progress label */
|
||||
if (!builder_.isDone)
|
||||
if (is_done)
|
||||
{
|
||||
progress_tag_.disconnect();
|
||||
}
|
||||
|
||||
// progress value
|
||||
auto percent_done = 1.0;
|
||||
auto piece_index = tr_piece_index_t{};
|
||||
|
||||
if (!is_done)
|
||||
{
|
||||
auto const [current, total] = builder_.checksumStatus();
|
||||
percent_done = static_cast<double>(current) / total;
|
||||
piece_index = current;
|
||||
}
|
||||
|
||||
// progress text
|
||||
auto str = std::string{};
|
||||
auto success = false;
|
||||
auto const base = Glib::path_get_basename(builder_.top());
|
||||
if (!is_done)
|
||||
{
|
||||
str = fmt::format(_("Creating '{path}'"), fmt::arg("path", base));
|
||||
}
|
||||
else if (builder_.result == TrMakemetaResult::OK)
|
||||
{
|
||||
str = fmt::format(_("Created '{path}'"), fmt::arg("path", base));
|
||||
}
|
||||
else if (builder_.result == TrMakemetaResult::CANCELLED)
|
||||
{
|
||||
str = _("Cancelled");
|
||||
}
|
||||
else if (builder_.result == TrMakemetaResult::ERR_URL)
|
||||
{
|
||||
str = fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", builder_.errfile));
|
||||
}
|
||||
else if (builder_.result == TrMakemetaResult::ERR_IO_READ)
|
||||
{
|
||||
str = fmt::format(
|
||||
_("Couldn't read '{path}': {error} ({error_code})"),
|
||||
fmt::arg("path", builder_.errfile),
|
||||
fmt::arg("error", Glib::strerror(builder_.my_errno)),
|
||||
fmt::arg("error_code", builder_.my_errno));
|
||||
}
|
||||
else if (builder_.result == TrMakemetaResult::ERR_IO_WRITE)
|
||||
{
|
||||
str = fmt::format(
|
||||
_("Couldn't save '{path}': {error} ({error_code})"),
|
||||
fmt::arg("path", builder_.errfile),
|
||||
fmt::arg("error", Glib::strerror(builder_.my_errno)),
|
||||
fmt::arg("error_code", builder_.my_errno));
|
||||
}
|
||||
else
|
||||
{
|
||||
g_assert_not_reached();
|
||||
tr_error* error = future_.get();
|
||||
|
||||
if (error == nullptr)
|
||||
{
|
||||
builder_.save(target_, &error);
|
||||
}
|
||||
|
||||
if (error == nullptr)
|
||||
{
|
||||
str = fmt::format(_("Created '{path}'"), fmt::arg("path", base));
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
str = fmt::format(
|
||||
_("Couldn't create '{path}': {error} ({error_code})"),
|
||||
fmt::arg("path", base),
|
||||
fmt::arg("error", error->message),
|
||||
fmt::arg("error_code", error->code));
|
||||
tr_error_free(error);
|
||||
}
|
||||
}
|
||||
|
||||
gtr_label_set_text(*progress_label_, str);
|
||||
|
||||
/* progress bar */
|
||||
if (builder_.pieceIndex == 0)
|
||||
if (piece_index == 0)
|
||||
{
|
||||
str.clear();
|
||||
}
|
||||
@@ -163,17 +185,18 @@ bool MakeProgressDialog::onProgressDialogRefresh()
|
||||
/* how much data we've scanned through to generate checksums */
|
||||
str = fmt::format(
|
||||
_("Scanned {file_size}"),
|
||||
fmt::arg("file_size", tr_strlsize((uint64_t)builder_.pieceIndex * (uint64_t)builder_.pieceSize)));
|
||||
fmt::arg("file_size", tr_strlsize(static_cast<uint64_t>(piece_index) * builder_.pieceSize())));
|
||||
}
|
||||
|
||||
progress_bar_->set_fraction(fraction);
|
||||
progress_bar_->set_fraction(percent_done);
|
||||
progress_bar_->set_text(str);
|
||||
|
||||
/* buttons */
|
||||
set_response_sensitive(Gtk::RESPONSE_CANCEL, !builder_.isDone);
|
||||
set_response_sensitive(Gtk::RESPONSE_CLOSE, builder_.isDone);
|
||||
set_response_sensitive(Gtk::RESPONSE_ACCEPT, builder_.isDone && builder_.result == TrMakemetaResult::OK);
|
||||
set_response_sensitive(Gtk::RESPONSE_CANCEL, !is_done);
|
||||
set_response_sensitive(Gtk::RESPONSE_CLOSE, is_done);
|
||||
set_response_sensitive(Gtk::RESPONSE_ACCEPT, is_done && success);
|
||||
|
||||
success_ = success;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -186,7 +209,7 @@ void MakeProgressDialog::addTorrent()
|
||||
{
|
||||
tr_ctor* ctor = tr_ctorNew(core_->get_session());
|
||||
tr_ctorSetMetainfoFromFile(ctor, target_.c_str(), nullptr);
|
||||
tr_ctorSetDownloadDir(ctor, TR_FORCE, Glib::path_get_dirname(builder_.top).c_str());
|
||||
tr_ctorSetDownloadDir(ctor, TR_FORCE, Glib::path_get_dirname(builder_.top()).c_str());
|
||||
core_->add_ctor(ctor);
|
||||
}
|
||||
|
||||
@@ -195,7 +218,7 @@ void MakeProgressDialog::onProgressDialogResponse(int response)
|
||||
switch (response)
|
||||
{
|
||||
case Gtk::RESPONSE_CANCEL:
|
||||
builder_.abortFlag = true;
|
||||
builder_.cancelChecksums();
|
||||
hide();
|
||||
break;
|
||||
|
||||
@@ -215,12 +238,14 @@ void MakeProgressDialog::onProgressDialogResponse(int response)
|
||||
MakeProgressDialog::MakeProgressDialog(
|
||||
Gtk::Window& parent,
|
||||
tr_metainfo_builder& builder,
|
||||
std::string const& target,
|
||||
std::future<tr_error*> future,
|
||||
std::string_view target,
|
||||
Glib::RefPtr<Session> const& core)
|
||||
: Gtk::Dialog(_("New Torrent"), parent, true)
|
||||
, builder_(builder)
|
||||
, target_(target)
|
||||
, core_(core)
|
||||
, builder_{ builder }
|
||||
, future_{ std::move(future) }
|
||||
, target_{ target }
|
||||
, core_{ core }
|
||||
{
|
||||
add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
|
||||
add_button(_("_Close"), Gtk::RESPONSE_CLOSE);
|
||||
@@ -250,15 +275,15 @@ MakeProgressDialog::MakeProgressDialog(
|
||||
gtr_dialog_set_content(*this, *fr);
|
||||
}
|
||||
|
||||
void MakeDialog::Impl::makeProgressDialog(std::string const& target)
|
||||
void MakeDialog::Impl::makeProgressDialog(std::string_view target, std::future<tr_error*> future)
|
||||
{
|
||||
progress_dialog_ = std::make_unique<MakeProgressDialog>(dialog_, *builder_, target, core_);
|
||||
progress_dialog_ = std::make_unique<MakeProgressDialog>(dialog_, *builder_, std::move(future), target, core_);
|
||||
progress_dialog_->signal_hide().connect(
|
||||
[this]()
|
||||
{
|
||||
auto const success = progress_dialog_->success();
|
||||
progress_dialog_.reset();
|
||||
|
||||
if (builder_->result == TrMakemetaResult::OK)
|
||||
if (success)
|
||||
{
|
||||
dialog_.hide();
|
||||
}
|
||||
@@ -270,54 +295,34 @@ void MakeDialog::Impl::onResponse(int response)
|
||||
{
|
||||
if (response == Gtk::RESPONSE_ACCEPT)
|
||||
{
|
||||
if (builder_ != nullptr)
|
||||
if (builder_)
|
||||
{
|
||||
auto const comment = comment_entry_->get_text();
|
||||
bool const isPrivate = private_check_->get_active();
|
||||
bool const useComment = comment_check_->get_active();
|
||||
bool const useSource = source_check_->get_active();
|
||||
auto const source = source_entry_->get_text();
|
||||
|
||||
/* destination file */
|
||||
// destination file
|
||||
auto const dir = destination_chooser_->get_filename();
|
||||
auto const base = Glib::path_get_basename(builder_->top);
|
||||
auto const target = gtr_sprintf("%s/%s.torrent", dir, base);
|
||||
auto const base = Glib::path_get_basename(builder_->top());
|
||||
auto const target = fmt::format("{:s}/{:s}.torrent", dir, base);
|
||||
|
||||
/* build the array of trackers */
|
||||
auto const tracker_text = announce_text_buffer_->get_text(false);
|
||||
std::istringstream tracker_strings(tracker_text);
|
||||
// build the announce list
|
||||
auto trackers = tr_announce_list{};
|
||||
trackers.parse(announce_text_buffer_->get_text(false).raw());
|
||||
builder_->setAnnounceList(std::move(trackers));
|
||||
|
||||
std::vector<tr_tracker_info> trackers;
|
||||
std::list<std::string> announce_urls;
|
||||
int tier = 0;
|
||||
|
||||
std::string str;
|
||||
while (std::getline(tracker_strings, str))
|
||||
// comment
|
||||
if (comment_check_->get_active())
|
||||
{
|
||||
if (str.empty())
|
||||
{
|
||||
++tier;
|
||||
}
|
||||
else
|
||||
{
|
||||
announce_urls.push_front(str);
|
||||
trackers.push_back(tr_tracker_info{ tier, announce_urls.front().data() });
|
||||
}
|
||||
builder_->setComment(comment_entry_->get_text().raw());
|
||||
}
|
||||
|
||||
/* build the .torrent */
|
||||
makeProgressDialog(target);
|
||||
tr_makeMetaInfo(
|
||||
builder_.get(),
|
||||
target.c_str(),
|
||||
trackers.data(),
|
||||
trackers.size(),
|
||||
nullptr,
|
||||
0,
|
||||
useComment ? comment.c_str() : nullptr,
|
||||
isPrivate,
|
||||
false,
|
||||
useSource ? source.c_str() : nullptr);
|
||||
// source
|
||||
if (source_check_->get_active())
|
||||
{
|
||||
builder_->setSource(source_entry_->get_text().raw());
|
||||
}
|
||||
|
||||
builder_->setPrivate(private_check_->get_active());
|
||||
|
||||
// build the .torrent
|
||||
makeProgressDialog(target, builder_->makeChecksums());
|
||||
}
|
||||
}
|
||||
else if (response == Gtk::RESPONSE_CLOSE)
|
||||
@@ -342,11 +347,11 @@ void onSourceToggled(Gtk::ToggleButton* tb, Gtk::Widget* widget)
|
||||
|
||||
void MakeDialog::Impl::updatePiecesLabel()
|
||||
{
|
||||
char const* filename = builder_ != nullptr ? builder_->top : nullptr;
|
||||
auto const filename = builder_ ? builder_->top() : ""sv;
|
||||
|
||||
auto gstr = Glib::ustring{ "<i>" };
|
||||
|
||||
if (filename == nullptr)
|
||||
if (std::empty(filename))
|
||||
{
|
||||
gstr += _("No source selected");
|
||||
piece_size_scale_->set_visible(false);
|
||||
@@ -354,17 +359,17 @@ void MakeDialog::Impl::updatePiecesLabel()
|
||||
else
|
||||
{
|
||||
gstr += fmt::format(
|
||||
ngettext("{total_size} in {file_count:L} file", "{total_size} in {file_count:L} files", builder_->fileCount),
|
||||
fmt::arg("total_size", tr_strlsize(builder_->totalSize)),
|
||||
fmt::arg("file_count", builder_->fileCount));
|
||||
ngettext("{total_size} in {file_count:L} file", "{total_size} in {file_count:L} files", builder_->fileCount()),
|
||||
fmt::arg("total_size", tr_strlsize(builder_->totalSize())),
|
||||
fmt::arg("file_count", builder_->fileCount()));
|
||||
gstr += ' ';
|
||||
gstr += fmt::format(
|
||||
ngettext(
|
||||
"({piece_count} BitTorrent piece @ {piece_size})",
|
||||
"({piece_count} BitTorrent pieces @ {piece_size})",
|
||||
builder_->pieceCount),
|
||||
fmt::arg("piece_count", builder_->pieceCount),
|
||||
fmt::arg("piece_size", tr_formatter_mem_B(builder_->pieceSize)));
|
||||
builder_->pieceCount()),
|
||||
fmt::arg("piece_count", builder_->pieceCount()),
|
||||
fmt::arg("piece_size", tr_formatter_mem_B(builder_->pieceSize())));
|
||||
}
|
||||
|
||||
gstr += "</i>";
|
||||
@@ -374,18 +379,18 @@ void MakeDialog::Impl::updatePiecesLabel()
|
||||
void MakeDialog::Impl::configurePieceSizeScale()
|
||||
{
|
||||
// the below lower & upper bounds would allow piece size selection between approx 1KiB - 16MiB
|
||||
auto adjustment = Gtk::Adjustment::create(log2(builder_->pieceSize), 10, 24, 1.0, 1.0);
|
||||
auto adjustment = Gtk::Adjustment::create(log2(builder_->pieceSize()), 10, 24, 1.0, 1.0);
|
||||
piece_size_scale_->set_adjustment(adjustment);
|
||||
piece_size_scale_->set_visible(true);
|
||||
}
|
||||
|
||||
void MakeDialog::Impl::setFilename(std::string const& filename)
|
||||
void MakeDialog::Impl::setFilename(std::string_view filename)
|
||||
{
|
||||
builder_.reset();
|
||||
|
||||
if (!filename.empty())
|
||||
{
|
||||
builder_ = { tr_metaInfoBuilderCreate(filename.c_str()), &tr_metaInfoBuilderFree };
|
||||
builder_.emplace(filename);
|
||||
configurePieceSizeScale();
|
||||
}
|
||||
|
||||
@@ -555,10 +560,9 @@ MakeDialog::Impl::Impl(MakeDialog& dialog, Glib::RefPtr<Session> const& core)
|
||||
|
||||
void MakeDialog::Impl::onPieceSizeUpdated()
|
||||
{
|
||||
if (builder_ != nullptr)
|
||||
if (builder_)
|
||||
{
|
||||
auto new_size = static_cast<int>(pow(2, piece_size_scale_->get_value()));
|
||||
tr_metaInfoBuilderSetPieceSize(builder_.get(), new_size);
|
||||
builder_->setPieceSize(static_cast<uint32_t>(std::pow(2, piece_size_scale_->get_value())));
|
||||
updatePiecesLabel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,13 @@
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstring> /* strcmp, strlen */
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <event2/util.h> /* evutil_ascii_strcasecmp() */
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "transmission.h"
|
||||
@@ -24,60 +19,90 @@
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "makemeta.h"
|
||||
#include "session.h"
|
||||
#include "session.h" // TR_NAME
|
||||
#include "tr-assert.h"
|
||||
#include "utils.h" /* buildpath */
|
||||
#include "utils.h"
|
||||
#include "variant.h"
|
||||
#include "version.h"
|
||||
#include "web-utils.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/****
|
||||
*****
|
||||
****/
|
||||
|
||||
struct FileList
|
||||
namespace
|
||||
{
|
||||
uint64_t size;
|
||||
char* filename;
|
||||
struct FileList* next;
|
||||
};
|
||||
|
||||
static struct FileList* getFiles(std::string_view dir, std::string_view base, struct FileList* list)
|
||||
namespace find_files_helpers
|
||||
{
|
||||
if (std::empty(dir) || std::empty(base))
|
||||
|
||||
struct TorrentFile
|
||||
{
|
||||
TorrentFile(std::string_view subpath, size_t size)
|
||||
: subpath_{ subpath }
|
||||
, lowercase_{ tr_strlower(subpath) }
|
||||
, size_{ size }
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto buf = tr_pathbuf{ dir, '/', base };
|
||||
tr_sys_path_native_separators(std::data(buf));
|
||||
[[nodiscard]] auto operator<(TorrentFile const& that) const noexcept
|
||||
{
|
||||
return lowercase_ < that.lowercase_;
|
||||
}
|
||||
|
||||
std::string subpath_;
|
||||
std::string lowercase_;
|
||||
uint64_t size_ = 0;
|
||||
};
|
||||
|
||||
void walkTree(std::string_view const top, std::string_view const subpath, std::set<TorrentFile>& files)
|
||||
{
|
||||
TR_ASSERT(!std::empty(top));
|
||||
TR_ASSERT(!std::empty(subpath));
|
||||
|
||||
if (std::empty(top) || std::empty(subpath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto path = tr_pathbuf{ top, '/', subpath };
|
||||
tr_sys_path_native_separators(std::data(path));
|
||||
tr_error* error = nullptr;
|
||||
auto const info = tr_sys_path_get_info(buf, 0, &error);
|
||||
auto const info = tr_sys_path_get_info(path, 0, &error);
|
||||
if (!info)
|
||||
{
|
||||
tr_logAddWarn(fmt::format(
|
||||
_("Skipping '{path}': {error} ({error_code})"),
|
||||
fmt::arg("path", buf),
|
||||
fmt::arg("path", path),
|
||||
fmt::arg("error", error->message),
|
||||
fmt::arg("error_code", error->code)));
|
||||
tr_error_free(error);
|
||||
return list;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info->type)
|
||||
{
|
||||
case TR_SYS_PATH_IS_DIRECTORY:
|
||||
if (auto const odir = tr_sys_dir_open(buf.c_str()); odir != TR_BAD_SYS_DIR)
|
||||
if (tr_sys_dir_t odir = tr_sys_dir_open(path.c_str()); odir != TR_BAD_SYS_DIR)
|
||||
{
|
||||
char const* name = nullptr;
|
||||
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
|
||||
for (;;)
|
||||
{
|
||||
if (name[0] != '.') /* skip dotfiles */
|
||||
char const* const name = tr_sys_dir_read_name(odir);
|
||||
|
||||
if (name == nullptr)
|
||||
{
|
||||
list = getFiles(buf.c_str(), name, list);
|
||||
break;
|
||||
}
|
||||
|
||||
if (name[0] == '.') // skip dotfiles
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std::empty(subpath))
|
||||
{
|
||||
walkTree(top, tr_pathbuf{ subpath, '/', name }, files);
|
||||
}
|
||||
else
|
||||
{
|
||||
walkTree(top, name, files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,23 +111,40 @@ static struct FileList* getFiles(std::string_view dir, std::string_view base, st
|
||||
break;
|
||||
|
||||
case TR_SYS_PATH_IS_FILE:
|
||||
{
|
||||
auto* const node = tr_new0(FileList, 1);
|
||||
node->size = info->size;
|
||||
node->filename = tr_strvDup(buf);
|
||||
node->next = list;
|
||||
list = node;
|
||||
}
|
||||
files.emplace(subpath, info->size);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static uint32_t bestPieceSize(uint64_t totalSize)
|
||||
} // namespace find_files_helpers
|
||||
|
||||
tr_torrent_files findFiles(std::string_view const top, std::string_view const subpath)
|
||||
{
|
||||
using namespace find_files_helpers;
|
||||
|
||||
auto tmp = std::set<TorrentFile>{};
|
||||
walkTree(top, subpath, tmp);
|
||||
auto files = tr_torrent_files{};
|
||||
for (auto const& file : tmp)
|
||||
{
|
||||
files.add(file.subpath_, file.size_);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
tr_metainfo_builder::tr_metainfo_builder(std::string_view filename)
|
||||
: top_{ filename }
|
||||
{
|
||||
files_ = findFiles(tr_sys_path_dirname(filename), tr_sys_path_basename(filename));
|
||||
block_info_ = tr_block_info{ files_.totalSize(), defaultPieceSize(files_.totalSize()) };
|
||||
}
|
||||
|
||||
uint32_t tr_metainfo_builder::defaultPieceSize(uint64_t totalSize)
|
||||
{
|
||||
uint32_t const KiB = 1024;
|
||||
uint32_t const MiB = 1048576;
|
||||
@@ -141,498 +183,249 @@ static uint32_t bestPieceSize(uint64_t totalSize)
|
||||
return 32 * KiB; /* less than 50 meg */
|
||||
}
|
||||
|
||||
tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFileArg)
|
||||
bool tr_metainfo_builder::isLegalPieceSize(uint32_t x)
|
||||
{
|
||||
char* const real_top = tr_sys_path_resolve(topFileArg);
|
||||
|
||||
if (real_top == nullptr)
|
||||
{
|
||||
/* TODO: Better error reporting */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* const ret = tr_new0(tr_metainfo_builder, 1);
|
||||
|
||||
ret->top = real_top;
|
||||
|
||||
auto const info = tr_sys_path_get_info(ret->top);
|
||||
ret->isFolder = info && info->isFolder();
|
||||
|
||||
/* build a list of files containing top file and,
|
||||
if it's a directory, all of its children */
|
||||
auto* files = getFiles(tr_sys_path_dirname(ret->top), tr_sys_path_basename(ret->top), nullptr);
|
||||
|
||||
for (auto* walk = files; walk != nullptr; walk = walk->next)
|
||||
{
|
||||
++ret->fileCount;
|
||||
}
|
||||
|
||||
ret->files = tr_new0(tr_metainfo_builder_file, ret->fileCount);
|
||||
|
||||
int i = 0;
|
||||
auto const offset = strlen(ret->top);
|
||||
while (files != nullptr)
|
||||
{
|
||||
auto* const tmp = files;
|
||||
files = files->next;
|
||||
|
||||
auto* const file = &ret->files[i++];
|
||||
file->filename = tmp->filename;
|
||||
file->size = tmp->size;
|
||||
file->is_portable = tr_torrent_files::isSubpathPortable(file->filename + offset);
|
||||
|
||||
ret->totalSize += tmp->size;
|
||||
|
||||
tr_free(tmp);
|
||||
}
|
||||
|
||||
std::sort(
|
||||
ret->files,
|
||||
ret->files + ret->fileCount,
|
||||
[](auto const& a, auto const& b) { return evutil_ascii_strcasecmp(a.filename, b.filename) < 0; });
|
||||
|
||||
tr_metaInfoBuilderSetPieceSize(ret, bestPieceSize(ret->totalSize));
|
||||
|
||||
return ret;
|
||||
// It must be a power of two and at least 16KiB
|
||||
static auto constexpr MinSize = uint32_t{ 1024U * 16U };
|
||||
auto const is_power_of_two = (x & (x - 1)) == 0;
|
||||
return x >= MinSize && is_power_of_two;
|
||||
}
|
||||
|
||||
static bool isValidPieceSize(uint32_t n)
|
||||
bool tr_metainfo_builder::setPieceSize(uint32_t piece_size) noexcept
|
||||
{
|
||||
bool const isPowerOfTwo = n != 0 && (n & (n - 1)) == 0;
|
||||
|
||||
return isPowerOfTwo;
|
||||
}
|
||||
|
||||
bool tr_metaInfoBuilderSetPieceSize(tr_metainfo_builder* b, uint32_t bytes)
|
||||
{
|
||||
if (!isValidPieceSize(bytes))
|
||||
if (!isLegalPieceSize(piece_size))
|
||||
{
|
||||
tr_logAddWarn(fmt::format(
|
||||
_("Couldn't use invalid piece size {expected_size}; using {actual_size} instead"),
|
||||
fmt::arg("expected_size", tr_formatter_mem_B(bytes)),
|
||||
fmt::arg("actual_size", tr_formatter_mem_B(b->pieceSize))));
|
||||
return false;
|
||||
}
|
||||
|
||||
b->pieceSize = bytes;
|
||||
b->pieceCount = (int)(b->totalSize / b->pieceSize);
|
||||
|
||||
if (b->totalSize % b->pieceSize != 0)
|
||||
{
|
||||
++b->pieceCount;
|
||||
}
|
||||
|
||||
block_info_ = tr_block_info{ files_.totalSize(), piece_size };
|
||||
return true;
|
||||
}
|
||||
|
||||
void tr_metaInfoBuilderFree(tr_metainfo_builder* builder)
|
||||
bool tr_metainfo_builder::blockingMakeChecksums(tr_error** error)
|
||||
{
|
||||
if (builder != nullptr)
|
||||
checksum_piece_ = 0;
|
||||
cancel_ = false;
|
||||
|
||||
if (totalSize() == 0U)
|
||||
{
|
||||
for (uint32_t i = 0; i < builder->fileCount; ++i)
|
||||
{
|
||||
tr_free(builder->files[i].filename);
|
||||
}
|
||||
|
||||
tr_free(builder->files);
|
||||
tr_free(builder->top);
|
||||
tr_free(builder->comment);
|
||||
tr_free(builder->source);
|
||||
|
||||
for (int i = 0; i < builder->trackerCount; ++i)
|
||||
{
|
||||
tr_free(builder->trackers[i].announce);
|
||||
}
|
||||
|
||||
for (int i = 0; i < builder->webseedCount; ++i)
|
||||
{
|
||||
tr_free(builder->webseeds[i]);
|
||||
}
|
||||
|
||||
tr_free(builder->trackers);
|
||||
tr_free(builder->webseeds);
|
||||
tr_free(builder->outputFile);
|
||||
tr_free(builder);
|
||||
}
|
||||
}
|
||||
|
||||
/****
|
||||
*****
|
||||
****/
|
||||
|
||||
static std::vector<std::byte> getHashInfo(tr_metainfo_builder* b)
|
||||
{
|
||||
auto ret = std::vector<std::byte>(std::size(tr_sha1_digest_t{}) * b->pieceCount);
|
||||
|
||||
if (b->totalSize == 0)
|
||||
{
|
||||
return {};
|
||||
tr_error_set(error, ENOENT, tr_strerror(ENOENT));
|
||||
return false;
|
||||
}
|
||||
|
||||
b->pieceIndex = 0;
|
||||
uint64_t totalRemain = b->totalSize;
|
||||
uint32_t fileIndex = 0;
|
||||
auto* walk = std::data(ret);
|
||||
auto buf = std::vector<char>(b->pieceSize);
|
||||
uint64_t off = 0;
|
||||
tr_error* error = nullptr;
|
||||
auto hashes = std::vector<std::byte>(std::size(tr_sha1_digest_t{}) * pieceCount());
|
||||
auto* walk = std::data(hashes);
|
||||
auto sha = tr_sha1::create();
|
||||
|
||||
tr_sys_file_t fd = tr_sys_file_open(b->files[fileIndex].filename, TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL, 0, &error);
|
||||
auto file_index = tr_file_index_t{ 0U };
|
||||
auto piece_index = tr_piece_index_t{ 0U };
|
||||
auto total_remain = totalSize();
|
||||
auto off = uint64_t{ 0U };
|
||||
|
||||
auto buf = std::vector<char>(pieceSize());
|
||||
|
||||
auto const parent = tr_sys_path_dirname(top_);
|
||||
auto fd = tr_sys_file_open(
|
||||
tr_pathbuf{ parent, '/', path(file_index) },
|
||||
TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
|
||||
0,
|
||||
error);
|
||||
if (fd == TR_BAD_SYS_FILE)
|
||||
{
|
||||
b->my_errno = error->code;
|
||||
tr_strlcpy(b->errfile, b->files[fileIndex].filename, sizeof(b->errfile));
|
||||
b->result = TrMakemetaResult::ERR_IO_READ;
|
||||
tr_error_free(error);
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
while (totalRemain != 0)
|
||||
while (!cancel_ && (total_remain > 0U))
|
||||
{
|
||||
TR_ASSERT(b->pieceIndex < b->pieceCount);
|
||||
checksum_piece_ = piece_index;
|
||||
|
||||
uint32_t const this_piece_size = std::min(uint64_t{ b->pieceSize }, totalRemain);
|
||||
buf.resize(this_piece_size);
|
||||
TR_ASSERT(piece_index < pieceCount());
|
||||
|
||||
uint32_t const piece_size = block_info_.pieceSize(piece_index);
|
||||
buf.resize(piece_size);
|
||||
auto* bufptr = std::data(buf);
|
||||
uint64_t leftInPiece = this_piece_size;
|
||||
|
||||
while (leftInPiece != 0)
|
||||
auto left_in_piece = piece_size;
|
||||
while (left_in_piece > 0U)
|
||||
{
|
||||
uint64_t const n_this_pass = std::min(b->files[fileIndex].size - off, leftInPiece);
|
||||
uint64_t n_read = 0;
|
||||
(void)tr_sys_file_read(fd, bufptr, n_this_pass, &n_read);
|
||||
auto const n_this_pass = std::min(fileSize(file_index) - off, uint64_t{ left_in_piece });
|
||||
auto n_read = uint64_t{};
|
||||
|
||||
(void)tr_sys_file_read(fd, bufptr, n_this_pass, &n_read, error);
|
||||
bufptr += n_read;
|
||||
off += n_read;
|
||||
leftInPiece -= n_read;
|
||||
left_in_piece -= n_read;
|
||||
|
||||
if (off == b->files[fileIndex].size)
|
||||
if (off == fileSize(file_index))
|
||||
{
|
||||
off = 0;
|
||||
tr_sys_file_close(fd);
|
||||
fd = TR_BAD_SYS_FILE;
|
||||
|
||||
if (++fileIndex < b->fileCount)
|
||||
if (++file_index < fileCount())
|
||||
{
|
||||
fd = tr_sys_file_open(b->files[fileIndex].filename, TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL, 0, &error);
|
||||
|
||||
fd = tr_sys_file_open(
|
||||
tr_pathbuf{ parent, '/', path(file_index) },
|
||||
TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
|
||||
0,
|
||||
error);
|
||||
if (fd == TR_BAD_SYS_FILE)
|
||||
{
|
||||
b->my_errno = error->code;
|
||||
tr_strlcpy(b->errfile, b->files[fileIndex].filename, sizeof(b->errfile));
|
||||
b->result = TrMakemetaResult::ERR_IO_READ;
|
||||
tr_error_free(error);
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TR_ASSERT(bufptr - std::data(buf) == (int)this_piece_size);
|
||||
TR_ASSERT(leftInPiece == 0);
|
||||
auto const digest = tr_sha1::digest(buf);
|
||||
TR_ASSERT(bufptr - std::data(buf) == (int)piece_size);
|
||||
TR_ASSERT(left_in_piece == 0);
|
||||
sha->add(std::data(buf), std::size(buf));
|
||||
auto const digest = sha->final();
|
||||
walk = std::copy(std::begin(digest), std::end(digest), walk);
|
||||
sha->clear();
|
||||
|
||||
if (b->abortFlag)
|
||||
{
|
||||
b->result = TrMakemetaResult::CANCELLED;
|
||||
break;
|
||||
}
|
||||
|
||||
totalRemain -= this_piece_size;
|
||||
++b->pieceIndex;
|
||||
total_remain -= piece_size;
|
||||
++piece_index;
|
||||
}
|
||||
|
||||
TR_ASSERT(b->abortFlag || size_t(walk - std::data(ret)) == std::size(ret));
|
||||
TR_ASSERT(b->abortFlag || !totalRemain);
|
||||
TR_ASSERT(cancel_ || size_t(walk - std::data(hashes)) == std::size(hashes));
|
||||
TR_ASSERT(cancel_ || total_remain == 0U);
|
||||
|
||||
if (fd != TR_BAD_SYS_FILE)
|
||||
{
|
||||
tr_sys_file_close(fd);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void getFileInfo(
|
||||
char const* topFile,
|
||||
tr_metainfo_builder_file const* file,
|
||||
tr_variant* uninitialized_length,
|
||||
tr_variant* uninitialized_path)
|
||||
{
|
||||
/* get the file size */
|
||||
tr_variantInitInt(uninitialized_length, file->size);
|
||||
|
||||
/* how much of file->filename to walk past */
|
||||
size_t offset = strlen(topFile);
|
||||
if (offset > 0 && topFile[offset - 1] != TR_PATH_DELIMITER)
|
||||
if (cancel_)
|
||||
{
|
||||
++offset; /* +1 for the path delimiter */
|
||||
tr_error_set(error, ECANCELED, tr_strerror(ECANCELED));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* build the path list */
|
||||
tr_variantInitList(uninitialized_path, 0);
|
||||
|
||||
auto filename = std::string_view{ file->filename };
|
||||
if (std::size(filename) > offset)
|
||||
{
|
||||
filename.remove_prefix(offset);
|
||||
auto token = std::string_view{};
|
||||
while (tr_strvSep(&filename, &token, TR_PATH_DELIMITER))
|
||||
{
|
||||
tr_variantListAddStr(uninitialized_path, token);
|
||||
}
|
||||
}
|
||||
piece_hashes_ = std::move(hashes);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void makeInfoDict(tr_variant* dict, tr_metainfo_builder* builder)
|
||||
std::string tr_metainfo_builder::benc(tr_error** error) const
|
||||
{
|
||||
tr_variantDictReserve(dict, 5);
|
||||
TR_ASSERT_MSG(!std::empty(piece_hashes_), "did you forget to call makeChecksums() first?");
|
||||
|
||||
if (builder->isFolder) /* root node is a directory */
|
||||
auto const anonymize = this->anonymize();
|
||||
auto const& comment = this->comment();
|
||||
auto const& source = this->source();
|
||||
auto const& webseeds = this->webseeds();
|
||||
|
||||
if (totalSize() == 0)
|
||||
{
|
||||
tr_variant* list = tr_variantDictAddList(dict, TR_KEY_files, builder->fileCount);
|
||||
tr_error_set(error, ENOENT, tr_strerror(ENOENT));
|
||||
return {};
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < builder->fileCount; ++i)
|
||||
auto top = tr_variant{};
|
||||
tr_variantInitDict(&top, 8);
|
||||
|
||||
// add the announce-list trackers
|
||||
if (!std::empty(announceList()))
|
||||
{
|
||||
auto* const announce_list = tr_variantDictAddList(&top, TR_KEY_announce_list, 0);
|
||||
tr_variant* tier_list = nullptr;
|
||||
auto prev_tier = std::optional<tr_tracker_tier_t>{};
|
||||
for (auto const& tracker : announceList())
|
||||
{
|
||||
tr_variant* d = tr_variantListAddDict(list, 2);
|
||||
tr_variant* length = tr_variantDictAdd(d, TR_KEY_length);
|
||||
tr_variant* pathVal = tr_variantDictAdd(d, TR_KEY_path);
|
||||
getFileInfo(builder->top, &builder->files[i], length, pathVal);
|
||||
if (!prev_tier || *prev_tier != tracker.tier)
|
||||
{
|
||||
tier_list = nullptr;
|
||||
}
|
||||
|
||||
if (tier_list == nullptr)
|
||||
{
|
||||
prev_tier = tracker.tier;
|
||||
tier_list = tr_variantListAddList(announce_list, 0);
|
||||
}
|
||||
|
||||
tr_variantListAddStr(tier_list, tracker.announce);
|
||||
}
|
||||
}
|
||||
|
||||
// add the webseeds
|
||||
if (!std::empty(webseeds))
|
||||
{
|
||||
auto* const url_list = tr_variantDictAddList(&top, TR_KEY_url_list, std::size(webseeds));
|
||||
|
||||
for (auto const& webseed : webseeds)
|
||||
{
|
||||
tr_variantListAddStr(url_list, webseed);
|
||||
}
|
||||
}
|
||||
|
||||
// add the comment
|
||||
if (!std::empty(comment))
|
||||
{
|
||||
tr_variantDictAddStr(&top, TR_KEY_comment, comment);
|
||||
}
|
||||
|
||||
// maybe add some optional metainfo
|
||||
if (!anonymize)
|
||||
{
|
||||
tr_variantDictAddStrView(&top, TR_KEY_created_by, TR_NAME "/" LONG_VERSION_STRING);
|
||||
tr_variantDictAddInt(&top, TR_KEY_creation_date, time(nullptr));
|
||||
}
|
||||
|
||||
tr_variantDictAddStrView(&top, TR_KEY_encoding, "UTF-8");
|
||||
|
||||
if (is_private_)
|
||||
{
|
||||
tr_variantDictAddInt(&top, TR_KEY_private, 1);
|
||||
}
|
||||
|
||||
if (!std::empty(source))
|
||||
{
|
||||
tr_variantDictAddStr(&top, TR_KEY_source, source_);
|
||||
}
|
||||
|
||||
auto* const info_dict = tr_variantDictAddDict(&top, TR_KEY_info, 5);
|
||||
auto const base = tr_sys_path_basename(top_);
|
||||
|
||||
// "There is also a key `length` or a key `files`, but not both or neither.
|
||||
// If length is present then the download represents a single file,
|
||||
// otherwise it represents a set of files which go in a directory structure."
|
||||
if (fileCount() == 1U && !tr_strvContains(path(0), '/'))
|
||||
{
|
||||
tr_variantDictAddInt(info_dict, TR_KEY_length, fileSize(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
tr_variantDictAddInt(dict, TR_KEY_length, builder->files[0].size);
|
||||
}
|
||||
auto const n_files = fileCount();
|
||||
auto* const file_list = tr_variantDictAddList(info_dict, TR_KEY_files, n_files);
|
||||
|
||||
if (auto const base = tr_sys_path_basename(builder->top); !std::empty(base))
|
||||
{
|
||||
tr_variantDictAddStr(dict, TR_KEY_name, base);
|
||||
}
|
||||
|
||||
tr_variantDictAddInt(dict, TR_KEY_piece_length, builder->pieceSize);
|
||||
|
||||
if (auto const piece_hashes = getHashInfo(builder); !std::empty(piece_hashes))
|
||||
{
|
||||
tr_variantDictAddRaw(dict, TR_KEY_pieces, std::data(piece_hashes), std::size(piece_hashes));
|
||||
}
|
||||
|
||||
if (builder->isPrivate)
|
||||
{
|
||||
tr_variantDictAddInt(dict, TR_KEY_private, 1);
|
||||
}
|
||||
|
||||
if (builder->source != nullptr)
|
||||
{
|
||||
tr_variantDictAddStr(dict, TR_KEY_source, builder->source);
|
||||
}
|
||||
}
|
||||
|
||||
static void tr_realMakeMetaInfo(tr_metainfo_builder* builder)
|
||||
{
|
||||
tr_variant top;
|
||||
|
||||
for (int i = 0; i < builder->trackerCount && builder->result == TrMakemetaResult::OK; ++i)
|
||||
{
|
||||
if (!tr_urlIsValidTracker(builder->trackers[i].announce))
|
||||
for (tr_file_index_t i = 0; i < n_files; ++i)
|
||||
{
|
||||
tr_strlcpy(builder->errfile, builder->trackers[i].announce, sizeof(builder->errfile));
|
||||
builder->result = TrMakemetaResult::ERR_URL;
|
||||
}
|
||||
}
|
||||
auto* const file_dict = tr_variantListAddDict(file_list, 2);
|
||||
tr_variantDictAddInt(file_dict, TR_KEY_length, fileSize(i));
|
||||
|
||||
for (int i = 0; i < builder->webseedCount && builder->result == TrMakemetaResult::OK; ++i)
|
||||
{
|
||||
if (!tr_urlIsValid(builder->webseeds[i]))
|
||||
{
|
||||
tr_strlcpy(builder->errfile, builder->webseeds[i], sizeof(builder->errfile));
|
||||
builder->result = TrMakemetaResult::ERR_URL;
|
||||
}
|
||||
}
|
||||
|
||||
tr_variantInitDict(&top, 6);
|
||||
|
||||
if (builder->fileCount == 0 || builder->totalSize == 0 || builder->pieceSize == 0 || builder->pieceCount == 0)
|
||||
{
|
||||
builder->errfile[0] = '\0';
|
||||
builder->my_errno = ENOENT;
|
||||
builder->result = TrMakemetaResult::ERR_IO_READ;
|
||||
builder->isDone = true;
|
||||
}
|
||||
|
||||
if (builder->result == TrMakemetaResult::OK && builder->trackerCount != 0)
|
||||
{
|
||||
int prevTier = -1;
|
||||
tr_variant* tier = nullptr;
|
||||
|
||||
if (builder->trackerCount > 1)
|
||||
{
|
||||
tr_variant* annList = tr_variantDictAddList(&top, TR_KEY_announce_list, 0);
|
||||
|
||||
for (int i = 0; i < builder->trackerCount; ++i)
|
||||
auto subpath = std::string_view{ path(i) };
|
||||
if (!std::empty(base))
|
||||
{
|
||||
if (prevTier != builder->trackers[i].tier)
|
||||
{
|
||||
prevTier = builder->trackers[i].tier;
|
||||
tier = tr_variantListAddList(annList, 0);
|
||||
}
|
||||
subpath.remove_prefix(std::size(base) + std::size("/"sv));
|
||||
}
|
||||
|
||||
tr_variantListAddStr(tier, builder->trackers[i].announce);
|
||||
auto* const path_list = tr_variantDictAddList(file_dict, TR_KEY_path, 0);
|
||||
auto token = std::string_view{};
|
||||
while (tr_strvSep(&subpath, &token, '/'))
|
||||
{
|
||||
tr_variantListAddStr(path_list, token);
|
||||
}
|
||||
}
|
||||
|
||||
tr_variantDictAddStr(&top, TR_KEY_announce, builder->trackers[0].announce);
|
||||
}
|
||||
|
||||
if (builder->result == TrMakemetaResult::OK && builder->webseedCount > 0)
|
||||
if (!std::empty(base))
|
||||
{
|
||||
tr_variant* url_list = tr_variantDictAddList(&top, TR_KEY_url_list, builder->webseedCount);
|
||||
|
||||
for (int i = 0; i < builder->webseedCount; ++i)
|
||||
{
|
||||
tr_variantListAddStr(url_list, builder->webseeds[i]);
|
||||
}
|
||||
tr_variantDictAddStr(info_dict, TR_KEY_name, base);
|
||||
}
|
||||
|
||||
if (builder->result == TrMakemetaResult::OK && !builder->abortFlag)
|
||||
{
|
||||
if (!tr_str_is_empty(builder->comment))
|
||||
{
|
||||
tr_variantDictAddStr(&top, TR_KEY_comment, builder->comment);
|
||||
}
|
||||
|
||||
if (!builder->anonymize)
|
||||
{
|
||||
tr_variantDictAddStrView(&top, TR_KEY_created_by, TR_NAME "/" LONG_VERSION_STRING);
|
||||
tr_variantDictAddInt(&top, TR_KEY_creation_date, time(nullptr));
|
||||
}
|
||||
tr_variantDictAddStrView(&top, TR_KEY_encoding, "UTF-8");
|
||||
makeInfoDict(tr_variantDictAddDict(&top, TR_KEY_info, 666), builder);
|
||||
}
|
||||
|
||||
/* save the file */
|
||||
if ((builder->result == TrMakemetaResult::OK) && (!builder->abortFlag) &&
|
||||
(tr_variantToFile(&top, TR_VARIANT_FMT_BENC, builder->outputFile) != 0))
|
||||
{
|
||||
builder->my_errno = errno;
|
||||
tr_strlcpy(builder->errfile, builder->outputFile, sizeof(builder->errfile));
|
||||
builder->result = TrMakemetaResult::ERR_IO_WRITE;
|
||||
}
|
||||
|
||||
/* cleanup */
|
||||
tr_variantDictAddInt(info_dict, TR_KEY_piece_length, pieceSize());
|
||||
tr_variantDictAddRaw(info_dict, TR_KEY_pieces, std::data(piece_hashes_), std::size(piece_hashes_));
|
||||
auto ret = tr_variantToStr(&top, TR_VARIANT_FMT_BENC);
|
||||
tr_variantFree(&top);
|
||||
|
||||
if (builder->abortFlag)
|
||||
{
|
||||
builder->result = TrMakemetaResult::CANCELLED;
|
||||
}
|
||||
|
||||
builder->isDone = true;
|
||||
}
|
||||
|
||||
/***
|
||||
****
|
||||
**** A threaded builder queue
|
||||
****
|
||||
***/
|
||||
|
||||
static tr_metainfo_builder* queue = nullptr;
|
||||
|
||||
static std::optional<std::thread::id> worker_thread_id;
|
||||
|
||||
static std::recursive_mutex queue_mutex_;
|
||||
|
||||
static void makeMetaWorkerFunc()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
tr_metainfo_builder* builder = nullptr;
|
||||
|
||||
/* find the next builder to process */
|
||||
queue_mutex_.lock();
|
||||
|
||||
if (queue != nullptr)
|
||||
{
|
||||
builder = queue;
|
||||
queue = queue->nextBuilder;
|
||||
}
|
||||
|
||||
queue_mutex_.unlock();
|
||||
|
||||
/* if no builders, this worker thread is done */
|
||||
if (builder == nullptr)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
tr_realMakeMetaInfo(builder);
|
||||
}
|
||||
|
||||
worker_thread_id.reset();
|
||||
}
|
||||
|
||||
void tr_makeMetaInfo(
|
||||
tr_metainfo_builder* builder,
|
||||
char const* outputFile,
|
||||
tr_tracker_info const* trackers,
|
||||
int trackerCount,
|
||||
char const** webseeds,
|
||||
int webseedCount,
|
||||
char const* comment,
|
||||
bool isPrivate,
|
||||
bool anonymize,
|
||||
char const* source)
|
||||
{
|
||||
/* free any variables from a previous run */
|
||||
for (int i = 0; i < builder->trackerCount; ++i)
|
||||
{
|
||||
tr_free(builder->trackers[i].announce);
|
||||
}
|
||||
|
||||
tr_free(builder->trackers);
|
||||
tr_free(builder->comment);
|
||||
tr_free(builder->source);
|
||||
tr_free(builder->outputFile);
|
||||
|
||||
/* initialize the builder variables */
|
||||
builder->abortFlag = false;
|
||||
builder->result = TrMakemetaResult::OK;
|
||||
builder->isDone = false;
|
||||
builder->pieceIndex = 0;
|
||||
builder->trackerCount = trackerCount;
|
||||
builder->trackers = tr_new0(tr_tracker_info, builder->trackerCount);
|
||||
|
||||
for (int i = 0; i < builder->trackerCount; ++i)
|
||||
{
|
||||
builder->trackers[i].tier = trackers[i].tier;
|
||||
builder->trackers[i].announce = tr_strdup(trackers[i].announce);
|
||||
}
|
||||
|
||||
builder->webseedCount = webseedCount;
|
||||
builder->webseeds = tr_new0(char*, webseedCount);
|
||||
for (int i = 0; i < webseedCount; ++i)
|
||||
{
|
||||
builder->webseeds[i] = tr_strdup(webseeds[i]);
|
||||
}
|
||||
|
||||
builder->comment = tr_strdup(comment);
|
||||
builder->isPrivate = isPrivate;
|
||||
builder->anonymize = anonymize;
|
||||
builder->source = tr_strdup(source);
|
||||
|
||||
builder->outputFile = !tr_str_is_empty(outputFile) ? tr_strdup(outputFile) :
|
||||
tr_strvDup(fmt::format(FMT_STRING("{:s}.torrent"), builder->top));
|
||||
|
||||
/* enqueue the builder */
|
||||
auto const lock = std::lock_guard(queue_mutex_);
|
||||
|
||||
builder->nextBuilder = queue;
|
||||
queue = builder;
|
||||
|
||||
if (!worker_thread_id)
|
||||
{
|
||||
auto thread = std::thread(makeMetaWorkerFunc);
|
||||
worker_thread_id = thread.get_id();
|
||||
thread.detach();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -5,129 +5,200 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // uint32_t, uint64_t
|
||||
#include <algorithm> // std::move
|
||||
#include <cstddef> // std::byte
|
||||
#include <future>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility> // std::pair
|
||||
#include <vector>
|
||||
|
||||
#include "transmission.h"
|
||||
|
||||
struct tr_metainfo_builder_file
|
||||
#include "announce-list.h"
|
||||
#include "block-info.h"
|
||||
#include "file.h"
|
||||
#include "torrent-files.h"
|
||||
|
||||
class tr_metainfo_builder
|
||||
{
|
||||
char* filename;
|
||||
uint64_t size;
|
||||
bool is_portable;
|
||||
public:
|
||||
tr_metainfo_builder(std::string_view single_file_or_parent_directory);
|
||||
|
||||
tr_metainfo_builder(tr_metainfo_builder&&) = delete;
|
||||
tr_metainfo_builder(tr_metainfo_builder const&) = delete;
|
||||
tr_metainfo_builder& operator=(tr_metainfo_builder&&) = delete;
|
||||
tr_metainfo_builder& operator=(tr_metainfo_builder const&) = delete;
|
||||
|
||||
// Generate piece checksums asynchronously.
|
||||
// - This must be done before calling `benc()` or `save()`.
|
||||
// - Runs in a worker thread because it can be time-consuming.
|
||||
// - Can be cancelled with `cancelChecksums()` and polled with `checksumStatus()`
|
||||
// - Resolves with a `tr_error*` which is set on failure or nullptr on success.
|
||||
std::future<tr_error*> makeChecksums()
|
||||
{
|
||||
return std::async(
|
||||
std::launch::async,
|
||||
[this]()
|
||||
{
|
||||
tr_error* error = nullptr;
|
||||
blockingMakeChecksums(&error);
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the status of a `makeChecksums()` call:
|
||||
// The current piece being tested and the total number of pieces in the torrent.
|
||||
[[nodiscard]] constexpr std::pair<tr_piece_index_t, tr_piece_index_t> checksumStatus() const noexcept
|
||||
{
|
||||
return std::make_pair(checksum_piece_, block_info_.pieceCount());
|
||||
}
|
||||
|
||||
// Tell the `makeChecksums()` worker thread to cleanly exit ASAP.
|
||||
constexpr void cancelChecksums() noexcept
|
||||
{
|
||||
cancel_ = true;
|
||||
}
|
||||
|
||||
// generate the metainfo
|
||||
std::string benc(tr_error** error = nullptr) const;
|
||||
|
||||
// generate the metainfo and save it to a torrent file
|
||||
bool save(std::string_view filename, tr_error** error = nullptr) const
|
||||
{
|
||||
return tr_saveFile(filename, benc(error), error);
|
||||
}
|
||||
|
||||
/// setters
|
||||
|
||||
void setAnnounceList(tr_announce_list announce)
|
||||
{
|
||||
announce_ = std::move(announce);
|
||||
}
|
||||
|
||||
// whether or not to include User-Agent and creation time
|
||||
constexpr void setAnonymize(bool anonymize) noexcept
|
||||
{
|
||||
anonymize_ = anonymize;
|
||||
}
|
||||
|
||||
void setComment(std::string_view comment)
|
||||
{
|
||||
comment_ = comment;
|
||||
}
|
||||
|
||||
bool setPieceSize(uint32_t piece_size) noexcept;
|
||||
|
||||
constexpr void setPrivate(bool is_private) noexcept
|
||||
{
|
||||
is_private_ = is_private;
|
||||
}
|
||||
|
||||
void setSource(std::string_view source)
|
||||
{
|
||||
source_ = source;
|
||||
}
|
||||
|
||||
void setWebseeds(std::vector<std::string> webseeds)
|
||||
{
|
||||
webseeds_ = std::move(webseeds);
|
||||
}
|
||||
|
||||
/// getters
|
||||
|
||||
[[nodiscard]] constexpr auto const& announceList() const noexcept
|
||||
{
|
||||
return announce_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& anonymize() const noexcept
|
||||
{
|
||||
return anonymize_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& comment() const noexcept
|
||||
{
|
||||
return comment_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto fileCount() const noexcept
|
||||
{
|
||||
return files_.fileCount();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto fileSize(tr_file_index_t i) const noexcept
|
||||
{
|
||||
return files_.fileSize(i);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& isPrivate() const noexcept
|
||||
{
|
||||
return is_private_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto name() const noexcept
|
||||
{
|
||||
return tr_sys_path_basename(top_);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto const& path(tr_file_index_t i) const noexcept
|
||||
{
|
||||
return files_.path(i);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto pieceSize() const noexcept
|
||||
{
|
||||
return block_info_.pieceSize();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto pieceCount() const noexcept
|
||||
{
|
||||
return block_info_.pieceCount();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& source() const noexcept
|
||||
{
|
||||
return source_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& top() const noexcept
|
||||
{
|
||||
return top_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto totalSize() const noexcept
|
||||
{
|
||||
return files_.totalSize();
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& webseeds() const noexcept
|
||||
{
|
||||
return webseeds_;
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
[[nodiscard]] static uint32_t defaultPieceSize(uint64_t total_size);
|
||||
|
||||
// must be a power of two and >= 16 KiB
|
||||
[[nodiscard]] static bool isLegalPieceSize(uint32_t x);
|
||||
|
||||
private:
|
||||
bool blockingMakeChecksums(tr_error** error = nullptr);
|
||||
|
||||
std::string top_;
|
||||
tr_torrent_files files_;
|
||||
tr_announce_list announce_;
|
||||
tr_block_info block_info_;
|
||||
std::vector<std::byte> piece_hashes_;
|
||||
std::vector<std::string> webseeds_;
|
||||
|
||||
std::string comment_;
|
||||
std::string source_;
|
||||
|
||||
tr_piece_index_t checksum_piece_ = 0;
|
||||
|
||||
bool is_private_ = false;
|
||||
bool anonymize_ = false;
|
||||
bool cancel_ = false;
|
||||
};
|
||||
|
||||
enum class TrMakemetaResult
|
||||
{
|
||||
OK,
|
||||
CANCELLED,
|
||||
ERR_URL, // invalid announce URL
|
||||
ERR_IO_READ, // see builder.errfile, builder.my_errno
|
||||
ERR_IO_WRITE // see builder.errfile, builder.my_errno
|
||||
};
|
||||
|
||||
struct tr_tracker_info
|
||||
{
|
||||
int tier;
|
||||
char* announce;
|
||||
};
|
||||
|
||||
struct tr_metainfo_builder
|
||||
{
|
||||
/**
|
||||
*** These are set by tr_makeMetaInfoBuilderCreate()
|
||||
*** and cleaned up by tr_metaInfoBuilderFree()
|
||||
**/
|
||||
|
||||
char* top;
|
||||
tr_metainfo_builder_file* files;
|
||||
uint64_t totalSize;
|
||||
uint32_t fileCount;
|
||||
uint32_t pieceSize;
|
||||
uint32_t pieceCount;
|
||||
bool isFolder;
|
||||
|
||||
/**
|
||||
*** These are set inside tr_makeMetaInfo()
|
||||
*** by copying the arguments passed to it,
|
||||
*** and cleaned up by tr_metaInfoBuilderFree()
|
||||
**/
|
||||
|
||||
tr_tracker_info* trackers;
|
||||
int trackerCount;
|
||||
|
||||
char** webseeds;
|
||||
int webseedCount;
|
||||
|
||||
char* comment;
|
||||
char* outputFile;
|
||||
bool isPrivate;
|
||||
char* source;
|
||||
bool anonymize;
|
||||
|
||||
/**
|
||||
*** These are set inside tr_makeMetaInfo() so the client
|
||||
*** can poll periodically to see what the status is.
|
||||
*** The client can also set abortFlag to nonzero to
|
||||
*** tell tr_makeMetaInfo() to abort and clean up after itself.
|
||||
**/
|
||||
|
||||
uint32_t pieceIndex;
|
||||
bool abortFlag;
|
||||
bool isDone;
|
||||
TrMakemetaResult result;
|
||||
|
||||
/* file in use when result was set to _IO_READ or _IO_WRITE,
|
||||
* or the URL in use when the result was set to _URL */
|
||||
char errfile[2048];
|
||||
|
||||
/* errno encountered when result was set to _IO_READ or _IO_WRITE */
|
||||
int my_errno;
|
||||
|
||||
/**
|
||||
*** This is an implementation detail.
|
||||
*** The client should never use these fields.
|
||||
**/
|
||||
|
||||
struct tr_metainfo_builder* nextBuilder;
|
||||
};
|
||||
|
||||
tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFile);
|
||||
|
||||
/**
|
||||
* Call this before tr_makeMetaInfo() to override the builder.pieceSize
|
||||
* and builder.pieceCount values that were set by tr_metainfoBuilderCreate()
|
||||
*
|
||||
* @return false if the piece size isn't valid; eg, isn't a power of two.
|
||||
*/
|
||||
bool tr_metaInfoBuilderSetPieceSize(tr_metainfo_builder* builder, uint32_t bytes);
|
||||
|
||||
void tr_metaInfoBuilderFree(tr_metainfo_builder*);
|
||||
|
||||
/**
|
||||
* @brief create a new torrent file
|
||||
*
|
||||
* This is actually done in a worker thread, not the main thread!
|
||||
* Otherwise the client's interface would lock up while this runs.
|
||||
*
|
||||
* It is the caller's responsibility to poll builder->isDone
|
||||
* from time to time! When the worker thread sets that flag,
|
||||
* the caller must pass the builder to tr_metaInfoBuilderFree().
|
||||
*
|
||||
* @param outputFile if nullptr, builder->top + ".torrent" will be used.
|
||||
|
||||
* @param trackers An array of trackers, sorted by tier from first to last.
|
||||
* NOTE: only the `tier' and `announce' fields are used.
|
||||
*
|
||||
* @param trackerCount size of the `trackers' array
|
||||
*/
|
||||
void tr_makeMetaInfo(
|
||||
tr_metainfo_builder* builder,
|
||||
char const* output_file,
|
||||
tr_tracker_info const* trackers,
|
||||
int n_trackers,
|
||||
char const** webseeds,
|
||||
int n_webseeds,
|
||||
char const* comment,
|
||||
bool is_private,
|
||||
bool anonymize,
|
||||
char const* source);
|
||||
|
||||
@@ -2379,7 +2379,7 @@ static void loadBlocklists(tr_session* session)
|
||||
}
|
||||
}
|
||||
else if (auto const pathinfo = tr_sys_path_get_info(path);
|
||||
path && pathinfo->last_modified_at >= bininfo->last_modified_at)
|
||||
pathinfo && pathinfo->last_modified_at >= bininfo->last_modified_at)
|
||||
{
|
||||
// update it
|
||||
auto const old = tr_pathbuf{ binname, ".old"sv };
|
||||
|
||||
@@ -147,8 +147,6 @@ public:
|
||||
return date_created_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string benc() const;
|
||||
|
||||
[[nodiscard]] constexpr auto infoDictSize() const noexcept
|
||||
{
|
||||
return info_dict_size_;
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
// It may be used under the MIT (SPDX: MIT) license.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
|
||||
#include <libtransmission/error.h>
|
||||
#include <libtransmission/makemeta.h>
|
||||
#include <libtransmission/utils.h>
|
||||
#include <libtransmission/web-utils.h> // tr_urlIsValidTracker()
|
||||
#include <cmath>
|
||||
|
||||
#import "CreatorWindowController.h"
|
||||
#import "Controller.h"
|
||||
@@ -33,9 +39,10 @@
|
||||
@property(nonatomic) IBOutlet NSView* fProgressView;
|
||||
@property(nonatomic) IBOutlet NSProgressIndicator* fProgressIndicator;
|
||||
|
||||
@property(nonatomic, readonly) tr_metainfo_builder* fInfo;
|
||||
@property(nonatomic, readonly) std::shared_ptr<tr_metainfo_builder> fBuilder;
|
||||
@property(nonatomic, readonly) NSURL* fPath;
|
||||
@property(nonatomic) NSURL* fLocation;
|
||||
@property(nonatomic) std::shared_future<tr_error*> fFuture;
|
||||
@property(nonatomic) NSURL* fLocation; // path to new torrent file
|
||||
@property(nonatomic) NSMutableArray<NSString*>* fTrackers;
|
||||
|
||||
@property(nonatomic) NSTimer* fTimer;
|
||||
@@ -89,9 +96,9 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
_fStarted = NO;
|
||||
|
||||
_fPath = path;
|
||||
_fInfo = tr_metaInfoBuilderCreate(_fPath.path.UTF8String);
|
||||
_fBuilder = std::make_shared<tr_metainfo_builder>(_fPath.path.UTF8String);
|
||||
|
||||
if (_fInfo->fileCount == 0)
|
||||
if (_fBuilder->fileCount() == 0U)
|
||||
{
|
||||
NSAlert* alert = [[NSAlert alloc] init];
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> no files -> button")];
|
||||
@@ -105,7 +112,7 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
|
||||
return nil;
|
||||
}
|
||||
if (_fInfo->totalSize == 0)
|
||||
if (_fBuilder->totalSize() == 0U)
|
||||
{
|
||||
NSAlert* alert = [[NSAlert alloc] init];
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> zero size -> button")];
|
||||
@@ -166,32 +173,26 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
self.fNameField.stringValue = name;
|
||||
self.fNameField.toolTip = self.fPath.path;
|
||||
|
||||
BOOL const multifile = self.fInfo->isFolder;
|
||||
auto const is_folder = self.fBuilder->fileCount() > 1 || tr_strvContains(self.fBuilder->path(0), '/');
|
||||
|
||||
NSImage* icon = [NSWorkspace.sharedWorkspace
|
||||
iconForFileType:multifile ? NSFileTypeForHFSTypeCode(kGenericFolderIcon) : self.fPath.pathExtension];
|
||||
iconForFileType:is_folder ? NSFileTypeForHFSTypeCode(kGenericFolderIcon) : self.fPath.pathExtension];
|
||||
icon.size = self.fIconView.frame.size;
|
||||
self.fIconView.image = icon;
|
||||
|
||||
NSString* statusString = [NSString stringForFileSize:self.fInfo->totalSize];
|
||||
if (multifile)
|
||||
NSString* status_string = [NSString stringForFileSize:self.fBuilder->totalSize()];
|
||||
if (is_folder)
|
||||
{
|
||||
NSString* fileString;
|
||||
NSUInteger count = self.fInfo->fileCount;
|
||||
if (count != 1)
|
||||
{
|
||||
fileString = [NSString stringWithFormat:NSLocalizedString(@"%lu files", "Create torrent -> info"), count];
|
||||
}
|
||||
else
|
||||
{
|
||||
fileString = NSLocalizedString(@"1 file", "Create torrent -> info");
|
||||
}
|
||||
statusString = [NSString stringWithFormat:@"%@, %@", fileString, statusString];
|
||||
NSUInteger const count = self.fBuilder->fileCount();
|
||||
NSString* const fileString = count != 1 ?
|
||||
[NSString stringWithFormat:NSLocalizedString(@"%lu files", "Create torrent -> info"), count] :
|
||||
NSLocalizedString(@"1 file", "Create torrent -> info");
|
||||
status_string = [NSString stringWithFormat:@"%@, %@", fileString, status_string];
|
||||
}
|
||||
self.fStatusField.stringValue = statusString;
|
||||
self.fStatusField.stringValue = status_string;
|
||||
|
||||
[self updatePiecesField];
|
||||
self.fPieceSizeStepper.intValue = (int)log2((double)self.fInfo->pieceSize);
|
||||
self.fPieceSizeStepper.intValue = static_cast<int>(log2(self.fBuilder->pieceSize()));
|
||||
|
||||
self.fLocation = [[self.fDefaults URLForKey:@"CreatorLocationURL"]
|
||||
URLByAppendingPathComponent:[name stringByAppendingPathExtension:@"torrent"]];
|
||||
@@ -225,10 +226,7 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_fInfo)
|
||||
{
|
||||
tr_metaInfoBuilderFree(_fInfo);
|
||||
}
|
||||
_fBuilder.reset();
|
||||
|
||||
[_fTimer invalidate];
|
||||
}
|
||||
@@ -359,14 +357,15 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
|
||||
- (IBAction)cancelCreateProgress:(id)sender
|
||||
{
|
||||
self.fInfo->abortFlag = 1;
|
||||
self.fBuilder->cancelChecksums();
|
||||
[self.fTimer fire];
|
||||
}
|
||||
|
||||
- (IBAction)incrementOrDecrementPieceSize:(id)sender
|
||||
{
|
||||
uint32_t pieceSize = (uint32_t)pow(2.0, [sender intValue]);
|
||||
if (tr_metaInfoBuilderSetPieceSize(self.fInfo, pieceSize))
|
||||
auto const piece_size = static_cast<uint32_t>(pow(2.0, [sender intValue]));
|
||||
|
||||
if (self.fBuilder->setPieceSize(piece_size))
|
||||
{
|
||||
[self updatePiecesField];
|
||||
}
|
||||
@@ -517,16 +516,19 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
|
||||
- (void)updatePiecesField
|
||||
{
|
||||
if (self.fInfo->pieceCount == 1)
|
||||
auto const piece_size = self.fBuilder->pieceSize();
|
||||
auto const piece_count = self.fBuilder->pieceCount();
|
||||
|
||||
if (piece_count == 1U)
|
||||
{
|
||||
self.fPiecesField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"1 piece, %@", "Create torrent -> info"),
|
||||
[NSString stringForFileSize:self.fInfo->pieceSize]];
|
||||
[NSString stringForFileSize:piece_size]];
|
||||
}
|
||||
else
|
||||
{
|
||||
self.fPiecesField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%u pieces, %@ each", "Create torrent -> info"),
|
||||
self.fInfo->pieceCount,
|
||||
[NSString stringForFileSize:self.fInfo->pieceSize]];
|
||||
piece_count,
|
||||
[NSString stringForFileSize:piece_size]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,14 +598,13 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
//parse non-empty tracker strings
|
||||
tr_tracker_info* trackerInfo = tr_new0(tr_tracker_info, self.fTrackers.count);
|
||||
|
||||
for (NSUInteger i = 0; i < self.fTrackers.count; i++)
|
||||
// trackers
|
||||
auto trackers = tr_announce_list{};
|
||||
for (NSUInteger i = 0; i < self.fTrackers.count; ++i)
|
||||
{
|
||||
trackerInfo[i].announce = (char*)(self.fTrackers[i]).UTF8String;
|
||||
trackerInfo[i].tier = i;
|
||||
trackers.add((char*)(self.fTrackers[i]).UTF8String, trackers.nextTier());
|
||||
}
|
||||
self.fBuilder->setAnnounceList(std::move(trackers));
|
||||
|
||||
//store values
|
||||
[self.fDefaults setObject:self.fTrackers forKey:@"CreatorTrackers"];
|
||||
@@ -617,88 +618,24 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
self.window.restorable = NO;
|
||||
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:@"BeginCreateTorrentFile" object:self.fLocation userInfo:nil];
|
||||
tr_makeMetaInfo(
|
||||
self.fInfo,
|
||||
self.fLocation.path.UTF8String,
|
||||
trackerInfo,
|
||||
self.fTrackers.count,
|
||||
nullptr,
|
||||
0,
|
||||
self.fCommentView.string.UTF8String,
|
||||
self.fPrivateCheck.state == NSControlStateValueOn,
|
||||
NO,
|
||||
self.fSource.stringValue.UTF8String);
|
||||
tr_free(trackerInfo);
|
||||
|
||||
self.fBuilder->setComment(self.fCommentView.string.UTF8String);
|
||||
self.fBuilder->setPrivate(self.fPrivateCheck.state == NSControlStateValueOn);
|
||||
self.fBuilder->setSource(self.fSource.stringValue.UTF8String);
|
||||
|
||||
self.fFuture = self.fBuilder->makeChecksums();
|
||||
self.fTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(checkProgress) userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
|
||||
- (void)checkProgress
|
||||
{
|
||||
if (self.fInfo->isDone)
|
||||
auto const is_done = !self.fFuture.valid() || self.fFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
|
||||
if (!is_done)
|
||||
{
|
||||
[self.fTimer invalidate];
|
||||
self.fTimer = nil;
|
||||
|
||||
NSAlert* alert;
|
||||
switch (self.fInfo->result)
|
||||
{
|
||||
case TrMakemetaResult::OK:
|
||||
if (self.fOpenWhenCreated)
|
||||
{
|
||||
NSDictionary* dict = @{
|
||||
@"File" : self.fLocation.path,
|
||||
@"Path" : self.fPath.URLByDeletingLastPathComponent.path
|
||||
};
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:@"OpenCreatedTorrentFile" object:self userInfo:dict];
|
||||
}
|
||||
|
||||
[self.window close];
|
||||
break;
|
||||
|
||||
case TrMakemetaResult::CANCELLED:
|
||||
[self.window close];
|
||||
break;
|
||||
|
||||
default:
|
||||
alert = [[NSAlert alloc] init];
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> failed -> button")];
|
||||
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Creation of \"%@\" failed.", "Create torrent -> failed -> title"),
|
||||
self.fLocation.lastPathComponent];
|
||||
alert.alertStyle = NSAlertStyleWarning;
|
||||
|
||||
if (self.fInfo->result == TrMakemetaResult::ERR_IO_READ)
|
||||
{
|
||||
alert.informativeText = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"Could not read \"%s\": %s.", "Create torrent -> failed -> warning"),
|
||||
self.fInfo->errfile,
|
||||
strerror(self.fInfo->my_errno)];
|
||||
}
|
||||
else if (self.fInfo->result == TrMakemetaResult::ERR_IO_WRITE)
|
||||
{
|
||||
alert.informativeText = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"Could not write \"%s\": %s.", "Create torrent -> failed -> warning"),
|
||||
self.fInfo->errfile,
|
||||
strerror(self.fInfo->my_errno)];
|
||||
}
|
||||
else //invalid url should have been caught before creating
|
||||
{
|
||||
alert.informativeText = [NSString
|
||||
stringWithFormat:@"%@ (%d)",
|
||||
NSLocalizedString(@"An unknown error has occurred.", "Create torrent -> failed -> warning"),
|
||||
self.fInfo->result];
|
||||
}
|
||||
|
||||
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
|
||||
[alert.window orderOut:nil];
|
||||
[self.window close];
|
||||
}];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.fProgressIndicator.doubleValue = (double)self.fInfo->pieceIndex / self.fInfo->pieceCount;
|
||||
auto const [current, total] = self.fBuilder->checksumStatus();
|
||||
self.fProgressIndicator.doubleValue = static_cast<double>(current) / total;
|
||||
|
||||
if (!self.fStarted)
|
||||
{
|
||||
@@ -725,6 +662,45 @@ NSMutableSet* creatorWindowControllerSet = nil;
|
||||
|
||||
[window standardWindowButton:NSWindowCloseButton].enabled = NO;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// stop the timer
|
||||
[self.fTimer invalidate];
|
||||
self.fTimer = nil;
|
||||
|
||||
auto success = false;
|
||||
tr_error* error = self.fFuture.get();
|
||||
if (error == nullptr)
|
||||
{
|
||||
self.fBuilder->save(self.fLocation.path.UTF8String, &error);
|
||||
}
|
||||
|
||||
if (error != nullptr)
|
||||
{
|
||||
auto* const alert = [[NSAlert alloc] init];
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> failed -> button")];
|
||||
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Creation of \"%@\" failed.", "Create torrent -> failed -> title"),
|
||||
self.fLocation.lastPathComponent];
|
||||
alert.alertStyle = NSAlertStyleWarning;
|
||||
|
||||
alert.informativeText = [NSString stringWithFormat:@"%s (%d)", error->message, error->code];
|
||||
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
|
||||
[alert.window orderOut:nil];
|
||||
[self.window close];
|
||||
}];
|
||||
tr_error_free(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (self.fOpenWhenCreated)
|
||||
{
|
||||
NSDictionary* dict = @{ @"File" : self.fLocation.path, @"Path" : self.fPath.URLByDeletingLastPathComponent.path };
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:@"OpenCreatedTorrentFile" object:self userInfo:dict];
|
||||
}
|
||||
|
||||
[self.window close];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
qt/MakeDialog.cc
157
qt/MakeDialog.cc
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "MakeDialog.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
#include <QDir>
|
||||
@@ -13,6 +15,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
|
||||
#include <libtransmission/error.h>
|
||||
#include <libtransmission/makemeta.h>
|
||||
#include <libtransmission/transmission.h>
|
||||
#include <libtransmission/utils.h>
|
||||
@@ -32,7 +35,12 @@ class MakeProgressDialog : public BaseDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MakeProgressDialog(Session& session, tr_metainfo_builder& builder, QWidget* parent = nullptr);
|
||||
MakeProgressDialog(
|
||||
Session& session,
|
||||
tr_metainfo_builder& builder,
|
||||
std::future<tr_error*> future,
|
||||
QString outfile,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onButtonBoxClicked(QAbstractButton* button);
|
||||
@@ -41,16 +49,25 @@ private slots:
|
||||
private:
|
||||
Session& session_;
|
||||
tr_metainfo_builder& builder_;
|
||||
std::future<tr_error*> future_;
|
||||
QString const outfile_;
|
||||
Ui::MakeProgressDialog ui_ = {};
|
||||
QTimer timer_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
MakeProgressDialog::MakeProgressDialog(Session& session, tr_metainfo_builder& builder, QWidget* parent)
|
||||
MakeProgressDialog::MakeProgressDialog(
|
||||
Session& session,
|
||||
tr_metainfo_builder& builder,
|
||||
std::future<tr_error*> future,
|
||||
QString outfile,
|
||||
QWidget* parent)
|
||||
: BaseDialog(parent)
|
||||
, session_(session)
|
||||
, builder_(builder)
|
||||
, future_(std::move(future))
|
||||
, outfile_(std::move(outfile))
|
||||
{
|
||||
ui_.setupUi(this);
|
||||
|
||||
@@ -67,13 +84,11 @@ void MakeProgressDialog::onButtonBoxClicked(QAbstractButton* button)
|
||||
switch (ui_.dialogButtons->standardButton(button))
|
||||
{
|
||||
case QDialogButtonBox::Open:
|
||||
session_.addNewlyCreatedTorrent(
|
||||
QString::fromUtf8(builder_.outputFile),
|
||||
QFileInfo(QString::fromUtf8(builder_.top)).dir().path());
|
||||
session_.addNewlyCreatedTorrent(outfile_, QFileInfo(QString::fromStdString(builder_.top())).dir().path());
|
||||
break;
|
||||
|
||||
case QDialogButtonBox::Abort:
|
||||
builder_.abortFlag = true;
|
||||
builder_.cancelChecksums();
|
||||
break;
|
||||
|
||||
default: // QDialogButtonBox::Ok:
|
||||
@@ -85,47 +100,59 @@ void MakeProgressDialog::onButtonBoxClicked(QAbstractButton* button)
|
||||
|
||||
void MakeProgressDialog::onProgress()
|
||||
{
|
||||
auto const is_done = !future_.valid() || future_.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
|
||||
if (is_done)
|
||||
{
|
||||
timer_.stop();
|
||||
}
|
||||
|
||||
// progress bar
|
||||
tr_metainfo_builder const& b = builder_;
|
||||
double const denom = b.pieceCount != 0 ? b.pieceCount : 1;
|
||||
ui_.progressBar->setValue(static_cast<int>((100.0 * b.pieceIndex) / denom));
|
||||
auto progress = int{ 100 }; // [0..100]
|
||||
if (!is_done)
|
||||
{
|
||||
auto const [current, total] = builder_.checksumStatus();
|
||||
progress = static_cast<int>((100.0 * current) / total);
|
||||
}
|
||||
ui_.progressBar->setValue(progress);
|
||||
|
||||
// progress label
|
||||
auto const top = QString::fromUtf8(b.top);
|
||||
auto const top = QString::fromStdString(builder_.top());
|
||||
auto const base = QFileInfo(top).completeBaseName();
|
||||
QString str;
|
||||
|
||||
if (!b.isDone)
|
||||
auto success = false;
|
||||
if (!is_done)
|
||||
{
|
||||
str = tr("Creating \"%1\"").arg(base);
|
||||
}
|
||||
else if (b.result == TrMakemetaResult::OK)
|
||||
else
|
||||
{
|
||||
str = tr("Created \"%1\"!").arg(base);
|
||||
}
|
||||
else if (b.result == TrMakemetaResult::CANCELLED)
|
||||
{
|
||||
str = tr("Cancelled");
|
||||
}
|
||||
else if (b.result == TrMakemetaResult::ERR_URL)
|
||||
{
|
||||
str = tr("Error: invalid announce URL \"%1\"").arg(QString::fromUtf8(b.errfile));
|
||||
}
|
||||
else if (b.result == TrMakemetaResult::ERR_IO_READ)
|
||||
{
|
||||
str = tr("Error reading \"%1\": %2").arg(QString::fromUtf8(b.errfile)).arg(QString::fromUtf8(tr_strerror(b.my_errno)));
|
||||
}
|
||||
else if (b.result == TrMakemetaResult::ERR_IO_WRITE)
|
||||
{
|
||||
str = tr("Error writing \"%1\": %2").arg(QString::fromUtf8(b.errfile)).arg(QString::fromUtf8(tr_strerror(b.my_errno)));
|
||||
tr_error* error = future_.get();
|
||||
|
||||
if (error == nullptr)
|
||||
{
|
||||
builder_.save(outfile_.toStdString(), &error);
|
||||
}
|
||||
|
||||
if (error == nullptr)
|
||||
{
|
||||
str = tr("Created \"%1\"!").arg(base);
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
str = tr("Couldn't create \"%1\": %2 (%3)").arg(base).arg(QString::fromUtf8(error->message)).arg(error->code);
|
||||
tr_error_free(error);
|
||||
}
|
||||
}
|
||||
|
||||
ui_.progressLabel->setText(str);
|
||||
|
||||
// buttons
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Abort)->setEnabled(!b.isDone);
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Ok)->setEnabled(b.isDone);
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Open)->setEnabled(b.isDone && b.result == TrMakemetaResult::OK);
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Abort)->setEnabled(!is_done);
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Ok)->setEnabled(is_done);
|
||||
ui_.dialogButtons->button(QDialogButtonBox::Open)->setEnabled(is_done && success);
|
||||
}
|
||||
|
||||
#include "MakeDialog.moc"
|
||||
@@ -136,68 +163,37 @@ void MakeProgressDialog::onProgress()
|
||||
|
||||
void MakeDialog::makeTorrent()
|
||||
{
|
||||
if (builder_ == nullptr)
|
||||
if (!builder_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// get the tiers
|
||||
int tier = 0;
|
||||
std::vector<tr_tracker_info> trackers;
|
||||
|
||||
for (QString const& line : ui_.trackersEdit->toPlainText().split(QLatin1Char('\n')))
|
||||
{
|
||||
QString const announce_url = line.trimmed();
|
||||
|
||||
if (announce_url.isEmpty())
|
||||
{
|
||||
++tier;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto tmp = tr_tracker_info{};
|
||||
tmp.announce = tr_strdup(announce_url.toUtf8().constData());
|
||||
tmp.tier = tier;
|
||||
trackers.push_back(tmp);
|
||||
}
|
||||
}
|
||||
// get the announce list
|
||||
auto trackers = tr_announce_list();
|
||||
trackers.parse(ui_.trackersEdit->toPlainText().toStdString());
|
||||
builder_->setAnnounceList(std::move(trackers));
|
||||
|
||||
// the file to create
|
||||
QString const path = QString::fromUtf8(builder_->top);
|
||||
auto const path = QString::fromStdString(builder_->top());
|
||||
auto const torrent_name = QFileInfo(path).completeBaseName() + QStringLiteral(".torrent");
|
||||
QString const target = QDir(ui_.destinationButton->path()).filePath(torrent_name);
|
||||
auto const outfile = QDir(ui_.destinationButton->path()).filePath(torrent_name);
|
||||
|
||||
// comment
|
||||
QString comment;
|
||||
|
||||
if (ui_.commentCheck->isChecked())
|
||||
{
|
||||
comment = ui_.commentEdit->text();
|
||||
builder_->setComment(ui_.commentEdit->text().toStdString());
|
||||
}
|
||||
|
||||
// source
|
||||
QString source;
|
||||
|
||||
if (ui_.sourceCheck->isChecked())
|
||||
{
|
||||
source = ui_.sourceEdit->text();
|
||||
builder_->setSource(ui_.sourceEdit->text().toStdString());
|
||||
}
|
||||
|
||||
// start making the torrent
|
||||
tr_makeMetaInfo(
|
||||
builder_.get(),
|
||||
target.toUtf8().constData(),
|
||||
trackers.empty() ? nullptr : trackers.data(),
|
||||
trackers.size(),
|
||||
nullptr,
|
||||
0,
|
||||
comment.isEmpty() ? nullptr : comment.toUtf8().constData(),
|
||||
ui_.privateCheck->isChecked(),
|
||||
false,
|
||||
source.isNull() ? nullptr : source.toUtf8().constData());
|
||||
builder_->setPrivate(ui_.privateCheck->isChecked());
|
||||
|
||||
// pop up the dialog
|
||||
auto* dialog = new MakeProgressDialog(session_, *builder_, this);
|
||||
auto* dialog = new MakeProgressDialog(session_, *builder_, builder_->makeChecksums(), outfile, this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->open();
|
||||
}
|
||||
@@ -221,24 +217,24 @@ void MakeDialog::onSourceChanged()
|
||||
|
||||
if (auto const filename = getSource(); !filename.isEmpty())
|
||||
{
|
||||
builder_.reset(tr_metaInfoBuilderCreate(filename.toUtf8().constData()));
|
||||
builder_.emplace(filename.toStdString());
|
||||
}
|
||||
|
||||
QString text;
|
||||
|
||||
if (builder_ == nullptr)
|
||||
if (!builder_)
|
||||
{
|
||||
text = tr("<i>No source selected</i>");
|
||||
}
|
||||
else
|
||||
{
|
||||
QString const files = tr("%Ln File(s)", nullptr, builder_->fileCount);
|
||||
QString const pieces = tr("%Ln Piece(s)", nullptr, builder_->pieceCount);
|
||||
auto const files = tr("%Ln File(s)", nullptr, builder_->fileCount());
|
||||
auto const pieces = tr("%Ln Piece(s)", nullptr, builder_->pieceCount());
|
||||
text = tr("%1 in %2; %3 @ %4")
|
||||
.arg(Formatter::get().sizeToString(builder_->totalSize))
|
||||
.arg(Formatter::get().sizeToString(builder_->totalSize()))
|
||||
.arg(files)
|
||||
.arg(pieces)
|
||||
.arg(Formatter::get().sizeToString(static_cast<uint64_t>(builder_->pieceSize)));
|
||||
.arg(Formatter::get().sizeToString(static_cast<uint64_t>(builder_->pieceSize())));
|
||||
}
|
||||
|
||||
ui_.sourceSizeLabel->setText(text);
|
||||
@@ -247,7 +243,6 @@ void MakeDialog::onSourceChanged()
|
||||
MakeDialog::MakeDialog(Session& session, QWidget* parent)
|
||||
: BaseDialog(parent)
|
||||
, session_(session)
|
||||
, builder_(nullptr, &tr_metaInfoBuilderFree)
|
||||
{
|
||||
ui_.setupUi(this);
|
||||
|
||||
|
||||
@@ -5,22 +5,17 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <libtransmission/tr-macros.h>
|
||||
#include <libtransmission/makemeta.h>
|
||||
|
||||
#include "BaseDialog.h"
|
||||
#include "ui_MakeDialog.h"
|
||||
|
||||
class QAbstractButton;
|
||||
|
||||
class Session;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
struct tr_metainfo_builder;
|
||||
}
|
||||
|
||||
class MakeDialog : public BaseDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -45,5 +40,5 @@ private:
|
||||
|
||||
Ui::MakeDialog ui_ = {};
|
||||
|
||||
std::unique_ptr<tr_metainfo_builder, void (*)(tr_metainfo_builder*)> builder_;
|
||||
std::optional<tr_metainfo_builder> builder_;
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ TEST(Crypto, DH)
|
||||
b.setPeerPublicKey(a.publicKey());
|
||||
EXPECT_EQ(toString(a.secret()), toString(b.secret()));
|
||||
EXPECT_EQ(a.secret(), b.secret());
|
||||
EXPECT_EQ(96, std::size(a.secret()));
|
||||
EXPECT_EQ(96U, std::size(a.secret()));
|
||||
|
||||
auto c = tr_message_stream_encryption::DH{};
|
||||
c.setPeerPublicKey(b.publicKey());
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib> // mktemp()
|
||||
#include <cstring> // strlen()
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
@@ -16,6 +20,7 @@
|
||||
#include "crypto-utils.h"
|
||||
#include "file.h"
|
||||
#include "makemeta.h"
|
||||
#include "session.h" // TR_NAME
|
||||
#include "torrent-metainfo.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -32,351 +37,188 @@ namespace test
|
||||
class MakemetaTest : public SandboxedTest
|
||||
{
|
||||
protected:
|
||||
void testSingleFileImpl(
|
||||
tr_torrent_metainfo& metainfo,
|
||||
tr_tracker_info const* trackers,
|
||||
int const trackerCount,
|
||||
char const** webseeds,
|
||||
int const webseedCount,
|
||||
void const* payload,
|
||||
size_t const payloadSize,
|
||||
char const* comment,
|
||||
bool isPrivate,
|
||||
bool anonymize,
|
||||
std::string_view source)
|
||||
static auto constexpr DefaultMaxFileCount = size_t{ 16 };
|
||||
static auto constexpr DefaultMaxFileSize = size_t{ 1024 };
|
||||
|
||||
auto makeRandomFiles(
|
||||
std::string_view top,
|
||||
size_t n_files = std::max(size_t{ 1U }, static_cast<size_t>(tr_rand_int_weak(DefaultMaxFileCount))),
|
||||
size_t max_size = DefaultMaxFileSize)
|
||||
{
|
||||
auto files = std::vector<std::pair<std::string, std::vector<std::byte>>>{};
|
||||
|
||||
// create a single input file
|
||||
auto input_file = tr_pathbuf{ sandboxDir(), '/', "test.XXXXXX" };
|
||||
createTmpfileWithContents(std::data(input_file), payload, payloadSize);
|
||||
tr_sys_path_native_separators(std::data(input_file));
|
||||
auto* builder = tr_metaInfoBuilderCreate(input_file);
|
||||
EXPECT_EQ(tr_file_index_t{ 1 }, builder->fileCount);
|
||||
EXPECT_EQ(input_file, builder->top);
|
||||
EXPECT_EQ(input_file, builder->files[0].filename);
|
||||
EXPECT_EQ(payloadSize, builder->files[0].size);
|
||||
EXPECT_EQ(payloadSize, builder->totalSize);
|
||||
EXPECT_FALSE(builder->isFolder);
|
||||
EXPECT_FALSE(builder->abortFlag);
|
||||
|
||||
// have tr_makeMetaInfo() build the torrent file
|
||||
auto const torrent_file = tr_pathbuf{ input_file, ".torrent"sv };
|
||||
tr_makeMetaInfo(
|
||||
builder,
|
||||
torrent_file,
|
||||
trackers,
|
||||
trackerCount,
|
||||
webseeds,
|
||||
webseedCount,
|
||||
comment,
|
||||
isPrivate,
|
||||
anonymize,
|
||||
std::string(source).c_str());
|
||||
EXPECT_EQ(isPrivate, builder->isPrivate);
|
||||
EXPECT_EQ(anonymize, builder->anonymize);
|
||||
EXPECT_EQ(torrent_file, builder->outputFile);
|
||||
EXPECT_STREQ(comment, builder->comment);
|
||||
EXPECT_EQ(source, builder->source);
|
||||
EXPECT_EQ(trackerCount, builder->trackerCount);
|
||||
|
||||
while (!builder->isDone)
|
||||
for (size_t i = 0; i < n_files; ++i)
|
||||
{
|
||||
tr_wait_msec(100);
|
||||
auto payload = std::vector<std::byte>{};
|
||||
// TODO(5.0.0): zero-sized files are disabled in these test
|
||||
// because tr_torrent_metainfo discards them, throwing off the
|
||||
// builder-to-metainfo comparisons here. tr_torrent_metainfo
|
||||
// will behave when BEP52 support is added in Transmission 5.
|
||||
static auto constexpr MinFileSize = size_t{ 1U };
|
||||
payload.resize(std::max(MinFileSize, static_cast<size_t>(tr_rand_int_weak(max_size))));
|
||||
tr_rand_buffer(std::data(payload), std::size(payload));
|
||||
|
||||
auto filename = tr_pathbuf{ top, '/', "test.XXXXXX" };
|
||||
createTmpfileWithContents(std::data(filename), std::data(payload), std::size(payload));
|
||||
tr_sys_path_native_separators(std::data(filename));
|
||||
|
||||
files.emplace_back(std::string{ filename.sv() }, payload);
|
||||
}
|
||||
sync();
|
||||
|
||||
// now let's check our work: parse the torrent file
|
||||
EXPECT_TRUE(metainfo.parseTorrentFile(torrent_file));
|
||||
|
||||
// quick check of some of the parsed metainfo
|
||||
EXPECT_EQ(payloadSize, metainfo.totalSize());
|
||||
EXPECT_EQ(tr_sys_path_basename(input_file), metainfo.name());
|
||||
EXPECT_EQ(comment, metainfo.comment());
|
||||
EXPECT_EQ(isPrivate, metainfo.isPrivate());
|
||||
EXPECT_EQ(size_t(trackerCount), std::size(metainfo.announceList()));
|
||||
EXPECT_EQ(size_t(webseedCount), metainfo.webseedCount());
|
||||
EXPECT_EQ(tr_file_index_t{ 1 }, metainfo.fileCount());
|
||||
EXPECT_EQ(tr_sys_path_basename(input_file), metainfo.fileSubpath(0));
|
||||
EXPECT_EQ(payloadSize, metainfo.fileSize(0));
|
||||
|
||||
// cleanup
|
||||
tr_metaInfoBuilderFree(builder);
|
||||
return files;
|
||||
}
|
||||
|
||||
void testSingleDirectoryImpl(
|
||||
tr_tracker_info const* trackers,
|
||||
int const tracker_count,
|
||||
char const** webseeds,
|
||||
int const webseed_count,
|
||||
void const** payloads,
|
||||
size_t const* payload_sizes,
|
||||
size_t const payload_count,
|
||||
char const* comment,
|
||||
bool const is_private,
|
||||
bool const anonymize,
|
||||
char const* source)
|
||||
static auto testBuilder(tr_metainfo_builder& builder)
|
||||
{
|
||||
// create the top temp directory
|
||||
auto top = tr_pathbuf{ sandboxDir(), '/', "folder.XXXXXX"sv };
|
||||
tr_sys_path_native_separators(std::data(top));
|
||||
tr_sys_dir_create_temp(std::data(top));
|
||||
tr_error* error = builder.makeChecksums().get();
|
||||
EXPECT_EQ(error, nullptr) << *error;
|
||||
|
||||
// build the payload files that go into the top temp directory
|
||||
auto files = std::vector<std::string>{};
|
||||
files.reserve(payload_count);
|
||||
size_t total_size = 0;
|
||||
|
||||
for (size_t i = 0; i < payload_count; i++)
|
||||
{
|
||||
auto path = fmt::format(FMT_STRING("{:s}/file.{:04}XXXXXX"), top.sv(), i);
|
||||
createTmpfileWithContents(std::data(path), payloads[i], payload_sizes[i]);
|
||||
tr_sys_path_native_separators(std::data(path));
|
||||
files.push_back(path);
|
||||
total_size += payload_sizes[i];
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
// init the builder
|
||||
auto* builder = tr_metaInfoBuilderCreate(top);
|
||||
EXPECT_FALSE(builder->abortFlag);
|
||||
EXPECT_EQ(top, builder->top);
|
||||
EXPECT_EQ(payload_count, builder->fileCount);
|
||||
EXPECT_EQ(total_size, builder->totalSize);
|
||||
EXPECT_TRUE(builder->isFolder);
|
||||
|
||||
for (size_t i = 0; i < builder->fileCount; ++i)
|
||||
{
|
||||
EXPECT_EQ(files[i], builder->files[i].filename);
|
||||
EXPECT_EQ(payload_sizes[i], builder->files[i].size);
|
||||
}
|
||||
|
||||
// build the torrent file
|
||||
auto const torrent_file = tr_pathbuf{ top, ".torrent"sv };
|
||||
tr_makeMetaInfo(
|
||||
builder,
|
||||
torrent_file.c_str(),
|
||||
trackers,
|
||||
tracker_count,
|
||||
webseeds,
|
||||
webseed_count,
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
source);
|
||||
EXPECT_EQ(is_private, builder->isPrivate);
|
||||
EXPECT_EQ(anonymize, builder->anonymize);
|
||||
EXPECT_EQ(torrent_file, builder->outputFile);
|
||||
EXPECT_STREQ(comment, builder->comment);
|
||||
EXPECT_STREQ(source, builder->source);
|
||||
EXPECT_EQ(tracker_count, builder->trackerCount);
|
||||
auto test = [&builder]()
|
||||
{
|
||||
return builder->isDone;
|
||||
};
|
||||
EXPECT_TRUE(waitFor(test, 5000));
|
||||
sync();
|
||||
|
||||
// now let's check our work: parse the torrent file
|
||||
auto metainfo = tr_torrent_metainfo{};
|
||||
EXPECT_TRUE(metainfo.parseTorrentFile(torrent_file));
|
||||
|
||||
// quick check of some of the parsed metainfo
|
||||
EXPECT_EQ(total_size, metainfo.totalSize());
|
||||
EXPECT_EQ(tr_sys_path_basename(top), metainfo.name());
|
||||
EXPECT_EQ(comment, metainfo.comment());
|
||||
EXPECT_EQ(source, metainfo.source());
|
||||
EXPECT_EQ(payload_count, metainfo.fileCount());
|
||||
EXPECT_EQ(is_private, metainfo.isPrivate());
|
||||
EXPECT_EQ(size_t(tracker_count), std::size(metainfo.announceList()));
|
||||
|
||||
// cleanup
|
||||
tr_metaInfoBuilderFree(builder);
|
||||
}
|
||||
|
||||
void testSingleDirectoryRandomPayloadImpl(
|
||||
tr_tracker_info const* trackers,
|
||||
int const tracker_count,
|
||||
char const** webseeds,
|
||||
int const webseed_count,
|
||||
size_t const max_file_count,
|
||||
size_t const max_file_size,
|
||||
char const* comment,
|
||||
bool const is_private,
|
||||
bool const anonymize,
|
||||
char const* source)
|
||||
{
|
||||
// build random payloads
|
||||
size_t const payload_count = 1 + tr_rand_int_weak(max_file_count);
|
||||
auto** payloads = tr_new0(void*, payload_count);
|
||||
auto* payload_sizes = tr_new0(size_t, payload_count);
|
||||
|
||||
for (size_t i = 0; i < payload_count; i++)
|
||||
EXPECT_TRUE(metainfo.parseBenc(builder.benc()));
|
||||
EXPECT_EQ(builder.fileCount(), metainfo.fileCount());
|
||||
EXPECT_EQ(builder.pieceSize(), metainfo.pieceSize());
|
||||
EXPECT_EQ(builder.totalSize(), metainfo.totalSize());
|
||||
EXPECT_EQ(builder.totalSize(), metainfo.totalSize());
|
||||
for (size_t i = 0, n = std::min(builder.fileCount(), metainfo.fileCount()); i < n; ++i)
|
||||
{
|
||||
size_t const n = 1 + tr_rand_int_weak(max_file_size);
|
||||
payloads[i] = tr_new(char, n);
|
||||
tr_rand_buffer(payloads[i], n);
|
||||
payload_sizes[i] = n;
|
||||
EXPECT_EQ(builder.fileSize(i), metainfo.files().fileSize(i));
|
||||
EXPECT_EQ(builder.path(i), metainfo.files().path(i));
|
||||
}
|
||||
|
||||
// run the test
|
||||
testSingleDirectoryImpl(
|
||||
trackers,
|
||||
tracker_count,
|
||||
webseeds,
|
||||
webseed_count,
|
||||
const_cast<void const**>(payloads),
|
||||
payload_sizes,
|
||||
payload_count,
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
source);
|
||||
|
||||
// cleanup
|
||||
for (size_t i = 0; i < payload_count; i++)
|
||||
{
|
||||
tr_free(payloads[i]);
|
||||
}
|
||||
|
||||
tr_free(payloads);
|
||||
tr_free(payload_sizes);
|
||||
EXPECT_EQ(builder.name(), metainfo.name());
|
||||
EXPECT_EQ(builder.comment(), metainfo.comment());
|
||||
EXPECT_EQ(builder.isPrivate(), metainfo.isPrivate());
|
||||
EXPECT_EQ(builder.announceList().toString(), metainfo.announceList().toString());
|
||||
return metainfo;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MakemetaTest, comment)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
|
||||
static auto constexpr Comment = "This is the comment"sv;
|
||||
builder.setComment(Comment);
|
||||
|
||||
EXPECT_EQ(Comment, testBuilder(builder).comment());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, source)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
|
||||
static auto constexpr Source = "This is the source"sv;
|
||||
builder.setSource(Source);
|
||||
|
||||
EXPECT_EQ(Source, testBuilder(builder).source());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, isPrivate)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
|
||||
for (bool const is_private : { true, false })
|
||||
{
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
builder.setPrivate(is_private);
|
||||
EXPECT_EQ(is_private, testBuilder(builder).isPrivate());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, pieceSize)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
|
||||
for (uint32_t const piece_size : { 16384, 32768 })
|
||||
{
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
builder.setPieceSize(piece_size);
|
||||
EXPECT_EQ(piece_size, testBuilder(builder).pieceSize());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, webseeds)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
|
||||
static auto constexpr Webseed = "https://www.example.com/linux.iso"sv;
|
||||
builder.setWebseeds(std::vector<std::string>{ std::string{ Webseed } });
|
||||
|
||||
auto const metainfo = testBuilder(builder);
|
||||
EXPECT_EQ(1U, metainfo.webseedCount());
|
||||
EXPECT_EQ(Webseed, metainfo.webseed(0));
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, nameIsRootSingleFile)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
EXPECT_EQ(tr_sys_path_basename(filename), testBuilder(builder).name());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, anonymizeTrue)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
builder.setAnonymize(true);
|
||||
auto const metainfo = testBuilder(builder);
|
||||
EXPECT_EQ(""sv, metainfo.creator());
|
||||
EXPECT_EQ(time_t{}, metainfo.dateCreated());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, anonymizeFalse)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
builder.setAnonymize(false);
|
||||
auto const metainfo = testBuilder(builder);
|
||||
EXPECT_TRUE(tr_strvContains(metainfo.creator(), TR_NAME)) << metainfo.creator();
|
||||
auto const now = time(nullptr);
|
||||
EXPECT_LE(metainfo.dateCreated(), now);
|
||||
EXPECT_LE(now - 60, metainfo.dateCreated());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, nameIsRootMultifile)
|
||||
{
|
||||
auto const files = makeRandomFiles(sandboxDir(), 10);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
EXPECT_EQ(tr_sys_path_basename(filename), testBuilder(builder).name());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, singleFile)
|
||||
{
|
||||
auto trackers = std::array<tr_tracker_info, 16>{};
|
||||
auto tracker_count = int{};
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
|
||||
++tracker_count;
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
|
||||
++tracker_count;
|
||||
auto const payload = std::string{ "Hello, World!\n" };
|
||||
char const* const comment = "This is the comment";
|
||||
bool const is_private = false;
|
||||
bool const anonymize = false;
|
||||
auto metainfo = tr_torrent_metainfo{};
|
||||
testSingleFileImpl(
|
||||
metainfo,
|
||||
trackers.data(),
|
||||
tracker_count,
|
||||
nullptr,
|
||||
0,
|
||||
payload.data(),
|
||||
payload.size(),
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
"TESTME"sv);
|
||||
}
|
||||
auto const files = makeRandomFiles(sandboxDir(), 1);
|
||||
auto const [filename, payload] = files.front();
|
||||
auto builder = tr_metainfo_builder{ filename };
|
||||
|
||||
TEST_F(MakemetaTest, webseed)
|
||||
{
|
||||
auto trackers = std::vector<tr_tracker_info>{};
|
||||
auto webseeds = std::vector<char const*>{};
|
||||
auto trackers = tr_announce_list{};
|
||||
trackers.add("udp://tracker.openbittorrent.com:80"sv, trackers.nextTier());
|
||||
trackers.add("udp://tracker.publicbt.com:80"sv, trackers.nextTier());
|
||||
builder.setAnnounceList(std::move(trackers));
|
||||
|
||||
webseeds.emplace_back("https://www.example.com/linux.iso");
|
||||
static auto constexpr Comment = "This is the comment"sv;
|
||||
builder.setComment(Comment);
|
||||
|
||||
auto const payload = std::string{ "Hello, World!\n" };
|
||||
char const* const comment = "This is the comment";
|
||||
bool const is_private = false;
|
||||
bool const anonymize = false;
|
||||
auto metainfo = tr_torrent_metainfo{};
|
||||
testSingleFileImpl(
|
||||
metainfo,
|
||||
std::data(trackers),
|
||||
std::size(trackers),
|
||||
std::data(webseeds),
|
||||
std::size(webseeds),
|
||||
payload.data(),
|
||||
payload.size(),
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
"TESTME"sv);
|
||||
}
|
||||
static auto constexpr IsPrivate = false;
|
||||
builder.setPrivate(IsPrivate);
|
||||
|
||||
TEST_F(MakemetaTest, singleFileDifferentSourceFlags)
|
||||
{
|
||||
auto trackers = std::array<tr_tracker_info, 16>{};
|
||||
auto tracker_count = int{};
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
|
||||
++tracker_count;
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
|
||||
++tracker_count;
|
||||
auto const payload = std::string{ "Hello, World!\n" };
|
||||
char const* const comment = "This is the comment";
|
||||
bool const is_private = false;
|
||||
bool const anonymize = false;
|
||||
static auto constexpr Anonymize = false;
|
||||
builder.setAnonymize(Anonymize);
|
||||
|
||||
auto metainfo_foobar = tr_torrent_metainfo{};
|
||||
testSingleFileImpl(
|
||||
metainfo_foobar,
|
||||
trackers.data(),
|
||||
tracker_count,
|
||||
nullptr,
|
||||
0,
|
||||
payload.data(),
|
||||
payload.size(),
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
"FOOBAR"sv);
|
||||
|
||||
auto metainfo_testme = tr_torrent_metainfo{};
|
||||
testSingleFileImpl(
|
||||
metainfo_testme,
|
||||
trackers.data(),
|
||||
tracker_count,
|
||||
nullptr,
|
||||
0,
|
||||
payload.data(),
|
||||
payload.size(),
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
"TESTME"sv);
|
||||
|
||||
EXPECT_NE(metainfo_foobar.infoHash(), metainfo_testme.infoHash());
|
||||
}
|
||||
|
||||
TEST_F(MakemetaTest, singleDirectoryRandomPayload)
|
||||
{
|
||||
auto constexpr DefaultMaxFileCount = size_t{ 16 };
|
||||
auto constexpr DefaultMaxFileSize = size_t{ 1024 };
|
||||
|
||||
auto trackers = std::array<tr_tracker_info, 16>{};
|
||||
auto tracker_count = int{};
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
|
||||
++tracker_count;
|
||||
trackers[tracker_count].tier = tracker_count;
|
||||
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
|
||||
++tracker_count;
|
||||
char const* const comment = "This is the comment";
|
||||
bool const is_private = false;
|
||||
bool const anonymize = false;
|
||||
char const* const source = "TESTME";
|
||||
|
||||
for (size_t i = 0; i < 10; ++i)
|
||||
{
|
||||
testSingleDirectoryRandomPayloadImpl(
|
||||
trackers.data(),
|
||||
tracker_count,
|
||||
nullptr,
|
||||
0,
|
||||
DefaultMaxFileCount,
|
||||
DefaultMaxFileSize,
|
||||
comment,
|
||||
is_private,
|
||||
anonymize,
|
||||
source);
|
||||
}
|
||||
testBuilder(builder);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
|
||||
@@ -198,7 +198,7 @@ protected:
|
||||
errno = tmperr;
|
||||
}
|
||||
|
||||
static void blockingFileWrite(tr_sys_file_t fd, void const* data, size_t data_len)
|
||||
static void blockingFileWrite(tr_sys_file_t fd, void const* data, size_t data_len, tr_error** error = nullptr)
|
||||
{
|
||||
uint64_t n_left = data_len;
|
||||
auto const* left = static_cast<uint8_t const*>(data);
|
||||
@@ -206,11 +206,12 @@ protected:
|
||||
while (n_left > 0)
|
||||
{
|
||||
uint64_t n = {};
|
||||
tr_error* error = nullptr;
|
||||
if (!tr_sys_file_write(fd, left, n_left, &n, &error))
|
||||
tr_error* local_error = nullptr;
|
||||
if (!tr_sys_file_write(fd, left, n_left, &n, error))
|
||||
{
|
||||
fprintf(stderr, "Error writing file: '%s'\n", error->message);
|
||||
tr_error_free(error);
|
||||
fprintf(stderr, "Error writing file: '%s'\n", local_error->message);
|
||||
tr_error_propagate(error, &local_error);
|
||||
tr_error_free(local_error);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -225,10 +226,21 @@ protected:
|
||||
|
||||
buildParentDir(tmpl);
|
||||
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.InnerPointer)
|
||||
auto const fd = tr_sys_file_open_temp(tmpl);
|
||||
blockingFileWrite(fd, payload, n);
|
||||
tr_sys_file_close(fd);
|
||||
tr_error* error = nullptr;
|
||||
auto const fd = tr_sys_file_open_temp(tmpl, &error);
|
||||
blockingFileWrite(fd, payload, n, &error);
|
||||
tr_sys_file_flush(fd, &error);
|
||||
tr_sys_file_flush(fd, &error);
|
||||
tr_sys_file_close(fd, &error);
|
||||
if (error != nullptr)
|
||||
{
|
||||
fmt::print(
|
||||
"Couldn't create '{path}': {error} ({error_code})\n",
|
||||
fmt::arg("path", tmpl),
|
||||
fmt::arg("error", error->message),
|
||||
fmt::arg("error_code", error->code));
|
||||
tr_error_free(error);
|
||||
}
|
||||
sync();
|
||||
|
||||
errno = tmperr;
|
||||
|
||||
172
utils/create.cc
172
utils/create.cc
@@ -4,14 +4,14 @@
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes> // PRIu32
|
||||
#include <chrono>
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <future>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
|
||||
#include <libtransmission/error.h>
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <libtransmission/log.h>
|
||||
#include <libtransmission/makemeta.h>
|
||||
#include <libtransmission/tr-getopt.h>
|
||||
#include <libtransmission/tr-strbuf.h>
|
||||
#include <libtransmission/utils.h>
|
||||
#include <libtransmission/version.h>
|
||||
|
||||
@@ -50,16 +49,16 @@ auto constexpr Options = std::array<tr_option, 10>{
|
||||
|
||||
struct app_options
|
||||
{
|
||||
std::vector<tr_tracker_info> trackers;
|
||||
std::vector<char const*> webseeds;
|
||||
tr_announce_list trackers;
|
||||
std::vector<std::string> webseeds;
|
||||
std::string outfile;
|
||||
char const* comment = nullptr;
|
||||
char const* infile = nullptr;
|
||||
char const* source = nullptr;
|
||||
uint32_t piecesize_kib = 0;
|
||||
std::string_view comment;
|
||||
std::string_view infile;
|
||||
std::string_view source;
|
||||
uint32_t piece_size = 0;
|
||||
bool anonymize = false;
|
||||
bool is_private = false;
|
||||
bool show_version = false;
|
||||
bool anonymize = false;
|
||||
};
|
||||
|
||||
int parseCommandLine(app_options& options, int argc, char const* const* argv)
|
||||
@@ -88,25 +87,23 @@ int parseCommandLine(app_options& options, int argc, char const* const* argv)
|
||||
break;
|
||||
|
||||
case 't':
|
||||
options.trackers.push_back(tr_tracker_info{ 0, const_cast<char*>(optarg) });
|
||||
options.trackers.add(optarg, options.trackers.nextTier());
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
options.webseeds.push_back(optarg);
|
||||
options.webseeds.emplace_back(optarg);
|
||||
break;
|
||||
|
||||
case 's':
|
||||
if (optarg != nullptr)
|
||||
{
|
||||
char* endptr = nullptr;
|
||||
options.piecesize_kib = strtoul(optarg, &endptr, 10);
|
||||
|
||||
options.piece_size = strtoul(optarg, &endptr, 10) * KiB;
|
||||
if (endptr != nullptr && *endptr == 'M')
|
||||
{
|
||||
options.piecesize_kib *= KiB;
|
||||
options.piece_size *= KiB;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
@@ -129,29 +126,24 @@ int parseCommandLine(app_options& options, int argc, char const* const* argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* tr_getcwd(void)
|
||||
std::string tr_getcwd()
|
||||
{
|
||||
char* result;
|
||||
tr_error* error = nullptr;
|
||||
|
||||
result = tr_sys_dir_get_current(&error);
|
||||
|
||||
if (result == nullptr)
|
||||
if (char* const cur = tr_sys_dir_get_current(&error); cur != nullptr)
|
||||
{
|
||||
fprintf(stderr, "getcwd error: \"%s\"", error->message);
|
||||
tr_error_free(error);
|
||||
result = tr_strdup("");
|
||||
auto path = std::string{ cur };
|
||||
tr_free(cur);
|
||||
return path;
|
||||
}
|
||||
|
||||
return result;
|
||||
fprintf(stderr, "getcwd error: \"%s\"", error->message);
|
||||
tr_error_free(error);
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int tr_main(int argc, char* argv[])
|
||||
{
|
||||
tr_metainfo_builder* b = nullptr;
|
||||
|
||||
tr_logSetLevel(TR_LOG_ERROR);
|
||||
tr_formatter_mem_init(MemK, MemKStr, MemMStr, MemGStr, MemTStr);
|
||||
tr_formatter_size_init(DiskK, DiskKStr, DiskMStr, DiskGStr, DiskTStr);
|
||||
@@ -169,7 +161,7 @@ int tr_main(int argc, char* argv[])
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (options.infile == nullptr)
|
||||
if (std::empty(options.infile))
|
||||
{
|
||||
fprintf(stderr, "ERROR: No input file or directory specified.\n");
|
||||
tr_getopt_usage(MyName, Usage, std::data(Options));
|
||||
@@ -188,9 +180,7 @@ int tr_main(int argc, char* argv[])
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char* const cwd = tr_getcwd();
|
||||
options.outfile = tr_strvDup(tr_pathbuf{ std::string_view{ cwd }, '/', base, ".torrent"sv });
|
||||
tr_free(cwd);
|
||||
options.outfile = fmt::format("{:s}/{:s}.torrent"sv, tr_getcwd(), base);
|
||||
}
|
||||
|
||||
if (std::empty(options.trackers))
|
||||
@@ -206,92 +196,88 @@ int tr_main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
printf("Creating torrent \"%s\"\n", options.outfile.c_str());
|
||||
fmt::print("Creating torrent \"{:s}\"\n", options.outfile);
|
||||
|
||||
b = tr_metaInfoBuilderCreate(options.infile);
|
||||
|
||||
if (b == nullptr)
|
||||
auto builder = tr_metainfo_builder(options.infile);
|
||||
auto const n_files = builder.fileCount();
|
||||
if (n_files == 0U)
|
||||
{
|
||||
fprintf(stderr, "ERROR: Cannot find specified input file or directory.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < b->fileCount; ++i)
|
||||
for (tr_file_index_t i = 0; i < n_files; ++i)
|
||||
{
|
||||
if (auto const& file = b->files[i]; !file.is_portable)
|
||||
auto const& path = builder.path(i);
|
||||
if (!tr_torrent_files::isSubpathPortable(path))
|
||||
{
|
||||
fprintf(stderr, "WARNING: consider renaming nonportable filename \"%s\".\n", file.filename);
|
||||
fmt::print(stderr, "WARNING\n");
|
||||
fmt::print(stderr, "filename \"{:s}\" may not be portable on all systems.\n", path);
|
||||
fmt::print(stderr, "consider \"{:s}\" instead.\n", tr_torrent_files::makeSubpathPortable(path));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.piecesize_kib != 0)
|
||||
if (options.piece_size != 0 && !builder.setPieceSize(options.piece_size))
|
||||
{
|
||||
tr_metaInfoBuilderSetPieceSize(b, options.piecesize_kib * KiB);
|
||||
fmt::print(stderr, "ERROR: piece size must be at least 16 KiB and must be a power of two.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
printf(
|
||||
b->fileCount > 1 ? " %" PRIu32 " files, %s\n" : " %" PRIu32 " file, %s\n",
|
||||
b->fileCount,
|
||||
tr_formatter_size_B(b->totalSize).c_str());
|
||||
printf(
|
||||
b->pieceCount > 1 ? " %" PRIu32 " pieces, %s each\n" : " %" PRIu32 " piece, %s\n",
|
||||
b->pieceCount,
|
||||
tr_formatter_size_B(b->pieceSize).c_str());
|
||||
fmt::print(
|
||||
ngettext("{:d} files, {:s}\n", "{:d} file, {:s}\n", builder.fileCount()),
|
||||
builder.fileCount(),
|
||||
tr_formatter_size_B(builder.totalSize()));
|
||||
|
||||
tr_makeMetaInfo(
|
||||
b,
|
||||
options.outfile.c_str(),
|
||||
std::data(options.trackers),
|
||||
static_cast<int>(std::size(options.trackers)),
|
||||
std::data(options.webseeds),
|
||||
static_cast<int>(std::size(options.webseeds)),
|
||||
options.comment,
|
||||
options.is_private,
|
||||
options.anonymize,
|
||||
options.source);
|
||||
fmt::print(
|
||||
ngettext("{:d} pieces, {:s} each\n", "{:d} piece, {:s}\n", builder.pieceCount()),
|
||||
builder.pieceCount(),
|
||||
tr_formatter_size_B(builder.pieceSize()));
|
||||
|
||||
uint32_t last = UINT32_MAX;
|
||||
while (!b->isDone)
|
||||
if (!std::empty(options.comment))
|
||||
{
|
||||
tr_wait_msec(500);
|
||||
builder.setComment(options.comment);
|
||||
}
|
||||
|
||||
uint32_t current = b->pieceIndex;
|
||||
if (current != last)
|
||||
if (!std::empty(options.source))
|
||||
{
|
||||
builder.setSource(options.source);
|
||||
}
|
||||
|
||||
builder.setPrivate(options.is_private);
|
||||
builder.setAnonymize(options.anonymize);
|
||||
builder.setWebseeds(std::move(options.webseeds));
|
||||
builder.setAnnounceList(std::move(options.trackers));
|
||||
|
||||
auto future = builder.makeChecksums();
|
||||
auto last = std::optional<tr_piece_index_t>{};
|
||||
while (future.wait_for(std::chrono::milliseconds(500)) != std::future_status::ready)
|
||||
{
|
||||
auto const [current, total] = builder.checksumStatus();
|
||||
|
||||
if (!last || current != *last)
|
||||
{
|
||||
printf("\rPiece %" PRIu32 "/%" PRIu32 " ...", current, b->pieceCount);
|
||||
fmt::print("\rPiece {:d}/{:d} ...", current, total);
|
||||
fflush(stdout);
|
||||
|
||||
last = current;
|
||||
}
|
||||
}
|
||||
|
||||
putc(' ', stdout);
|
||||
fmt::print(" ");
|
||||
|
||||
switch (b->result)
|
||||
if (tr_error* error = future.get(); error != nullptr)
|
||||
{
|
||||
case TrMakemetaResult::OK:
|
||||
printf("done!");
|
||||
break;
|
||||
|
||||
case TrMakemetaResult::ERR_URL:
|
||||
printf("bad announce URL: \"%s\"", b->errfile);
|
||||
break;
|
||||
|
||||
case TrMakemetaResult::ERR_IO_READ:
|
||||
printf("error reading \"%s\": %s", b->errfile, tr_strerror(b->my_errno));
|
||||
break;
|
||||
|
||||
case TrMakemetaResult::ERR_IO_WRITE:
|
||||
printf("error writing \"%s\": %s", b->errfile, tr_strerror(b->my_errno));
|
||||
break;
|
||||
|
||||
case TrMakemetaResult::CANCELLED:
|
||||
printf("cancelled");
|
||||
break;
|
||||
fmt::print("ERROR: {:s} {:d}\n", error->message, error->code);
|
||||
tr_error_free(error);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
putc('\n', stdout);
|
||||
if (tr_error* error = nullptr; !builder.save(options.outfile, &error))
|
||||
{
|
||||
fmt::print("ERROR: could not save \"{:s}\": {:s} {:d}\n", options.outfile, error->message, error->code);
|
||||
tr_error_free(error);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
tr_metaInfoBuilderFree(b);
|
||||
fmt::print("done!\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user