File indexing completed on 2025-01-19 03:52:59

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2013-11-18
0007  * Description : a tool to export items to Google web services
0008  *
0009  * SPDX-FileCopyrightText: 2013      by Pankaj Kumar <me at panks dot me>
0010  * SPDX-FileCopyrightText: 2013-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 "gdtalker.h"
0017 
0018 // Qt includes
0019 
0020 #include <QMimeDatabase>
0021 #include <QApplication>
0022 #include <QJsonDocument>
0023 #include <QJsonParseError>
0024 #include <QJsonObject>
0025 #include <QJsonValue>
0026 #include <QJsonArray>
0027 #include <QByteArray>
0028 #include <QFileInfo>
0029 #include <QMessageBox>
0030 #include <QUrlQuery>
0031 
0032 // KDE includes
0033 
0034 #include <klocalizedstring.h>
0035 
0036 // Local includes
0037 
0038 #include "wstoolutils.h"
0039 #include "digikam_version.h"
0040 #include "gswindow.h"
0041 #include "gdmpform.h"
0042 #include "digikam_debug.h"
0043 #include "previewloadthread.h"
0044 #include "dmetadata.h"
0045 
0046 using namespace Digikam;
0047 
0048 namespace DigikamGenericGoogleServicesPlugin
0049 {
0050 
0051 static bool gdriveLessThan(const GSFolder& p1, const GSFolder& p2)
0052 {
0053     return (p1.title.toLower() < p2.title.toLower());
0054 }
0055 
0056 class Q_DECL_HIDDEN GDTalker::Private
0057 {
0058 public:
0059 
0060     enum State
0061     {
0062         GD_LOGOUT      = -1,
0063         GD_LISTFOLDERS = 0,
0064         GD_CREATEFOLDER,
0065         GD_ADDPHOTO,
0066         GD_USERNAME,
0067     };
0068 
0069 public:
0070 
0071     explicit Private()
0072       : apiUrl(QLatin1String("https://www.googleapis.com/drive/v2/%1")),
0073         uploadUrl(QLatin1String("https://www.googleapis.com/upload/drive/v2/files")),
0074         rootid(QLatin1String("root")),
0075         rootfoldername(QLatin1String("GoogleDrive Root")),
0076         state(GD_LOGOUT),
0077         listPhotoId(QStringList())
0078     {
0079     }
0080 
0081 public:
0082 
0083     QString                apiUrl;
0084     QString                uploadUrl;
0085     QString                rootid;
0086     QString                rootfoldername;
0087     QString                username;
0088     State                  state;
0089     QStringList            listPhotoId;
0090 };
0091 
0092 GDTalker::GDTalker(QWidget* const parent)
0093     : GSTalkerBase(parent, QStringList(QLatin1String("https://www.googleapis.com/auth/drive")), QLatin1String("GoogleDrive")),
0094       d(new Private)
0095 {
0096     connect(m_service->networkAccessManager(), SIGNAL(finished(QNetworkReply*)),
0097             this, SLOT(slotFinished(QNetworkReply*)));
0098 
0099     connect(this, SIGNAL(signalReadyToUpload()),
0100             this, SLOT(slotUploadPhoto()));
0101 }
0102 
0103 GDTalker::~GDTalker()
0104 {
0105     if (m_reply)
0106     {
0107         m_reply->abort();
0108     }
0109 
0110     WSToolUtils::removeTemporaryDir("google");
0111 
0112     delete d;
0113 }
0114 
0115 /**
0116  * Gets username
0117  */
0118 void GDTalker::getUserName()
0119 {
0120     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "getUserName";
0121 
0122     QUrl url(d->apiUrl.arg(QLatin1String("about")));
0123 
0124     m_reply  = m_service->get(url);
0125 
0126     d->state = Private::GD_USERNAME;
0127 
0128     Q_EMIT signalBusy(true);
0129 }
0130 
0131 /**
0132  * Gets list of folder of user in json format
0133  */
0134 void GDTalker::listFolders()
0135 {
0136     QUrl url(d->apiUrl.arg(QLatin1String("files")));
0137 
0138     QUrlQuery q;
0139     q.addQueryItem(QLatin1String("q"), QLatin1String("mimeType = 'application/vnd.google-apps.folder'"));
0140 
0141     url.setQuery(q);
0142 
0143     m_reply  = m_service->get(url);
0144 
0145     d->state = Private::GD_LISTFOLDERS;
0146 
0147     Q_EMIT signalBusy(true);
0148 }
0149 
0150 /**
0151  * Creates folder inside any folder(of which id is passed)
0152  */
0153 void GDTalker::createFolder(const QString& title, const QString& id)
0154 {
0155     if (m_reply)
0156     {
0157         m_reply->abort();
0158         m_reply = nullptr;
0159     }
0160 
0161     QUrl url(d->apiUrl.arg(QLatin1String("files")));
0162     QByteArray data;
0163     data += "{\"title\":\"";
0164     data += title.toLatin1();
0165     data += "\",\r\n";
0166     data += "\"parents\":";
0167     data += "[{";
0168     data += "\"id\":\"";
0169     data += id.toLatin1();
0170     data += "\"}],\r\n";
0171     data += "\"mimeType\":";
0172     data += "\"application/vnd.google-apps.folder\"";
0173     data += "}\r\n";
0174 
0175     // qCDebug(DIGIKAM_WEBSERVICES_LOG) << "data:" << data;
0176 
0177     m_reply  = m_service->post(url, data);
0178 
0179     d->state = Private::GD_CREATEFOLDER;
0180 
0181     Q_EMIT signalBusy(true);
0182 }
0183 
0184 bool GDTalker::addPhoto(const QString& imgPath, const GSPhoto& info,
0185                         const QString& id, bool original, bool rescale, int maxDim, int imageQuality)
0186 {
0187     if (m_reply)
0188     {
0189         m_reply->abort();
0190         m_reply = nullptr;
0191     }
0192 
0193     Q_EMIT signalBusy(true);
0194 
0195     QString path = imgPath;
0196 
0197     QMimeDatabase mimeDB;
0198 
0199     if (!original && mimeDB.mimeTypeForFile(imgPath).name().startsWith(QLatin1String("image/")))
0200     {
0201         QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
0202 
0203         if (image.isNull())
0204         {
0205             image.load(imgPath);
0206         }
0207 
0208         if (image.isNull())
0209         {
0210             Q_EMIT signalBusy(false);
0211             return false;
0212         }
0213 
0214         path = WSToolUtils::makeTemporaryDir("google").filePath(QFileInfo(imgPath)
0215                                              .baseName().trimmed() + QLatin1String(".jpg"));
0216 
0217         if (rescale && (image.width() > maxDim || image.height() > maxDim))
0218         {
0219             image = image.scaled(maxDim,maxDim,Qt::KeepAspectRatio,Qt::SmoothTransformation);
0220         }
0221 
0222         image.save(path, "JPEG", imageQuality);
0223 
0224         QScopedPointer<DMetadata> meta(new DMetadata);
0225 
0226         if (meta->load(imgPath))
0227         {
0228             meta->setItemDimensions(image.size());
0229             meta->setItemOrientation(MetaEngine::ORIENTATION_NORMAL);
0230             meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
0231             meta->save(path, true);
0232         }
0233     }
0234 
0235     GDMPForm form;
0236     form.addPair(QUrl::fromLocalFile(path).fileName(), info.description, path, id);
0237 
0238     if (!form.addFile(path))
0239     {
0240         Q_EMIT signalBusy(false);
0241         return false;
0242     }
0243 
0244     form.finish();
0245 
0246     QUrl url(d->uploadUrl);
0247 
0248     QUrlQuery q;
0249     q.addQueryItem(QLatin1String("uploadType"), QLatin1String("multipart"));
0250 
0251     url.setQuery(q);
0252 
0253     QNetworkRequest netRequest(url);
0254     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
0255     netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1());
0256     netRequest.setRawHeader("Host", "www.googleapis.com");
0257 
0258     m_reply  = m_service->networkAccessManager()->post(netRequest, form.formData());
0259 
0260     d->state = Private::GD_ADDPHOTO;
0261 
0262     return true;
0263 }
0264 
0265 void GDTalker::slotFinished(QNetworkReply* reply)
0266 {
0267     if (reply != m_reply)
0268     {
0269         return;
0270     }
0271 
0272     m_reply = nullptr;
0273 
0274     if (reply->error() != QNetworkReply::NoError)
0275     {
0276         Q_EMIT signalBusy(false);
0277         QMessageBox::critical(QApplication::activeWindow(),
0278                               i18nc("@title:window", "Error"), reply->errorString());
0279 
0280         reply->deleteLater();
0281         return;
0282     }
0283 
0284     QByteArray buffer =  reply->readAll();
0285 
0286     switch (d->state)
0287     {
0288         case (Private::GD_LOGOUT):
0289             break;
0290         case (Private::GD_LISTFOLDERS):
0291             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In Private::GD_LISTFOLDERS";
0292             parseResponseListFolders(buffer);
0293             break;
0294         case (Private::GD_CREATEFOLDER):
0295             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In Private::GD_CREATEFOLDER";
0296             parseResponseCreateFolder(buffer);
0297             break;
0298         case (Private::GD_ADDPHOTO):
0299             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In Private::GD_ADDPHOTO"; // << buffer;
0300             parseResponseAddPhoto(buffer);
0301             break;
0302         case (Private::GD_USERNAME):
0303             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In Private::GD_USERNAME"; // << buffer;
0304             parseResponseUserName(buffer);
0305             break;
0306         default:
0307             break;
0308     }
0309 
0310     reply->deleteLater();
0311 }
0312 
0313 void GDTalker::slotUploadPhoto()
0314 {
0315     qCDebug(DIGIKAM_WEBSERVICES_LOG) << d->listPhotoId.join(QLatin1String(", "));
0316     Q_EMIT signalUploadPhotoDone(1, QString(), d->listPhotoId);
0317 }
0318 
0319 void GDTalker::parseResponseUserName(const QByteArray& data)
0320 {
0321     QJsonParseError err;
0322     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0323 
0324     if (err.error != QJsonParseError::NoError)
0325     {
0326         Q_EMIT signalBusy(false);
0327         return;
0328     }
0329 
0330     QJsonObject jsonObject = doc.object();
0331     qCDebug(DIGIKAM_WEBSERVICES_LOG)<<"User Name is: " << jsonObject[QLatin1String("name")].toString();
0332     QString temp           = jsonObject[QLatin1String("name")].toString();
0333 
0334     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "in parseResponseUserName";
0335 
0336     Q_EMIT signalBusy(false);
0337     Q_EMIT signalSetUserName(temp);
0338 }
0339 
0340 void GDTalker::parseResponseListFolders(const QByteArray& data)
0341 {
0342     QJsonParseError err;
0343     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0344 
0345     qCDebug(DIGIKAM_WEBSERVICES_LOG) << doc;
0346 
0347     if (err.error != QJsonParseError::NoError)
0348     {
0349         Q_EMIT signalBusy(false);
0350         Q_EMIT signalListAlbumsDone(0,i18n("Failed to list folders"),QList<GSFolder>());
0351         return;
0352     }
0353 
0354     QJsonObject jsonObject = doc.object();
0355     QJsonArray jsonArray   = jsonObject[QLatin1String("items")].toArray();
0356 
0357     QList<GSFolder> albumList;
0358     GSFolder fps;
0359     fps.id    = d->rootid;
0360     fps.title = d->rootfoldername;
0361     albumList.append(fps);
0362 
0363     Q_FOREACH (const QJsonValue& value, jsonArray)
0364     {
0365         QJsonObject obj      = value.toObject();
0366 
0367         // Verify if album is in trash
0368         QJsonObject labels   = obj[QLatin1String("labels")].toObject();
0369         bool        trashed  = labels[QLatin1String("trashed")].toBool();
0370 
0371         // Verify if album is editable
0372         bool        editable = obj[QLatin1String("editable")].toBool();
0373 
0374         /* Verify if album is visualized in a folder inside My Drive
0375          * If parents is empty, album is shared by another person and not added to My Drive yet
0376          */
0377         QJsonArray  parents  = obj[QLatin1String("parents")].toArray();
0378 
0379         fps.id          = obj[QLatin1String("id")].toString();
0380         fps.title       = obj[QLatin1String("title")].toString();
0381 
0382         if (editable && !trashed && !parents.isEmpty())
0383         {
0384             albumList.append(fps);
0385         }
0386     }
0387 
0388     std::sort(albumList.begin(), albumList.end(), gdriveLessThan);
0389 
0390     Q_EMIT signalBusy(false);
0391     Q_EMIT signalListAlbumsDone(1, QString(), albumList);
0392 }
0393 
0394 void GDTalker::parseResponseCreateFolder(const QByteArray& data)
0395 {
0396     QJsonParseError err;
0397     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0398 
0399     if (err.error != QJsonParseError::NoError)
0400     {
0401         Q_EMIT signalBusy(false);
0402         return;
0403     }
0404 
0405     QJsonObject jsonObject = doc.object();
0406     QString temp           = jsonObject[QLatin1String("alternateLink")].toString();
0407     bool success           = false;
0408 
0409     if (!(QString::compare(temp, QLatin1String(""), Qt::CaseInsensitive) == 0))
0410         success = true;
0411 
0412     Q_EMIT signalBusy(false);
0413 
0414     if (!success)
0415     {
0416         Q_EMIT signalCreateFolderDone(0,i18n("Failed to create folder"));
0417     }
0418     else
0419     {
0420         Q_EMIT signalCreateFolderDone(1,QString());
0421     }
0422 }
0423 
0424 void GDTalker::parseResponseAddPhoto(const QByteArray& data)
0425 {
0426     QJsonParseError err;
0427     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0428 
0429     if (err.error != QJsonParseError::NoError)
0430     {
0431         Q_EMIT signalBusy(false);
0432         return;
0433     }
0434 
0435     QJsonObject jsonObject = doc.object();
0436     QString altLink        = jsonObject[QLatin1String("alternateLink")].toString();
0437     QString photoId        = jsonObject[QLatin1String("id")].toString();
0438     bool success           = false;
0439 
0440     if (!(QString::compare(altLink, QLatin1String(""), Qt::CaseInsensitive) == 0))
0441         success = true;
0442 
0443     Q_EMIT signalBusy(false);
0444 
0445     if (!success)
0446     {
0447         Q_EMIT signalAddPhotoDone(0, i18n("Failed to upload photo"));
0448     }
0449     else
0450     {
0451         d->listPhotoId << photoId;
0452         Q_EMIT signalAddPhotoDone(1, QString());
0453     }
0454 }
0455 
0456 void GDTalker::cancel()
0457 {
0458     if (m_reply)
0459     {
0460         m_reply->abort();
0461         m_reply = nullptr;
0462     }
0463 
0464     Q_EMIT signalBusy(false);
0465 }
0466 
0467 } // namespace DigikamGenericGoogleServicesPlugin
0468 
0469 #include "moc_gdtalker.cpp"