// Copyright (c) Robin E.R. Davies // Copyright (c) Gabriel Hernandez // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "pch.h" #include "Locale.hpp" #include "Storage.hpp" #include "AudioConfig.hpp" #include "PiPedalException.hpp" #include "AlsaSequencer.hpp" #include #include #include "json.hpp" #include #include "Lv2Log.hpp" #include #include #include "PiPedalUI.hpp" #include "PluginHost.hpp" #include "ss.hpp" #include "ofstream_synced.hpp" #include "ModFileTypes.hpp" #include #include #include "util.hpp" #include "AudioFiles.hpp" #include "Utf8Utils.hpp" #include "AtomConverter.hpp" #include "FileBrowserFilesFeature.hpp" #include #include "util.hpp" using namespace pipedal; using namespace ::pipedal::implementation; namespace fs = std::filesystem; const char* BANK_EXTENSION = ".bank"; const char* BANKS_FILENAME = "index.banks"; #define USER_SETTINGS_FILENAME "userSettings.json"; static bool hasSyntheticModRoot(const UiFileProperty& fileProperty) { return (fileProperty.modDirectories().size() > 1 || (fileProperty.modDirectories().size() == 1 && fileProperty.useLegacyModDirectory())); } Storage::Storage() { SetConfigRoot("~/var/Config"); SetDataRoot("~/var/PiPedal"); } inline bool isSafeCharacter(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '-'; } static int fromhexStrict(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; // lowercase letters won't round-trip properly. return -1; } std::string Storage::SafeDecodeName(const std::string& name) { std::stringstream s; for (int i = 0; i < name.length(); /**/) { char c = name[i++]; if (c < ' ') throw PiPedalArgumentException("Unsafe file name."); if (c == '+') { s << ' '; } else if (c == '%') { if (i + 2 > name.length()) { throw PiPedalArgumentException("Unsafe file name."); } int v1 = fromhexStrict(name[i]); int v2 = fromhexStrict(name[i + 1]); i += 2; if (v1 == -1 || v2 == -1) throw PiPedalArgumentException("Unsafe file name."); char c = (char)((v1 << 4) + v2); if (isSafeCharacter(c)) { // name won't round-trip because the escape will not be re-encoded. throw PiPedalArgumentException("Unsafe file name."); } s << c; } else { if (isSafeCharacter(c)) { s << c; } else { throw PiPedalArgumentException("Unsafe file name."); } } } return s.str(); } static const char* hex = "0123456789ABCDEF"; std::string Storage::SafeEncodeName(const std::string& name) { std::stringstream s; for (char c : name) { if (c < ' ') c = ' '; bool safe = isSafeCharacter(c); if (safe) { s << c; } else if (c == ' ') { s << '+'; } else { s << '%' << hex[(c >> 4) & 0x0F] << hex[(c) & 0x0F]; } } return s.str(); } std::filesystem::path ResolveHomePath(const std::filesystem::path& path) { if (path.begin() == path.end()) return path; // is the first element "~"? auto it = path.begin(); auto el = (*it); if (el != "~") return path; const char* homeDirectory = nullptr; homeDirectory = getenv("HOME"); if (homeDirectory == nullptr) { homeDirectory = getenv("USERPROFILE"); } if (homeDirectory == nullptr) { return path; } std::filesystem::path result = homeDirectory; bool first = true; for (auto i = path.begin(); i != path.end(); ++i) { if (!first) { result /= *i; } first = false; } return result; } void Storage::SetConfigRoot(const std::filesystem::path& path) { this->configRoot = ResolveHomePath(path); } void Storage::SetDataRoot(const std::filesystem::path& path) { this->dataRoot = ResolveHomePath(path); this->dataRoot__audio_uploads = dataRoot / "audio_uploads"; } const std::filesystem::path& Storage::GetConfigRoot() const { return this->configRoot; } const std::filesystem::path& Storage::GetDataRoot() const { return this->dataRoot; } static void CopyDirectory(const std::filesystem::path& source, const std::filesystem::path& destination) { for (auto& directoryEntry : std::filesystem::directory_iterator(source)) { if (!IsValidUtf8(directoryEntry.path().string())) { // skip invalid UTF-8 paths. Lv2Log::warning("Skipping invalid UTF-8 path: %s", directoryEntry.path().string().c_str()); continue; } if (directoryEntry.is_regular_file()) { std::filesystem::path sourceFile = directoryEntry.path(); std::filesystem::path destFile = destination / sourceFile.filename(); std::filesystem::copy_file(sourceFile, destFile); std::ignore = chmod(destFile.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); } } } static void removeFileNoThrow(const std::filesystem::path& path) { try { fs::remove(path); } catch (const std::exception&) { } } int32_t Storage::GetInstallerPresetVersion() { auto installerConfiDirectory = this->configRoot / "default_presets" / "presets"; BrowserFilesVersionInfo versionInfo; if (!versionInfo.Load(installerConfiDirectory / "banks.versionInfo")) { throw std::runtime_error("Can't open Installer banks.versionInfo file."); } return versionInfo.Version(); } int32_t Storage::GetVarPresetVersion() { auto presetsDirectory = this->GetPresetsDirectory(); BrowserFilesVersionInfo versionInfo; versionInfo.Load(presetsDirectory / "banks.versionInfo"); return versionInfo.Version(); } void Storage::SetVarPresetVersion(int32_t version) { auto presetsDirectory = this->GetPresetsDirectory(); BrowserFilesVersionInfo versionInfo; versionInfo.Load(presetsDirectory / "banks.versionInfo"); versionInfo.Version(version); versionInfo.Save(presetsDirectory / "banks.versionInfo"); } void Storage::MoveExistingFactoryPresetsBank() { BankIndexEntry* entry = bankIndex.getEntryByName("Factory Presets"); if (entry) { std::string newName = "Old Factory Presets"; int32_t nextVersionNumber = 2; while (true) { if (bankIndex.getEntryByName(newName) == nullptr) { break; } newName = SS("Old Factory Presets (" << nextVersionNumber << ")"); } RenameBank(entry->instanceId(), newName); } } namespace pipedal::implementation { extern int64_t ImportBankFile(PiPedalModel &model, const std::filesystem::path& filePath,uint64_t uploadAfter = -1); } void Storage::InstallFactoryPresets() { fs::path defaultBankFilePath = this->configRoot / "default_presets" / "presets" / "Factory Presets.piBank"; try { ImportBankFile(*this->model,defaultBankFilePath, -1); } catch (const std::exception &e) { throw std::runtime_error(SS("Failed to install Factory Presets. " << e.what())); } } void Storage::CopyFactoryPresetsToDefaultBank() { Lv2Log::info("Creating Default Bank"); auto defaultBankEntry = bankIndex.getEntryByName("Default Bank"); if (defaultBankEntry != nullptr) { throw std::runtime_error("Internal error: CopyFactoryPresetsToDefaultBank: Default Bank already exists."); } fs::path factoryPresetPath = GetBankFileName("Factory Presets"); fs::path defaultBankPath = GetBankFileName("Default Bank"); if (!fs::exists(factoryPresetPath)) { throw std::runtime_error("Internal error: CopyFactoryPresetsToDefaultBank: Factory Presets file does not exist."); } auto instanceId = bankIndex.addBank(-2,"Default Bank"); fs::copy(factoryPresetPath, defaultBankPath); bankIndex.selectedBank(instanceId); SaveBankIndex(); } void Storage::ProvisionDefaultBanks() { fs::path indexFilename = this->GetIndexFileName(); this->bankIndex = BankIndex(); bool freshInstall = false; bool installFactoryPresets = false; int32_t etcVersion = GetInstallerPresetVersion(); int32_t varPresetVersion = GetVarPresetVersion(); if (!std::filesystem::exists(indexFilename)) { freshInstall = true; installFactoryPresets = true; } else { freshInstall = false; std::ifstream s; s.open(indexFilename); json_reader reader(s); reader.read(&(this->bankIndex)); } // Compare the Factory Preset verion in the /var directory with the one in the /etc directory, // to decide whether the Factory bank needs to be provisions. auto varPresetsDirectory = this->GetPresetsDirectory(); auto etcPresetsDirector = this->configRoot / "default_presets" / "presets"; constexpr uint32_t V2_VERSION_NUMBER = 2; // first to have a Factory Presets constexpr uint32_t V3_VERSION_NUMBER = 3; // New A2 Presets, completely replacing the previons version. if (varPresetVersion < V3_VERSION_NUMBER) { Lv2Log::info("Installing Factory Presets"); // Move the old factory presets bank out of the way. MoveExistingFactoryPresetsBank(); InstallFactoryPresets(); SetVarPresetVersion(V3_VERSION_NUMBER); SaveBankIndex(); // checkpoint. } // Next time we will have to MERGE factor presets. if (freshInstall) { CopyFactoryPresetsToDefaultBank(); SetVarPresetVersion(V3_VERSION_NUMBER); } } void Storage::UpgradeV1FactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); auto presetsConfigDirectory = this->configRoot / "default_presets" / "presets"; using namespace ::pipedal::implementation; BrowserFilesVersionInfo defaultConfigPresetsVersion; fs::path defaultConfigPresetsVersionFile = presetsConfigDirectory / "banks.versionInfo"; defaultConfigPresetsVersion.Load(defaultConfigPresetsVersionFile); BrowserFilesVersionInfo presetsVersion; fs::path defaultPresetsVersionFile = presetsDirectory / "banks.versionInfo"; presetsVersion.Load(defaultPresetsVersionFile); constexpr uint32_t V1_VERSION_NUMBER = 1; // set the default banks.versionInfo value to 0 to force complete upgrade path bool testUpgrade = (defaultConfigPresetsVersion.Version() == 0); // Maybe install or upgrade Version 1 factory presets. if ((V1_VERSION_NUMBER > presetsVersion.Version() && presetsVersion.Version() != 0) || testUpgrade) { // Forces TooBMl to re-install V1 factory models (?) removeFileNoThrow("/usr/lib/lv2/ToobAmp.lv2/models/tones/README.md"); removeFileNoThrow("/var/pipedal/audio_uploads/ToobMlModels/model.index"); removeFileNoThrow("/var/pipedal/audio_uploads/ToobMlModels/README.md"); removeFileNoThrow("/var/pipedal/audio_uploads/ToobMlModels/model.index"); // Do NOT upgrade to v1 Factory Presets. #if 0 { std::string name = "Factory Presets"; BankFile newFactoryPresets; { fs::path defaultBankPath = presetsConfigDirectory / "Default+Bank.bank"; try { std::ifstream is(defaultBankPath); json_reader reader(is); reader.read(&newFactoryPresets); } catch (const std::exception& e) { Lv2Log::error(SS("Failed to isntall factory presets. Can't read " << defaultBankPath << ".")); } } newFactoryPresets.name("Factory Presets"); BankIndexEntry* existingEntry = bankIndex.getEntryByName(name); if (existingEntry == nullptr) { BankFile bankFile; bankFile.name(name); for (auto& presetEntry : newFactoryPresets.presets()) { bankFile.addPreset(presetEntry->preset(), -1); } int64_t instanceId = bankFile.presets()[0]->instanceId(); bankFile.selectedPreset(instanceId); SaveBankFile(name, bankFile); this->bankIndex.addBank(-1, name); this->SaveBankIndex(); } else { // either use the current bank (if the factory bank is selected), or create a new one. BankFile bankFile; bankFile.name(name); BankFile* pFactoryPresetsBank = nullptr; bool usingCurrentBank = false; if (bankIndex.selectedBank() == existingEntry->instanceId()) { usingCurrentBank = true; pFactoryPresetsBank = &(this->currentBank); } else { LoadBankFile(name, &bankFile); pFactoryPresetsBank = &bankFile; } // index existing presets. std::unordered_map nameToPositionIndex; for (size_t i = 0; i < pFactoryPresetsBank->presets().size(); ++i) { auto& preset = pFactoryPresetsBank->presets()[i]; nameToPositionIndex[preset->preset().name()] = i; } // merge new presets into the existing ones (overwriting as neccessary) for (auto& newPresetEntry : newFactoryPresets.presets()) { const std::string name = newPresetEntry->preset().name(); auto f = nameToPositionIndex.find(name); if (f != nameToPositionIndex.end()) { size_t postition = f->second; // overwrite the existing entry. pFactoryPresetsBank->presets()[postition]->preset(newPresetEntry->preset()); } else { pFactoryPresetsBank->addPreset(newPresetEntry->preset()); } } SaveBankFile(name, *pFactoryPresetsBank); } } #endif presetsVersion.Version(2); presetsVersion.Save(defaultPresetsVersionFile); } } void Storage::Initialize(PiPedalModel *model) { this->model = model; try { std::filesystem::create_directories(this->GetPresetsDirectory()); std::filesystem::create_directories(this->GetPluginPresetsDirectory()); } catch (const std::exception& e) { throw PiPedalStateException("Can't create presets directory. (" + (std::string)this->GetPresetsDirectory() + ") (" + e.what() + ")"); } ProvisionDefaultBanks(); LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); try { LoadJackChannelSelection(); } catch (const std::exception&) { } LoadAlsaSequencerConfiguration(); LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); this->channelRouterSettings = LoadChannelRouterSettings(); LoadUserSettings(); UpgradeChannelRouterSettings(); } void Storage::LoadBank(int64_t instanceId) { auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId); try { LoadBankFile(indexEntry.name(), &(this->currentBank)); if (this->bankIndex.selectedBank() != instanceId) { this->bankIndex.selectedBank(instanceId); SaveBankIndex(); } this->LoadPreset(this->currentBank.selectedPreset()); } catch (const std::exception& e) { throw std::logic_error(SS("Bank file corrupted. " << e.what() << "(" << GetBankFileName(indexEntry.name()) << ")")); } } void Storage::LoadCurrentBank() { LoadBank(bankIndex.selectedBank()); } std::filesystem::path Storage::GetPresetsDirectory() const { return this->dataRoot / "presets"; } std::filesystem::path Storage::GetPluginPresetsDirectory() const { return this->dataRoot / "plugin_presets"; } const std::filesystem::path& Storage::GetPluginUploadDirectory() const { return dataRoot__audio_uploads; } std::filesystem::path Storage::GetCurrentPresetPath() const { return this->dataRoot / "currentPreset.json"; } std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; } std::filesystem::path Storage::GetAlsaSequencerConfigurationFileName() { return this->dataRoot / "MidiDevices.json"; } std::filesystem::path Storage::GetIndexFileName() const { return this->GetPresetsDirectory() / BANKS_FILENAME; } std::filesystem::path Storage::GetBankFileName(const std::string& name) const { std::string fileName = SafeEncodeName(name) + BANK_EXTENSION; return this->GetPresetsDirectory() / fileName; } void Storage::LoadBankIndex() { try { auto path = GetIndexFileName(); std::ifstream s; s.open(path); json_reader reader(s); reader.read(&bankIndex); } catch (const std::exception&) { bankIndex.clear(); ReIndex(); if (bankIndex.entries().size() == 0) { currentBank.clear(); Pedalboard defaultPedalboard = Pedalboard::MakeDefault(); int64_t instanceId = currentBank.addPreset(defaultPedalboard); currentBank.selectedPreset(instanceId); std::string name = "Default Bank"; SaveBankFile(name, currentBank); int64_t selectedBank = bankIndex.addBank(-1, name); bankIndex.selectedBank(selectedBank); } SaveBankIndex(); } } void Storage::LoadPluginPresetIndex() { try { auto path = GetPluginPresetsDirectory() / "index.json"; std::ifstream s; if (std::filesystem::exists(path)) { s.open(path); json_reader reader(s); reader.read(&pluginPresetIndex); } } catch (const std::exception&) { } } void Storage::SavePluginPresetIndex() { if (pluginPresetIndexChanged) { pluginPresetIndexChanged = false; pipedal::ofstream_synced os; auto path = GetPluginPresetsDirectory() / "index.json"; os.open(path, std::ios_base::trunc); if (os.fail()) { throw PiPedalException(SS("Can't write to " << path)); } json_writer writer(os, true); writer.write(this->pluginPresetIndex); } } void Storage::SaveBankIndex() { pipedal::ofstream_synced os; os.open(GetIndexFileName(), std::ios_base::trunc); json_writer writer(os, true); writer.write(this->bankIndex); } void Storage::ReIndex() { for (const auto& dirEntry : std::filesystem::directory_iterator(GetPresetsDirectory())) { if (!IsValidUtf8(dirEntry.path().string())) { // skip invalid UTF-8 paths. Lv2Log::warning("Skipping invalid UTF-8 path: %s", dirEntry.path().string().c_str()); continue; } if (!dirEntry.is_directory()) { auto path = dirEntry.path(); if (path.extension() == BANK_EXTENSION) { std::string name = SafeDecodeName(path.stem()); bankIndex.addBank(-1, name); } } } if (bankIndex.entries().size() == 0) { CreateBank("Default Bank"); } bankIndex.selectedBank(bankIndex.entries()[0].instanceId()); } void Storage::CreateBank(const std::string& name) { BankFile bankFile; Pedalboard defaultPreset = Pedalboard::MakeDefault(); defaultPreset.name(std::string("Default Preset")); bankFile.addPreset(defaultPreset); int64_t instanceId = bankFile.presets()[0]->instanceId(); bankFile.selectedPreset(instanceId); SaveBankFile(name, bankFile); this->bankIndex.addBank(-1, name); this->SaveBankIndex(); } void Storage::GetBankFile(int64_t instanceId, BankFile* pBank) const { auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId); auto name = indexEntry.name(); std::filesystem::path fileName = GetBankFileName(name); std::ifstream is(fileName); json_reader reader(is); reader.read(pBank); pBank->name(indexEntry.name()); } void Storage::LoadBankFile(const std::string& name, BankFile* pBank) { std::filesystem::path fileName = GetBankFileName(name); std::ifstream is(fileName); json_reader reader(is); reader.read(pBank); } void Storage::SaveBankFile(const std::string& name, const BankFile& bankFile) { std::filesystem::path fileName = GetBankFileName(name); std::filesystem::path backupFile = ((std::string)fileName) + ".$$$"; if (std::filesystem::exists(backupFile)) { std::filesystem::remove(backupFile); } if (std::filesystem::exists(fileName)) { std::filesystem::rename(fileName, backupFile); } try { pipedal::ofstream_synced s; s.open(fileName, std::ios_base::trunc); json_writer writer(s, true); writer.write(bankFile); if (std::filesystem::exists(backupFile)) { std::filesystem::remove(backupFile); } } catch (const std::exception& e) { std::filesystem::remove(fileName); if (std::filesystem::exists(backupFile)) { std::filesystem::rename(backupFile, fileName); } throw; } } void Storage::SaveCurrentBank() { auto indexEntry = this->bankIndex.getBankIndexEntry(this->bankIndex.selectedBank()); SaveBankFile(indexEntry.name(), this->currentBank); } const Pedalboard& Storage::GetCurrentPreset() { auto& item = currentBank.getItem(currentBank.selectedPreset()); return item.preset(); } bool Storage::LoadPreset(int64_t instanceId) { if (!currentBank.hasItem(instanceId)) return false; if (instanceId != currentBank.selectedPreset()) { currentBank.selectedPreset(instanceId); SaveCurrentBank(); } return true; } void Storage::SaveCurrentPreset(const Pedalboard& pedalboard) { auto& item = currentBank.getItem(currentBank.selectedPreset()); item.preset(pedalboard); SaveCurrentBank(); } int64_t Storage::SaveCurrentPresetAs(const Pedalboard& pedalboard, int64_t bankInstanceId, const std::string& name, int64_t saveAfterInstanceId) { Pedalboard newPedalboard = pedalboard; newPedalboard.name(name); if (bankInstanceId == this->bankIndex.selectedBank()) { int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); currentBank.selectedPreset(newInstanceId); SaveCurrentBank(); return newInstanceId; } else { auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); try { BankFile bankFile; LoadBankFile(indexEntry.name(), &(bankFile)); int64_t newInstanceId = bankFile.addPreset(newPedalboard, -1); SaveBankFile(indexEntry.name(), bankFile); return -1; } catch (const std::exception& e) { throw std::logic_error(SS("Bank file corrupted. " << e.what() << "(" << GetBankFileName(indexEntry.name()) << ")")); } } } static std::string stripNumericSuffix(const std::string& name) { // remove (digit*) from the end of the string. // Find the last '(' character size_t pos = name.find_last_of('('); if (pos == std::string::npos) { return name; } // Check if everything between '(' and ')' is digits size_t len = name.length() - 1; if (name[len] != ')') { return name; } bool allDigits = true; for (size_t i = pos + 1; i < len - 1; ++i) { if (name[i] < '0' || name[i] > '9') { allDigits = false; break; } } if (!allDigits) { return name; } // Remove trailing spaces before the '(' while (pos > 0 && name[pos - 1] == ' ') { --pos; } return name.substr(0, pos); } static std::string makeUniqueName(const std::string& name, const std::set& existingNames) { if (!existingNames.contains(name)) { return name; } std::string baseName = stripNumericSuffix(name); size_t i = 2; while (true) { std::string newName = SS(baseName << " (" << i << ")"); if (!existingNames.contains(newName)) { return newName; } ++i; } } int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector& presets) { if (bankIndex.selectedBank() == bankInstanceId) { throw std::runtime_error("Can't import to self."); } std::set presetsSet{ presets.begin(), presets.end() }; auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); std::set existingNames; for (auto& preset : this->currentBank.presets()) { existingNames.insert(preset->preset().name()); } BankFile bankFile; LoadBankFile(indexEntry.name(), &bankFile); int64_t lastPresetId = -1; for (auto& presetEntry : bankFile.presets()) { if (presetsSet.contains(presetEntry->instanceId())) { std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); lastPresetId = this->currentBank.addPreset(t); } } SaveCurrentBank(); return lastPresetId; } int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector& presets) { if (bankIndex.selectedBank() == bankInstanceId) { throw std::runtime_error("Can't copy to self."); } auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); BankFile bankFile; LoadBankFile(indexEntry.name(), &bankFile); std::set presetsSet{ presets.begin(), presets.end() }; std::set existingNames; for (auto& preset : bankFile.presets()) { existingNames.insert(preset->preset().name()); } for (auto& presetEntry : this->currentBank.presets()) { if (presetsSet.contains(presetEntry->instanceId())) { std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); bankFile.addPreset(t); } } SaveBankFile(indexEntry.name(), bankFile); return -1; } std::vector Storage::RequestBankPresets(int64_t bankInstanceId) { auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); std::vector result; BankFile bankFile; LoadBankFile(indexEntry.name(), &bankFile); for (auto& preset : bankFile.presets()) { result.push_back( PresetIndexEntry(preset->instanceId(), preset->preset().name())); } return result; } void Storage::SetPresetIndex(const PresetIndex& presets) { // painful because we must move unique_ptrs. std::map*> entries; for (int i = 0; i < this->currentBank.presets().size(); ++i) { auto& preset = this->currentBank.presets()[i]; entries[preset->instanceId()] = &(this->currentBank.presets()[i]); } std::vector*> newPresets; for (size_t i = 0; i < presets.presets().size(); ++i) { std::unique_ptr* p = entries[presets.presets()[i].instanceId()]; if (p == nullptr) { throw PiPedalStateException("Presets do not match the currently loaded bank."); } newPresets.push_back(p); } // we made it this far. So now we know we can rebuild a new BankFile. BankFile bankFile; bankFile.presets().resize(newPresets.size()); for (int i = 0; i < presets.presets().size(); ++i) { std::unique_ptr* oldBankFilePreset = newPresets[i]; bankFile.presets()[i] = std::move((*oldBankFilePreset)); } bankFile.nextInstanceId(currentBank.nextInstanceId()); bankFile.selectedPreset(currentBank.selectedPreset()); this->currentBank = std::move(bankFile); // deleting any stray presets while we're at it. this->SaveCurrentBank(); } void Storage::GetPresetIndex(PresetIndex* pResult) { *pResult = PresetIndex(); pResult->selectedInstanceId(this->currentBank.selectedPreset()); for (auto& item : this->currentBank.presets()) { PresetIndexEntry entry; entry.instanceId(item->instanceId()); entry.name(item->preset().name()); pResult->presets().push_back(entry); } } int64_t Storage::GetPresetByProgramNumber(uint8_t program) const { if (program >= currentBank.presets().size()) { if (currentBank.presets().size() == 0) return -1; program = (uint8_t)currentBank.presets().size() - 1; } return currentBank.presets()[program]->instanceId(); } Pedalboard Storage::GetPreset(int64_t instanceId) const { for (size_t i = 0; i < currentBank.presets().size(); ++i) { if (currentBank.presets()[i]->instanceId() == instanceId) { return currentBank.presets()[i]->preset(); } } throw PiPedalException("Not found."); } int64_t Storage::DeletePresets(const std::vector& presetInstanceIds) { int64_t newSelection = currentBank.selectedPreset(); for (auto presetId : presetInstanceIds) { newSelection = currentBank.deletePreset(presetId); } SaveCurrentBank(); return newSelection; } bool Storage::RenamePreset(int64_t presetId, const std::string& name) { if (this->currentBank.renamePreset(presetId, name)) { SaveCurrentBank(); return true; } return false; } static int lastIndexOf(const char* sz, char c) { size_t len = strlen(sz); for (size_t i = len - 1; i >= 0; --i) { if (sz[i] == c) { if (i > 0 && sz[i - 1] == ' ') --i; return i; } } return -1; } std::string Storage::GetPresetCopyName(const std::string& name) { const char* str = name.c_str(); std::string stem; if (name.length() != 0 && name[name.length() - 1] == ')') { const char* str = name.c_str(); size_t pos = lastIndexOf(str, '('); if (pos == -1) { stem = name; } else { stem = name.substr(0, pos); } } else { stem = name; } int copyNumber = 1; while (true) { std::string candidate; if (copyNumber == 1) { candidate = stem + " (Copy)"; } else { std::stringstream s; s << stem << " (Copy " << copyNumber << ")"; candidate = s.str(); } if (!this->currentBank.hasName(candidate)) { return candidate; } ++copyNumber; } } int64_t Storage::CreateNewPreset() { Pedalboard newPedalboard = Pedalboard::MakeDefault(); std::string name = "New"; if (currentBank.hasName(name)) { int copyNumber = 2; while (true) { name = SS("New (" << copyNumber << ")"); if (!currentBank.hasName(name)) { break; } ++copyNumber; } } newPedalboard.name(name); auto t = this->currentBank.addPreset(newPedalboard, -1); SaveCurrentBank(); return t; } int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto& fromItem = this->currentBank.getItem(fromId); int64_t result; if (toId == -1) { Pedalboard newPedalboard = fromItem.preset(); std::string name = GetPresetCopyName(fromItem.preset().name()); newPedalboard.name(name); result = this->currentBank.addPreset(newPedalboard, fromId); } else { auto& toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); result = toId; } SaveCurrentBank(); return result; } void Storage::SetJackChannelSelection(const JackChannelSelection& channelSelection) { this->jackChannelSelection = channelSelection; this->isJackChannelSelectionValid = true; SaveChannelSelection(); } JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration& jackConfiguration) { if (!this->isJackChannelSelectionValid) { if (jackConfiguration.isValid()) { auto defaultSelection = JackChannelSelection::MakeDefault(jackConfiguration); SetJackChannelSelection(defaultSelection); } } return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); } static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector& selectedDevices) { AlsaSequencerConfiguration result; try { auto sequencerPorts = AlsaSequencer::EnumeratePorts(); // Prepare Migrate raw MIDI devices to ALSA sequencer ports. for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i) { if (i->name_.starts_with("hw:")) { for (const auto& port : sequencerPorts) { if (i->name_ == port.rawMidiDevice) { result.connections().push_back( AlsaSequencerPortSelection(port.id, port.name, port.displaySortOrder)); break; } } } } } catch (const std::exception& e) { Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what())); // ick. } return result; } void Storage::LoadAlsaSequencerConfiguration() { auto fileName = this->GetAlsaSequencerConfigurationFileName(); if (std::filesystem::exists(fileName)) { try { std::ifstream s(fileName); json_reader reader(s); AlsaSequencerConfiguration result; reader.read(&result); this->alsaSequencerConfiguration = result; } catch (const std::exception& e) { Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); } } else { // migrate legacy settings from JackConfiguration? if (this->isJackChannelSelectionValid) { this->alsaSequencerConfiguration = MigrateRawMidiToAlsaSequencer( this->jackChannelSelection.LegacyGetInputMidiDevices()); this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated. } else { // no legacy settings, so just create a default configuration. this->alsaSequencerConfiguration = AlsaSequencerConfiguration(); } SaveAlsaSequencerConfiguration(); } } void Storage::SaveAlsaSequencerConfiguration() { auto fileName = this->GetAlsaSequencerConfigurationFileName(); try { pipedal::ofstream_synced s(fileName); json_writer writer(s); writer.write(this->alsaSequencerConfiguration); } catch (const std::exception& e) { Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); } } void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration& alsaSequencerConfiguration) { this->alsaSequencerConfiguration = alsaSequencerConfiguration; SaveAlsaSequencerConfiguration(); } AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const { return this->alsaSequencerConfiguration; } void Storage::LoadJackChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); if (std::filesystem::exists(fileName)) { try { std::ifstream s(fileName); json_reader reader(s); reader.read(&this->jackChannelSelection); this->isJackChannelSelectionValid = true; } catch (const std::exception& e) { Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); } } } void Storage::SaveChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); try { // replaced with AlsaSequencerConfiguration. Delete legacy data. this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); pipedal::ofstream_synced s(fileName); json_writer writer(s, false); writer.write(this->jackChannelSelection); } catch (const std::exception& e) { Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); throw PiPedalStateException("Unexpected error writing Jack settings file."); } } void Storage::RenameBank(int64_t bankId, const std::string& newName) { auto existingBank = this->bankIndex.getEntryByName(newName); if (existingBank != nullptr) { throw PiPedalStateException("A bank by that name already exists."); } auto& entry = this->bankIndex.getBankIndexEntry(bankId); std::filesystem::path oldPath = this->GetBankFileName(entry.name()); std::filesystem::path newPath = this->GetBankFileName(newName); try { std::filesystem::rename(oldPath, newPath); } catch (std::exception& e) { std::stringstream s; s << "Unable to rename the bank. (" << e.what() << ")"; throw PiPedalException(s.str()); } entry.name(newName); SaveBankIndex(); } int64_t Storage::SaveBankAs(int64_t bankId, const std::string& newName) { auto existingBank = this->bankIndex.getEntryByName(newName); if (existingBank != nullptr) { throw PiPedalStateException("A bank by that name already exists."); } auto& entry = this->bankIndex.getBankIndexEntry(bankId); std::filesystem::path oldPath = this->GetBankFileName(entry.name()); std::filesystem::path newPath = this->GetBankFileName(newName); try { std::filesystem::copy(oldPath, newPath); } catch (std::exception& e) { std::stringstream s; s << "Unable to save the bank. (" << e.what() << ")"; throw PiPedalException(s.str()); } int64_t newId = this->bankIndex.addBank(bankId, newName); SaveBankIndex(); return newId; } void Storage::MoveBank(int from, int to) { this->bankIndex.move(from, to); this->SaveBankIndex(); } int64_t Storage::GetBankByMidiBankNumber(uint8_t bankNumber) { auto& entries = this->bankIndex.entries(); if (bankNumber >= entries.size()) { if (entries.size() == 0) return -1; bankNumber = (uint8_t)(entries.size() - 1); } return entries[bankNumber].instanceId(); } int64_t Storage::DeleteBank(int64_t bankId) { auto& entries = this->bankIndex.entries(); for (size_t i = 0; i < entries.size(); ++i) { auto& entry = entries[i]; if (entry.instanceId() == bankId) { std::filesystem::path fileName = this->GetBankFileName(entry.name()); entries.erase(entries.begin() + i); int64_t newSelection; if (i < entries.size()) { newSelection = entries[i].instanceId(); } else if (entries.size() > 0) { newSelection = entries[entries.size() - 1].instanceId(); } else { // zero entries? // Create a default empty bank. BankIndexEntry newEntry; BankFile defaultBank; Pedalboard defaultPedalboard = Pedalboard::MakeDefault(); int64_t instanceId = defaultBank.addPreset(defaultPedalboard); defaultBank.selectedPreset(instanceId); std::string name = "Default Bank"; SaveBankFile(name, defaultBank); int64_t selectedBank = bankIndex.addBank(-1, name); newSelection = selectedBank; } if (this->bankIndex.selectedBank() == bankId) { this->bankIndex.selectedBank(newSelection); } this->SaveBankIndex(); std::filesystem::remove(fileName); return newSelection; } } throw PiPedalStateException("Bank not found."); } int64_t Storage::GetCurrentPresetId() const { return this->currentBank.selectedPreset(); } int64_t Storage::UploadPreset(const BankFile& bankFile, int64_t uploadAfter) { int64_t lastPreset = this->currentBank.selectedPreset(); if (uploadAfter != -1) { lastPreset = uploadAfter; } if (bankFile.presets().size() == 0) { throw PiPedalException("Invalid preset."); } for (size_t i = 0; i < bankFile.presets().size(); ++i) { Pedalboard preset = bankFile.presets()[i]->preset(); int n = 2; std::string baseName = preset.name(); while (this->currentBank.hasName(preset.name())) { std::stringstream s; s << baseName << "(" << n++ << ")"; preset.name(s.str()); } lastPreset = this->currentBank.addPreset(preset, lastPreset); } this->SaveCurrentBank(); return lastPreset; } int64_t Storage::UploadBank(BankFile& bankFile, int64_t uploadAfter) { int64_t lastBank = this->bankIndex.selectedBank(); if (uploadAfter != -1) { lastBank = uploadAfter; } if (bankFile.presets().size() == 0) { throw PiPedalException("Invalid bank."); } bankFile.updateNextIndex(); int n = 2; std::string baseName = bankFile.name(); while (this->bankIndex.hasName(bankFile.name())) { std::stringstream s; s << baseName << "(" << n++ << ")"; bankFile.name(s.str()); } std::filesystem::path path = this->GetBankFileName(bankFile.name()); pipedal::ofstream_synced f(path); if (!f.is_open()) { throw PiPedalException("Can't write to bank file."); } json_writer writer(f, true); writer.write(bankFile); lastBank = this->bankIndex.addBank(lastBank, bankFile.name()); this->SaveBankIndex(); return lastBank; } void Storage::SetGovernorSettings(const std::string& governor) { userSettings.governor_ = governor; SaveUserSettings(); } void Storage::SaveUserSettings() { std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME; { pipedal::ofstream_synced f(path); if (!f.is_open()) { throw PiPedalException("Unable to write to " + ((std::string)path)); } json_writer writer(f, false); writer.write(userSettings); } } void Storage::SetShowStatusMonitor(bool show) { this->userSettings.showStatusMonitor_ = show; SaveUserSettings(); } bool Storage::GetShowStatusMonitor() const { return this->userSettings.showStatusMonitor_; } std::string Storage::GetGovernorSettings() const { return this->userSettings.governor_; } bool Storage::SetWifiConfigSettings(const WifiConfigSettings& wifiConfigSettings) { WifiConfigSettings copyToSave = wifiConfigSettings; WifiConfigSettings previousValue; previousValue.Load(); if (!copyToSave.hasPassword_) { copyToSave.hasPassword_ = previousValue.hasPassword_; copyToSave.password_ = previousValue.password_; copyToSave.hasSavedPassword_ = previousValue.hasPassword_; } else { if (copyToSave.IsEnabled()) { copyToSave.hasSavedPassword_ = copyToSave.hasPassword_; } } bool configChanged = copyToSave.ConfigurationChanged(previousValue); copyToSave.Save(); this->wifiConfigSettings = copyToSave; this->wifiConfigSettings.hasPassword_ = false; this->wifiConfigSettings.password_ = ""; return configChanged; } void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings& wifiDirectConfigSettings) { WifiDirectConfigSettings copyToSave = wifiDirectConfigSettings; copyToSave.rebootRequired_ = false; if (!copyToSave.enable_) { copyToSave.pinChanged_ = false; } WifiDirectConfigSettings copyToStore = wifiDirectConfigSettings; copyToStore.pinChanged_ = false; this->wifiDirectConfigSettings = copyToStore; } void Storage::LoadUserSettings() { std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME; try { if (std::filesystem::is_regular_file(path)) { std::ifstream f(path); if (!f.is_open()) { throw PiPedalException("Unable to read from " + ((std::string)path)); } json_reader reader(f); reader.read(&userSettings); } } catch (const std::exception&) { } this->wifiConfigSettings.valid_ = true; } void Storage::LoadWifiConfigSettings() { this->wifiConfigSettings.Load(); this->wifiConfigSettings.valid_ = this->wifiConfigSettings.hasPassword_ && !this->wifiConfigSettings.countryCode_.empty(); } void Storage::LoadWifiDirectConfigSettings() { WifiDirectConfigSettings settings; settings.enable_ = false; settings.Load(); settings.pinChanged_ = false; settings.valid_ = true; this->wifiDirectConfigSettings = settings; } WifiConfigSettings Storage::GetWifiConfigSettings() { WifiConfigSettings result = this->wifiConfigSettings; result.hasPassword_ = false; result.password_ = ""; return result; } WifiDirectConfigSettings Storage::GetWifiDirectConfigSettings() { return this->wifiDirectConfigSettings; } void Storage::SaveCurrentPreset(const CurrentPreset& currentPreset) { try { std::filesystem::path path = GetCurrentPresetPath(); pipedal::ofstream_synced f(path); json_writer writer(f, true); writer.write(currentPreset); } catch (std::exception&) { // called from destructor. Must be nothrow(). } } bool Storage::RestoreCurrentPreset(CurrentPreset* pResult) { std::filesystem::path path = GetCurrentPresetPath(); if (std::filesystem::exists(path)) { try { std::ifstream f(path); json_reader reader(f); reader.read(pResult); std::filesystem::remove(path); // one-shot only, restore the state from the last *orderly* shutdown. } catch (const std::exception& e) { Lv2Log::warning(SS("Failed to restore current preset. " << e.what())); std::filesystem::remove(path); // one-shot only, restore the state from the last *orderly* shutdown. return false; } return true; } else { return false; } } uint64_t Storage::GetPluginPresetIndexVersion() { return pluginPresetIndex.version_; } void Storage::SetPluginPresetIndexVersion(uint64_t version) { if (this->pluginPresetIndex.version_ != version) { this->pluginPresetIndexChanged = true; this->pluginPresetIndex.version_ = version; SavePluginPresetIndex(); } } bool Storage::HasPluginPresets(const std::string& pluginUri) const { for (const auto& entry : this->pluginPresetIndex.entries_) { if (entry.pluginUri_ == pluginUri) { return true; } } return false; } std::filesystem::path Storage::GetPluginPresetPath(const std::string& pluginUri) const { for (const auto& entry : this->pluginPresetIndex.entries_) { if (entry.pluginUri_ == pluginUri) { return this->GetPluginPresetsDirectory() / entry.fileName_; } } throw PiPedalArgumentException("Plugin preset file not found."); } void Storage::MergePluginPresets(const std::string& pluginUri, const PluginPresets& presets) { std::string name; std::filesystem::path path; bool presetAdded = false; PluginPresets existingPresets; if (!HasPluginPresets(pluginUri)) { name = SS(pluginPresetIndex.nextInstanceId_ << ".json"); path = GetPluginPresetsDirectory() / name; presetAdded = true; } else { path = GetPluginPresetPath(pluginUri); try { std::ifstream f(path); if (f.is_open()) { json_reader reader(f); reader.read(&existingPresets); } } catch (const std::exception& e) { Lv2Log::error(SS("Storage::MergePluginPresets: Can't reading existing plugin presets." << e.what())); } } for (const PluginPreset& preset : presets.presets_) { existingPresets.MergePreset(preset); } auto tempPath = path.string() + ".$$$"; { pipedal::ofstream_synced os; os.open(tempPath, std::ios_base::trunc); if (os.fail()) { throw PiPedalException(SS("Can't write to " << path)); } json_writer writer(os, true); writer.write(existingPresets); } if (std::filesystem::exists(path)) { std::filesystem::remove(path); } std::filesystem::rename(tempPath, path); if (presetAdded) { pluginPresetIndex.entries_.push_back( PluginPresetIndexEntry( pluginUri, name)); pluginPresetIndex.nextInstanceId_++; this->pluginPresetIndexChanged = true; SavePluginPresetIndex(); } } void Storage::SavePluginPresets(const std::string& pluginUri, const PluginPresets& presets) { std::string name; std::filesystem::path path; bool presetAdded = false; if (!HasPluginPresets(pluginUri)) { name = SS(pluginPresetIndex.nextInstanceId_ << ".json"); path = GetPluginPresetsDirectory() / name; presetAdded = true; } else { path = GetPluginPresetPath(pluginUri); } auto tempPath = path.string() + ".$$$"; { pipedal::ofstream_synced os; os.open(tempPath, std::ios_base::trunc); if (os.fail()) { throw PiPedalException(SS("Can't write to " << path)); } json_writer writer(os, true); writer.write(presets); } if (std::filesystem::exists(path)) { std::filesystem::remove(path); } std::filesystem::rename(tempPath, path); if (presetAdded) { pluginPresetIndex.entries_.push_back( PluginPresetIndexEntry( pluginUri, name)); pluginPresetIndex.nextInstanceId_++; this->pluginPresetIndexChanged = true; SavePluginPresetIndex(); } } PluginPresets Storage::GetPluginPresets(const std::string& pluginUri) const { PluginPresets result; if (!HasPluginPresets(pluginUri)) { result.pluginUri_ = pluginUri; return result; } std::filesystem::path path = GetPluginPresetPath(pluginUri); std::ifstream s; s.open(path); if (s.fail()) { return result; } json_reader reader(s); reader.read(&result); return result; } PluginUiPresets Storage::GetPluginUiPresets(const std::string& pluginUri) const { PluginPresets presets = GetPluginPresets(pluginUri); PluginUiPresets result; result.pluginUri_ = presets.pluginUri_; for (size_t i = 0; i < presets.presets_.size(); ++i) { const auto& preset = presets.presets_[i]; result.presets_.push_back( PluginUiPreset{ preset.label_, preset.instanceId_ }); } return result; } PluginPresetValues Storage::GetPluginPresetValues(const std::string& pluginUri, uint64_t instanceId) { auto presets = GetPluginPresets(pluginUri); for (const auto& preset : presets.presets_) { if (preset.instanceId_ == instanceId) { PluginPresetValues result; for (const auto& valuePair : preset.controlValues_) { result.controls.push_back(ControlValue(valuePair.first.c_str(), valuePair.second)); } for (const auto& pair : preset.pathProperties_) { result.pathProperties[pair.first] = pair.second; } result.state = preset.state_; result.lilvPresetUri = preset.lilvPresetUri_; return result; } } throw PiPedalException("Plugin preset not found."); } void Storage::ToAbstractPaths(PluginPreset& pluginPreset) { if (pluginPreset.pathProperties_.size() != 0) { std::map newPathProperties; for (const auto& pair : pluginPreset.pathProperties_) { newPathProperties[pair.first] = this->ToAbstractPathFromJson(pair.second); } pluginPreset.pathProperties_ = newPathProperties; } } void Storage::FromAbstractPaths(PluginPreset& pluginPreset) { if (pluginPreset.pathProperties_.size() != 0) { std::map newPathProperties; for (const auto& pair : pluginPreset.pathProperties_) { newPathProperties[pair.first] = this->FromAbstractPathJson(pair.second); } pluginPreset.pathProperties_ = newPathProperties; } } uint64_t Storage::SavePluginPreset( const std::string& name, const PedalboardItem& pedalboardItem) { const std::string& pluginUri = pedalboardItem.uri(); auto presets = GetPluginPresets(pluginUri); uint64_t result = -1; bool existing = false; for (size_t i = 0; i < presets.presets_.size(); ++i) { auto& preset = presets.presets_[i]; if (preset.label_ == name) { existing = true; result = preset.instanceId_; PluginPreset pluginPreset{ preset.instanceId_, preset.label_, pedalboardItem }; presets.presets_[i] = PluginPreset(preset.instanceId_, preset.label_, pedalboardItem); break; } } if (!existing) { result = presets.nextInstanceId_++; auto preset = PluginPreset( result, name, pedalboardItem); ToAbstractPaths(preset); presets.presets_.push_back(preset); } this->SavePluginPresets(pluginUri, presets); return result; } uint64_t Storage::SavePluginPreset( const std::string& pluginUri, PluginPreset& pluginPreset) { auto presets = GetPluginPresets(pluginUri); uint64_t result = -1; bool existing = false; for (size_t i = 0; i < presets.presets_.size(); ++i) { auto& preset = presets.presets_[i]; if (preset.label_ == pluginPreset.label_) { existing = true; auto t = preset.instanceId_; preset = pluginPreset; preset.instanceId_ = t; result = preset.instanceId_; break; } } if (!existing) { result = presets.nextInstanceId_++; pluginPreset.instanceId_ = result; presets.presets_.push_back( pluginPreset); } this->SavePluginPresets(pluginUri, presets); return result; } void Storage::UpdatePluginPresets(const PluginUiPresets& pluginPresets) { // handles deletions, renaming, and reordering only. // If you need to add a preset, you need to call SavePluginPreset or DuplicatePluginPreset instead. PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_); PluginPresets newPresets; newPresets.pluginUri_ = pluginPresets.pluginUri_; newPresets.nextInstanceId_ = presets.nextInstanceId_; for (const auto& preset : pluginPresets.presets_) { PluginPreset newPreset = presets.GetPreset(preset.instanceId_); newPreset.label_ = preset.label_; newPresets.presets_.push_back(std::move(newPreset)); } SavePluginPresets(newPresets.pluginUri_, newPresets); } static std::string stripCopySuffix(const std::string& s) { int pos = s.length() - 1; if (pos >= 0 && s[pos] == ')') { --pos; while (pos >= 0 && s[pos] >= '0' && s[pos] <= '9') { --pos; } if (pos >= 0 && s[pos] == '(') { --pos; } while (pos >= 0 && s[pos] == ' ') { --pos; } return s.substr(0, pos + 1); } return s; } uint64_t Storage::CopyPluginPreset(const std::string& pluginUri, uint64_t presetId) { PluginPresets presets = this->GetPluginPresets(pluginUri); size_t pos = presets.Find(presetId); if (pos == -1) { throw PiPedalException("Preset not found."); } PluginPreset t = presets.presets_[pos]; t.instanceId_ = presets.nextInstanceId_++; std::string baseName = stripCopySuffix(t.label_); std::string name = baseName; if (presets.Find(name) != -1) { int nCopy = 1; while (true) { name = SS(baseName << " (" << nCopy << ")"); if (presets.Find(name) == -1) { break; } ++nCopy; } } t.label_ = name; presets.presets_.insert(presets.presets_.begin() + pos + 1, t); SavePluginPresets(presets.pluginUri_, presets); return t.instanceId_; } std::map Storage::GetFavorites() const { std::map result; std::filesystem::path fileName = this->dataRoot / "favorites.json"; if (!std::filesystem::exists(fileName)) { fileName = this->configRoot / "defaultFavorites.json"; } std::ifstream f; f.open(fileName); if (f.is_open()) { json_reader reader(f); reader.read(&result); } return result; } void Storage::SetFavorites(const std::map& favorites) { std::filesystem::path fileName = this->dataRoot / "favorites.json"; pipedal::ofstream_synced f; f.open(fileName); if (f.is_open()) { json_writer writer(f, true); writer.write(favorites); } } pipedal::JackServerSettings Storage::GetJackServerSettings() { JackServerSettings result; std::filesystem::path fileName = this->dataRoot / "AudioConfig.json"; std::ifstream f; f.open(fileName); if (f.is_open()) { json_reader reader(f); reader.read(&result); if (!result.GetLegacyAlsaDevice().empty() && result.GetAlsaInputDevice().empty() && result.GetAlsaOutputDevice().empty()) { std::string legacyDeviceId = result.GetLegacyAlsaDevice(); result.SetAlsaInputDevice(legacyDeviceId, ""); result.SetAlsaOutputDevice(legacyDeviceId, ""); result.SetLegacyAlsaDevice(""); } result.FixUpDeviceNames(); } return result; } void Storage::SetJackServerSettings(const pipedal::JackServerSettings& jackConfiguration) { std::filesystem::path fileName = this->dataRoot / "AudioConfig.json"; pipedal::ofstream_synced f; f.open(fileName); if (f.is_open()) { json_writer writer(f, false); writer.write(jackConfiguration); } } void Storage::SetSystemMidiBindings(const std::vector& bindings) { std::filesystem::path fileName = this->dataRoot / "config" / "SystemMidiBindings.json"; pipedal::ofstream_synced f; f.open(fileName); if (f.is_open()) { json_writer writer(f, true); writer.write(bindings); } } static bool hasBinding(std::vector& bindings, const std::string& name) { for (auto& binding : bindings) { if (binding.symbol() == name) { return true; } } return false; } std::vector Storage::GetSystemMidiBindings() { std::vector result; std::filesystem::path fileName = this->dataRoot / "config" / "SystemMidiBindings.json"; if (!std::filesystem::exists(fileName)) { // pick up from legacy location? fileName = this->configRoot / "config" / "SystemMidiBindings.json"; } std::ifstream f; f.open(fileName); if (f.is_open()) { try { json_reader reader(f); reader.read(&result); if (result.size() == 2) { result.push_back(MidiBinding::SystemBinding("stopHotspot")); result.push_back(MidiBinding::SystemBinding("startHotspot")); result.push_back(MidiBinding::SystemBinding("shutdown")); result.push_back(MidiBinding::SystemBinding("reboot")); } if (!hasBinding(result, "prevBank")) { result.insert(result.begin(), MidiBinding::SystemBinding("nextBank")); // reverse order. result.insert(result.begin(), MidiBinding::SystemBinding("prevBank")); } if (!hasBinding(result, "snapshot1")) { auto position = 4; for (int i = 0; i < 6; ++i) { result.insert(result.begin() + position, MidiBinding::SystemBinding(SS("snapshot" << (i + 1)))); ++position; } } return result; } catch (const std::exception& e) { Lv2Log::warning(SS("Can't read file " << fileName << ". " << e.what())); } } result.clear(); result.push_back(MidiBinding::SystemBinding("prevBank")); result.push_back(MidiBinding::SystemBinding("nextBank")); result.push_back(MidiBinding::SystemBinding("prevProgram")); result.push_back(MidiBinding::SystemBinding("nextProgram")); result.push_back(MidiBinding::SystemBinding("snapshot1")); result.push_back(MidiBinding::SystemBinding("snapshot2")); result.push_back(MidiBinding::SystemBinding("snapshot3")); result.push_back(MidiBinding::SystemBinding("snapshot4")); result.push_back(MidiBinding::SystemBinding("snapshot5")); result.push_back(MidiBinding::SystemBinding("snapshot6")); result.push_back(MidiBinding::SystemBinding("stopHotspot")); result.push_back(MidiBinding::SystemBinding("startHotspot")); result.push_back(MidiBinding::SystemBinding("shutdown")); result.push_back(MidiBinding::SystemBinding("reboot")); return result; } static bool containsDirectorySeparator(const std::string& value) { if (value.find("/") != std::string::npos) return true; // linux if (value.find("\\") != std::string::npos) return true; // windows if (value.find("::") != std::string::npos) return true; // mac return false; } static void ThrowPermissionDeniedError() { throw std::logic_error("Permission denied."); } static bool ensureNoDotDot(const std::filesystem::path& path) { for (auto segment_ : path) { std::string segment = segment_.string(); if (segment.starts_with(".")) { // the linux rule: any path that consists of all '.'s. bool valid = false; for (auto c : segment) { if (c != '.') { valid = true; break; } } if (!valid) { return false; } } } return true; } bool isInfoFile(const fs::path& path) { auto extension = path.extension(); if (extension == ".pdf") { return true; } auto filename = path.filename(); if (filename == "LICENSE.txt" || filename == "README.txt") { return true; } if (filename == "LICENSE.md" || filename == "README.md") { return true; } if (filename == "license.txt" || filename == "readme.txt") { return true; } return false; } static bool isInfoFile(const FileEntry& l) { return isInfoFile(l.displayName_); } static void AddFilesToResult( FileRequestResult& result, const ModFileTypes::ModDirectory* modDirectoryInfo, // yyx const UiFileProperty& fileProperty, const fs::path& rootPath) { if (!fs::exists(rootPath)) { return; // silently without error. } auto& resultFiles = result.files_; std::set validExtensions = fileProperty.GetPermittedFileExtensions( modDirectoryInfo ? modDirectoryInfo->modType : ""); try { for (auto const& dir_entry : std::filesystem::directory_iterator(rootPath)) { if (!IsValidUtf8(dir_entry.path().string())) { Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string()); continue; // skip invalid UTF-8 names. } const auto& path = dir_entry.path(); auto name = path.filename().string(); if (dir_entry.is_regular_file()) { std::string extension = UiFileProperty::GetFileExtension(path); bool match = false; if (name.length() > 0 && name[0] != '.') // don't show hidden files. { bool match = validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"); if (match && !name.starts_with(".")) { resultFiles.push_back( FileEntry(path, SafeFilenameToString(name), false, false)); } } } else if (dir_entry.is_directory()) { resultFiles.push_back(FileEntry{ path, SafeFilenameToString(name), true, fs::is_symlink(path) }); } } } catch (const std::exception& error) { throw std::logic_error( SS("GetFileList failed. " << rootPath.string() << " - " << error.what())); } // sort lexicographically auto collator = Locale::GetInstance()->GetCollator(); std::sort(resultFiles.begin(), resultFiles.end(), [&collator](const FileEntry& l, const FileEntry& r) { if (l.isDirectory_ != r.isDirectory_) { return l.isDirectory_ > r.isDirectory_; } bool lIsInfoFile = isInfoFile(l); bool rIsInfoFile = isInfoFile(r); if (lIsInfoFile != rIsInfoFile) { return lIsInfoFile > rIsInfoFile; } return collator->Compare(l.displayName_, r.displayName_) < 0; }); } static void AddTracksToResult( const fs::path& audioRootDirectory, FileRequestResult& result, const ModFileTypes::ModDirectory* modDirectoryInfo, // yyx const UiFileProperty& fileProperty, const fs::path& rootPath) { if (!fs::exists(rootPath)) { return; // silently without error. } auto& resultFiles = result.files_; std::set validExtensions = fileProperty.GetPermittedFileExtensions( modDirectoryInfo ? modDirectoryInfo->modType : ""); if (validExtensions.size() == 0) { const auto& audioExtensions = MimeTypes::instance().AudioExtensions(); validExtensions.insert(audioExtensions.begin(), audioExtensions.end()); } validExtensions.insert(".jpg"); validExtensions.insert(".png"); try { // Add directories first. try { for (auto const& dir_entry : std::filesystem::directory_iterator(rootPath)) { if (!IsValidUtf8(dir_entry.path().string())) { Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string()); continue; // skip invalid UTF-8 names. } const auto& path = dir_entry.path(); auto name = path.filename().string(); try { if (dir_entry.is_directory()) { resultFiles.push_back(FileEntry{ path, name, true, dir_entry.is_symlink() }); } } catch (const std::exception& e) { Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what())); } } } catch (const std::exception& error) { throw std::logic_error( SS("AddTracksToResult failed to enumerate directories. " << rootPath.string() << " - " << error.what())); } auto collator = Locale::GetInstance()->GetCollator(); std::sort( resultFiles.begin(), resultFiles.end(), [collator](const FileEntry& l, const FileEntry& r) { if (l.isDirectory_ != r.isDirectory_) { return l.isDirectory_ > r.isDirectory_; } bool lIsInfoFile = isInfoFile(l); bool rIsInfoFile = isInfoFile(r); if (lIsInfoFile != rIsInfoFile) { return lIsInfoFile < rIsInfoFile; } return collator->Compare(l.displayName_, r.displayName_) < 0; }); // Add audio files. auto audioFiles = AudioDirectoryInfo::Create(rootPath, GetShadowIndexDirectory(audioRootDirectory, rootPath)); try { for (const auto& audioFile : audioFiles->GetFiles()) { fs::path audioFilePath = rootPath / audioFile.fileName(); std::string extension = UiFileProperty::GetFileExtension(audioFilePath); if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*")) { resultFiles.push_back( FileEntry( audioFilePath, audioFile.title(), false, std::make_shared(audioFile))); } } } catch (const std::exception& error) { throw std::logic_error( SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what())); } } catch (const std::exception& error) { throw std::logic_error(SS("GetFileList failed. " << error.what() << "(" << rootPath.string() << ")")); } } FileRequestResult Storage::GetModFileList2(const std::string& relativePath, const UiFileProperty& fileProperty) { FileRequestResult result; const fs::path& uploadsDirectory = GetPluginUploadDirectory(); if (relativePath.empty()) { // return the synthetic root. result.isProtected_ = true; for (const auto& modDirectory : fileProperty.modDirectories()) { const auto directoryInfo = ModFileTypes::GetModDirectory(modDirectory); if (directoryInfo) { result.files_.push_back( FileEntry(uploadsDirectory / directoryInfo->pipedalPath, directoryInfo->displayName, true, true)); } } if (fileProperty.useLegacyModDirectory()) { result.files_.push_back( FileEntry(uploadsDirectory / fileProperty.directory(), fs::path(fileProperty.directory()).filename().string(), true, true)); } result.breadcrumbs_.push_back({ "", "Home" }); result.currentDirectory_ = relativePath; return result; } fs::path modDirectoryPath; result.breadcrumbs_.push_back({ "", "Home" }); fs::path fsRelativePath{ relativePath }; const ModFileTypes::ModDirectory* rootModDirectory = nullptr; for (const auto& modDirectory : fileProperty.modDirectories()) { const ModFileTypes::ModDirectory* modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); if (modDirectoryInfo) { if (IsSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath)) { rootModDirectory = modDirectoryInfo; modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; result.breadcrumbs_.push_back({ modDirectoryPath.string(), (modDirectoryInfo->displayName) }); break; } } } if (modDirectoryPath.empty() && fileProperty.useLegacyModDirectory()) { if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) { modDirectoryPath = uploadsDirectory / fileProperty.directory(); result.breadcrumbs_.push_back({ modDirectoryPath.string(), SafeFilenameToString(fs::path(fileProperty.directory()).filename().string()) }); } else { ThrowPermissionDeniedError(); } } // add reaming path segments as breadcrumbs. { fs::path modPath{ modDirectoryPath }; fs::path rp{ relativePath }; auto iRp = rp.begin(); // skip past one or more segments in the modDirectoryPath. for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath) { if (iRp != rp.end()) { ++iRp; } } fs::path cumulativePath = modDirectoryPath; while (iRp != rp.end()) { cumulativePath /= (*iRp); result.breadcrumbs_.push_back({ cumulativePath, SafeFilenameToString(*iRp) }); ++iRp; } } if (IsInAudioTracksDirectory(relativePath)) { AddTracksToResult(this->GetPluginUploadDirectory(), result, rootModDirectory, fileProperty, relativePath); } else { AddFilesToResult(result, rootModDirectory, fileProperty, relativePath); } result.currentDirectory_ = relativePath; return result; } static bool IsChildDirectory(const fs::path& child, const fs::path& parent) { auto iChild = child.begin(); for (auto i = parent.begin(); i != parent.end(); ++i) { if (iChild == child.end() || (*i) != *iChild) { return false; } ++iChild; } return true; } FileRequestResult Storage::GetFileList2(const std::string& relativePath_, const UiFileProperty& fileProperty) { std::string absolutePath = relativePath_; if (!ensureNoDotDot(absolutePath)) { ThrowPermissionDeniedError(); } if (hasSyntheticModRoot(fileProperty)) { return Storage::GetModFileList2(absolutePath, fileProperty); } FileRequestResult result; if (fileProperty.directory().empty()) { throw std::runtime_error("fileProperty.directory() not specified."); } std::filesystem::path pluginRootDirectory = this->GetPluginUploadDirectory() / fileProperty.directory(); if (absolutePath.empty()) { absolutePath = pluginRootDirectory.string(); } // handle resource directories. if (!fileProperty.resourceDirectory().empty()) { if (fileProperty.resourceDirectory() == absolutePath) { absolutePath = pluginRootDirectory; } } if (!IsChildDirectory(absolutePath, pluginRootDirectory)) { absolutePath = pluginRootDirectory; } result.currentDirectory_ = absolutePath; { // watch out for resource files!!! result.breadcrumbs_.push_back({ "", "Home" }); fs::path fsAbsolutePath{ absolutePath }; auto iAbsolutePath = fsAbsolutePath.begin(); for (auto i = pluginRootDirectory.begin(); i != pluginRootDirectory.end(); ++i) { if (iAbsolutePath == fsAbsolutePath.end() || (*i) != *iAbsolutePath) { throw std::runtime_error("Directory is not a subdirectory of the plugin root directory."); } ++iAbsolutePath; } auto cumulativePath = pluginRootDirectory; while (iAbsolutePath != fsAbsolutePath.end()) { cumulativePath /= (*iAbsolutePath); result.breadcrumbs_.push_back({ cumulativePath.string(), SafeFilenameToString(iAbsolutePath->string()) }); ++iAbsolutePath; } } if (!IsSubdirectory(absolutePath, pluginRootDirectory)) { throw std::runtime_error(SS("Improper location. " << absolutePath)); } const ModFileTypes::ModDirectory* pModDirectory = nullptr; if (fileProperty.modDirectories().size() > 0) { pModDirectory = ModFileTypes::GetModDirectory(fileProperty.modDirectories()[0]); } if (IsInAudioTracksDirectory(absolutePath)) { AddTracksToResult(GetPluginUploadDirectory(), result, pModDirectory, fileProperty, absolutePath); } else { AddFilesToResult(result, pModDirectory, fileProperty, absolutePath); } return result; } bool Storage::IsValidSampleFileName(const std::filesystem::path& fileName) { if (!fileName.is_absolute()) { return false; } std::filesystem::path audioFilePath = this->GetPluginUploadDirectory(); std::filesystem::path parentDirectory = fileName.parent_path(); auto iTarget = parentDirectory.begin(); for (auto i = audioFilePath.begin(); i != audioFilePath.end(); ++i) { if (iTarget == parentDirectory.end()) { return false; } if (*i != *iTarget) { return false; } ++iTarget; } while (iTarget != parentDirectory.end()) { if (iTarget->string() == "..") { return false; } ++iTarget; } return true; } void Storage::DeleteSampleFile(const std::filesystem::path& fileName) { if (!IsValidSampleFileName(fileName)) { throw std::logic_error("Permission denied."); } if (!std::filesystem::exists(fileName)) { throw std::logic_error("File not found."); } try { if (std::filesystem::is_directory(fileName)) { if (fileName.string().length() <= 1) // guard against rm -rf / (bitter experience) { throw std::logic_error("Invalid filename."); } if (std::filesystem::is_symlink(fileName)) { std::filesystem::remove(fileName); } else { if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) { std::filesystem::remove_all(fileName); } } } else { std::filesystem::remove(fileName); if (IsInAudioTracksDirectory(fileName)) { // remove the metadata file as well. std::filesystem::remove(fileName.string() + ".mdata"); } } } catch (const std::exception&) { throw std::logic_error("Permission denied."); } } std::filesystem::path Storage::MakeUserFilePath(const std::string& directory, const std::string& filename) { std::filesystem::path filePath{ filename }; std::filesystem::path result = fs::path(directory) / filename; if (!result.is_absolute()) { result = this->GetPluginUploadDirectory() / result; } if (!this->IsValidSampleFileName(result)) { throw std::logic_error(SS("Invalid upload path: " << result)); } return result; } bool Storage::IsValidReadmeFile(const std::filesystem::path& fullPath) { return isInfoFile(fullPath.filename().string()); } bool Storage::IsValidArtworkFile(const std::filesystem::path& fullPath) { if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string())) { // allow artwork files. return true; } return false; } std::string Storage::UploadUserFile(const std::string& directory, UiFileProperty::ptr uiFileProperty, const std::string& filename, std::istream& stream, size_t contentLength) { std::filesystem::path path; if (directory.length() != 0) { path = this->MakeUserFilePath(directory, filename); } else { throw std::logic_error("Directory argument not supplied."); } fs::path relativePath; try { relativePath = MakeRelativePath(path, this->GetPluginUploadDirectory()); } catch (const std::exception& e) { throw std::logic_error("Permission denied. Path is outside the upload storage directory."); } if (!(uiFileProperty->IsValidExtension(relativePath) || IsValidArtworkFile(path) || IsValidReadmeFile(path))) { throw std::logic_error("Permission denied. Invalid file extension for this directory."); } { try { std::filesystem::create_directories(path.parent_path()); pipedal::ofstream_synced f(path, std::ios_base::trunc | std::ios_base::binary); if (!f.is_open()) { throw std::logic_error(SS("Can't create file " << path << ".")); } std::vector buffer; size_t BUFFER_SIZE = 64 * 1024; buffer.resize(BUFFER_SIZE); char* pBuffer = (char*)&(buffer[0]); while (contentLength != 0) { size_t thisTime = std::min(BUFFER_SIZE, contentLength); stream.read(pBuffer, (std::streamsize)thisTime); if (!stream) { throw std::runtime_error("Unable to read body input stream."); } f.write(pBuffer, (std::streamsize)thisTime); if (!f) { throw std::runtime_error("Failed to write to upload file."); } contentLength -= thisTime; } } catch (const std::exception& e) { Lv2Log::error(SS("Upload failed. " << e.what())); std::filesystem::remove(path); throw; } } return path.string(); } std::string Storage::CreateNewSampleDirectory(const std::string& relativePath, const UiFileProperty& uiFileProperty) { if (uiFileProperty.directory().empty()) { throw std::runtime_error("Invalid UI File Property."); } std::filesystem::path path = this->GetPluginUploadDirectory() / uiFileProperty.directory() / relativePath; if (!this->IsValidSampleFileName(path)) { throw std::runtime_error("Invalid file name."); } if (std::filesystem::exists(path)) { throw std::runtime_error("A directory with that name already exists."); } std::filesystem::create_directories(path); return path; } std::string Storage::RenameFilePropertyFile( const std::string& oldRelativePath, const std::string& newRelativePath, const UiFileProperty& uiFileProperty) { if (uiFileProperty.directory().empty()) { throw std::runtime_error("Invalid UI File Property."); } std::filesystem::path oldPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / oldRelativePath; if (!this->IsValidSampleFileName(oldPath)) { throw std::runtime_error("Invalid file name."); } if (!std::filesystem::exists(oldPath)) { throw std::runtime_error("Original path does not exist."); } std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath; if (!this->IsValidSampleFileName(newPath)) { throw std::runtime_error("Invalid file name."); } if (std::filesystem::exists(newPath)) { if (std::filesystem::is_directory(newPath)) { throw std::runtime_error("A directory with that name already exists."); } else { throw std::runtime_error("A file with that name already exists."); } } std::filesystem::rename(oldPath, newPath); if (fs::exists(oldPath.string() + ".mdata")) { // rename the metadata file as well. std::filesystem::rename(oldPath.string() + ".mdata", newPath.string() + ".mdata"); } return newPath; } fs::path MakeVersionedPath(const fs::path& path) { if (!fs::exists(path)) { return path; // no need to version a non-existing file. } fs::path newPath = path; auto stem = newPath.stem().string(); ; if (stem.ends_with(")")) { // remove the trailing (n) from the file name. size_t pos = stem.find_last_of('('); std::string stemVersion = stem.substr(pos + 1, stem.length() - 1 - (pos + 1)); // check if it is a number. if (stemVersion.find_first_not_of("0123456789") == std::string::npos) { // it is a number, remove the trailing (n). stem = stem.substr(0, pos); while (stem.ends_with(" ")) { // remove trailing space. stem = stem.substr(0, stem.length() - 1); } } } int version = 0; while (fs::exists(newPath)) { // append (n) to the file name. std::string newFileName; if (version == 0) { newFileName = SS(stem << newPath.extension().string()); } else { // append (n) to the file name. newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string()); } newPath = newPath.parent_path() / newFileName; ++version; } return newPath; } std::string Storage::CopyFilePropertyFile( const std::string& oldRelativePath, const std::string& newRelativePath, const UiFileProperty& uiFileProperty, bool overwrite) { if (uiFileProperty.directory().empty()) { throw std::runtime_error("Invalid UI File Property."); } std::filesystem::path oldPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / oldRelativePath; if (!this->IsValidSampleFileName(oldPath)) { throw std::runtime_error("Invalid file name."); } if (!std::filesystem::exists(oldPath)) { throw std::runtime_error("Original path does not exist."); } std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath; if (!this->IsValidSampleFileName(newPath)) { throw std::runtime_error("Invalid file name."); } if (std::filesystem::exists(newPath)) { if (std::filesystem::is_directory(newPath)) { throw std::runtime_error("A directory with that name already exists."); } else { newPath = MakeVersionedPath(newPath); } } std::filesystem::create_hard_link(oldPath, newPath); if (IsInAudioTracksDirectory(oldPath) && IsInAudioTracksDirectory(newPath)) { std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata"); if (fs::exists(metadataPath)) { // copy the metadata file as well. std::filesystem::copy_file( metadataPath, SS(newPath.string() << ".mdata"), fs::copy_options::overwrite_existing); } } return newPath; } void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree* node, const std::filesystem::path& directory) const { for (auto child : std::filesystem::directory_iterator(directory)) { if (!IsValidUtf8(child.path().string())) { Lv2Log::warning("Invalid UTF-8 name in directory: " + child.path().string()); // skip invalid UTF-8 paths. continue; } if (child.is_directory()) { const auto& childPath = child.path(); FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath); FillSampleDirectoryTree(childTree.get(), childPath); node->children_.push_back(std::move(childTree)); } } auto collator = Locale::GetInstance()->GetCollator(); std::sort(node->children_.begin(), node->children_.end(), [&collator](const FilePropertyDirectoryTree::ptr& left, const FilePropertyDirectoryTree::ptr& right) { return collator->Compare(left->directoryName_, right->directoryName_) < 0; }); } static void GetAllExtensions(std::set& result, const std::filesystem::path& path) { assert(fs::is_directory(path)); for (const auto& dirEnt : fs::directory_iterator(path)) { if (!IsValidUtf8(dirEnt.path().string())) { // skip invalid UTF-8 paths. Lv2Log::warning("Invalid UTF-8 name in directory: " + dirEnt.path().string()); continue; } if (dirEnt.is_directory()) { GetAllExtensions(result, dirEnt.path()); } else { std::string filename = dirEnt.path().filename(); if (dirEnt.path().has_extension()) { std::string extension = dirEnt.path().extension(); result.insert(extension); } } } } static std::set GetAllExtensions(const std::filesystem::path& path) { std::set result; if (fs::is_regular_file(path)) { result.insert(UiFileProperty::GetFileExtension(path)); } else { GetAllExtensions(result, path); } return result; } FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty& uiFileProperty, const std::filesystem::path& selectedPath) { fs::path uploadDirectory = this->GetPluginUploadDirectory(); fs::path relativePath; try { relativePath = MakeRelativePath(selectedPath, uploadDirectory); } catch (const std::exception& e) { // not an upload directory. throw std::runtime_error(SS("Permission denied: " << selectedPath)); } std::set fileExtensions = GetAllExtensions(selectedPath); if (hasSyntheticModRoot(uiFileProperty)) { if (relativePath.empty()) { throw std::runtime_error(SS("Permission denied: " << selectedPath)); } FilePropertyDirectoryTree::ptr result = std::make_unique("", "Home"); result->isProtected_ = true; std::string parentModDirectory = uiFileProperty.getParentModDirectory(relativePath); for (const auto& modDirectory : uiFileProperty.modDirectories()) { auto modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); if (modDirectoryInfo) { if (IsSubset(fileExtensions, modDirectoryInfo->fileExtensions) || modDirectoryInfo->fileExtensions.contains(".*") || modDirectoryInfo->fileExtensions.empty() // Private directory that accepts files of any type. ) { auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath; FilePropertyDirectoryTree::ptr child = std::make_unique( childPath, modDirectoryInfo->displayName); FillSampleDirectoryTree(child.get(), childPath); result->children_.push_back(std::move(child)); } } } if (uiFileProperty.useLegacyModDirectory()) { auto childPath = uploadDirectory / uiFileProperty.directory(); FilePropertyDirectoryTree::ptr child = std::make_unique( childPath); FillSampleDirectoryTree(child.get(), childPath); result->children_.push_back(std::move(child)); } return result; } else { if (uiFileProperty.directory().empty()) { throw std::runtime_error("Invalid uiFileProperty"); } if (!ensureNoDotDot(uiFileProperty.directory())) { throw std::runtime_error("Invalid uiFileProperty"); } std::filesystem::path rootDirectory = uploadDirectory / uiFileProperty.directory(); FilePropertyDirectoryTree::ptr result = std::make_unique(rootDirectory.string(), "Home"); FillSampleDirectoryTree(result.get(), rootDirectory); return result; } } bool Storage::IsInAudioTracksDirectory(const std::filesystem::path& path) const { std::filesystem::path audioTracksDirectory = this->GetPluginUploadDirectory() / "shared/audio/Tracks"; return IsSubdirectory(path, audioTracksDirectory); } bool Storage::IsInUploadsDirectory(const std::filesystem::path& path) const { return IsSubdirectory(path, this->GetPluginUploadDirectory()); } const PluginPresetIndex& Storage::GetPluginPresetIndex() { return pluginPresetIndex; } std::filesystem::path Storage::FromAbstractPathString(const std::string& stringPath) { if (stringPath.empty()) { return ""; } fs::path path{ stringPath }; if (path.is_absolute()) { return path; } return GetPluginUploadDirectory() / path; } std::string Storage::ToAbstractPathFromJson(const std::string& pathJson) { json_variant v = json_variant::parse(pathJson); v = AtomConverter::AbstractPath(v, GetPluginUploadDirectory().string()); return v.to_string(); } std::string Storage::FromAbstractPathJson(const std::string& pathJson) { json_variant v = json_variant::parse(pathJson); v = AtomConverter::MapPath(v, GetPluginUploadDirectory().string()); return v.to_string(); } void Storage::SetChannelRouterSettings(ChannelRouterSettings::ptr settings) { this->channelRouterSettings = settings; this->channelSelection = ChannelSelection(*settings); SaveChannelRouterSettings(settings); } ChannelRouterSettings::ptr Storage::GetChannelRouterSettings() { return channelRouterSettings; } std::filesystem::path Storage::GetChannelRouterSettingsPath() const { return GetDataRoot() / "ChannelRouterSettings.json"; } void Storage::SaveChannelRouterSettings(ChannelRouterSettings::ptr settings) { std::ofstream os(GetChannelRouterSettingsPath()); json_writer writer(os); writer.write(settings); } ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() { auto path = GetChannelRouterSettingsPath(); if (!fs::exists(path)) { // See UpgradeChannelRouterSettings(), which will upgrade from old settings // or create a default instance. return nullptr; // will be upgraded later. } try { std::ifstream is(path); json_reader reader(is); ChannelRouterSettings::ptr result; reader.read(&result); this->channelSelection = ChannelSelection(*result); return result; } catch (const std::exception& e) { Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); return std::make_shared(); } } static int64_t GetUpgradedPortIndex(const std::string& portName) { auto nPos = portName.find_last_of('_'); if (nPos != std::string::npos) { std::string channelStr = portName.substr(nPos + 1); try { int64_t channelIndex = std::stoll(channelStr); return channelIndex; } catch (const std::exception&) { return -1; } } return -1; } void Storage::UpgradeChannelRouterSettings() { if (channelRouterSettings == nullptr) { channelRouterSettings = std::make_shared(); if (jackChannelSelection.isValid()) { channelRouterSettings->configured(true); channelRouterSettings->mainInputChannels().resize(2); const std::vector& oldInputs = jackChannelSelection.GetInputAudioPorts(); std::vector& newInputs = channelRouterSettings->mainInputChannels(); if (oldInputs.size() == 1) { int64_t leftIndex = GetUpgradedPortIndex(oldInputs[0]); newInputs.resize(2); newInputs[0] = leftIndex; newInputs[1] = leftIndex; } else if (oldInputs.size() == 2) { newInputs.resize(2); for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) { int64_t portIndex = GetUpgradedPortIndex(oldInputs[i]); newInputs[i] = portIndex; } } const std::vector& oldOutputs = jackChannelSelection.GetOutputAudioPorts(); auto& newMainOutputs = channelRouterSettings->mainOutputChannels(); if (oldOutputs.size() == 1) { int64_t leftIndex = GetUpgradedPortIndex(oldOutputs[0]); newMainOutputs.resize(2); newMainOutputs[0] = leftIndex; newMainOutputs[1] = leftIndex; } else if (oldOutputs.size() == 2) { newMainOutputs.resize(2); for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) { int64_t portIndex = GetUpgradedPortIndex(oldOutputs[i]); newMainOutputs[i] = portIndex; } } auto& newAuxInputs = channelRouterSettings->auxInputChannels(); newAuxInputs.resize(2); newAuxInputs[0] = -1; newAuxInputs[1] = -1; auto& newAuxOutputs = channelRouterSettings->auxOutputChannels(); newAuxOutputs.resize(2); newAuxOutputs[0] = -1; newAuxOutputs[1] = -1; SaveChannelRouterSettings(channelRouterSettings); } channelSelection = ChannelSelection(*channelRouterSettings); } } const ChannelSelection& Storage::GetChannelSelection() const { return channelSelection; } JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) JSON_MAP_END() JSON_MAP_BEGIN(CurrentPreset) JSON_MAP_REFERENCE(CurrentPreset, modified) JSON_MAP_REFERENCE(CurrentPreset, preset) JSON_MAP_END()