Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
862ad76
A local no-replace rename falls back where the filesystem refuses it
5cript Oct 14, 2026
26ddd8e
A remote no-replace rename falls back when the server refuses hard links
5cript Oct 14, 2026
dd91bce
Transfers and renames move into place through one dispatcher per side
5cript Oct 14, 2026
e604a0d
A timed-out queued rename keeps its paths; transfers await their tasks
5cript Oct 14, 2026
a83e543
A download with reserved space replaces only when every byte arrived
5cript Oct 14, 2026
34ad054
Add Utility::restrictToOwner and Utility::newFilePermissions
5cript Oct 14, 2026
7d6d65e
Transfers set times and permissions on the temp file before the move
5cript Oct 14, 2026
da88215
A downloaded symlink is created under the temp name and moved into place
5cript Oct 14, 2026
1ae51fa
A bulk download applies directory permissions once every entry is done
5cript Oct 14, 2026
196dc8d
The tar writer gives members with unknown permissions the default mode
5cript Oct 14, 2026
bc61be9
Uploaded archives never carry setuid, setgid or sticky bits
5cript Oct 14, 2026
8e24eb7
AsyncTransferContext answers hasEnded from its outcome
5cript Oct 14, 2026
52c520b
Archive downloads avoid the throwing filesystem overloads
5cript Oct 14, 2026
1295372
Read the process umask at the start of main
5cript Oct 14, 2026
0d75e4e
PathError::toString always yields UTF-8; one splitter for path segments
5cript Oct 14, 2026
06e08a4
Windows device names and name length follow the Windows rules
5cript Oct 14, 2026
4775092
A Windows LocalPath is normalized and device paths are refused
5cript Oct 14, 2026
2717366
The local delete guard folds case and compares homes by file identity
5cript Oct 14, 2026
95fe1a4
SftpSession canonicalizes paths and the start directory on the server
5cript Oct 14, 2026
d27cb6a
Remote deletes are checked by a RemoteDeleteGuard
5cript Oct 14, 2026
62f8949
A sync delete of a protected directory refuses everything below it
5cript Oct 14, 2026
1426914
SftpSession checks strand and paths with one requireSetInStrand
5cript Oct 14, 2026
a15329f
The frontend asks for the local path platform before loading the state
5cript Oct 14, 2026
c60369a
Document one delete operation per bulk delete request
5cript Oct 14, 2026
c1c0d5b
paths/utf8.hpp is the one UTF-8 module
5cript Oct 14, 2026
c7abeff
Native paths convert to UTF-8 without throwing
5cript Oct 14, 2026
e7e6683
A listed name that is not UTF-8 is shown sanitized and never acted on
5cript Oct 14, 2026
e693c59
Calls into the frontend never throw on text that is not UTF-8
5cript Oct 14, 2026
15ad707
Error texts and local paths reach logs and the UI as UTF-8
5cript Oct 14, 2026
0463435
Starting a process uses native and wide text, not the ANSI code page
5cript Oct 14, 2026
d9822a8
Key files are read by their native path and imported from memory
5cript Oct 14, 2026
a4cff3d
The Windows executable runs with the UTF-8 active code page
5cript Oct 14, 2026
717f8b5
Help and --console output on Windows writes UTF-8
5cript Oct 14, 2026
831913d
Case conversion changes ASCII letters only
5cript Oct 14, 2026
9c31593
The tar writer stores non-ASCII names in PAX records
5cript Oct 14, 2026
298cfe0
Link-safe removal goes on past failures and names the failed item
5cript Oct 14, 2026
50257fe
A bulk download inspects each local directory once for links
5cript Oct 14, 2026
e0669b5
The queued local delete can delete whole trees
5cript Oct 14, 2026
dbf38aa
RpcFilesystem::removeSome removes items without following links
5cript Oct 14, 2026
157bace
Add Utility::LifetimeToken for callbacks that outlive their owner
5cript Oct 14, 2026
837fd67
Local deletes of a selection with a directory are queued with a card
5cript Oct 14, 2026
811b75d
The sync diff compares each side with a per-pair baseline
5cript Oct 14, 2026
6355991
A prescanned bulk works below its request's roots
5cript Oct 14, 2026
d61bd3a
Sync keeps a baseline per pair of a saved session
5cript Oct 14, 2026
65801f9
Synchronize is off while a compare runs; refusals show their reason
5cript Oct 14, 2026
f90d136
A bulk delete goes on past directories it cannot scan
5cript Oct 14, 2026
883d380
When one scan of a sync fails, the other one is cancelled
5cript Oct 14, 2026
849f304
Sync matches names through one case and normalization fold
5cript Oct 14, 2026
63e0e72
The remote existence check lists a directory only for ten children
5cript Oct 14, 2026
28c70d6
Sync scans exclude the asides of replacing moves
5cript Oct 14, 2026
1a2bec1
A directory upload never creates an unreadable subdirectory empty
5cript Oct 14, 2026
406bc09
A dropped link or junction is described as the link itself
5cript Oct 14, 2026
0186411
EnqueuePlanEntry is a plain struct; stale comments are corrected
5cript Oct 14, 2026
3463ba9
Sync baseline files carry a hash of their session in the name
5cript Oct 14, 2026
b315b91
Format the sync, existence check and bulk changes with clang-format
5cript Oct 14, 2026
bb6bf26
Operations hold their SFTP session weakly and lock it per work step
5cript Oct 14, 2026
bde0458
A connection's operation queue starts unless its profile pauses it
5cript Oct 14, 2026
94021ca
Every SFTP request on a channel is answered
5cript Oct 14, 2026
836149c
A bulk resumed after a reconnect leaves out what it finished
5cript Oct 14, 2026
59eac88
The frontend moves bulk entries into requests instead of copying
5cript Oct 14, 2026
66598c7
Completions are built from their common fields once
5cript Oct 14, 2026
cb77b31
A resumed bulk card shows the whole bulk
5cript Oct 14, 2026
67c9d82
Walked trees build each directory's path once
5cript Oct 14, 2026
f03ccfb
Remote and local scans share ScanWalkers templates
5cript Oct 14, 2026
203f3f7
An archive download looks up each file root again before packing it
5cript Oct 14, 2026
422e997
Uploads and local scans use the error code filesystem overloads
5cript Oct 14, 2026
477f6ea
A finished delete card no longer shows its scan status
5cript Oct 14, 2026
1b64dbd
Sync remembers nothing across runs
5cript Oct 15, 2026
b196ac9
Case variants block a sync only towards a side that folds case
5cript Oct 15, 2026
d9e71ff
Add ComparedState and the ChangedSinceCompare error
5cript Oct 15, 2026
1e0a696
Transfers and deletes can be held to what a sync compare saw
5cript Oct 15, 2026
96b52c6
A sync entry replaces or deletes only what its compare saw
5cript Oct 15, 2026
45e9d25
Sync rows name a destination that changed since the compare
5cript Oct 15, 2026
17a8af0
Local deletes run in a LocalOperationQueue of their own
5cript Oct 15, 2026
b57991b
RpcFilesystem::removeSome no longer removes whole trees
5cript Oct 15, 2026
b055845
Closing a session tab ends its session in the backend
5cript Oct 15, 2026
d4f2cae
Local delete cards show in the queue of the tab that started them
5cript Oct 15, 2026
e4a7542
Tests: A local no-replace rename falls back where the filesystem refu…
5cript Oct 14, 2026
91b6415
Tests: A remote no-replace rename falls back when the server refuses …
5cript Oct 14, 2026
1807162
Tests: A timed-out queued rename keeps its paths; transfers await the…
5cript Oct 14, 2026
3827b7e
Tests: A download with reserved space replaces only when every byte a…
5cript Oct 14, 2026
45230dd
Tests: Add Utility::restrictToOwner and Utility::newFilePermissions
5cript Oct 14, 2026
ad5dc28
Tests: Transfers set times and permissions on the temp file before th…
5cript Oct 14, 2026
f5e1b4b
Tests: A downloaded symlink is created under the temp name and moved …
5cript Oct 14, 2026
302b184
Tests: A bulk download applies directory permissions once every entry…
5cript Oct 14, 2026
664abc1
Tests: The tar writer gives members with unknown permissions the defa…
5cript Oct 14, 2026
eeda820
Tests: Uploaded archives never carry setuid, setgid or sticky bits
5cript Oct 14, 2026
e1dac5a
Tests: PathError::toString always yields UTF-8; one splitter for path…
5cript Oct 14, 2026
23c5cf0
Tests: Windows device names and name length follow the Windows rules
5cript Oct 14, 2026
f54e675
Tests: A Windows LocalPath is normalized and device paths are refused
5cript Oct 14, 2026
b139c9f
Tests: The local delete guard folds case and compares homes by file i…
5cript Oct 14, 2026
506ff06
Tests: SftpSession canonicalizes paths and the start directory on the…
5cript Oct 14, 2026
a4c735c
Tests: Remote deletes are checked by a RemoteDeleteGuard
5cript Oct 14, 2026
ee98b35
Tests: A sync delete of a protected directory refuses everything belo…
5cript Oct 14, 2026
fc2e913
Tests: paths/utf8.hpp is the one UTF-8 module
5cript Oct 14, 2026
a85a74d
Tests: Native paths convert to UTF-8 without throwing
5cript Oct 14, 2026
6047fcd
Tests: A listed name that is not UTF-8 is shown sanitized and never a…
5cript Oct 14, 2026
96d268b
Tests: Calls into the frontend never throw on text that is not UTF-8
5cript Oct 14, 2026
53f4d06
Tests: Key files are read by their native path and imported from memory
5cript Oct 14, 2026
a590bf1
Tests: Case conversion changes ASCII letters only
5cript Oct 14, 2026
f7b2bdf
Tests: The tar writer stores non-ASCII names in PAX records
5cript Oct 14, 2026
f6b3c67
Tests: Add the NarrowPathConversions ctest
5cript Oct 14, 2026
5ab42d2
Tests: Local names that are not UTF-8 and non-ASCII file tracking paths
5cript Oct 14, 2026
ee3c4b0
Tests: Link-safe removal goes on past failures and names the failed item
5cript Oct 14, 2026
b412f43
Tests: The queued local delete can delete whole trees
5cript Oct 14, 2026
301430d
Tests: RpcFilesystem::removeSome removes items without following links
5cript Oct 14, 2026
7865d5d
Tests: Add Utility::LifetimeToken for callbacks that outlive their owner
5cript Oct 14, 2026
63ad8c1
Tests: The sync diff compares each side with a per-pair baseline
5cript Oct 14, 2026
748dea8
Tests: A prescanned bulk works below its request's roots
5cript Oct 14, 2026
cf6c560
Tests: Sync keeps a baseline per pair of a saved session
5cript Oct 14, 2026
bbb26bb
Tests: A bulk delete goes on past directories it cannot scan
5cript Oct 14, 2026
91e37aa
Tests: When one scan of a sync fails, the other one is cancelled
5cript Oct 14, 2026
3db6bd2
Tests: Sync matches names through one case and normalization fold
5cript Oct 14, 2026
0f82b3f
Tests: The remote existence check lists a directory only for ten chil…
5cript Oct 14, 2026
2214b3f
Tests: Sync scans exclude the asides of replacing moves
5cript Oct 14, 2026
8160d5a
Tests: A directory upload never creates an unreadable subdirectory empty
5cript Oct 14, 2026
4b196bf
Tests: A dropped link or junction is described as the link itself
5cript Oct 14, 2026
8cc9c2d
Tests: Format the sync, existence check and bulk changes with clang-f…
5cript Oct 14, 2026
4ea159d
Tests: Operations hold their SFTP session weakly and lock it per work…
5cript Oct 14, 2026
94b2388
Tests: Every SFTP request on a channel is answered
5cript Oct 14, 2026
47db53d
Tests: A bulk resumed after a reconnect leaves out what it finished
5cript Oct 14, 2026
60dad13
Tests: A resumed bulk card shows the whole bulk
5cript Oct 14, 2026
3fea404
Tests: Walked trees build each directory's path once
5cript Oct 14, 2026
8cd3e1d
Tests: An archive download looks up each file root again before packi…
5cript Oct 14, 2026
3bfda24
Tests: Uploads and local scans use the error code filesystem overloads
5cript Oct 14, 2026
323e51b
Tests: Sync remembers nothing across runs
5cript Oct 15, 2026
307bb92
Tests: Case variants block a sync only towards a side that folds case
5cript Oct 15, 2026
38497c4
Tests: Add ComparedState and the ChangedSinceCompare error
5cript Oct 15, 2026
2d7e601
Tests: Transfers and deletes can be held to what a sync compare saw
5cript Oct 15, 2026
1d316b1
Tests: A sync entry replaces or deletes only what its compare saw
5cript Oct 15, 2026
6b6f8df
Tests: Local deletes run in a LocalOperationQueue of their own
5cript Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ project(nui-sftp VERSION "${PROJECT_VERSION}" LANGUAGES CXX)

set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "Use an external fmt library (provide it manually)" FORCE)
set(SPDLOG_BUILD_SHARED OFF CACHE BOOL "Dont build shared spdlog" FORCE)
if (WIN32)
set(SPDLOG_WCHAR_FILENAMES ON CACHE BOOL "Open log files by their wide name" FORCE)
endif()
set(NUI_FILE_EXPLORER_MEMORY64 ON CACHE BOOL "Enable MEMORY64 for nui-file-explorer" FORCE)
set(JSON_Diagnostics ON CACHE BOOL "Enable json diagnostics" FORCE)
set(NUI_JSON_DIAGNOSTICS ON CACHE BOOL "Enable json diagnostics for nlohmann_json through nui" FORCE)
Expand Down Expand Up @@ -88,6 +91,9 @@ if (WIN32)
set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE)
include("${CMAKE_CURRENT_LIST_DIR}/_cmake/windows/icon.cmake")
file(TO_CMAKE_PATH "${NUI_SFTP_ICO}" NUI_SFTP_ICO_FORWARD)
# activeCodePage UTF-8: narrow Windows APIs in third-party code (libssh, yaml-cpp, spdlog) read UTF-8.
set(NUI_SFTP_MANIFEST "${CMAKE_CURRENT_LIST_DIR}/_cmake/windows/nui-sftp.manifest")
file(TO_CMAKE_PATH "${NUI_SFTP_MANIFEST}" NUI_SFTP_MANIFEST_FORWARD)
configure_file(
"${CMAKE_CURRENT_LIST_DIR}/_cmake/windows/nui-sftp.rc.in"
"${CMAKE_BINARY_DIR}/generated/nui-sftp.rc"
Expand All @@ -96,7 +102,7 @@ if (WIN32)
target_sources(${PROJECT_NAME} PRIVATE "${CMAKE_BINARY_DIR}/generated/nui-sftp.rc")
set_source_files_properties(
"${CMAKE_BINARY_DIR}/generated/nui-sftp.rc"
PROPERTIES OBJECT_DEPENDS "${NUI_SFTP_ICO}"
PROPERTIES OBJECT_DEPENDS "${NUI_SFTP_ICO};${NUI_SFTP_MANIFEST}"
)
add_dependencies(${PROJECT_NAME} nui-sftp-icon)
elseif (APPLE AND NOT EMSCRIPTEN)
Expand Down Expand Up @@ -199,6 +205,10 @@ else()
add_subdirectory(shared_data/benchmark/shared_data)
add_subdirectory(tar-archive/test/tar_archive)
add_subdirectory(command-store/test/command-store)
add_test(
NAME NarrowPathConversions
COMMAND "${CMAKE_COMMAND}" "-DSOURCE_DIR=${CMAKE_SOURCE_DIR}" -P "${CMAKE_SOURCE_DIR}/_cmake/check_narrow_path_text.cmake"
)
endif()

if (NUI_SFT_ENABLE_LANGUAGE_TOOLING)
Expand Down
45 changes: 45 additions & 0 deletions _cmake/check_narrow_path_text.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Fails when native first-party sources call string() or generic_string(), which convert a path through the ANSI code
# page on Windows and throw outside it. Paths::nativeToUtf8 and Paths::nativeFromUtf8 convert explicitly. Frontend
# sources are not checked, a path there is UTF-8 already. Run with -DSOURCE_DIR=<repository root>.

set(roots
backend
command-store/include
command-store/source
log
nui-file-explorer/source/nui-file-explorer/support
paths/include
persistence/include
persistence/source
shared_data/include
shared_data/source
ssh/include
ssh/source
tar-archive/include
tar-archive/source
utility/include
utility/source
)

set(found "")
foreach(root IN LISTS roots)
file(GLOB_RECURSE sources "${SOURCE_DIR}/${root}/*.cpp" "${SOURCE_DIR}/${root}/*.hpp" "${SOURCE_DIR}/${root}/*.mm")
foreach(source IN LISTS sources)
file(RELATIVE_PATH relative "${SOURCE_DIR}" "${source}")
if (relative MATCHES "(^|/)test/")
continue()
endif()
file(STRINGS "${source}" lines REGEX "\\.(generic_)?string\\(\\)")
foreach(line IN LISTS lines)
if (NOT line MATCHES "^[ \t]*//")
string(STRIP "${line}" stripped)
string(APPEND found "\n ${relative}: ${stripped}")
endif()
endforeach()
endforeach()
endforeach()

if (found)
message(FATAL_ERROR "Narrow path conversions, use Paths::nativeToUtf8 or Paths::nativeFromUtf8:${found}")
endif()
message(STATUS "No narrow path conversions in native sources.")
8 changes: 8 additions & 0 deletions _cmake/windows/nui-sftp.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>
3 changes: 3 additions & 0 deletions _cmake/windows/nui-sftp.rc.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
// (Windows picks the lowest-numbered ICON resource).
1 ICON "@NUI_SFTP_ICO_FORWARD@"

// The application manifest: CREATEPROCESS_MANIFEST_RESOURCE_ID (1) of type RT_MANIFEST (24).
1 24 "@NUI_SFTP_MANIFEST_FORWARD@"

VS_VERSION_INFO VERSIONINFO
FILEVERSION @SEMVER_MAJOR@,@SEMVER_MINOR@,@SEMVER_PATCH@,0
PRODUCTVERSION @SEMVER_MAJOR@,@SEMVER_MINOR@,@SEMVER_PATCH@,0
Expand Down
6 changes: 4 additions & 2 deletions backend/include/backend/dropped_entry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#include <optional>

/**
* @brief Describes a file dropped onto the window from the desktop, with just enough detail for the transfer.
* @param path Local path of the dropped file or directory, nothing when it is not an absolute path.
* @brief Describes a file dropped onto the window from the desktop, with just enough detail for the transfer. A
* dropped link or junction is a symlink, never what it points to.
* @param path Local path of the dropped item. Nothing, and a log line, when it is not an absolute path or cannot be
* inspected.
*/
std::optional<SharedData::DroppedItem> droppedItemFromPath(std::filesystem::path const& path);
12 changes: 10 additions & 2 deletions backend/include/backend/existence_check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <ssh/sftp_session.hpp>

#include <chrono>
#include <cstddef>
#include <expected>
#include <string>
#include <vector>
Expand All @@ -14,8 +15,15 @@
std::expected<std::vector<bool>, std::string> localPathsExist(std::vector<std::string> const& paths);

/**
* @brief Whether each of @p paths exists on the server, with the same rules as localPathsExist. Few paths are
* checked with one lstat each, more with one listing per parent directory.
* @brief How many queried children of one remote directory are checked by listing it instead of an lstat each.
*/
constexpr std::size_t remoteListingThreshold = 10;

/**
* @brief Whether each of @p paths exists on the server, with the same rules as localPathsExist. The children of a
* directory with at least @ref remoteListingThreshold of them queried are checked with one listing of it
* (names only), the others with one lstat each. A directory that cannot be listed for its permissions has its
* children looked up one by one, and so has a name only a case or normalization variant of is listed.
*/
std::expected<std::vector<bool>, std::string> remotePathsExist(
SecureShell::SftpSession& session,
Expand Down
60 changes: 19 additions & 41 deletions backend/include/backend/local_delete_guard.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,33 @@

#include <paths/local_path.hpp>

#include <fmt/format.h>

#include <expected>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

/**
* @brief Refuses a root directory and @p home or a directory containing it.
* @brief Refuses an unset path, a root directory, and each of @p homes or a directory containing one.
*
* Paths are compared as text, and without ASCII case for Windows paths and on macOS, whose filesystems fold case.
* A target that is an existing directory (not a link to one) is also compared by file identity with every home and
* each of its ancestors, which catches other spellings of the same directory: case beyond ASCII, 8.3 names, NFD
* names and ancestors reached through a symlink.
*/
inline std::expected<void, std::string>
refuseProtectedLocalDelete(Paths::LocalPath const& target, std::optional<Paths::LocalPath> const& home)
{
if (target.empty())
return std::unexpected(std::string{"no path"});
if (target.isRoot())
return std::unexpected(fmt::format("'{}' is a root directory", target.toUtf8()));
if (home && home->isWithin(target))
return std::unexpected(fmt::format("'{}' contains the home directory", target.toUtf8()));
return {};
}
std::expected<void, std::string>
refuseProtectedLocalDelete(Paths::LocalPath const& target, std::vector<Paths::LocalPath> const& homes);

/**
* @brief A local path that may be deleted, or the reason to refuse it: not absolute, containing '..', a root
* directory, or @p home or a directory containing it.
* @brief A local path that may be deleted, or the reason to refuse it: not a valid absolute path, containing '..'
* (also as '\\..' on Windows) or refused by @ref refuseProtectedLocalDelete.
*/
inline std::expected<Paths::LocalPath, std::string>
localDeleteTarget(std::string const& utf8, std::optional<Paths::LocalPath> const& home)
{
// LocalPath resolves ".." lexically, which goes elsewhere when a directory on the way is a symlink.
for (auto const segment : std::views::split(std::string_view{utf8}, '/'))
{
for (auto const part : std::views::split(segment, '\\'))
{
if (std::string_view{part.begin(), part.end()} == "..")
return std::unexpected(fmt::format("'{}' contains '..'", utf8));
}
}

auto target = Paths::LocalPath::fromUtf8(utf8);
if (!target)
return std::unexpected(fmt::format("'{}' {}", utf8, Paths::toString(target.error().reason)));
if (auto allowed = refuseProtectedLocalDelete(*target, home); !allowed)
return std::unexpected(std::move(allowed).error());
return *std::move(target);
}
std::expected<Paths::LocalPath, std::string>
localDeleteTarget(std::string const& utf8, std::vector<Paths::LocalPath> const& homes);

/**
* @brief The home directory of the user running the backend, empty when it cannot be determined.
* @brief The home directories of the user running the backend, which the delete guards protect, each once.
*
* On Windows the profile folder (FOLDERID_Profile) and the USERPROFILE and HOME variables, elsewhere the HOME
* variable and the home of the passwd entry. When none can be determined a warning is logged and only roots stay
* protected. Never throws.
*/
std::optional<Paths::LocalPath> localHomeDirectory();
std::vector<Paths::LocalPath> localHomeDirectories();
152 changes: 152 additions & 0 deletions backend/include/backend/local_operation_queue.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#pragma once

#include <backend/rpc_helper.hpp>
#include <backend/sftp/local_delete_operation.hpp>
#include <ids/ids.hpp>
#include <paths/local_path.hpp>
#include <shared_data/file_operations/compared_state.hpp>
#include <shared_data/file_operations/operation_completed.hpp>
#include <shared_data/file_operations/operation_mode.hpp>

#include <nui/rpc.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/strand.hpp>

#include <atomic>
#include <deque>
#include <expected>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

/**
* @brief Runs local deletes apart from the SSH sessions, so closing a tab or losing a connection never stops one.
*
* The deletes run one after another on the strand the queue shares with the sessions, a work step at a time, and
* report to the frontend under "LocalOperationQueue::...". A delete a session started waits while the queue of that
* session is paused, for as long as the session exists.
*/
class LocalOperationQueue
: public RpcHelper::StrandRpc
, public std::enable_shared_from_this<LocalOperationQueue>
{
public:
using OperationCompleted = SharedData::OperationCompleted;

struct DeleteRequest
{
Ids::OperationId operationId{};
std::vector<Paths::LocalPath> targets{};
/** @brief See LocalDeleteOperation::LocalDeleteOperationOptions::recursive. */
bool recursive{false};
/** @brief The card refreshes the local listing when the delete ends. */
bool insertRefresh{false};
/** @brief A priority delete runs before the others and is never held. */
SharedData::OperationMode mode{SharedData::OperationMode::Queued};
/** @brief The session whose paused queue holds the delete, none for a delete without a session. */
std::optional<Ids::SessionId> owner{};
/** @brief The queue of @ref owner is paused, so the delete starts held. */
bool held{false};
/** @brief See LocalDeleteOperation::LocalDeleteOperationOptions::comparedStates. */
std::vector<SharedData::ComparedState> comparedStates{};
};

LocalOperationQueue(
boost::asio::any_io_executor executor,
std::shared_ptr<boost::asio::strand<boost::asio::any_io_executor>> strand,
Nui::Window& wnd,
Nui::RpcHub& hub,
std::shared_ptr<std::atomic_bool const> localDeletionPrevented
);
~LocalOperationQueue() override = default;
LocalOperationQueue(LocalOperationQueue const&) = delete;
LocalOperationQueue& operator=(LocalOperationQueue const&) = delete;
LocalOperationQueue(LocalOperationQueue&&) = delete;
LocalOperationQueue& operator=(LocalOperationQueue&&) = delete;

/**
* @brief Handles "LocalOperationQueue::addDelete" (operation id, local paths), the delete of explorer items
* without a session, and "LocalOperationQueue::cancel" (operation id).
*/
void registerRpc();

/**
* @brief Queues a delete with one card, labelled with the directory its targets lie in. Refused as a whole when
* one target is a root directory or contains a home directory. Assumed in strand.
*/
std::expected<void, Operation::Error> addDelete(DeleteRequest request);

/**
* @brief Queues the delete of the explorer items @p pathsUtf8 with their content, as @ref addDelete with a card
* that refreshes the local listing. Refused when deletion is prevented in the settings or a path is not
* an item that may be deleted. Assumed in strand.
*
* @param owner The session whose explorer asks, none without one.
* @param held The queue of @p owner is paused.
* @return A message for Utility::localizedMessage or the language file saying why nothing was queued.
*/
std::expected<void, std::string> addExplorerDelete(
Ids::OperationId operationId,
std::vector<std::string> const& pathsUtf8,
std::optional<Ids::SessionId> owner,
bool held
);

/**
* @brief Holds the deletes of @p owner, or lets them run again. Assumed in strand.
*/
void hold(Ids::SessionId const& owner, bool held);

/**
* @brief The deletes of @p owner run on their own from now on, never held again. Assumed in strand.
*/
void release(Ids::SessionId const& owner);

/**
* @brief Ends the delete @p operationId as canceled and tells the frontend. Assumed in strand.
*/
void cancel(Ids::OperationId const& operationId);

/**
* @brief Works the first delete that is not held for one step and tells the frontend when it ends. Assumed in
* strand; the queue drives itself, this is public for tests.
*
* @return True while a delete that is not held is left.
*/
bool work();

/**
* @brief Test hook: @p observer sees every completion right before it is sent to the frontend.
*/
void setCompletionObserver(std::function<void(OperationCompleted const&)> observer);

private:
struct QueuedDelete
{
Ids::OperationId operationId;
std::unique_ptr<LocalDeleteOperation> operation;
std::optional<Ids::SessionId> owner;
bool held;
bool priority;
};

/**
* @brief Posts the next work step unless one is posted already.
*/
void schedule();

void complete(
QueuedDelete const& queued,
SharedData::OperationCompletionReason reason,
std::optional<Operation::Error> error
);
std::string rpcName(std::string_view event) const;

std::shared_ptr<std::atomic_bool const> localDeletionPrevented_;
std::deque<QueuedDelete> deletes_{};
bool scheduled_{false};
std::function<void(OperationCompleted const&)> completionObserver_{};
};
Loading
Loading