File indexing completed on 2024-05-12 05:13:14
0001 /* 0002 SPDX-FileCopyrightText: 2000, 2001 Cornelius Schumacher <schumacher@kde.org> 0003 SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org> 0004 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 0007 */ 0008 0009 #include "eventarchiver.h" 0010 0011 #include "kcalprefs.h" 0012 0013 #include <Akonadi/CalendarUtils> 0014 #include <Akonadi/IncidenceChanger> 0015 0016 #include <KCalendarCore/FileStorage> 0017 #include <KCalendarCore/ICalFormat> 0018 #include <KCalendarCore/MemoryCalendar> 0019 0020 #include <KCalUtils/Stringify> 0021 0022 #include "calendarsupport_debug.h" 0023 #include <KIO/FileCopyJob> 0024 #include <KIO/StatJob> 0025 #include <KJobWidgets> 0026 #include <KLocalizedString> 0027 #include <KMessageBox> 0028 0029 #include <QLocale> 0030 #include <QTemporaryFile> 0031 #include <QTimeZone> 0032 0033 using namespace KCalendarCore; 0034 using namespace KCalUtils; 0035 using namespace CalendarSupport; 0036 0037 class GroupwareScoppedDisabler 0038 { 0039 public: 0040 GroupwareScoppedDisabler(Akonadi::IncidenceChanger *changer) 0041 : m_changer(changer) 0042 { 0043 m_wasEnabled = m_changer->groupwareCommunication(); 0044 m_changer->setGroupwareCommunication(false); 0045 } 0046 0047 ~GroupwareScoppedDisabler() 0048 { 0049 m_changer->setGroupwareCommunication(m_wasEnabled); 0050 } 0051 0052 bool m_wasEnabled = false; 0053 Akonadi::IncidenceChanger *const m_changer; 0054 }; 0055 0056 EventArchiver::EventArchiver(QObject *parent) 0057 : QObject(parent) 0058 { 0059 } 0060 0061 EventArchiver::~EventArchiver() = default; 0062 0063 void EventArchiver::runOnce(const Akonadi::ETMCalendar::Ptr &calendar, Akonadi::IncidenceChanger *changer, QDate limitDate, QWidget *widget) 0064 { 0065 run(calendar, changer, limitDate, widget, true, true); 0066 } 0067 0068 void EventArchiver::runAuto(const Akonadi::ETMCalendar::Ptr &calendar, Akonadi::IncidenceChanger *changer, QWidget *widget, bool withGUI) 0069 { 0070 QDate limitDate(QDate::currentDate()); 0071 const int expiryTime = KCalPrefs::instance()->mExpiryTime; 0072 switch (KCalPrefs::instance()->mExpiryUnit) { 0073 case KCalPrefs::UnitDays: // Days 0074 limitDate = limitDate.addDays(-expiryTime); 0075 break; 0076 case KCalPrefs::UnitWeeks: // Weeks 0077 limitDate = limitDate.addDays(-expiryTime * 7); 0078 break; 0079 case KCalPrefs::UnitMonths: // Months 0080 limitDate = limitDate.addMonths(-expiryTime); 0081 break; 0082 default: 0083 return; 0084 } 0085 run(calendar, changer, limitDate, widget, withGUI, false); 0086 } 0087 0088 void EventArchiver::run(const Akonadi::ETMCalendar::Ptr &calendar, 0089 Akonadi::IncidenceChanger *changer, 0090 QDate limitDate, 0091 QWidget *widget, 0092 bool withGUI, 0093 bool errorIfNone) 0094 { 0095 GroupwareScoppedDisabler disabler(changer); // Disables groupware communication temporarily 0096 0097 // We need to use rawEvents, otherwise events hidden by filters will not be archived. 0098 KCalendarCore::Event::List events; 0099 KCalendarCore::Todo::List todos; 0100 KCalendarCore::Journal::List journals; 0101 0102 if (KCalPrefs::instance()->mArchiveEvents) { 0103 events = calendar->rawEvents(QDate(1769, 12, 1), 0104 // #29555, also advertised by the "limitDate not included" in the class docu 0105 limitDate.addDays(-1), 0106 QTimeZone::systemTimeZone(), 0107 true); 0108 } 0109 if (KCalPrefs::instance()->mArchiveTodos) { 0110 const KCalendarCore::Todo::List rawTodos = calendar->rawTodos(); 0111 0112 for (const KCalendarCore::Todo::Ptr &todo : rawTodos) { 0113 Q_ASSERT(todo); 0114 if (isSubTreeComplete(calendar, todo, limitDate)) { 0115 todos.append(todo); 0116 } 0117 } 0118 } 0119 0120 const KCalendarCore::Incidence::List incidences = calendar->mergeIncidenceList(events, todos, journals); 0121 0122 qCDebug(CALENDARSUPPORT_LOG) << "archiving incidences before" << limitDate << " ->" << incidences.count() << " incidences found."; 0123 if (incidences.isEmpty()) { 0124 if (withGUI && errorIfNone) { 0125 KMessageBox::information(widget, 0126 i18n("There are no items before %1", QLocale::system().toString(limitDate, QLocale::ShortFormat)), 0127 i18nc("@title:window", "Archive"), 0128 QStringLiteral("ArchiverNoIncidences")); 0129 } 0130 return; 0131 } 0132 0133 switch (KCalPrefs::instance()->mArchiveAction) { 0134 case KCalPrefs::actionDelete: 0135 deleteIncidences(changer, limitDate, widget, calendar->itemList(incidences), withGUI); 0136 break; 0137 case KCalPrefs::actionArchive: 0138 archiveIncidences(calendar, changer, limitDate, widget, incidences, withGUI); 0139 break; 0140 } 0141 } 0142 0143 void EventArchiver::deleteIncidences(Akonadi::IncidenceChanger *changer, QDate limitDate, QWidget *widget, const Akonadi::Item::List &items, bool withGUI) 0144 { 0145 QStringList incidenceStrs; 0146 Akonadi::Item::List::ConstIterator it; 0147 Akonadi::Item::List::ConstIterator end(items.constEnd()); 0148 incidenceStrs.reserve(items.count()); 0149 for (it = items.constBegin(); it != end; ++it) { 0150 incidenceStrs.append(Akonadi::CalendarUtils::incidence(*it)->summary()); 0151 } 0152 0153 if (withGUI) { 0154 const int result = KMessageBox::warningContinueCancelList(widget, 0155 i18n("Delete all items before %1 without saving?\n" 0156 "The following items will be deleted:", 0157 QLocale::system().toString(limitDate, QLocale::ShortFormat)), 0158 incidenceStrs, 0159 i18nc("@title:window", "Delete Old Items"), 0160 KStandardGuiItem::del()); 0161 if (result != KMessageBox::Continue) { 0162 return; 0163 } 0164 } 0165 0166 changer->deleteIncidences(items, /**parent=*/widget); 0167 0168 // TODO: Q_EMIT only after hearing back from incidence changer 0169 Q_EMIT eventsDeleted(); 0170 } 0171 0172 void EventArchiver::archiveIncidences(const Akonadi::ETMCalendar::Ptr &calendar, 0173 Akonadi::IncidenceChanger *changer, 0174 QDate limitDate, 0175 QWidget *widget, 0176 const KCalendarCore::Incidence::List &incidences, 0177 bool withGUI) 0178 { 0179 Q_UNUSED(limitDate) 0180 Q_UNUSED(withGUI) 0181 0182 FileStorage storage(calendar); 0183 0184 QString tmpFileName; 0185 // KSaveFile cannot be called with an open File Handle on Windows. 0186 // So we use QTemporaryFile only to generate a unique filename 0187 // and then close/delete the file again. This file must be deleted 0188 // here. 0189 { 0190 QTemporaryFile tmpFile; 0191 tmpFile.open(); 0192 tmpFileName = tmpFile.fileName(); 0193 } 0194 // Save current calendar to disk 0195 storage.setFileName(tmpFileName); 0196 if (!storage.save()) { 0197 qCDebug(CALENDARSUPPORT_LOG) << "Can't save calendar to temp file"; 0198 return; 0199 } 0200 0201 // Duplicate current calendar by loading in new calendar object 0202 MemoryCalendar::Ptr archiveCalendar(new MemoryCalendar(QTimeZone::systemTimeZone())); 0203 0204 FileStorage archiveStore(archiveCalendar); 0205 archiveStore.setFileName(tmpFileName); 0206 auto format = new ICalFormat(); 0207 archiveStore.setSaveFormat(format); 0208 if (!archiveStore.load()) { 0209 qCDebug(CALENDARSUPPORT_LOG) << "Can't load calendar from temp file"; 0210 QFile::remove(tmpFileName); 0211 return; 0212 } 0213 0214 // Strip active events from calendar so that only events to be archived 0215 // remain. This is not really efficient, but there is no other easy way. 0216 QStringList uids; 0217 Incidence::List allIncidences = archiveCalendar->rawIncidences(); 0218 uids.reserve(incidences.count()); 0219 for (const KCalendarCore::Incidence::Ptr &incidence : std::as_const(incidences)) { 0220 uids.append(incidence->uid()); 0221 } 0222 for (const KCalendarCore::Incidence::Ptr &incidence : std::as_const(allIncidences)) { 0223 if (!uids.contains(incidence->uid())) { 0224 archiveCalendar->deleteIncidence(incidence); 0225 } 0226 } 0227 0228 // Get or create the archive file 0229 QUrl archiveURL(KCalPrefs::instance()->mArchiveFile); 0230 QString archiveFile; 0231 QTemporaryFile downloadTempFile; 0232 0233 bool fileExists = false; 0234 if (archiveURL.isLocalFile()) { 0235 fileExists = QFile::exists(archiveURL.toLocalFile()); 0236 } else { 0237 auto job = KIO::stat(archiveURL, KIO::StatJob::SourceSide, KIO::StatBasic); 0238 0239 KJobWidgets::setWindow(job, widget); 0240 fileExists = job->exec(); 0241 } 0242 0243 if (fileExists) { 0244 archiveFile = downloadTempFile.fileName(); 0245 auto job = KIO::file_copy(archiveURL, QUrl::fromLocalFile(archiveFile)); 0246 KJobWidgets::setWindow(job, widget); 0247 if (!job->exec()) { 0248 qCDebug(CALENDARSUPPORT_LOG) << "Can't download archive file"; 0249 QFile::remove(tmpFileName); 0250 return; 0251 } 0252 // Merge with events to be archived. 0253 archiveStore.setFileName(archiveFile); 0254 if (!archiveStore.load()) { 0255 qCDebug(CALENDARSUPPORT_LOG) << "Can't merge with archive file"; 0256 QFile::remove(tmpFileName); 0257 return; 0258 } 0259 } else { 0260 archiveFile = tmpFileName; 0261 } 0262 0263 // Save archive calendar 0264 if (!archiveStore.save()) { 0265 QString errmess; 0266 if (format->exception()) { 0267 errmess = Stringify::errorMessage(*format->exception()); 0268 } else { 0269 errmess = i18nc("save failure cause unknown", "Reason unknown"); 0270 } 0271 KMessageBox::error(widget, i18n("Cannot write archive file %1. %2", archiveStore.fileName(), errmess)); 0272 QFile::remove(tmpFileName); 0273 return; 0274 } 0275 0276 // Upload if necessary 0277 QUrl srcUrl = QUrl::fromLocalFile(archiveFile); 0278 if (srcUrl != archiveURL) { 0279 auto job = KIO::file_copy(QUrl::fromLocalFile(archiveFile), archiveURL); 0280 KJobWidgets::setWindow(job, widget); 0281 if (!job->exec()) { 0282 KMessageBox::error(widget, i18n("Cannot write archive. %1", job->errorString())); 0283 QFile::remove(tmpFileName); 0284 return; 0285 } 0286 } 0287 0288 QFile::remove(tmpFileName); 0289 0290 // We don't want it to ask to send invitations for each incidence. 0291 changer->startAtomicOperation(i18n("Archiving events")); 0292 0293 // Delete archived events from calendar 0294 const Akonadi::Item::List items = calendar->itemList(incidences); 0295 for (const Akonadi::Item &item : items) { 0296 changer->deleteIncidence(item, widget); 0297 } // TODO: Q_EMIT only after hearing back from incidence changer 0298 changer->endAtomicOperation(); 0299 0300 Q_EMIT eventsDeleted(); 0301 } 0302 0303 bool EventArchiver::isSubTreeComplete(const Akonadi::ETMCalendar::Ptr &calendar, const Todo::Ptr &todo, QDate limitDate, QStringList checkedUids) const 0304 { 0305 if (!todo->isCompleted() || todo->completed().date() >= limitDate) { 0306 return false; 0307 } 0308 0309 // This QList is only to prevent infinite recursion 0310 if (checkedUids.contains(todo->uid())) { 0311 // Probably will never happen, calendar.cpp checks for this 0312 qCWarning(CALENDARSUPPORT_LOG) << "To-do hierarchy loop detected!"; 0313 return false; 0314 } 0315 0316 checkedUids.append(todo->uid()); 0317 const KCalendarCore::Incidence::List children = calendar->childIncidences(todo->uid()); 0318 for (const KCalendarCore::Incidence::Ptr &incidence : children) { 0319 const Todo::Ptr t = incidence.dynamicCast<KCalendarCore::Todo>(); 0320 if (t && !isSubTreeComplete(calendar, t, limitDate, checkedUids)) { 0321 return false; 0322 } 0323 } 0324 0325 return true; 0326 } 0327 0328 #include "moc_eventarchiver.cpp"