1 /* Copyright 2009 Klarälvdalens Datakonsult AB
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public License as
5 published by the Free Software Foundation; either version 2 of
6 the License or (at your option) version 3 or any later version
7 accepted by the membership of KDE e.V. (or its successor approved
8 by the membership of KDE e.V.), which shall act as a proxy
9 defined in Section 14 of version 3 of the license.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include "backupjob.h"
21 #include <Akonadi/CollectionDeleteJob>
22 #include <Akonadi/CollectionFetchJob>
23 #include <Akonadi/CollectionFetchScope>
24 #include <Akonadi/ItemFetchJob>
25 #include <Akonadi/ItemFetchScope>
26 #include <KMime/Message>
31 #include <kmessagebox.h>
32 #include <kio/global.h>
37 using namespace KMail
;
39 BackupJob::BackupJob( QWidget
*parent
)
44 mParentWidget( parent
),
45 mArchivedMessages( 0 ),
49 mDeleteFoldersAfterCompletion( false ),
50 mCurrentFolder( Akonadi::Collection() ),
55 BackupJob::~BackupJob()
57 mPendingFolders
.clear();
62 void BackupJob::setRootFolder( const Akonadi::Collection
&rootFolder
)
64 mRootFolder
= rootFolder
;
67 void BackupJob::setSaveLocation( const KUrl
& savePath
)
69 mMailArchivePath
= savePath
;
72 void BackupJob::setArchiveType( ArchiveType type
)
77 void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem
)
79 mDeleteFoldersAfterCompletion
= deleteThem
;
82 bool BackupJob::queueFolders( const Akonadi::Collection
&root
)
84 mPendingFolders
.append( root
);
85 // FIXME: Get rid of the exec()
86 // We could do a recursive CollectionFetchJob, but we only fetch the first level
87 // and then recurse manually. This is needed because a recursive fetch doesn't
88 // sort the collections the way we want. We need all first level children to be
89 // in the mPendingFolders list before all second level children, so that the
90 // directories for the first level are written before the directories in the
91 // second level, in the archive file.
92 Akonadi::CollectionFetchJob
*job
= new Akonadi::CollectionFetchJob( root
,
93 Akonadi::CollectionFetchJob::FirstLevel
);
94 job
->fetchScope().setAncestorRetrieval( Akonadi::CollectionFetchScope::All
);
97 kWarning() << job
->errorString();
98 abort( i18n( "Unable to retrieve folder list." ) );
102 foreach ( const Akonadi::Collection
&collection
, job
->collections() ) {
103 if ( !queueFolders( collection
) )
107 mAllFolders
= mPendingFolders
;
111 bool BackupJob::hasChildren( const Akonadi::Collection
&collection
) const
113 foreach( const Akonadi::Collection
&curCol
, mAllFolders
) {
114 if ( collection
== curCol
.parentCollection() )
120 void BackupJob::cancelJob()
122 abort( i18n( "The operation was canceled by the user." ) );
125 void BackupJob::abort( const QString
&errorMessage
)
127 // We could be called this twice, since killing the current job below will cause the job to fail,
128 // and that will call abort()
133 if ( mCurrentFolder
.isValid() ) {
134 mCurrentFolder
= Akonadi::Collection();
136 if ( mArchive
&& mArchive
->isOpen() ) {
143 if ( mProgressItem
) {
144 mProgressItem
->setComplete();
146 // The progressmanager will delete it
149 QString text
= i18n( "Failed to archive the folder '%1'.", mRootFolder
.name() );
150 text
+= '\n' + errorMessage
;
151 KMessageBox::sorry( mParentWidget
, text
, i18n( "Archiving failed" ) );
153 // Clean up archive file here?
156 void BackupJob::finish()
158 if ( mArchive
->isOpen() ) {
159 if ( !mArchive
->close() ) {
160 abort( i18n( "Unable to finalize the archive file." ) );
165 mProgressItem
->setStatus( i18n( "Archiving finished" ) );
166 mProgressItem
->setComplete();
169 QFileInfo
archiveFileInfo( mMailArchivePath
.path() );
170 QString text
= i18n( "Archiving folder '%1' successfully completed. "
171 "The archive was written to the file '%2'.",
172 mRootFolder
.name(), mMailArchivePath
.path() );
173 text
+= '\n' + i18np( "1 message of size %2 was archived.",
174 "%1 messages with the total size of %2 were archived.",
175 mArchivedMessages
, KIO::convertSize( mArchivedSize
) );
176 text
+= '\n' + i18n( "The archive file has a size of %1.",
177 KIO::convertSize( archiveFileInfo
.size() ) );
178 KMessageBox::information( mParentWidget
, text
, i18n( "Archiving finished" ) );
180 if ( mDeleteFoldersAfterCompletion
) {
181 // Some safety checks first...
182 if ( archiveFileInfo
.size() > 0 && ( mArchivedSize
> 0 || mArchivedMessages
== 0 ) ) {
183 // Sorry for any data loss!
184 new Akonadi::CollectionDeleteJob( mRootFolder
);
191 void BackupJob::archiveNextMessage()
196 if ( mPendingMessages
.isEmpty() ) {
197 kDebug() << "===> All messages done in folder " << mCurrentFolder
.name();
202 Akonadi::Item item
= mPendingMessages
.front();
203 mPendingMessages
.pop_front();
204 kDebug() << "Fetching item with ID" << item
.id() << "for folder" << mCurrentFolder
.name();
206 mCurrentJob
= new Akonadi::ItemFetchJob( item
);
207 mCurrentJob
->fetchScope().fetchFullPayload( true );
208 connect( mCurrentJob
, SIGNAL(result(KJob
*)),
209 this, SLOT(itemFetchJobResult(KJob
*)) );
212 void BackupJob::processMessage( const Akonadi::Item
&item
)
217 const KMime::Message::Ptr message
= item
.payload
<KMime::Message::Ptr
>();
218 kDebug() << "Processing message with subject " << message
->subject( false );
219 const QByteArray messageData
= message
->encodedContent();
220 const qint64 messageSize
= messageData
.size();
221 const QString messageName
= QString::number( item
.id() );
222 const QString fileName
= pathForCollection( mCurrentFolder
) + "/cur/" + messageName
;
224 // PORT ME: user and group!
225 kDebug() << "AKONDI PORT: disabled code here!";
226 if ( !mArchive
->writeFile( fileName
, "user", "group", messageData
, messageSize
) ) {
227 abort( i18n( "Failed to write a message into the archive folder '%1'.", mCurrentFolder
.name() ) );
232 mArchivedSize
+= messageSize
;
234 // Use a singleshot timer, otherwise the job started in archiveNextMessage()
236 QTimer::singleShot( 0, this, SLOT(archiveNextMessage()) );
239 void BackupJob::itemFetchJobResult( KJob
*job
)
244 Q_ASSERT( job
== mCurrentJob
);
247 if ( job
->error() ) {
248 Q_ASSERT( mCurrentFolder
.isValid() );
249 kWarning() << job
->errorString();
250 abort( i18n( "Downloading a message in folder '%1' failed.", mCurrentFolder
.name() ) );
253 Akonadi::ItemFetchJob
*fetchJob
= dynamic_cast<Akonadi::ItemFetchJob
*>( job
);
254 Q_ASSERT( fetchJob
);
255 Q_ASSERT( fetchJob
->items().size() == 1 );
256 processMessage( fetchJob
->items().first() );
260 bool BackupJob::writeDirHelper( const QString
&directoryPath
)
262 // PORT ME: Correct user/group
263 kDebug() << "AKONDI PORT: Disabled code here!";
264 return mArchive
->writeDir( directoryPath
, "user", "group" );
267 QString
BackupJob::collectionName( const Akonadi::Collection
&collection
) const
269 foreach ( const Akonadi::Collection
&curCol
, mAllFolders
) {
270 if ( curCol
== collection
)
271 return curCol
.name();
277 QString
BackupJob::pathForCollection( const Akonadi::Collection
&collection
) const
279 QString fullPath
= collectionName( collection
);
280 Akonadi::Collection curCol
= collection
.parentCollection();
281 if ( collection
!= mRootFolder
) {
282 Q_ASSERT( curCol
.isValid() );
283 while( curCol
!= mRootFolder
) {
284 fullPath
.prepend( '.' + collectionName( curCol
) + ".directory" + '/' );
285 curCol
= curCol
.parentCollection();
287 Q_ASSERT( curCol
== mRootFolder
);
288 fullPath
.prepend( '.' + collectionName( curCol
) + ".directory" + '/' );
293 QString
BackupJob::subdirPathForCollection( const Akonadi::Collection
&collection
) const
295 QString path
= pathForCollection( collection
);
296 const int parentDirEndIndex
= path
.lastIndexOf( collection
.name() );
297 Q_ASSERT( parentDirEndIndex
!= -1 );
298 path
= path
.left( parentDirEndIndex
);
299 path
.append( '.' + collection
.name() + ".directory" );
303 void BackupJob::archiveNextFolder()
308 if ( mPendingFolders
.isEmpty() ) {
313 mCurrentFolder
= mPendingFolders
.takeAt( 0 );
314 kDebug() << "===> Archiving next folder: " << mCurrentFolder
.name();
315 mProgressItem
->setStatus( i18n( "Archiving folder %1", mCurrentFolder
.name() ) );
317 const QString folderName
= mCurrentFolder
.name();
319 if ( hasChildren( mCurrentFolder
) ) {
320 if ( !writeDirHelper( subdirPathForCollection( mCurrentFolder
) ) )
323 if ( !writeDirHelper( pathForCollection( mCurrentFolder
) ) )
325 if ( !writeDirHelper( pathForCollection( mCurrentFolder
) + "/cur" ) )
327 if ( !writeDirHelper( pathForCollection( mCurrentFolder
) + "/new" ) )
329 if ( !writeDirHelper( pathForCollection( mCurrentFolder
) + "/tmp" ) )
332 abort( i18n( "Unable to create folder structure for folder '%1' within archive file.",
333 mCurrentFolder
.name() ) );
337 Akonadi::ItemFetchJob
*job
= new Akonadi::ItemFetchJob( mCurrentFolder
);
338 job
->setProperty( "folderName", folderName
);
339 connect( job
, SIGNAL(result(KJob
*)), SLOT(onArchiveNextFolderDone(KJob
*)) );
342 void BackupJob::onArchiveNextFolderDone( KJob
*job
)
344 if ( job
->error() ) {
345 kWarning() << job
->errorString();
346 abort( i18n( "Unable to get message list for folder %1.", job
->property( "folderName" ).toString() ) );
350 Akonadi::ItemFetchJob
*fetchJob
= qobject_cast
<Akonadi::ItemFetchJob
*>( job
);
351 mPendingMessages
+= fetchJob
->items();
352 archiveNextMessage();
355 void BackupJob::start()
357 Q_ASSERT( !mMailArchivePath
.isEmpty() );
358 Q_ASSERT( mRootFolder
.isValid() );
360 if ( !queueFolders( mRootFolder
) )
363 switch ( mArchiveType
) {
365 KZip
*zip
= new KZip( mMailArchivePath
.path() );
366 zip
->setCompression( KZip::DeflateCompression
);
371 mArchive
= new KTar( mMailArchivePath
.path(), "application/x-tar" );
375 mArchive
= new KTar( mMailArchivePath
.path(), "application/x-gzip" );
379 mArchive
= new KTar( mMailArchivePath
.path(), "application/x-bzip2" );
384 kDebug() << "Starting backup.";
385 if ( !mArchive
->open( QIODevice::WriteOnly
) ) {
386 abort( i18n( "Unable to open archive for writing." ) );
390 mProgressItem
= KPIM::ProgressManager::createProgressItem(
395 mProgressItem
->setUsesBusyIndicator( true );
396 connect( mProgressItem
, SIGNAL(progressItemCanceled(KPIM::ProgressItem
*)),
397 this, SLOT(cancelJob()) );
402 #include "backupjob.moc"