File indexing completed on 2023-10-03 03:04:58

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