File indexing completed on 2024-05-19 05:42:15

0001 // ct_lvtprj_project_file.h                                         -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtmdb_soci_writer.h>
0021 #include <ct_lvtprj_projectfile.h>
0022 #include <ct_lvtshr_stringhelpers.h>
0023 
0024 #include <KZip>
0025 
0026 // Std
0027 #include <algorithm>
0028 #include <iostream>
0029 #include <random>
0030 #include <string_view>
0031 
0032 // Qt, Json
0033 #include <QCoreApplication>
0034 #include <QDir>
0035 #include <QFile>
0036 #include <QJsonDocument>
0037 #include <QJsonObject>
0038 #include <QStandardPaths>
0039 #include <QTimer>
0040 
0041 using namespace Codethink::lvtprj;
0042 namespace fs = std::filesystem;
0043 
0044 namespace {
0045 constexpr std::string_view CODE_DB = "code_database.db";
0046 constexpr std::string_view CAD_DB = "cad_database.db";
0047 constexpr std::string_view METADATA = "metadata.json";
0048 constexpr std::string_view LEFT_PANEL_HISTORY = "left_panel";
0049 constexpr std::string_view RIGHT_PANEL_HISTORY = "right_panel";
0050 constexpr std::string_view BOOKMARKS_FOLDER = "bookmarks";
0051 
0052 bool compressDir(QFileInfo const& saveTo, QDir const& dirToCompress)
0053 {
0054     if (!QDir{}.exists(saveTo.absolutePath()) && !QDir{}.mkdir(saveTo.absolutePath())) {
0055         qDebug() << "[compressDir] Could not prepare path to save.";
0056         return false;
0057     }
0058 
0059     auto zipFile = KZip(saveTo.absoluteFilePath());
0060     if (!zipFile.open(QIODevice::WriteOnly)) {
0061         qDebug() << "[compressDir] Could not open file to compress:" << saveTo;
0062         qDebug() << zipFile.errorString();
0063         return false;
0064     }
0065 
0066     auto r = zipFile.addLocalDirectory(dirToCompress.path(), "");
0067     if (!r) {
0068         qDebug() << "[compressDir] Could not add files to project:" << dirToCompress;
0069         qDebug() << zipFile.errorString();
0070         return false;
0071     }
0072 
0073     return true;
0074 }
0075 
0076 bool extractDir(QFileInfo const& projectFile, QDir const& openLocation)
0077 {
0078     auto zipFile = KZip(projectFile.absoluteFilePath());
0079     if (!zipFile.open(QIODevice::ReadOnly)) {
0080         qDebug() << "[compressDir] Could not open file to read contents:" << projectFile;
0081         qDebug() << zipFile.errorString();
0082         return false;
0083     }
0084     const KArchiveDirectory *dir = zipFile.directory();
0085     return dir->copyTo(openLocation.path());
0086 }
0087 
0088 // return a random string, for the unique project folder in $temp.
0089 [[nodiscard]] std::string random_string(size_t length)
0090 {
0091     constexpr std::string_view charset =
0092         "0123456789"
0093         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
0094         "abcdefghijklmnopqrstuvwxyz";
0095 
0096     std::random_device rd;
0097     std::mt19937 mt(rd());
0098 
0099     // doc says that the range is [lower, upper], and not [lower, upper) as I'd expect.
0100     std::uniform_int_distribution<size_t> dist(0, charset.size() - 1);
0101 
0102     std::string str(length, 0);
0103     std::generate_n(str.begin(), length, [&dist, &mt, &charset]() -> char {
0104         return charset[dist(mt)];
0105     });
0106     return str;
0107 }
0108 
0109 // creates a non-exisitng 16 alphanumeric folder for the project.
0110 [[nodiscard]] std::pair<std::error_code, fs::path> createNonExistingFolder(const std::filesystem::path& tmp)
0111 {
0112     fs::path openLocation;
0113     for (;;) {
0114         openLocation = tmp / random_string(16);
0115         bool openLocationExists = fs::exists(fs::status(openLocation));
0116         if (openLocationExists) {
0117             continue;
0118         }
0119 
0120         std::error_code ec;
0121         fs::create_directories(openLocation, ec);
0122         return {ec, openLocation};
0123     }
0124 
0125     assert(false && "unreachable");
0126     return {std::error_code{}, fs::path{}};
0127 }
0128 
0129 [[nodiscard]] auto createUniqueTempFolder() -> cpp::result<std::filesystem::path, ProjectFileError>
0130 {
0131     std::error_code ec;
0132     const fs::path tmp = fs::temp_directory_path(ec);
0133     if (ec) {
0134         return cpp::fail(ProjectFileError{ec.message()});
0135     }
0136 
0137     auto [ec2, folderName] = createNonExistingFolder(tmp);
0138     if (ec2) {
0139         return cpp::fail(ProjectFileError{ec2.message()});
0140     }
0141 
0142     return folderName;
0143 }
0144 
0145 } // namespace
0146 
0147 struct ProjectFile::Private {
0148     bool isOpen = false;
0149     // is the project open?
0150 
0151     bool isDirty = false;
0152     // are there changes in the project that are not saved?
0153 
0154     bool hasCodeDatabase = false;
0155     // do we have the datbase for the code db yet?
0156 
0157     bool hasGraphDatabase = false;
0158     // do we have the graph database? (the db that stores the positions and metadata)
0159 
0160     bool allowedAutoSave = true;
0161     // Will we autosave?
0162 
0163     std::filesystem::path backupLocation;
0164     // location in the disk where to open and save the backup files.
0165 
0166     std::filesystem::path location;
0167     // location in disk where to open and save the contents of the project.
0168 
0169     std::filesystem::path openLocation;
0170     // where this project is open on disk. this is a unzipped folder in $TEMP
0171 
0172     std::filesystem::path sourceCodePath;
0173     // Location to the actual source code for parsed projects
0174 
0175     std::string projectName;
0176     // name of the project
0177 
0178     std::string projectInformation;
0179     // longer explanation of the project
0180 
0181     QTimer projectSaveBackupTimer;
0182     // when the timer is over, we save the backup file.
0183 };
0184 
0185 ProjectFile::ProjectFile(): d(std::make_unique<ProjectFile::Private>())
0186 {
0187     std::filesystem::path backupF = backupFolder();
0188     std::filesystem::directory_entry backupDir(backupF);
0189     if (!backupDir.exists()) {
0190         if (!std::filesystem::create_directories(backupDir)) {
0191             std::cerr << "Could not create directories for the project backup.";
0192             std::cerr << "Disabling autosave features";
0193             d->allowedAutoSave = false;
0194         }
0195     }
0196 }
0197 
0198 ProjectFile::~ProjectFile()
0199 {
0200     (void) close();
0201 }
0202 
0203 std::filesystem::path ProjectFile::backupFolder()
0204 {
0205     const QString appName = qApp ? qApp->applicationName() : QStringLiteral("CodeVis");
0206     const QString docPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + QDir::separator()
0207         + appName + QDir::separator() + "Backup";
0208     return docPath.toStdString();
0209 }
0210 
0211 void ProjectFile::requestAutosave(int msec)
0212 {
0213     if (!d->allowedAutoSave) {
0214         return;
0215     }
0216 
0217     // initialize the autosave timer only once.
0218     static bool unused = [this]() -> bool {
0219         d->projectSaveBackupTimer.setSingleShot(true);
0220         connect(&d->projectSaveBackupTimer, &QTimer::timeout, this, [this] {
0221             auto res = saveBackup();
0222             if (res.has_error()) {
0223                 Q_EMIT communicateError(res.error());
0224             }
0225         });
0226         return true;
0227     }();
0228     (void) unused;
0229 
0230     if (msec == 0) {
0231         auto res = saveBackup();
0232         if (res.has_error()) {
0233             Q_EMIT communicateError(res.error());
0234         }
0235     } else {
0236         d->projectSaveBackupTimer.start(std::chrono::milliseconds{msec});
0237     }
0238 }
0239 
0240 std::filesystem::path ProjectFile::backupPath() const
0241 {
0242     return d->backupLocation;
0243 }
0244 
0245 auto ProjectFile::close() -> cpp::result<void, ProjectFileError>
0246 {
0247     if (!isOpen()) {
0248         return {}; // closing a closed project is a noop.
0249     }
0250 
0251     try {
0252         if (!fs::remove_all(d->openLocation)) {
0253             return cpp::fail(ProjectFileError{"Error removing temporary folder"});
0254         }
0255     } catch (std::filesystem::filesystem_error& error) {
0256         return cpp::fail(ProjectFileError{error.what()});
0257     }
0258 
0259     d = std::make_unique<ProjectFile::Private>();
0260     return {};
0261 }
0262 
0263 bool ProjectFile::isOpen() const
0264 {
0265     return d->isOpen;
0266 }
0267 
0268 auto ProjectFile::createEmpty() -> cpp::result<void, ProjectFileError>
0269 {
0270     cpp::result<fs::path, ProjectFileError> tmpFolder = createUniqueTempFolder();
0271     if (tmpFolder.has_error()) {
0272         return cpp::fail(tmpFolder.error());
0273     }
0274 
0275     d->openLocation = tmpFolder.value();
0276     d->isOpen = true;
0277 
0278     {
0279         lvtmdb::SociWriter writer;
0280         if (!writer.createOrOpen(codeDatabasePath().string(), "codebase_db.sql")) {
0281             return cpp::fail(ProjectFileError{
0282                 "Couldnt create database. Tip: Make sure the database spec folder is installed properly."});
0283         }
0284     }
0285     {
0286         lvtmdb::SociWriter writer;
0287         if (!writer.createOrOpen(cadDatabasePath().string(), "cad_db.sql")) {
0288             return cpp::fail(ProjectFileError{
0289                 "Couldnt create database. Tip: Make sure the database spec folder is installed properly."});
0290         }
0291     }
0292 
0293     return {};
0294 }
0295 
0296 auto ProjectFile::open(const std::filesystem::path& path) -> cpp::result<void, ProjectFileError>
0297 {
0298     // std::string lacks .endsWith - cpp20 fixes this but we are on 17.
0299     const QString tmp = QString::fromStdString(path.string());
0300     const std::string filePath = tmp.endsWith(".lks") ? tmp.toStdString() : tmp.toStdString() + ".lks";
0301 
0302     const bool exists = fs::exists(fs::status(filePath));
0303     if (!exists) {
0304         return cpp::fail(ProjectFileError{"Project file does not exist on disk"});
0305     }
0306 
0307     cpp::result<fs::path, ProjectFileError> tmpFolder = createUniqueTempFolder();
0308     if (tmpFolder.has_error()) {
0309         return cpp::fail(ProjectFileError{tmpFolder.error()});
0310     }
0311 
0312     d->openLocation = tmpFolder.value();
0313     const auto projectFile = QFileInfo{QString::fromStdString(filePath)};
0314     const auto openLocation = QDir{QString::fromStdString(d->openLocation.string())};
0315     if (!extractDir(projectFile, openLocation)) {
0316         return cpp::fail(ProjectFileError{"Failed to extract project contents"});
0317     }
0318 
0319     d->location = filePath;
0320     d->isOpen = true;
0321     loadProjectMetadata();
0322 
0323     return {};
0324 }
0325 
0326 auto ProjectFile::saveAs(const fs::path& path, BackupFileBehavior behavior) -> cpp::result<void, ProjectFileError>
0327 {
0328     fs::path projectPath = path;
0329     if (!projectPath.string().ends_with(".lks")) {
0330         projectPath += ".lks";
0331     }
0332 
0333     // create a backup file the project already exists.
0334     // if any error happens, the user can revert to the backup.
0335     try {
0336         if (fs::exists(fs::status(projectPath))) {
0337             // fs path has no operator `+`, so I can't initialize correctly.
0338             // but it has operator +=. :|
0339             fs::path bkpPath = projectPath;
0340             bkpPath += ".bpk";
0341             fs::rename(projectPath, bkpPath);
0342         }
0343     } catch (fs::filesystem_error& error) {
0344         return cpp::fail(ProjectFileError{error.what()});
0345     }
0346 
0347     d->location = projectPath;
0348 
0349     auto dumped = dumpProjectMetadata();
0350     if (dumped.has_error()) {
0351         return cpp::fail(dumped.error());
0352     }
0353 
0354     // save new file.
0355     const auto saveWhat = QDir{QString::fromStdString(d->openLocation.string())};
0356     const auto saveTo = QFileInfo{QString::fromStdString(projectPath.string())};
0357     if (!compressDir(saveTo, saveWhat)) {
0358         qDebug() << "Failed to generate project file";
0359         return cpp::fail(ProjectFileError{"Failed to generate project file"});
0360     }
0361 
0362     // We correctly saved the file, we can delete the auto backup file.
0363     if (behavior == BackupFileBehavior::Discard) {
0364         if (!std::filesystem::remove(d->backupLocation)) {
0365             // error deleting the backup file, but the project file was correctly saved.
0366             // this is not a hard error, just complain on the terminal.
0367             qDebug() << "Error removing backup file";
0368         }
0369     }
0370 
0371     return {};
0372 }
0373 
0374 cpp::result<void, ProjectFileError> ProjectFile::saveBackup()
0375 {
0376     return saveAs(d->backupLocation, BackupFileBehavior::Keep);
0377 }
0378 
0379 auto ProjectFile::save() -> cpp::result<void, ProjectFileError>
0380 {
0381     auto ret = saveAs(d->location, BackupFileBehavior::Discard);
0382     if (ret.has_error()) {
0383         return ret;
0384     }
0385 
0386     d->isDirty = false;
0387     return ret;
0388 }
0389 
0390 void ProjectFile::setProjectName(std::string name)
0391 {
0392     d->projectName = std::move(name);
0393     d->backupLocation = backupFolder().string() + "/" + d->projectName + ".lks";
0394 }
0395 
0396 void ProjectFile::setProjectInformation(std::string projectInfo)
0397 {
0398     d->projectInformation = std::move(projectInfo);
0399 }
0400 
0401 fs::path ProjectFile::openLocation() const
0402 {
0403     assert(d->isOpen);
0404     return d->openLocation;
0405 }
0406 
0407 fs::path ProjectFile::codeDatabasePath() const
0408 {
0409     assert(d->isOpen);
0410     return d->openLocation / CODE_DB;
0411 }
0412 
0413 bool ProjectFile::hasCodeDatabase() const
0414 {
0415     const fs::path codeDb = codeDatabasePath();
0416     return fs::exists(fs::status(codeDb));
0417 }
0418 
0419 fs::path ProjectFile::cadDatabasePath() const
0420 {
0421     return d->openLocation / CAD_DB;
0422 }
0423 
0424 bool ProjectFile::hasCadDatabase() const
0425 {
0426     const fs::path cadDb = cadDatabasePath();
0427     return fs::exists(fs::status(cadDb));
0428 }
0429 
0430 std::string ProjectFile::projectInformation() const
0431 {
0432     return d->projectInformation;
0433 }
0434 
0435 std::string ProjectFile::projectName() const
0436 {
0437     return d->projectName;
0438 }
0439 
0440 auto ProjectFile::dumpProjectMetadata() const -> cpp::result<void, ProjectFileError>
0441 {
0442     QJsonObject obj{{"name", QString::fromStdString(d->projectName)},
0443                     {"description", QString::fromStdString(d->projectInformation)},
0444                     {"sourcePath", QString::fromStdString(d->sourceCodePath.string())}};
0445     QJsonDocument doc(obj);
0446 
0447     const fs::path metadata_file = d->openLocation / METADATA;
0448     auto saveFileName = QString::fromStdString(metadata_file.string());
0449     QFile saveFile(saveFileName);
0450     if (!saveFile.open(QIODevice::WriteOnly)) {
0451         return cpp::fail(ProjectFileError{saveFile.errorString().toStdString()});
0452     }
0453 
0454     qint64 bytesSaved = saveFile.write(doc.toJson());
0455     if (bytesSaved == 0) {
0456         return cpp::fail(ProjectFileError{"Error saving metadata file, zero bytes size."});
0457     }
0458     return {};
0459 }
0460 
0461 void ProjectFile::loadProjectMetadata()
0462 {
0463     const fs::path metadata_file = d->openLocation / METADATA;
0464     const auto loadFileName = QString::fromStdString(metadata_file.string());
0465 
0466     QFile loadFile(loadFileName);
0467     loadFile.open(QIODevice::ReadOnly);
0468 
0469     const QByteArray fileData = loadFile.readAll();
0470     QJsonObject obj = QJsonDocument::fromJson(fileData).object();
0471 
0472     if (obj.contains("name")) {
0473         d->projectName = obj.value("name").toString().toStdString();
0474     }
0475     if (obj.contains("description")) {
0476         d->projectInformation = obj.value("description").toString().toStdString();
0477     }
0478     if (obj.contains("sourcePath")) {
0479         d->sourceCodePath = obj.value("sourcePath").toString().toStdString();
0480     }
0481 }
0482 
0483 fs::path ProjectFile::location() const
0484 {
0485     return d->location;
0486 }
0487 
0488 std::string_view ProjectFile::codebaseDbFilename()
0489 {
0490     return CODE_DB;
0491 }
0492 
0493 void ProjectFile::setSourceCodePath(std::filesystem::path sourceCodePath)
0494 {
0495     d->sourceCodePath = std::move(sourceCodePath);
0496 }
0497 
0498 std::filesystem::path ProjectFile::sourceCodePath() const
0499 {
0500     return d->sourceCodePath;
0501 }
0502 
0503 void ProjectFile::setDirty()
0504 {
0505     d->isDirty = true;
0506 }
0507 
0508 bool ProjectFile::isDirty() const
0509 {
0510     return d->isDirty;
0511 }
0512 
0513 std::vector<QJsonDocument> jsonDocInFolder(const QString& folder)
0514 {
0515     // get the list of files on the left panel tab
0516     QDir dir(folder);
0517     const auto dirEntries = dir.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files);
0518 
0519     std::vector<QJsonDocument> ret;
0520     ret.reserve(dirEntries.size());
0521 
0522     for (const QString& filePath : dirEntries) {
0523         QFile file(folder + QDir::separator() + filePath);
0524         const bool opened = file.open(QIODevice::ReadOnly);
0525         if (!opened) {
0526             continue;
0527         }
0528         ret.push_back(QJsonDocument::fromJson(file.readAll()));
0529     }
0530 
0531     return ret;
0532 }
0533 
0534 std::vector<QJsonDocument> ProjectFile::leftPanelTab()
0535 {
0536     const auto folder = QString::fromStdString((d->openLocation / LEFT_PANEL_HISTORY).string());
0537     return jsonDocInFolder(folder);
0538 }
0539 
0540 std::vector<QJsonDocument> ProjectFile::rightPanelTab()
0541 {
0542     const auto folder = QString::fromStdString((d->openLocation / RIGHT_PANEL_HISTORY).string());
0543     return jsonDocInFolder(folder);
0544 }
0545 
0546 cpp::result<void, ProjectFileError> ProjectFile::resetCadDatabaseFromCodeDatabase()
0547 {
0548     auto cadDbPath = cadDatabasePath();
0549     try {
0550         std::filesystem::remove(cadDbPath);
0551     } catch (std::filesystem::filesystem_error& err) {
0552         const auto errorMsg = std::string{"Error removing cad database: "} + err.what();
0553         return cpp::fail(ProjectFileError{errorMsg});
0554     }
0555     try {
0556         std::filesystem::copy(codeDatabasePath(), cadDbPath);
0557     } catch (std::filesystem::filesystem_error& err) {
0558         const auto errorMsg = std::string{"Error removing cad database: "} + err.what();
0559         return cpp::fail(ProjectFileError{errorMsg});
0560     }
0561 
0562     lvtmdb::SociWriter writer;
0563     if (!writer.updateDbSchema(cadDbPath.string(), "cad_db.sql")) {
0564         const auto errorMsg = std::string{"Error removing adding cad tables to the database."};
0565         return cpp::fail(ProjectFileError{errorMsg});
0566     }
0567 
0568     return {};
0569 }
0570 
0571 void ProjectFile::prepareSave()
0572 {
0573     for (const auto folder : {LEFT_PANEL_HISTORY, RIGHT_PANEL_HISTORY}) {
0574         QDir full_folder(QString::fromStdString((d->openLocation / folder).string()));
0575         const auto dirEntries = full_folder.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files);
0576         for (const QString& filePath : dirEntries) {
0577             QFile file(full_folder.absolutePath() + QDir::separator() + filePath);
0578             file.remove();
0579         }
0580     }
0581 }
0582 
0583 QList<QString> ProjectFile::bookmarks() const
0584 {
0585     const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string());
0586     QDir dir(folderPath);
0587 
0588     auto dirEntries = dir.entryList(QDir::Filter::NoDotAndDotDot | QDir::Filter::Files);
0589     for (auto& dirEntry : dirEntries) {
0590         dirEntry.replace(".json", "");
0591     }
0592 
0593     qDebug() << dirEntries;
0594     return dirEntries;
0595 }
0596 
0597 QJsonDocument ProjectFile::getBookmark(const QString& name) const
0598 {
0599     const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string());
0600     QFile bookmark(folderPath + QDir::separator() + name + ".json");
0601     if (!bookmark.exists()) {
0602         qDebug() << "Json document doesn't exists";
0603     }
0604     if (!bookmark.open(QIODevice::ReadOnly)) {
0605         qDebug() << "Impossible to load json file";
0606     }
0607 
0608     return QJsonDocument::fromJson(bookmark.readAll());
0609 }
0610 
0611 void ProjectFile::removeBookmark(const QString& name)
0612 {
0613     const auto folderPath = QString::fromStdString((d->openLocation / BOOKMARKS_FOLDER).string());
0614     QFile bookmark(folderPath + QDir::separator() + name + ".json");
0615     if (!bookmark.exists()) {
0616         qDebug() << "Json document doesn't exists" << bookmark.fileName();
0617         return;
0618     }
0619 
0620     if (!bookmark.remove()) {
0621         qDebug() << "Couldn't remove bookmark";
0622         return;
0623     }
0624     setDirty();
0625 
0626     Q_EMIT bookmarksChanged();
0627 }
0628 
0629 [[nodiscard]] cpp::result<void, ProjectFileError> ProjectFile::saveBookmark(const QJsonDocument& doc,
0630                                                                             ProjectFile::BookmarkType bookmarkType)
0631 {
0632     const auto file_id = doc.object()["id"].toString();
0633     const auto folder = [bookmarkType]() -> std::string_view {
0634         switch (bookmarkType) {
0635         case LeftPane:
0636             return LEFT_PANEL_HISTORY;
0637         case RightPane:
0638             return RIGHT_PANEL_HISTORY;
0639         case Bookmark:
0640             return BOOKMARKS_FOLDER;
0641         }
0642 
0643         // there's a bug on compilers (at least g++) and it does not
0644         // realizes I'm actually returning something always from the
0645         // switch case, requiring me to also add a return afterwards.
0646         assert(false && "should never hit here");
0647         return BOOKMARKS_FOLDER;
0648     }();
0649 
0650     QDir dir; // just to call mkpath, this is not a static method.
0651     const auto folderPath = QString::fromStdString((d->openLocation / folder).string());
0652     dir.mkpath(folderPath);
0653 
0654     QFile saveFile(folderPath + QDir::separator() + (file_id + ".json"));
0655     if (!saveFile.open(QIODevice::WriteOnly)) {
0656         return cpp::fail(ProjectFileError{saveFile.errorString().toStdString()});
0657     }
0658 
0659     qint64 bytesSaved = saveFile.write(doc.toJson());
0660     if (bytesSaved == 0) {
0661         return cpp::fail(ProjectFileError{"Error saving bookmark, zero bytes saved."});
0662     }
0663 
0664     if (folder == BOOKMARKS_FOLDER) {
0665         Q_EMIT bookmarksChanged();
0666     }
0667 
0668     setDirty();
0669     return {};
0670 }