diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index 2ab99e24..edcb5f86 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,508 +1,508 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FileWriter.h" #include "CompressFileInfo.h" #include "Database.h" #include "ElementWriter.h" #include "Logging.h" #include "NumberedBackup.h" #include "XMLCategory.h" #include #include #include #include #include #include #include #include // // // // +++++++++++++++++++++++++++++++ REMEMBER ++++++++++++++++++++++++++++++++ // // // // // Update XMLDB::Database::fileVersion every time you update the file format! // // // // // // // // // (sorry for the noise, but it is really important :-) using Utilities::StringSet; void XMLDB::FileWriter::save( const QString& fileName, bool isAutoSave ) { setUseCompressedFileFormat( Settings::SettingsData::instance()->useCompressedIndexXML() ); if ( !isAutoSave ) - NumberedBackup().makeNumberedBackup(); + NumberedBackup(m_db->uiDelegate()).makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if ( !out.open(QIODevice::WriteOnly | QIODevice::Text)) { m_db->uiDelegate().sorry( QString::fromUtf8("Error saving to file '%1': %2").arg(out.fileName()).arg(out.errorString()) , i18n("

Could not save the image database to XML.

" "File %1 could not be opened because of the following error: %2" , out.fileName(), out.errorString() ) , i18n("Error while saving...") ); return; } QTime t; if (TimingLog().isDebugEnabled()) t.start(); QXmlStreamWriter writer(&out); writer.setAutoFormatting(true); writer.writeStartDocument(); { ElementWriter dummy(writer, QString::fromLatin1("KPhotoAlbum")); writer.writeAttribute( QString::fromLatin1( "version" ), QString::number(Database::fileVersion())); writer.writeAttribute( QString::fromLatin1( "compressed" ), QString::number(useCompressedFileFormat())); saveCategories( writer ); saveImages( writer ); saveBlockList( writer ); saveMemberGroups( writer ); //saveSettings(writer); } writer.writeEndDocument(); qCDebug(TimingLog) << "XMLDB::FileWriter::save(): Saving took" << t.elapsed() <<"ms"; // State: index.xml has previous DB version, index.xml.tmp has the current version. // original file can be safely deleted if ( ( ! QFile::remove( fileName ) ) && QFile::exists( fileName ) ) { m_db->uiDelegate().sorry( QString::fromUtf8("Removal of file '%1' failed.").arg(fileName) , i18n("

Failed to remove old version of image database.

" "

Please try again or replace the file %1 with file %2 manually!

", fileName, out.fileName() ) , i18n("Error while saving...") ); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if ( ! out.rename( fileName ) ) { m_db->uiDelegate().sorry( QString::fromUtf8("Renaming index.xml to '%1' failed.").arg(out.fileName()) , i18n("

Failed to move temporary XML file to permanent location.

" "

Please try again or rename file %1 to %2 manually!

", out.fileName(), fileName ) , i18n("Error while saving...") ); // State: index.xml.tmp has the current version. return; } // State: index.xml has the current version. } void XMLDB::FileWriter::saveCategories( QXmlStreamWriter& writer ) { QStringList categories = DB::ImageDB::instance()->categoryCollection()->categoryNames(); ElementWriter dummy(writer, QString::fromLatin1("Categories") ); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial( DB::Category::TokensCategory ); for (QString name : categories) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); if (! shouldSaveCategory(name)) { continue; } ElementWriter dummy(writer, QString::fromUtf8("Category")); writer.writeAttribute(QString::fromUtf8("name"), name); writer.writeAttribute(QString::fromUtf8("icon"), category->iconName()); writer.writeAttribute(QString::fromUtf8("show"), QString::number(category->doShow())); writer.writeAttribute(QString::fromUtf8("viewtype"), QString::number(category->viewType())); writer.writeAttribute(QString::fromUtf8("thumbnailsize"), QString::number(category->thumbnailSize())); writer.writeAttribute(QString::fromUtf8("positionable"), QString::number(category->positionable())); if (category == tokensCategory) { writer.writeAttribute(QString::fromUtf8("meta"),QString::fromUtf8("tokens")); } // FIXME (l3u): // Correct me if I'm wrong, but we don't need this, as the tags used as groups are // added to the respective category anyway when they're created, so there's no need to // re-add them here. Apart from this, adding an empty group (one without members) does // add an empty tag ("") doing so. /* QStringList list = Utilities::mergeListsUniqly(category->items(), m_db->_members.groups(name)); */ Q_FOREACH(const QString &tagName, category->items()) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), tagName ); writer.writeAttribute( QString::fromLatin1( "id" ), QString::number(static_cast( category.data() )->idForName( tagName ) )); QDate birthDate = category->birthDate(tagName); if (!birthDate.isNull()) writer.writeAttribute( QString::fromUtf8("birthDate"), birthDate.toString(Qt::ISODate) ); } } } void XMLDB::FileWriter::saveImages( QXmlStreamWriter& writer ) { DB::ImageInfoList list = m_db->m_images; // Copy files from clipboard to end of overview, so we don't loose them Q_FOREACH(const DB::ImageInfoPtr &infoPtr, m_db->m_clipboard) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1( "images" ) ); Q_FOREACH(const DB::ImageInfoPtr &infoPtr, list) { save( writer, infoPtr ); } } } void XMLDB::FileWriter::saveBlockList( QXmlStreamWriter& writer ) { ElementWriter dummy( writer, QString::fromLatin1( "blocklist" ) ); QList blockList = m_db->m_blockList.toList(); // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); Q_FOREACH(const DB::FileName &block, blockList) { ElementWriter dummy( writer, QString::fromLatin1( "block" ) ); writer.writeAttribute( QString::fromLatin1( "file" ), block.relative() ); } } void XMLDB::FileWriter::saveMemberGroups( QXmlStreamWriter& writer ) { if ( m_db->m_members.isEmpty() ) return; ElementWriter dummy( writer, QString::fromLatin1( "member-groups" ) ); for( QMap< QString,QMap >::ConstIterator memberMapIt= m_db->m_members.memberMap().constBegin(); memberMapIt != m_db->m_members.memberMap().constEnd(); ++memberMapIt ) { const QString categoryName = memberMapIt.key(); // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (categoryName.isEmpty()) { continue; } if ( !shouldSaveCategory( categoryName ) ) continue; QMap groupMap = memberMapIt.value(); for( QMap::ConstIterator groupMapIt= groupMap.constBegin(); groupMapIt != groupMap.constEnd(); ++groupMapIt ) { // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (groupMapIt.key().isEmpty()) { continue; } if ( useCompressedFileFormat() ) { StringSet members = groupMapIt.value(); ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), categoryName ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); QStringList idList; Q_FOREACH(const QString& member, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( categoryName ); XMLCategory* category = static_cast( catPtr.data() ); if (category->idForName(member)==0) qCWarning(XMLDBLog) << "Member" << member << "in group" << categoryName << "->" << groupMapIt.key() << "has no id!"; idList.append( QString::number( category->idForName( member ) ) ); } std::sort(idList.begin(), idList.end()); writer.writeAttribute( QString::fromLatin1( "members" ), idList.join( QString::fromLatin1( "," ) ) ); } else { QStringList members = groupMapIt.value().toList(); std::sort(members.begin(), members.end()); Q_FOREACH(const QString& member, members) { ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), memberMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "member" ), member ); } // Add an entry even if the group is empty // (this is not necessary for the compressed format) if (members.size() == 0) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); } } } } } /* Perhaps, we may need this later ;-) void XMLDB::FileWriter::saveSettings(QXmlStreamWriter& writer) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementWriter dummy(writer, settingsString); QMap settings; // For testing settings.insert(QString::fromUtf8("tokensCategory"), QString::fromUtf8("Tokens")); settings.insert(QString::fromUtf8("untaggedCategory"), QString::fromUtf8("Events")); settings.insert(QString::fromUtf8("untaggedTag"), QString::fromUtf8("untagged")); QMapIterator settingsIterator(settings); while (settingsIterator.hasNext()) { ElementWriter dummy(writer, settingString); settingsIterator.next(); writer.writeAttribute(keyString, escape(settingsIterator.key())); writer.writeAttribute(valueString, escape(settingsIterator.value())); } } */ void XMLDB::FileWriter::save( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter dummy( writer, QString::fromLatin1("image") ); writer.writeAttribute( QString::fromLatin1("file"), info->fileName().relative() ); if ( info->label() != QFileInfo(info->fileName().relative()).completeBaseName() ) writer.writeAttribute( QString::fromLatin1("label"), info->label() ); if ( !info->description().isEmpty() ) writer.writeAttribute( QString::fromLatin1("description"), info->description() ); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); writer.writeAttribute( QString::fromLatin1( "startDate" ), start.toString(Qt::ISODate) ); if ( start != end ) writer.writeAttribute( QString::fromLatin1( "endDate" ), end.toString(Qt::ISODate) ); if ( info->angle() != 0 ) writer.writeAttribute( QString::fromLatin1("angle"), QString::number(info->angle())); writer.writeAttribute( QString::fromLatin1( "md5sum" ), info->MD5Sum().toHexString() ); writer.writeAttribute( QString::fromLatin1( "width" ), QString::number(info->size().width())); writer.writeAttribute( QString::fromLatin1( "height" ), QString::number(info->size().height())); if ( info->rating() != -1 ) { writer.writeAttribute( QString::fromLatin1("rating"), QString::number(info->rating())); } if ( info->stackId() ) { writer.writeAttribute( QString::fromLatin1("stackId"), QString::number(info->stackId())); writer.writeAttribute( QString::fromLatin1("stackOrder"), QString::number(info->stackOrder())); } if ( info->isVideo() ) writer.writeAttribute( QLatin1String("videoLength"), QString::number(info->videoLength())); if ( useCompressedFileFormat() ) writeCategoriesCompressed( writer, info ); else writeCategories( writer, info ); } QString XMLDB::FileWriter::areaToString(QRect area) const { QStringList areaString; areaString.append( QString::number(area.x()) ); areaString.append( QString::number(area.y()) ); areaString.append( QString::number(area.width()) ); areaString.append( QString::number(area.height()) ); return areaString.join( QString::fromLatin1(" ") ); } void XMLDB::FileWriter::writeCategories( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter topElm(writer, QString::fromLatin1("options"), false ); QStringList grps = info->availableCategories(); Q_FOREACH(const QString &name, grps) { if ( !shouldSaveCategory( name ) ) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false ); QStringList items = info->itemsOfCategory(name).toList(); std::sort(items.begin(), items.end()); if ( !items.isEmpty() ) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), name ); } Q_FOREACH(const QString& itemValue, items) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), itemValue ); QRect area = info->areaForTag(name, itemValue); if ( ! area.isNull() ) { writer.writeAttribute(QString::fromLatin1("area"), areaToString(area)); } } } } void XMLDB::FileWriter::writeCategoriesCompressed( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { QMap>> positionedTags; QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH(const DB::CategoryPtr &category, categoryList) { QString categoryName = category->name(); if ( !shouldSaveCategory( categoryName ) ) continue; StringSet items = info->itemsOfCategory(categoryName); if ( !items.empty() ) { QStringList idList; Q_FOREACH(const QString &itemValue, items) { QRect area = info->areaForTag(categoryName, itemValue); if ( area.isValid() ) { // Positioned tags can't be stored in the "fast" format // so we have to handle them separately positionedTags[categoryName] << QPair(itemValue, area); } else { int id = static_cast(category.data())->idForName(itemValue); idList.append( QString::number( id ) ); } } // Possibly all ids of a category have area information, so only // write the category attribute if there are actually ids to write if ( !idList.isEmpty() ) { std::sort(idList.begin(), idList.end()); writer.writeAttribute( escape( categoryName ), idList.join( QString::fromLatin1( "," ) ) ); } } } // Add a "readable" sub-element for the positioned tags // FIXME: can this be merged with the code in writeCategories()? if ( ! positionedTags.isEmpty() ) { ElementWriter topElm( writer, QString::fromLatin1("options"), false ); topElm.writeStartElement(); QMapIterator>> categoryWithAreas(positionedTags); while (categoryWithAreas.hasNext()) { categoryWithAreas.next(); ElementWriter categoryElm( writer, QString::fromLatin1("option"), false ); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), categoryWithAreas.key() ); QList> areas = categoryWithAreas.value(); std::sort(areas.begin(),areas.end(), [](QPair a, QPair b) { return a.first < b.first; } ); Q_FOREACH( const auto &positionedTag, areas) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), positionedTag.first ); writer.writeAttribute( QString::fromLatin1("area"), areaToString(positionedTag.second) ); } } } } bool XMLDB::FileWriter::shouldSaveCategory( const QString& categoryName ) const { // Profiling indicated that this function was a hotspot, so this cache improved saving speed with 25% static QHash cache; if ( cache.contains(categoryName)) return cache[categoryName]; // A few bugs has shown up, where an invalid category name has crashed KPA. It therefore checks for such invalid names here. if ( !m_db->m_categoryCollection.categoryForName( categoryName ) ) { qCWarning(XMLDBLog,"Invalid category name: %s", qPrintable(categoryName)); cache.insert(categoryName,false); return false; } const bool shouldSave = dynamic_cast( m_db->m_categoryCollection.categoryForName( categoryName ).data() )->shouldSave(); cache.insert(categoryName,shouldSave); return shouldSave; } /** * @brief Escape problematic characters in a string that forms an XML attribute name. * * N.B.: Attribute values do not need to be escaped! * @see XMLDB::FileReader::unescape * * @param str the string to be escaped * @return the escaped string */ QString XMLDB::FileWriter::escape( const QString& str ) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if ( s_cache.contains(str) ) return s_cache[str]; QString tmp( str ); // Regex to match characters that are not allowed to start XML attribute names const QRegExp rx( QString::fromLatin1( "([^a-zA-Z0-9:_])" ) ); int pos = 0; // Encoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ); QString after; after.sprintf( "_.%0X", rx.cap( 1 ).data()->toLatin1()); tmp.replace( pos, before.length(), after); pos += after.length(); } } else tmp.replace( QString::fromLatin1( " " ), QString::fromLatin1( "_" ) ); s_cache.insert(str,tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/NumberedBackup.cpp b/XMLDB/NumberedBackup.cpp index b3903397..3f0b2646 100644 --- a/XMLDB/NumberedBackup.cpp +++ b/XMLDB/NumberedBackup.cpp @@ -1,110 +1,123 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "NumberedBackup.h" -#include "Settings/SettingsData.h" -#include -#include + +#include +#include +#include + #include -#include -#include -#include "Utilities/FileUtil.h" +#include +#include +#include + +XMLDB::NumberedBackup::NumberedBackup(DB::UIDelegate &ui) + : m_ui(ui) +{ +} void XMLDB::NumberedBackup::makeNumberedBackup() { deleteOldBackupFiles(); int max = getMaxId(); QString fileName; fileName.sprintf( "index.xml~%04d~", max+1 ); if ( !QFileInfo( QString::fromLatin1( "%1/index.xml" ).arg( Settings::SettingsData::instance()->imageDirectory() ) ).exists() ) return; if ( Settings::SettingsData::instance()->compressBackup() ) { QString fileNameWithExt = fileName + QString::fromLatin1( ".zip" ); QString fileAndDir = QString::fromLatin1( "%1/%2" ).arg(Settings::SettingsData::instance()->imageDirectory() ).arg(fileNameWithExt); KZip zip( fileAndDir ); if ( ! zip.open( QIODevice::WriteOnly ) ) { - KMessageBox::error( nullptr, i18n("Error creating zip file %1",fileAndDir) ); + m_ui.error( QString::fromUtf8("Error creating zip file %1").arg(fileAndDir) + , i18n("Error creating zip file %1",fileAndDir) + , i18n("Error Making Numbered Backup") + ); return; } if ( !zip.addLocalFile( QString::fromLatin1( "%1/index.xml" ).arg( Settings::SettingsData::instance()->imageDirectory() ), fileName ) ) { - KMessageBox::error( nullptr, i18n("Error writing file %1 to zip file %2", fileName, fileAndDir) ); + m_ui.error( QString::fromUtf8("Error writing file %1 to zip file %2").arg(fileName).arg(fileAndDir) + , i18n("Error writing file %1 to zip file %2", fileName, fileAndDir) + , i18n("Error Making Numbered Backup") + ); } zip.close(); } else { Utilities::copyOrOverwrite( QString::fromLatin1( "%1/index.xml" ).arg( Settings::SettingsData::instance()->imageDirectory() ), QString::fromLatin1( "%1/%2" ).arg( Settings::SettingsData::instance()->imageDirectory() ).arg( fileName ) ); } } int XMLDB::NumberedBackup::getMaxId() const { QStringList files = backupFiles(); int max = 0; for( QStringList::ConstIterator fileIt = files.constBegin(); fileIt != files.constEnd(); ++fileIt ) { bool OK; max = qMax( max, idForFile( *fileIt, OK ) ); } return max; } QStringList XMLDB::NumberedBackup::backupFiles() const { QDir dir( Settings::SettingsData::instance()->imageDirectory() ); return dir.entryList( QStringList() << QString::fromLatin1( "index.xml~*~*" ), QDir::Files ); } int XMLDB::NumberedBackup::idForFile( const QString& fileName, bool& OK ) const { QRegExp reg( QString::fromLatin1( "index\\.xml~([0-9]+)~(.zip)?" ) ); if ( reg.exactMatch( fileName ) ) { OK = true; return reg.cap(1).toInt(); } else { OK = false; return -1; } } void XMLDB::NumberedBackup::deleteOldBackupFiles() { int maxId = getMaxId(); int maxBackupFiles = Settings::SettingsData::instance()->backupCount(); if ( maxBackupFiles == -1 ) return; QStringList files = backupFiles(); for( QStringList::ConstIterator fileIt = files.constBegin(); fileIt != files.constEnd(); ++fileIt ) { bool OK; int num = idForFile( *fileIt, OK ); if ( OK && num <= maxId+1 - maxBackupFiles ) { (QDir( Settings::SettingsData::instance()->imageDirectory() )).remove( *fileIt ); } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/NumberedBackup.h b/XMLDB/NumberedBackup.h index 5f5172bc..3de95a13 100644 --- a/XMLDB/NumberedBackup.h +++ b/XMLDB/NumberedBackup.h @@ -1,37 +1,44 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef NUMBEREDBACKUP_H #define NUMBEREDBACKUP_H -#include +#include + +namespace DB { +class UIDelegate; +} namespace XMLDB { class NumberedBackup { public: + explicit NumberedBackup( DB::UIDelegate &ui ); void makeNumberedBackup(); protected: int getMaxId() const; QStringList backupFiles() const; int idForFile( const QString& fileName, bool& OK ) const; void deleteOldBackupFiles(); + private: + DB::UIDelegate &m_ui; }; } #endif /* NUMBEREDBACKUP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: