[Kde-pim] [kdepim] console: New command line tool: kdepim/console/calendarjanitor.
Sergio Martins
iamsergio at gmail.com
Sat Aug 17 19:46:01 BST 2013
Git commit b36122a8743cc48d37e5f0384ad6bccc9308c478 by Sergio Martins.
Committed on 14/08/2013 at 21:17.
Pushed by smartins into branch 'master'.
New command line tool: kdepim/console/calendarjanitor.
Scans all your calendar data for buggy incidences.
Also prints some useful statistics like total inline attachment size.
In --fix mode, duplicate incidences ( with same payload ) will be deleted,
incidences with empty summary and description (and no attachments) are deleted
If invalid, DTSTART gets the value of DTEND, or current date if DTEND is invalid too.
A very common use case is to fix recurring to-dos that have a DTDUE but no DTSTART,
RFC doesn't allow this.
Scan with:
$ calendarjanitor
Scan and fix incidences:
$ calendarjanitor --fix
Backup your data first:
$ calendarjanitor --backup
CCMAIL: kde-pim at kde.org
**********************************************************************
Processing collection Sergio Martins calendar (id=567) ...
Checking for incidences with empty summary and description... [OK]
Checking for incidences with empty UID... [OK]
Checking for events with invalid DTSTART... [OK]
Checking for recurring to-dos with invalid DTSTART... [OK]
Checking for journal with invalid DTSTART... [OK]
Checking for duplicate UIDs... [OK]
Gathering statistics...
Events : 516
Todos : 52
Journal : 175
Passed events and to-dos (>365 days) : 431
Old incidences with alarms : 259
Inline attachments : 2
Total size of inline attachments (KB) : 61
**********************************************************************
M +2 -0 console/CMakeLists.txt
A +40 -0 console/calendarjanitor/CMakeLists.txt
A +341 -0 console/calendarjanitor/COPYING
A +2 -0 console/calendarjanitor/Messages.sh
A +151 -0 console/calendarjanitor/backuper.cpp [License: GPL (v2+) (+Qt exception)]
A +65 -0 console/calendarjanitor/backuper.h [License: GPL (v2+) (+Qt exception)]
A +542 -0 console/calendarjanitor/calendarjanitor.cpp [License: GPL (v2+) (+Qt exception)]
A +103 -0 console/calendarjanitor/calendarjanitor.h [License: GPL (v2+) (+Qt exception)]
A +70 -0 console/calendarjanitor/collectionloader.cpp [License: GPL (v2+) (+Qt exception)]
A +50 -0 console/calendarjanitor/collectionloader.h [License: GPL (v2+) (+Qt exception)]
A +177 -0 console/calendarjanitor/main.cpp [License: GPL (v2+) (+Qt exception)]
A +52 -0 console/calendarjanitor/options.cpp [License: GPL (v2+) (+Qt exception)]
A +71 -0 console/calendarjanitor/options.h [License: GPL (v2+) (+Qt exception)]
http://commits.kde.org/kdepim/b36122a8743cc48d37e5f0384ad6bccc9308c478
diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt
index 7e767d7..05da374 100644
--- a/console/CMakeLists.txt
+++ b/console/CMakeLists.txt
@@ -1,5 +1,7 @@
project(console)
+add_subdirectory(calendarjanitor)
+
if (KDEPIMLIBS_KRESOURCES_LIBS)
add_subdirectory(konsolekalendar)
add_subdirectory(kabcclient)
diff --git a/console/calendarjanitor/CMakeLists.txt b/console/calendarjanitor/CMakeLists.txt
new file mode 100644
index 0000000..f23fcc4
--- /dev/null
+++ b/console/calendarjanitor/CMakeLists.txt
@@ -0,0 +1,40 @@
+project(calendarjanitor)
+
+add_definitions(-DKDE_DEFAULT_DEBUG_AREA=5860)
+
+########### next target ###############
+
+set(calendarjanitor_SRCS
+ backuper.cpp
+ calendarjanitor.cpp
+ collectionloader.cpp
+ main.cpp
+ options.cpp)
+
+include_directories(
+ ${CMAKE_CURRENT_SOURCE_DIR}/interfaces
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/calendarsupport
+ ${CMAKE_BINARY_DIR}/calendarsupport
+
+
+ ${AKONADI_INCLUDE_DIR}
+ ${Boost_INCLUDE_DIRS}
+ ${QT_INCLUDES}
+ ${ZLIB_INCLUDE_DIRS}
+)
+
+
+kde4_add_app_icon(calendarjanitor_SRCS "${KDE4_ICON_DIR}/oxygen/*/apps/office-calendar.png")
+
+kde4_add_executable(calendarjanitor NOGUI ${calendarjanitor_SRCS})
+
+target_link_libraries(calendarjanitor
+ ${KDE4_KDECORE_LIBS}
+ ${KDEPIMLIBS_KCALUTILS_LIBS}
+ ${KDEPIMLIBS_KCALCORE_LIBS}
+ kdepim
+ calendarsupport
+ akonadi-calendar)
+
+install(TARGETS calendarjanitor ${INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/console/calendarjanitor/COPYING b/console/calendarjanitor/COPYING
new file mode 100644
index 0000000..54754ab
--- /dev/null
+++ b/console/calendarjanitor/COPYING
@@ -0,0 +1,341 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ 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; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) 19yy name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/console/calendarjanitor/Messages.sh b/console/calendarjanitor/Messages.sh
new file mode 100644
index 0000000..504219a
--- /dev/null
+++ b/console/calendarjanitor/Messages.sh
@@ -0,0 +1,2 @@
+#! /bin/sh
+$XGETTEXT *.cpp -o $podir/calendarjanitor.pot
diff --git a/console/calendarjanitor/backuper.cpp b/console/calendarjanitor/backuper.cpp
new file mode 100644
index 0000000..5430f76
--- /dev/null
+++ b/console/calendarjanitor/backuper.cpp
@@ -0,0 +1,151 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#include "backuper.h"
+
+#include <calendarsupport/utils.h>
+
+#include <KCalCore/Incidence>
+#include <KCalCore/FileStorage>
+
+#include <Akonadi/CollectionFetchJob>
+#include <Akonadi/CollectionFetchScope>
+#include <Akonadi/ItemFetchJob>
+#include <Akonadi/ItemFetchScope>
+
+#include <KLocale>
+#include <KJob>
+
+#include <QMetaObject>
+#include <QDebug>
+#include <QCoreApplication>
+
+static void print(const QString &message)
+{
+ QTextStream out(stdout);
+ out << message << "\n";
+}
+
+void Backuper::emitFinished(bool success, const QString &message)
+{
+ if (success) {
+ print(QLatin1Char('\n') + i18n("Backup was successful. %1 incidences were saved.", m_calendar->incidences().count()));
+ } else {
+ print(message);
+ }
+
+ m_calendar.clear();
+
+ emit finished(success, message);
+ qApp->exit(success ? 0 : -1); // TODO: If we move this class to kdepimlibs, remove this
+}
+
+Backuper::Backuper(QObject *parent) : QObject(parent), m_backupInProgress(false)
+{
+}
+
+void Backuper::backup(const QString &filename, const QList<Akonadi::Entity::Id> &collectionIds)
+{
+ if (filename.isEmpty()) {
+ emitFinished(false, i18n("File is empty."));
+ return;
+ }
+
+ if (m_backupInProgress) {
+ emitFinished(false, i18n("A backup is already in progress."));
+ return;
+ }
+ print(i18n("Backing up your calendar data..."));
+ m_calendar = KCalCore::MemoryCalendar::Ptr(new KCalCore::MemoryCalendar(KDateTime::LocalZone));
+ m_requestedCollectionIds = collectionIds;
+ m_backupInProgress = true;
+ m_filename = filename;
+
+ Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(),
+ Akonadi::CollectionFetchJob::Recursive);
+
+ job->fetchScope().setContentMimeTypes(KCalCore::Incidence::mimeTypes());
+ connect(job, SIGNAL(result(KJob*)), SLOT(onCollectionsFetched(KJob *)));
+ job->start();
+}
+
+void Backuper::onCollectionsFetched(KJob *job)
+{
+ if (job->error() == 0) {
+ QSet<QString> mimeTypeSet = KCalCore::Incidence::mimeTypes().toSet();
+ Akonadi::CollectionFetchJob *cfj = qobject_cast<Akonadi::CollectionFetchJob*>(job);
+ foreach(const Akonadi::Collection &collection, cfj->collections()) {
+ if (!m_requestedCollectionIds.isEmpty() && !m_requestedCollectionIds.contains(collection.id()))
+ continue;
+ if (!mimeTypeSet.intersect(collection.contentMimeTypes().toSet()).isEmpty()) {
+ m_collections << collection;
+ loadCollection(collection);
+ }
+ }
+
+ if (m_collections.isEmpty()) {
+ emitFinished(false, i18n("No data to backup."));
+ }
+ } else {
+ kError() << job->errorString();
+ m_backupInProgress = false;
+ emitFinished(false, job->errorString());
+ }
+}
+
+void Backuper::loadCollection(const Akonadi::Collection &collection)
+{
+ print(i18n("Processing collection %1 (id=%2)...", collection.displayName(), collection.id()));
+ Akonadi::ItemFetchJob *ifj = new Akonadi::ItemFetchJob(collection, this);
+ ifj->setProperty("collectionId", collection.id());
+ ifj->fetchScope().fetchFullPayload(true);
+ connect(ifj, SIGNAL(result(KJob*)), SLOT(onCollectionLoaded(KJob*)));
+ m_pendingCollections << collection.id();
+}
+
+void Backuper::onCollectionLoaded(KJob *job)
+{
+ if (job->error()) {
+ m_backupInProgress = false;
+ m_calendar.clear();
+ emitFinished(false, job->errorString());
+ } else {
+ Akonadi::ItemFetchJob *ifj = qobject_cast<Akonadi::ItemFetchJob *>(job);
+ Akonadi::Collection::Id id = ifj->property("collectionId").toInt();
+ Q_ASSERT(id != -1);
+ Akonadi::Item::List items = ifj->items();
+ m_pendingCollections.removeAll(id);
+
+ foreach (const Akonadi::Item &item, items) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ Q_ASSERT(incidence);
+ m_calendar->addIncidence(incidence);
+ }
+
+ if (m_pendingCollections.isEmpty()) { // We're done
+ KCalCore::FileStorage storage(m_calendar, m_filename);
+ bool success = storage.save();
+ QString message = success ? QString() : i18n("An error ocurred");
+ emitFinished(success, message);
+ }
+ }
+}
diff --git a/console/calendarjanitor/backuper.h b/console/calendarjanitor/backuper.h
new file mode 100644
index 0000000..d20797d
--- /dev/null
+++ b/console/calendarjanitor/backuper.h
@@ -0,0 +1,65 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#ifndef BACKUPER_H
+#define BACKUPER_H
+
+#include "options.h"
+
+#include <KCalCore/MemoryCalendar>
+#include <Akonadi/Collection>
+
+#include <QObject>
+#include <QList>
+
+class KJob;
+
+class Backuper : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Backuper(QObject *parent = 0);
+ void backup(const QString &filename, const QList<Akonadi::Collection::Id> &collectionIds);
+
+Q_SIGNALS:
+ void finished(bool success, const QString &errorMessage);
+
+private Q_SLOTS:
+ void onCollectionsFetched(KJob *);
+ void onCollectionLoaded(KJob *);
+
+private:
+ void loadCollection(const Akonadi::Collection &collection);
+ void emitFinished(bool success, const QString &message);
+
+ QList<Akonadi::Collection::Id> m_requestedCollectionIds;
+ QList<Akonadi::Collection::Id> m_pendingCollections;
+
+ Akonadi::Collection::List m_collections;
+ QString m_filename;
+ KCalCore::MemoryCalendar::Ptr m_calendar;
+
+ bool m_backupInProgress;
+
+};
+
+#endif // BACKUPER_H
diff --git a/console/calendarjanitor/calendarjanitor.cpp b/console/calendarjanitor/calendarjanitor.cpp
new file mode 100644
index 0000000..b13701f
--- /dev/null
+++ b/console/calendarjanitor/calendarjanitor.cpp
@@ -0,0 +1,542 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#include "calendarjanitor.h"
+#include "collectionloader.h"
+
+#include <calendarsupport/utils.h>
+
+#include <Akonadi/ItemFetchJob>
+#include <Akonadi/ItemFetchScope>
+
+#include <KCalCore/Attachment>
+#include <KCalCore/Alarm>
+#include <KCalCore/Event>
+#include <KCalCore/Todo>
+#include <KCalCore/Journal>
+
+#include <KLocale>
+#include <KDateTime>
+
+#include <QList>
+#include <QString>
+#include <QTextStream>
+#include <QCoreApplication>
+
+#define TEXT_WIDTH 70
+
+static void print(const QString &message, bool newline = true)
+{
+ QTextStream out(stdout);
+ out << message;
+ if (newline)
+ out << "\n";
+}
+
+static void bailOut()
+{
+ print(i18n("Bailing out. Fix your akonadi setup first. These kind of errors should not happen."));
+ qApp->exit(-1);
+}
+
+static bool collectionIsReadOnly(const Akonadi::Collection &collection)
+{
+ return !(collection.rights() & Akonadi::Collection::CanChangeItem) ||
+ !(collection.rights() & Akonadi::Collection::CanDeleteItem);
+}
+
+CalendarJanitor::CalendarJanitor(const Options &options, QObject *parent) : QObject(parent)
+ , m_collectionLoader(new CollectionLoader(this))
+ , m_options(options)
+ , m_currentSanityCheck(Options::CheckNone)
+ , m_pendingModifications(0)
+ , m_pendingDeletions(0)
+{
+ m_changer = new Akonadi::IncidenceChanger(this);
+ m_changer->setShowDialogsOnError(false);
+ connect(m_changer, SIGNAL(modifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)),
+ SLOT(onModifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)));
+ connect(m_changer, SIGNAL(deleteFinished(int,QVector<Akonadi::Item::Id>,Akonadi::IncidenceChanger::ResultCode,QString)),
+ SLOT(onDeleteFinished(int,QVector<Akonadi::Item::Id>,Akonadi::IncidenceChanger::ResultCode,QString)));
+ connect(m_collectionLoader, SIGNAL(loaded(bool)), SLOT(onCollectionsFetched(bool)));
+}
+
+void CalendarJanitor::start()
+{
+ m_collectionLoader->load();
+}
+
+void CalendarJanitor::onCollectionsFetched(bool success)
+{
+ if (!success) {
+ emit finished(false);
+ qApp->exit(-1);
+ return;
+ }
+
+ foreach (const Akonadi::Collection &collection, m_collectionLoader->collections()) {
+ if (m_options.testCollection(collection.id()))
+ m_collectionsToProcess << collection;
+ }
+
+ if (m_collectionsToProcess.isEmpty()) {
+ print(i18n("There are no collection to process!"));
+ qApp->exit((-1));
+ return;
+ } else {
+ processNextCollection();
+ }
+}
+
+void CalendarJanitor::onItemsFetched(KJob *job)
+{
+ Akonadi::ItemFetchJob *ifj = qobject_cast<Akonadi::ItemFetchJob *>(job);
+ Q_ASSERT(ifj);
+ m_itemsToProcess = ifj->items();
+ if (m_itemsToProcess.isEmpty()) {
+ print(i18n("Collection is empty, ignoring it."));
+ } else {
+ m_incidenceMap.clear();
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ Q_ASSERT(incidence);
+ m_incidenceMap.insert(incidence->instanceIdentifier(), incidence);
+ m_incidenceToItem.insert(incidence, item);
+ }
+ runNextTest();
+ }
+}
+
+void CalendarJanitor::onModifyFinished(int changeId, const Akonadi::Item &item,
+ Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
+{
+ Q_UNUSED(changeId);
+ if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
+ print(i18n("Error while modifying incidence: %1!", errorMessage));
+ bailOut();
+ return;
+ }
+ print(i18n("Fixed item %1", item.id()));
+ m_pendingModifications--;
+ if (m_pendingModifications == 0) {
+ runNextTest();
+ }
+}
+
+void CalendarJanitor::onDeleteFinished(int changeId, const QVector<Akonadi::Entity::Id> &items,
+ Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
+{
+ Q_UNUSED(changeId);
+ if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
+ print(i18n("Error while deleting incidence: %1!", errorMessage));
+ bailOut();
+ return;
+ }
+ print(i18n("Deleted item %1", items.first()));
+ m_pendingDeletions--;
+ if (m_pendingDeletions == 0) {
+ runNextTest();
+ }
+}
+
+void CalendarJanitor::processNextCollection()
+{
+ m_itemsToProcess.clear();
+ m_currentSanityCheck = Options::CheckNone;
+
+ if (m_collectionsToProcess.isEmpty()) {
+ print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
+ emit finished(true);
+ qApp->exit(0);
+ return;
+ }
+
+ m_currentCollection = m_collectionsToProcess.takeFirst();
+ print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
+ print(i18n("Processing collection %1 (id=%2) ...", m_currentCollection.displayName(), m_currentCollection.id()));
+
+ if (collectionIsReadOnly(m_currentCollection) && m_options.action() == Options::ActionScanAndFix) {
+ print(i18n("Collection is read only, disabling fix mode."));
+ }
+
+ Akonadi::ItemFetchJob *ifj = new Akonadi::ItemFetchJob(m_currentCollection, this);
+ ifj->fetchScope().fetchFullPayload(true);
+ connect(ifj, SIGNAL(result(KJob*)), SLOT(onItemsFetched(KJob*)));
+}
+
+void CalendarJanitor::runNextTest()
+{
+ int currentType = static_cast<int>(m_currentSanityCheck);
+ m_currentSanityCheck = static_cast<Options::SanityCheck>(currentType+1);
+ switch(m_currentSanityCheck) {
+ case Options::CheckEmptySummary:
+ sanityCheck1();
+ break;
+ case Options::CheckEmptyUid:
+ sanityCheck2();
+ break;
+ case Options::CheckEventDates:
+ sanityCheck3();
+ break;
+ case Options::CheckTodoDates:
+ sanityCheck4();
+ break;
+ case Options::CheckJournalDates:
+ sanityCheck5();
+ break;
+ case Options::CheckOrphans:
+ //sanityCheck6(); // Disabled for now
+ runNextTest();
+ break;
+ case Options::CheckDuplicateUIDs:
+ sanityCheck7();
+ break;
+ case Options::CheckStats:
+ sanityCheck8();
+ break;
+ case Options::CheckCount:
+ processNextCollection();
+ break;
+ default:
+ Q_ASSERT(false);
+ }
+}
+
+void CalendarJanitor::sanityCheck1()
+{
+ beginTest(i18n("Checking for incidences with empty summary and description..."));
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ if (incidence->summary().isEmpty() && incidence->description().isEmpty()
+ && incidence->attachments().isEmpty()) {
+ printFound(item);
+ deleteIncidence(item);
+ }
+ }
+
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck2()
+{
+ beginTest(i18n("Checking for incidences with empty UID..."));
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ if (incidence->uid().isEmpty()) {
+ printFound(item);
+ if (m_fixingEnabled) {
+ incidence->recreate();
+ m_pendingModifications++;
+ m_changer->modifyIncidence(item);
+ }
+ }
+ }
+
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck3()
+{
+ beginTest(i18n("Checking for events with invalid DTSTART..."));
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ KCalCore::Event::Ptr event = incidence.dynamicCast<KCalCore::Event>();
+ if (!event)
+ continue;
+
+ KDateTime start = event->dtStart();
+ KDateTime end = event->dtEnd();
+
+ bool modify = false;
+ QString message;
+ if (!start.isValid() && end.isValid()) {
+ modify = true;
+ printFound(item);
+ event->setDtStart(end);
+ } else if (!start.isValid() && !end.isValid()) {
+ modify = true;
+ printFound(item);
+ event->setDtStart(KDateTime::currentLocalDateTime());
+ event->setDtEnd(event->dtStart().addSecs(3600));
+ }
+
+ if (modify) {
+ if (m_fixingEnabled) {
+ m_changer->modifyIncidence(item);
+ m_pendingModifications++;
+ }
+ }
+ }
+
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck4()
+{
+ beginTest(i18n("Checking for recurring to-dos with invalid DTSTART..."));
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ KCalCore::Todo::Ptr todo = incidence.dynamicCast<KCalCore::Todo>();
+ if (!todo)
+ continue;
+
+ KDateTime start = todo->dtStart();
+ KDateTime due = todo->dtDue();
+ bool modify = false;
+ if (todo->recurs() && !start.isValid() && due.isValid()) {
+ modify = true;
+ printFound(item);
+ todo->setDtStart(due);
+ }
+
+ if (todo->recurs() && !start.isValid() && !due.isValid()) {
+ modify = true;
+ printFound(item);
+ todo->setDtStart(KDateTime::currentLocalDateTime());
+ }
+
+ if (modify) {
+ if (m_fixingEnabled) {
+ m_changer->modifyIncidence(item);
+ m_pendingModifications++;
+ }
+ }
+ }
+
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck5()
+{
+ beginTest(i18n("Checking for journal with invalid DTSTART..."));
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ if (incidence->type() != KCalCore::Incidence::TypeJournal)
+ continue;
+
+ if (!incidence->dtStart().isValid()) {
+ printFound(item);
+ incidence->setDtStart(KDateTime::currentLocalDateTime());
+ if (m_fixingEnabled) {
+ m_changer->modifyIncidence(item);
+ m_pendingModifications++;
+ }
+ }
+ }
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck6()
+{
+ beginTest(i18n("Checking for orphans...")); // Incidences without a parent
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ const QString parentUid = incidence->relatedTo();
+ if (!parentUid.isEmpty() && !m_incidenceMap.contains(parentUid)) {
+ printFound(item);
+ if (m_fixingEnabled) {
+ incidence->setRelatedTo(QString());
+ m_changer->modifyIncidence(item);
+ m_pendingModifications++;
+ }
+ }
+ }
+
+ endTest();
+}
+
+void CalendarJanitor::sanityCheck7()
+{
+ beginTest(i18n("Checking for duplicate UIDs..."));
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ QList<KCalCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
+
+ if (existingIncidences.count() == 1)
+ continue;
+
+ foreach (const KCalCore::Incidence::Ptr &existingIncidence, existingIncidences) {
+ if (existingIncidence != incidence && *incidence == *existingIncidence) {
+ printFound(item);
+ deleteIncidence(item);
+ break;
+ }
+ }
+ }
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ QList<KCalCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
+
+ if (existingIncidences.count() == 1)
+ continue;
+
+ for (int i=1; i<existingIncidences.count(); ++i) {
+ printFound(item);
+ if (m_fixingEnabled) {
+ KCalCore::Incidence::Ptr existingIncidence = existingIncidences.at(i);
+ Akonadi::Item item = m_incidenceToItem.value(existingIncidence);
+ Q_ASSERT(item.isValid());
+ if (item.isValid()) {
+ existingIncidence->recreate();
+ m_changer->modifyIncidence(item);
+ m_pendingModifications++;
+ m_incidenceMap.remove(incidence->instanceIdentifier(), existingIncidence);
+ }
+ }
+ }
+ }
+
+ endTest();
+}
+
+static void printStat(const QString &message, int arg)
+{
+ if (arg > 0) {
+ print(message.leftJustified(50), false);
+ const QString s = QLatin1String(": %1");
+ print(s.arg(arg));
+ }
+}
+
+void CalendarJanitor::sanityCheck8()
+{
+ beginTest(i18n("Gathering statistics..."));
+ print("\n");
+
+ int numOldAlarms = 0;
+ int numAttachments = 0;
+ int totalAttachmentSize = 0;
+ int numOldIncidences = 0;
+ QHash<KCalCore::Incidence::IncidenceType, int> m_counts;
+
+ foreach (const Akonadi::Item &item, m_itemsToProcess) {
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ if (!incidence->attachments().isEmpty()) {
+ foreach (const KCalCore::Attachment::Ptr &attachment, incidence->attachments()) {
+ if (!attachment->isUri()) {
+ numAttachments++;
+ totalAttachmentSize += attachment->size();
+ }
+ }
+ }
+
+ m_counts[incidence->type()]++;
+
+ if (incidence->dtStart().isValid() && !incidence->recurs() &&
+ incidence->dtStart().daysTo(KDateTime::currentDateTime(KDateTime::LocalZone)) > 365 &&
+ incidence->type() != KCalCore::Incidence::TypeJournal) {
+
+ if (!incidence->alarms().isEmpty())
+ numOldAlarms++;
+
+ numOldIncidences++;
+ }
+
+ numAttachments += incidence->attachments().count();
+ numOldAlarms += incidence->alarms().count();
+ }
+
+ printStat(i18n("Events"), m_counts[KCalCore::Incidence::TypeEvent]);
+ printStat(i18n("Todos"), m_counts[KCalCore::Incidence::TypeTodo]);
+ printStat(i18n("Journal"), m_counts[KCalCore::Incidence::TypeJournal]);
+ printStat(i18n("Passed events and to-dos (>365 days)"), numOldIncidences);
+ printStat(i18n("Old incidences with alarms"), numOldAlarms);
+ printStat(i18n("Inline attachments"), numAttachments);
+
+ if (totalAttachmentSize < 1024) {
+ printStat(i18n("Total size of inline attachments (bytes)"), totalAttachmentSize);
+ } else {
+ printStat(i18n("Total size of inline attachments (KB)"), totalAttachmentSize / 1024);
+ }
+
+ endTest(/**print=*/false);
+}
+
+static QString dateString(const KCalCore::Incidence::Ptr &incidence)
+{
+ KDateTime start = incidence->dtStart();
+ KDateTime end = incidence->dateTime(KCalCore::Incidence::RoleEnd);
+ QString str = QLatin1String("DTSTART=") + (start.isValid() ? start.toString() : i18n("invalid")) + QLatin1String("; ");
+
+ if (incidence->type() == KCalCore::Incidence::TypeJournal) {
+ return str;
+ }
+
+ str += QLatin1String("\n ");
+
+ if (incidence->type() == KCalCore::Incidence::TypeTodo)
+ str += QLatin1String("DTDUE=");
+ else if (incidence->type() == KCalCore::Incidence::TypeEvent)
+ str += QLatin1String("DTEND=");
+
+ str+= (start.isValid() ? end.toString() : i18n("invalid")) + QLatin1String("; ");
+
+ if (incidence->recurs())
+ str += i18n("recurrent");
+
+ return str;
+}
+
+void CalendarJanitor::printFound(const Akonadi::Item &item)
+{
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ m_numDamaged++;
+ if (m_numDamaged == 1)
+ print(QLatin1String(" [!!]"));
+ print(QString(" * ") + i18n("Found buggy item:"));
+ print(QString(" ") + i18n("id=%1; summary=\"%2\"", item.id(), incidence->summary()));
+ print(QString(" ") + dateString(incidence));
+}
+
+void CalendarJanitor::beginTest(const QString &message)
+{
+ m_numDamaged = 0;
+ m_fixingEnabled = m_options.action() == Options::ActionScanAndFix && !collectionIsReadOnly(m_currentCollection);
+ print(message.leftJustified(TEXT_WIDTH), false);
+}
+
+void CalendarJanitor::endTest(bool printEnabled)
+{
+ if (m_numDamaged == 0 && printEnabled) {
+ print(QLatin1String(" [OK]"));
+ }
+
+ if (m_pendingDeletions == 0 && m_pendingModifications == 0) {
+ runNextTest();
+ }
+}
+
+void CalendarJanitor::deleteIncidence(const Akonadi::Item &item)
+{
+ if (m_fixingEnabled && !collectionIsReadOnly(m_currentCollection)) {
+ m_pendingDeletions++;
+ m_changer->deleteIncidence(item);
+ KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+ m_incidenceMap.remove(incidence->instanceIdentifier(), incidence);
+ m_incidenceToItem.remove(incidence);
+ }
+}
diff --git a/console/calendarjanitor/calendarjanitor.h b/console/calendarjanitor/calendarjanitor.h
new file mode 100644
index 0000000..5b66732
--- /dev/null
+++ b/console/calendarjanitor/calendarjanitor.h
@@ -0,0 +1,103 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#ifndef CALENDARJANITOR_H
+#define CALENDARJANITOR_H
+
+#include "options.h"
+
+#include <KCalCore/Incidence>
+
+#include <Akonadi/Calendar/IncidenceChanger>
+#include <Akonadi/Collection>
+#include <Akonadi/Item>
+#include <QObject>
+#include <QString>
+#include <QMultiMap>
+
+class CollectionLoader;
+
+class KJob;
+
+class CalendarJanitor : public QObject
+{
+ Q_OBJECT
+public:
+ explicit CalendarJanitor(const Options &options, QObject *parent = 0);
+
+ void start();
+
+Q_SIGNALS:
+ void finished(bool success);
+
+private Q_SLOTS:
+ void onCollectionsFetched(bool success);
+ void onItemsFetched(KJob *job);
+ void onModifyFinished(int changeId, const Akonadi::Item &item,
+ Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage);
+ void onDeleteFinished(int changeId, const QVector<Akonadi::Item::Id> &,
+ Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage);
+
+ void processNextCollection();
+
+ // For each collection we process, we run a bunch of tests on it.
+ void runNextTest();
+
+ void sanityCheck1();
+ void sanityCheck2();
+ void sanityCheck3();
+ void sanityCheck4();
+ void sanityCheck5();
+ void sanityCheck6();
+ void sanityCheck7();
+ void sanityCheck8();
+
+ void printFound(const Akonadi::Item &item);
+
+ void beginTest(const QString &message);
+ void endTest(bool print = true);
+
+ void deleteIncidence(const Akonadi::Item &item);
+
+private:
+ CollectionLoader *m_collectionLoader;
+ Akonadi::Collection::List m_collectionsToProcess;
+ Akonadi::Item::List m_itemsToProcess;
+ Options m_options;
+ Akonadi::IncidenceChanger *m_changer;
+ Akonadi::Collection m_currentCollection;
+ Options::SanityCheck m_currentSanityCheck;
+ int m_pendingModifications;
+ int m_pendingDeletions;
+
+ QList<Akonadi::Item::Id> m_test1Results;
+ QStringList m_test2Results;
+
+ int m_numDamaged;
+ bool m_fixingEnabled;
+
+ QString m_summary; // to print at the end.
+ QMultiMap<QString, KCalCore::Incidence::Ptr> m_incidenceMap;
+ QMap<KCalCore::Incidence::Ptr, Akonadi::Item> m_incidenceToItem;
+};
+
+#endif // CALENDARJANITOR_H
diff --git a/console/calendarjanitor/collectionloader.cpp b/console/calendarjanitor/collectionloader.cpp
new file mode 100644
index 0000000..d0a8c6a
--- /dev/null
+++ b/console/calendarjanitor/collectionloader.cpp
@@ -0,0 +1,70 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#include "collectionloader.h"
+
+#include <KCalCore/Incidence>
+
+#include <Akonadi/CollectionFetchJob>
+#include <Akonadi/CollectionFetchScope>
+#include <QString>
+#include <QSet>
+
+
+CollectionLoader::CollectionLoader(QObject *parent) :
+ QObject(parent)
+{
+}
+
+void CollectionLoader::load()
+{
+ Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(),
+ Akonadi::CollectionFetchJob::Recursive);
+
+ job->fetchScope().setContentMimeTypes(KCalCore::Incidence::mimeTypes());
+ connect(job, SIGNAL(result(KJob*)), SLOT(onCollectionsLoaded(KJob *)));
+ job->start();
+}
+
+Akonadi::Collection::List CollectionLoader::collections() const
+{
+ return m_collections;
+}
+
+void CollectionLoader::onCollectionsLoaded(KJob *job)
+{
+ if (job->error() == 0) {
+ QSet<QString> mimeTypeSet = KCalCore::Incidence::mimeTypes().toSet();
+ Akonadi::CollectionFetchJob *cfj = qobject_cast<Akonadi::CollectionFetchJob*>(job);
+ Q_ASSERT(cfj);
+ foreach(const Akonadi::Collection &collection, cfj->collections()) {
+ if (!mimeTypeSet.intersect(collection.contentMimeTypes().toSet()).isEmpty()) {
+ m_collections << collection;
+ }
+ }
+
+ emit loaded(true);
+ } else {
+ kError() << job->errorString();
+ emit loaded(false);
+ }
+}
diff --git a/console/calendarjanitor/collectionloader.h b/console/calendarjanitor/collectionloader.h
new file mode 100644
index 0000000..0807bac
--- /dev/null
+++ b/console/calendarjanitor/collectionloader.h
@@ -0,0 +1,50 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#ifndef COLLECTIONLOADER_H
+#define COLLECTIONLOADER_H
+
+#include <Akonadi/Collection>
+
+#include <QObject>
+
+class KJob;
+
+class CollectionLoader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit CollectionLoader(QObject *parent = 0);
+ void load();
+ Akonadi::Collection::List collections() const;
+
+Q_SIGNALS:
+ void loaded(bool succcess);
+
+private Q_SLOTS:
+ void onCollectionsLoaded(KJob*);
+
+private:
+ Akonadi::Collection::List m_collections;
+};
+
+#endif // COLLECTIONLOADER_H
diff --git a/console/calendarjanitor/main.cpp b/console/calendarjanitor/main.cpp
new file mode 100644
index 0000000..e141785
--- /dev/null
+++ b/console/calendarjanitor/main.cpp
@@ -0,0 +1,177 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#include "calendarjanitor.h"
+#include "options.h"
+#include "backuper.h"
+
+#include "kdepim-version.h"
+
+#include <KAboutData>
+#include <KLocale>
+#include <KCmdLineArgs>
+#include <KApplication>
+
+#include <QTextStream>
+#include <QString>
+#include <qglobal.h>
+
+#ifdef Q_OS_UNIX
+# include <sys/types.h>
+# include <sys/stat.h>
+# include <fcntl.h>
+#endif
+
+static const char progName[] = "calendarjanitor";
+static const char progDisplay[] = "CalendarJanitor";
+
+static const char progVersion[] = KDEPIM_VERSION;
+static const char progDesc[] = "A command line interface to report and fix errors in your calendar data";
+
+static void print(const QString &message)
+{
+ QTextStream out(stdout);
+ out << message << "\n";
+}
+
+static void printCollectionsUsage()
+{
+ print(i18n("Error while parsing %1", QLatin1String("--collections")));
+ print(i18n("Example usage %1", QLatin1String(": --collections 90,23,40")));
+}
+
+static void silenceStderr()
+{
+#ifdef Q_OS_UNIX
+ if (qgetenv("KDE_CALENDARJANITOR_DEBUG") != "1") {
+ // Disable stderr so we can actually read what's going on
+ int fd = ::open("/dev/null", O_WRONLY);
+ ::dup2(fd, 2);
+ ::close(fd);
+ }
+#endif
+}
+
+int main(int argv, char *argc[])
+{
+ KAboutData aboutData(progName, 0, // internal program name
+ ki18n(progDisplay), // displayable program name.
+ progVersion, // version string
+ ki18n(progDesc), // short program description
+ KAboutData::License_GPL, // license type
+ ki18n("(c) 2013, Sérgio Martins"),
+ ki18n(0), // any free form text
+ 0, // program home page address
+ "bugs.kde.org");
+ aboutData.addAuthor(ki18n("Sérgio Martins"), ki18n("Maintainer"), "iamsergio at gmail.com", 0);
+
+ KCmdLineArgs::init(argv, argc, &aboutData, KCmdLineArgs::CmdLineArgNone);
+
+
+ KCmdLineOptions options;
+ options.add("collections <ids>", ki18n("List of collection ids to scan"));
+ options.add("fix", ki18n("Fix broken incidences"));
+ options.add("backup <output.ics>", ki18n("Backup your calendar"));
+
+ options.add("", ki18n("\nExamples:\n\nScan all collections:\n"
+ "$ calendarjanitor\n\n"
+ "Scan and fix all collections:\n"
+ "$ calendarjanitor --fix\n\n"
+ "Scan and fix some collections:\n"
+ "$ calendarjanitor --collections 10,20 --fix\n\n"
+ "Backup all collections:\n"
+ "$ calendarjanitor --backup backup.ics\n\n"
+ "Backup some collections:\n"
+ "$ calendarjanitor --backup backup.ics --collections 10,20"));
+
+ KCmdLineArgs::addCmdLineOptions(options);
+ KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
+
+ Options janitorOptions;
+
+ if (args->isSet("collections")) {
+ QString option = args->getOption("collections");
+ QStringList collections = option.split(",");
+ QList<Akonadi::Collection::Id> ids;
+ foreach (const QString &collection, collections) {
+ bool ok = false;
+ int num = collection.toInt(&ok);
+ if (ok) {
+ ids << num;
+ } else {
+ printCollectionsUsage();
+ return -1;
+ }
+
+ if (ids.isEmpty()) {
+ printCollectionsUsage();
+ return -1;
+ } else {
+ janitorOptions.setCollections(ids);
+ }
+ }
+ }
+
+ if (args->isSet("fix") && args->isSet("backup")) {
+ print("--fix is incompatible with --backup");
+ return -1;
+ }
+
+ KApplication app(false);
+
+ silenceStderr(); // Switching off mobile phones, movie is about to start
+
+ QString backupFile;
+ if (args->isSet("fix")) {
+ janitorOptions.setAction(Options::ActionScanAndFix);
+ print(i18n("Running in fix mode."));
+ } else if (args->isSet("backup")) {
+ backupFile = args->getOption("backup");
+ if (backupFile.isEmpty()) {
+ print("Please specify a output file.");
+ return -1;
+ }
+ janitorOptions.setAction(Options::ActionBackup);
+ } else {
+ print(i18n("Running in scan only mode."));
+ janitorOptions.setAction(Options::ActionScan);
+ }
+
+ switch(janitorOptions.action()) {
+ case Options::ActionBackup: {
+ Backuper *backuper = new Backuper();
+ backuper->backup(backupFile, janitorOptions.collections());
+ break;
+ }
+ case Options::ActionScan:
+ case Options::ActionScanAndFix: {
+ CalendarJanitor *janitor = new CalendarJanitor(janitorOptions);
+ janitor->start();
+ break;
+ }
+ default:
+ Q_ASSERT(false);
+ }
+
+
+ return app.exec();
+}
diff --git a/console/calendarjanitor/options.cpp b/console/calendarjanitor/options.cpp
new file mode 100644
index 0000000..2c4103e
--- /dev/null
+++ b/console/calendarjanitor/options.cpp
@@ -0,0 +1,52 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#include "options.h"
+
+Options::Options() : m_action(ActionNone)
+{
+}
+
+void Options::setAction(Options::Action action)
+{
+ m_action = action;
+}
+
+Options::Action Options::action() const
+{
+ return m_action;
+}
+
+QList<Akonadi::Entity::Id> Options::collections() const
+{
+ return m_collectionIds;
+}
+
+void Options::setCollections(const QList<Akonadi::Collection::Id> &collections)
+{
+ m_collectionIds = collections;
+}
+
+bool Options::testCollection(Akonadi::Entity::Id id) const
+{
+ return m_collectionIds.isEmpty() || m_collectionIds.contains(id);
+}
diff --git a/console/calendarjanitor/options.h b/console/calendarjanitor/options.h
new file mode 100644
index 0000000..150dfa6
--- /dev/null
+++ b/console/calendarjanitor/options.h
@@ -0,0 +1,71 @@
+/*
+ Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+ 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; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ As a special exception, permission is given to link this program
+ with any edition of Qt, and distribute the resulting executable,
+ without including the source code for Qt in the source distribution.
+*/
+
+#ifndef OPTIONS_H
+#define OPTIONS_H
+
+#include <Akonadi/Collection>
+#include <QList>
+
+class Options
+{
+public:
+
+ enum SanityCheck {
+ CheckNone,
+ CheckEmptySummary, // Checks for empty summary and description. In fix mode, it deletes them.
+ CheckEmptyUid, // Checks for an empty UID. In fix mode, a new UID is assigned.
+ CheckEventDates, // Check for missing DTSTART or DTEND. New dates will be assigned.
+ CheckTodoDates, // Check for recurring to-dos without DTSTART. DTDUE will be assigned to DTSTART, or current date if DTDUE is also invalid.
+ CheckJournalDates, // Check for journals without DTSTART
+ CheckOrphans, // Check for orphan to-dos. Will be unparented." <disabled for now>
+ CheckDuplicateUIDs, // Check for duplicated UIDs. Copies will be deleted if the payload is the same. Otherwise a new UID is assigned.
+ CheckStats, // Gathers some statistics. No fixing is done.
+ CheckCount // For iteration purposes. Keep at end.
+ };
+
+ enum Action {
+ ActionNone,
+ ActionScan,
+ ActionScanAndFix,
+ ActionBackup
+ };
+
+ Options();
+
+ void setAction(Action);
+ Action action() const;
+
+ /**
+ * List of collections for backup or fix modes.
+ * If empty, all collections will be considered.
+ */
+ QList<Akonadi::Collection::Id> collections() const;
+ void setCollections(const QList<Akonadi::Collection::Id> &);
+ bool testCollection(Akonadi::Collection::Id) const;
+
+private:
+ QList<Akonadi::Collection::Id> m_collectionIds;
+ Action m_action;
+};
+
+#endif // OPTIONS_H
_______________________________________________
KDE PIM mailing list kde-pim at kde.org
https://mail.kde.org/mailman/listinfo/kde-pim
KDE PIM home page at http://pim.kde.org/
More information about the kde-pim
mailing list