File indexing completed on 2025-01-05 03:53:35

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2012-02-12
0007  * Description : a tool to export images to IPFS web service
0008  *
0009  * SPDX-FileCopyrightText: 2018      by Amar Lakshya <amar dot lakshya at xaviers dot edu dot in>
0010  * SPDX-FileCopyrightText: 2018-2020 by Caulier Gilles <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "ipfstalker.h"
0017 
0018 // Qt includes
0019 
0020 #include <QUrl>
0021 #include <QFile>
0022 #include <QQueue>
0023 #include <QUrlQuery>
0024 #include <QFileInfo>
0025 #include <QHttpMultiPart>
0026 #include <QJsonDocument>
0027 #include <QJsonObject>
0028 #include <QNetworkReply>
0029 
0030 // KDE includes
0031 
0032 #include <klocalizedstring.h>
0033 
0034 // Local includes
0035 
0036 #include "digikam_debug.h"
0037 #include "networkmanager.h"
0038 
0039 namespace DigikamGenericIpfsPlugin
0040 {
0041 
0042 class Q_DECL_HIDDEN IpfsTalker::Private
0043 {
0044 public:
0045 
0046     explicit Private()
0047       : ipfsUploadUrl(QLatin1String("https://api.globalupload.io/transport/add")),
0048         workTimer    (0),
0049         reply        (nullptr),
0050         image        (nullptr),
0051         netMngr      (nullptr)
0052     {
0053     }
0054 
0055     // The ipfs upload url
0056 
0057     const QString            ipfsUploadUrl;
0058 
0059     // Work queue
0060 
0061     QQueue<IpfsTalkerAction> workQueue;
0062 
0063     // ID of timer triggering on idle (0ms)
0064 
0065     int                      workTimer;
0066 
0067     // Current QNetworkReply
0068 
0069     QNetworkReply*           reply;
0070 
0071     // Current image being uploaded
0072 
0073     QFile*                   image;
0074 
0075     // The QNetworkAccessManager used for connections
0076 
0077     QNetworkAccessManager*   netMngr;
0078 };
0079 
0080 IpfsTalker::IpfsTalker(QObject* const parent)
0081     : QObject(parent),
0082       d      (new Private)
0083 {
0084     d->netMngr = NetworkManager::instance()->getNetworkManager(this);
0085 }
0086 
0087 IpfsTalker::~IpfsTalker()
0088 {
0089     // Disconnect all signals as cancelAllWork may emit
0090 
0091     disconnect(this, nullptr, nullptr, nullptr);
0092     cancelAllWork();
0093 
0094     delete d;
0095 }
0096 
0097 unsigned int IpfsTalker::workQueueLength()
0098 {
0099     return d->workQueue.size();
0100 }
0101 
0102 void IpfsTalker::queueWork(const IpfsTalkerAction& action)
0103 {
0104     d->workQueue.enqueue(action);
0105     startWorkTimer();
0106 }
0107 
0108 void IpfsTalker::cancelAllWork()
0109 {
0110     stopWorkTimer();
0111 
0112     if (d->reply)
0113     {
0114         d->reply->abort();
0115     }
0116 
0117     // Should error be emitted for those actions?
0118 
0119     while (!d->workQueue.empty())
0120     {
0121         d->workQueue.dequeue();
0122     }
0123 }
0124 
0125 void IpfsTalker::uploadProgress(qint64 sent, qint64 total)
0126 {
0127     if (total > 0) // Don't divide by 0
0128     {
0129         Q_EMIT progress((sent * 100) / total, d->workQueue.first());
0130     }
0131 }
0132 
0133 void IpfsTalker::replyFinished()
0134 {
0135     auto* reply = d->reply;
0136     reply->deleteLater();
0137     d->reply     = nullptr;
0138 
0139     if (d->image)
0140     {
0141         delete d->image;
0142         d->image = nullptr;
0143     }
0144 
0145     if (d->workQueue.empty())
0146     {
0147         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received result without request";
0148         return;
0149     }
0150 
0151     // toInt() returns 0 if conversion fails. That fits nicely already.
0152 
0153     int netCode   = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0154     auto response = QJsonDocument::fromJson(reply->readAll());
0155 
0156     if ((netCode == 200) && !response.isEmpty())
0157     {
0158         /* Success! */
0159 
0160         IpfsTalkerResult result;
0161         result.action = &d->workQueue.first();
0162 
0163         switch (result.action->type)
0164         {
0165             case IpfsTalkerActionType::IMG_UPLOAD:
0166             {
0167                 result.image.name = response.object()[QLatin1String("Name")].toString();
0168                 result.image.size = response.object()[QLatin1String("Size")].toInt();
0169                 result.image.url  = QLatin1String("https://ipfs.io/ipfs/") + response.object()[QLatin1String("Hash")].toString();
0170                 break;
0171             }
0172 
0173             default:
0174             {
0175                 qCWarning(DIGIKAM_WEBSERVICES_LOG) << "Unexpected action";
0176                 qCDebug(DIGIKAM_WEBSERVICES_LOG) << response.toJson();
0177                 break;
0178             }
0179         }
0180 
0181         Q_EMIT success(result);
0182     }
0183     else
0184     {
0185         if (netCode == 403)
0186         {
0187             /* HTTP 403 Forbidden -> Invalid token?
0188              * That needs to be handled internally, so don't Q_EMIT progress
0189              * and keep the action in the queue for later retries.
0190              */
0191             return;
0192         }
0193         else
0194         {
0195             /*
0196              * Failed.
0197              */
0198             auto msg = response.object()[QLatin1String("data")]
0199                        .toObject()[QLatin1String("error")]
0200                        .toString(QLatin1String("Could not read response."));
0201 
0202             Q_EMIT error(msg, d->workQueue.first());
0203         }
0204     }
0205 
0206     // Next work item.
0207 
0208     d->workQueue.dequeue();
0209     startWorkTimer();
0210 }
0211 
0212 void IpfsTalker::timerEvent(QTimerEvent* event)
0213 {
0214     if (event->timerId() != d->workTimer)
0215     {
0216         QObject::timerEvent(event);
0217         return;
0218     }
0219 
0220     event->accept();
0221 
0222     // One-shot only.
0223 
0224     QObject::killTimer(event->timerId());
0225     d->workTimer = 0;
0226 
0227     doWork();
0228 }
0229 
0230 void IpfsTalker::startWorkTimer()
0231 {
0232     if (!d->workQueue.empty() && (d->workTimer == 0))
0233     {
0234         d->workTimer = QObject::startTimer(0);
0235         Q_EMIT busy(true);
0236     }
0237     else
0238     {
0239         Q_EMIT busy(false);
0240     }
0241 }
0242 
0243 void IpfsTalker::stopWorkTimer()
0244 {
0245     if (d->workTimer != 0)
0246     {
0247         QObject::killTimer(d->workTimer);
0248         d->workTimer = 0;
0249     }
0250 }
0251 
0252 void IpfsTalker::doWork()
0253 {
0254     if (d->workQueue.empty() || (d->reply != nullptr))
0255     {
0256         return;
0257     }
0258 
0259     auto &work = d->workQueue.first();
0260 
0261     switch (work.type)
0262     {
0263         case IpfsTalkerActionType::IMG_UPLOAD:
0264         {
0265             d->image = new QFile(work.upload.imgpath);
0266 
0267             if (!d->image->open(QIODevice::ReadOnly))
0268             {
0269                 delete d->image;
0270                 d->image = nullptr;
0271 
0272                 /* Failed. */
0273 
0274                 Q_EMIT error(i18n("Could not open file"), d->workQueue.first());
0275 
0276                 d->workQueue.dequeue();
0277                 doWork();
0278                 return;
0279             }
0280 
0281             /* Set ownership to d->image to delete that as well. */
0282 
0283             auto* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType, d->image);
0284             QHttpPart title;
0285             title.setHeader(QNetworkRequest::ContentDispositionHeader,
0286                             QLatin1String("form-data; name=\"keyphrase\""));
0287             multipart->append(title);
0288 
0289             QHttpPart description;
0290             description.setHeader(QNetworkRequest::ContentDispositionHeader,
0291                                   QLatin1String("form-data; name=\"user\""));
0292             multipart->append(description);
0293 
0294             QHttpPart image;
0295             image.setHeader(QNetworkRequest::ContentDispositionHeader,
0296                             QVariant(QString::fromLatin1("form-data; name=\"file\";  filename=\"%1\"")
0297                             .arg(QLatin1String(QFileInfo(work.upload.imgpath).fileName().toUtf8().toPercentEncoding()))));
0298             image.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("image/jpeg"));
0299             image.setBodyDevice(d->image);
0300             multipart->append(image);
0301             QNetworkRequest request(QUrl(d->ipfsUploadUrl));
0302             d->reply = d->netMngr->post(request, multipart);
0303 
0304             break;
0305         }
0306     }
0307 
0308     if (d->reply)
0309     {
0310         connect(d->reply, &QNetworkReply::uploadProgress,
0311                 this, &IpfsTalker::uploadProgress);
0312 
0313         connect(d->reply, &QNetworkReply::finished,
0314                 this, &IpfsTalker::replyFinished);
0315     }
0316 }
0317 
0318 } // namespace DigikamGenericIpfsPlugin
0319 
0320 #include "moc_ipfstalker.cpp"