Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
20d4e21
Mock SFTP server gets fault injection, extensions and fixture commands
5cript Oct 12, 2026
3d1e057
Failed transfers no longer replace the destination
5cript Oct 12, 2026
9e6282a
Scans are folded into the operations that consume them
5cript Oct 12, 2026
87bfebb
Sync plans nothing for entries it cannot see into
5cript Oct 12, 2026
74a5b99
Transfers no longer apply unknown or special permissions
5cript Oct 12, 2026
2926dae
Overwrites and renames never remove the existing item first
5cript Oct 12, 2026
2ffa667
Resumed downloads take the partial file's size from disk
5cript Oct 12, 2026
9b66227
Add the paths target with LocalPath, RemotePath and RelativePath
5cript Oct 12, 2026
8f377a6
Validate names from directory listings
5cript Oct 12, 2026
31d6c85
Close the mock server's stdin after the exit command
5cript Oct 12, 2026
2805128
Drain the mock server's stderr in tests
5cript Oct 12, 2026
e6d3828
Set the result of a full read only after its last callback
5cript Oct 12, 2026
34545c6
Move the SftpSession API to RemotePath
5cript Oct 12, 2026
0bea72d
Add a bulk delete request with RemotePath targets
5cript Oct 12, 2026
0802faa
Type backend deletes with RemotePath and refuse root and home
5cript Oct 12, 2026
b0e3c66
Type remote deletes in the frontend with RemotePath
5cript Oct 12, 2026
116baf1
Type local deletes with LocalPath and refuse root and home
5cript Oct 12, 2026
31689a7
Type the sync roots as LocalPath and RemotePath
5cript Oct 12, 2026
71ec960
Make a bulk delete one operation with one card
5cript Oct 12, 2026
ec78cc5
Add a queued local delete operation
5cript Oct 12, 2026
3a7f214
Execute sync selections in the backend
5cript Oct 12, 2026
fdc5289
Move the sync dialog to executeSyncSelection
5cript Oct 12, 2026
546d872
Remove the frontend's direct directory creation calls
5cript Oct 12, 2026
c85131f
Add typed bulk upload and download requests
5cript Oct 12, 2026
a713465
Type the roots and entries of bulk transfers
5cript Oct 12, 2026
12c2447
Type the queue's transfer entry points and bulk RPCs
5cript Oct 12, 2026
cf743df
Move the explorer's bulk transfers to typed entries
5cript Oct 12, 2026
cf056a8
Release the instance lock before cleanupNow removes the directory
5cript Oct 12, 2026
074352c
Close the metadata stream before the dead instance cleanup
5cript Oct 12, 2026
e33af4e
Resume directory transfers under their own card id
5cript Oct 13, 2026
b738dac
Type the paths of single uploads and downloads
5cript Oct 13, 2026
d4ef387
Type the archive paths
5cript Oct 13, 2026
e3d5e00
Type queued renames as remote paths
5cript Oct 13, 2026
1ecb422
Type added and completed operations, resumables are a variant per kind
5cript Oct 13, 2026
5e32960
Type the frontend's single transfers, renames and archives
5cript Oct 13, 2026
9e73bb6
Directory listings carry the directory they were requested for
5cript Oct 13, 2026
c198272
Type the explorer's current paths and item paths
5cript Oct 13, 2026
efd37ed
A transfer stops when the existence check of its destinations fails
5cript Oct 13, 2026
e415835
Existence checks fail instead of reporting absent when unsure
5cript Oct 13, 2026
be49ece
Drop DirectoryEntry::longName, linkTarget is the raw UTF-8 literal
5cript Oct 13, 2026
3686a54
Directory walkers never see the root path
5cript Oct 13, 2026
e705bdc
Archives carry typed paths
5cript Oct 13, 2026
afbeebc
The explorer's full path lives on its Item, '..' is a parent link
5cript Oct 13, 2026
d7af149
DirectoryEntry carries the listed name as a UTF-8 string
5cript Oct 13, 2026
36174c0
Remove path_utf.hpp, u8_path.hpp and remote_path_bridge.hpp
5cript Oct 13, 2026
efeef2b
Drop the filesystem include of OperationCompleted
5cript Oct 13, 2026
1bd7e61
Local deletes and scans never follow a Windows junction
5cript Oct 13, 2026
4702d4f
The macOS drop handler sends DroppedItems
5cript Oct 13, 2026
e7a27a1
Every test process works in a temporary directory of its own
5cript Oct 14, 2026
31c0f1a
Docker shell suites run one at a time, everything else in parallel
5cript Oct 14, 2026
43ca8eb
TemporaryDirectory cleans up without following links or junctions
5cript Oct 14, 2026
ffcff7a
A directory download never writes through a link in its local tree
5cript Oct 14, 2026
2ef14dc
Sync deletes remove exactly what the compare listed
5cript Oct 14, 2026
f66a9ee
A directory upload fails subdirectories it cannot list
5cript Oct 14, 2026
756c9f4
A failed or cancelled sync scan reports back
5cript Oct 14, 2026
341db5e
The sync dialogs share their provider with the session
5cript Oct 14, 2026
8cd7967
A partial file is only continued when its last 64 KiB match
5cript Oct 14, 2026
4ee9bb4
Explorer transfers replace only destinations the user approved
5cript Oct 14, 2026
ff7a13c
Delete confirmations focus No and name their side
5cript Oct 14, 2026
a7b43cf
File tracking paths go through UTF-8 instead of narrow strings
5cript Oct 14, 2026
b765c9e
File tracking mirrors a local deletion without recursion
5cript Oct 14, 2026
0ddb8b1
Resumes keep the overwrite permission and recursion of the original
5cript Oct 14, 2026
4c35f38
A failed upload reports its error and paths in its completion
5cript Oct 14, 2026
6a02651
Local explorer deletes go on past an item that cannot be removed
5cript Oct 14, 2026
79052fa
Creating a remote file refuses an existing one (O_EXCL)
5cript Oct 14, 2026
4ea4f8e
Sync keeps transfer temp files as excluded entries
5cript Oct 14, 2026
ad99ce5
Dropped text yields only absolute paths and file URLs
5cript Oct 14, 2026
4a77b69
Saving all terminals never replaces existing files
5cript Oct 14, 2026
214110f
File tracking no longer drops edits made while an upload is queued
5cript Oct 14, 2026
98f6015
Sync blocks names that differ only in case
5cript Oct 14, 2026
0c22ede
Uploads from Windows no longer inherit execute bits
5cript Oct 14, 2026
eac26cc
The sync dialog's blocked notice opens a list of entries left alone
5cript Oct 14, 2026
b2353fc
Opening a file that can run code follows a new setting
5cript Oct 14, 2026
8cbdb96
File tracking checks the server before replacing a file
5cript Oct 14, 2026
7dba82d
Sync compares modification times against the last sync of a pair
5cript Oct 14, 2026
d098bec
The Windows app is a GUI program
5cript Oct 14, 2026
ecc8685
Local commands on Windows start with backslashes in their path
5cript Oct 14, 2026
a7abf32
Tests: Mock SFTP server gets fault injection, extensions and fixture …
5cript Oct 12, 2026
4ccda9c
Tests: Failed transfers no longer replace the destination
5cript Oct 12, 2026
1013f6c
Tests: Scans are folded into the operations that consume them
5cript Oct 12, 2026
b24b7bd
Tests: Sync plans nothing for entries it cannot see into
5cript Oct 12, 2026
d25b73e
Tests: Transfers no longer apply unknown or special permissions
5cript Oct 12, 2026
f2d3a7b
Tests: Overwrites and renames never remove the existing item first
5cript Oct 12, 2026
deea6a0
Tests: Exit the mock SFTP server even when a handle stays open
5cript Oct 12, 2026
d7111be
Tests: Add the paths target with LocalPath, RemotePath and RelativePath
5cript Oct 12, 2026
a4f4d05
Tests: Validate names from directory listings
5cript Oct 12, 2026
3d4f66f
Tests: Run the mock server's exit handler once
5cript Oct 12, 2026
2f8246c
Tests: Build the walker test's names from UTF-8
5cript Oct 12, 2026
b4cecab
Tests: Move the SftpSession API to RemotePath
5cript Oct 12, 2026
4647ac9
Tests: Add a bulk delete request with RemotePath targets
5cript Oct 12, 2026
dc16b8c
Tests: Type local deletes with LocalPath and refuse root and home
5cript Oct 12, 2026
67ff62f
Tests: Type the sync roots as LocalPath and RemotePath
5cript Oct 12, 2026
2bd2711
Tests: Execute sync selections in the backend
5cript Oct 12, 2026
fd36c51
Tests: Move the sync dialog to executeSyncSelection
5cript Oct 12, 2026
da54c77
Tests: Add typed bulk upload and download requests
5cript Oct 12, 2026
dc509ee
Tests: Type the roots and entries of bulk transfers
5cript Oct 12, 2026
851dc82
Tests: Type the queue's transfer entry points and bulk RPCs
5cript Oct 12, 2026
2bbc7c6
Tests: Close the metadata stream in DeadTimestampWrittenOnDestruction
5cript Oct 12, 2026
4feb2b1
Tests: Type the paths of single uploads and downloads
5cript Oct 13, 2026
712f26e
Tests: Type the archive paths
5cript Oct 13, 2026
0e3cee8
Tests: Type added and completed operations, resumables are a variant …
5cript Oct 13, 2026
31ad39b
Tests: Existence checks fail instead of reporting absent when unsure
5cript Oct 13, 2026
71ca485
Tests: Drop DirectoryEntry::longName, linkTarget is the raw UTF-8 lit…
5cript Oct 13, 2026
5df264b
Tests: Directory walkers never see the root path
5cript Oct 13, 2026
607f50c
Tests: Archives carry typed paths
5cript Oct 13, 2026
40c7496
Tests: The explorer's full path lives on its Item, '..' is a parent link
5cript Oct 13, 2026
4ede539
Tests: DirectoryEntry carries the listed name as a UTF-8 string
5cript Oct 13, 2026
ef3c0b1
Tests: Remove path_utf.hpp, u8_path.hpp and remote_path_bridge.hpp
5cript Oct 13, 2026
f45fb5c
Tests: GNU tar interop tests pass --force-local and forward slashes
5cript Oct 13, 2026
842930c
Tests: Local deletes and scans never follow a Windows junction
5cript Oct 13, 2026
c056478
Tests: Every test process works in a temporary directory of its own
5cript Oct 14, 2026
cbf46e4
Tests: Docker shell suites run one at a time, everything else in para…
5cript Oct 14, 2026
15384bc
Tests: A directory download never writes through a link in its local …
5cript Oct 14, 2026
efa8065
Tests: Sync deletes remove exactly what the compare listed
5cript Oct 14, 2026
1f90b52
Tests: A directory upload fails subdirectories it cannot list
5cript Oct 14, 2026
280c1f8
Tests: A partial file is only continued when its last 64 KiB match
5cript Oct 14, 2026
7307bc3
Tests: Explorer transfers replace only destinations the user approved
5cript Oct 14, 2026
0bac871
Tests: Creating a remote file refuses an existing one (O_EXCL)
5cript Oct 14, 2026
cd247c6
Tests: Sync keeps transfer temp files as excluded entries
5cript Oct 14, 2026
dbbd50d
Tests: Sync blocks names that differ only in case
5cript Oct 14, 2026
52dbe55
Tests: Uploads from Windows no longer inherit execute bits
5cript Oct 14, 2026
17a9b6f
Tests: Short archive entries, failed archive scans, upload directory …
5cript Oct 14, 2026
715288b
Tests: Opening a file that can run code follows a new setting
5cript Oct 14, 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
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ add_executable(${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} PUBLIC core-target)

if (WIN32)
# A GUI program: a console program lives in a console window and exits with it, closing that window or a crash
# of Windows Terminal ended the app silently. --console attaches to the console it was started from.
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)
configure_file(
Expand Down Expand Up @@ -139,6 +142,7 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/dependencies/gimo" EXCLUDE_FROM_ALL)
add_subdirectory("${CMAKE_SOURCE_DIR}/dependencies/traits" EXCLUDE_FROM_ALL)
add_subdirectory("${CMAKE_SOURCE_DIR}/constants/source/constants")
add_subdirectory("${CMAKE_SOURCE_DIR}/ids/source/ids")
add_subdirectory("${CMAKE_SOURCE_DIR}/paths/source/paths")
add_subdirectory("${CMAKE_SOURCE_DIR}/shared_data/source/shared_data")
add_subdirectory("${CMAKE_SOURCE_DIR}/utility/source/utility")
add_subdirectory("${CMAKE_SOURCE_DIR}/log/source/log")
Expand Down Expand Up @@ -190,6 +194,7 @@ else()
add_subdirectory(ssh/test/ssh)
add_subdirectory(backend/test/backend)
add_subdirectory(utility/test/utility)
add_subdirectory(paths/test/paths)
add_subdirectory(shared_data/test/shared_data)
add_subdirectory(shared_data/benchmark/shared_data)
add_subdirectory(tar-archive/test/tar_archive)
Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ cmake --build build/clang_debug --target \
nui-sftp-shared-data-test nui-sftp-ssh-test \
nui-sftp-tar-archive-test nui-sftp-command-store-test

# Run the whole suite.
ctest --test-dir build/clang_debug --output-on-failure
# Run the whole suite. Tests run in parallel; the timeout makes a hang fail
# after two minutes instead of blocking the run.
ctest --test-dir build/clang_debug --output-on-failure --timeout 120 -j$(nproc)

# Or run an individual binary.
./build/clang_debug/test/nui-sftp-utility-test
Expand Down Expand Up @@ -328,7 +329,7 @@ update both workflows if it changes the build surface.
1. Fork the repository and create a feature branch from `main`.
2. Keep commits focused; prefer small, reviewable changes over large rewrites.
3. Run a debug build with tests enabled locally and make sure `ctest
--output-on-failure` passes from `build/clang_debug`.
--output-on-failure --timeout 120 -j$(nproc)` passes from `build/clang_debug`.
4. Format any C++ changes with clang-format.
5. Open the PR against `main`. Describe the user-visible behaviour, the
motivation, and anything reviewers should pay particular attention to.
Expand Down Expand Up @@ -377,7 +378,7 @@ needed.
"type": "shell",
"dependsOn": "build_debug",
"command": "ctest",
"args": ["--test-dir", "build/clang_debug", "--output-on-failure"],
"args": ["--test-dir", "build/clang_debug", "--output-on-failure", "--timeout", "120", "-j", "8"],
"group": "test"
}
]
Expand Down
7 changes: 4 additions & 3 deletions backend/include/backend/dropped_entry.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#pragma once

#include <shared_data/directory_entry.hpp>
#include <shared_data/dropped_item.hpp>

#include <filesystem>
#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.
* @param path Local path of the dropped file or directory, nothing when it is not an absolute path.
*/
SharedData::DirectoryEntry leanDirectoryEntryFromPath(std::filesystem::path const& path);
std::optional<SharedData::DroppedItem> droppedItemFromPath(std::filesystem::path const& path);
24 changes: 24 additions & 0 deletions backend/include/backend/existence_check.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <ssh/sftp_session.hpp>

#include <chrono>
#include <expected>
#include <string>
#include <vector>

/**
* @brief Whether each of @p paths exists on the local disk. Only "not found" is absent, a dangling symlink exists. A
* path that cannot be checked fails the whole check: a transfer overwrites what it takes for absent.
*/
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.
*/
std::expected<std::vector<bool>, std::string> remotePathsExist(
SecureShell::SftpSession& session,
std::vector<std::string> const& paths,
std::chrono::milliseconds timeout
);
56 changes: 56 additions & 0 deletions backend/include/backend/local_delete_guard.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <paths/local_path.hpp>

#include <fmt/format.h>

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

/**
* @brief Refuses a root directory and @p home or a directory containing it.
*/
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 {};
}

/**
* @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.
*/
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);
}

/**
* @brief The home directory of the user running the backend, empty when it cannot be determined.
*/
std::optional<Paths::LocalPath> localHomeDirectory();
8 changes: 8 additions & 0 deletions backend/include/backend/opener.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <expected>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>

class Opener
Expand All @@ -19,6 +20,13 @@ class Opener

std::expected<void, std::string> openFile(std::filesystem::path const& path, bool openWith);

/**
* @brief Why opening @p path could run code: a program, a script, a shortcut or an application bundle. Nothing
* for an ordinary document or a folder.
* @return A localized message key describing the risk.
*/
std::optional<std::string> riskOf(std::filesystem::path const& path) const;

/**
* @brief Open the native file manager showing @p path.
* @details For a directory, opens the file manager at that directory. For a file, opens the
Expand Down
7 changes: 6 additions & 1 deletion backend/include/backend/rpc_filesystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <backend/opener.hpp>
#include <persistence/state/local_filesystem_options.hpp>

#include <functional>

class RpcFilesystem : public RpcHelper::StrandRpc
{
public:
Expand All @@ -12,7 +14,8 @@ class RpcFilesystem : public RpcHelper::StrandRpc
Nui::Window& wnd,
Nui::RpcHub& hub,
Persistence::LocalFilesystemOptions options,
Opener& opener
Opener& opener,
std::function<Persistence::RiskyFilePolicy()> riskyFilePolicy = []() { return Persistence::RiskyFilePolicy::Warn; }
);

private:
Expand All @@ -34,4 +37,6 @@ class RpcFilesystem : public RpcHelper::StrandRpc
private:
Persistence::LocalFilesystemOptions options_;
Opener* opener_;
/** @brief Read on every open, so a changed setting applies at once. */
std::function<Persistence::RiskyFilePolicy()> riskyFilePolicy_;
};
2 changes: 1 addition & 1 deletion backend/include/backend/session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class Session
void registerRpcSftpOpenSyncSession();
void registerRpcSftpRecomputeSyncDiff();
void registerRpcSftpLoadSyncDiffChildren();
void registerRpcSftpBuildSyncEnqueuePlan();
void registerRpcSftpExecuteSyncSelection();
void registerRpcSftpCancelSyncDiff();
void registerRpcSftpCloseSyncSession();
void registerOperationQueuePauseUnpause();
Expand Down
5 changes: 5 additions & 0 deletions backend/include/backend/sftp/all_operations.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <backend/sftp/download_operation.hpp>
#include <backend/sftp/upload_operation.hpp>
#include <backend/sftp/delete_operation.hpp>
#include <backend/sftp/local_delete_operation.hpp>
#include <backend/sftp/rename_operation.hpp>
#include <backend/sftp/scan_operation.hpp>
#include <backend/sftp/bulk_download_operation.hpp>
Expand Down Expand Up @@ -48,6 +49,10 @@ auto Operation::visit(FunctionT&& func) const
{
return func(static_cast<RenameOperation const&>(*this));
}
case LocalDelete:
{
return func(static_cast<LocalDeleteOperation const&>(*this));
}
case ArchiveDownload:
{
return func(static_cast<ArchiveDownloadOperation const&>(*this));
Expand Down
54 changes: 39 additions & 15 deletions backend/include/backend/sftp/archive_download_operation.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#pragma once

#include <backend/sftp/operation.hpp>
#include <backend/sftp/scan_operation.hpp>
#include <backend/sftp/scan_phase.hpp>
#include <paths/local_path.hpp>
#include <ssh/async/buffer_provider.hpp>
#include <ssh/async/transfer_context.hpp>
#include <ssh/file_stream.hpp>
Expand All @@ -10,6 +13,8 @@
#include <tar_archive/entry_writer.hpp>
#include <tar_archive/compression.hpp>
#include <shared_data/directory_entry.hpp>
#include <shared_data/file_operations/archive_download_root.hpp>
#include <tar_archive/member_header.hpp>

#include <cstdint>
#include <filesystem>
Expand Down Expand Up @@ -62,13 +67,12 @@ class ArchiveDownloadOperation : public Operation
* `foo/sub/`, `foo/sub/baz.txt`.
* - Other types (symlinks, devices, ...) are silently skipped.
*
* `fullPath` must carry the full remote path; `path` is used only if
* `fullPath` is empty.
* A root that is the remote root directory has no name and is refused.
*/
std::vector<SharedData::DirectoryEntry> entries{};
std::vector<SharedData::ArchiveDownloadRoot> roots{};

/** @brief Destination archive path on the local filesystem. */
std::filesystem::path localArchivePath{};
Paths::LocalPath localArchivePath{};

/** @brief Compression codec. Use TarArchive::Compression::Auto to pick from the extension. */
TarArchive::Compression compression{TarArchive::Compression::Auto};
Expand All @@ -81,6 +85,8 @@ class ArchiveDownloadOperation : public Operation

/** @brief Refuse to overwrite a pre-existing final archive. */
bool mayOverwrite{false};
/** @brief Mark the archive as downloaded from the internet, see Utility::markAsDownloaded. */
bool markAsDownloaded{true};

/** @brief Create any missing parents of @ref localArchivePath. */
bool createMissingDirectories{false};
Expand All @@ -98,9 +104,25 @@ class ArchiveDownloadOperation : public Operation
std::expected<WorkStatus, Error> work() override;
std::expected<WorkStatus, Error> workInStrand() override;

// The scan phase blocks on SFTP futures, so it runs off the strand like a standalone scan.
bool usesStrand() const noexcept override
{
return scanPhase_ == nullptr;
}

bool isBarrier() const noexcept override
{
return false;
return scanPhase_ != nullptr;
}

/**
* @brief Scans the directory root of @p scan before packing; roots are scanned in the order added.
*/
void addScanRoot(std::unique_ptr<ScanOperation> scan);

bool scanning() const noexcept
{
return scanPhase_ != nullptr;
}

int parallelWorkDoable(int) const noexcept override
Expand Down Expand Up @@ -129,16 +151,15 @@ class ArchiveDownloadOperation : public Operation
* rather than silently buried inside prepare.
*
* Safe to call multiple times — one call per directory root in
* @ref Options::entries. Matching is by @p remoteRootPath which
* must equal the root entry's full remote path
* (fullPath-or-fallback-to-path in the root @ref DirectoryEntry).
* @ref Options::roots. Matching is by @p remoteRootPath, the
* path of the root.
*
* Threading: invoked on the queue's asio strand between SFTP-strand
* work batches, so no extra synchronisation is required against
* prepareInStrand (which runs on the SFTP strand).
*/
void setScanResultForRoot(
std::filesystem::path const& remoteRootPath,
Paths::RemotePath const& remoteRootPath,
std::vector<SharedData::DirectoryEntry> entries,
std::uint64_t totalBytes
);
Expand All @@ -152,8 +173,8 @@ class ArchiveDownloadOperation : public Operation
*/
struct ResolvedEntry
{
SharedData::DirectoryEntry tarMeta{};
std::filesystem::path remoteFullPath{};
TarArchive::MemberHeader header{};
Paths::RemotePath source{};
};

std::expected<void, Error> prepareInStrand();
Expand All @@ -162,14 +183,14 @@ class ArchiveDownloadOperation : public Operation
* @brief Expand a preceding ScanOperation's flat entry vector into the
* op's ResolvedEntry list, prefixing each entry's tar path with
* @p archivePrefix and composing remote paths under
* @p remoteRootFullPath. Index 0 (the synthetic scan root) is
* @p remoteRoot. Index 0 (the synthetic scan root) is
* skipped because the root directory entry is emitted separately
* by flattenRootsInStrand.
*/
void appendPrescannedInStrand(
std::vector<SharedData::DirectoryEntry> const& entries,
std::filesystem::path const& remoteRootFullPath,
std::filesystem::path const& archivePrefix
Paths::RemotePath const& remoteRoot,
Paths::RelativePath const& archivePrefix
);
std::expected<void, Error> openNextEntryInStrand();
std::expected<bool, Error> readChunkInStrand();
Expand All @@ -191,14 +212,16 @@ class ArchiveDownloadOperation : public Operation
std::uint64_t totalBytesTransferred_{0u};
std::uint64_t totalPayloadBytes_{0u};
SecureShell::BufferLease buffer_;
/** @brief options_.localArchivePath for the filesystem calls, empty when none was given. */
std::filesystem::path localArchiveNativePath_{};
std::filesystem::path tempPath_{};
/**
* @brief Pre-scanned entry lists delivered by preceding ScanOperation(s),
* keyed by the directory root's remote full path. flattenRoots
* looks each directory root up here before falling back to a live
* recurseIntoDirectoryInStrand listing.
*/
std::map<std::filesystem::path, std::pair<std::vector<SharedData::DirectoryEntry>, std::uint64_t>>
std::map<Paths::RemotePath, std::pair<std::vector<SharedData::DirectoryEntry>, std::uint64_t>>
prescannedRoots_{};
/**
* @brief Reused purely for its bytes-transferred + B/s meter machinery;
Expand All @@ -208,4 +231,5 @@ class ArchiveDownloadOperation : public Operation
*/
std::shared_ptr<SecureShell::AsyncTransferContext> throughputMeter_{
std::make_shared<SecureShell::AsyncTransferContext>()};
std::unique_ptr<ScanPhase<ScanOperation>> scanPhase_{};
};
Loading
Loading