diff --git a/autotests/urltest.cpp b/autotests/urltest.cpp --- a/autotests/urltest.cpp +++ b/autotests/urltest.cpp @@ -91,6 +91,16 @@ << QStringList {QStringLiteral("foo@gmail.com"), GDriveUrl::TrashDir, QStringLiteral("baz.txt")} << QStringLiteral("baz.txt"); + QTest::newRow("account shared drives url") + << gdriveUrl(QStringLiteral("/foo@gmail.com/") + GDriveUrl::SharedDrivesDir) + << QStringLiteral("gdrive:/foo@gmail.com/") + GDriveUrl::SharedDrivesDir + << QStringLiteral("foo@gmail.com") + << QStringLiteral("/foo@gmail.com") + << false + << true + << QStringList {QStringLiteral("foo@gmail.com"), GDriveUrl::SharedDrivesDir} + << GDriveUrl::SharedDrivesDir; + QTest::newRow("file in account root") << gdriveUrl(QStringLiteral("/foo@gmail.com/bar.txt")) << QStringLiteral("gdrive:/foo@gmail.com/bar.txt") diff --git a/src/gdriveurl.h b/src/gdriveurl.h --- a/src/gdriveurl.h +++ b/src/gdriveurl.h @@ -34,13 +34,16 @@ bool isRoot() const; bool isAccountRoot() const; bool isTopLevel() const; + bool isSharedDrivesRoot() const; + bool isSharedDrive() const; bool isTrashDir() const; bool isTrashed() const; QUrl url() const; QString parentPath() const; QStringList pathComponents() const; static const QString Scheme; + static const QString SharedDrivesDir; static const QString TrashDir; private: QUrl m_url; diff --git a/src/gdriveurl.cpp b/src/gdriveurl.cpp --- a/src/gdriveurl.cpp +++ b/src/gdriveurl.cpp @@ -21,8 +21,9 @@ #include "gdriveurl.h" -const QString GDriveUrl::Scheme = QStringLiteral("gdrive"); -const QString GDriveUrl::TrashDir = QStringLiteral("trash"); +const QString GDriveUrl::Scheme = QLatin1String("gdrive"); +const QString GDriveUrl::SharedDrivesDir = QLatin1String("Shared Drives"); +const QString GDriveUrl::TrashDir = QLatin1String("trash"); GDriveUrl::GDriveUrl(const QUrl &url) : m_url(url) @@ -64,6 +65,16 @@ return m_components.length() == 2; } +bool GDriveUrl::isSharedDrivesRoot() const +{ + return m_components.length() == 2 && m_components.at(1) == SharedDrivesDir; +} + +bool GDriveUrl::isSharedDrive() const +{ + return m_components.length() == 3 && m_components.at(1) == SharedDrivesDir; +} + bool GDriveUrl::isTrashDir() const { return m_components.length() == 2 && m_components.at(1) == TrashDir; diff --git a/src/kio_gdrive.h b/src/kio_gdrive.h --- a/src/kio_gdrive.h +++ b/src/kio_gdrive.h @@ -77,11 +77,23 @@ PathIsFile = 2 }; + enum class FetchEntryFlags { + None = 0, + CurrentDir = 1 + }; + static KIO::UDSEntry accountToUDSEntry(const QString &accountName); + static KIO::UDSEntry sharedDriveToUDSEntry(const KGAPI2::Drive::DrivesPtr &sharedDrive); void listAccounts(); void createAccount(); + void listSharedDrivesRoot(const QUrl &url); + bool createSharedDrive(const QUrl &url); + bool deleteSharedDrive(const QUrl &url); + void statSharedDrive(const QUrl &url); + KIO::UDSEntry fetchSharedDrivesRootEntry(const QString &accountId, FetchEntryFlags flags = FetchEntryFlags::None); + QString resolveFileIdFromPath(const QString &path, PathFlags flags = None); Action handleError(const KGAPI2::Job &job, const QUrl &url); diff --git a/src/kio_gdrive.cpp b/src/kio_gdrive.cpp --- a/src/kio_gdrive.cpp +++ b/src/kio_gdrive.cpp @@ -25,34 +25,39 @@ #include "gdriveversion.h" #include -#include +#include +#include +#include #include +#include +#include #include #include #include #include #include -#include #include +#include +#include +#include +#include +#include +#include #include #include #include -#include -#include -#include #include +#include +#include #include +#include #include #include #include #include #include -#include -#include -#include - using namespace KGAPI2; using namespace Drive; @@ -268,6 +273,27 @@ return entry; } +KIO::UDSEntry KIOGDrive::sharedDriveToUDSEntry(const DrivesPtr &sharedDrive) +{ + KIO::UDSEntry entry; + + qlonglong udsAccess = S_IRUSR | S_IXUSR | S_IRGRP; + if (sharedDrive->capabilities()->canRenameDrive() || sharedDrive->capabilities()->canDeleteDrive()) { + udsAccess |= S_IWUSR; + } + + entry.fastInsert(KIO::UDSEntry::UDS_NAME, sharedDrive->id()); + entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, sharedDrive->name()); + entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); + entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); + entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, sharedDrive->createdDate().toSecsSinceEpoch()); + entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, udsAccess); + entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, sharedDrive->hidden()); + entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-gdrive")); + + return entry; +} + void KIOGDrive::createAccount() { const KGAPI2::AccountPtr account = m_accountManager->createAccount(); @@ -319,6 +345,124 @@ return; } +void KIOGDrive::listSharedDrivesRoot(const QUrl &url) +{ + const auto gdriveUrl = GDriveUrl(url); + const QString accountId = gdriveUrl.account(); + DrivesFetchJob sharedDrivesFetchJob(getAccount(accountId)); + sharedDrivesFetchJob.setFields({ + Drives::Fields::Kind, + Drives::Fields::Id, + Drives::Fields::Name, + Drives::Fields::Hidden, + Drives::Fields::CreatedDate, + Drives::Fields::Capabilities + }); + + if (runJob(sharedDrivesFetchJob, url, accountId)) { + const auto objects = sharedDrivesFetchJob.items(); + for (const auto &object : objects) { + const DrivesPtr sharedDrive = object.dynamicCast(); + const KIO::UDSEntry entry = sharedDriveToUDSEntry(sharedDrive); + listEntry(entry); + } + + auto entry = fetchSharedDrivesRootEntry(accountId, FetchEntryFlags::CurrentDir); + listEntry(entry); + + finished(); + } +} + +bool KIOGDrive::createSharedDrive(const QUrl &url) +{ + const auto gdriveUrl = GDriveUrl(url); + const QString accountId = gdriveUrl.account(); + + DrivesPtr sharedDrive = DrivesPtr::create(); + sharedDrive->setName(gdriveUrl.filename()); + + const QString requestId = QUuid::createUuid().toString(); + DrivesCreateJob createJob(requestId, sharedDrive, getAccount(accountId)); + return runJob(createJob, url, accountId); +} + +bool KIOGDrive::deleteSharedDrive(const QUrl &url) +{ + const auto gdriveUrl = GDriveUrl(url); + const QString accountId = gdriveUrl.account(); + DrivesDeleteJob sharedDriveDeleteJob(gdriveUrl.filename(), getAccount(accountId)); + return runJob(sharedDriveDeleteJob, url, accountId); +} + +void KIOGDrive::statSharedDrive(const QUrl &url) +{ + const auto gdriveUrl = GDriveUrl(url); + const QString accountId = gdriveUrl.account(); + DrivesFetchJob sharedDriveFetchJob(gdriveUrl.filename(), getAccount(accountId)); + sharedDriveFetchJob.setFields({ + Drives::Fields::Kind, + Drives::Fields::Id, + Drives::Fields::Name, + Drives::Fields::Hidden, + Drives::Fields::CreatedDate, + Drives::Fields::Capabilities + }); + if (!runJob(sharedDriveFetchJob, url, accountId)) { + return; + } + + ObjectPtr object = sharedDriveFetchJob.items().at(0); + const DrivesPtr sharedDrive = object.dynamicCast(); + const auto entry = sharedDriveToUDSEntry(sharedDrive); + statEntry(entry); + finished(); +} + +KIO::UDSEntry KIOGDrive::fetchSharedDrivesRootEntry(const QString &accountId, FetchEntryFlags flags) +{ + // Not every user is allowed to create shared Drives, + // check with About resource. + bool canCreateDrives = false; + AboutFetchJob aboutFetch(getAccount(accountId)); + aboutFetch.setFields({ + About::Fields::Kind, + About::Fields::CanCreateDrives + }); + QEventLoop eventLoop; + QObject::connect(&aboutFetch, &KGAPI2::Job::finished, + &eventLoop, &QEventLoop::quit); + eventLoop.exec(); + if (aboutFetch.error() == KGAPI2::OK || aboutFetch.error() == KGAPI2::NoError) { + const AboutPtr about = aboutFetch.aboutData(); + if (about) { + canCreateDrives = about->canCreateDrives(); + } + } + qCDebug(GDRIVE) << "Account" << accountId << (canCreateDrives ? "can" : "can't") << "create Drives"; + + KIO::UDSEntry entry; + + if (flags == FetchEntryFlags::CurrentDir) { + entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); + } else { + entry.fastInsert(KIO::UDSEntry::UDS_NAME, GDriveUrl::SharedDrivesDir); + entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, i18n("Shared Drives")); + } + entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); + entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0); + entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-gdrive")); + + qlonglong udsAccess = S_IRUSR | S_IXUSR; + // If user is allowed to create shared Drives, add write bit on directory + if (canCreateDrives) { + udsAccess |= S_IWUSR; + } + entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, udsAccess); + + return entry; +} + class RecursionDepthCounter { public: @@ -371,6 +515,16 @@ return rootFolderId(gdriveUrl.account()); } + if (gdriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "Resolved" << path << "to Shared Drive" << gdriveUrl.filename(); + return gdriveUrl.filename(); + } + + if (gdriveUrl.isSharedDrivesRoot()) { + qCDebug(GDRIVE) << "Resolved" << path << "to Shared Drives root"; + return QString(); + } + // Try to recursively resolve ID of parent path - either from cache, or by // querying Google const QString parentId = resolveFileIdFromPath(gdriveUrl.parentPath(), KIOGDrive::PathIsFolder); @@ -452,7 +606,12 @@ listAccounts(); return; } else if (gdriveUrl.isAccountRoot()) { + auto entry = fetchSharedDrivesRootEntry(accountId); + listEntry(entry); folderId = rootFolderId(accountId); + } else if (gdriveUrl.isSharedDrivesRoot()) { + listSharedDrivesRoot(url); + return; } else { folderId = m_cache.idForPath(url.path()); if (folderId.isEmpty()) { @@ -502,7 +661,6 @@ finished(); } - void KIOGDrive::mkdir(const QUrl &url, int permissions) { // NOTE: We deliberately ignore the permissions field here, because GDrive @@ -519,6 +677,16 @@ error(KIO::ERR_DOES_NOT_EXIST, url.path()); return; } + + if (gdriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "Directory is shared drive, creating that instead" << url; + if (createSharedDrive(url)) { + // runJob will have shown an error otherwise + finished(); + } + return; + } + QString parentId; if (gdriveUrl.isTopLevel()) { parentId = rootFolderId(accountId); @@ -561,6 +729,18 @@ finished(); return; } + if (gdriveUrl.isSharedDrivesRoot()) { + qCDebug(GDRIVE) << "stat()ing Shared Drives root"; + const auto entry = fetchSharedDrivesRootEntry(accountId); + statEntry(entry); + finished(); + return; + } + if (gdriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "stat()ing Shared Drive" << url; + statSharedDrive(url); + return; + } const QUrlQuery urlQuery(url); const QString fileId @@ -575,6 +755,7 @@ FileFetchJob fileFetchJob(fileId, getAccount(accountId)); if (!runJob(fileFetchJob, url, accountId)) { + qCDebug(GDRIVE) << "Failed stat()ing file" << fileFetchJob.errorString(); return; } @@ -826,6 +1007,14 @@ qCDebug(GDRIVE) << Q_FUNC_INFO << url; + const auto gdriveUrl = GDriveUrl(url); + + if (gdriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "Can't create files in Shared Drives root" << url; + error(KIO::ERR_CANNOT_WRITE, url.path()); + return; + } + if (QUrlQuery(url).hasQueryItem(QStringLiteral("id"))) { if (!putUpdate(url)) { return; @@ -943,6 +1132,15 @@ // their local trashes. This however requires fixing the first FIXME first, // otherwise we are risking severe data loss. + const auto gdriveUrl = GDriveUrl(url); + + // Trying to delete the Team Drive root is pointless + if (gdriveUrl.isSharedDrivesRoot()) { + qCDebug(GDRIVE) << "Tried deleting Shared Drives root."; + error(KIO::ERR_SLAVE_DEFINED, i18n("Can't delete Shared Drives root.")); + return; + } + qCDebug(GDRIVE) << "Deleting URL" << url << "- is it a file?" << isfile; const QUrlQuery urlQuery(url); @@ -955,7 +1153,6 @@ error(KIO::ERR_DOES_NOT_EXIST, url.path()); return; } - const auto gdriveUrl = GDriveUrl(url); const QString accountId = gdriveUrl.account(); // If user tries to delete the account folder, remove the account from the keychain @@ -970,6 +1167,15 @@ return; } + if (gdriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "Deleting Shared Drive" << url; + if (deleteSharedDrive(url)) { + // error( will have been called in case of error + finished(); + } + return; + } + // GDrive allows us to delete entire directory even when it's not empty, // so we need to emulate the normal behavior ourselves by checking number of // child references @@ -1029,6 +1235,21 @@ return; } + if (srcGDriveUrl.isSharedDrive()) { + qCDebug(GDRIVE) << "Renaming Shared Drive" << srcGDriveUrl.filename() << "to" << destGDriveUrl.filename(); + DrivesPtr drives = DrivesPtr::create(); + drives->setId(sourceFileId); + drives->setName(destGDriveUrl.filename()); + + DrivesModifyJob modifyJob(drives, getAccount(sourceAccountId)); + if (!runJob(modifyJob, src, sourceAccountId)) { + return; + } + + finished(); + return; + } + // We need to fetch ALL, so that we can do update later FileFetchJob sourceFileFetchJob(sourceFileId, getAccount(sourceAccountId)); if (!runJob(sourceFileFetchJob, src, sourceAccountId)) {