File indexing completed on 2024-04-14 03:40:36

0001 /*
0002     This file is part of Kiten, a KDE Japanese Reference Tool...
0003     SPDX-FileCopyrightText: 2011 Daniel E. Moctezuma <democtezuma@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "dictionaryupdatemanager.h"
0009 
0010 #include "kiten.h"
0011 #include "kitenconfig.h"
0012 #include "kitenmacros.h"
0013 
0014 #include <KActionCollection>
0015 #include <KCompressionDevice>
0016 #include <KIO/StoredTransferJob>
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 #include <QAction>
0020 #include <QDir>
0021 #include <QUrl>
0022 
0023 #include <QStandardPaths>
0024 #include <QStringDecoder>
0025 #include <QTemporaryFile>
0026 #include <QTextStream>
0027 
0028 // URL to the information file.
0029 #define INFO_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/edicthdr.txt")
0030 // URL to the EDICT dictionary.
0031 #define EDICT_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/edict.gz")
0032 // URL to the KANJIDIC dictionary.
0033 #define KANJIDIC_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/kanjidic.gz")
0034 
0035 using namespace Qt::StringLiterals;
0036 
0037 DictionaryUpdateManager::DictionaryUpdateManager(Kiten *parent)
0038     : QObject(parent)
0039     , _parent(parent)
0040     , _config(parent->getConfig())
0041     , _succeeded(QStringList())
0042     , _failed(QStringList())
0043     , _counter(0)
0044 {
0045     _actionUpdate = _parent->actionCollection()->add<QAction>(QStringLiteral("update_dictionaries"));
0046     _actionUpdate->setText(i18n("Check for dictionary &updates"));
0047     _parent->actionCollection()->setDefaultShortcut(_actionUpdate, Qt::CTRL | Qt::Key_U);
0048 
0049     connect(_actionUpdate, &QAction::triggered, this, &DictionaryUpdateManager::checkForUpdates);
0050 }
0051 
0052 void DictionaryUpdateManager::checkForUpdates()
0053 {
0054     qDebug() << "Checking for EDICT & KANJIDIC updates.";
0055     // Download the information file we need to check
0056     // whether or not an update to our dictionaries is necessary.
0057     KIO::StoredTransferJob *job = KIO::storedGet(QUrl(INFO_URL));
0058     connect(job, &KIO::StoredTransferJob::result, this, &DictionaryUpdateManager::checkInfoFile);
0059 }
0060 
0061 void DictionaryUpdateManager::checkIfUpdateFinished()
0062 {
0063     // Emit the updateFinished signal once we finished
0064     // to update our installed dictionaries.
0065     if (_counter == _config->dictionary_list().size()) {
0066         // Make sure to reset this variable to 0.
0067         _counter = 0;
0068         Q_EMIT updateFinished();
0069     }
0070 }
0071 
0072 void DictionaryUpdateManager::checkInfoFile(KJob *job)
0073 {
0074     KIO::StoredTransferJob *storedJob = static_cast<KIO::StoredTransferJob *>(job);
0075     QByteArray data(storedJob->data());
0076     // Check if we have valid data to work with.
0077     if (data.isNull() || data.isEmpty()) {
0078         KMessageBox::error(nullptr, i18n("Update canceled.\nCould not read file."));
0079         job->deleteLater();
0080         return;
0081     }
0082 
0083     // We don't need to store this information file in our Hard Disk Drive,
0084     // a temporary file is enough.
0085     QTemporaryFile tempFile;
0086     if (!tempFile.open()) {
0087         KMessageBox::error(nullptr, i18n("Update canceled.\nCould not open file."));
0088         qDebug() << "Could not open tempFile.";
0089         tempFile.deleteLater();
0090         job->deleteLater();
0091         return;
0092     }
0093     tempFile.write(data);
0094     tempFile.close();
0095 
0096     // Get the latest creation date for the EDICT dictionary on the server.
0097     QDate webFileDate = getFileDate(tempFile);
0098     if (!webFileDate.isValid()) {
0099         // The program should not get to this point.
0100         // Maybe the format/content of (one or both) the files changed?
0101         // or maybe one or both of the files could not be opened in the
0102         // getFileDate function that would return an invalid empty date.
0103         KMessageBox::error(nullptr, i18n("Update canceled.\nThe update information file has an invalid date."));
0104         tempFile.deleteLater();
0105         job->deleteLater();
0106         return;
0107     }
0108 
0109     // At this point we have a valid creation date from the server.
0110     // Now we can check our dictionaries.
0111 
0112     // Connect to this signal to know when the update process is finished.
0113     connect(this, &DictionaryUpdateManager::updateFinished, this, &DictionaryUpdateManager::showUpdateResults);
0114 
0115     // This variable help us to know if we need to download any file.
0116     bool updates = false;
0117     // Iterate on each of our installed dictionaries.
0118     for (const QString &dict : _config->dictionary_list()) {
0119         QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kiten/") + dict.toLower());
0120         QFile file(filePath);
0121         qDebug() << "Local dictionary path:" << file.fileName();
0122 
0123         // Get the creation date for this dictionary.
0124         QDate localFileDate = getFileDate(file);
0125 
0126         if (!localFileDate.isValid()) {
0127             // Add it to our 'failed to udate' list.
0128             _failed.append(dict.toUpper());
0129             qDebug() << "Failed (invalid date):" << dict.toUpper();
0130             continue;
0131         } else if (localFileDate == webFileDate) {
0132             // Add it to our 'up to date' list.
0133             _succeeded.append(dict.toUpper());
0134             qDebug() << "Success (up to date):" << dict.toUpper();
0135             continue;
0136         } else {
0137             // Indicate we need to download something.
0138             updates = true;
0139 
0140             if (dict.toLower() == EDICT) {
0141                 downloadDictionary(EDICT_URL);
0142             } else {
0143                 downloadDictionary(KANJIDIC_URL);
0144             }
0145         }
0146     }
0147 
0148     // Let Kiten know we finished and don't need to download anything else.
0149     if (!updates) {
0150         Q_EMIT updateFinished();
0151     }
0152 
0153     tempFile.deleteLater();
0154     job->deleteLater();
0155 }
0156 
0157 void DictionaryUpdateManager::downloadDictionary(const QString &url)
0158 {
0159     qDebug() << "Download started!";
0160     // Download dictionary.
0161     KIO::StoredTransferJob *dictionaryJob = KIO::storedGet(QUrl(url));
0162     connect(dictionaryJob, &KIO::StoredTransferJob::result, this, &DictionaryUpdateManager::installDictionary);
0163 }
0164 
0165 QDate DictionaryUpdateManager::getFileDate(QFile &file)
0166 {
0167     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0168         qDebug() << "Could not open " << file.fileName();
0169         return QDate();
0170     }
0171 
0172     QStringDecoder decoder("EUC-JP");
0173     const QString decoded = decoder(file.readAll());
0174 
0175     QTextStream fileStream(decoded.toUtf8());
0176 
0177     // The first line of the file is in the following form:
0178     //  ??? /
0179     // EDICT, EDICT_SUB(P), EDICT2 Japanese-English Electronic Dictionary Files/
0180     // Copyright Electronic Dictionary Research & Development Group - year/
0181     // Created: year-month-day/
0182     // ###### entries
0183 
0184     // NOTE: the above last line only appears in the web status file for EDICT.
0185     // That file has only 2 lines, one is the '?' symbols with the name of the
0186     // dictionary, copyright and creation date, the other one is the number
0187     // of entries in the EDICT dictionary.
0188     // (see INFO_URL macro).
0189 
0190     // We take the 4th section which is the last one
0191     // separated by the '/' character and the
0192     // 10 rightmost characters for that section.
0193     QString dateSection = fileStream.readLine().section('/'_L1, -1, -1, QString::SectionSkipEmpty).right(10);
0194 
0195     // dateSection has the following value: year-month-day
0196     // Finally we take the numbers separated by the '-' character.
0197     int year = dateSection.section('-'_L1, 0, 0).toInt();
0198     int month = dateSection.section('-'_L1, 1, 1).toInt();
0199     int day = dateSection.section('-'_L1, 2, 2).toInt();
0200 
0201     file.close();
0202 
0203     qDebug() << "Date found:" << dateSection << "(" << file.fileName() << ")";
0204 
0205     return QDate(year, month, day);
0206 }
0207 
0208 void DictionaryUpdateManager::installDictionary(KJob *job)
0209 {
0210     // Increase the number of dictionaries we try to install.
0211     // This way we can know when we finished with our installed dictionaries.
0212     _counter++;
0213 
0214     KIO::StoredTransferJob *storedJob = static_cast<KIO::StoredTransferJob *>(job);
0215     QByteArray data(storedJob->data());
0216     QString url(storedJob->url().toDisplayString());
0217 
0218     // What we actually downloaded was a GZIP file that we need to extract.
0219     // As there is no need to keep this file, we make it a temporary file.
0220     QTemporaryFile compressedFile;
0221     if (!compressedFile.open()) {
0222         qDebug() << "Could not create the downloaded .gz file.";
0223         _failed.append(url.contains(EDICT) ? EDICT : KANJIDIC);
0224         job->deleteLater();
0225         checkIfUpdateFinished();
0226         return;
0227     }
0228     // Create the GZIP file from the downloaded data.
0229     compressedFile.write(data);
0230     compressedFile.close();
0231 
0232     qDebug() << "Dictionary download finished!";
0233     qDebug() << "Extracting dictionary...";
0234 
0235     // Extract the GZIP file.
0236     // QIODevice *device = KFilterDev::deviceForFile( compressedFile.fileName(), "application/x-gzip" );
0237     KCompressionDevice *device = new KCompressionDevice(compressedFile.fileName(), KCompressionDevice::GZip);
0238     if (!device->open(QIODevice::ReadOnly)) {
0239         qDebug() << "Could not extract the dictionary file.";
0240         _failed.append(url.contains(EDICT) ? EDICT : KANJIDIC);
0241         delete device;
0242         job->deleteLater();
0243         checkIfUpdateFinished();
0244         return;
0245     }
0246 
0247     // Check the first line's first character of the extracted file.
0248     // EDICT starts with a ' ', KANJIDIC starts with a '#'.
0249     QString fileName = device->readLine().startsWith('#') ? QStringLiteral("kanjidic") : QStringLiteral("edict");
0250     // Reset the position where we are going to start reading the content.
0251     device->reset();
0252     // Thanks to the above lines we can get the path to the correct file to be updated.
0253     QString dictPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kiten/");
0254     QDir dir(dictPath);
0255     dir.mkpath(dictPath);
0256 
0257     QFile dictionary(dictPath + fileName);
0258     if (!dictionary.open(QIODevice::WriteOnly)) {
0259         qDebug() << "Could not create the new dictionary file.";
0260         _failed.append(fileName.toUpper());
0261         device->close();
0262         delete device;
0263         job->deleteLater();
0264         checkIfUpdateFinished();
0265         return;
0266     }
0267 
0268     // Write the new dictionary file to disk.
0269     dictionary.write(device->readAll());
0270     dictionary.close();
0271     device->close();
0272 
0273     delete device;
0274     job->deleteLater();
0275 
0276     // Check if we finished updating.
0277     checkIfUpdateFinished();
0278     qDebug() << "Successfully installed at:" << dictionary.fileName();
0279 }
0280 
0281 void DictionaryUpdateManager::showUpdateResults()
0282 {
0283     // Avoid multiple calls to this slot.
0284     disconnect(this, &DictionaryUpdateManager::updateFinished, this, &DictionaryUpdateManager::showUpdateResults);
0285 
0286     if (!_succeeded.isEmpty() && _failed.isEmpty()) {
0287         KMessageBox::information(nullptr, i18n("You already have the latest updates."));
0288     } else if (_succeeded.isEmpty() && _failed.isEmpty()) {
0289         KMessageBox::information(nullptr, i18n("Successfully updated your dictionaries."));
0290     } else if (!_succeeded.isEmpty() && !_failed.isEmpty()) {
0291         KMessageBox::information(nullptr, i18n("Successfully updated:\n%1\n\nFailed to update:\n%2", _succeeded.join("\n"_L1), _failed.join("\n"_L1)));
0292     } else if (_succeeded.isEmpty() && !_failed.isEmpty()) {
0293         KMessageBox::error(nullptr, i18n("Failed to update:\n%1", _failed.join("\n"_L1)));
0294     }
0295 
0296     // Avoid repetitions in our lists.
0297     _succeeded.clear();
0298     _failed.clear();
0299 }
0300 
0301 #include "moc_dictionaryupdatemanager.cpp"