From 2866638e1b3672c897692b7fb10c8e17354ee05c Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Fri, 15 Apr 2022 18:39:04 -0500 Subject: [PATCH] refactor: add tr_torrent_files::move() and remove() (#2919) --- Transmission.xcodeproj/project.pbxproj | 16 +- libtransmission/CMakeLists.txt | 4 +- libtransmission/files.cc | 101 ----- libtransmission/inout.cc | 2 +- libtransmission/torrent-files.cc | 312 +++++++++++++ libtransmission/{files.h => torrent-files.h} | 80 +++- libtransmission/torrent-metainfo.cc | 2 +- libtransmission/torrent-metainfo.h | 8 +- libtransmission/torrent.cc | 449 ++++--------------- libtransmission/torrent.h | 12 +- libtransmission/utils.h | 2 +- tests/libtransmission/CMakeLists.txt | 1 + tests/libtransmission/files-test.cc | 30 +- tests/libtransmission/move-test.cc | 2 +- tests/libtransmission/remove-test.cc | 341 ++++++++++++++ tests/libtransmission/test-fixtures.h | 56 +-- tests/libtransmission/variant-test.cc | 14 +- 17 files changed, 894 insertions(+), 538 deletions(-) delete mode 100644 libtransmission/files.cc create mode 100644 libtransmission/torrent-files.cc rename libtransmission/{files.h => torrent-files.h} (53%) create mode 100644 tests/libtransmission/remove-test.cc diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 3e6400ea2..4b9df2247 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -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 = ""; }; 2B9BA6C508B488FE586A0AB3 /* torrents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = torrents.h; sourceTree = ""; }; - A47A7C87B8B57BE50DF0D411 /* files.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = files.cc; sourceTree = ""; }; - A47A7C87B8B57BE50DF0D413 /* files.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = files.h; sourceTree = ""; }; + A47A7C87B8B57BE50DF0D411 /* torrent-files.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = torrent-files.cc; sourceTree = ""; }; + A47A7C87B8B57BE50DF0D413 /* torrent-files.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = torrent-files.h; sourceTree = ""; }; 32CA4F630368D1EE00C91783 /* Transmission_Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Transmission_Prefix.pch; sourceTree = ""; }; 35F373000C2DA88F00DAA8F2 /* FilePriorityCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FilePriorityCell.h; sourceTree = ""; }; 35F373010C2DA88F00DAA8F2 /* FilePriorityCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FilePriorityCell.mm; sourceTree = ""; }; @@ -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 */, diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 29fefd7ff..561ca2dc8 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -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 diff --git a/libtransmission/files.cc b/libtransmission/files.cc deleted file mode 100644 index 7dd15cbf1..000000000 --- a/libtransmission/files.cc +++ /dev/null @@ -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 -#include -#include - -#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(std::size(files_)); - files_.emplace_back(path, size); - return file_index; -} - -void tr_files::clear() noexcept -{ - files_.clear(); -} - -std::optional 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; -} diff --git a/libtransmission/inout.cc b/libtransmission/inout.cc index 4e001313e..2b7d2cdde 100644 --- a/libtransmission/inout.cc +++ b/libtransmission/inout.cc @@ -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) }; } diff --git a/libtransmission/torrent-files.cc b/libtransmission/torrent-files.cc new file mode 100644 index 000000000..33b7c3ea6 --- /dev/null +++ b/libtransmission/torrent-files.cc @@ -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 +#include +#include +#include + +#include + +#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; + +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 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. + if (tr_strvStartsWith(base, "._"sv)) + { + return true; + } +#endif + + auto constexpr Files = std::array{ + ".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::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{ 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(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{ 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{}; + 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); + } +} diff --git a/libtransmission/files.h b/libtransmission/torrent-files.h similarity index 53% rename from libtransmission/files.h rename to libtransmission/torrent-files.h index 51bd6f3d9..9fe662240 100644 --- a/libtransmission/files.h +++ b/libtransmission/torrent-files.h @@ -5,7 +5,8 @@ #pragma once -#include +#include // uint64_t +#include #include #include #include @@ -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(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 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 files_; + uint64_t total_size_; }; diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc index f46d911de..64a364402 100644 --- a/libtransmission/torrent-metainfo.cc +++ b/libtransmission/torrent-metainfo.cc @@ -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 } + '/'; } diff --git a/libtransmission/torrent-metainfo.h b/libtransmission/torrent-metainfo.h index 6030a4460..698168e79 100644 --- a/libtransmission/torrent-metainfo.h +++ b/libtransmission/torrent-metainfo.h @@ -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 pieces_; diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index e90493d0c..77d463ddd 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -611,7 +611,7 @@ static size_t buildSearchPathArray(tr_torrent const* tor, std::string_view* path return walk - paths; } -std::optional tr_torrent::findFile(tr_file_index_t file_index) const +std::optional tr_torrent::findFile(tr_file_index_t file_index) const { auto paths = std::array{}; 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. - if (tr_strvStartsWith(base, "._"sv)) - { - return true; - } -#endif - - auto constexpr Files = std::array{ - ".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{}; - auto folders = std::set{}; - 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; diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index c2d0d4cad..03046d380 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -368,7 +368,7 @@ public: metainfo_.setFileSubpath(i, subpath); } - [[nodiscard]] std::optional findFile(tr_file_index_t file_index) const; + [[nodiscard]] std::optional 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); diff --git a/libtransmission/utils.h b/libtransmission/utils.h index 43655281f..2e42abb3a 100644 --- a/libtransmission/utils.h +++ b/libtransmission/utils.h @@ -306,7 +306,7 @@ template -[[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; } diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index 9f7365258..f7872c9fa 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -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 diff --git a/tests/libtransmission/files-test.cc b/tests/libtransmission/files-test.cc index a692ac2e0..85e5e7951 100644 --- a/tests/libtransmission/files-test.cc +++ b/tests/libtransmission/files-test.cc @@ -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{ 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() }; diff --git a/tests/libtransmission/move-test.cc b/tests/libtransmission/move-test.cc index ff8ffe162..20d2b8493 100644 --- a/tests/libtransmission/move-test.cc +++ b/tests/libtransmission/move-test.cc @@ -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))); diff --git a/tests/libtransmission/remove-test.cc b/tests/libtransmission/remove-test.cc new file mode 100644 index 000000000..571e9d1a5 --- /dev/null +++ b/tests/libtransmission/remove-test.cc @@ -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 +#include +#include +#include +#include + +#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; + +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 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{ { { "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{}; + + 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{}; + + 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{ 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{ 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{ 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{ 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{ 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{ 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)); +} diff --git a/tests/libtransmission/test-fixtures.h b/tests/libtransmission/test-fixtures.h index 733cd5aca..e6775db81 100644 --- a/tests/libtransmission/test-fixtures.h +++ b/tests/libtransmission/test-fixtures.h @@ -39,6 +39,32 @@ namespace libtransmission namespace test { +using file_func_t = std::function; + +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; - - 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); diff --git a/tests/libtransmission/variant-test.cc b/tests/libtransmission/variant-test.cc index ade1678ae..c8151d9af 100644 --- a/tests/libtransmission/variant-test.cc +++ b/tests/libtransmission/variant-test.cc @@ -541,7 +541,6 @@ TEST_F(VariantTest, dictFindType) TEST_F(VariantTest, variantFromBufFuzz) { auto buf = std::vector{}; - 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); + } } }