File indexing completed on 2025-01-05 03:53:24
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 images to Dropbox web service 0008 * 0009 * SPDX-FileCopyrightText: 2013 by Pankaj Kumar <me at panks dot me> 0010 * SPDX-FileCopyrightText: 2018-2019 by Maik Qualmann <metzpinguin at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include <dbtalker.h> 0017 0018 // Qt includes 0019 0020 #include <QMimeDatabase> 0021 #include <QJsonDocument> 0022 #include <QJsonParseError> 0023 #include <QJsonObject> 0024 #include <QJsonValue> 0025 #include <QJsonArray> 0026 #include <QByteArray> 0027 #include <QFileInfo> 0028 #include <QWidget> 0029 #include <QSettings> 0030 #include <QMessageBox> 0031 #include <QApplication> 0032 #include <QDesktopServices> 0033 0034 // KDE includes 0035 0036 #include <klocalizedstring.h> 0037 0038 // Local includes 0039 0040 #include "digikam_debug.h" 0041 #include "digikam_version.h" 0042 #include "previewloadthread.h" 0043 #include "wstoolutils.h" 0044 #include "dmetadata.h" 0045 #include "dbwindow.h" 0046 #include "dbmpform.h" 0047 #include "dbitem.h" 0048 #include "o2.h" 0049 #include "o0globals.h" 0050 #include "o0settingsstore.h" 0051 #include "networkmanager.h" 0052 0053 using namespace Digikam; 0054 0055 namespace DigikamGenericDropBoxPlugin 0056 { 0057 0058 class Q_DECL_HIDDEN DBTalker::Private 0059 { 0060 public: 0061 0062 enum State 0063 { 0064 DB_USERNAME = 0, 0065 DB_LISTFOLDERS, 0066 DB_CREATEFOLDER, 0067 DB_ADDPHOTO 0068 }; 0069 0070 public: 0071 0072 explicit Private(QWidget* const p) 0073 { 0074 apikey = QLatin1String("mv2pk07ym9bx3r8"); 0075 secret = QLatin1String("f33sflc8jhiozqu"); 0076 0077 authUrl = QLatin1String("https://www.dropbox.com/oauth2/authorize"); 0078 tokenUrl = QLatin1String("https://api.dropboxapi.com/oauth2/token"); 0079 0080 state = DB_USERNAME; 0081 settings = nullptr; 0082 netMngr = nullptr; 0083 reply = nullptr; 0084 o2 = nullptr; 0085 parent = p; 0086 } 0087 0088 public: 0089 0090 QString apikey; 0091 QString secret; 0092 QString authUrl; 0093 QString tokenUrl; 0094 QList<QPair<QString, QString> > folderList; 0095 0096 QWidget* parent; 0097 QNetworkAccessManager* netMngr; 0098 0099 QNetworkReply* reply; 0100 0101 QSettings* settings; 0102 0103 State state; 0104 0105 O2* o2; 0106 }; 0107 0108 DBTalker::DBTalker(QWidget* const parent) 0109 : d (new Private(parent)) 0110 { 0111 d->netMngr = NetworkManager::instance()->getNetworkManager(this); 0112 0113 connect(d->netMngr, SIGNAL(finished(QNetworkReply*)), 0114 this, SLOT(slotFinished(QNetworkReply*))); 0115 0116 d->o2 = new O2(this); 0117 0118 d->o2->setClientId(d->apikey); 0119 d->o2->setClientSecret(d->secret); 0120 d->o2->setRefreshTokenUrl(d->tokenUrl); 0121 d->o2->setRequestUrl(d->authUrl); 0122 d->o2->setTokenUrl(d->tokenUrl); 0123 d->o2->setLocalPort(8000); 0124 0125 d->settings = WSToolUtils::getOauthSettings(this); 0126 O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this); 0127 store->setGroupKey(QLatin1String("Dropbox")); 0128 d->o2->setStore(store); 0129 0130 connect(d->o2, SIGNAL(linkingFailed()), 0131 this, SLOT(slotLinkingFailed())); 0132 0133 connect(d->o2, SIGNAL(linkingSucceeded()), 0134 this, SLOT(slotLinkingSucceeded())); 0135 0136 connect(d->o2, SIGNAL(openBrowser(QUrl)), 0137 this, SLOT(slotOpenBrowser(QUrl))); 0138 } 0139 0140 DBTalker::~DBTalker() 0141 { 0142 if (d->reply) 0143 { 0144 d->reply->abort(); 0145 } 0146 0147 WSToolUtils::removeTemporaryDir("dropbox"); 0148 0149 delete d; 0150 } 0151 0152 void DBTalker::link() 0153 { 0154 Q_EMIT signalBusy(true); 0155 d->o2->link(); 0156 } 0157 0158 void DBTalker::unLink() 0159 { 0160 d->o2->unlink(); 0161 0162 d->settings->beginGroup(QLatin1String("Dropbox")); 0163 d->settings->remove(QString()); 0164 d->settings->endGroup(); 0165 } 0166 0167 void DBTalker::reauthenticate() 0168 { 0169 d->o2->unlink(); 0170 0171 // Wait until user account is unlinked completely 0172 while (authenticated()); 0173 0174 d->o2->link(); 0175 } 0176 0177 void DBTalker::slotLinkingFailed() 0178 { 0179 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Dropbox fail"; 0180 Q_EMIT signalBusy(false); 0181 } 0182 0183 void DBTalker::slotLinkingSucceeded() 0184 { 0185 if (!d->o2->linked()) 0186 { 0187 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Dropbox ok"; 0188 Q_EMIT signalBusy(false); 0189 return; 0190 } 0191 0192 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Dropbox ok"; 0193 Q_EMIT signalLinkingSucceeded(); 0194 } 0195 0196 void DBTalker::slotOpenBrowser(const QUrl& url) 0197 { 0198 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser..."; 0199 QDesktopServices::openUrl(url); 0200 } 0201 0202 bool DBTalker::authenticated() 0203 { 0204 return d->o2->linked(); 0205 } 0206 0207 /** Creates folder at specified path 0208 */ 0209 void DBTalker::createFolder(const QString& path) 0210 { 0211 //path also has name of new folder so send path parameter accordingly 0212 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "createFolder:" << path; 0213 0214 QUrl url(QLatin1String("https://api.dropboxapi.com/2/files/create_folder_v2")); 0215 0216 QNetworkRequest netRequest(url); 0217 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_JSON)); 0218 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0219 0220 QByteArray postData = QString::fromUtf8("{\"path\": \"%1\"}").arg(path).toUtf8(); 0221 0222 d->reply = d->netMngr->post(netRequest, postData); 0223 0224 d->state = Private::DB_CREATEFOLDER; 0225 Q_EMIT signalBusy(true); 0226 } 0227 0228 /** Get username of dropbox user 0229 */ 0230 void DBTalker::getUserName() 0231 { 0232 QUrl url(QLatin1String("https://api.dropboxapi.com/2/users/get_current_account")); 0233 0234 QNetworkRequest netRequest(url); 0235 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0236 0237 d->reply = d->netMngr->post(netRequest, QByteArray()); 0238 0239 d->state = Private::DB_USERNAME; 0240 Q_EMIT signalBusy(true); 0241 } 0242 0243 /** Get list of folders by parsing json sent by dropbox 0244 */ 0245 void DBTalker::listFolders(const QString& cursor) 0246 { 0247 QUrl url(QLatin1String("https://api.dropboxapi.com/2/files/list_folder")); 0248 QByteArray postData; 0249 0250 if (cursor.isEmpty()) 0251 { 0252 d->folderList.clear(); 0253 postData = QString::fromUtf8("{\"path\": \"\",\"recursive\": true}").toUtf8(); 0254 } 0255 else 0256 { 0257 url.setPath(url.path() + QLatin1String("/continue")); 0258 postData = QString::fromUtf8("{\"cursor\": \"%1\"}").arg(cursor).toUtf8(); 0259 } 0260 0261 QNetworkRequest netRequest(url); 0262 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_JSON)); 0263 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0264 0265 d->reply = d->netMngr->post(netRequest, postData); 0266 0267 d->state = Private::DB_LISTFOLDERS; 0268 Q_EMIT signalBusy(true); 0269 } 0270 0271 bool DBTalker::addPhoto(const QString& imgPath, const QString& uploadFolder, 0272 bool original, bool rescale, int maxDim, int imageQuality) 0273 { 0274 if (d->reply) 0275 { 0276 d->reply->abort(); 0277 d->reply = nullptr; 0278 } 0279 0280 Q_EMIT signalBusy(true); 0281 0282 QString path = imgPath; 0283 0284 QMimeDatabase mimeDB; 0285 0286 if (!original && mimeDB.mimeTypeForFile(imgPath).name().startsWith(QLatin1String("image/"))) 0287 { 0288 QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage(); 0289 0290 if (image.isNull()) 0291 { 0292 image.load(imgPath); 0293 } 0294 0295 if (image.isNull()) 0296 { 0297 Q_EMIT signalBusy(false); 0298 return false; 0299 } 0300 0301 path = WSToolUtils::makeTemporaryDir("dropbox").filePath(QFileInfo(imgPath) 0302 .baseName().trimmed() + QLatin1String(".jpg")); 0303 0304 if (rescale && (image.width() > maxDim || image.height() > maxDim)) 0305 { 0306 image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0307 } 0308 0309 image.save(path, "JPEG", imageQuality); 0310 0311 QScopedPointer<DMetadata> meta(new DMetadata); 0312 0313 if (meta->load(imgPath)) 0314 { 0315 meta->setItemDimensions(image.size()); 0316 meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL); 0317 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); 0318 meta->save(path, true); 0319 } 0320 } 0321 0322 DBMPForm form; 0323 0324 if (!form.addFile(path)) 0325 { 0326 Q_EMIT signalBusy(false); 0327 return false; 0328 } 0329 0330 QString uploadPath = uploadFolder + QUrl(QUrl::fromLocalFile(imgPath)).fileName(); 0331 QUrl url(QLatin1String("https://content.dropboxapi.com/2/files/upload")); 0332 0333 QNetworkRequest netRequest(url); 0334 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/octet-stream")); 0335 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0336 0337 QByteArray postData = QString::fromUtf8("{\"path\": \"%1\",\"mode\": \"add\"}").arg(uploadPath).toUtf8(); 0338 netRequest.setRawHeader("Dropbox-API-Arg", postData); 0339 0340 d->reply = d->netMngr->post(netRequest, form.formData()); 0341 0342 d->state = Private::DB_ADDPHOTO; 0343 return true; 0344 } 0345 0346 void DBTalker::cancel() 0347 { 0348 if (d->reply) 0349 { 0350 d->reply->abort(); 0351 d->reply = nullptr; 0352 } 0353 0354 Q_EMIT signalBusy(false); 0355 } 0356 0357 void DBTalker::slotFinished(QNetworkReply* reply) 0358 { 0359 if (reply != d->reply) 0360 { 0361 return; 0362 } 0363 0364 d->reply = nullptr; 0365 0366 if (reply->error() != QNetworkReply::NoError) 0367 { 0368 if (d->state != Private::DB_CREATEFOLDER) 0369 { 0370 Q_EMIT signalBusy(false); 0371 QMessageBox::critical(QApplication::activeWindow(), 0372 i18nc("@title:window", "Error"), reply->errorString()); 0373 0374 reply->deleteLater(); 0375 return; 0376 } 0377 } 0378 0379 QByteArray buffer = reply->readAll(); 0380 0381 switch (d->state) 0382 { 0383 case Private::DB_LISTFOLDERS: 0384 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_LISTFOLDERS"; 0385 parseResponseListFolders(buffer); 0386 break; 0387 case Private::DB_CREATEFOLDER: 0388 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_CREATEFOLDER"; 0389 parseResponseCreateFolder(buffer); 0390 break; 0391 case Private::DB_ADDPHOTO: 0392 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_ADDPHOTO"; 0393 parseResponseAddPhoto(buffer); 0394 break; 0395 case Private::DB_USERNAME: 0396 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_USERNAME"; 0397 parseResponseUserName(buffer); 0398 break; 0399 default: 0400 break; 0401 } 0402 0403 reply->deleteLater(); 0404 } 0405 0406 void DBTalker::parseResponseAddPhoto(const QByteArray& data) 0407 { 0408 QJsonDocument doc = QJsonDocument::fromJson(data); 0409 QJsonObject jsonObject = doc.object(); 0410 bool success = jsonObject.contains(QLatin1String("size")); 0411 Q_EMIT signalBusy(false); 0412 0413 if (!success) 0414 { 0415 Q_EMIT signalAddPhotoFailed(i18n("Failed to upload photo")); 0416 } 0417 else 0418 { 0419 Q_EMIT signalAddPhotoSucceeded(); 0420 } 0421 } 0422 0423 void DBTalker::parseResponseUserName(const QByteArray& data) 0424 { 0425 QJsonDocument doc = QJsonDocument::fromJson(data); 0426 QJsonObject jsonObject = doc.object()[QLatin1String("name")].toObject(); 0427 0428 QString name = jsonObject[QLatin1String("display_name")].toString(); 0429 0430 Q_EMIT signalBusy(false); 0431 Q_EMIT signalSetUserName(name); 0432 } 0433 0434 void DBTalker::parseResponseListFolders(const QByteArray& data) 0435 { 0436 QJsonParseError err; 0437 QJsonDocument doc = QJsonDocument::fromJson(data, &err); 0438 0439 if (err.error != QJsonParseError::NoError) 0440 { 0441 Q_EMIT signalBusy(false); 0442 Q_EMIT signalListAlbumsFailed(i18n("Failed to list folders")); 0443 return; 0444 } 0445 0446 QJsonObject jsonObject = doc.object(); 0447 QJsonArray jsonArray = jsonObject[QLatin1String("entries")].toArray(); 0448 0449 if (d->folderList.isEmpty()) 0450 { 0451 d->folderList.append(qMakePair(QLatin1String(""), QLatin1String("root"))); 0452 } 0453 0454 Q_FOREACH (const QJsonValue& value, jsonArray) 0455 { 0456 QString path; 0457 QString folder; 0458 0459 QJsonObject obj = value.toObject(); 0460 path = obj[QLatin1String("path_display")].toString(); 0461 folder = obj[QLatin1String(".tag")].toString(); 0462 0463 if (folder == QLatin1String("folder")) 0464 { 0465 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Path is" << path; 0466 QString listName = path.section(QLatin1Char('/'), 1); 0467 d->folderList.append(qMakePair(path, listName)); 0468 } 0469 } 0470 0471 if (jsonObject[QLatin1String("has_more")].toBool()) 0472 { 0473 QString cursor = jsonObject[QLatin1String("cursor")].toString(); 0474 0475 if (!cursor.isEmpty()) 0476 { 0477 listFolders(cursor); 0478 return; 0479 } 0480 } 0481 0482 std::sort(d->folderList.begin(), d->folderList.end()); 0483 0484 Q_EMIT signalBusy(false); 0485 Q_EMIT signalListAlbumsDone(d->folderList); 0486 } 0487 0488 void DBTalker::parseResponseCreateFolder(const QByteArray& data) 0489 { 0490 QJsonDocument doc = QJsonDocument::fromJson(data); 0491 QJsonObject jsonObject = doc.object(); 0492 bool fail = jsonObject.contains(QLatin1String("error")); 0493 0494 Q_EMIT signalBusy(false); 0495 0496 if (fail) 0497 { 0498 Q_EMIT signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString()); 0499 } 0500 else 0501 { 0502 Q_EMIT signalCreateFolderSucceeded(); 0503 } 0504 } 0505 0506 } // namespace DigikamGenericDropBoxPlugin 0507 0508 #include "moc_dbtalker.cpp"