File indexing completed on 2024-05-05 17:57:23

0001 /*
0002     SPDX-FileCopyrightText: 2003 Csaba Karai <krusader@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2004-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "combiner.h"
0009 #include "../FileSystem/filesystem.h"
0010 
0011 // QtCore
0012 #include <QFileInfo>
0013 
0014 #include <KI18n/KLocalizedString>
0015 #include <KIO/Job>
0016 #include <KIO/JobUiDelegate>
0017 #include <KIOCore/KFileItem>
0018 #include <KWidgetsAddons/KMessageBox>
0019 #include <kio_version.h>
0020 #include <utility>
0021 
0022 // TODO: delete destination file on error
0023 // TODO: cache more than one byte array of data
0024 
0025 Combiner::Combiner(QWidget *parent, QUrl baseURLIn, QUrl destinationURLIn, bool unixNamingIn)
0026     : QProgressDialog(parent, Qt::WindowFlags())
0027     , baseURL(std::move(baseURLIn))
0028     , destinationURL(std::move(destinationURLIn))
0029     , hasValidSplitFile(false)
0030     , fileCounter(0)
0031     , permissions(-1)
0032     , receivedSize(0)
0033     , statJob(nullptr)
0034     , combineReadJob(nullptr)
0035     , combineWriteJob(nullptr)
0036     , unixNaming(unixNamingIn)
0037 {
0038     crcContext = new CRC32();
0039 
0040     splitFile = "";
0041 
0042     setMaximum(100);
0043     setAutoClose(false); /* don't close or reset the dialog automatically */
0044     setAutoReset(false);
0045     setLabelText("Krusader::Combiner");
0046     setWindowModality(Qt::WindowModal);
0047 
0048     firstFileIs000 = true; // start with this assumption, will set it to false as soon as .000 isn't found
0049 }
0050 
0051 Combiner::~Combiner()
0052 {
0053     combineAbortJobs();
0054     delete crcContext;
0055 }
0056 
0057 void Combiner::combine()
0058 {
0059     setWindowTitle(i18n("Krusader::Combining..."));
0060     setLabelText(i18n("Combining the file %1...", baseURL.toDisplayString(QUrl::PreferLocalFile)));
0061 
0062     /* check whether the .crc file exists */
0063     splURL = baseURL.adjusted(QUrl::RemoveFilename);
0064     splURL.setPath(splURL.path() + baseURL.fileName() + ".crc");
0065     KFileItem file(splURL);
0066     // FIXME: works only for local files - use KIO::stat() instead
0067     file.refresh();
0068 
0069     if (!file.isReadable()) {
0070         int ret = KMessageBox::questionYesNo(nullptr,
0071                                              i18n("The CRC information file (%1) is missing.\n"
0072                                                   "Validity checking is impossible without it. Continue combining?",
0073                                                   splURL.toDisplayString(QUrl::PreferLocalFile)),
0074                                              {},
0075                                              KStandardGuiItem::cont(),
0076                                              KStandardGuiItem::cancel());
0077 
0078         if (ret == KMessageBox::No) {
0079             reject();
0080             return;
0081         }
0082 
0083         statDest();
0084     } else {
0085         permissions = file.permissions() | QFile::WriteUser;
0086 
0087         combineReadJob = KIO::get(splURL, KIO::NoReload, KIO::HideProgressInfo);
0088 
0089         connect(combineReadJob, &KIO::TransferJob::data, this, &Combiner::combineSplitFileDataReceived);
0090         connect(combineReadJob, &KIO::TransferJob::result, this, &Combiner::combineSplitFileFinished);
0091     }
0092 
0093     exec();
0094 }
0095 
0096 void Combiner::combineSplitFileDataReceived(KIO::Job *, const QByteArray &byteArray)
0097 {
0098     splitFile += QString(byteArray);
0099 }
0100 
0101 void Combiner::combineSplitFileFinished(KJob *job)
0102 {
0103     combineReadJob = nullptr;
0104     QString error;
0105 
0106     if (job->error())
0107         error = i18n("Error at reading the CRC file (%1).", splURL.toDisplayString(QUrl::PreferLocalFile));
0108     else {
0109         splitFile.remove('\r'); // Windows compatibility
0110         QStringList splitFileContent = splitFile.split('\n');
0111 
0112         bool hasFileName = false, hasSize = false, hasCrc = false;
0113 
0114         for (int i = 0; i != splitFileContent.count(); i++) {
0115             int ndx = splitFileContent[i].indexOf('=');
0116             if (ndx == -1)
0117                 continue;
0118             QString token = splitFileContent[i].left(ndx).trimmed();
0119             QString value = splitFileContent[i].mid(ndx + 1);
0120 
0121             if (token == "filename") {
0122                 expectedFileName = value;
0123                 hasFileName = true;
0124             } else if (token == "size") {
0125                 // FIXME - don't use c functions !!!
0126                 sscanf(value.trimmed().toLocal8Bit(), "%llu", &expectedSize);
0127                 hasSize = true;
0128             }
0129             if (token == "crc32") {
0130                 expectedCrcSum = value.trimmed().rightJustified(8, '0');
0131                 hasCrc = true;
0132             }
0133         }
0134 
0135         if (!hasFileName || !hasSize || !hasCrc)
0136             error = i18n("Not a valid CRC file.");
0137         else
0138             hasValidSplitFile = true;
0139     }
0140 
0141     if (!error.isEmpty()) {
0142         int ret = KMessageBox::questionYesNo(nullptr,
0143                                              error + i18n("\nValidity checking is impossible without a good CRC file. Continue combining?"),
0144                                              {},
0145                                              KStandardGuiItem::cont(),
0146                                              KStandardGuiItem::cancel());
0147         if (ret == KMessageBox::No) {
0148             reject();
0149             return;
0150         }
0151     }
0152 
0153     statDest();
0154 }
0155 
0156 void Combiner::statDest()
0157 {
0158     if (writeURL.isEmpty()) {
0159         writeURL = FileSystem::ensureTrailingSlash(destinationURL);
0160         if (hasValidSplitFile)
0161             writeURL.setPath(writeURL.path() + expectedFileName);
0162         else if (unixNaming)
0163             writeURL.setPath(writeURL.path() + baseURL.fileName() + ".out");
0164         else
0165             writeURL.setPath(writeURL.path() + baseURL.fileName());
0166     }
0167 
0168 #if KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0)
0169     statJob = KIO::statDetails(writeURL, KIO::StatJob::DestinationSide, KIO::StatNoDetails, KIO::HideProgressInfo);
0170 #else
0171     statJob = KIO::stat(writeURL, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo);
0172 #endif
0173     connect(statJob, &KIO::StatJob::result, this, &Combiner::statDestResult);
0174 }
0175 
0176 void Combiner::statDestResult(KJob *job)
0177 {
0178     statJob = nullptr;
0179 
0180     if (job->error()) {
0181         if (job->error() == KIO::ERR_DOES_NOT_EXIST) {
0182             openNextFile();
0183         } else {
0184             dynamic_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
0185             reject();
0186         }
0187     } else { // destination already exists
0188         KIO::RenameDialog_Options mode = dynamic_cast<KIO::StatJob *>(job)->statResult().isDir() ? KIO::RenameDialog_IsDirectory : KIO::RenameDialog_Overwrite;
0189         KIO::RenameDialog dlg(this, i18n("File Already Exists"), QUrl(), writeURL, mode);
0190         switch (dlg.exec()) {
0191         case KIO::Result_Overwrite:
0192             openNextFile();
0193             break;
0194         case KIO::Result_Rename: {
0195             writeURL = dlg.newDestUrl();
0196             statDest();
0197             break;
0198         }
0199         default:
0200             reject();
0201         }
0202     }
0203 }
0204 
0205 void Combiner::openNextFile()
0206 {
0207     if (unixNaming) {
0208         if (readURL.isEmpty())
0209             readURL = baseURL;
0210         else {
0211             QString name = readURL.fileName();
0212             int pos = name.length() - 1;
0213             QChar ch;
0214 
0215             do {
0216                 ch = name.at(pos).toLatin1() + 1;
0217                 if (ch == QChar('Z' + 1))
0218                     ch = 'A';
0219                 if (ch == QChar('z' + 1))
0220                     ch = 'a';
0221                 name[pos] = ch;
0222                 pos--;
0223             } while (pos >= 0 && ch.toUpper() == QChar('A'));
0224             readURL = readURL.adjusted(QUrl::RemoveFilename);
0225             readURL.setPath(readURL.path() + name);
0226         }
0227     } else {
0228         QString index("%1"); /* determining the filename */
0229         index = index.arg(fileCounter++).rightJustified(3, '0');
0230         readURL = baseURL.adjusted(QUrl::RemoveFilename);
0231         readURL.setPath(readURL.path() + baseURL.fileName() + '.' + index);
0232     }
0233 
0234     /* creating a read job */
0235     combineReadJob = KIO::get(readURL, KIO::NoReload, KIO::HideProgressInfo);
0236 
0237     connect(combineReadJob, &KIO::TransferJob::data, this, &Combiner::combineDataReceived);
0238     connect(combineReadJob, &KIO::TransferJob::result, this, &Combiner::combineReceiveFinished);
0239     if (hasValidSplitFile)
0240         connect(combineReadJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(combineWritePercent(KJob *, ulong)));
0241 }
0242 
0243 void Combiner::combineDataReceived(KIO::Job *, const QByteArray &byteArray)
0244 {
0245     if (byteArray.size() == 0)
0246         return;
0247 
0248     crcContext->update(reinterpret_cast<unsigned char *>(const_cast<char *>(byteArray.data())), byteArray.size());
0249     transferArray = QByteArray(byteArray.data(), byteArray.length());
0250 
0251     receivedSize += byteArray.size();
0252 
0253     if (combineWriteJob == nullptr) {
0254         combineWriteJob = KIO::put(writeURL, permissions, KIO::HideProgressInfo | KIO::Overwrite);
0255 
0256         connect(combineWriteJob, &KIO::TransferJob::dataReq, this, &Combiner::combineDataSend);
0257         connect(combineWriteJob, &KIO::TransferJob::result, this, &Combiner::combineSendFinished);
0258     }
0259 
0260     // continue writing and suspend read job until received data is handed over to the write job
0261     combineReadJob->suspend();
0262     combineWriteJob->resume();
0263 }
0264 
0265 void Combiner::combineReceiveFinished(KJob *job)
0266 {
0267     combineReadJob = nullptr; /* KIO automatically deletes the object after Finished signal */
0268     if (job->error()) {
0269         if (job->error() == KIO::ERR_DOES_NOT_EXIST) {
0270             if (fileCounter == 1) { // .000 file doesn't exist but .001 is still a valid first file
0271                 firstFileIs000 = false;
0272                 openNextFile();
0273             } else if (!firstFileIs000 && fileCounter == 2) { // neither .000 nor .001 exist
0274                 combineAbortJobs();
0275                 KMessageBox::error(nullptr, i18n("Cannot open the first split file of %1.", baseURL.toDisplayString(QUrl::PreferLocalFile)));
0276                 reject();
0277             } else { // we've received the last file
0278                 // write out the remaining part of the file
0279                 combineWriteJob->resume();
0280 
0281                 if (hasValidSplitFile) {
0282                     QString crcResult = QString("%1").arg(crcContext->result(), 0, 16).toUpper().trimmed().rightJustified(8, '0');
0283 
0284                     if (receivedSize != expectedSize)
0285                         error = i18n("Incorrect filesize, the file might have been corrupted.");
0286                     else if (crcResult != expectedCrcSum.toUpper().trimmed())
0287                         error = i18n("Incorrect CRC checksum, the file might have been corrupted.");
0288                 }
0289             }
0290         } else {
0291             combineAbortJobs();
0292             dynamic_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
0293             reject();
0294         }
0295     } else
0296         openNextFile();
0297 }
0298 
0299 void Combiner::combineDataSend(KIO::Job *, QByteArray &byteArray)
0300 {
0301     byteArray = transferArray;
0302     transferArray = QByteArray();
0303 
0304     if (combineReadJob) {
0305         // continue reading and suspend write job until data is available
0306         combineReadJob->resume();
0307         combineWriteJob->suspend();
0308     }
0309 }
0310 
0311 void Combiner::combineSendFinished(KJob *job)
0312 {
0313     combineWriteJob = nullptr; /* KIO automatically deletes the object after Finished signal */
0314 
0315     if (job->error()) { /* any error occurred? */
0316         combineAbortJobs();
0317         dynamic_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
0318         reject();
0319     } else if (!error.isEmpty()) { /* was any error message at reading ? */
0320         combineAbortJobs(); /* we cannot write out it in combineReceiveFinished */
0321         KMessageBox::error(nullptr, error); /* because emit accept closes it in this function */
0322         reject();
0323     } else
0324         accept();
0325 }
0326 
0327 void Combiner::combineAbortJobs()
0328 {
0329     if (statJob)
0330         statJob->kill(KJob::Quietly);
0331     if (combineReadJob)
0332         combineReadJob->kill(KJob::Quietly);
0333     if (combineWriteJob)
0334         combineWriteJob->kill(KJob::Quietly);
0335 
0336     statJob = combineReadJob = combineWriteJob = nullptr;
0337 }
0338 
0339 void Combiner::combineWritePercent(KJob *, unsigned long)
0340 {
0341     auto percent = static_cast<int>(((static_cast<long double>(receivedSize) / expectedSize) * 100.) + 0.5);
0342     setValue(percent);
0343 }