import: a .rsbank lands as a new bank, whole or not at all

Four collisions answered explicitly: ids reminted, names never overwritten,
content deduped before the write, bank name auto-suffixed. Degraded ledger
refuses before the picker.
This commit is contained in:
2026-08-02 13:21:48 -04:00
parent 33ea95078d
commit a927dad2f4
26 changed files with 1689 additions and 24 deletions
+3 -1
View File
@@ -4,7 +4,8 @@
The bindable action families routed through REAPER's `command_id`/`gaccel`/
`hookcommand` contract (Design View toggle actions, bank actions, the prune
action, and the shared registration plumbing/table), plus the three drag-out
action, the bank-package import action, and the shared registration
plumbing/table), plus the three drag-out
outcome shells (OS hand-off, instrument drop, arrange drop), plus the
extension-side ingest-through-the-bank shell. This is
where user-facing REAPER actions and OS-level drag/drop live; the underlying
@@ -42,6 +43,7 @@ is owned by other directories and only skinned here.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns), and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Owns every message the import produces; the verb itself is promptless.
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** Surface (2)'s action is the one in this directory published into a NON-main action section (Media Explorer) as well as Main — two ids, one handler, two dispatch hooks; see root `CLAUDE.md` §"REAPER extension contract" for the mechanism.
## Gotchas
+183
View File
@@ -0,0 +1,183 @@
// package_import_action.cpp — see package_import_action.h for the contract.
// main.cpp owns the API pointers; this TU gets them extern.
#include "shell/actions/package_import_action.h"
#include <string>
#include "core/package/import_plan.h"
#include "core/package/package_format.h"
#include "core/version/app_version.h"
#include "shell/package/import_bank.h"
#include "shell/package/package_pickers.h"
#include "shell/persist/session.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_ShowMessageBox
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
constexpr const char* kTitle = "ReaSampler: import bank package";
std::string quoted(const std::string& s) { return "\"" + s + "\""; }
// Mirrors prune's abort block in structure and tone, because a user who has hit that
// one should recognise this one. Every recovery line names THIS build's namespace: a
// beta user handed the stable spelling clears the wrong key and is still blocked.
void reportLedgerRefusal(package::LedgerRefusal refusal) {
const std::string& ns = version::extStateNamespace();
std::string msg =
"ReaSampler import: ABORTED -- the file-tracking ledger could not be read. "
"Nothing was imported.\n";
if (refusal == package::LedgerRefusal::Malformed) {
msg += "The stored file-tracking ledger is malformed. It has been left intact "
"rather than overwritten, so it can be repaired or cleared:\n"
" reaper.SetProjExtState(0, \"" + ns + "\", \"owned_files\", \"\")\n"
"Clearing it makes every existing bank file un-reclaimable (they stop "
"being attributable to ReaSampler); no file is lost. Reopen the project "
"afterwards -- the block is held for the rest of this session.\n";
} else {
msg += "The stored file-tracking ledger was written by a NEWER version of "
"ReaSampler than this one, so its records cannot be read safely. It has "
"been left intact and will NOT be overwritten. Reopen the project with "
"that newer version -- do NOT clear this key from here, that would "
"discard tracking records this build cannot see. The block is held for "
"the rest of this session.\n";
}
msg += "An import can land hundreds of files in one gesture. With no readable "
"ledger, none of them could be given a birth record, and every one would be "
"permanently unreclaimable.\n";
ShowConsoleMsg(msg.c_str());
}
// The refusal a user can act on names all three: what the package needs, what this
// build reads, and which build wrote it. Any two of them leave them stuck.
void reportTooNew(const ImportBankResult& r) {
const std::string writer =
r.header.writerVersion.empty() ? std::string("an unidentified build")
: "ReaSampler " + r.header.writerVersion;
const std::string msg =
"Cannot import this bank package.\n"
"It was written by " + writer + " and needs package format " +
std::to_string(r.header.minReaderVersion) + " or newer.\n"
"This build (" + version::appVersion() + ") reads package format " +
std::to_string(package::kPackageFormatVersion) + ".\n"
"Nothing was imported. Install " + writer + " or newer and try again.";
ShowMessageBox(msg.c_str(), kTitle, 0);
}
void reportSuccess(const ImportBankResult& r) {
std::string detail = "ReaSampler import: imported " + std::to_string(r.landedCount) +
" sample(s) into a new bank: " + quoted(r.bankDisplayName);
if (r.bankNameAdjusted)
detail += " (a bank named " + quoted(r.seedBankName) +
" already exists in this project)";
detail += ".\n";
if (r.renamedCount > 0) {
detail += " " + std::to_string(r.renamedCount) +
" file(s) landed under a freshly minted name (the package's own name "
"was already taken in the bank folder, or was not spelled the way "
"this bank spells a file). An existing bank file is never "
"overwritten.\n";
}
if (r.collapsedCount > 0) {
detail += " " + std::to_string(r.collapsedCount) +
" sample(s) were already present by content and were not written "
"again.\n";
}
detail += "One undo removes the imported bank and its entries. It does NOT delete "
"the imported files -- they stay in the bank folder, referenced by "
"nothing, until a prune reclaims them.\n";
ShowConsoleMsg(detail.c_str());
// The console carries the copyable detail; the box makes the outcome unmissable.
const std::string summary = "Imported " + std::to_string(r.landedCount) +
" sample(s) into a new bank: " +
quoted(r.bankDisplayName) + ".";
ShowMessageBox(summary.c_str(), kTitle, 0);
}
void reportRollback(const RollbackResult& rollback, std::string& msg) {
if (rollback.failedCount > 0) {
msg += "\n" + std::to_string(rollback.failedCount) +
" partly-imported file(s) could not be removed and are still in the bank "
"folder. They are referenced by no bank; a prune will reclaim them.";
}
}
void report(const ImportBankResult& r) {
switch (r.outcome) {
case ImportOutcome::Landed:
reportSuccess(r);
return;
case ImportOutcome::TooNew:
reportTooNew(r);
return;
case ImportOutcome::NoProject:
ShowMessageBox("Save the project before importing a bank package -- an "
"unsaved project has no bank folder to import into.",
kTitle, 0);
return;
case ImportOutcome::Unreadable:
ShowMessageBox("That file could not be opened. Nothing was imported.",
kTitle, 0);
return;
case ImportOutcome::Malformed:
// Distinct from TooNew on purpose: the recoveries are opposite -- one is
// "install a newer build", this one is "get an intact copy".
ShowMessageBox("This file is not a readable bank package (corrupt or "
"truncated). Nothing was imported.",
kTitle, 0);
return;
case ImportOutcome::IntegrityFailed: {
std::string msg = "This bank package is damaged (entry " +
quoted(r.failedEntryName) +
" failed its integrity check). Nothing was imported.";
ShowMessageBox(msg.c_str(), kTitle, 0);
return;
}
case ImportOutcome::WriteFailed: {
std::string msg = "Import failed and was rolled back. Nothing was added.";
reportRollback(r.rollback, msg);
ShowMessageBox(msg.c_str(), kTitle, 0);
return;
}
case ImportOutcome::IndexRejected: {
std::string msg = "The bank index rejected the import. Nothing was added.";
reportRollback(r.rollback, msg);
ShowMessageBox(msg.c_str(), kTitle, 0);
return;
}
}
}
// FIRST, before the picker: making the user find and choose a file we have already
// decided to refuse is the wrong order.
bool ledgerPermits(ReaSamplerSession& session) {
const package::LedgerRefusal refusal =
package::importLedgerRefusal(session.ledgerStatus());
if (refusal == package::LedgerRefusal::None) return true;
reportLedgerRefusal(refusal);
return false;
}
} // namespace
void doImportBankPackage(ReaSamplerSession& session) {
if (!ledgerPermits(session)) return;
std::string path;
if (!pickPackageForImport(path) || path.empty()) return;
report(importBankPackage(session, path));
}
void doImportBankPackageFile(ReaSamplerSession& session, const std::string& packageAbsPath) {
if (packageAbsPath.empty()) return;
if (!ledgerPermits(session)) return;
report(importBankPackage(session, packageAbsPath));
}
} // namespace reasampler
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// package_import_action — the bindable/menu/drop skin over importBankPackage: the
// ledger gate (which runs BEFORE the picker, so a refusal never costs the user a file
// choice), the picker itself, and every message the import produces.
#include <string>
namespace reasampler {
class ReaSamplerSession;
// Gate, pick, import, report. The bound action and the panel's bank menu both call this.
void doImportBankPackage(ReaSamplerSession& session);
// Same, for a .rsbank already named by the user — the panel's file-drop route. The gate
// still runs first; only the picker is skipped.
void doImportBankPackageFile(ReaSamplerSession& session, const std::string& packageAbsPath);
} // namespace reasampler