File indexing completed on 2024-04-28 15:58:54

0001 /* This file is part of the KDE project
0002    Copyright (C) 2006-2013 Jarosław Staniek <staniek@kde.org>
0003 
0004    This library is free software; you can redistribute it and/or
0005    modify it under the terms of the GNU Library General Public
0006    License as published by the Free Software Foundation; either
0007    version 2 of the License, or (at your option) any later version.
0008 
0009    This library is distributed in the hope that it will be useful,
0010    but WITHOUT ANY WARRANTY; without even the implied warranty of
0011    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012    Library General Public License for more details.
0013 
0014    You should have received a copy of the GNU Library General Public License
0015    along with this library; see the file COPYING.LIB.  If not, write to
0016    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0017  * Boston, MA 02110-1301, USA.
0018 */
0019 
0020 #include "SqliteVacuum.h"
0021 #include "sqlite_debug.h"
0022 
0023 #include "KDb.h"
0024 
0025 #include <QMessageBox>
0026 #include <QProgressDialog>
0027 #include <QFileInfo>
0028 #include <QFile>
0029 #include <QDir>
0030 #include <QApplication>
0031 #include <QProcess>
0032 #include <QCursor>
0033 #include <QLocale>
0034 #include <QTemporaryFile>
0035 
0036 namespace {
0037 #ifdef Q_OS_WIN
0038 #include <Windows.h>
0039 void usleep(unsigned int usec)
0040 {
0041     Sleep(usec/1000);
0042 }
0043 
0044 //! @todo Use when it's in kdewin
0045 #define CONV(x) ((wchar_t*)x.utf16())
0046 int atomic_rename(const QString &in, const QString &out)
0047 {
0048     // better than :waccess/_wunlink/_wrename
0049 # ifndef _WIN32_WCE
0050     bool ok = (MoveFileExW(CONV(in), CONV(out),
0051                            MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) != 0);
0052 # else
0053     bool ok = (MoveFileW(CONV(in), CONV(out)) != 0);
0054 # endif
0055     return ok ? 0 : -1;
0056 }
0057 #else
0058 #include <unistd.h>
0059 int atomic_rename(const QString &in, const QString &out)
0060 {
0061     return ::rename(QFile::encodeName(in).constData(), QFile::encodeName(out).constData());
0062 }
0063 #endif
0064 } // namespace
0065 
0066 SqliteVacuum::SqliteVacuum(const QString& filePath)
0067         : m_filePath(filePath)
0068 {
0069     m_dumpProcess = nullptr;
0070     m_sqliteProcess = nullptr;
0071     m_percent = 0;
0072     m_dlg = nullptr;
0073     m_canceled = false;
0074 }
0075 
0076 SqliteVacuum::~SqliteVacuum()
0077 {
0078     if (m_dumpProcess) {
0079         m_dumpProcess->waitForFinished();
0080         delete m_dumpProcess;
0081     }
0082     if (m_sqliteProcess) {
0083         m_sqliteProcess->waitForFinished();
0084         delete m_sqliteProcess;
0085     }
0086     if (m_dlg)
0087         m_dlg->reset();
0088     delete m_dlg;
0089     QFile::remove(m_tmpFilePath);
0090 }
0091 
0092 tristate SqliteVacuum::run()
0093 {
0094     const QString dump_app = QString::fromLatin1(KDB_SQLITE_DUMP_TOOL);
0095     //sqliteDebug() << dump_app;
0096     if (dump_app.isEmpty()) {
0097         m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Could not find tool \"%1\".")
0098                              .arg(dump_app));
0099         sqliteWarning() << m_result;
0100         return false;
0101     }
0102     const QString sqlite_app(KDb::sqlite3ProgramPath());
0103     //sqliteDebug() << sqlite_app;
0104     if (sqlite_app.isEmpty()) {
0105         m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Could not find application \"%1\".")
0106                              .arg(sqlite_app));
0107         sqliteWarning() << m_result;
0108         return false;
0109     }
0110 
0111     QFileInfo fi(m_filePath);
0112     if (!fi.isReadable()) {
0113         m_result = KDbResult(ERR_OBJECT_NOT_FOUND, tr("Could not read file \"%1\".")
0114                              .arg(m_filePath));
0115         sqliteWarning() << m_result;
0116         return false;
0117     }
0118 
0119     //sqliteDebug() << fi.absoluteFilePath() << fi.absoluteDir().path();
0120 
0121     delete m_dumpProcess;
0122     m_dumpProcess = new QProcess(this);
0123     m_dumpProcess->setWorkingDirectory(fi.absoluteDir().path());
0124     m_dumpProcess->setReadChannel(QProcess::StandardError);
0125     connect(m_dumpProcess, SIGNAL(readyReadStandardError()), this, SLOT(readFromStdErr()));
0126     connect(m_dumpProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
0127             this, SLOT(dumpProcessFinished(int,QProcess::ExitStatus)));
0128 
0129     delete m_sqliteProcess;
0130     m_sqliteProcess = new QProcess(this);
0131     m_sqliteProcess->setWorkingDirectory(fi.absoluteDir().path());
0132     connect(m_sqliteProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
0133             this, SLOT(sqliteProcessFinished(int,QProcess::ExitStatus)));
0134 
0135     m_dumpProcess->setStandardOutputProcess(m_sqliteProcess);
0136     m_dumpProcess->start(dump_app, QStringList() << fi.absoluteFilePath());
0137     if (!m_dumpProcess->waitForStarted()) {
0138         delete m_dumpProcess;
0139         m_dumpProcess = nullptr;
0140         m_result.setCode(ERR_OTHER);
0141         return false;
0142     }
0143 
0144     {
0145         QTemporaryFile tempFile(fi.absoluteFilePath());
0146         if (!tempFile.open()) {
0147             delete m_dumpProcess;
0148             m_dumpProcess = nullptr;
0149             m_result.setCode(ERR_OTHER);
0150             return false;
0151         }
0152         m_tmpFilePath = tempFile.fileName();
0153     }
0154     //sqliteDebug() << m_tmpFilePath;
0155     m_sqliteProcess->start(sqlite_app, QStringList() << m_tmpFilePath);
0156     if (!m_sqliteProcess->waitForStarted()) {
0157         delete m_dumpProcess;
0158         m_dumpProcess = nullptr;
0159         delete m_sqliteProcess;
0160         m_sqliteProcess = nullptr;
0161         m_result.setCode(ERR_OTHER);
0162         return false;
0163     }
0164 
0165     delete m_dlg;
0166     m_dlg = new QProgressDialog(nullptr); // krazy:exclude=qclasses
0167     m_dlg->setWindowModality(Qt::WindowModal);
0168     m_dlg->setWindowTitle(tr("Compacting database"));
0169     m_dlg->setLabelText(
0170         QLatin1String("<qt>") + tr("Compacting database \"%1\"...")
0171             .arg(QLatin1String("<nobr>")
0172                  + QDir::fromNativeSeparators(fi.fileName())
0173                  + QLatin1String("</nobr>"))
0174     );
0175     m_dlg->adjustSize();
0176     m_dlg->resize(300, m_dlg->height());
0177     m_dlg->setMinimumDuration(1000);
0178     m_dlg->setAutoClose(true);
0179     m_dlg->setRange(0, 100);
0180     m_dlg->exec();
0181     if (m_dlg->wasCanceled()) {
0182         cancelClicked();
0183     }
0184     delete m_dlg;
0185     m_dlg = nullptr;
0186     while (m_dumpProcess->state() == QProcess::Running
0187            && m_sqliteProcess->state()  == QProcess::Running)
0188     {
0189         readFromStdErr();
0190         qApp->processEvents(QEventLoop::AllEvents, 50000);
0191     }
0192 
0193     readFromStdErr();
0194     return !m_result.isError();
0195 }
0196 
0197 void SqliteVacuum::readFromStdErr()
0198 {
0199     while (true) {
0200         QByteArray s(m_dumpProcess->readLine(1000));
0201         if (s.isEmpty())
0202             break;
0203         //sqliteDebug() << s;
0204         if (s.startsWith("DUMP: ")) {
0205             //set previously known progress
0206             if (m_dlg) {
0207                 m_dlg->setValue(m_percent);
0208             }
0209             //update progress info
0210             if (s.mid(6, 4) == "100%") {
0211                 m_percent = 100;
0212 //! @todo IMPORTANT: m_dlg->setAllowCancel(false);
0213                 if (m_dlg) {
0214                     m_dlg->setCursor(QCursor(Qt::WaitCursor));
0215                 }
0216             } else if (s.mid(7, 1) == "%") {
0217                 m_percent = s.mid(6, 1).toInt();
0218             } else if (s.mid(8, 1) == "%") {
0219                 m_percent = s.mid(6, 2).toInt();
0220             }
0221             if (m_dlg) {
0222                 m_dlg->setValue(m_percent);
0223             }
0224         }
0225     }
0226 }
0227 
0228 void SqliteVacuum::dumpProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
0229 {
0230     //sqliteDebug() << exitCode << exitStatus;
0231     if (exitCode != 0 || exitStatus != QProcess::NormalExit) {
0232         cancelClicked();
0233         m_result.setCode(ERR_OTHER);
0234     }
0235 }
0236 
0237 void SqliteVacuum::sqliteProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
0238 {
0239     //sqliteDebug() << exitCode << exitStatus;
0240     if (exitCode != 0 || exitStatus != QProcess::NormalExit) {
0241         m_result.setCode(ERR_OTHER);
0242     }
0243 
0244     if (m_dlg) {
0245         m_dlg->reset();
0246     }
0247 
0248     if (m_result.isError() || m_canceled) {
0249         return;
0250     }
0251 
0252     // dump process and sqlite process finished by now so we can rename the result to the original name
0253     QFileInfo fi(m_filePath);
0254     const qint64 origSize = fi.size();
0255 
0256     const QString newName(fi.absoluteFilePath());
0257     if (0 != atomic_rename(m_tmpFilePath, newName)) {
0258         m_result= KDbResult(ERR_ACCESS_RIGHTS,
0259                         tr("Could not rename file \"%1\" to \"%2\".").arg(m_tmpFilePath, newName));
0260         sqliteWarning() << m_result;
0261     }
0262 
0263     if (!m_result.isError()) {
0264         const qint64 newSize = QFileInfo(m_filePath).size();
0265         const qint64 decrease = 100 - 100 * newSize / origSize;
0266         QMessageBox::information(nullptr, QString(), // krazy:exclude=qclasses
0267             tr("The database has been compacted. Current size decreased by %1% to %2 MB.")
0268                .arg(decrease).arg(QLocale().toString(double(newSize)/1000000.0, 'f', 2)));
0269     }
0270 }
0271 
0272 void SqliteVacuum::cancelClicked()
0273 {
0274     m_sqliteProcess->terminate();
0275     m_canceled = true;
0276     QFile::remove(m_tmpFilePath);
0277 }