File indexing completed on 2024-09-29 05:07:37
0001 /** 0002 * SPDX-FileCopyrightText: (C) 2006 by Sébastien Laoût <slaout@linux62.org> 0003 * SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "archive.h" 0007 0008 #include <QDebug> 0009 #include <QGuiApplication> 0010 #include <QLocale> 0011 #include <QProgressBar> 0012 #include <QProgressDialog> 0013 #include <QStandardPaths> 0014 #include <QStringList> 0015 #include <QTemporaryDir> 0016 #include <QtCore/QDir> 0017 #include <QtCore/QList> 0018 #include <QtCore/QMap> 0019 #include <QtCore/QString> 0020 #include <QtCore/QStringList> 0021 #include <QtCore/QTextStream> 0022 #include <QtGui/QPainter> 0023 #include <QtGui/QPixmap> 0024 #include <QtXml/QDomDocument> 0025 0026 #include <KAboutData> 0027 #include <KIconLoader> 0028 #include <KLocalizedString> 0029 #include <KMainWindow> //For Global::MainWindow() 0030 #include <KMessageBox> 0031 #include <KTar> 0032 0033 #include "backgroundmanager.h" 0034 #include "basketfactory.h" 0035 #include "basketlistview.h" 0036 #include "basketscene.h" 0037 #include "bnpview.h" 0038 #include "common.h" 0039 #include "formatimporter.h" 0040 #include "global.h" 0041 #include "tag.h" 0042 #include "tools.h" 0043 #include "xmlwork.h" 0044 0045 #include <array> 0046 0047 void Archive::save(BasketScene *basket, bool withSubBaskets, const QString &destination) 0048 { 0049 QDir dir; 0050 QProgressDialog dialog; 0051 dialog.setWindowTitle(i18n("Save as Basket Archive")); 0052 dialog.setLabelText(i18n("Saving as basket archive. Please wait...")); 0053 dialog.setCancelButton(nullptr); 0054 dialog.setAutoClose(true); 0055 0056 dialog.setRange(0, /*Preparation:*/ 1 + /*Finishing:*/ 1 + /*Basket:*/ 1 + /*SubBaskets:*/ (withSubBaskets ? Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket)) : 0)); 0057 dialog.setValue(0); 0058 dialog.show(); 0059 0060 // Create the temporary folder: 0061 QString tempFolder = Global::savesFolder() + "temp-archive/"; 0062 dir.mkdir(tempFolder); 0063 0064 // Create the temporary archive file: 0065 QString tempDestination = tempFolder + "temp-archive.tar.gz"; 0066 KTar tar(tempDestination, "application/x-gzip"); 0067 tar.open(QIODevice::WriteOnly); 0068 tar.writeDir(QStringLiteral("baskets"), QString(), QString()); 0069 0070 dialog.setValue(dialog.value() + 1); // Preparation finished 0071 0072 qDebug() << "Preparation finished out of " << dialog.maximum(); 0073 0074 // Copy the baskets data into the archive: 0075 QStringList backgrounds; 0076 Archive::saveBasketToArchive(basket, withSubBaskets, &tar, backgrounds, tempFolder, &dialog); 0077 0078 // Create a Small baskets.xml Document: 0079 QString data; 0080 QXmlStreamWriter stream(&data); 0081 XMLWork::setupXmlStream(stream, "basketTree"); 0082 Global::bnpView->saveSubHierarchy(Global::bnpView->listViewItemForBasket(basket), stream, withSubBaskets); 0083 stream.writeEndElement(); 0084 stream.writeEndDocument(); 0085 FileStorage::safelySaveToFile(tempFolder + "baskets.xml", data); 0086 tar.addLocalFile(tempFolder + "baskets.xml", "baskets/baskets.xml"); 0087 dir.remove(tempFolder + "baskets.xml"); 0088 0089 // Save a Small tags.xml Document: 0090 QList<Tag *> tags; 0091 listUsedTags(basket, withSubBaskets, tags); 0092 Tag::saveTagsTo(tags, tempFolder + "tags.xml"); 0093 tar.addLocalFile(tempFolder + "tags.xml", "tags.xml"); 0094 dir.remove(tempFolder + "tags.xml"); 0095 0096 // Save Tag Emblems (in case they are loaded on a computer that do not have those icons): 0097 QString tempIconFile = tempFolder + "icon.png"; 0098 for (Tag::List::iterator it = tags.begin(); it != tags.end(); ++it) { 0099 State::List states = (*it)->states(); 0100 for (State::List::iterator it2 = states.begin(); it2 != states.end(); ++it2) { 0101 State *state = (*it2); 0102 QPixmap icon = KIconLoader::global()->loadIcon(state->emblem(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), nullptr, true); 0103 if (!icon.isNull()) { 0104 icon.save(tempIconFile, "PNG"); 0105 QString iconFileName = state->emblem().replace('/', '_'); 0106 tar.addLocalFile(tempIconFile, "tag-emblems/" + iconFileName); 0107 } 0108 } 0109 } 0110 dir.remove(tempIconFile); 0111 0112 // Finish Tar.Gz Exportation: 0113 tar.close(); 0114 0115 // Computing the File Preview: 0116 BasketScene *previewBasket = basket; // FIXME: Use the first non-empty basket! 0117 // QPixmap previewPixmap(previewBasket->visibleWidth(), previewBasket->visibleHeight()); 0118 QPixmap previewPixmap(previewBasket->width(), previewBasket->height()); 0119 QPainter painter(&previewPixmap); 0120 // Save old state, and make the look clean ("smile, you are filmed!"): 0121 NoteSelection *selection = previewBasket->selectedNotes(); 0122 previewBasket->unselectAll(); 0123 Note *focusedNote = previewBasket->focusedNote(); 0124 previewBasket->setFocusedNote(nullptr); 0125 previewBasket->doHoverEffects(nullptr, Note::None); 0126 // Take the screenshot: 0127 previewBasket->render(&painter); 0128 // Go back to the old look: 0129 previewBasket->selectSelection(selection); 0130 previewBasket->setFocusedNote(focusedNote); 0131 previewBasket->doHoverEffects(); 0132 // End and save our splandid painting: 0133 painter.end(); 0134 QImage previewImage = previewPixmap.toImage(); 0135 const int PREVIEW_SIZE = 256; 0136 previewImage = previewImage.scaled(PREVIEW_SIZE, PREVIEW_SIZE, Qt::KeepAspectRatio); 0137 previewImage.save(tempFolder + "preview.png", "PNG"); 0138 0139 // Finally Save to the Real Destination file: 0140 QFile file(destination); 0141 if (file.open(QIODevice::WriteOnly)) { 0142 ulong previewSize = QFile(tempFolder + "preview.png").size(); 0143 ulong archiveSize = QFile(tempDestination).size(); 0144 QTextStream stream(&file); 0145 stream.setCodec("ISO-8859-1"); 0146 stream << "BasKetNP:archive\n" 0147 << "version:0.6.1\n" 0148 // << "read-compatible:0.6.1\n" 0149 // << "write-compatible:0.6.1\n" 0150 << "preview*:" << previewSize << "\n"; 0151 0152 stream.flush(); 0153 // Copy the Preview File: 0154 const unsigned long BUFFER_SIZE = 1024; 0155 char *buffer = new char[BUFFER_SIZE]; 0156 long sizeRead; 0157 QFile previewFile(tempFolder + "preview.png"); 0158 if (previewFile.open(QIODevice::ReadOnly)) { 0159 while ((sizeRead = previewFile.read(buffer, BUFFER_SIZE)) > 0) 0160 file.write(buffer, sizeRead); 0161 } 0162 stream << "archive*:" << archiveSize << "\n"; 0163 stream.flush(); 0164 0165 // Copy the Archive File: 0166 QFile archiveFile(tempDestination); 0167 if (archiveFile.open(QIODevice::ReadOnly)) { 0168 while ((sizeRead = archiveFile.read(buffer, BUFFER_SIZE)) > 0) 0169 file.write(buffer, sizeRead); 0170 } 0171 // Clean Up: 0172 delete[] buffer; 0173 buffer = nullptr; 0174 file.close(); 0175 } 0176 0177 dialog.setValue(dialog.value() + 1); // Finishing finished 0178 qDebug() << "Finishing finished"; 0179 0180 // Clean Up Everything: 0181 dir.remove(tempFolder + "preview.png"); 0182 dir.remove(tempDestination); 0183 dir.rmdir(tempFolder); 0184 } 0185 0186 void Archive::saveBasketToArchive(BasketScene *basket, bool recursive, KTar *tar, QStringList &backgrounds, const QString &tempFolder, QProgressDialog *progress) 0187 { 0188 // Basket need to be loaded for tags exportation. 0189 // We load it NOW so that the progress bar really reflect the state of the exportation: 0190 if (!basket->isLoaded()) { 0191 basket->load(); 0192 } 0193 0194 QDir dir; 0195 // Save basket data: 0196 tar->addLocalDirectory(basket->fullPath(), "baskets/" + basket->folderName()); 0197 // Save basket icon: 0198 QString tempIconFile = tempFolder + "icon.png"; 0199 if (!basket->icon().isEmpty() && basket->icon() != "basket") { 0200 QPixmap icon = KIconLoader::global()->loadIcon(basket->icon(), KIconLoader::Small, 16, KIconLoader::DefaultState, QStringList(), /*path_store=*/nullptr, /*canReturnNull=*/true); 0201 if (!icon.isNull()) { 0202 icon.save(tempIconFile, "PNG"); 0203 QString iconFileName = basket->icon().replace('/', '_'); 0204 tar->addLocalFile(tempIconFile, "basket-icons/" + iconFileName); 0205 } 0206 } 0207 // Save basket background image: 0208 QString imageName = basket->backgroundImageName(); 0209 if (!basket->backgroundImageName().isEmpty() && !backgrounds.contains(imageName)) { 0210 QString backgroundPath = Global::backgroundManager->pathForImageName(imageName); 0211 if (!backgroundPath.isEmpty()) { 0212 // Save the background image: 0213 tar->addLocalFile(backgroundPath, "backgrounds/" + imageName); 0214 // Save the preview image: 0215 QString previewPath = Global::backgroundManager->previewPathForImageName(imageName); 0216 if (!previewPath.isEmpty()) 0217 tar->addLocalFile(previewPath, "backgrounds/previews/" + imageName); 0218 // Save the configuration file: 0219 QString configPath = backgroundPath + ".config"; 0220 if (dir.exists(configPath)) 0221 tar->addLocalFile(configPath, "backgrounds/" + imageName + ".config"); 0222 } 0223 backgrounds.append(imageName); 0224 } 0225 0226 progress->setValue(progress->value() + 1); // Basket exportation finished 0227 qDebug() << basket->basketName() << " finished"; 0228 0229 // Recursively save child baskets: 0230 BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); 0231 if (recursive) { 0232 for (int i = 0; i < item->childCount(); i++) { 0233 saveBasketToArchive(((BasketListViewItem *)item->child(i))->basket(), recursive, tar, backgrounds, tempFolder, progress); 0234 } 0235 } 0236 } 0237 0238 void Archive::listUsedTags(BasketScene *basket, bool recursive, QList<Tag *> &list) 0239 { 0240 basket->listUsedTags(list); 0241 BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); 0242 if (recursive) { 0243 for (int i = 0; i < item->childCount(); i++) { 0244 listUsedTags(((BasketListViewItem *)item->child(i))->basket(), recursive, list); 0245 } 0246 } 0247 } 0248 0249 void Archive::open(const QString &path) 0250 { 0251 // Use the temporary folder: 0252 QString tempFolder = Global::savesFolder() + "temp-archive/"; 0253 0254 switch (extractArchive(path, tempFolder, false)) { 0255 case IOErrorCode::FailedToOpenResource: 0256 KMessageBox::error(nullptr, i18n("Failed to open a file resource."), i18n("Basket Archive Error")); 0257 break; 0258 case IOErrorCode::NotABasketArchive: 0259 KMessageBox::error(nullptr, i18n("This file is not a basket archive."), i18n("Basket Archive Error")); 0260 break; 0261 case IOErrorCode::CorruptedBasketArchive: 0262 KMessageBox::error(nullptr, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); 0263 break; 0264 case IOErrorCode::DestinationExists: 0265 KMessageBox::error(nullptr, i18n("Extraction path already exists."), i18n("Basket Archive Error")); 0266 break; 0267 case IOErrorCode::IncompatibleBasketVersion: 0268 KMessageBox::error(nullptr, 0269 i18n("This file was created with a recent version of %1." 0270 "Please upgrade to a newer version to be able to open that file.", 0271 QGuiApplication::applicationDisplayName()), 0272 i18n("Basket Archive Error")); 0273 break; 0274 case IOErrorCode::PossiblyCompatibleBasketVersion: 0275 KMessageBox::information(nullptr, 0276 i18n("This file was created with a recent version of %1. " 0277 "It can be opened but not every information will be available to you. " 0278 "For instance, some notes may be missing because they are of a type only available in new versions. " 0279 "When saving the file back, consider to save it to another file, to preserve the original one.", 0280 QGuiApplication::applicationDisplayName()), 0281 i18n("Basket Archive Error")); 0282 [[fallthrough]]; 0283 case IOErrorCode::NoError: 0284 if (Global::activeMainWindow()) { 0285 Global::activeMainWindow()->raise(); 0286 } 0287 // Import the Tags: 0288 0289 importTagEmblems(tempFolder); // Import and rename tag emblems BEFORE loading them! 0290 QMap<QString, QString> mergedStates = Tag::loadTags(tempFolder + "tags.xml"); 0291 if (mergedStates.count() > 0) { 0292 Tag::saveTags(); 0293 } 0294 0295 // Import the Background Images: 0296 importArchivedBackgroundImages(tempFolder); 0297 0298 // Import the Baskets: 0299 renameBasketFolders(tempFolder, mergedStates); 0300 0301 Tools::deleteRecursively(tempFolder); 0302 break; 0303 } 0304 } 0305 0306 Archive::IOErrorCode Archive::extractArchive(const QString &path, const QString &destination, const bool protectDestination) 0307 { 0308 IOErrorCode retCode = IOErrorCode::NoError; 0309 0310 QString l_destination; 0311 0312 // derive name of the extraction directory 0313 if (destination.isEmpty()) { 0314 // have the decoded baskets the same name as the archive 0315 l_destination = QFileInfo(path).path() + QDir::separator() + QFileInfo(path).baseName() + "-source"; 0316 } else { 0317 l_destination = QDir::cleanPath(destination); 0318 } 0319 0320 QDir dir(l_destination); 0321 0322 // do nothing when writeProtected 0323 if (dir.exists() && protectDestination) { 0324 return IOErrorCode::DestinationExists; 0325 } 0326 0327 // Create directory and delete its content in case it was not empty 0328 if (!dir.removeRecursively()) { 0329 return IOErrorCode::FailedToOpenResource; 0330 } 0331 dir.mkpath(QStringLiteral(".")); 0332 0333 const qint64 BUFFER_SIZE = 1024; 0334 0335 QFile file(path); 0336 if (file.open(QIODevice::ReadOnly)) { 0337 QTextStream stream(&file); 0338 stream.setCodec("ISO-8859-1"); 0339 QString line = stream.readLine(); 0340 if (line != "BasKetNP:archive") { 0341 file.close(); 0342 Tools::deleteRecursively(l_destination); 0343 return IOErrorCode::NotABasketArchive; 0344 } 0345 QString version; 0346 QStringList readCompatibleVersions; 0347 QStringList writeCompatibleVersions; 0348 while (!stream.atEnd()) { 0349 // Get Key/Value Pair From the Line to Read: 0350 line = stream.readLine(); 0351 int index = line.indexOf(':'); 0352 QString key; 0353 QString value; 0354 if (index >= 0) { 0355 key = line.left(index); 0356 value = line.right(line.length() - index - 1); 0357 } else { 0358 key = line; 0359 value = QString(); 0360 } 0361 if (key == "version") { 0362 version = value; 0363 } else if (key == "read-compatible") { 0364 readCompatibleVersions = value.split(';'); 0365 } else if (key == "write-compatible") { 0366 writeCompatibleVersions = value.split(';'); 0367 } else if (key == "preview*") { 0368 bool ok; 0369 const qint64 size = value.toULong(&ok); 0370 if (!ok) { 0371 file.close(); 0372 Tools::deleteRecursively(l_destination); 0373 return IOErrorCode::CorruptedBasketArchive; 0374 } 0375 // Get the preview file: 0376 QFile previewFile(dir.absolutePath() + QDir::separator() + "preview.png"); 0377 if (previewFile.open(QIODevice::WriteOnly)) { 0378 std::array<char, BUFFER_SIZE> buffer{}; 0379 qint64 remainingBytes = size; 0380 qint64 sizeRead = 0; 0381 file.seek(stream.pos()); 0382 0383 while ((sizeRead = file.read(buffer.data(), qMin(BUFFER_SIZE, remainingBytes))) > 0) { 0384 previewFile.write(buffer.data(), sizeRead); 0385 remainingBytes -= sizeRead; 0386 } 0387 previewFile.close(); 0388 } 0389 stream.seek(stream.pos() + size); 0390 } else if (key == "archive*") { 0391 if (version != "0.6.1" && readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { 0392 retCode = IOErrorCode::PossiblyCompatibleBasketVersion; 0393 } 0394 if (version != "0.6.1" && !readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { 0395 file.close(); 0396 Tools::deleteRecursively(l_destination); 0397 return IOErrorCode::IncompatibleBasketVersion; 0398 } 0399 0400 bool ok; 0401 qint64 size = value.toULong(&ok); 0402 if (!ok) { 0403 file.close(); 0404 Tools::deleteRecursively(l_destination); 0405 return IOErrorCode::CorruptedBasketArchive; 0406 } 0407 0408 // Get the archive file and extract it to destination: 0409 QTemporaryDir tempDir; 0410 if (!tempDir.isValid()) { 0411 return IOErrorCode::FailedToOpenResource; 0412 } 0413 QString tempArchive = tempDir.path() + QDir::separator() + "temp-archive.tar.gz"; 0414 QFile archiveFile(tempArchive); 0415 file.seek(stream.pos()); 0416 if (archiveFile.open(QIODevice::WriteOnly)) { 0417 char *buffer = new char[BUFFER_SIZE]; 0418 qint64 sizeRead; 0419 while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) { 0420 archiveFile.write(buffer, sizeRead); 0421 size -= sizeRead; 0422 } 0423 archiveFile.close(); 0424 delete[] buffer; 0425 0426 // Extract the Archive: 0427 KTar tar(tempArchive, "application/x-gzip"); 0428 tar.open(QIODevice::ReadOnly); 0429 tar.directory()->copyTo(l_destination); 0430 tar.close(); 0431 0432 stream.seek(file.pos()); 0433 } 0434 } else if (key.endsWith('*')) { 0435 // We do not know what it is, but we should read the embedded-file in 0436 // order to discard it: 0437 bool ok; 0438 qint64 size = value.toULong(&ok); 0439 if (!ok) { 0440 file.close(); 0441 Tools::deleteRecursively(l_destination); 0442 return IOErrorCode::CorruptedBasketArchive; 0443 } 0444 // Get the archive file: 0445 char *buffer = new char[BUFFER_SIZE]; 0446 qint64 sizeRead; 0447 while ((sizeRead = file.read(buffer, qMin(BUFFER_SIZE, size))) > 0) { 0448 size -= sizeRead; 0449 } 0450 delete[] buffer; 0451 } else { 0452 // We do not know what it is, and we do not care. 0453 } 0454 // Analyze the Value, if Understood: 0455 } 0456 file.close(); 0457 } 0458 0459 return retCode; 0460 } 0461 0462 Archive::IOErrorCode Archive::createArchiveFromSource(const QString &sourcePath, const QString &previewImage, const QString &destination, const bool protectDestination) 0463 { 0464 QDir source(sourcePath); 0465 QFileInfo destinationFile(destination); 0466 0467 // sourcePath must be a valid directory 0468 if (!source.exists()) { 0469 return IOErrorCode::FailedToOpenResource; 0470 } 0471 0472 // destinationFile must not previously exist; 0473 if (destinationFile.exists() && protectDestination) { 0474 return IOErrorCode::DestinationExists; 0475 } 0476 0477 QTemporaryDir tempDir; 0478 if (!tempDir.isValid()) { 0479 return IOErrorCode::FailedToOpenResource; 0480 } 0481 0482 // Create the temporary archive file: 0483 QString tempDestinationFile = tempDir.path() + QDir::separator() + "temp-archive.tar.gz"; 0484 KTar archive(tempDestinationFile, "application/x-gzip"); 0485 0486 // Prepare the archive for writing. 0487 if (!archive.open(QIODevice::WriteOnly)) { 0488 // Failed to open file. 0489 archive.close(); 0490 Tools::deleteRecursively(tempDir.path()); 0491 return IOErrorCode::FailedToOpenResource; 0492 } 0493 0494 // Add files and directories to tar archive 0495 auto sourceFiles = source.entryList(QDir::Files); 0496 sourceFiles.removeOne("preview.png"); 0497 std::for_each(sourceFiles.constBegin(), sourceFiles.constEnd(), [&](const QString &entry) { 0498 archive.addLocalFile(source.absolutePath() + QDir::separator() + entry, entry); 0499 }); 0500 const auto sourceDirectories = source.entryList(QDir::Dirs | QDir::NoDotAndDotDot); 0501 std::for_each(sourceDirectories.constBegin(), sourceDirectories.constEnd(), [&](const QString &entry) { 0502 archive.addLocalDirectory(source.absolutePath() + QDir::separator() + entry, entry); 0503 }); 0504 0505 archive.close(); 0506 0507 // use generic basket icon as preview if no valid image supplied 0508 /// \todo write a way to create preview the way it's done in Archive::save 0509 QString previewImagePath = previewImage; 0510 if (previewImage.isEmpty() && !QFileInfo(previewImage).exists()) { 0511 previewImagePath = ":/images/128-apps-org.kde.basket.png"; 0512 } 0513 0514 // Finally Save to the Real Destination file: 0515 QFile file(destination); 0516 if (file.open(QIODevice::WriteOnly)) { 0517 ulong previewSize = QFile(previewImagePath).size(); 0518 ulong archiveSize = QFile(tempDestinationFile).size(); 0519 QTextStream stream(&file); 0520 stream.setCodec("ISO-8859-1"); 0521 stream << "BasKetNP:archive\n" 0522 << "version:0.6.1\n" 0523 // << "read-compatible:0.6.1\n" 0524 // << "write-compatible:0.6.1\n" 0525 << "preview*:" << previewSize << "\n"; 0526 0527 stream.flush(); 0528 // Copy the Preview File: 0529 const unsigned long BUFFER_SIZE = 1024; 0530 char *buffer = new char[BUFFER_SIZE]; 0531 long sizeRead; 0532 QFile previewFile(previewImagePath); 0533 if (previewFile.open(QIODevice::ReadOnly)) { 0534 while ((sizeRead = previewFile.read(buffer, BUFFER_SIZE)) > 0) 0535 file.write(buffer, sizeRead); 0536 } 0537 stream << "archive*:" << archiveSize << "\n"; 0538 stream.flush(); 0539 0540 // Copy the Archive File: 0541 QFile archiveFile(tempDestinationFile); 0542 if (archiveFile.open(QIODevice::ReadOnly)) { 0543 while ((sizeRead = archiveFile.read(buffer, BUFFER_SIZE)) > 0) 0544 file.write(buffer, sizeRead); 0545 } 0546 // Clean Up: 0547 delete[] buffer; 0548 buffer = nullptr; 0549 file.close(); 0550 } 0551 0552 return IOErrorCode::NoError; 0553 } 0554 0555 /** 0556 * When opening a basket archive that come from another computer, 0557 * it can contains tags that use icons (emblems) that are not present on that computer. 0558 * Fortunately, basket archives contains a copy of every used icons. 0559 * This method check for every emblems and import the missing ones. 0560 * It also modify the tags.xml copy for the emblems to point to the absolute path of the imported icons. 0561 */ 0562 void Archive::importTagEmblems(const QString &extractionFolder) 0563 { 0564 QDomDocument *document = XMLWork::openFile("basketTags", extractionFolder + "tags.xml"); 0565 if (document == nullptr) 0566 return; 0567 QDomElement docElem = document->documentElement(); 0568 0569 QDir dir; 0570 dir.mkdir(Global::savesFolder() + "tag-emblems/"); 0571 FormatImporter copier; // Only used to copy files synchronously 0572 0573 QDomNode node = docElem.firstChild(); 0574 while (!node.isNull()) { 0575 QDomElement element = node.toElement(); 0576 if ((!element.isNull()) && element.tagName() == "tag") { 0577 QDomNode subNode = element.firstChild(); 0578 while (!subNode.isNull()) { 0579 QDomElement subElement = subNode.toElement(); 0580 if ((!subElement.isNull()) && subElement.tagName() == "state") { 0581 QString emblemName = XMLWork::getElementText(subElement, "emblem"); 0582 if (!emblemName.isEmpty()) { 0583 QPixmap emblem = KIconLoader::global()->loadIcon(emblemName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); 0584 // The icon does not exists on that computer, import it: 0585 if (emblem.isNull()) { 0586 // Of the emblem path was eg. "/home/seb/emblem.png", it was exported as "tag-emblems/_home_seb_emblem.png". 0587 // So we need to copy that image to "~/.local/share/basket/tag-emblems/emblem.png": 0588 int slashIndex = emblemName.lastIndexOf('/'); 0589 QString emblemFileName = (slashIndex < 0 ? emblemName : emblemName.right(slashIndex - 2)); 0590 QString source = extractionFolder + "tag-emblems/" + emblemName.replace('/', '_'); 0591 QString destination = Global::savesFolder() + "tag-emblems/" + emblemFileName; 0592 if (!dir.exists(destination) && dir.exists(source)) 0593 copier.copyFolder(source, destination); 0594 // Replace the emblem path in the tags.xml copy: 0595 QDomElement emblemElement = XMLWork::getElement(subElement, "emblem"); 0596 subElement.removeChild(emblemElement); 0597 XMLWork::addElement(*document, subElement, "emblem", destination); 0598 } 0599 } 0600 } 0601 subNode = subNode.nextSibling(); 0602 } 0603 } 0604 node = node.nextSibling(); 0605 } 0606 FileStorage::safelySaveToFile(extractionFolder + "tags.xml", document->toString()); 0607 } 0608 0609 void Archive::importArchivedBackgroundImages(const QString &extractionFolder) 0610 { 0611 FormatImporter copier; // Only used to copy files synchronously 0612 QString destFolder = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/basket/backgrounds/"; 0613 QDir().mkpath(destFolder); // does not exist at the first run when addWelcomeBaskets is called 0614 0615 QDir dir(extractionFolder + "backgrounds/", /*nameFilder=*/"*.png", /*sortSpec=*/QDir::Name | QDir::IgnoreCase, /*filterSpec=*/QDir::Files | QDir::NoSymLinks); 0616 QStringList files = dir.entryList(); 0617 for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) { 0618 QString image = *it; 0619 if (!Global::backgroundManager->exists(image)) { 0620 // Copy images: 0621 QString imageSource = extractionFolder + "backgrounds/" + image; 0622 QString imageDest = destFolder + image; 0623 copier.copyFolder(imageSource, imageDest); 0624 // Copy configuration file: 0625 QString configSource = extractionFolder + "backgrounds/" + image + ".config"; 0626 QString configDest = destFolder + image; 0627 if (dir.exists(configSource)) 0628 copier.copyFolder(configSource, configDest); 0629 // Copy preview: 0630 QString previewSource = extractionFolder + "backgrounds/previews/" + image; 0631 QString previewDest = destFolder + "previews/" + image; 0632 if (dir.exists(previewSource)) { 0633 dir.mkdir(destFolder + "previews/"); // Make sure the folder exists! 0634 copier.copyFolder(previewSource, previewDest); 0635 } 0636 // Append image to database: 0637 Global::backgroundManager->addImage(imageDest); 0638 } 0639 } 0640 } 0641 0642 void Archive::renameBasketFolders(const QString &extractionFolder, QMap<QString, QString> &mergedStates) 0643 { 0644 QDomDocument *doc = XMLWork::openFile("basketTree", extractionFolder + "baskets/baskets.xml"); 0645 if (doc != nullptr) { 0646 QMap<QString, QString> folderMap; 0647 QDomElement docElem = doc->documentElement(); 0648 QDomNode node = docElem.firstChild(); 0649 renameBasketFolder(extractionFolder, node, folderMap, mergedStates); 0650 loadExtractedBaskets(extractionFolder, node, folderMap, nullptr); 0651 } 0652 } 0653 0654 void Archive::renameBasketFolder(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, QMap<QString, QString> &mergedStates) 0655 { 0656 QDomNode n = basketNode; 0657 while (!n.isNull()) { 0658 QDomElement element = n.toElement(); 0659 if ((!element.isNull()) && element.tagName() == "basket") { 0660 QString folderName = element.attribute("folderName"); 0661 if (!folderName.isEmpty()) { 0662 // Find a folder name: 0663 QString newFolderName = BasketFactory::newFolderName(); 0664 folderMap[folderName] = newFolderName; 0665 // Reserve the folder name: 0666 QDir dir; 0667 dir.mkdir(Global::basketsFolder() + newFolderName); 0668 // Rename the merged tag ids: 0669 // if (mergedStates.count() > 0) { 0670 renameMergedStatesAndBasketIcon(extractionFolder + "baskets/" + folderName + ".basket", mergedStates, extractionFolder); 0671 // } 0672 // Child baskets: 0673 QDomNode node = element.firstChild(); 0674 renameBasketFolder(extractionFolder, node, folderMap, mergedStates); 0675 } 0676 } 0677 n = n.nextSibling(); 0678 } 0679 } 0680 0681 void Archive::renameMergedStatesAndBasketIcon(const QString &fullPath, QMap<QString, QString> &mergedStates, const QString &extractionFolder) 0682 { 0683 QDomDocument *doc = XMLWork::openFile("basket", fullPath); 0684 if (doc == nullptr) 0685 return; 0686 QDomElement docElem = doc->documentElement(); 0687 QDomElement properties = XMLWork::getElement(docElem, "properties"); 0688 importBasketIcon(properties, extractionFolder); 0689 QDomElement notes = XMLWork::getElement(docElem, "notes"); 0690 if (mergedStates.count() > 0) 0691 renameMergedStates(notes, mergedStates); 0692 FileStorage::safelySaveToFile(fullPath, /*"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + */ doc->toString()); 0693 } 0694 0695 void Archive::importBasketIcon(QDomElement properties, const QString &extractionFolder) 0696 { 0697 QString iconName = XMLWork::getElementText(properties, "icon"); 0698 if (!iconName.isEmpty() && iconName != "basket") { 0699 QPixmap icon = KIconLoader::global()->loadIcon(iconName, KIconLoader::NoGroup, 16, KIconLoader::DefaultState, QStringList(), nullptr, /*canReturnNull=*/true); 0700 // The icon does not exists on that computer, import it: 0701 if (icon.isNull()) { 0702 QDir dir; 0703 dir.mkdir(Global::savesFolder() + "basket-icons/"); 0704 FormatImporter copier; // Only used to copy files synchronously 0705 // Of the icon path was eg. "/home/seb/icon.png", it was exported as "basket-icons/_home_seb_icon.png". 0706 // So we need to copy that image to "~/.local/share/basket/basket-icons/icon.png": 0707 int slashIndex = iconName.lastIndexOf('/'); 0708 QString iconFileName = (slashIndex < 0 ? iconName : iconName.right(slashIndex - 2)); 0709 QString source = extractionFolder + "basket-icons/" + iconName.replace('/', '_'); 0710 QString destination = Global::savesFolder() + "basket-icons/" + iconFileName; 0711 if (!dir.exists(destination)) 0712 copier.copyFolder(source, destination); 0713 // Replace the emblem path in the tags.xml copy: 0714 QDomElement iconElement = XMLWork::getElement(properties, "icon"); 0715 properties.removeChild(iconElement); 0716 QDomDocument document = properties.ownerDocument(); 0717 XMLWork::addElement(document, properties, "icon", destination); 0718 } 0719 } 0720 } 0721 0722 void Archive::renameMergedStates(QDomNode notes, QMap<QString, QString> &mergedStates) 0723 { 0724 QDomNode n = notes.firstChild(); 0725 while (!n.isNull()) { 0726 QDomElement element = n.toElement(); 0727 if (!element.isNull()) { 0728 if (element.tagName() == "group") { 0729 renameMergedStates(n, mergedStates); 0730 } else if (element.tagName() == "note") { 0731 QString tags = XMLWork::getElementText(element, "tags"); 0732 if (!tags.isEmpty()) { 0733 QStringList tagNames = tags.split(';'); 0734 for (QStringList::Iterator it = tagNames.begin(); it != tagNames.end(); ++it) { 0735 QString &tag = *it; 0736 if (mergedStates.contains(tag)) { 0737 tag = mergedStates[tag]; 0738 } 0739 } 0740 QString newTags = tagNames.join(";"); 0741 QDomElement tagsElement = XMLWork::getElement(element, "tags"); 0742 element.removeChild(tagsElement); 0743 QDomDocument document = element.ownerDocument(); 0744 XMLWork::addElement(document, element, "tags", newTags); 0745 } 0746 } 0747 } 0748 n = n.nextSibling(); 0749 } 0750 } 0751 0752 void Archive::loadExtractedBaskets(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, BasketScene *parent) 0753 { 0754 bool basketSetAsCurrent = (parent != nullptr); 0755 QDomNode n = basketNode; 0756 while (!n.isNull()) { 0757 QDomElement element = n.toElement(); 0758 if ((!element.isNull()) && element.tagName() == "basket") { 0759 QString folderName = element.attribute("folderName"); 0760 if (!folderName.isEmpty()) { 0761 // Move the basket folder to its destination, while renaming it uniquely: 0762 QString newFolderName = folderMap[folderName]; 0763 FormatImporter copier; 0764 // The folder has been "reserved" by creating it. Avoid asking the user to override: 0765 QDir dir; 0766 dir.rmdir(Global::basketsFolder() + newFolderName); 0767 copier.moveFolder(extractionFolder + "baskets/" + folderName, Global::basketsFolder() + newFolderName); 0768 // Append and load the basket in the tree: 0769 BasketScene *basket = Global::bnpView->loadBasket(newFolderName); 0770 BasketListViewItem *basketItem = Global::bnpView->appendBasket(basket, (basket && parent ? Global::bnpView->listViewItemForBasket(parent) : nullptr)); 0771 basketItem->setExpanded(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false)); 0772 QDomElement properties = XMLWork::getElement(element, "properties"); 0773 importBasketIcon(properties, extractionFolder); // Rename the icon fileName if necessary 0774 basket->loadProperties(properties); 0775 // Open the first basket of the archive: 0776 if (!basketSetAsCurrent) { 0777 Global::bnpView->setCurrentBasket(basket); 0778 basketSetAsCurrent = true; 0779 } 0780 QDomNode node = element.firstChild(); 0781 loadExtractedBaskets(extractionFolder, node, folderMap, basket); 0782 } 0783 } 0784 n = n.nextSibling(); 0785 } 0786 }