diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -44,6 +44,7 @@ configure_file(twitter/contents/code/main.js.in ${CMAKE_CURRENT_BINARY_DIR}/twitter/contents/code/main.js @ONLY) kpackage_install_package(${CMAKE_CURRENT_BINARY_DIR}/twitter Twitter Purpose) kaccounts_add_service(${CMAKE_CURRENT_SOURCE_DIR}/twitter-microblog.service.in) + add_subdirectory(nextcloud) endif() add_subdirectory(kdeconnect) add_subdirectory(reviewboard) diff --git a/src/plugins/nextcloud/CMakeLists.txt b/src/plugins/nextcloud/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/CMakeLists.txt @@ -0,0 +1,8 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"purpose_nextcloud\") + +kaccounts_add_service(${CMAKE_CURRENT_SOURCE_DIR}/nextcloud-upload.service.in) + +add_share_plugin(nextcloudplugin nextcloudplugin.cpp nextcloudjob.cpp) +target_link_libraries(nextcloudplugin KF5::KIOCore KF5::I18n KF5::Purpose KAccounts) + + diff --git a/src/plugins/nextcloud/Messages.sh b/src/plugins/nextcloud/Messages.sh new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/Messages.sh @@ -0,0 +1,4 @@ +#!/bin/sh +$EXTRACTRC `find . -name \*.rc` `find . -name \*.ui` >> rc.cpp +$XGETTEXT `find . -not -path \*/tests/\* -name \*.cpp -o -name \*.cc -o -name \*.h` -o $podir/purpose_nextcloud.pot +rm -f rc.cpp diff --git a/src/plugins/nextcloud/nextcloud-upload.service.in b/src/plugins/nextcloud/nextcloud-upload.service.in new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloud-upload.service.in @@ -0,0 +1,8 @@ + + + nextcloud-upload + NextCloud Upload + owncloud + owncloud + kaccounts-providers + diff --git a/src/plugins/nextcloud/nextcloudjob.h b/src/plugins/nextcloud/nextcloudjob.h new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloudjob.h @@ -0,0 +1,44 @@ +/* + Copyright 2017 Lim Yuen Hoe + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef NEXTCLOUDJOB_H +#define NEXTCLOUDJOB_H + +#include +#include +#include + +class NextcloudJob : public Purpose::Job +{ + Q_OBJECT + public: + NextcloudJob(QObject* parent) + : Purpose::Job(parent), m_pendingJobs(0) + {} + void start() override; + + private Q_SLOTS: + void fileUploaded(KJob*); + void checkTargetFolder(KJob*); + + private: + void checkTargetFile(const QUrl& local, KJob* job); + void fileFetched(const QUrl& uploadUrl, KJob*); + QUrl m_davUrl; + int m_pendingJobs; +}; +#endif /* NEXTCLOUDJOB_H */ diff --git a/src/plugins/nextcloud/nextcloudjob.cpp b/src/plugins/nextcloud/nextcloudjob.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloudjob.cpp @@ -0,0 +1,144 @@ +/* + Copyright 2017 Lim Yuen Hoe + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "nextcloudjob.h" +#include +#include +#include +#include +#include +#include +#include +#include + +void NextcloudJob::start() +{ + // get all the info we need + const QString folder = data().value(QStringLiteral("folder")).toString(); + const Accounts::AccountId id = data().value(QStringLiteral("accountId")).toInt(); + Accounts::Account* acc = Accounts::Account::fromId(KAccounts::accountsManager(), id); + auto job = new GetCredentialsJob(id, this); + bool b = job->exec(); + if (!b) { + qWarning() << "Couldn't fetch credentials"; + setError(job->error()); + setErrorText(job->errorText()); + emitResult(); + return; + } + Q_FOREACH(const Accounts::Service &service, acc->services()) { + if (service.name() == QStringLiteral("nextcloud-upload")) { + acc->selectService(service); + } + } + m_davUrl = QUrl(acc->valueAsString(QStringLiteral("server")) + + QStringLiteral("remote.php/webdav/") + folder + QStringLiteral("/")); + m_davUrl.setUserName(job->credentialsData().value(QStringLiteral("UserName")).toString()); + m_davUrl.setPassword(job->credentialsData().value(QStringLiteral("Secret")).toString()); + + // first we check that the folder exists + KIO::DavJob* davjob = KIO::davPropFind(m_davUrl, QDomDocument(), QStringLiteral("0"), KIO::HideProgressInfo); + connect(davjob, &KJob::finished, this, &NextcloudJob::checkTargetFolder); +} + +void NextcloudJob::checkTargetFolder(KJob* j) +{ + QString responseString = qobject_cast(j)->response().toString(); + // TODO: prob a better way to do this + if (responseString.contains(QStringLiteral(""))) { + const QJsonArray urls = data().value(QStringLiteral("urls")).toArray(); + + foreach(const QJsonValue& url, urls) { + // before uploading, we try to avoid overwrite by checking for existing file on nextcloud + QUrl local = QUrl(url.toString()); + QUrl uploadTarget = m_davUrl; + // disallow giant filenames + // TODO: this doesn't currently deal well with say image clipboard data. might see if we can somehow add the right extension. + uploadTarget.setPath(uploadTarget.path() + KStringHandler::csqueeze(local.fileName(), 100)); + KIO::DavJob* davjob = KIO::davPropFind(uploadTarget, QDomDocument(), QStringLiteral("0"), KIO::HideProgressInfo); + connect(davjob, &KJob::finished, this, [=](KJob* job) { NextcloudJob::checkTargetFile(local, job); }); + m_pendingJobs++; + } + + } else { + qWarning() << "invalid folder"; + setError(KIO::Error::ERR_CANNOT_ENTER_DIRECTORY); + setErrorText(i18n("Invalid folder!")); + emitResult(); + } +} + +void NextcloudJob::checkTargetFile(const QUrl& local, KJob* j) +{ + if (j->error()) { + setError(j->error()); + setErrorText(j->errorText()); + emitResult(); + return; + } + + KIO::DavJob* job = qobject_cast(j); + QString responseString = job->response().toString(); + + // TODO: better way to do this + if (responseString.contains(QStringLiteral("DAV\\Exception\\NotFound"))) { + // okay file doesn't exist on nextcloud, we'll fetch the file, then upload it + KIO::StoredTransferJob* next = KIO::storedGet(local); + QUrl targetUrl = job->url(); + connect(next, &KJob::finished, this, [=](KJob* jj) { NextcloudJob::fileFetched(targetUrl, jj); }); + } else { + // file already exists! we try successive suggestions until we find a free name + QUrl uploadTarget = m_davUrl; + uploadTarget.setPath(uploadTarget.path() + KIO::suggestName(m_davUrl, job->url().fileName())); + qDebug() << "Trying: " << uploadTarget.toString(); + KIO::DavJob* davjob = KIO::davPropFind(uploadTarget, QDomDocument(), QStringLiteral("0"), KIO::HideProgressInfo); + connect(davjob, &KJob::finished, this, [=](KJob* jj) { NextcloudJob::checkTargetFile(local, jj); }); + } +} + +void NextcloudJob::fileFetched(const QUrl& uploadUrl, KJob* j) +{ + if (j->error()) { + setError(j->error()); + setErrorText(j->errorText()); + emitResult(); + return; + } + + KIO::StoredTransferJob* job = qobject_cast(j); + + // we fetched our file and we have a place to upload to. Time to upload! + KIO::StoredTransferJob *tJob = KIO::storedPut(job->data(), uploadUrl, KIO::HideProgressInfo); + connect(tJob, &KJob::result, this, &NextcloudJob::fileUploaded); +} + +void NextcloudJob::fileUploaded(KJob* j) +{ + if (j->error()) { + setError(j->error()); + setErrorText(j->errorText()); + emitResult(); + return; + } + + //KIO::StoredTransferJob *sjob = qobject_cast(j); + m_pendingJobs--; + if (m_pendingJobs == 0) { + setOutput( {{ QStringLiteral("url"), QString() }}); + emitResult(); + } +} diff --git a/src/plugins/nextcloud/nextcloudplugin.cpp b/src/plugins/nextcloud/nextcloudplugin.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloudplugin.cpp @@ -0,0 +1,44 @@ +/* + Copyright 2017 Lim Yuen Hoe + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include +#include "nextcloudjob.h" + +#include +#include + +class NextcloudPlugin : public Purpose::PluginBase +{ +Q_OBJECT +public: + NextcloudPlugin(QObject* parent, const QVariantList& args) + : Purpose::PluginBase(parent) + { + Q_UNUSED(args); + } + + virtual Purpose::Job* createJob() const override + { + return new NextcloudJob(nullptr); + } +}; + +K_PLUGIN_FACTORY_WITH_JSON(NextcloudShare, "nextcloudplugin.json", registerPlugin();) + +EXPORT_SHARE_VERSION + +#include "nextcloudplugin.moc" diff --git a/src/plugins/nextcloud/nextcloudplugin.json b/src/plugins/nextcloud/nextcloudplugin.json new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloudplugin.json @@ -0,0 +1,23 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Lim Yuen Hoe", + "Name[x-test]": "xxLim Yuen Hoexx" + } + ], + "Category": "Utilities", + "Description": "Upload files to Nextcloud", + "Icon": "edit-paste", + "License": "GPL", + "Name": "NextCloud", + "Name[x-test]": "xxNextCloudxx" + }, + "X-Purpose-Configuration": [ + "folder", + "accountId" + ], + "X-Purpose-PluginTypes": [ + "Export" + ] +} diff --git a/src/plugins/nextcloud/nextcloudplugin_config.qml b/src/plugins/nextcloud/nextcloudplugin_config.qml new file mode 100644 --- /dev/null +++ b/src/plugins/nextcloud/nextcloudplugin_config.qml @@ -0,0 +1,76 @@ +/* + Copyright 2017 Lim Yuen Hoe + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +import QtQuick 2.2 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kquickcontrolsaddons 2.0 as KQCA +import Ubuntu.OnlineAccounts 0.1 as OA + +ColumnLayout +{ + id: root + + property alias folder: folderField.text + property var accountId + property var urls + property var mimeType + + function accountChanged() + { + var valid = accountsCombo.enabled && accountsCombo.currentIndex>=0; + if (valid) { root.accountId = serviceModel.get(accountsCombo.currentIndex, "accountId"); } + refreshConfigReady(); + } + + // without manually refreshing, auto-filled values don't seem to activate the "Run" button + function refreshConfigReady() + { + var jobData = configuration.data; + jobData['accountId'] = root.accountId; + jobData['folder'] = root.folder; + configuration.data = jobData; + } + + Label { text: i18n("Account:") } + RowLayout { + Layout.fillWidth: true + ComboBox { + id: accountsCombo + + Layout.fillWidth: true + textRole: "displayName" + enabled: count>0 + model: OA.AccountServiceModel { + id: serviceModel + serviceType: "nextcloud-upload" + } + onCurrentIndexChanged: root.accountChanged() + Component.onCompleted: root.accountChanged() + } + Button { + iconName: "settings-configure" + onClicked: KQCA.KCMShell.open("kcm_kaccounts"); + } + } + Label { text: i18n("Upload to folder:") } + TextField { + id: folderField + Layout.fillWidth: true + text: "/" + } +}