File indexing completed on 2024-05-19 05:00:40
0001 /* 0002 * SPDX-FileCopyrightText: 2013-2014 Daniel Vrátil <dvratil@redhat.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 * 0006 */ 0007 0008 #include "kio_gdrive.h" 0009 #include "gdrive_udsentry.h" 0010 #include "gdrivebackend.h" 0011 #include "gdrivedebug.h" 0012 #include "gdrivehelper.h" 0013 #include "gdriveurl.h" 0014 #include "gdriveversion.h" 0015 0016 #include <QApplication> 0017 #include <QMimeDatabase> 0018 #include <QNetworkReply> 0019 #include <QNetworkRequest> 0020 #include <QTemporaryFile> 0021 #include <QUrlQuery> 0022 #include <QUuid> 0023 0024 #include <KGAPI/AuthJob> 0025 #include <KGAPI/Drive/About> 0026 #include <KGAPI/Drive/AboutFetchJob> 0027 #include <KGAPI/Drive/ChildReference> 0028 #include <KGAPI/Drive/ChildReferenceCreateJob> 0029 #include <KGAPI/Drive/ChildReferenceFetchJob> 0030 #include <KGAPI/Drive/Drives> 0031 #include <KGAPI/Drive/DrivesCreateJob> 0032 #include <KGAPI/Drive/DrivesDeleteJob> 0033 #include <KGAPI/Drive/DrivesFetchJob> 0034 #include <KGAPI/Drive/DrivesModifyJob> 0035 #include <KGAPI/Drive/File> 0036 #include <KGAPI/Drive/FileCopyJob> 0037 #include <KGAPI/Drive/FileCreateJob> 0038 #include <KGAPI/Drive/FileFetchContentJob> 0039 #include <KGAPI/Drive/FileFetchJob> 0040 #include <KGAPI/Drive/FileModifyJob> 0041 #include <KGAPI/Drive/FileSearchQuery> 0042 #include <KGAPI/Drive/FileTrashJob> 0043 #include <KGAPI/Drive/ParentReference> 0044 #include <KGAPI/Drive/Permission> 0045 #include <KIO/Job> 0046 #include <KLocalizedString> 0047 0048 using namespace KGAPI2; 0049 using namespace Drive; 0050 0051 class KIOPluginForMetaData : public QObject 0052 { 0053 Q_OBJECT 0054 Q_PLUGIN_METADATA(IID "org.kde.kio.slave.gdrive" FILE "gdrive.json") 0055 }; 0056 0057 extern "C" { 0058 int Q_DECL_EXPORT kdemain(int argc, char **argv) 0059 { 0060 QApplication app(argc, argv); 0061 app.setApplicationName(QStringLiteral("kio_gdrive")); 0062 0063 if (argc != 4) { 0064 fprintf(stderr, "Usage: kio_gdrive protocol domain-socket1 domain-socket2\n"); 0065 exit(-1); 0066 } 0067 0068 KIOGDrive slave(argv[1], argv[2], argv[3]); 0069 slave.dispatchLoop(); 0070 return 0; 0071 } 0072 } 0073 0074 KIOGDrive::KIOGDrive(const QByteArray &protocol, const QByteArray &pool_socket, const QByteArray &app_socket) 0075 : WorkerBase("gdrive", pool_socket, app_socket) 0076 { 0077 Q_UNUSED(protocol); 0078 0079 m_accountManager.reset(new AccountManager); 0080 0081 qCDebug(GDRIVE) << "KIO GDrive ready: version" << GDRIVE_VERSION_STRING; 0082 } 0083 0084 KIOGDrive::~KIOGDrive() 0085 { 0086 closeConnection(); 0087 } 0088 0089 KIOGDrive::Result KIOGDrive::handleError(const KGAPI2::Job &job, const QUrl &url) 0090 { 0091 qCDebug(GDRIVE) << "Completed job" << (&job) << "error code:" << job.error() << "- message:" << job.errorString(); 0092 0093 switch (job.error()) { 0094 case KGAPI2::OK: 0095 case KGAPI2::NoError: 0096 return Result::success(); 0097 case KGAPI2::AuthCancelled: 0098 case KGAPI2::AuthError: 0099 return Result::fail(KIO::ERR_CANNOT_LOGIN, url.toDisplayString()); 0100 case KGAPI2::Unauthorized: { 0101 const AccountPtr oldAccount = job.account(); 0102 const AccountPtr account = m_accountManager->refreshAccount(oldAccount); 0103 if (!account) { 0104 return Result::fail(KIO::ERR_CANNOT_LOGIN, url.toDisplayString()); 0105 } 0106 return Result::restart(); 0107 } 0108 case KGAPI2::Forbidden: 0109 return Result::fail(KIO::ERR_ACCESS_DENIED, url.toDisplayString()); 0110 case KGAPI2::NotFound: 0111 return Result::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString()); 0112 case KGAPI2::NoContent: 0113 return Result::fail(KIO::ERR_NO_CONTENT, url.toDisplayString()); 0114 case KGAPI2::QuotaExceeded: 0115 return Result::fail(KIO::ERR_DISK_FULL, url.toDisplayString()); 0116 default: 0117 return Result::fail(KIO::ERR_WORKER_DEFINED, job.errorString()); 0118 } 0119 0120 return Result::fail(KIO::ERR_WORKER_DEFINED, i18n("Unknown error")); 0121 } 0122 0123 KIO::WorkerResult KIOGDrive::fileSystemFreeSpace(const QUrl &url) 0124 { 0125 const auto gdriveUrl = GDriveUrl(url); 0126 if (gdriveUrl.isNewAccountPath()) { 0127 qCDebug(GDRIVE) << "fileSystemFreeSpace is not supported for new-account url"; 0128 return KIO::WorkerResult::pass(); 0129 } 0130 if (gdriveUrl.isRoot()) { 0131 qCDebug(GDRIVE) << "fileSystemFreeSpace is not supported for gdrive root url"; 0132 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_STAT, url.toDisplayString()); 0133 } 0134 0135 qCDebug(GDRIVE) << "Getting fileSystemFreeSpace for" << url; 0136 const QString accountId = gdriveUrl.account(); 0137 AboutFetchJob aboutFetch(getAccount(accountId)); 0138 aboutFetch.setFields({ 0139 About::Fields::Kind, 0140 About::Fields::QuotaBytesTotal, 0141 About::Fields::QuotaBytesUsedAggregate, 0142 }); 0143 if (auto result = runJob(aboutFetch, url, accountId); result.success()) { 0144 const AboutPtr about = aboutFetch.aboutData(); 0145 if (about) { 0146 setMetaData(QStringLiteral("total"), QString::number(about->quotaBytesTotal())); 0147 setMetaData(QStringLiteral("available"), QString::number(about->quotaBytesTotal() - about->quotaBytesUsedAggregate())); 0148 return KIO::WorkerResult::pass(); 0149 } 0150 } else { 0151 return result; 0152 } 0153 0154 return KIO::WorkerResult::fail(); 0155 } 0156 0157 AccountPtr KIOGDrive::getAccount(const QString &accountName) 0158 { 0159 return m_accountManager->account(accountName); 0160 } 0161 0162 KIO::UDSEntry KIOGDrive::fileToUDSEntry(const FilePtr &origFile, const QString &path) const 0163 { 0164 KIO::UDSEntry entry; 0165 bool isFolder = false; 0166 0167 FilePtr file = origFile; 0168 if (GDriveHelper::isGDocsDocument(file)) { 0169 GDriveHelper::convertFromGDocs(file); 0170 } 0171 0172 entry.fastInsert(KIO::UDSEntry::UDS_NAME, file->title()); 0173 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, file->title()); 0174 entry.fastInsert(KIO::UDSEntry::UDS_COMMENT, file->description()); 0175 0176 if (file->isFolder()) { 0177 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0178 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0179 isFolder = true; 0180 } else { 0181 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); 0182 entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, file->mimeType()); 0183 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, file->fileSize()); 0184 0185 entry.fastInsert(KIO::UDSEntry::UDS_URL, fileToUrl(origFile, path).toString()); 0186 } 0187 0188 entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, file->createdDate().toSecsSinceEpoch()); 0189 entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, file->modifiedDate().toSecsSinceEpoch()); 0190 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, file->lastViewedByMeDate().toSecsSinceEpoch()); 0191 if (!file->ownerNames().isEmpty()) { 0192 entry.fastInsert(KIO::UDSEntry::UDS_USER, file->ownerNames().first()); 0193 } 0194 0195 if (!isFolder) { 0196 if (file->editable()) { 0197 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); 0198 } else { 0199 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); 0200 } 0201 } else { 0202 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); 0203 } 0204 0205 entry.fastInsert(GDriveUDSEntryExtras::Id, file->id()); 0206 entry.fastInsert(GDriveUDSEntryExtras::Url, file->alternateLink().toString()); 0207 entry.fastInsert(GDriveUDSEntryExtras::Version, QString::number(file->version())); 0208 entry.fastInsert(GDriveUDSEntryExtras::Md5, file->md5Checksum()); 0209 entry.fastInsert(GDriveUDSEntryExtras::LastModifyingUser, file->lastModifyingUserName()); 0210 entry.fastInsert(GDriveUDSEntryExtras::Owners, file->ownerNames().join(QStringLiteral(", "))); 0211 if (file->sharedWithMeDate().isValid()) { 0212 entry.fastInsert(GDriveUDSEntryExtras::SharedWithMeDate, QLocale::system().toString(file->sharedWithMeDate(), QLocale::LongFormat)); 0213 } 0214 0215 return entry; 0216 } 0217 0218 QUrl KIOGDrive::fileToUrl(const FilePtr &file, const QString &path) const 0219 { 0220 QUrl url; 0221 url.setScheme(GDriveUrl::Scheme); 0222 url.setPath(path + QLatin1Char('/') + file->title()); 0223 0224 QUrlQuery urlQuery; 0225 urlQuery.addQueryItem(QStringLiteral("id"), file->id()); 0226 url.setQuery(urlQuery); 0227 0228 return url; 0229 } 0230 0231 KIO::WorkerResult KIOGDrive::openConnection() 0232 { 0233 qCDebug(GDRIVE) << "Ready to talk to GDrive"; 0234 return KIO::WorkerResult::pass(); 0235 } 0236 0237 KIO::UDSEntry KIOGDrive::newAccountUDSEntry() 0238 { 0239 KIO::UDSEntry entry; 0240 0241 entry.fastInsert(KIO::UDSEntry::UDS_NAME, GDriveUrl::NewAccountPath); 0242 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, i18nc("login in a new google account", "New account")); 0243 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0244 entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("list-add-user")); 0245 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR); 0246 0247 return entry; 0248 } 0249 0250 KIO::UDSEntry KIOGDrive::sharedWithMeUDSEntry() 0251 { 0252 KIO::UDSEntry entry; 0253 0254 entry.fastInsert(KIO::UDSEntry::UDS_NAME, GDriveUrl::SharedWithMeDir); 0255 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, i18nc("folder containing gdrive files shared with me", "Shared With Me")); 0256 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0257 entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-publicshare")); 0258 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR); 0259 0260 return entry; 0261 } 0262 0263 KIO::UDSEntry KIOGDrive::accountToUDSEntry(const QString &accountNAme) 0264 { 0265 KIO::UDSEntry entry; 0266 0267 entry.fastInsert(KIO::UDSEntry::UDS_NAME, accountNAme); 0268 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, accountNAme); 0269 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0270 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0271 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); 0272 entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-gdrive")); 0273 0274 return entry; 0275 } 0276 0277 KIO::UDSEntry KIOGDrive::sharedDriveToUDSEntry(const DrivesPtr &sharedDrive) 0278 { 0279 KIO::UDSEntry entry; 0280 0281 qlonglong udsAccess = S_IRUSR | S_IXUSR | S_IRGRP; 0282 if (sharedDrive->capabilities()->canRenameDrive() || sharedDrive->capabilities()->canDeleteDrive()) { 0283 udsAccess |= S_IWUSR; 0284 } 0285 0286 entry.fastInsert(KIO::UDSEntry::UDS_NAME, sharedDrive->id()); 0287 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, sharedDrive->name()); 0288 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0289 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0290 entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, sharedDrive->createdDate().toSecsSinceEpoch()); 0291 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, udsAccess); 0292 entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, sharedDrive->hidden()); 0293 entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-gdrive")); 0294 0295 return entry; 0296 } 0297 0298 KIO::WorkerResult KIOGDrive::createAccount() 0299 { 0300 const KGAPI2::AccountPtr account = m_accountManager->createAccount(); 0301 if (!account->accountName().isEmpty()) { 0302 // Redirect to the account we just created. 0303 redirection(QUrl(QStringLiteral("gdrive:/%1").arg(account->accountName()))); 0304 return KIO::WorkerResult::pass(); 0305 } 0306 0307 if (m_accountManager->accounts().isEmpty()) { 0308 return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("There are no Google Drive accounts enabled. Please add at least one.")); 0309 } 0310 0311 // Redirect to the root, we already have some account. 0312 redirection(QUrl(QStringLiteral("gdrive:/"))); 0313 return KIO::WorkerResult::pass(); 0314 } 0315 0316 KIO::WorkerResult KIOGDrive::listAccounts() 0317 { 0318 const auto accounts = m_accountManager->accounts(); 0319 if (accounts.isEmpty()) { 0320 return createAccount(); 0321 } 0322 0323 for (const QString &account : accounts) { 0324 const KIO::UDSEntry entry = accountToUDSEntry(account); 0325 listEntry(entry); 0326 } 0327 0328 KIO::UDSEntry newAccountEntry = newAccountUDSEntry(); 0329 listEntry(newAccountEntry); 0330 0331 // Create also non-writable UDSentry for "." 0332 KIO::UDSEntry entry; 0333 entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); 0334 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0335 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0336 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 0337 listEntry(entry); 0338 0339 return KIO::WorkerResult::pass(); 0340 } 0341 0342 KIO::WorkerResult KIOGDrive::listSharedDrivesRoot(const QUrl &url) 0343 { 0344 const auto gdriveUrl = GDriveUrl(url); 0345 const QString accountId = gdriveUrl.account(); 0346 DrivesFetchJob sharedDrivesFetchJob(getAccount(accountId)); 0347 sharedDrivesFetchJob.setFields({ 0348 Drives::Fields::Kind, 0349 Drives::Fields::Id, 0350 Drives::Fields::Name, 0351 Drives::Fields::Hidden, 0352 Drives::Fields::CreatedDate, 0353 Drives::Fields::Capabilities, 0354 }); 0355 0356 if (auto result = runJob(sharedDrivesFetchJob, url, accountId); result.success()) { 0357 const auto objects = sharedDrivesFetchJob.items(); 0358 for (const auto &object : objects) { 0359 const DrivesPtr sharedDrive = object.dynamicCast<Drives>(); 0360 const KIO::UDSEntry entry = sharedDriveToUDSEntry(sharedDrive); 0361 listEntry(entry); 0362 } 0363 0364 auto entry = fetchSharedDrivesRootEntry(accountId, FetchEntryFlags::CurrentDir); 0365 listEntry(entry); 0366 0367 return KIO::WorkerResult::pass(); 0368 } else { 0369 return result; 0370 } 0371 0372 return KIO::WorkerResult::fail(); 0373 } 0374 0375 KIO::WorkerResult KIOGDrive::createSharedDrive(const QUrl &url) 0376 { 0377 const auto gdriveUrl = GDriveUrl(url); 0378 const QString accountId = gdriveUrl.account(); 0379 0380 DrivesPtr sharedDrive = DrivesPtr::create(); 0381 sharedDrive->setName(gdriveUrl.filename()); 0382 0383 const QString requestId = QUuid::createUuid().toString(); 0384 DrivesCreateJob createJob(requestId, sharedDrive, getAccount(accountId)); 0385 return runJob(createJob, url, accountId); 0386 } 0387 0388 KIO::WorkerResult KIOGDrive::deleteSharedDrive(const QUrl &url) 0389 { 0390 const auto gdriveUrl = GDriveUrl(url); 0391 const QString accountId = gdriveUrl.account(); 0392 DrivesDeleteJob sharedDriveDeleteJob(gdriveUrl.filename(), getAccount(accountId)); 0393 return runJob(sharedDriveDeleteJob, url, accountId); 0394 } 0395 0396 KIO::WorkerResult KIOGDrive::statSharedDrive(const QUrl &url) 0397 { 0398 const auto gdriveUrl = GDriveUrl(url); 0399 const QString accountId = gdriveUrl.account(); 0400 0401 const auto sharedDriveId = resolveSharedDriveId(gdriveUrl.filename(), accountId); 0402 if (sharedDriveId.isEmpty()) { 0403 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0404 } 0405 0406 DrivesFetchJob sharedDriveFetchJob(sharedDriveId, getAccount(accountId)); 0407 sharedDriveFetchJob.setFields({ 0408 Drives::Fields::Kind, 0409 Drives::Fields::Id, 0410 Drives::Fields::Name, 0411 Drives::Fields::Hidden, 0412 Drives::Fields::CreatedDate, 0413 Drives::Fields::Capabilities, 0414 }); 0415 if (auto result = runJob(sharedDriveFetchJob, url, accountId); !result.success()) { 0416 return result; 0417 } 0418 0419 ObjectPtr object = sharedDriveFetchJob.items().at(0); 0420 const DrivesPtr sharedDrive = object.dynamicCast<Drives>(); 0421 const auto entry = sharedDriveToUDSEntry(sharedDrive); 0422 statEntry(entry); 0423 return KIO::WorkerResult::pass(); 0424 } 0425 0426 KIO::UDSEntry KIOGDrive::fetchSharedDrivesRootEntry(const QString &accountId, FetchEntryFlags flags) 0427 { 0428 // Not every user is allowed to create shared Drives, 0429 // check with About resource. 0430 bool canCreateDrives = false; 0431 AboutFetchJob aboutFetch(getAccount(accountId)); 0432 aboutFetch.setFields({ 0433 About::Fields::Kind, 0434 About::Fields::CanCreateDrives, 0435 }); 0436 QEventLoop eventLoop; 0437 QObject::connect(&aboutFetch, &KGAPI2::Job::finished, &eventLoop, &QEventLoop::quit); 0438 eventLoop.exec(); 0439 if (aboutFetch.error() == KGAPI2::OK || aboutFetch.error() == KGAPI2::NoError) { 0440 const AboutPtr about = aboutFetch.aboutData(); 0441 if (about) { 0442 canCreateDrives = about->canCreateDrives(); 0443 } 0444 } 0445 qCDebug(GDRIVE) << "Account" << accountId << (canCreateDrives ? "can" : "can't") << "create Shared Drives"; 0446 0447 KIO::UDSEntry entry; 0448 0449 if (flags == FetchEntryFlags::CurrentDir) { 0450 entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); 0451 } else { 0452 entry.fastInsert(KIO::UDSEntry::UDS_NAME, GDriveUrl::SharedDrivesDir); 0453 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, i18n("Shared Drives")); 0454 } 0455 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0456 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0457 entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-gdrive")); 0458 0459 qlonglong udsAccess = S_IRUSR | S_IXUSR; 0460 // If user is allowed to create shared Drives, add write bit on directory 0461 if (canCreateDrives) { 0462 udsAccess |= S_IWUSR; 0463 } 0464 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, udsAccess); 0465 0466 return entry; 0467 } 0468 0469 class RecursionDepthCounter 0470 { 0471 public: 0472 RecursionDepthCounter() 0473 { 0474 ++sDepth; 0475 } 0476 ~RecursionDepthCounter() 0477 { 0478 --sDepth; 0479 } 0480 0481 RecursionDepthCounter(const RecursionDepthCounter &) = delete; 0482 RecursionDepthCounter &operator=(const RecursionDepthCounter &) = delete; 0483 0484 int depth() const 0485 { 0486 return sDepth; 0487 } 0488 0489 private: 0490 static int sDepth; 0491 }; 0492 0493 int RecursionDepthCounter::sDepth = 0; 0494 0495 std::pair<KIO::WorkerResult, QString> KIOGDrive::resolveFileIdFromPath(const QString &path, PathFlags flags) 0496 { 0497 qCDebug(GDRIVE) << "Resolving file ID for" << path; 0498 0499 if (path.isEmpty()) { 0500 return {KIO::WorkerResult::pass(), QString()}; 0501 } 0502 0503 const QString fileId = m_cache.idForPath(path); 0504 if (!fileId.isEmpty()) { 0505 qCDebug(GDRIVE) << "Resolved" << path << "to" << fileId << "(from cache)"; 0506 return {KIO::WorkerResult::pass(), fileId}; 0507 } 0508 0509 QUrl url; 0510 url.setScheme(GDriveUrl::Scheme); 0511 url.setPath(path); 0512 const auto gdriveUrl = GDriveUrl(url); 0513 Q_ASSERT(!gdriveUrl.isRoot()); 0514 0515 if (gdriveUrl.isAccountRoot() || gdriveUrl.isTrashDir() || gdriveUrl.isSharedWithMeRoot()) { 0516 qCDebug(GDRIVE) << "Resolved" << path << "to account root"; 0517 return rootFolderId(gdriveUrl.account()); 0518 } 0519 0520 if (gdriveUrl.isSharedDrive()) { 0521 // The gdriveUrl.filename() could be the Shared Drive id or 0522 // the name depending on whether we are navigating from a parent 0523 // or accessing the url directly, use the shared drive specific 0524 // solver to disambiguate 0525 return {KIO::WorkerResult::pass(), resolveSharedDriveId(gdriveUrl.filename(), gdriveUrl.account())}; 0526 } 0527 0528 if (gdriveUrl.isSharedDrivesRoot()) { 0529 qCDebug(GDRIVE) << "Resolved" << path << "to Shared Drives root"; 0530 return {KIO::WorkerResult::pass(), QString()}; 0531 } 0532 0533 QString parentId; 0534 if (!gdriveUrl.isSharedWithMeTopLevel()) { 0535 // Try to recursively resolve ID of parent path - either from cache, or by querying Google 0536 auto [result, id] = resolveFileIdFromPath(gdriveUrl.parentPath(), KIOGDrive::PathIsFolder); 0537 0538 if (!result.success()) { 0539 return {result, QString()}; 0540 } 0541 id = parentId; 0542 0543 if (parentId.isEmpty()) { 0544 // We failed to resolve parent -> error 0545 return {KIO::WorkerResult::pass(), QString()}; 0546 } 0547 qCDebug(GDRIVE) << "Getting ID for" << gdriveUrl.filename() << "in parent with ID" << parentId; 0548 } else { 0549 qCDebug(GDRIVE) << "Getting ID for" << gdriveUrl.filename() << "(top-level shared-with-me file without a parentId)"; 0550 } 0551 0552 FileSearchQuery query; 0553 if (flags != KIOGDrive::None) { 0554 query.addQuery(FileSearchQuery::MimeType, 0555 (flags & KIOGDrive::PathIsFolder ? FileSearchQuery::Equals : FileSearchQuery::NotEquals), 0556 GDriveHelper::folderMimeType()); 0557 } 0558 query.addQuery(FileSearchQuery::Title, FileSearchQuery::Equals, gdriveUrl.filename()); 0559 if (!parentId.isEmpty()) { 0560 query.addQuery(FileSearchQuery::Parents, FileSearchQuery::In, parentId); 0561 } 0562 query.addQuery(FileSearchQuery::Trashed, FileSearchQuery::Equals, gdriveUrl.isTrashed()); 0563 0564 const QString accountId = gdriveUrl.account(); 0565 FileFetchJob fetchJob(query, getAccount(accountId)); 0566 fetchJob.setFields({File::Fields::Id, File::Fields::Title, File::Fields::Labels}); 0567 if (auto result = runJob(fetchJob, url, accountId); !result.success()) { 0568 return {result, QString()}; 0569 } 0570 0571 const ObjectsList objects = fetchJob.items(); 0572 if (objects.isEmpty()) { 0573 qCWarning(GDRIVE) << "Failed to resolve" << path; 0574 return {KIO::WorkerResult::pass(), QString()}; 0575 } 0576 0577 const FilePtr file = objects[0].dynamicCast<File>(); 0578 0579 m_cache.insertPath(path, file->id()); 0580 0581 qCDebug(GDRIVE) << "Resolved" << path << "to" << file->id() << "(from network)"; 0582 return {KIO::WorkerResult::pass(), file->id()}; 0583 } 0584 0585 QString KIOGDrive::resolveSharedDriveId(const QString &idOrName, const QString &accountId) 0586 { 0587 qCDebug(GDRIVE) << "Resolving shared drive id for" << idOrName; 0588 0589 const auto idOrNamePath = GDriveUrl::buildSharedDrivePath(accountId, idOrName); 0590 QString fileId = m_cache.idForPath(idOrNamePath); 0591 if (!fileId.isEmpty()) { 0592 qCDebug(GDRIVE) << "Resolved shared drive id" << idOrName << "to" << fileId << "(from cache)"; 0593 return fileId; 0594 } 0595 0596 // We start by trying to fetch a shared drive with the filename as id 0597 DrivesFetchJob searchByIdJob(idOrName, getAccount(accountId)); 0598 searchByIdJob.setFields({ 0599 Drives::Fields::Kind, 0600 Drives::Fields::Id, 0601 Drives::Fields::Name, 0602 }); 0603 QEventLoop eventLoop; 0604 QObject::connect(&searchByIdJob, &KGAPI2::Job::finished, &eventLoop, &QEventLoop::quit); 0605 eventLoop.exec(); 0606 if (searchByIdJob.error() == KGAPI2::OK || searchByIdJob.error() == KGAPI2::NoError) { 0607 // A Shared Drive with that id exists so we return it 0608 const auto objects = searchByIdJob.items(); 0609 const DrivesPtr sharedDrive = objects.at(0).dynamicCast<Drives>(); 0610 fileId = sharedDrive->id(); 0611 qCDebug(GDRIVE) << "Resolved shared drive id" << idOrName << "to" << fileId; 0612 0613 const auto idPath = idOrNamePath; 0614 const auto namePath = GDriveUrl::buildSharedDrivePath(accountId, sharedDrive->name()); 0615 m_cache.insertPath(idPath, fileId); 0616 m_cache.insertPath(namePath, fileId); 0617 0618 return fileId; 0619 } 0620 0621 // The gdriveUrl's filename is not a shared drive id, we must 0622 // search for a shared drive with the filename name. 0623 // Unfortunately searching by name is only allowed for admin 0624 // accounts (i.e. useDomainAdminAccess=true) so we retrieve all 0625 // shared drives and search by name here 0626 DrivesFetchJob sharedDrivesFetchJob(getAccount(accountId)); 0627 sharedDrivesFetchJob.setFields({ 0628 Drives::Fields::Kind, 0629 Drives::Fields::Id, 0630 Drives::Fields::Name, 0631 }); 0632 QObject::connect(&sharedDrivesFetchJob, &KGAPI2::Job::finished, &eventLoop, &QEventLoop::quit); 0633 eventLoop.exec(); 0634 if (sharedDrivesFetchJob.error() == KGAPI2::OK || sharedDrivesFetchJob.error() == KGAPI2::NoError) { 0635 const auto objects = sharedDrivesFetchJob.items(); 0636 for (const auto &object : objects) { 0637 const DrivesPtr sharedDrive = object.dynamicCast<Drives>(); 0638 0639 // If we have one or more hits we will take the first as good because we 0640 // don't have any other measures for picking the correct drive 0641 if (sharedDrive->name() == idOrName) { 0642 fileId = sharedDrive->id(); 0643 qCDebug(GDRIVE) << "Resolved shared drive id" << idOrName << "to" << fileId; 0644 0645 const auto idPath = GDriveUrl::buildSharedDrivePath(accountId, fileId); 0646 const auto namePath = idOrNamePath; 0647 m_cache.insertPath(idPath, fileId); 0648 m_cache.insertPath(namePath, fileId); 0649 0650 return fileId; 0651 } 0652 } 0653 } 0654 0655 // We couldn't find any shared drive with that id or name 0656 qCDebug(GDRIVE) << "Failed resolving shared drive" << idOrName << "(couldn't find drive with that id or name)"; 0657 return QString(); 0658 } 0659 0660 std::pair<KIO::WorkerResult, QString> KIOGDrive::rootFolderId(const QString &accountId) 0661 { 0662 auto it = m_rootIds.constFind(accountId); 0663 if (it == m_rootIds.cend()) { 0664 qCDebug(GDRIVE) << "Getting root ID for" << accountId; 0665 AboutFetchJob aboutFetch(getAccount(accountId)); 0666 aboutFetch.setFields({About::Fields::Kind, About::Fields::RootFolderId}); 0667 QUrl url; 0668 if (auto result = runJob(aboutFetch, url, accountId); !result.success()) { 0669 return {result, QString()}; 0670 } 0671 0672 const AboutPtr about = aboutFetch.aboutData(); 0673 if (!about || about->rootFolderId().isEmpty()) { 0674 qCWarning(GDRIVE) << "Failed to obtain root ID"; 0675 return {KIO::WorkerResult::pass(), QString()}; 0676 } 0677 0678 auto v = m_rootIds.insert(accountId, about->rootFolderId()); 0679 return {KIO::WorkerResult::pass(), *v}; 0680 } 0681 0682 return {KIO::WorkerResult::pass(), *it}; 0683 } 0684 0685 KIO::WorkerResult KIOGDrive::listDir(const QUrl &url) 0686 { 0687 qCDebug(GDRIVE) << "Going to list" << url; 0688 0689 const auto gdriveUrl = GDriveUrl(url); 0690 0691 if (gdriveUrl.isRoot()) { 0692 return listAccounts(); 0693 } 0694 if (gdriveUrl.isNewAccountPath()) { 0695 return createAccount(); 0696 } 0697 0698 // We are committed to listing an url that belongs to 0699 // an account (i.e. not root or new account path), lets 0700 // make sure we know about the account 0701 const QString accountId = gdriveUrl.account(); 0702 const auto account = getAccount(accountId); 0703 if (account->accountName().isEmpty()) { 0704 qCDebug(GDRIVE) << "Unknown account" << accountId << "for" << url; 0705 return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("%1 isn't a known GDrive account", accountId)); 0706 } 0707 0708 QString folderId; 0709 if (gdriveUrl.isAccountRoot()) { 0710 auto entry = fetchSharedDrivesRootEntry(accountId); 0711 listEntry(entry); 0712 auto [result, id] = rootFolderId(accountId); 0713 0714 if (!result.success()) { 0715 return result; 0716 } 0717 folderId = id; 0718 0719 auto sharedWithMeEntry = sharedWithMeUDSEntry(); 0720 listEntry(sharedWithMeEntry); 0721 } else if (gdriveUrl.isSharedDrivesRoot()) { 0722 return listSharedDrivesRoot(url); 0723 } else { 0724 folderId = m_cache.idForPath(url.path()); 0725 if (folderId.isEmpty()) { 0726 auto [result, id] = resolveFileIdFromPath(url.adjusted(QUrl::StripTrailingSlash).path(), KIOGDrive::PathIsFolder); 0727 0728 if (!result.success()) { 0729 return result; 0730 } 0731 folderId = id; 0732 } 0733 if (folderId.isEmpty()) { 0734 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0735 } 0736 } 0737 0738 FileSearchQuery query; 0739 query.addQuery(FileSearchQuery::Trashed, FileSearchQuery::Equals, false); 0740 // Top-level Shared-with-me files don't have a parent. 0741 if (gdriveUrl.isSharedWithMeRoot()) { 0742 query.addQuery(FileSearchQuery::SharedWithMe, FileSearchQuery::Equals, true); 0743 } else { 0744 query.addQuery(FileSearchQuery::Parents, FileSearchQuery::In, folderId); 0745 } 0746 FileFetchJob fileFetchJob(query, getAccount(accountId)); 0747 const auto extraFields = QStringList({ 0748 KGAPI2::Drive::File::Fields::Labels, 0749 KGAPI2::Drive::File::Fields::ExportLinks, 0750 KGAPI2::Drive::File::Fields::LastViewedByMeDate, 0751 KGAPI2::Drive::File::Fields::AlternateLink, 0752 }); 0753 fileFetchJob.setFields(KGAPI2::Drive::FileFetchJob::FieldShorthands::BasicFields + extraFields); 0754 if (auto result = runJob(fileFetchJob, url, accountId); !result.success()) { 0755 return result; 0756 } 0757 0758 const ObjectsList objects = fileFetchJob.items(); 0759 for (const ObjectPtr &object : objects) { 0760 const FilePtr file = object.dynamicCast<File>(); 0761 0762 const KIO::UDSEntry entry = fileToUDSEntry(file, url.adjusted(QUrl::StripTrailingSlash).path()); 0763 listEntry(entry); 0764 0765 const QString path = url.path().endsWith(QLatin1Char('/')) ? url.path() : url.path() + QLatin1Char('/'); 0766 m_cache.insertPath(path + file->title(), file->id()); 0767 } 0768 0769 // We also need a non-null and writable UDSentry for "." 0770 KIO::UDSEntry entry; 0771 entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); 0772 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0773 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); 0774 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); 0775 listEntry(entry); 0776 0777 return KIO::WorkerResult::pass(); 0778 } 0779 0780 KIO::WorkerResult KIOGDrive::mkdir(const QUrl &url, int permissions) 0781 { 0782 // NOTE: We deliberately ignore the permissions field here, because GDrive 0783 // does not recognize any privileges that could be mapped to standard UNIX 0784 // file permissions. 0785 Q_UNUSED(permissions); 0786 0787 qCDebug(GDRIVE) << "Creating directory" << url; 0788 0789 const auto gdriveUrl = GDriveUrl(url); 0790 const QString accountId = gdriveUrl.account(); 0791 // At least account and new folder name 0792 if (gdriveUrl.isRoot() || gdriveUrl.isAccountRoot()) { 0793 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0794 } 0795 0796 if (gdriveUrl.isSharedDrive()) { 0797 qCDebug(GDRIVE) << "Directory is shared drive, creating that instead" << url; 0798 return createSharedDrive(url); 0799 } 0800 0801 QString parentId; 0802 if (gdriveUrl.isTopLevel()) { 0803 auto [result, id] = rootFolderId(accountId); 0804 if (!result.success()) { 0805 return result; 0806 } 0807 parentId = id; 0808 0809 } else { 0810 auto [result, id] = resolveFileIdFromPath(gdriveUrl.parentPath(), KIOGDrive::PathIsFolder); 0811 if (!result.success()) { 0812 return result; 0813 } 0814 parentId = id; 0815 } 0816 0817 if (parentId.isEmpty()) { 0818 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0819 } 0820 0821 FilePtr file(new File()); 0822 file->setTitle(gdriveUrl.filename()); 0823 file->setMimeType(File::folderMimeType()); 0824 0825 ParentReferencePtr parent(new ParentReference(parentId)); 0826 file->setParents(ParentReferencesList() << parent); 0827 0828 FileCreateJob createJob(file, getAccount(accountId)); 0829 return runJob(createJob, url, accountId); 0830 } 0831 0832 KIO::WorkerResult KIOGDrive::stat(const QUrl &url) 0833 { 0834 // TODO We should be using StatDetails to limit how we respond to a stat request 0835 // const QString statDetails = metaData(QStringLiteral("statDetails")); 0836 // KIO::StatDetails details = statDetails.isEmpty() ? KIO::StatDefaultDetails : static_cast<KIO::StatDetails>(statDetails.toInt()); 0837 // qCDebug(GDRIVE) << "Going to stat()" << url << "for details" << details; 0838 0839 const auto gdriveUrl = GDriveUrl(url); 0840 if (gdriveUrl.isRoot()) { 0841 // TODO Can we stat() root? 0842 return KIO::WorkerResult::pass(); 0843 } 0844 if (gdriveUrl.isNewAccountPath()) { 0845 qCDebug(GDRIVE) << "stat()ing new-account path"; 0846 const KIO::UDSEntry entry = newAccountUDSEntry(); 0847 statEntry(entry); 0848 return KIO::WorkerResult::pass(); 0849 } 0850 0851 if (gdriveUrl.isSharedWithMeRoot()) { 0852 qCDebug(GDRIVE) << "stat()ing Shared With Me path"; 0853 const KIO::UDSEntry entry = sharedWithMeUDSEntry(); 0854 statEntry(entry); 0855 return KIO::WorkerResult::pass(); 0856 } 0857 0858 // We are committed to stat()ing an url that belongs to 0859 // an account (i.e. not root or new account path), lets 0860 // make sure we know about the account 0861 const QString accountId = gdriveUrl.account(); 0862 const auto account = getAccount(accountId); 0863 if (account->accountName().isEmpty()) { 0864 qCDebug(GDRIVE) << "Unknown account" << accountId << "for" << url; 0865 return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("%1 isn't a known GDrive account", accountId)); 0866 } 0867 0868 if (gdriveUrl.isAccountRoot()) { 0869 qCDebug(GDRIVE) << "stat()ing account root"; 0870 const KIO::UDSEntry entry = accountToUDSEntry(accountId); 0871 statEntry(entry); 0872 return KIO::WorkerResult::pass(); 0873 } 0874 if (gdriveUrl.isSharedDrivesRoot()) { 0875 qCDebug(GDRIVE) << "stat()ing Shared Drives root"; 0876 const auto entry = fetchSharedDrivesRootEntry(accountId); 0877 statEntry(entry); 0878 return KIO::WorkerResult::pass(); 0879 } 0880 if (gdriveUrl.isSharedDrive()) { 0881 qCDebug(GDRIVE) << "stat()ing Shared Drive" << url; 0882 return statSharedDrive(url); 0883 } 0884 0885 const QUrlQuery urlQuery(url); 0886 QString fileId; 0887 0888 if (urlQuery.hasQueryItem(QStringLiteral("id"))) { 0889 fileId = urlQuery.queryItemValue(QStringLiteral("id")); 0890 } else { 0891 auto [result, id] = resolveFileIdFromPath(url.adjusted(QUrl::StripTrailingSlash).path(), KIOGDrive::None); 0892 0893 if (!result.success()) { 0894 return result; 0895 } 0896 fileId = id; 0897 } 0898 0899 if (fileId.isEmpty()) { 0900 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0901 } 0902 0903 FileFetchJob fileFetchJob(fileId, getAccount(accountId)); 0904 if (auto result = runJob(fileFetchJob, url, accountId); !result.success()) { 0905 qCDebug(GDRIVE) << "Failed stat()ing file" << fileFetchJob.errorString(); 0906 return result; 0907 } 0908 0909 const ObjectsList objects = fileFetchJob.items(); 0910 if (objects.count() != 1) { 0911 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0912 } 0913 0914 const FilePtr file = objects.first().dynamicCast<File>(); 0915 if (file->labels()->trashed()) { 0916 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0917 } 0918 0919 const KIO::UDSEntry entry = fileToUDSEntry(file, gdriveUrl.parentPath()); 0920 0921 statEntry(entry); 0922 return KIO::WorkerResult::pass(); 0923 } 0924 0925 KIO::WorkerResult KIOGDrive::get(const QUrl &url) 0926 { 0927 qCDebug(GDRIVE) << "Fetching content of" << url; 0928 0929 const auto gdriveUrl = GDriveUrl(url); 0930 const QString accountId = gdriveUrl.account(); 0931 0932 if (gdriveUrl.isRoot()) { 0933 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0934 } 0935 if (gdriveUrl.isAccountRoot()) { 0936 // You cannot GET an account folder! 0937 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, url.path()); 0938 } 0939 0940 const QUrlQuery urlQuery(url); 0941 QString fileId; 0942 0943 if (urlQuery.hasQueryItem(QStringLiteral("id"))) { 0944 fileId = urlQuery.queryItemValue(QStringLiteral("id")); 0945 } else { 0946 auto [result, id] = resolveFileIdFromPath(url.adjusted(QUrl::StripTrailingSlash).path(), KIOGDrive::PathIsFile); 0947 0948 if (!result.success()) { 0949 return result; 0950 } 0951 fileId = id; 0952 } 0953 if (fileId.isEmpty()) { 0954 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0955 } 0956 0957 FileFetchJob fileFetchJob(fileId, getAccount(accountId)); 0958 fileFetchJob.setFields({File::Fields::Id, File::Fields::MimeType, File::Fields::ExportLinks, File::Fields::DownloadUrl}); 0959 if (auto result = runJob(fileFetchJob, url, accountId); !result.success()) { 0960 return result; 0961 } 0962 0963 const ObjectsList objects = fileFetchJob.items(); 0964 if (objects.count() != 1) { 0965 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.fileName()); 0966 } 0967 0968 FilePtr file = objects.first().dynamicCast<File>(); 0969 QUrl downloadUrl; 0970 if (GDriveHelper::isGDocsDocument(file)) { 0971 downloadUrl = GDriveHelper::convertFromGDocs(file); 0972 } else { 0973 downloadUrl = GDriveHelper::downloadUrl(file); 0974 } 0975 0976 mimeType(file->mimeType()); 0977 0978 FileFetchContentJob contentJob(downloadUrl, getAccount(accountId)); 0979 QObject::connect(&contentJob, &KGAPI2::Job::progress, [this](KGAPI2::Job *, int processed, int total) { 0980 processedSize(processed); 0981 totalSize(total); 0982 }); 0983 if (auto result = runJob(contentJob, url, accountId); !result.success()) { 0984 return result; 0985 } 0986 0987 QByteArray contentData = contentJob.data(); 0988 0989 processedSize(contentData.size()); 0990 totalSize(contentData.size()); 0991 0992 // data() has a maximum transfer size of 14 MiB so we need to send it in chunks. 0993 // See TransferJob::slotDataReq. 0994 int transferred = 0; 0995 // do-while loop to call data() even for empty files. 0996 do { 0997 const size_t nextChunk = qMin(contentData.size() - transferred, 14 * 1024 * 1024); 0998 data(QByteArray::fromRawData(contentData.constData() + transferred, nextChunk)); 0999 transferred += nextChunk; 1000 } while (transferred < contentData.size()); 1001 return KIO::WorkerResult::pass(); 1002 } 1003 1004 KIO::WorkerResult KIOGDrive::readPutData(QTemporaryFile &tempFile, FilePtr &fileMetaData) 1005 { 1006 // TODO: Instead of using a temp file, upload directly the raw data (requires 1007 // support in LibKGAPI) 1008 1009 // TODO: For large files, switch to resumable upload and upload the file in 1010 // reasonably large chunks (requires support in LibKGAPI) 1011 1012 // TODO: Support resumable upload (requires support in LibKGAPI) 1013 1014 if (!tempFile.open()) { 1015 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_WRITE, tempFile.fileName()); 1016 } 1017 1018 int result; 1019 do { 1020 QByteArray buffer; 1021 dataReq(); 1022 result = readData(buffer); 1023 if (!buffer.isEmpty()) { 1024 qint64 size = tempFile.write(buffer); 1025 if (size != buffer.size()) { 1026 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_WRITE, tempFile.fileName()); 1027 } 1028 } 1029 } while (result > 0); 1030 1031 const QMimeType mime = QMimeDatabase().mimeTypeForFileNameAndData(fileMetaData->title(), &tempFile); 1032 fileMetaData->setMimeType(mime.name()); 1033 1034 tempFile.close(); 1035 1036 if (result == -1) { 1037 qCWarning(GDRIVE) << "Could not read source file" << tempFile.fileName(); 1038 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_READ, QString()); 1039 } 1040 1041 return KIO::WorkerResult::pass(); 1042 } 1043 1044 KIO::WorkerResult KIOGDrive::runJob(KGAPI2::Job &job, const QUrl &url, const QString &accountId) 1045 { 1046 auto account = getAccount(accountId); 1047 if (account->accessToken().isEmpty()) { 1048 qCWarning(GDRIVE) << "Expired or missing access/refresh token for account" << accountId; 1049 return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("Expired or missing access tokens for account %1", accountId)); 1050 } 1051 1052 Q_FOREVER { 1053 qCDebug(GDRIVE) << "Running job" << (&job) << "with accessToken" << GDriveHelper::elideToken(job.account()->accessToken()); 1054 QEventLoop eventLoop; 1055 QObject::connect(&job, &KGAPI2::Job::finished, &eventLoop, &QEventLoop::quit); 1056 eventLoop.exec(); 1057 Result result = handleError(job, url); 1058 if (result.action == KIOGDrive::Success) { 1059 break; 1060 } else if (result.action == KIOGDrive::Fail) { 1061 return KIO::WorkerResult::fail(result.error, result.errorString); 1062 } 1063 job.setAccount(account); 1064 job.restart(); 1065 }; 1066 1067 return KIO::WorkerResult::pass(); 1068 } 1069 1070 KIO::WorkerResult KIOGDrive::putUpdate(const QUrl &url) 1071 { 1072 const QString fileId = QUrlQuery(url).queryItemValue(QStringLiteral("id")); 1073 qCDebug(GDRIVE) << Q_FUNC_INFO << url << fileId; 1074 1075 const auto gdriveUrl = GDriveUrl(url); 1076 const auto accountId = gdriveUrl.account(); 1077 1078 FileFetchJob fetchJob(fileId, getAccount(accountId)); 1079 if (auto result = runJob(fetchJob, url, accountId); !result.success()) { 1080 return result; 1081 } 1082 1083 const ObjectsList objects = fetchJob.items(); 1084 if (objects.size() != 1) { 1085 return putCreate(url); 1086 } 1087 1088 FilePtr file = objects[0].dynamicCast<File>(); 1089 1090 QTemporaryFile tmpFile; 1091 if (auto result = readPutData(tmpFile, file); !result.success()) { 1092 return result; 1093 } 1094 1095 FileModifyJob modifyJob(tmpFile.fileName(), file, getAccount(accountId)); 1096 modifyJob.setUpdateModifiedDate(true); 1097 if (auto result = runJob(modifyJob, url, accountId); !result.success()) { 1098 return result; 1099 } 1100 1101 return KIO::WorkerResult::pass(); 1102 } 1103 1104 KIO::WorkerResult KIOGDrive::putCreate(const QUrl &url) 1105 { 1106 qCDebug(GDRIVE) << Q_FUNC_INFO << url; 1107 ParentReferencesList parentReferences; 1108 1109 const auto gdriveUrl = GDriveUrl(url); 1110 if (gdriveUrl.isRoot() || gdriveUrl.isAccountRoot()) { 1111 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, url.path()); 1112 } 1113 1114 if (!gdriveUrl.isTopLevel()) { 1115 // Not creating in root directory, fill parent references 1116 QString parentId; 1117 1118 auto [result, id] = resolveFileIdFromPath(gdriveUrl.parentPath()); 1119 1120 if (!result.success()) { 1121 return result; 1122 } 1123 parentId = id; 1124 1125 if (parentId.isEmpty()) { 1126 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); 1127 } 1128 parentReferences << ParentReferencePtr(new ParentReference(parentId)); 1129 } 1130 1131 FilePtr file(new File); 1132 file->setTitle(gdriveUrl.filename()); 1133 file->setParents(parentReferences); 1134 /* 1135 if (hasMetaData(QLatin1String("modified"))) { 1136 const QString modified = metaData(QLatin1String("modified")); 1137 qCDebug(GDRIVE) << modified; 1138 file->setModifiedDate(KDateTime::fromString(modified, KDateTime::ISODate)); 1139 } 1140 */ 1141 1142 QTemporaryFile tmpFile; 1143 if (auto result = readPutData(tmpFile, file); !result.success()) { 1144 return result; 1145 } 1146 1147 const auto accountId = gdriveUrl.account(); 1148 FileCreateJob createJob(tmpFile.fileName(), file, getAccount(accountId)); 1149 if (auto result = runJob(createJob, url, accountId); !result.success()) { 1150 return result; 1151 } 1152 1153 return KIO::WorkerResult::pass(); 1154 } 1155 1156 KIO::WorkerResult KIOGDrive::put(const QUrl &url, int permissions, KIO::JobFlags flags) 1157 { 1158 // NOTE: We deliberately ignore the permissions field here, because GDrive 1159 // does not recognize any privileges that could be mapped to standard UNIX 1160 // file permissions. 1161 Q_UNUSED(permissions) 1162 Q_UNUSED(flags) 1163 1164 qCDebug(GDRIVE) << Q_FUNC_INFO << url; 1165 1166 const auto gdriveUrl = GDriveUrl(url); 1167 1168 if (gdriveUrl.isSharedDrive()) { 1169 qCDebug(GDRIVE) << "Can't create files in Shared Drives root" << url; 1170 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_WRITE, url.path()); 1171 } 1172 1173 if (QUrlQuery(url).hasQueryItem(QStringLiteral("id"))) { 1174 if (auto result = putUpdate(url); !result.success()) { 1175 return result; 1176 } 1177 } else { 1178 if (auto result = putCreate(url); !result.success()) { 1179 return result; 1180 } 1181 } 1182 1183 // FIXME: Update the cache now! 1184 1185 return KIO::WorkerResult::pass(); 1186 } 1187 1188 KIO::WorkerResult KIOGDrive::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags) 1189 { 1190 qCDebug(GDRIVE) << "Going to copy" << src << "to" << dest; 1191 1192 // NOTE: We deliberately ignore the permissions field here, because GDrive 1193 // does not recognize any privileges that could be mapped to standard UNIX 1194 // file permissions. 1195 Q_UNUSED(permissions); 1196 1197 // NOTE: We deliberately ignore the flags field here, because the "overwrite" 1198 // flag would have no effect on GDrive, since file name don't have to be 1199 // unique. IOW if there is a file "foo.bar" and user copy-pastes into the 1200 // same directory, the FileCopyJob will succeed and a new file with the same 1201 // name will be created. 1202 Q_UNUSED(flags); 1203 1204 const auto srcGDriveUrl = GDriveUrl(src); 1205 const auto destGDriveUrl = GDriveUrl(dest); 1206 const QString sourceAccountId = srcGDriveUrl.account(); 1207 const QString destAccountId = destGDriveUrl.account(); 1208 1209 // TODO: Does this actually happen, or does KIO treat our account name as host? 1210 if (sourceAccountId != destAccountId) { 1211 // KIO will fallback to get+post 1212 return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, src.path()); 1213 } 1214 1215 if (srcGDriveUrl.isRoot()) { 1216 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1217 } 1218 if (srcGDriveUrl.isAccountRoot()) { 1219 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, src.path()); 1220 } 1221 1222 const QUrlQuery urlQuery(src); 1223 QString sourceFileId; 1224 1225 if (urlQuery.hasQueryItem(QStringLiteral("id"))) { 1226 sourceFileId = urlQuery.queryItemValue(QStringLiteral("id")); 1227 } else { 1228 auto [result, id] = resolveFileIdFromPath(src.adjusted(QUrl::StripTrailingSlash).path()); 1229 1230 if (!result.success()) { 1231 return result; 1232 } 1233 sourceFileId = id; 1234 } 1235 1236 if (sourceFileId.isEmpty()) { 1237 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1238 } 1239 FileFetchJob sourceFileFetchJob(sourceFileId, getAccount(sourceAccountId)); 1240 sourceFileFetchJob.setFields({File::Fields::Id, File::Fields::ModifiedDate, File::Fields::LastViewedByMeDate, File::Fields::Description}); 1241 if (auto result = runJob(sourceFileFetchJob, src, sourceAccountId); !result.success()) { 1242 return result; 1243 } 1244 1245 const ObjectsList objects = sourceFileFetchJob.items(); 1246 if (objects.count() != 1) { 1247 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1248 } 1249 1250 const FilePtr sourceFile = objects[0].dynamicCast<File>(); 1251 1252 ParentReferencesList destParentReferences; 1253 if (destGDriveUrl.isRoot()) { 1254 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, dest.path()); 1255 } 1256 1257 QString destDirId; 1258 if (destGDriveUrl.isTopLevel()) { 1259 auto [result, id] = rootFolderId(destAccountId); 1260 1261 if (!result.success()) { 1262 return result; 1263 } 1264 destDirId = id; 1265 } else { 1266 auto [result, id] = resolveFileIdFromPath(destGDriveUrl.parentPath(), KIOGDrive::PathIsFolder); 1267 1268 if (!result.success()) { 1269 return result; 1270 } 1271 destDirId = id; 1272 } 1273 destParentReferences << ParentReferencePtr(new ParentReference(destDirId)); 1274 1275 FilePtr destFile(new File); 1276 destFile->setTitle(destGDriveUrl.filename()); 1277 destFile->setModifiedDate(sourceFile->modifiedDate()); 1278 destFile->setLastViewedByMeDate(sourceFile->lastViewedByMeDate()); 1279 destFile->setDescription(sourceFile->description()); 1280 destFile->setParents(destParentReferences); 1281 1282 FileCopyJob copyJob(sourceFile, destFile, getAccount(sourceAccountId)); 1283 return runJob(copyJob, dest, sourceAccountId); 1284 } 1285 1286 KIO::WorkerResult KIOGDrive::del(const QUrl &url, bool isfile) 1287 { 1288 // FIXME: Verify that a single file cannot actually have multiple parent 1289 // references. If it can, then we need to be more careful: currently this 1290 // implementation will simply remove the file from all it's parents but 1291 // it actually should just remove the current parent reference 1292 1293 // FIXME: Because of the above, we are not really deleting the file, but only 1294 // moving it to trash - so if users really really really wants to delete the 1295 // file, they have to go to GDrive web interface and delete it there. I think 1296 // that we should do the DELETE operation here, because for trash people have 1297 // their local trashes. This however requires fixing the first FIXME first, 1298 // otherwise we are risking severe data loss. 1299 1300 const auto gdriveUrl = GDriveUrl(url); 1301 1302 // Trying to delete the Team Drive root is pointless 1303 if (gdriveUrl.isSharedDrivesRoot()) { 1304 qCDebug(GDRIVE) << "Tried deleting Shared Drives root."; 1305 return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("Can't delete Shared Drives root.")); 1306 } 1307 1308 qCDebug(GDRIVE) << "Deleting URL" << url << "- is it a file?" << isfile; 1309 1310 const QUrlQuery urlQuery(url); 1311 QString fileId; 1312 1313 if (isfile && urlQuery.hasQueryItem(QStringLiteral("id"))) { 1314 fileId = urlQuery.queryItemValue(QStringLiteral("id")); 1315 } else { 1316 auto [result, id] = resolveFileIdFromPath(url.adjusted(QUrl::StripTrailingSlash).path(), isfile ? KIOGDrive::PathIsFile : KIOGDrive::PathIsFolder); 1317 if (!result.success()) { 1318 return result; 1319 } 1320 fileId = id; 1321 } 1322 1323 if (fileId.isEmpty()) { 1324 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 1325 } 1326 const QString accountId = gdriveUrl.account(); 1327 1328 // If user tries to delete the account folder, remove the account from the keychain 1329 if (gdriveUrl.isAccountRoot()) { 1330 const KGAPI2::AccountPtr account = getAccount(accountId); 1331 if (account->accountName().isEmpty()) { 1332 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, accountId); 1333 } 1334 m_accountManager->removeAccount(accountId); 1335 return KIO::WorkerResult::pass(); 1336 } 1337 1338 if (gdriveUrl.isSharedDrive()) { 1339 qCDebug(GDRIVE) << "Deleting Shared Drive" << url; 1340 return deleteSharedDrive(url); 1341 } 1342 1343 // GDrive allows us to delete entire directory even when it's not empty, 1344 // so we need to emulate the normal behavior ourselves by checking number of 1345 // child references 1346 if (!isfile) { 1347 ChildReferenceFetchJob referencesFetch(fileId, getAccount(accountId)); 1348 if (auto result = runJob(referencesFetch, url, accountId); !result.success()) { 1349 return result; 1350 } 1351 const bool isEmpty = !referencesFetch.items().count(); 1352 1353 if (!isEmpty && metaData(QStringLiteral("recurse")) != QLatin1String("true")) { 1354 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_RMDIR, url.path()); 1355 } 1356 } 1357 1358 FileTrashJob trashJob(fileId, getAccount(accountId)); 1359 1360 auto result = runJob(trashJob, url, accountId); 1361 m_cache.removePath(url.path()); 1362 return result; 1363 } 1364 1365 KIO::WorkerResult KIOGDrive::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) 1366 { 1367 Q_UNUSED(flags) 1368 qCDebug(GDRIVE) << "Renaming" << src << "to" << dest; 1369 1370 const auto srcGDriveUrl = GDriveUrl(src); 1371 const auto destGDriveUrl = GDriveUrl(dest); 1372 const QString sourceAccountId = srcGDriveUrl.account(); 1373 const QString destAccountId = destGDriveUrl.account(); 1374 1375 // TODO: Does this actually happen, or does KIO treat our account name as host? 1376 if (sourceAccountId != destAccountId) { 1377 return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, src.path()); 1378 } 1379 1380 if (srcGDriveUrl.isRoot()) { 1381 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, dest.path()); 1382 } 1383 if (srcGDriveUrl.isAccountRoot()) { 1384 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, dest.path()); 1385 } 1386 1387 const QUrlQuery urlQuery(src); 1388 QString sourceFileId; 1389 1390 if (urlQuery.hasQueryItem(QStringLiteral("id"))) { 1391 sourceFileId = urlQuery.queryItemValue(QStringLiteral("id")); 1392 } else { 1393 auto [result, id] = resolveFileIdFromPath(src.adjusted(QUrl::StripTrailingSlash).path(), KIOGDrive::PathIsFile); 1394 if (!result.success()) { 1395 return result; 1396 } 1397 sourceFileId = id; 1398 } 1399 1400 if (sourceFileId.isEmpty()) { 1401 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1402 } 1403 1404 if (srcGDriveUrl.isSharedDrive()) { 1405 qCDebug(GDRIVE) << "Renaming Shared Drive" << srcGDriveUrl.filename() << "to" << destGDriveUrl.filename(); 1406 DrivesPtr drives = DrivesPtr::create(); 1407 drives->setId(sourceFileId); 1408 drives->setName(destGDriveUrl.filename()); 1409 1410 DrivesModifyJob modifyJob(drives, getAccount(sourceAccountId)); 1411 if (auto result = runJob(modifyJob, src, sourceAccountId); !result.success()) { 1412 return result; 1413 } 1414 1415 return KIO::WorkerResult::pass(); 1416 } 1417 1418 // We need to fetch ALL, so that we can do update later 1419 FileFetchJob sourceFileFetchJob(sourceFileId, getAccount(sourceAccountId)); 1420 if (auto result = runJob(sourceFileFetchJob, src, sourceAccountId); !result.success()) { 1421 return result; 1422 } 1423 1424 const ObjectsList objects = sourceFileFetchJob.items(); 1425 if (objects.count() != 1) { 1426 qCDebug(GDRIVE) << "FileFetchJob retrieved" << objects.count() << "items, while only one was expected."; 1427 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1428 } 1429 1430 const FilePtr sourceFile = objects[0].dynamicCast<File>(); 1431 1432 ParentReferencesList parentReferences = sourceFile->parents(); 1433 if (destGDriveUrl.isRoot()) { 1434 // user is trying to move to top-level gdrive:/// 1435 return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, dest.fileName()); 1436 } 1437 if (destGDriveUrl.isAccountRoot()) { 1438 // user is trying to move to root -> we are only renaming 1439 } else { 1440 // skip filename and extract the second-to-last component 1441 auto [destDirResult, destDirId] = resolveFileIdFromPath(destGDriveUrl.parentPath(), KIOGDrive::PathIsFolder); 1442 1443 if (!destDirResult.success()) { 1444 return destDirResult; 1445 } 1446 1447 auto [srcDirResult, srcDirId] = resolveFileIdFromPath(srcGDriveUrl.parentPath(), KIOGDrive::PathIsFolder); 1448 1449 if (!srcDirResult.success()) { 1450 return srcDirResult; 1451 } 1452 1453 // Remove source from parent references 1454 auto iter = parentReferences.begin(); 1455 bool removed = false; 1456 while (iter != parentReferences.end()) { 1457 const ParentReferencePtr ref = *iter; 1458 if (ref->id() == srcDirId) { 1459 parentReferences.erase(iter); 1460 removed = true; 1461 break; 1462 } 1463 ++iter; 1464 } 1465 if (!removed) { 1466 qCDebug(GDRIVE) << "Could not remove" << src << "from parent references."; 1467 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src.path()); 1468 } 1469 1470 // Add destination to parent references 1471 parentReferences << ParentReferencePtr(new ParentReference(destDirId)); 1472 } 1473 1474 FilePtr destFile(sourceFile); 1475 destFile->setTitle(destGDriveUrl.filename()); 1476 destFile->setParents(parentReferences); 1477 1478 FileModifyJob modifyJob(destFile, getAccount(sourceAccountId)); 1479 modifyJob.setUpdateModifiedDate(true); 1480 return runJob(modifyJob, dest, sourceAccountId); 1481 } 1482 1483 KIO::WorkerResult KIOGDrive::mimetype(const QUrl &url) 1484 { 1485 qCDebug(GDRIVE) << Q_FUNC_INFO << url; 1486 1487 const QUrlQuery urlQuery(url); 1488 QString fileId; 1489 1490 if (urlQuery.hasQueryItem(QStringLiteral("id"))) { 1491 fileId = urlQuery.queryItemValue(QStringLiteral("id")); 1492 } else { 1493 auto [result, id] = resolveFileIdFromPath(url.adjusted(QUrl::StripTrailingSlash).path()); 1494 1495 if (!result.success()) { 1496 return result; 1497 } 1498 fileId = id; 1499 } 1500 1501 if (fileId.isEmpty()) { 1502 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 1503 } 1504 const QString accountId = GDriveUrl(url).account(); 1505 1506 FileFetchJob fileFetchJob(fileId, getAccount(accountId)); 1507 fileFetchJob.setFields({File::Fields::Id, File::Fields::MimeType}); 1508 if (auto result = runJob(fileFetchJob, url, accountId); !result.success()) { 1509 return result; 1510 } 1511 1512 const ObjectsList objects = fileFetchJob.items(); 1513 if (objects.count() != 1) { 1514 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 1515 } 1516 1517 const FilePtr file = objects.first().dynamicCast<File>(); 1518 mimeType(file->mimeType()); 1519 return KIO::WorkerResult::pass(); 1520 } 1521 1522 #include "kio_gdrive.moc"