doc fixes found while translating
[kdepim.git] / kmail / backupjob.cpp
blob12e008dab3903025f19e17762471c69b7283fd54
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>
28 #include <klocale.h>
29 #include <kzip.h>
30 #include <ktar.h>
31 #include <kmessagebox.h>
32 #include <kio/global.h>
34 #include <QFileInfo>
35 #include <QTimer>
37 using namespace KMail;
39 BackupJob::BackupJob( QWidget *parent )
40 : QObject( parent ),
41 mArchiveType( Zip ),
42 mRootFolder( 0 ),
43 mArchive( 0 ),
44 mParentWidget( parent ),
45 mArchivedMessages( 0 ),
46 mArchivedSize( 0 ),
47 mProgressItem( 0 ),
48 mAborted( false ),
49 mDeleteFoldersAfterCompletion( false ),
50 mCurrentFolder( Akonadi::Collection() ),
51 mCurrentJob( 0 )
55 BackupJob::~BackupJob()
57 mPendingFolders.clear();
58 delete mArchive;
59 mArchive = 0;
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 )
74 mArchiveType = 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 );
95 job->exec();
96 if ( job->error() ) {
97 kWarning() << job->errorString();
98 abort( i18n( "Unable to retrieve folder list." ) );
99 return false;
102 foreach ( const Akonadi::Collection &collection, job->collections() ) {
103 if ( !queueFolders( collection ) )
104 return false;
107 mAllFolders = mPendingFolders;
108 return true;
111 bool BackupJob::hasChildren( const Akonadi::Collection &collection ) const
113 foreach( const Akonadi::Collection &curCol, mAllFolders ) {
114 if ( collection == curCol.parentCollection() )
115 return true;
117 return false;
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()
129 if ( mAborted )
130 return;
132 mAborted = true;
133 if ( mCurrentFolder.isValid() ) {
134 mCurrentFolder = Akonadi::Collection();
136 if ( mArchive && mArchive->isOpen() ) {
137 mArchive->close();
139 if ( mCurrentJob ) {
140 mCurrentJob->kill();
141 mCurrentJob = 0;
143 if ( mProgressItem ) {
144 mProgressItem->setComplete();
145 mProgressItem = 0;
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" ) );
152 deleteLater();
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." ) );
161 return;
165 mProgressItem->setStatus( i18n( "Archiving finished" ) );
166 mProgressItem->setComplete();
167 mProgressItem = 0;
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 );
188 deleteLater();
191 void BackupJob::archiveNextMessage()
193 if ( mAborted )
194 return;
196 if ( mPendingMessages.isEmpty() ) {
197 kDebug() << "===> All messages done in folder " << mCurrentFolder.name();
198 archiveNextFolder();
199 return;
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 )
214 if ( mAborted )
215 return;
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() ) );
228 return;
231 mArchivedMessages++;
232 mArchivedSize += messageSize;
234 // Use a singleshot timer, otherwise the job started in archiveNextMessage()
235 // will hang
236 QTimer::singleShot( 0, this, SLOT(archiveNextMessage()) );
239 void BackupJob::itemFetchJobResult( KJob *job )
241 if ( mAborted )
242 return;
244 Q_ASSERT( job == mCurrentJob );
245 mCurrentJob = 0;
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() ) );
252 else {
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();
273 Q_ASSERT( false );
274 return QString();
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" + '/' );
290 return fullPath;
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" );
300 return path;
303 void BackupJob::archiveNextFolder()
305 if ( mAborted )
306 return;
308 if ( mPendingFolders.isEmpty() ) {
309 finish();
310 return;
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();
318 bool success = true;
319 if ( hasChildren( mCurrentFolder ) ) {
320 if ( !writeDirHelper( subdirPathForCollection( mCurrentFolder ) ) )
321 success = false;
323 if ( !writeDirHelper( pathForCollection( mCurrentFolder ) ) )
324 success = false;
325 if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/cur" ) )
326 success = false;
327 if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/new" ) )
328 success = false;
329 if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/tmp" ) )
330 success = false;
331 if ( !success ) {
332 abort( i18n( "Unable to create folder structure for folder '%1' within archive file.",
333 mCurrentFolder.name() ) );
334 return;
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() ) );
347 return;
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 ) )
361 return;
363 switch ( mArchiveType ) {
364 case Zip: {
365 KZip *zip = new KZip( mMailArchivePath.path() );
366 zip->setCompression( KZip::DeflateCompression );
367 mArchive = zip;
368 break;
370 case Tar: {
371 mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
372 break;
374 case TarGz: {
375 mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
376 break;
378 case TarBz2: {
379 mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
380 break;
384 kDebug() << "Starting backup.";
385 if ( !mArchive->open( QIODevice::WriteOnly ) ) {
386 abort( i18n( "Unable to open archive for writing." ) );
387 return;
390 mProgressItem = KPIM::ProgressManager::createProgressItem(
391 "BackupJob",
392 i18n( "Archiving" ),
393 QString(),
394 true );
395 mProgressItem->setUsesBusyIndicator( true );
396 connect( mProgressItem, SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
397 this, SLOT(cancelJob()) );
399 archiveNextFolder();
402 #include "backupjob.moc"