454f67b3bc
Clamps insertSuffix's underflow, floors uniqueEntryName's validity guard, suppresses the redundant overwrite confirm via a picker out-param, adds a PayloadBuffer high-water mark, and corrects stale CLAUDE.md/CMake claims.
197 lines
8.3 KiB
C++
197 lines
8.3 KiB
C++
// package_export_action.cpp — see package_export_action.h for the contract this TU
|
|
// preserves. main.cpp owns the API pointers; this TU gets them extern.
|
|
|
|
#include "shell/actions/package_export_action.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/capture/capture_paths.h" // projectDirOfRpp, sanitizeStem
|
|
#include "shell/package/export_bank.h"
|
|
#include "shell/package/package_pickers.h"
|
|
#include "shell/persist/session.h"
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_EnumProjects
|
|
#define REAPERAPI_WANT_ShowConsoleMsg
|
|
#define REAPERAPI_WANT_ShowMessageBox
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kUnsavedProjectMsg =
|
|
"ReaSampler export: save the project first -- an unsaved project has no bank folder "
|
|
"to read from.\n";
|
|
|
|
std::string currentProjectDir() {
|
|
std::vector<char> buf(4096, '\0');
|
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
|
return capture::projectDirOfRpp(std::string(buf.data()));
|
|
}
|
|
|
|
// 6 == YES; anything else cancels (SDK ~6544).
|
|
bool confirmed(const std::string& msg, const char* title) {
|
|
return ShowMessageBox(msg.c_str(), title, 4) == 6;
|
|
}
|
|
|
|
std::string entryLine(const package::ExcludedEntry& e) {
|
|
const char* why = e.reason == package::ExclusionReason::FileMissing ? "missing"
|
|
: e.reason == package::ExclusionReason::FileUnreadable ? "unreadable"
|
|
: "unusable index record";
|
|
return " " + (e.displayName.empty() ? e.sampleId : e.displayName) + " [" + why +
|
|
"] " + e.relativePath + "\n";
|
|
}
|
|
|
|
// `maxLines` == 0 lists everything (the console record); a positive cap keeps a
|
|
// confirm dialog readable on a bank with hundreds of absent files, prune's own
|
|
// truncate-the-confirm-not-the-report discipline.
|
|
std::string excludedManifest(const std::vector<package::ExcludedEntry>& excluded,
|
|
std::size_t maxLines) {
|
|
std::string msg;
|
|
std::size_t shown = 0;
|
|
for (const package::ExcludedEntry& e : excluded) {
|
|
if (maxLines != 0 && shown == maxLines) {
|
|
msg += " ... (" + std::to_string(excluded.size() - shown) +
|
|
" more, listed in the console)\n";
|
|
break;
|
|
}
|
|
msg += entryLine(e);
|
|
++shown;
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
void reportOutcome(const ExportOutcome& out, const std::string& destPath) {
|
|
switch (out.status) {
|
|
case ExportStatus::Written:
|
|
ShowConsoleMsg(("ReaSampler export: wrote " + std::to_string(out.entriesWritten) +
|
|
" entry/entries (" + std::to_string(out.bytesWritten) +
|
|
" bytes) to " + destPath + "\n")
|
|
.c_str());
|
|
return;
|
|
case ExportStatus::SourceReadFailed:
|
|
ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + out.offendingName +
|
|
"\" could not be read. Nothing was written.\n")
|
|
.c_str());
|
|
return;
|
|
case ExportStatus::SourceChanged:
|
|
ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + out.offendingName +
|
|
"\" changed on disk while the package was being written. "
|
|
"Nothing was written; run the export again.\n")
|
|
.c_str());
|
|
return;
|
|
case ExportStatus::EncodeFailed:
|
|
ShowConsoleMsg("ReaSampler export: ABORTED -- this bank could not be encoded "
|
|
"as a package. Nothing was written.\n");
|
|
return;
|
|
// Both refusals are re-derived from a FRESH plan, so reaching them after the
|
|
// survey means the bank changed under the export, not that the user declined.
|
|
case ExportStatus::RefusedIncomplete:
|
|
case ExportStatus::RefusedUnrepresentable:
|
|
ShowConsoleMsg("ReaSampler export: ABORTED -- the bank changed between the "
|
|
"report and the write. Nothing was written; run the export "
|
|
"again.\n");
|
|
return;
|
|
case ExportStatus::RefusedDestinationExists:
|
|
ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n");
|
|
return;
|
|
case ExportStatus::NoSuchBank:
|
|
ShowConsoleMsg("ReaSampler export: that bank no longer exists.\n");
|
|
return;
|
|
case ExportStatus::NoProjectDir:
|
|
ShowConsoleMsg(kUnsavedProjectMsg);
|
|
return;
|
|
case ExportStatus::WriteFailed:
|
|
ShowConsoleMsg(("ReaSampler export: FAILED writing " + destPath +
|
|
". No package was left behind; any file already at that path is "
|
|
"untouched.\n")
|
|
.c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void doBankPackageExport(const ReaSamplerSession& session, const std::string& bankId) {
|
|
const std::string projectDir = currentProjectDir();
|
|
if (projectDir.empty()) {
|
|
ShowConsoleMsg(kUnsavedProjectMsg);
|
|
return;
|
|
}
|
|
|
|
// Report before acting, and before the picker opens: a refusal the user cannot
|
|
// act on should not cost them a trip through a save dialog first.
|
|
const ExportSurvey survey = surveyBankExport(session, projectDir, bankId);
|
|
if (!survey.bankFound) {
|
|
ShowConsoleMsg("ReaSampler export: no such bank.\n");
|
|
return;
|
|
}
|
|
const std::string bankName = survey.plan.manifest.bankDisplayName;
|
|
|
|
bool allowIncomplete = false;
|
|
if (survey.plan.verdict == package::ExportVerdict::Refused) {
|
|
ShowConsoleMsg(("ReaSampler export: ABORTED -- \"" + bankName +
|
|
"\" holds index record(s) a package cannot carry. Nothing was "
|
|
"written.\n" +
|
|
excludedManifest(survey.plan.excluded, 0))
|
|
.c_str());
|
|
return;
|
|
}
|
|
if (survey.plan.verdict == package::ExportVerdict::Incomplete) {
|
|
const std::string headline =
|
|
"ReaSampler export: \"" + bankName + "\" has " +
|
|
std::to_string(survey.plan.excluded.size()) +
|
|
" entry/entries whose file is missing or unreadable:\n";
|
|
ShowConsoleMsg((headline + excludedManifest(survey.plan.excluded, 0)).c_str());
|
|
if (!confirmed(headline + excludedManifest(survey.plan.excluded, 10) +
|
|
"\nExport the " +
|
|
std::to_string(survey.plan.manifest.entries.size()) +
|
|
" present entry/entries anyway?",
|
|
"ReaSampler: incomplete bank")) {
|
|
ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n");
|
|
return;
|
|
}
|
|
allowIncomplete = true;
|
|
}
|
|
|
|
// The bank's own name, not the project's: the artifact is a bank, and a user
|
|
// exporting three banks from one project needs three distinguishable files.
|
|
const std::string suggested =
|
|
projectDir + "/" + capture::sanitizeStem(bankName) + ".rsbank";
|
|
std::string dest;
|
|
bool appended = false;
|
|
if (!pickPackageSavePath(suggested, dest, &appended)) return; // user cancelled the picker
|
|
|
|
ExportRequest req;
|
|
req.projectDir = projectDir;
|
|
req.bankId = bankId;
|
|
req.destAbsPath = dest;
|
|
req.exportTimestamp = static_cast<std::int64_t>(std::time(nullptr));
|
|
req.allowIncomplete = allowIncomplete;
|
|
// The dialog's own overwrite confirm covered exactly this path when the picker
|
|
// did not need to append `.rsbank` to reach it — asking again would be a second
|
|
// prompt for the same consent. An appended path is one the dialog never saw, so
|
|
// that case still falls through to exportBank's own refusal and the confirm below.
|
|
req.allowOverwrite = !appended;
|
|
|
|
ExportOutcome out = exportBank(session, req);
|
|
if (out.status == ExportStatus::RefusedDestinationExists) {
|
|
if (!confirmed("A file already exists at:\n\n " + dest +
|
|
"\n\nReplace it with this bank package?",
|
|
"ReaSampler: replace package")) {
|
|
ShowConsoleMsg("ReaSampler export: cancelled -- nothing was written.\n");
|
|
return;
|
|
}
|
|
req.allowOverwrite = true;
|
|
out = exportBank(session, req);
|
|
}
|
|
reportOutcome(out, dest);
|
|
}
|
|
|
|
} // namespace reasampler
|