fix(prune): honest reclaim/skip counts; cross-ref layout coupling

Already-absent files no longer inflate reclaimedCount (outAlreadyAbsent
distinguishes vanished-at-delete from real failures). Stale skip count
de-dups confirmed before subtraction so duplicates aren't tallied as
stale. prune_button.h rightInset and drawTailFooter vrc.right now
cross-reference each other by name.
This commit is contained in:
2026-07-26 18:59:30 -04:00
parent 7b53ba16f6
commit ef2d3fa846
4 changed files with 54 additions and 22 deletions
+34 -13
View File
@@ -79,6 +79,7 @@
#include <string>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
#include <vector>
// Move-to-trash surface (fork R-C, trash-preferred). On Windows the Recycle Bin is
@@ -352,9 +353,13 @@ std::vector<std::string> ReaSamplerSession::pruneOrphanSet() const {
namespace {
// Deletes ONE orphan file, trash-preferred (fork R-C, settled). Returns true iff the
// file is gone from disk after the call (deleted here, OR already absent — an already-
// vanished file is a success for the reclaim's purpose, not a failure). `absPath` is the
// resolved absolute path (forward-slashed). NON-THROWING: no exception may cross the C ABI.
// file was deleted BY THIS CALL (reclaimed here). Returns false for two distinct cases:
// * `outAlreadyAbsent` set true — the file was already gone before we touched it;
// the caller folds this into the stale/staleness tally, NOT reclaimedCount.
// * `outAlreadyAbsent` left false — a real delete failure (locked, conversion error);
// the caller folds this into skippedCount.
// `absPath` is the resolved absolute path (forward-slashed). NON-THROWING: no exception
// may cross the C ABI.
//
// Per-platform routing:
// * Windows — SHFileOperationW(FO_DELETE, pFrom=<double-NUL path>, FOF_ALLOWUNDO |
@@ -364,7 +369,8 @@ namespace {
// * Other (SWELL: macOS/Linux) — no portable move-to-trash surface is available in this
// codebase, so fall back to std::filesystem::remove (hard unlink) behind the R3
// confirm guardrail. `outUsedTrash` left as-is (false).
bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash) {
bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash,
bool& outAlreadyAbsent) {
#ifdef _WIN32
// Convert forward-slashed UTF-8 to a back-slashed, double-NUL-terminated wide string.
// SHFileOperation's pFrom is a list; a single path still needs the extra terminating
@@ -373,7 +379,7 @@ bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash) {
for (char& c : win) if (c == '/') c = '\\';
const int wlen = MultiByteToWideChar(CP_UTF8, 0, win.c_str(), -1, nullptr, 0);
if (wlen <= 0) return false; // conversion failed -> report as skip
if (wlen <= 0) return false; // conversion failed -> real skip (outAlreadyAbsent stays false)
std::vector<wchar_t> wbuf(static_cast<std::size_t>(wlen) + 1, L'\0'); // +1 for list NUL
MultiByteToWideChar(CP_UTF8, 0, win.c_str(), -1, wbuf.data(), wlen);
// wbuf now holds the path + its NUL at [wlen-1]; the extra trailing L'\0' at [wlen]
@@ -389,20 +395,25 @@ bool deleteOrphanFile(const std::string& absPath, bool& outUsedTrash) {
const int rv = SHFileOperationW(&op);
if (rv == 0 && !op.fAnyOperationsAborted) {
outUsedTrash = true;
return true;
return true; // deleted this call -> reclaimed
}
// SHFileOperation failed (e.g. file already gone yields a nonzero code on some
// versions, or a lock). Treat "already absent" as success; otherwise a real skip.
// versions, or a lock). Distinguish "already absent" from a real failure so the
// caller can tally them separately (absent -> staleness skip; failure -> locked skip).
std::error_code ec;
return !fs::exists(absPath, ec);
if (!fs::exists(absPath, ec)) {
outAlreadyAbsent = true; // vanished between scan and delete -> staleness, not reclaim
}
return false;
#else
// No portable trash surface on SWELL platforms -> hard unlink behind the confirm.
std::error_code ec;
const bool removed = fs::remove(absPath, ec);
if (removed) return true; // deleted this call
if (removed) return true; // deleted this call -> reclaimed
if (ec) return false; // a real failure (locked / permission) -> skip
// remove returned false with no error == the file did not exist -> already gone.
return !fs::exists(absPath, ec);
outAlreadyAbsent = true; // vanished between scan and delete -> staleness, not reclaim
return false;
#endif
}
@@ -423,8 +434,13 @@ PruneDeletionResult ReaSamplerSession::pruneReclaim(
const std::vector<std::string> plan = pruneDeletePlan(confirmed, scan.orphans);
// Anything the user confirmed but that is no longer a fresh orphan is a staleness skip.
result.skippedCount += confirmed.size() - plan.size();
// Staleness skip count: entries the user confirmed that are no longer fresh orphans
// (vanished or became referenced between confirm and delete). pruneDeletePlan already
// de-dups confirmed internally, so compute the unique-confirmed size to avoid counting
// de-duplicated entries as stale — that would be dishonest.
const std::size_t uniqueConfirmedCount =
std::unordered_set<std::string>(confirmed.begin(), confirmed.end()).size();
result.skippedCount += uniqueConfirmedCount - plan.size();
for (const std::string& rel : plan) {
// Reconstruct the absolute path from the resolved bank dir + the entry's file name.
@@ -437,9 +453,14 @@ PruneDeletionResult ReaSamplerSession::pruneReclaim(
const auto szIt = scan.sizeByRel.find(rel);
const std::uint64_t bytes = (szIt != scan.sizeByRel.end()) ? szIt->second : 0;
if (deleteOrphanFile(absPath, result.usedTrash)) {
bool alreadyAbsent = false;
if (deleteOrphanFile(absPath, result.usedTrash, alreadyAbsent)) {
++result.reclaimedCount;
result.reclaimedBytes += bytes;
} else if (alreadyAbsent) {
// File vanished between plan and delete — treat as staleness, same as the
// confirm→plan gap above. Does NOT count as reclaimed (we didn't delete it).
++result.skippedCount;
} else {
++result.skippedCount; // locked / conversion failure -> recorded, not thrown
}