1 /***************************************************************************
2 * Copyright (C) 2003 by S�astien Laot *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
24 #include "variouswidgets.h"
27 #include "formatimporter.h" // To move a folder
33 #include <qpushbutton.h>
35 #include <Q3TextStream>
36 #include <Q3HBoxLayout>
39 #include <kapplication.h>
40 #include <kaboutdata.h>
41 #include <q3groupbox.h>
42 #include <kdirselectdialog.h>
46 #include <kfiledialog.h>
47 #include <kprogress.h>
48 #include <kmessagebox.h>
49 #include <unistd.h> // usleep()
52 * Backups are wrapped in a .tar.gz, inside that folder name.
53 * An archive is not a backup or is corrupted if data are not in that folder!
55 const QString backupMagicFolder
= "BasKet-Note-Pads_Backup";
57 /** class BackupDialog: */
59 BackupDialog::BackupDialog(QWidget
*parent
, const char *name
)
60 : KDialogBase(parent
, name
, /*modal=*/true, i18n("Backup & Restore"),
61 KDialogBase::Close
, KDialogBase::Close
, /*separator=*/false)
63 Q3VBox
*page
= makeVBoxMainWidget();
64 // page->setSpacing(spacingHint());
66 QString savesFolder
= Global::savesFolder();
67 savesFolder
= savesFolder
.left(savesFolder
.length() - 1); // savesFolder ends with "/"
69 Q3GroupBox
*folderGroup
= new Q3GroupBox(1, Qt::Horizontal
, i18n("Save Folder"), page
);
70 new QLabel("<qt><nobr>" + i18n("Your baskets are currently stored in that folder:<br><b>%1</b>").arg(savesFolder
), folderGroup
);
71 QWidget
*folderWidget
= new QWidget(folderGroup
);
72 Q3HBoxLayout
*folderLayout
= new Q3HBoxLayout(folderWidget
, 0, spacingHint());
73 QPushButton
*moveFolder
= new QPushButton(i18n("&Move to Another Folder..."), folderWidget
);
74 QPushButton
*useFolder
= new QPushButton(i18n("&Use Another Existing Folder..."), folderWidget
);
75 HelpLabel
*helpLabel
= new HelpLabel(i18n("Why to do that?"), i18n(
76 "<p>You can move the folder where %1 store your baskets to:</p><ul>"
77 "<li>Store your baskets in a visible place in your home folder, like ~/Notes or ~/Baskets, so you can manually backup them when you want.</li>"
78 "<li>Store your baskets on a server to share them between two computers.<br>"
79 "In this case, mount the shared-folder to the local file system and ask %2 to use that mount point.<br>"
80 "Warning: you should not run %3 at the same time on both computers, or you risk to loss data while the two applications are desynced.</li>"
81 "</ul><p>Please remember that you should not change the content of that folder manually (eg. adding a file in a basket folder will not add that file to the basket).</p>")
82 .arg(kapp
->aboutData()->programName())
83 .arg(kapp
->aboutData()->programName())
84 .arg(kapp
->aboutData()->programName()),
86 folderLayout
->addWidget(moveFolder
);
87 folderLayout
->addWidget(useFolder
);
88 folderLayout
->addWidget(helpLabel
);
89 folderLayout
->addStretch();
90 connect( moveFolder
, SIGNAL(clicked()), this, SLOT(moveToAnotherFolder()) );
91 connect( useFolder
, SIGNAL(clicked()), this, SLOT(useAnotherExistingFolder()) );
93 Q3GroupBox
*backupGroup
= new Q3GroupBox(1, Qt::Horizontal
, i18n("Backups"), page
);
94 QWidget
*backupWidget
= new QWidget(backupGroup
);
95 Q3HBoxLayout
*backupLayout
= new Q3HBoxLayout(backupWidget
, 0, spacingHint());
96 QPushButton
*backupButton
= new QPushButton(i18n("&Backup..."), backupWidget
);
97 QPushButton
*restoreButton
= new QPushButton(i18n("&Restore a Backup..."), backupWidget
);
98 m_lastBackup
= new QLabel("", backupWidget
);
99 backupLayout
->addWidget(backupButton
);
100 backupLayout
->addWidget(restoreButton
);
101 backupLayout
->addWidget(m_lastBackup
);
102 backupLayout
->addStretch();
103 connect( backupButton
, SIGNAL(clicked()), this, SLOT(backup()) );
104 connect( restoreButton
, SIGNAL(clicked()), this, SLOT(restore()) );
106 populateLastBackup();
108 (new QWidget(page
))->setSizePolicy(QSizePolicy::Expanding
, QSizePolicy::Expanding
);
111 BackupDialog::~BackupDialog()
115 void BackupDialog::populateLastBackup()
117 QString lastBackupText
= i18n("Last backup: never");
118 if (Settings::lastBackup().isValid())
119 lastBackupText
= i18n("Last backup: %1").arg(Settings::lastBackup().toString(Qt::LocalDate
));
121 m_lastBackup
->setText(lastBackupText
);
124 void BackupDialog::moveToAnotherFolder()
126 KURL selectedURL
= KDirSelectDialog::selectDirectory(
127 /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0,
128 /*caption=*/i18n("Choose a Folder Where to Move Baskets"));
130 if (!selectedURL
.isEmpty()) {
131 QString folder
= selectedURL
.path();
133 // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway):
135 // Get the content of the folder:
136 QStringList content
= dir
.entryList();
137 if (content
.count() > 2) { // "." and ".."
138 int result
= KMessageBox::questionYesNo(
140 "<qt>" + i18n("The folder <b>%1</b> is not empty. Do you want to override it?").arg(folder
),
141 i18n("Override Folder?"),
142 KGuiItem(i18n("&Override"), "filesave")
144 if (result
== KMessageBox::No
)
147 Tools::deleteRecursively(folder
);
149 FormatImporter copier
;
150 copier
.moveFolder(Global::savesFolder(), folder
);
151 Backup::setFolderAndRestart(folder
, i18n("Your baskets have been successfuly moved to <b>%1</b>. %2 is going to be restarted to take this change into account."));
155 void BackupDialog::useAnotherExistingFolder()
157 KURL selectedURL
= KDirSelectDialog::selectDirectory(
158 /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0,
159 /*caption=*/i18n("Choose an Existing Folder to Store Baskets"));
161 if (!selectedURL
.isEmpty()) {
162 Backup::setFolderAndRestart(selectedURL
.path(), i18n("Your basket save folder has been successfuly changed to <b>%1</b>. %2 is going to be restarted to take this change into account."));
166 void BackupDialog::backup()
170 // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"):
171 KConfig
*config
= KGlobal::config();
172 config
->setGroup("Backups");
173 QString folder
= config
->readEntry("lastFolder", QDir::homeDirPath()) + "/";
174 QString fileName
= i18n("Backup filename (without extension), %1 is the date", "Baskets_%1")
175 .arg(QDate::currentDate().toString(Qt::ISODate
));
176 QString url
= folder
+ fileName
;
178 // Ask a file name & path to the user:
179 QString filter
= "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
180 QString destination
= url
;
181 for (bool askAgain
= true; askAgain
; ) {
183 destination
= KFileDialog::getSaveFileName(destination
, filter
, 0, i18n("Backup Baskets"));
185 if (destination
.isEmpty())
187 // File already existing? Ask for overriding:
188 if (dir
.exists(destination
)) {
189 int result
= KMessageBox::questionYesNoCancel(
191 "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to override it?")
192 .arg(KURL(destination
).fileName()),
193 i18n("Override File?"),
194 KGuiItem(i18n("&Override"), "filesave")
196 if (result
== KMessageBox::Cancel
)
198 else if (result
== KMessageBox::Yes
)
204 KProgressDialog
dialog(0, 0, i18n("Backup Baskets"), i18n("Backing up baskets. Please wait..."), /*modal=*/true);
205 dialog
.setAllowCancel(false);
206 dialog
.setAutoClose(true);
208 KProgress
*progress
= dialog
.progressBar();
209 progress
->setTotalSteps(0/*Busy/Undefined*/);
210 progress
->setProgress(0);
211 progress
->setPercentageVisible(false);
213 BackupThread
thread(destination
, Global::savesFolder());
215 while (thread
.running()) {
216 progress
->advance(1); // Or else, the animation is not played!
217 kapp
->processEvents();
218 usleep(300); // Not too long because if the backup process is finished, we wait for nothing
221 Settings::setLastBackup(QDate::currentDate());
222 Settings::saveConfig();
223 populateLastBackup();
226 void BackupDialog::restore()
228 // Get last backup folder:
229 KConfig
*config
= KGlobal::config();
230 config
->setGroup("Backups");
231 QString folder
= config
->readEntry("lastFolder", QDir::homeDirPath()) + "/";
233 // Ask a file name to the user:
234 QString filter
= "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
235 QString path
= KFileDialog::getOpenFileName(folder
, filter
, this, i18n("Open Basket Archive"));
236 if (path
.isEmpty()) // User has canceled
239 // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder.
240 // So if the backup is corrupted or something goes wrong while restoring (power cut...) the user will be able to restore the old working data:
241 QString safetyPath
= Backup::newSafetyFolder();
242 FormatImporter copier
;
243 copier
.moveFolder(Global::savesFolder(), safetyPath
);
245 // Add the README file for user to cancel a bad restoration:
246 QString readmePath
= safetyPath
+ i18n("README.txt");
247 QFile
file(readmePath
);
248 if (file
.open(QIODevice::WriteOnly
)) {
249 Q3TextStream
stream(&file
);
250 stream
<< i18n("This is a safety copy of your baskets like they were before you started to restore the backup %1.").arg(KURL(path
).fileName()) + "\n\n"
251 << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n"
252 << i18n("If something went wrong during the restoration process, you can re-use this folder to store your baskets and nothing will be lost.") + "\n\n"
253 << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + "\n";
258 "<p><nobr>" + i18n("Restoring <b>%1</b>. Please wait...").arg(KURL(path
).fileName()) + "</nobr></p><p>" +
259 i18n("If something goes wrong during the restoration process, read the file <b>%1</b>.").arg(readmePath
);
261 KProgressDialog
*dialog
= new KProgressDialog(0, 0, i18n("Restore Baskets"), message
, /*modal=*/true);
262 dialog
->setAllowCancel(false);
263 dialog
->setAutoClose(true);
265 KProgress
*progress
= dialog
->progressBar();
266 progress
->setTotalSteps(0/*Busy/Undefined*/);
267 progress
->setProgress(0);
268 progress
->setPercentageVisible(false);
271 RestoreThread
thread(path
, Global::savesFolder());
273 while (thread
.running()) {
274 progress
->advance(1); // Or else, the animation is not played!
275 kapp
->processEvents();
276 usleep(300); // Not too long because if the restore process is finished, we wait for nothing
279 dialog
->hide(); // The restore is finished, do not continue to show it while telling the user the application is going to be restarted
280 delete dialog
; // If we only hidden it, it reappeared just after having restored a small backup... Very strange.
281 dialog
= 0; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable.
282 //kapp->processEvents();
285 if (!thread
.success()) {
286 // Restore the old baskets:
288 dir
.remove(readmePath
);
289 copier
.moveFolder(safetyPath
, Global::savesFolder());
291 KMessageBox::error(0, i18n("This archive is either not a backup of baskets or is corrupted. It cannot be imported. Your old baskets have been preserved instead."), i18n("Restore Error"));
295 // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhapse restored an older backup by error...
296 // The restore process will not be called very often (it is possible it will only be called once or twice arround the world during the next years).
297 // So it is rare enough to force the user to remove the safety folder, but keep him in control and let him safely recover from restoration errors.
299 Backup::setFolderAndRestart(Global::savesFolder()/*No change*/, i18n("Your backup has been successfuly restored to <b>%1</b>. %2 is going to be restarted to take this change into account."));
304 QString
Backup::binaryPath
= "";
308 void Backup::figureOutBinaryPath(const char *argv0
, QApplication
&app
)
311 The application can be launched by two ways:
312 - Globaly (app.applicationFilePath() is good)
313 - In KDevelop or with an absolute path (app.applicationFilePath() is wrong)
314 This function is called at the very start of main() so that the current directory has not been changed yet.
316 Command line (argv[0]) QDir(argv[0]).canonicalPath() app.applicationFilePath()
317 ====================== ============================================= =========================
318 "basket" "" "/opt/kde3/bin/basket"
319 "./src/.libs/basket" "/home/seb/prog/basket/debug/src/.lib/basket" "/opt/kde3/bin/basket"
322 binaryPath
= QDir(argv0
).canonicalPath();
323 if (binaryPath
.isEmpty())
324 binaryPath
= app
.applicationFilePath();
327 void Backup::setFolderAndRestart(const QString
&folder
, const QString
&message
)
330 Settings::setDataFolder(folder
);
331 Settings::saveConfig();
333 // Rassure the user that the application main window disapearition is not a crash, but a normal restart.
334 // This is important for users to trust the application in such a critical phase and understands what's happening:
335 KMessageBox::information(
337 "<qt>" + message
.arg(
338 (folder
.endsWith("/") ? folder
.left(folder
.length() - 1) : folder
),
339 kapp
->aboutData()->programName()),
343 // Restart the application:
344 KRun::runCommand(binaryPath
, kapp
->aboutData()->programName(), kapp
->iconName());
348 QString
Backup::newSafetyFolder()
353 fullPath
= QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + "/";
354 if (!dir
.exists(fullPath
))
357 for (int i
= 2; ; ++i
) {
358 fullPath
= QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)").arg(i
) + "/";
359 if (!dir
.exists(fullPath
))
366 /** class BackupThread: */
368 BackupThread::BackupThread(const QString
&tarFile
, const QString
&folderToBackup
)
369 : m_tarFile(tarFile
), m_folderToBackup(folderToBackup
)
373 void BackupThread::run()
375 KTar
tar(m_tarFile
, "application/x-gzip");
376 tar
.open(QIODevice::WriteOnly
);
377 tar
.addLocalDirectory(m_folderToBackup
, backupMagicFolder
);
378 // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually:
379 QDir
dir(m_folderToBackup
+ "baskets/");
380 QStringList baskets
= dir
.entryList(QDir::Dirs
);
381 for (QStringList::Iterator it
= baskets
.begin(); it
!= baskets
.end(); ++it
) {
383 m_folderToBackup
+ "baskets/" + *it
+ "/.basket",
384 backupMagicFolder
+ "/baskets/" + *it
+ "/.basket"
391 /** class RestoreThread: */
393 RestoreThread::RestoreThread(const QString
&tarFile
, const QString
&destFolder
)
394 : m_tarFile(tarFile
), m_destFolder(destFolder
)
398 void RestoreThread::run()
401 KTar
tar(m_tarFile
, "application/x-gzip");
402 tar
.open(QIODevice::ReadOnly
);
403 if (tar
.isOpened()) {
404 const KArchiveDirectory
*directory
= tar
.directory();
405 if (directory
->entries().contains(backupMagicFolder
)) {
406 const KArchiveEntry
*entry
= directory
->entry(backupMagicFolder
);
407 if (entry
->isDirectory()) {
408 ((const KArchiveDirectory
*) entry
)->copyTo(m_destFolder
);
416 #include "backup.moc"