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"