// Copyright (c) 2022-2024 Robin Davies // // 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 #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" using namespace pipedal; 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); } const std::filesystem::path &Storage::GetConfigRoot() { return this->configRoot; } const std::filesystem::path &Storage::GetDataRoot() { 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); } } } void Storage::MaybeCopyDefaultPresets() { auto presetsDirectory = this->GetPresetsDirectory(); if (!std::filesystem::exists(presetsDirectory / "index.banks")) { CopyDirectory(this->configRoot / "default_presets" / "presets", presetsDirectory); } // Obsolete: TooB effects now have correct preset declarations. // auto pluginDirectory = this->GetPluginPresetsDirectory(); // if (!std::filesystem::exists(pluginDirectory / "index.json")) // { // CopyDirectory(this->configRoot / "default_presets" / "plugin_presets", // pluginDirectory); // } } void Storage::Initialize() { try { std::filesystem::create_directories(this->GetPresetsDirectory()); std::filesystem::create_directories(this->GetPluginPresetsDirectory()); MaybeCopyDefaultPresets(); } catch (const std::exception &e) { throw PiPedalStateException("Can't create presets directory. (" + (std::string)this->GetPresetsDirectory() + ") (" + e.what() + ")"); } LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); try { LoadChannelSelection(); } catch (const std::exception &) { } LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); LoadUserSettings(); } 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"; } std::filesystem::path Storage::GetPluginUploadDirectory() const { return this->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::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, const std::string &name, int64_t saveAfterInstanceId) { Pedalboard newPedalboard = pedalboard; newPedalboard.name(name); int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); currentBank.selectedPreset(newInstanceId); SaveCurrentBank(); return newInstanceId; } 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::DeletePreset(int64_t presetId) { int64_t 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); return this->currentBank.addPreset(newPedalboard, -1); } int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &fromItem = this->currentBank.getItem(fromId); if (toId == -1) { Pedalboard newPedalboard = fromItem.preset(); std::string name = GetPresetCopyName(fromItem.preset().name()); newPedalboard.name(name); return this->currentBank.addPreset(newPedalboard, fromId); } else { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); return toId; } } 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); } void Storage::LoadChannelSelection() { 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()); throw PiPedalStateException("Unexpected error reading Jack settings file."); } } } void Storage::SaveChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); try { 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 ¤tPreset) { 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; } } 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)); } result.state = preset.state_; result.lilvPresetUri = preset.lilvPresetUri_; return result; } } throw PiPedalException("Plugin preset not found."); } 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_; presets.presets_[i] = PluginPreset(preset.instanceId_, preset.label_, pedalboardItem); break; } } if (!existing) { result = presets.nextInstanceId_++; presets.presets_.push_back( PluginPreset(result, name, pedalboardItem)); } this->SavePluginPresets(pluginUri, presets); return result; } uint64_t Storage::SavePluginPreset( const std::string &pluginUri, const std::string &name, float inputVolume, float outputVolume, const std::map &values, const Lv2PluginState &lv2State) { 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; preset.controlValues_ = values; preset.state_ = lv2State; result = preset.instanceId_; break; } } if (!existing) { result = presets.nextInstanceId_++; presets.presets_.push_back( PluginPreset( result, name, values, lv2State)); } 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("Perset 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 JACK_HOST result.Initialize(); #endif 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); } #if JACK_HOST jackConfiguration.Write(); #endif } 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; } 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, name, false, false)); } } } else if (dir_entry.is_directory()) { resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); } } } catch (const std::exception &error) { throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); } // 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_; } 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. 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_directory()) { resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); } } 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_; } return collator->Compare(l.displayName_ ,r.displayName_) < 0; }); // Add audio files. auto audioFiles = AudioDirectoryInfo::Create(rootPath, GetShadowIndexDirectory(audioRootDirectory,rootPath) ); 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("GetFileList failed. Directory not found: " + rootPath.string()); } } FileRequestResult Storage::GetModFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) { FileRequestResult result; 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(), 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, *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(), 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::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))) { 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; } 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()