refactor: add tr_torrent_files::move() and remove() (#2919)

This commit is contained in:
Charles Kerr 2022-04-15 18:39:04 -05:00 committed by GitHub
parent 4590d172de
commit 2866638e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 894 additions and 538 deletions

View File

@ -14,8 +14,8 @@
1BB44E07B1B52E28291B4E32 /* file-piece-map.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1BB44E07B1B52E28291B4E30 /* file-piece-map.cc */; };
1BB44E07B1B52E28291B4E33 /* file-piece-map.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BB44E07B1B52E28291B4E31 /* file-piece-map.h */; };
2856E0656A49F2665D69E760 /* benc.h in Headers */ = {isa = PBXBuildFile; fileRef = 2856E0656A49F2665D69E761 /* benc.h */; };
A47A7C87B8B57BE50DF0D410 /* files.cc in Sources */ = {isa = PBXBuildFile; fileRef = A47A7C87B8B57BE50DF0D411 /* files.cc */; };
A47A7C87B8B57BE50DF0D412 /* files.h in Headers */ = {isa = PBXBuildFile; fileRef = A47A7C87B8B57BE50DF0D413 /* files.h */; };
A47A7C87B8B57BE50DF0D410 /* torrent-files.cc in Sources */ = {isa = PBXBuildFile; fileRef = A47A7C87B8B57BE50DF0D411 /* torrent-files.cc */; };
A47A7C87B8B57BE50DF0D412 /* torrent-files.h in Headers */ = {isa = PBXBuildFile; fileRef = A47A7C87B8B57BE50DF0D413 /* torrent-files.h */; };
2B9BA6C508B488FE586A0AB0 /* torrents.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2B9BA6C508B488FE586A0AB1 /* torrents.cc */; };
2B9BA6C508B488FE586A0AB2 /* torrents.h in Headers */ = {isa = PBXBuildFile; fileRef = 2B9BA6C508B488FE586A0AB3 /* torrents.h */; };
35F373030C2DA89000DAA8F2 /* FilePriorityCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 35F373010C2DA88F00DAA8F2 /* FilePriorityCell.mm */; };
@ -571,8 +571,8 @@
29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
2B9BA6C508B488FE586A0AB1 /* torrents.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = torrents.cc; sourceTree = "<group>"; };
2B9BA6C508B488FE586A0AB3 /* torrents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = torrents.h; sourceTree = "<group>"; };
A47A7C87B8B57BE50DF0D411 /* files.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = files.cc; sourceTree = "<group>"; };
A47A7C87B8B57BE50DF0D413 /* files.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = files.h; sourceTree = "<group>"; };
A47A7C87B8B57BE50DF0D411 /* torrent-files.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = torrent-files.cc; sourceTree = "<group>"; };
A47A7C87B8B57BE50DF0D413 /* torrent-files.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = torrent-files.h; sourceTree = "<group>"; };
32CA4F630368D1EE00C91783 /* Transmission_Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Transmission_Prefix.pch; sourceTree = "<group>"; };
35F373000C2DA88F00DAA8F2 /* FilePriorityCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FilePriorityCell.h; sourceTree = "<group>"; };
35F373010C2DA88F00DAA8F2 /* FilePriorityCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FilePriorityCell.mm; sourceTree = "<group>"; };
@ -1567,7 +1567,7 @@
A29DF8B70DB2544C00D04E5A /* resume.h */,
A29DF8B80DB2544C00D04E5A /* torrent.h */,
2B9BA6C508B488FE586A0AB3 /* torrents.h */,
A47A7C87B8B57BE50DF0D413 /* files.h */,
A47A7C87B8B57BE50DF0D413 /* torrent-files.h */,
C1033E031A3279B800EF44D8 /* crypto-utils-fallback.cc */,
C1033E041A3279B800EF44D8 /* crypto-utils-ccrypto.cc */,
C1033E051A3279B800EF44D8 /* crypto-utils.cc */,
@ -1615,7 +1615,7 @@
A2AA9BE0132CAC8D00FA131E /* announcer-udp.cc */,
BEFC1DF90C07861A00B0BB3C /* torrent.cc */,
2B9BA6C508B488FE586A0AB1 /* torrents.cc */,
A47A7C87B8B57BE50DF0D411 /* files.cc */,
A47A7C87B8B57BE50DF0D411 /* torrent-files.cc */,
BEFC1DFC0C07861A00B0BB3C /* port-forwarding.h */,
BEFC1DFD0C07861A00B0BB3C /* port-forwarding.cc */,
A21FBBA90EDA78C300BC3C51 /* bandwidth.h */,
@ -2109,7 +2109,7 @@
A29DF8BA0DB2544C00D04E5A /* resume.h in Headers */,
A29DF8BB0DB2544C00D04E5A /* torrent.h in Headers */,
2B9BA6C508B488FE586A0AB2 /* torrents.h in Headers */,
A47A7C87B8B57BE50DF0D412 /* files.h in Headers */,
A47A7C87B8B57BE50DF0D412 /* torrent-files.h in Headers */,
A29DF8BE0DB2545F00D04E5A /* verify.h in Headers */,
C1FEE57B1C3223CC00D62832 /* watchdir.h in Headers */,
A2AAB6650DE0D08B00E04DDA /* blocklist.h in Headers */,
@ -2786,7 +2786,7 @@
BEFC1E2F0C07861A00B0BB3C /* session.cc in Sources */,
BEFC1E320C07861A00B0BB3C /* torrent.cc in Sources */,
2B9BA6C508B488FE586A0AB0 /* torrents.cc in Sources */,
A47A7C87B8B57BE50DF0D410 /* files.cc */,
A47A7C87B8B57BE50DF0D410 /* torrent-files.cc */,
BEFC1E360C07861A00B0BB3C /* port-forwarding.cc in Sources */,
BEFC1E3C0C07861A00B0BB3C /* platform.cc in Sources */,
BEFC1E460C07861A00B0BB3C /* net.cc in Sources */,

View File

@ -31,7 +31,6 @@ set(PROJECT_FILES
file-posix.cc
file-win32.cc
file.cc
files.cc
handshake.cc
inout.cc
log.cc
@ -58,6 +57,7 @@ set(PROJECT_FILES
subprocess-posix.cc
subprocess-win32.cc
torrent-ctor.cc
torrent-files.cc
torrent-magnet.cc
torrent-metainfo.cc
torrent.cc
@ -171,7 +171,6 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
fdlimit.h
file-info.h
file-piece-map.h
files.h
handshake.h
history.h
inout.h
@ -195,6 +194,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
session.h
stats.h
subprocess.h
torrent-files.h
torrent-magnet.h
torrent-metainfo.h
torrent.h

View File

@ -1,101 +0,0 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <optional>
#include <string>
#include <string_view>
#include "transmission.h"
#include "files.h"
bool tr_files::empty() const noexcept
{
return std::empty(files_);
}
size_t tr_files::size() const noexcept
{
return std::size(files_);
}
uint64_t tr_files::size(tr_file_index_t file_index) const
{
return files_.at(file_index).size_;
}
std::string const& tr_files::path(tr_file_index_t file_index) const
{
return files_.at(file_index).path_;
}
void tr_files::setPath(tr_file_index_t file_index, std::string_view path)
{
files_.at(file_index).setPath(path);
}
void tr_files::reserve(size_t n_files)
{
files_.reserve(n_files);
}
void tr_files::shrinkToFit()
{
files_.shrink_to_fit();
}
tr_file_index_t tr_files::add(std::string_view path, uint64_t size)
{
auto const file_index = static_cast<tr_file_index_t>(std::size(files_));
files_.emplace_back(path, size);
return file_index;
}
void tr_files::clear() noexcept
{
files_.clear();
}
std::optional<tr_files::FoundFile> tr_files::find(
tr_file_index_t file_index,
std::string_view const* search_paths,
size_t n_paths) const
{
auto filename = tr_pathbuf{};
auto file_info = tr_sys_path_info{};
auto const& subpath = path(file_index);
for (size_t path_idx = 0; path_idx < n_paths; ++path_idx)
{
auto const base = search_paths[path_idx];
filename.assign(base, '/', subpath);
if (tr_sys_path_get_info(filename, 0, &file_info))
{
return FoundFile{ file_info, std::move(filename), std::size(base) };
}
filename.assign(filename, base, '/', subpath, PartialFileSuffix);
if (tr_sys_path_get_info(filename, 0, &file_info))
{
return FoundFile{ file_info, std::move(filename), std::size(base) };
}
}
return {};
}
bool tr_files::hasAnyLocalData(std::string_view const* search_paths, size_t n_paths) const
{
for (tr_file_index_t i = 0, n = size(); i < n; ++i)
{
if (find(i, search_paths, n_paths))
{
return true;
}
}
return false;
}

View File

@ -116,7 +116,7 @@ int readOrWriteBytes(
// We didn't find the file that we want to write to.
// Let's figure out where it goes so that we can create it.
auto const base = tor->currentDir();
auto const suffix = tor->session->isIncompleteFileNamingEnabled ? tr_files::PartialFileSuffix : ""sv;
auto const suffix = tor->session->isIncompleteFileNamingEnabled ? tr_torrent_files::PartialFileSuffix : ""sv;
found = { {}, tr_pathbuf{ base, "/"sv, tor->fileSubpath(file_index), suffix }, std::size(base) };
}

View File

@ -0,0 +1,312 @@
// This file Copyright © 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <fmt/format.h>
#include "transmission.h"
#include "error.h"
#include "log.h"
#include "torrent-files.h"
#include "utils.h"
using namespace std::literals;
namespace
{
using file_func_t = std::function<void(char const* filename)>;
bool isDirectory(char const* path)
{
auto info = tr_sys_path_info{};
return tr_sys_path_get_info(path, 0, &info) && (info.type == TR_SYS_PATH_IS_DIRECTORY);
}
bool isEmptyDirectory(char const* path)
{
if (!isDirectory(path))
{
return false;
}
if (auto const odir = tr_sys_dir_open(path); odir != TR_BAD_SYS_DIR)
{
char const* name_cstr = nullptr;
while ((name_cstr = tr_sys_dir_read_name(odir)) != nullptr)
{
auto const name = std::string_view{ name_cstr };
if (name != "." && name != "..")
{
tr_sys_dir_close(odir);
return false;
}
}
tr_sys_dir_close(odir);
}
return true;
}
void depthFirstWalk(char const* path, file_func_t const& func, std::optional<int> max_depth = {})
{
if (isDirectory(path) && (!max_depth || *max_depth > 0))
{
if (auto const odir = tr_sys_dir_open(path); odir != TR_BAD_SYS_DIR)
{
char const* name_cstr = nullptr;
while ((name_cstr = tr_sys_dir_read_name(odir)) != nullptr)
{
auto const name = std::string_view{ name_cstr };
if (name == "." || name == "..")
{
continue;
}
depthFirstWalk(tr_pathbuf{ path, '/', name }.c_str(), func, max_depth ? *max_depth - 1 : max_depth);
}
tr_sys_dir_close(odir);
}
}
func(path);
}
bool isJunkFile(std::string_view filename)
{
auto const base = tr_sys_path_basename(filename);
#ifdef __APPLE__
// check for resource forks. <http://support.apple.com/kb/TA20578>
if (tr_strvStartsWith(base, "._"sv))
{
return true;
}
#endif
auto constexpr Files = std::array<std::string_view, 3>{
".DS_Store"sv,
"Thumbs.db"sv,
"desktop.ini"sv,
};
return std::find(std::begin(Files), std::end(Files), base) != std::end(Files);
}
} // unnamed namespace
///
std::optional<tr_torrent_files::FoundFile> tr_torrent_files::find(
tr_file_index_t file_index,
std::string_view const* search_paths,
size_t n_paths) const
{
auto filename = tr_pathbuf{};
auto file_info = tr_sys_path_info{};
auto const& subpath = path(file_index);
for (size_t path_idx = 0; path_idx < n_paths; ++path_idx)
{
auto const base = search_paths[path_idx];
filename.assign(base, '/', subpath);
if (tr_sys_path_get_info(filename, 0, &file_info))
{
return FoundFile{ file_info, std::move(filename), std::size(base) };
}
filename.assign(filename, base, '/', subpath, PartialFileSuffix);
if (tr_sys_path_get_info(filename, 0, &file_info))
{
return FoundFile{ file_info, std::move(filename), std::size(base) };
}
}
return {};
}
bool tr_torrent_files::hasAnyLocalData(std::string_view const* search_paths, size_t n_paths) const
{
for (tr_file_index_t i = 0, n = fileCount(); i < n; ++i)
{
if (find(i, search_paths, n_paths))
{
return true;
}
}
return false;
}
///
bool tr_torrent_files::move(
std::string_view old_parent_in,
std::string_view parent_in,
double volatile* setme_progress,
std::string_view log_name,
tr_error** error) const
{
if (setme_progress != nullptr)
{
*setme_progress = 0.0;
}
auto const old_parent = tr_pathbuf{ old_parent_in };
auto const parent = tr_pathbuf{ parent_in };
tr_logAddTrace(fmt::format(FMT_STRING("Moving files from '{:s}' to '{:s}'"), old_parent, parent), log_name);
if (tr_sys_path_is_same(old_parent, parent))
{
return true;
}
if (!tr_sys_dir_create(parent, TR_SYS_DIR_CREATE_PARENTS, 0777, error))
{
return false;
}
auto const search_paths = std::array<std::string_view, 1>{ old_parent.sv() };
auto const total_size = totalSize();
auto err = bool{};
auto bytes_moved = uint64_t{};
for (tr_file_index_t i = 0, n = fileCount(); i < n; ++i)
{
auto const found = find(i, std::data(search_paths), std::size(search_paths));
if (!found)
{
continue;
}
auto const& old_path = found->filename();
auto const path = tr_pathbuf{ parent, '/', found->subpath() };
tr_logAddTrace(fmt::format(FMT_STRING("Found file #{:d} '{:s}'"), i, old_path), log_name);
if (tr_sys_path_is_same(old_path, path))
{
continue;
}
tr_logAddTrace(fmt::format(FMT_STRING("Moving file #{:d} to '{:s}'"), i, old_path, path), log_name);
if (!tr_moveFile(old_path, path, error))
{
err = true;
break;
}
if (setme_progress != nullptr && total_size > 0U)
{
bytes_moved += fileSize(i);
*setme_progress = static_cast<double>(bytes_moved) / total_size;
}
}
// after moving the files, remove any leftover empty directories
if (!err)
{
auto const remove_empty_directories = [](char const* filename)
{
if (isEmptyDirectory(filename))
{
tr_sys_path_remove(filename, nullptr);
}
};
remove(old_parent, "transmission-removed", remove_empty_directories);
}
return !err;
}
///
/**
* This convoluted code does something (seemingly) simple:
* remove the torrent's local files.
*
* Fun complications:
* 1. Try to preserve the directory hierarchy in the recycle bin.
* 2. If there are nontorrent files, don't delete them...
* 3. ...unless the other files are "junk", such as .DS_Store
*/
void tr_torrent_files::remove(std::string_view parent_in, std::string_view tmpdir_prefix, FileFunc const& func) const
{
auto const parent = tr_pathbuf{ parent_in };
// don't try to delete local data if the directory's gone missing
if (!tr_sys_path_exists(parent))
{
return;
}
// make a tmpdir
auto tmpdir = tr_pathbuf{ parent, '/', tmpdir_prefix, "__XXXXXX"sv };
tr_sys_dir_create_temp(std::data(tmpdir));
// move the local data to the tmpdir
auto const search_paths = std::array<std::string_view, 1>{ parent.sv() };
for (tr_file_index_t idx = 0, n_files = fileCount(); idx < n_files; ++idx)
{
if (auto const found = find(idx, std::data(search_paths), std::size(search_paths)); found)
{
tr_moveFile(found->filename(), tr_pathbuf{ tmpdir, '/', found->subpath() });
}
}
// Make a list of the top-level torrent files & folders
// because we'll need it below in the 'remove junk' phase
auto top_files = std::set<std::string>{};
depthFirstWalk(
tmpdir,
[&parent, &tmpdir, &top_files](char const* filename)
{
if (tmpdir != filename)
{
top_files.emplace(tr_pathbuf{ parent, '/', tr_sys_path_basename(filename) });
}
},
1);
auto const func_wrapper = [&tmpdir, &func](char const* filename)
{
if (tmpdir != filename)
{
func(filename);
}
};
// Remove the tmpdir.
// Since `func` might send files to a recycle bin, try to preserve
// the folder hierarchy by removing top-level files & folders first.
// But that can fail -- e.g. `func` might refuse to remove nonempty
// directories -- so plan B is to remove everything bottom-up.
depthFirstWalk(tmpdir, func_wrapper, 1);
depthFirstWalk(tmpdir, func_wrapper);
tr_sys_path_remove(tmpdir);
// OK we've removed the local data.
// What's left are empty folders, junk, and user-generated files.
// Remove the first two categories and leave the third alone.
auto const remove_junk = [](char const* filename)
{
if (isEmptyDirectory(filename) || isJunkFile(filename))
{
tr_sys_path_remove(filename);
}
};
for (auto const& filename : top_files)
{
depthFirstWalk(filename.c_str(), remove_junk);
}
}

View File

@ -5,7 +5,8 @@
#pragma once
#include <cstdint>
#include <cstdint> // uint64_t
#include <functional>
#include <optional>
#include <string>
#include <string_view>
@ -16,23 +17,77 @@
#include "file.h"
#include "tr-strbuf.h"
struct tr_error;
/**
* A simple ordered collection of files.
* A simple collection of files & utils for finding them, moving them, etc.
*/
struct tr_files
struct tr_torrent_files
{
public:
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] size_t size() const noexcept;
[[nodiscard]] uint64_t size(tr_file_index_t) const;
[[nodiscard]] std::string const& path(tr_file_index_t) const;
[[nodiscard]] bool empty() const noexcept
{
return std::empty(files_);
}
void setPath(tr_file_index_t, std::string_view path);
[[nodiscard]] size_t fileCount() const noexcept
{
return std::size(files_);
}
void reserve(size_t);
void shrinkToFit();
void clear() noexcept;
tr_file_index_t add(std::string_view path, uint64_t size);
[[nodiscard]] uint64_t fileSize(tr_file_index_t file_index) const
{
return files_.at(file_index).size_;
}
[[nodiscard]] constexpr auto totalSize() const noexcept
{
return total_size_;
}
[[nodiscard]] std::string const& path(tr_file_index_t file_index) const
{
return files_.at(file_index).path_;
}
void setPath(tr_file_index_t file_index, std::string_view path)
{
files_.at(file_index).setPath(path);
}
void reserve(size_t n_files)
{
files_.reserve(n_files);
}
void shrinkToFit()
{
files_.shrink_to_fit();
}
void clear() noexcept
{
files_.clear();
total_size_ = uint64_t{};
}
tr_file_index_t add(std::string_view path, uint64_t file_size)
{
auto const ret = static_cast<tr_file_index_t>(std::size(files_));
files_.emplace_back(path, file_size);
total_size_ += file_size;
return ret;
}
bool move(
std::string_view old_top_in,
std::string_view top_in,
double volatile* setme_progress,
std::string_view log_name = "",
tr_error** error = nullptr) const;
using FileFunc = std::function<void(char const* filename)>;
void remove(std::string_view top_in, std::string_view tmpdir_prefix, FileFunc const& func) const;
struct FoundFile : public tr_sys_path_info
{
@ -92,4 +147,5 @@ private:
};
std::vector<file_t> files_;
uint64_t total_size_;
};

View File

@ -148,7 +148,7 @@ std::string tr_torrent_metainfo::fixWebseedUrl(tr_torrent_metainfo const& tm, st
{
url = tr_strvStrip(url);
if (std::size(tm.files_) > 1 && !std::empty(url) && url.back() != '/')
if (tm.fileCount() > 1U && !std::empty(url) && url.back() != '/')
{
return std::string{ url } + '/';
}

View File

@ -14,8 +14,8 @@
#include "transmission.h"
#include "block-info.h"
#include "files.h"
#include "magnet-metainfo.h"
#include "torrent-files.h"
#include "tr-strbuf.h"
struct tr_error;
@ -44,11 +44,11 @@ public:
}
[[nodiscard]] auto fileCount() const noexcept
{
return std::size(files());
return files().fileCount();
}
[[nodiscard]] auto fileSize(tr_file_index_t i) const
{
return files().size(i);
return files().fileSize(i);
}
[[nodiscard]] auto const& fileSubpath(tr_file_index_t i) const
{
@ -208,7 +208,7 @@ private:
tr_block_info block_info_ = tr_block_info{ 0, 0 };
tr_files files_;
tr_torrent_files files_;
std::vector<tr_sha1_digest_t> pieces_;

View File

@ -611,7 +611,7 @@ static size_t buildSearchPathArray(tr_torrent const* tor, std::string_view* path
return walk - paths;
}
std::optional<tr_files::FoundFile> tr_torrent::findFile(tr_file_index_t file_index) const
std::optional<tr_torrent_files::FoundFile> tr_torrent::findFile(tr_file_index_t file_index) const
{
auto paths = std::array<std::string_view, 4>{};
auto const n_paths = buildSearchPathArray(this, std::data(paths));
@ -663,7 +663,7 @@ static bool isNewTorrentASeed(tr_torrent* tor)
}
// it's not a new seed if a file is partial
if (tr_strvEndsWith(found->filename(), tr_files::PartialFileSuffix))
if (tr_strvEndsWith(found->filename(), tr_torrent_files::PartialFileSuffix))
{
return false;
}
@ -685,8 +685,6 @@ static bool isNewTorrentASeed(tr_torrent* tor)
return tor->ensurePieceIsChecked(0);
}
static void refreshCurrentDir(tr_torrent* tor);
static void torrentInit(tr_torrent* tor, tr_ctor const* ctor)
{
auto const lock = tor->unique_lock();
@ -751,7 +749,7 @@ static void torrentInit(tr_torrent* tor, tr_ctor const* ctor)
tr_ctorInitTorrentPriorities(ctor, tor);
tr_ctorInitTorrentWanted(ctor, tor);
refreshCurrentDir(tor);
tor->refreshCurrentDir();
bool const doStart = tor->isRunning;
tor->isRunning = false;
@ -904,12 +902,8 @@ void tr_torrentSetDownloadDir(tr_torrent* tor, char const* path)
if (tor->download_dir != path)
{
tor->download_dir = path;
tor->markEdited();
tor->setDirty();
tor->setDownloadDir(path);
}
refreshCurrentDir(tor);
}
char const* tr_torrentGetDownloadDir(tr_torrent const* tor)
@ -1576,7 +1570,7 @@ static void stopTorrent(tr_torrent* const tor)
{
tor->magnetVerify = false;
tr_logAddTraceTor(tor, "Magnet Verify");
refreshCurrentDir(tor);
tor->refreshCurrentDir();
tr_torrentVerify(tor);
callScriptIfEnabled(tor, TR_SCRIPT_ON_TORRENT_ADDED);
@ -1640,40 +1634,40 @@ void tr_torrentFree(tr_torrent* tor)
}
}
struct remove_data
static void removeTorrentInEventThread(tr_torrent* tor, bool delete_flag, tr_fileFunc delete_func)
{
tr_torrent* tor;
bool deleteFlag;
tr_fileFunc deleteFunc;
};
auto const lock = tor->unique_lock();
static void tr_torrentDeleteLocalData(tr_torrent* /*tor*/, tr_fileFunc /*func*/);
static void removeTorrent(struct remove_data* const data)
{
auto const lock = data->tor->unique_lock();
if (data->deleteFlag)
if (delete_flag && tor->hasMetainfo())
{
tr_torrentDeleteLocalData(data->tor, data->deleteFunc);
// ensure the files are all closed and idle before moving
tr_cacheFlushTorrent(tor->session->cache, tor);
tr_fdTorrentClose(tor->session, tor->uniqueId);
tr_verifyRemove(tor);
if (delete_func == nullptr)
{
delete_func = tr_sys_path_remove;
}
auto const delete_func_wrapper = [&delete_func](char const* filename)
{
delete_func(filename, nullptr);
};
tor->metainfo_.files().remove(tor->currentDir(), tor->name(), delete_func_wrapper);
}
tr_torrentClearCompletenessCallback(data->tor);
closeTorrent(data->tor);
tr_free(data);
tr_torrentClearCompletenessCallback(tor);
closeTorrent(tor);
}
void tr_torrentRemove(tr_torrent* tor, bool deleteFlag, tr_fileFunc deleteFunc)
void tr_torrentRemove(tr_torrent* tor, bool delete_flag, tr_fileFunc delete_func)
{
TR_ASSERT(tr_isTorrent(tor));
tor->isDeleting = true;
auto* const data = tr_new0(struct remove_data, 1);
data->tor = tor;
data->deleteFlag = deleteFlag;
data->deleteFunc = deleteFunc;
tr_runInEventThread(tor->session, removeTorrent, data);
tr_runInEventThread(tor->session, removeTorrentInEventThread, tor, delete_flag, delete_func);
}
/**
@ -2176,328 +2170,66 @@ uint64_t tr_torrentGetBytesLeftToAllocate(tr_torrent const* tor)
return bytes_left;
}
/****
***** Removing the torrent's local data
****/
///
static bool isJunkFile(std::string_view base)
{
#ifdef __APPLE__
// check for resource forks. <http://support.apple.com/kb/TA20578>
if (tr_strvStartsWith(base, "._"sv))
{
return true;
}
#endif
auto constexpr Files = std::array<std::string_view, 3>{
".DS_Store"sv,
"Thumbs.db"sv,
"desktop.ini"sv,
};
return std::find(std::begin(Files), std::end(Files), base) != std::end(Files);
}
static void removeEmptyFoldersAndJunkFiles(char const* folder)
{
auto const odir = tr_sys_dir_open(folder);
if (odir == TR_BAD_SYS_DIR)
{
return;
}
char const* name = nullptr;
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
{
if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0)
{
auto const filename = tr_strvPath(folder, name);
auto info = tr_sys_path_info{};
if (tr_sys_path_get_info(filename.c_str(), TR_SYS_PATH_NO_FOLLOW, &info) && info.type == TR_SYS_PATH_IS_DIRECTORY)
{
removeEmptyFoldersAndJunkFiles(filename.c_str());
}
else if (isJunkFile(name))
{
tr_sys_path_remove(filename.c_str());
}
}
}
tr_sys_path_remove(folder);
tr_sys_dir_close(odir);
}
/**
* This convoluted code does something (seemingly) simple:
* remove the torrent's local files.
*
* Fun complications:
* 1. Try to preserve the directory hierarchy in the recycle bin.
* 2. If there are nontorrent files, don't delete them...
* 3. ...unless the other files are "junk", such as .DS_Store
*/
static void deleteLocalData(tr_torrent const* tor, tr_fileFunc func)
{
auto files = std::vector<std::string>{};
auto folders = std::set<std::string>{};
auto const top = std::string{ tor->currentDir() };
/* don't try to delete local data if the directory's gone missing */
if (!tr_sys_path_exists(top.c_str()))
{
return;
}
/* if it's a magnet link, there's nothing to move... */
if (!tor->hasMetainfo())
{
return;
}
/***
**** Move the local data to a new tmpdir
***/
auto tmpdir = tr_strvPath(top, tr_torrentName(tor) + "__XXXXXX"s);
tr_sys_dir_create_temp(std::data(tmpdir));
for (tr_file_index_t f = 0, n = tor->fileCount(); f < n; ++f)
{
/* try to find the file, looking in the partial and download dirs */
auto filename = tr_strvPath(top, tor->fileSubpath(f));
if (!tr_sys_path_exists(filename.c_str()))
{
filename += tr_files::PartialFileSuffix;
if (!tr_sys_path_exists(filename.c_str()))
{
filename.clear();
}
}
/* if we found the file, move it */
if (!std::empty(filename))
{
auto target = tr_strvPath(tmpdir, tor->fileSubpath(f));
tr_moveFile(filename, target);
files.emplace_back(target);
}
}
/***
**** Remove tmpdir.
****
**** Try deleting the top-level files & folders to preserve
**** the directory hierarchy in the recycle bin.
**** If case that fails -- for example, rmdir () doesn't
**** delete nonempty folders -- go from the bottom up too.
***/
/* try deleting the local data's top-level files & folders */
if (auto const odir = tr_sys_dir_open(tmpdir.c_str()); odir != TR_BAD_SYS_DIR)
{
char const* name = nullptr;
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
{
if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0)
{
auto const file = tr_strvPath(tmpdir, name);
(*func)(file.c_str(), nullptr);
}
}
tr_sys_dir_close(odir);
}
/* go from the bottom up */
for (auto const& file : files)
{
auto walk = file;
while (tr_sys_path_exists(walk.c_str()) && !tr_sys_path_is_same(tmpdir.c_str(), walk.c_str()))
{
(*func)(walk.c_str(), nullptr);
walk = tr_sys_path_dirname(walk);
}
}
/***
**** The local data has been removed.
**** What's left in top are empty folders, junk, and user-generated files.
**** Remove the first two categories and leave the third.
***/
/* build a list of 'top's child directories that belong to this torrent */
for (tr_file_index_t f = 0, n = tor->fileCount(); f < n; ++f)
{
/* get the directory that this file goes in... */
auto const filename = tr_strvPath(top, tor->fileSubpath(f));
auto dir = tr_sys_path_dirname(filename);
if (std::empty(dir))
{
continue;
}
/* walk up the directory tree until we reach 'top' */
if (!tr_sys_path_is_same(top.c_str(), dir.c_str()) && dir == top)
{
for (;;)
{
auto const parent = tr_sys_path_dirname(dir);
if (tr_sys_path_is_same(top.c_str(), parent.c_str()) || parent == top)
{
folders.emplace(dir);
break;
}
/* walk upwards to parent */
dir = parent;
}
}
}
for (auto const& folder : folders)
{
removeEmptyFoldersAndJunkFiles(folder.c_str());
}
/* cleanup */
tr_sys_path_remove(tmpdir.c_str());
}
static void tr_torrentDeleteLocalData(tr_torrent* tor, tr_fileFunc func)
static void setLocationInEventThread(
tr_torrent* tor,
std::string const& path,
bool move_from_old_path,
double volatile* setme_progress,
int volatile* setme_state)
{
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(tr_amInEventThread(tor->session));
if (func == nullptr)
auto ok = bool{ true };
if (move_from_old_path)
{
func = tr_sys_path_remove;
}
if (setme_state != nullptr)
{
*setme_state = TR_LOC_MOVING;
}
/* close all the files because we're about to delete them */
tr_cacheFlushTorrent(tor->session->cache, tor);
tr_fdTorrentClose(tor->session, tor->uniqueId);
deleteLocalData(tor, func);
}
/***
****
***/
struct LocationData
{
std::string location;
tr_torrent* tor = nullptr;
double volatile* setme_progress = nullptr;
int volatile* setme_state = nullptr;
bool move_from_old_location = false;
};
static void setLocationImpl(struct LocationData* const data)
{
auto* const tor = data->tor;
TR_ASSERT(tr_isTorrent(tor));
auto const lock = tor->unique_lock();
bool err = false;
bool const do_move = data->move_from_old_location;
auto const& location = data->location;
double bytesHandled = 0;
tr_logAddTraceTor(
tor,
fmt::format("Moving '{}' location from currentDir '{}' to '{}'", tor->name(), tor->currentDir(), location));
tr_sys_dir_create(location.c_str(), TR_SYS_DIR_CREATE_PARENTS, 0777);
if (!tr_sys_path_is_same(location.c_str(), tor->currentDir().c_str()))
{
/* bad idea to move files while they're being verified... */
// ensure the files are all closed and idle before moving
tr_cacheFlushTorrent(tor->session->cache, tor);
tr_fdTorrentClose(tor->session, tor->uniqueId);
tr_verifyRemove(tor);
/* try to move the files.
* FIXME: there are still all kinds of nasty cases, like what
* if the target directory runs out of space halfway through... */
for (tr_file_index_t i = 0, n = tor->fileCount(); !err && i < n; ++i)
tr_error* error = nullptr;
ok = tor->metainfo_.files().move(tor->currentDir(), path, setme_progress, tor->name(), &error);
if (error != nullptr)
{
auto const file_size = tor->fileSize(i);
if (auto found = tor->findFile(i); found)
{
auto const& oldpath = found->filename();
auto const newpath = tr_pathbuf{ location, '/', found->subpath() };
tr_logAddTraceTor(tor, fmt::format("Found file #{}: '{}'", i, oldpath));
if (do_move && !tr_sys_path_is_same(oldpath, newpath))
{
tr_error* error = nullptr;
tr_logAddTraceTor(tor, fmt::format("moving '{}' to '{}'", oldpath, newpath));
if (!tr_moveFile(oldpath, newpath, &error))
{
err = true;
tr_logAddErrorTor(
tor,
fmt::format(
_("Couldn't move '{old_path}' to '{path}': {error} ({error_code})"),
fmt::arg("old_path", oldpath),
fmt::arg("path", newpath),
fmt::arg("error", error->message),
fmt::arg("error_code", error->code)));
tr_error_free(error);
}
}
}
if (data->setme_progress != nullptr)
{
bytesHandled += file_size;
*data->setme_progress = bytesHandled / tor->totalSize();
}
}
if (!err && do_move)
{
/* blow away the leftover subdirectories in the old location */
tr_torrentDeleteLocalData(tor, tr_sys_path_remove);
tr_logAddError(fmt::format(
_("Couldn't move '{old_path}' to '{path}': {error} ({error_code})"),
fmt::arg("old_path", tor->currentDir()),
fmt::arg("path", path),
fmt::arg("error", error->message),
fmt::arg("error_code", error->code)));
tr_error_clear(&error);
}
}
if (!err)
// tell the torrent where the files are
if (ok)
{
/* set the new location and reverify */
tr_torrentSetDownloadDir(tor, location.c_str());
tor->setDownloadDir(path);
if (do_move)
if (move_from_old_path)
{
tor->incomplete_dir.clear();
tor->current_dir = tor->downloadDir();
}
}
if (data->setme_state != nullptr)
if (setme_state != nullptr)
{
*data->setme_state = err ? TR_LOC_ERROR : TR_LOC_DONE;
*setme_state = ok ? TR_LOC_DONE : TR_LOC_ERROR;
}
/* cleanup */
delete data;
}
void tr_torrent::setLocation(
std::string_view location,
bool move_from_old_location,
std::string_view path,
bool move_from_old_path,
double volatile* setme_progress,
int volatile* setme_state)
{
@ -2506,34 +2238,31 @@ void tr_torrent::setLocation(
*setme_state = TR_LOC_MOVING;
}
if (setme_progress != nullptr)
{
*setme_progress = 0;
}
/* run this in the libtransmission thread */
auto* const data = new LocationData{};
data->tor = this;
data->location = location;
data->move_from_old_location = move_from_old_location;
data->setme_state = setme_state;
data->setme_progress = setme_progress;
tr_runInEventThread(this->session, setLocationImpl, data);
tr_runInEventThread(
this->session,
setLocationInEventThread,
this,
std::string{ path },
move_from_old_path,
setme_progress,
setme_state);
}
void tr_torrentSetLocation(
tr_torrent* tor,
char const* location,
bool move_from_old_location,
char const* path,
bool move_from_old_path,
double volatile* setme_progress,
int volatile* setme_state)
{
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(!tr_str_is_empty(location));
TR_ASSERT(!tr_str_is_empty(path));
return tor->setLocation(location != nullptr ? location : "", move_from_old_location, setme_progress, setme_state);
tor->setLocation(path, move_from_old_path, setme_progress, setme_state);
}
///
std::string_view tr_torrent::primaryMimeType() const
{
// count up how many bytes there are for each mime-type in the torrent
@ -2667,29 +2396,29 @@ char* tr_torrentFindFile(tr_torrent const* tor, tr_file_index_t fileNum)
return found ? tr_strdup(found->filename()) : nullptr;
}
/* Decide whether we should be looking for files in downloadDir or incompleteDir. */
static void refreshCurrentDir(tr_torrent* tor)
// decide whether we should be looking for files in downloadDir or incompleteDir
void tr_torrent::refreshCurrentDir()
{
auto dir = tr_interned_string{};
if (std::empty(tor->incompleteDir()))
if (std::empty(incompleteDir()))
{
dir = tor->downloadDir();
dir = downloadDir();
}
else if (!tor->hasMetainfo()) /* no files to find */
else if (!hasMetainfo()) // no files to find
{
dir = tor->incompleteDir();
dir = incompleteDir();
}
else
{
auto const found = tor->findFile(0);
dir = found ? tr_interned_string{ found->base() } : tor->incompleteDir();
auto const found = findFile(0);
dir = found ? tr_interned_string{ found->base() } : incompleteDir();
}
TR_ASSERT(!std::empty(dir));
TR_ASSERT(dir == tor->downloadDir() || dir == tor->incompleteDir());
TR_ASSERT(dir == downloadDir() || dir == incompleteDir());
tor->current_dir = dir;
current_dir = dir;
}
/***
@ -2881,14 +2610,14 @@ static int renamePath(tr_torrent* tor, char const* oldpath, char const* newname)
if (!tr_sys_path_exists(src.c_str())) /* check for it as a partial */
{
src += tr_files::PartialFileSuffix;
src += tr_torrent_files::PartialFileSuffix;
}
if (tr_sys_path_exists(src.c_str()))
{
auto const parent = tr_sys_path_dirname(src);
auto const tgt = tr_strvEndsWith(src, tr_files::PartialFileSuffix) ?
tr_pathbuf{ parent, '/', newname, tr_files::PartialFileSuffix } :
auto const tgt = tr_strvEndsWith(src, tr_torrent_files::PartialFileSuffix) ?
tr_pathbuf{ parent, '/', newname, tr_torrent_files::PartialFileSuffix } :
tr_pathbuf{ parent, '/', newname };
auto tmp = errno;

View File

@ -368,7 +368,7 @@ public:
metainfo_.setFileSubpath(i, subpath);
}
[[nodiscard]] std::optional<tr_files::FoundFile> findFile(tr_file_index_t file_index) const;
[[nodiscard]] std::optional<tr_torrent_files::FoundFile> findFile(tr_file_index_t file_index) const;
[[nodiscard]] bool hasAnyLocalData() const;
@ -559,6 +559,16 @@ public:
this->error_string = errmsg;
}
void setDownloadDir(std::string_view path)
{
download_dir = path;
markEdited();
setDirty();
refreshCurrentDir();
}
void refreshCurrentDir();
void setVerifyState(tr_verify_state state);
void setDateActive(time_t t);

View File

@ -306,7 +306,7 @@ template<typename... T, typename std::enable_if_t<(std::is_convertible_v<T, std:
}
template<typename T>
[[nodiscard]] constexpr bool tr_strvContains(std::string_view sv, T key) // c++23
[[nodiscard]] constexpr bool tr_strvContains(std::string_view sv, T key) noexcept // c++23
{
return sv.find(key) != sv.npos;
}

View File

@ -24,6 +24,7 @@ add_executable(libtransmission-test
peer-mgr-wishlist-test.cc
peer-msgs-test.cc
quark-test.cc
remove-test.cc
rename-test.cc
rpc-test.cc
session-test.cc

View File

@ -5,7 +5,7 @@
#include "transmission.h"
#include "files.h"
#include "torrent-files.h"
#include "test-fixtures.h"
@ -20,14 +20,14 @@ TEST_F(FilesTest, add)
auto constexpr Path = "/hello/world"sv;
auto constexpr Size = size_t{ 1024 };
auto files = tr_files{};
EXPECT_EQ(size_t{ 0U }, std::size(files));
auto files = tr_torrent_files{};
EXPECT_EQ(size_t{ 0U }, files.fileCount());
EXPECT_TRUE(std::empty(files));
auto const file_index = files.add(Path, Size);
EXPECT_EQ(tr_file_index_t{ 0U }, file_index);
EXPECT_EQ(size_t{ 1U }, std::size(files));
EXPECT_EQ(Size, files.size(file_index));
EXPECT_EQ(size_t{ 1U }, files.fileCount());
EXPECT_EQ(Size, files.fileSize(file_index));
EXPECT_EQ(Path, files.path(file_index));
EXPECT_FALSE(std::empty(files));
}
@ -38,14 +38,14 @@ TEST_F(FilesTest, setPath)
auto constexpr Path2 = "/hello/there"sv;
auto constexpr Size = size_t{ 2048 };
auto files = tr_files{};
auto files = tr_torrent_files{};
auto const file_index = files.add(Path1, Size);
EXPECT_EQ(Path1, files.path(file_index));
EXPECT_EQ(Size, files.size(file_index));
EXPECT_EQ(Size, files.fileSize(file_index));
files.setPath(file_index, Path2);
EXPECT_EQ(Path2, files.path(file_index));
EXPECT_EQ(Size, files.size(file_index));
EXPECT_EQ(Size, files.fileSize(file_index));
}
TEST_F(FilesTest, clear)
@ -54,15 +54,15 @@ TEST_F(FilesTest, clear)
auto constexpr Path2 = "/hello/there"sv;
auto constexpr Size = size_t{ 2048 };
auto files = tr_files{};
auto files = tr_torrent_files{};
files.add(Path1, Size);
EXPECT_EQ(size_t{ 1U }, std::size(files));
EXPECT_EQ(size_t{ 1U }, files.fileCount());
files.add(Path2, Size);
EXPECT_EQ(size_t{ 2U }, std::size(files));
EXPECT_EQ(size_t{ 2U }, files.fileCount());
files.clear();
EXPECT_TRUE(std::empty(files));
EXPECT_EQ(size_t{ 0U }, std::size(files));
EXPECT_EQ(size_t{ 0U }, files.fileCount());
}
TEST_F(FilesTest, find)
@ -71,7 +71,7 @@ TEST_F(FilesTest, find)
auto const filename = tr_pathbuf{ sandboxDir(), "/first_dir/hello.txt"sv };
createFileWithContents(std::string{ filename }, std::data(Contents), std::size(Contents));
auto files = tr_files{};
auto files = tr_torrent_files{};
auto const file_index = files.add("first_dir/hello.txt", 1024);
auto const search_path_1 = tr_pathbuf{ sandboxDir() };
@ -89,7 +89,7 @@ TEST_F(FilesTest, find)
EXPECT_EQ(filename, found->filename());
// now make it an incomplete file
auto const partial_filename = tr_pathbuf{ filename, tr_files::PartialFileSuffix };
auto const partial_filename = tr_pathbuf{ filename, tr_torrent_files::PartialFileSuffix };
EXPECT_TRUE(tr_sys_path_rename(filename, partial_filename));
search_path = std::vector<std::string_view>{ search_path_1.sv(), search_path_2.sv() };
found = files.find(file_index, std::data(search_path), std::size(search_path));
@ -113,7 +113,7 @@ TEST_F(FilesTest, hasAnyLocalData)
auto const filename = tr_pathbuf{ sandboxDir(), "/first_dir/hello.txt"sv };
createFileWithContents(std::string{ filename }, std::data(Contents), std::size(Contents));
auto files = tr_files{};
auto files = tr_torrent_files{};
files.add("first_dir/hello.txt", 1024);
auto const search_path_1 = tr_pathbuf{ sandboxDir() };

View File

@ -58,7 +58,7 @@ TEST_P(IncompleteDirTest, incompleteDir)
auto* const tor = zeroTorrentInit(ZeroTorrentState::Partial);
auto path = tr_pathbuf{};
path.assign(incomplete_dir, '/', tr_torrentFile(tor, 0).name, ".part"sv);
path.assign(incomplete_dir, '/', tr_torrentFile(tor, 0).name, tr_torrent_files::PartialFileSuffix);
EXPECT_EQ(path, makeString(tr_torrentFindFile(tor, 0)));
path.assign(incomplete_dir, '/', tr_torrentFile(tor, 1).name);
EXPECT_EQ(path, makeString(tr_torrentFindFile(tor, 1)));

View File

@ -0,0 +1,341 @@
// This file Copyright (C) 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <array>
#include <cstdio>
#include <set>
#include <string_view>
#include <utility>
#include "transmission.h"
#include "file.h"
#include "torrent-files.h"
#include "tr-strbuf.h"
#include "test-fixtures.h"
using namespace std::literals;
using SubpathAndSize = std::pair<std::string_view, uint64_t>;
class RemoveTest : public libtransmission::test::SandboxedTest
{
protected:
static constexpr std::string_view Content = "Hello, World!"sv;
static constexpr std::string_view JunkBasename = ".DS_Store"sv;
static constexpr std::string_view NonJunkBasename = "passwords.txt"sv;
static void sysPathRemove(char const* filename)
{
tr_sys_path_remove(filename, nullptr);
}
static auto aliceFiles()
{
static constexpr std::array<SubpathAndSize, 106> AliceFiles = { {
{ "alice_in_wonderland_librivox/AliceInWonderland_librivox.m4b", 87525736ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland.jpg", 81464ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland.pdf", 185367ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_abbyy.gz", 24582ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_chocr.html.gz", 22527ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_djvu.txt", 2039ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_djvu.xml", 28144ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_hocr.html", 56942ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_hocr_pageindex.json.gz", 40ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_hocr_searchtext.txt.gz", 943ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_jp2.zip", 1499986ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_page_numbers.json", 136ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_scandata.xml", 538ULL },
{ "alice_in_wonderland_librivox/Alice_in_Wonderland_thumb.jpg", 26987ULL },
{ "alice_in_wonderland_librivox/__ia_thumb.jpg", 16557ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox.json", 13740ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox.storj-store.trigger", 0ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox_128kb.m3u", 984ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox_64kb.m3u", 1044ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox_meta.sqlite", 20480ULL },
{ "alice_in_wonderland_librivox/alice_in_wonderland_librivox_meta.xml", 2805ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01.mp3", 10249859ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01.ogg", 7509828ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01.png", 10779ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01_64kb.mp3", 5124992ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01_esshigh.json.gz", 1977ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01_esslow.json.gz", 29258ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_01_spectrogram.png", 234022ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02.mp3", 11772312ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02.ogg", 5148365ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02.png", 10962ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02_64kb.mp3", 5886455ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02_esshigh.json.gz", 1980ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02_esslow.json.gz", 30287ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_02_spectrogram.png", 326161ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03.mp3", 17024560ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03.ogg", 12046177ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03.png", 8725ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03_64kb.mp3", 8512448ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03_esshigh.json.gz", 1966ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03_esslow.json.gz", 34110ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_03_spectrogram.png", 218264ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04.mp3", 19087768ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04.ogg", 9880920ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04.png", 6055ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04_64kb.mp3", 9544016ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04_esshigh.json.gz", 1967ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04_esslow.json.gz", 36154ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_04_spectrogram.png", 282145ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05.mp3", 12946949ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05.ogg", 7470734ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05.png", 12061ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05_64kb.mp3", 6473687ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05_esshigh.json.gz", 1963ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05_esslow.json.gz", 31048ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_05_spectrogram.png", 304150ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06.mp3", 12413304ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06.ogg", 7154040ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06.png", 11383ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06_64kb.mp3", 6206820ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06_esshigh.json.gz", 1942ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06_esslow.json.gz", 30295ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_06_spectrogram.png", 288601ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07.mp3", 16742808ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07.ogg", 10513847ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07.png", 8180ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07_64kb.mp3", 8371136ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07_esshigh.json.gz", 1963ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07_esslow.json.gz", 33992ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_07_spectrogram.png", 233725ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08.mp3", 12784781ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08.ogg", 9306961ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08.png", 11470ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08_64kb.mp3", 6392576ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08_esshigh.json.gz", 1973ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08_esslow.json.gz", 31964ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_08_spectrogram.png", 233626ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09.mp3", 14528920ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09.ogg", 8062952ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09.png", 9439ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09_64kb.mp3", 7264466ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09_esshigh.json.gz", 1946ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09_esslow.json.gz", 32295ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_09_spectrogram.png", 282898ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10.mp3", 21894203ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10.ogg", 15226220ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10.png", 8796ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10_64kb.mp3", 10947200ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10_esshigh.json.gz", 1970ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10_esslow.json.gz", 37062ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_10_spectrogram.png", 221277ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11.mp3", 9919894ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11.ogg", 7676067ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11.png", 7140ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11_64kb.mp3", 4959296ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11_esshigh.json.gz", 1959ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11_esslow.json.gz", 28694ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_11_spectrogram.png", 229471ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12.mp3", 12359368ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12.ogg", 8065179ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12.png", 11074ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12_64kb.mp3", 6179840ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12_esshigh.json.gz", 1981ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12_esslow.json.gz", 30975ULL },
{ "alice_in_wonderland_librivox/wonderland_ch_12_spectrogram.png", 235568ULL },
{ "alice_in_wonderland_librivox/history/files/alice_in_wonderland_librivox.storj-store.trigger.~1~", 0ULL },
} };
auto files = tr_torrent_files{};
for (auto const& file : AliceFiles)
{
auto const& [filename, size] = file;
files.add(filename, size);
}
return files;
}
static auto ubuntuFiles()
{
static auto constexpr Files = std::array<SubpathAndSize, 1>{ { { "ubuntu-20.04.4-desktop-amd64.iso"sv,
3379068928ULL } } };
auto files = tr_torrent_files{};
for (auto const& file : Files)
{
auto const& [filename, size] = file;
files.add(filename, size);
}
return files;
}
auto createFiles(tr_torrent_files const& files, char const* parent)
{
auto paths = std::set<std::string>{};
for (tr_file_index_t i = 0, n = files.fileCount(); i < n; ++i)
{
auto filename = tr_pathbuf{ parent, '/', files.path(i) };
createFileWithContents(filename, std::data(Content), std::size(Content));
paths.emplace(filename);
while (!tr_sys_path_is_same(parent, filename))
{
filename = tr_sys_path_dirname(filename);
paths.emplace(filename);
}
}
return paths;
}
static auto getSubtreeContents(std::string_view parent_dir)
{
auto filenames = std::set<std::string>{};
auto file_func = [&filenames](char const* filename)
{
filenames.emplace(filename);
};
libtransmission::test::depthFirstWalk(tr_pathbuf{ parent_dir }, file_func);
return filenames;
}
};
TEST_F(RemoveTest, RemovesSingleFile)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = ubuntuFiles();
expected_tree = createFiles(files, parent.c_str());
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
files.remove(parent, "tmpdir_prefix"sv, sysPathRemove);
expected_tree = { parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}
TEST_F(RemoveTest, RemovesSubtree)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = aliceFiles();
expected_tree = createFiles(files, parent.c_str());
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
files.remove(parent, "tmpdir_prefix"sv, sysPathRemove);
expected_tree = { parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}
TEST_F(RemoveTest, RemovesLeftoverJunk)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = aliceFiles();
expected_tree = createFiles(files, parent.c_str());
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
// add a junk file *inside of* the torrent's top directory.
auto const junk_file = tr_pathbuf{ parent, "/alice_in_wonderland_librivox/", JunkBasename };
createFileWithContents(junk_file, std::data(Content), std::size(Content));
expected_tree.emplace(junk_file);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
files.remove(parent, "tmpdir_prefix"sv, sysPathRemove);
expected_tree = { parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}
TEST_F(RemoveTest, LeavesSiblingsAlone)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = aliceFiles();
expected_tree = createFiles(files, parent.c_str());
EXPECT_GT(std::size(expected_tree), 100U);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
// add a junk file *as a sibling of* the torrent's top directory.
auto const junk_file = tr_pathbuf{ parent, '/', JunkBasename };
createFileWithContents(junk_file, std::data(Content), std::size(Content));
expected_tree.emplace(junk_file);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
// add a non-junk file *as a sibling of* the torrent's top directory.
auto const non_junk_file = tr_pathbuf{ parent, '/', NonJunkBasename };
createFileWithContents(non_junk_file, std::data(Content), std::size(Content));
expected_tree.emplace(non_junk_file);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
files.remove(parent, "tmpdir_prefix"sv, sysPathRemove);
expected_tree = { parent, junk_file.c_str(), non_junk_file.c_str() };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}
TEST_F(RemoveTest, LeavesNonJunkAlone)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = aliceFiles();
expected_tree = createFiles(files, parent.c_str());
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
// add a non-junk file.
auto const nonjunk_file = tr_pathbuf{ parent, "/alice_in_wonderland_librivox/", NonJunkBasename };
createFileWithContents(nonjunk_file, std::data(Content), std::size(Content));
expected_tree.emplace(nonjunk_file);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
files.remove(parent, "tmpdir_prefix"sv, sysPathRemove);
expected_tree = { parent, tr_sys_path_dirname(nonjunk_file), nonjunk_file.c_str() };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}
TEST_F(RemoveTest, PreservesDirectoryHierarchyIfPossible)
{
auto const parent = sandboxDir();
auto expected_tree = std::set<std::string>{ parent };
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
// add a recycle bin
auto const recycle_bin = tr_pathbuf{ parent, "/Trash"sv };
tr_sys_dir_create(recycle_bin, TR_SYS_DIR_CREATE_PARENTS, 0777);
expected_tree.emplace(recycle_bin);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const files = aliceFiles();
expected_tree = createFiles(files, parent.c_str());
expected_tree.emplace(recycle_bin);
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
auto const recycle_func = [&recycle_bin](char const* filename)
{
tr_sys_path_rename(filename, tr_pathbuf{ recycle_bin, '/', tr_sys_path_basename(filename) });
};
files.remove(parent, "tmpdir_prefix"sv, recycle_func);
// after remove, the subtree should be:
expected_tree = { parent, recycle_bin.c_str() };
for (tr_file_index_t i = 0, n = files.fileCount(); i < n; ++i)
{
expected_tree.emplace(tr_pathbuf{ recycle_bin, '/', files.path(i) });
}
expected_tree.emplace(tr_pathbuf{ recycle_bin, "/alice_in_wonderland_librivox"sv });
expected_tree.emplace(tr_pathbuf{ recycle_bin, "/alice_in_wonderland_librivox/history"sv });
expected_tree.emplace(tr_pathbuf{ recycle_bin, "/alice_in_wonderland_librivox/history/files"sv });
EXPECT_EQ(expected_tree, getSubtreeContents(parent));
}

View File

@ -39,6 +39,32 @@ namespace libtransmission
namespace test
{
using file_func_t = std::function<void(char const* filename)>;
static void depthFirstWalk(char const* path, file_func_t func)
{
auto info = tr_sys_path_info{};
if (tr_sys_path_get_info(path, 0, &info) && (info.type == TR_SYS_PATH_IS_DIRECTORY))
{
if (auto const odir = tr_sys_dir_open(path); odir != TR_BAD_SYS_DIR)
{
char const* name;
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
{
if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0)
{
auto const filename = tr_strvPath(path, name);
depthFirstWalk(tr_strvPath(path, name).c_str(), func);
}
}
tr_sys_dir_close(odir);
}
}
func(path);
}
inline std::string makeString(char*&& s)
{
auto const ret = std::string(s != nullptr ? s : "");
@ -116,32 +142,6 @@ protected:
return path;
}
using file_func_t = std::function<void(char const* filename)>;
static void depthFirstWalk(char const* path, file_func_t func)
{
auto info = tr_sys_path_info{};
if (tr_sys_path_get_info(path, 0, &info) && (info.type == TR_SYS_PATH_IS_DIRECTORY))
{
if (auto const odir = tr_sys_dir_open(path); odir != TR_BAD_SYS_DIR)
{
char const* name;
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
{
if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0)
{
auto const filename = tr_strvPath(path, name);
depthFirstWalk(tr_strvPath(path, name).c_str(), func);
}
}
tr_sys_dir_close(odir);
}
}
func(path);
}
static void rimraf(std::string const& path, bool verbose = false)
{
auto remove = [verbose](char const* filename)
@ -227,14 +227,14 @@ protected:
errno = tmperr;
}
void createFileWithContents(std::string const& path, void const* payload, size_t n) const
void createFileWithContents(std::string_view path, void const* payload, size_t n) const
{
auto const tmperr = errno;
buildParentDir(path);
auto const fd = tr_sys_file_open(
path.c_str(),
tr_pathbuf{ path },
TR_SYS_FILE_WRITE | TR_SYS_FILE_CREATE | TR_SYS_FILE_TRUNCATE,
0600,
nullptr);

View File

@ -541,7 +541,6 @@ TEST_F(VariantTest, dictFindType)
TEST_F(VariantTest, variantFromBufFuzz)
{
auto buf = std::vector<char>{};
auto top = tr_variant{};
for (size_t i = 0; i < 100000; ++i)
{
@ -550,7 +549,16 @@ TEST_F(VariantTest, variantFromBufFuzz)
auto const sv = std::string_view{ std::data(buf), std::size(buf) };
// std::cerr << '[' << tr_base64_encode({ std::data(buf), std::size(buf) }) << ']' << std::endl;
tr_variantFromBuf(&top, TR_VARIANT_PARSE_JSON | TR_VARIANT_PARSE_INPLACE, sv, nullptr, nullptr);
tr_variantFromBuf(&top, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, sv, nullptr, nullptr);
if (auto top = tr_variant{};
tr_variantFromBuf(&top, TR_VARIANT_PARSE_JSON | TR_VARIANT_PARSE_INPLACE, sv, nullptr, nullptr))
{
tr_variantFree(&top);
}
if (auto top = tr_variant{};
tr_variantFromBuf(&top, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, sv, nullptr, nullptr))
{
tr_variantFree(&top);
}
}
}