Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
find_package(LibreGraphAPI 1.0.4 REQUIRED)
set_package_properties(LibreGraphAPI PROPERTIES
URL https://github.com/owncloud/libre-graph-api-cpp-qt-client.git
DESCRIPTION "Libre Graph is a free API for cloud collaboration inspired by the MS Graph API"
TYPE REQUIRED
)
configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
add_library(libsync SHARED
account.cpp
bandwidthmanager.cpp
capabilities.cpp
cookiejar.cpp
discovery.cpp
discoveryinfo.cpp
discoveryphase.cpp
discoveryremoteinfo.cpp
filesystem.cpp
httplogger.cpp
jobqueue.cpp
logger.cpp
accessmanager.cpp
configfile.cpp
globalconfig.cpp
abstractnetworkjob.cpp
networkjobs.cpp
owncloudpropagator.cpp
qsferatheme.cpp
platform.cpp
progressdispatcher.cpp
propagatorjobs.cpp
propagatedownload.cpp
propagateupload.cpp
propagateuploadv1.cpp
propagateuploadtus.cpp
propagateremotedelete.cpp
propagateremotemove.cpp
propagateremotemkdir.cpp
syncengine.cpp
syncfileitem.cpp
syncfilestatustracker.cpp
localdiscoverytracker.cpp
syncresult.cpp
syncoptions.cpp
theme.cpp
creds/abstractcredentials.cpp
creds/credentialmanager.cpp
creds/httpcredentials.cpp
creds/oauth.cpp
creds/jwt.cpp
creds/webfinger.cpp
creds/idtoken.cpp
networkjobs/getfilejob.cpp
networkjobs/checkserverjobfactory.cpp
networkjobs/fetchuserinfojobfactory.cpp
networkjobs/jsonjob.cpp
networkjobs/simplenetworkjob.cpp
networkjobs/resources.cpp
abstractcorejob.cpp
appprovider.cpp
path.cpp
)
if(WIN32)
target_sources(libsync PRIVATE platform_win.cpp)
elseif(UNIX)
target_sources(libsync PRIVATE xattr.cpp)
if (APPLE)
target_sources(libsync PRIVATE platform_mac.mm)
else()
target_sources(libsync PRIVATE platform_unix.cpp)
endif()
endif()
# These headers are installed for libqsferasync to be used by 3rd party apps
INSTALL(
FILES
${CMAKE_CURRENT_BINARY_DIR}/qsferasynclib.h
logger.h
accessmanager.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR}/${APPLICATION_SHORTNAME}/libsync
)
set_target_properties(libsync PROPERTIES EXPORT_NAME SyncCore)
target_link_libraries(libsync
PUBLIC
QSferaResources
Qt::Core
Qt::Network
Qt::Widgets
Qt::QuickControls2
PRIVATE
Qt::Concurrent
ZLIB::ZLIB
Qt6Keychain::Qt6Keychain
SQLite::SQLite3
)
apply_common_target_settings(libsync)
ecm_add_qml_module(libsync
URI eu.QSfera.libsync
VERSION 1.0
NAMESPACE OCC
GENERATE_PLUGIN_SOURCE
)
ecm_finalize_qml_module(libsync DESTINATION ${KDE_INSTALL_QMLDIR})
set_source_files_properties(configfile.cpp
PROPERTIES
COMPILE_DEFINITIONS EXCLUDE_FILE_NAME="${EXCLUDE_FILE_NAME}"
)
target_link_libraries(libsync PUBLIC $<BUILD_INTERFACE:OpenAPI::LibreGraphAPI>)
add_subdirectory(graphapi)
add_subdirectory(vfs)
if ( APPLE )
target_link_libraries(libsync PUBLIC
/System/Library/Frameworks/CoreServices.framework
/System/Library/Frameworks/Foundation.framework
/System/Library/Frameworks/AppKit.framework
/System/Library/Frameworks/IOKit.framework
)
endif()
if(Inotify_FOUND)
target_include_directories(libsync PRIVATE ${Inotify_INCLUDE_DIRS})
target_link_libraries(libsync PUBLIC ${Inotify_LIBRARIES})
endif()
if(NO_MSG_HANDLER)
target_compile_definitions(libsync PRIVATE -DNO_MSG_HANDLER=1)
endif()
GENERATE_EXPORT_HEADER(libsync
EXPORT_MACRO_NAME QSFERA_SYNC_EXPORT
EXPORT_FILE_NAME qsferasynclib.h
STATIC_DEFINE QSFERA_BUILT_AS_STATIC
)
set_target_properties(libsync PROPERTIES
OUTPUT_NAME "QSferaLibSync"
VERSION ${MIRALL_VERSION}
SOVERSION ${MIRALL_SOVERSION}
)
if(WITH_CRASHREPORTER)
set_source_files_properties(platform.cpp PROPERTIES COMPILE_DEFINITIONS CRASHREPORTER_EXECUTABLE="${CRASHREPORTER_EXECUTABLE}")
target_link_libraries(libsync PRIVATE CrashReporterQt::Handler)
if(UNIX AND NOT MAC)
find_package(Threads REQUIRED)
target_link_libraries(libsync PUBLIC Threads::Threads)
endif()
endif()
target_sources(libsync PRIVATE
csync.cpp
csync_exclude.cpp
)
try_compile(HAS_CLOCK_CAST
SOURCE_FROM_CONTENT chrono_test.cpp "
#include <chrono>
int main() {
std::chrono::clock_cast<std::chrono::system_clock>;
}"
)
message(STATUS "HAS_CLOCK_CAST: ${HAS_CLOCK_CAST}")
if(HAS_CLOCK_CAST)
set_source_files_properties(filesystem.cpp PROPERTIES COMPILE_DEFINITIONS HAS_CLOCK_CAST)
endif()
add_subdirectory(common)
install(TARGETS libsync EXPORT ${APPLICATION_SHORTNAME}Config ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright (C) Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "abstractcorejob.h"
using namespace OCC;
AbstractCoreJobFactory::AbstractCoreJobFactory(QNetworkAccessManager *nam)
: _nam(nam)
{
}
AbstractCoreJobFactory::~AbstractCoreJobFactory()
{
}
QNetworkAccessManager *AbstractCoreJobFactory::nam() const
{
return _nam;
}
void AbstractCoreJobFactory::setJobResult(CoreJob *job, const QVariant &result)
{
job->setResult(result);
}
void AbstractCoreJobFactory::setJobError(CoreJob *job, const QString &errorMessage)
{
job->setError(errorMessage);
}
const QVariant &CoreJob::result() const
{
return _result;
}
const QString &CoreJob::errorMessage() const
{
return _errorMessage;
}
QNetworkReply *CoreJob::reply() const
{
return _reply;
}
bool CoreJob::success() const
{
return _success;
}
void CoreJob::setResult(const QVariant &result)
{
if (OC_ENSURE(assertNotFinished())) {
_success = true;
_result = result;
Q_EMIT finished();
}
}
void CoreJob::setError(const QString &errorMessage)
{
if (OC_ENSURE(assertNotFinished())) {
_errorMessage = errorMessage;
Q_EMIT finished();
}
}
CoreJob::CoreJob(QNetworkReply *reply, QObject *parent)
: QObject(parent)
, _reply(reply)
{
_reply->setParent(this);
connect(this, &CoreJob::finished, this, &CoreJob::deleteLater, Qt::QueuedConnection);
}
bool CoreJob::assertNotFinished() const
{
OC_ASSERT(_result.isNull());
OC_ASSERT(_errorMessage.isEmpty());
return _result.isNull() && _errorMessage.isEmpty();
}
+147
View File
@@ -0,0 +1,147 @@
/*
* Copyright (C) Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "abstractnetworkjob.h"
#include "libsync/qsferasynclib.h"
namespace OCC {
/**
* This class manages an (HTTP) network job's result. It holds a result on success and error details on failures.
* The class is universally usable for all kinds of network requests, there is no difference in handling the responses.
* Instead, instances are created that start a suitable request and wire up the signals accordingly.
* In contrast to the traditional network jobs (e.g., SimpleNetworkJob), core jobs are not bound to an account. Therefore,
* they can be used with ease in situations where an account object is not available (e.g., the new wizard).
*/
class QSFERA_SYNC_EXPORT CoreJob : public QObject
{
Q_OBJECT
friend class AbstractCoreJobFactory;
public:
explicit CoreJob(QNetworkReply *reply, QObject *parent);
[[nodiscard]] const QVariant &result() const;
[[nodiscard]] const QString &errorMessage() const;
[[nodiscard]] QNetworkReply *reply() const;
[[nodiscard]] bool success() const;
protected:
/**
* Set job result. This emits the finished() signal, and marks the job as successful.
* The job result or error can be set only once.
* @param result job result
*/
void setResult(const QVariant &result);
/**
* Set job error details. This emits the finished() signal, and marks the job as failed.
* The job result or error can be set only once.
* @param errorMessage network error or other suitable error message
*/
void setError(const QString &errorMessage);
Q_SIGNALS:
/**
* Emitted when a result or error is set. May be emitted once only.
*/
void finished();
/**
* Emitted whenever the user accepts a CA certificate which is not trusted yet using the TlsErrorDialog.
* Handlers should store the certificate in the access manager passed to the corresponding job.
* @param caCertificate accepted CA certificate
*/
void caCertificateAccepted(const QSslCertificate &caCertificate);
private:
// job result/error should be set only once, because that emits the "finished" signal
[[nodiscard]] bool assertNotFinished() const;
bool _success = false;
QVariant _result;
QString _errorMessage;
QNetworkReply *_reply = nullptr;
};
/**
* Abstract base class for core job factories.
*
* Jobs are built by the startJob factory method, which creates a job instance as well as a network request, wires the required signals up, then sends the request.
*/
class QSFERA_SYNC_EXPORT AbstractCoreJobFactory
{
public:
/**
* Create a new instance
* @param nam network access manager used to send the requests
* @param parent optional parent which will be set at this object's parent as well as all jobs' parent.
*/
explicit AbstractCoreJobFactory(QNetworkAccessManager *nam);
virtual ~AbstractCoreJobFactory();
/**
* Send network request and return associated job.
* @param url URL to send request to
* @return job
*/
virtual CoreJob *startJob(const QUrl &url, QObject *parent) = 0;
protected:
[[nodiscard]] QNetworkAccessManager *nam() const;
/**
* Set job result. Needed because the jobs' methods are protected, and this class is a friend of Job.
* The job result or error can be set only once.
* @param result job result
*/
static void setJobResult(CoreJob *job, const QVariant &result);
/**
* Set job error details. Needed because the jobs' methods are protected, and this class is a friend of Job.
* @param errorMessage network error or other suitable error message
*/
static void setJobError(CoreJob *job, const QString &errorMessage);
/**
* Factory to create QNetworkRequests with properly set timeout.
*/
template <typename... Params>
[[nodiscard]] static QNetworkRequest makeRequest(Params... params)
{
auto request = QNetworkRequest(params...);
const auto timeoutMilliseconds = static_cast<int>(AbstractNetworkJob::httpTimeout.count());
request.setTransferTimeout(timeoutMilliseconds);
return request;
}
private:
QNetworkAccessManager *_nam;
};
}
+445
View File
@@ -0,0 +1,445 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include <QAuthenticator>
#include <QNetworkRequest>
#include "common/asserts.h"
#include "networkjobs.h"
#include "account.h"
#include "owncloudpropagator.h"
#include "httplogger.h"
#include "creds/abstractcredentials.h"
Q_DECLARE_METATYPE(QTimer *)
using namespace std::chrono;
using namespace std::chrono_literals;
namespace {
constexpr int MaxRetryCount = 5;
}
namespace OCC {
Q_LOGGING_CATEGORY(lcNetworkJob, "sync.networkjob", QtInfoMsg)
// If not set, it is overwritten by the Application constructor with the value from the config
milliseconds AbstractNetworkJob::httpTimeout = AbstractNetworkJob::DefaultHttpTimeout;
AbstractNetworkJob::AbstractNetworkJob(AccountPtr account, const QUrl &baseUrl, const QString &path, QObject *parent)
: QObject(parent)
, _account(account)
, _baseUrl(baseUrl)
, _path(path)
{
// Since we hold a QSharedPointer to the account, this makes no sense. (issue #6893)
Q_ASSERT(account != parent);
Q_ASSERT(baseUrl.isValid());
}
QUrl AbstractNetworkJob::baseUrl() const
{
return _baseUrl;
}
QUrl AbstractNetworkJob::url() const
{
return Utility::concatUrlPath(baseUrl(), path(), query());
}
void AbstractNetworkJob::setQuery(const QUrlQuery &query)
{
_query = query;
}
QUrlQuery AbstractNetworkJob::query() const
{
return _query;
}
void AbstractNetworkJob::setTimeout(const std::chrono::milliseconds sec)
{
_timeout = sec;
}
void AbstractNetworkJob::setForceIgnoreCredentialFailure(bool ignore)
{
_forceIgnoreCredentialFailure = ignore;
}
bool AbstractNetworkJob::ignoreCredentialFailure() const
{
return _forceIgnoreCredentialFailure || _isAuthenticationJob;
}
QNetworkReply *AbstractNetworkJob::reply() const
{
Q_ASSERT(_reply);
return _reply;
}
bool AbstractNetworkJob::isAuthenticationJob() const
{
return _isAuthenticationJob;
}
void AbstractNetworkJob::setAuthenticationJob(bool b)
{
_isAuthenticationJob = b;
}
bool AbstractNetworkJob::needsRetry() const
{
if (isAuthenticationJob()) {
qCDebug(lcNetworkJob) << u"Not Retry auth job" << this << url();
return false;
}
if (retryCount() >= MaxRetryCount) {
qCDebug(lcNetworkJob) << u"Not Retry too many retries" << retryCount() << this << url();
return false;
}
if (auto reply = this->reply()) {
// we had an unsupported redirect
if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
return true;
}
switch (reply->error()) {
case QNetworkReply::AuthenticationRequiredError:
return true;
case QNetworkReply::ContentReSendError:
if (_reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool()) {
return true;
}
[[fallthrough]];
default:
break;
}
}
return false;
}
void AbstractNetworkJob::sendRequest(const QByteArray &verb, const QNetworkRequest &req, std::unique_ptr<QIODevice> &&requestBody)
{
_verb = verb;
_request = req;
// allow transferring files with a big compression ratio
_request.setDecompressedSafetyCheckThreshold(-1);
_request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, _storeInCache);
if (_cacheLoadControl.has_value()) {
_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, _cacheLoadControl.value());
}
if (requestBody) {
_requestBody = requestBody.release();
_requestBody->setParent(this);
}
Q_ASSERT(_request.url().isEmpty() || _request.url() == url());
Q_ASSERT(_request.transferTimeout() == 0 || _request.transferTimeout() == _timeout.count());
_request.setUrl(url());
_request.setPriority(_priority);
_request.setTransferTimeout(_timeout);
if (!isAuthenticationJob() && _account->jobQueue()->enqueue(this)) {
return;
}
auto reply = _account->sendRawRequest(verb, _request.url(), _request, _requestBody);
adoptRequest(reply);
}
void AbstractNetworkJob::adoptRequest(QPointer<QNetworkReply> reply)
{
std::swap(_reply, reply);
if (reply) {
reply->disconnect();
reply->abort();
reply->deleteLater();
}
_request = _reply->request();
connect(_reply, &QNetworkReply::finished, this, &AbstractNetworkJob::slotFinished);
newReplyHook(_reply);
}
void AbstractNetworkJob::slotFinished()
{
_finished = true;
_account->credentials()->checkCredentials(_reply);
if (_reply->error() != QNetworkReply::NoError) {
if (_account->jobQueue()->retry(this)) {
qCDebug(lcNetworkJob) << u"Queued:" << this << u"for retry";
return;
}
if (_reply->error() == QNetworkReply::OperationCanceledError && !_aborted) {
_timedout = true;
}
Q_EMIT networkError(_reply);
}
// get the Date timestamp from reply
_responseTimestamp = _reply->rawHeader("Date");
if (!reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull() && !(isAuthenticationJob() || reply()->request().hasRawHeader(QByteArrayLiteral("OC-Connection-Validator")))) {
Q_EMIT _account->unknownConnectionState();
qCWarning(lcNetworkJob) << this << u"Unsupported redirect on" << _reply->url().toString() << u"to"
<< reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
Q_EMIT networkError(_reply);
if (_account->jobQueue()->retry(this)) {
qCWarning(lcNetworkJob) << u"Retry Nr:" << _retryCount << _reply->url();
return;
} else {
qCWarning(lcNetworkJob) << u"Don't retry:" << _reply->url();
}
}
Q_EMIT aboutToFinishSignal(AbstractNetworkJob::QPrivateSignal());
finished();
Q_EMIT finishedSignal(AbstractNetworkJob::QPrivateSignal());
qCDebug(lcNetworkJob) << u"Network job finished" << this;
deleteLater();
}
QByteArray AbstractNetworkJob::responseTimestamp() const
{
return _responseTimestamp;
}
QDateTime OCC::AbstractNetworkJob::responseQTimeStamp() const
{
return Utility::parseRFC1123Date(QString::fromUtf8(responseTimestamp()));
}
QByteArray AbstractNetworkJob::requestId()
{
return _reply ? _reply->request().rawHeader("X-Request-ID") : QByteArray();
}
QString AbstractNetworkJob::errorString() const
{
if (_timedout) {
return tr("Connection timed out");
} else if (!reply()) {
return tr("Unknown error: network reply was deleted");
} else if (reply()->hasRawHeader("OC-ErrorString")) {
return QString::fromUtf8(reply()->rawHeader("OC-ErrorString"));
} else {
return networkReplyErrorString(*reply());
}
}
QString AbstractNetworkJob::errorStringParsingBody(QByteArray *body)
{
QString base = errorString();
if (base.isEmpty() || !reply()) {
return QString();
}
QByteArray replyBody = reply()->readAll();
if (body) {
*body = replyBody;
}
QString extra = extractErrorMessage(replyBody);
// Don't append the XML error message to a OC-ErrorString message.
if (!extra.isEmpty() && !reply()->hasRawHeader("OC-ErrorString")) {
return QStringLiteral("%1 (%2)").arg(base, extra);
}
return base;
}
AbstractNetworkJob::~AbstractNetworkJob()
{
if (!_finished && !_aborted && !_timedout) {
qCCritical(lcNetworkJob) << u"Deleting running job" << this;
}
if (_reply) {
// the body must live as long as the reply exists
// until now the body was parented by this network job
if (_requestBody) {
_requestBody->setParent(_reply);
}
_reply->disconnect(this);
_reply->abort();
_reply->deleteLater();
_reply.clear();
}
}
void AbstractNetworkJob::start()
{
qCInfo(lcNetworkJob) << u"Created" << this << u"for" << parent();
}
QString AbstractNetworkJob::replyStatusString() {
Q_ASSERT(reply());
if (reply()->error() == QNetworkReply::NoError) {
return QStringLiteral("OK");
} else {
return QStringLiteral("%1, %2").arg(Utility::enumToString(reply()->error()), errorString());
}
}
QString extractErrorMessage(const QByteArray &errorResponse)
{
QXmlStreamReader reader(errorResponse);
reader.readNextStartElement();
if (reader.name() != QLatin1String("error")) {
return QString();
}
QString exception;
while (!reader.atEnd() && !reader.hasError()) {
reader.readNextStartElement();
if (reader.name() == QLatin1String("message")) {
QString message = reader.readElementText();
if (!message.isEmpty()) {
return message;
}
} else if (reader.name() == QLatin1String("exception")) {
exception = reader.readElementText();
}
}
// Fallback, if message could not be found
return exception;
}
QString errorMessage(const QString &baseError, const QByteArray &body)
{
QString msg = baseError;
QString extra = extractErrorMessage(body);
if (!extra.isEmpty()) {
msg += QStringLiteral(" (%1)").arg(extra);
}
return msg;
}
QString networkReplyErrorString(const QNetworkReply &reply)
{
QString base = reply.errorString();
int httpStatus = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString httpReason = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
// Only adjust HTTP error messages of the expected format.
if (httpReason.isEmpty() || httpStatus == 0 || !base.contains(httpReason)) {
return base;
}
return AbstractNetworkJob::tr("Server replied \"%1 %2\" to \"%3 %4\"").arg(QString::number(httpStatus), httpReason, QString::fromLatin1(HttpLogger::requestVerb(reply)), reply.request().url().toDisplayString());
}
void AbstractNetworkJob::retry()
{
OC_ENFORCE(!_verb.isEmpty());
_retryCount++;
qCInfo(lcNetworkJob) << u"Restarting" << this << u"for the" << _retryCount << u"time";
if (_requestBody) {
if (!_requestBody->isSequential()) {
Q_ASSERT(_requestBody->isOpen());
_requestBody->seek(0);
} else {
qCWarning(lcNetworkJob) << u"Can't resend request, body not suitable" << this;
abort();
return;
}
}
// this will reuse _requestBody
sendRequest(_verb, _request);
}
void AbstractNetworkJob::abort()
{
if (_reply) {
// calling abort will trigger the execution of finished()
// with _reply->error() == QNetworkReply::OperationCanceledError
// the api user can then decide whether to discard this job or retry it.
// The order is important, mark as _aborted before we abort the reply which will trigger slotFinished
_aborted = true;
_reply->abort();
} else {
deleteLater();
}
}
void AbstractNetworkJob::setPriority(QNetworkRequest::Priority priority)
{
_priority = priority;
}
QNetworkRequest::Priority AbstractNetworkJob::priority() const
{
return _priority;
}
int AbstractNetworkJob::httpStatusCode() const
{
return reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
void AbstractNetworkJob::setStoreInCache(bool storeInCache)
{
_storeInCache = storeInCache;
}
void AbstractNetworkJob::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
{
_cacheLoadControl = cacheLoadControl;
}
QPointer<QIODevice> AbstractNetworkJob::body() const
{
return _requestBody;
}
} // namespace OCC
QDebug operator<<(QDebug debug, const OCC::AbstractNetworkJob *job)
{
QDebugStateSaver saver(debug);
debug.setAutoInsertSpaces(false);
debug << job->metaObject()->className() << u"(" << job->account().data() << u", " << job->url().toDisplayString() << u", " << job->_verb;
if (auto reply = job->_reply) {
debug << u", Original-Request-ID: " << reply->request().rawHeader("Original-Request-ID") << u", X-Request-ID: "
<< reply->request().rawHeader("X-Request-ID");
const auto errorString = reply->rawHeader(QByteArrayLiteral("OC-ErrorString"));
if (!errorString.isEmpty()) {
debug << u", Error:" << errorString;
}
if (reply->error() != QNetworkReply::NoError) {
debug << u", NetworkError: " << reply->errorString();
}
}
if (job->_timedout) {
debug << u", timedout";
}
debug << u")";
return debug.maybeSpace();
}
+273
View File
@@ -0,0 +1,273 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "accountfwd.h"
#include "jobqueue.h"
#include "common/asserts.h"
#include "qsferasynclib.h"
#include <QDateTime>
#include <QElapsedTimer>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QPointer>
#include <QTimer>
#include <QUrlQuery>
#include <chrono>
#include <optional>
class QUrl;
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::AbstractNetworkJob *job);
namespace OCC {
using HeaderMap = QMap<QByteArray, QByteArray>;
/**
* @brief The AbstractNetworkJob class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT AbstractNetworkJob : public QObject
{
Q_OBJECT
public:
explicit AbstractNetworkJob(AccountPtr account, const QUrl &baseUrl, const QString &path, QObject *parent = nullptr);
~AbstractNetworkJob() override;
virtual void start();
AccountPtr account() const { return _account; }
QString path() const { return _path; }
/*
* A base Url, for most of the jobs this will be the WebDAV entry point.
*/
QUrl baseUrl() const;
/*
* The absolute url: baseUrl() + path() + query()
*/
QUrl url() const;
QNetworkReply *reply() const;
void setForceIgnoreCredentialFailure(bool ignore);
bool ignoreCredentialFailure() const;
QByteArray responseTimestamp() const;
QDateTime responseQTimeStamp() const;
int httpStatusCode() const;
/* Content of the X-Request-ID header. (Only set after the request is sent) */
QByteArray requestId();
auto timeoutSec() const { return _timeout; }
bool timedOut() const { return _timedout; }
bool aborted() const { return _aborted; }
void setPriority(QNetworkRequest::Priority priority);
QNetworkRequest::Priority priority() const;
/** Returns an error message, if any. */
QString errorString() const;
/** Like errorString, but also checking the reply body for information.
*
* Specifically, sometimes xml bodies have extra error information.
* This function reads the body of the reply and parses out the
* error information, if possible.
*
* \a body is optinally filled with the reply body.
*
* Warning: Needs to call reply()->readAll().
*/
QString errorStringParsingBody(QByteArray *body = nullptr);
/** Make a new request */
void retry();
/** Abort the job due to an error */
void abort();
/** static variable the HTTP timeout. If set to 0, the default will be used
*/
static std::chrono::milliseconds httpTimeout;
/**
* The default 5 minutes timeout if none is specified by the config.
* Qt's default would be 30s.
*/
static constexpr std::chrono::seconds DefaultHttpTimeout{std::chrono::minutes(5)};
/** whether or noth this job should be restarted after authentication */
bool isAuthenticationJob() const;
void setAuthenticationJob(bool b);
/** How many times was that job retried */
int retryCount() const { return _retryCount; }
virtual bool needsRetry() const;
void setTimeout(const std::chrono::milliseconds sec);
/**
* Configure whether to store replies in a cache configured in the corresponding network access manager (if one is set there).
* By default, replies are not stored in the cache. Qt normally would store all eligible resources (as configured by server policies) in the cache.
* See also \ref setCacheLoadControl
* @param storeInCache whether to store replies in the cache
*/
void setStoreInCache(bool storeInCache);
/**
* Configure whether to load data from the cache.
* We inherit Qt's default behavior (which in Qt 5.15 and 6.4 is to prefer a network response but provide a cached value if the server permits it in its
* cache policies when the server cannot be reached).
* See also \ref setStoreInCache
* @param cacheLoadControl a policy suitable for the current job
*/
void setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl);
/**
*
* @return the body of the the request, if set
*/
QPointer<QIODevice> body() const;
Q_SIGNALS:
/** Emitted on network error.
*
* \a reply is never null
*/
void networkError(QNetworkReply *reply);
/**
* The job is done
*/
void aboutToFinishSignal(QPrivateSignal);
void finishedSignal(QPrivateSignal);
protected:
/** Initiate a network request, returning a QNetworkReply.
*
* Calls setReply() and setupConnections() on it.
*
* Takes ownership of the requestBody (to allow redirects).
*/
void sendRequest(const QByteArray &verb, const QNetworkRequest &req = QNetworkRequest(), std::unique_ptr<QIODevice> &&requestBody = {});
/** Can be used by derived classes to set up the network reply.
*
* Particularly useful when the request is redirected and reply()
* changes. For things like setting up additional signal connections
* on the new reply.
*/
virtual void newReplyHook(QNetworkReply *) {}
/** Called at the end of QNetworkReply::finished processing.
*/
virtual void finished() = 0;
QByteArray _responseTimestamp;
QString replyStatusString();
/*
* The url query appended to the url.
* The query will not be set as part of the body.
* The query must be fully encoded.
*/
void setQuery(const QUrlQuery &query);
QUrlQuery query() const;
AccountPtr _account;
private:
/** Makes this job drive a pre-made QNetworkReply
*
* This reply cannot have a QIODevice request body because we can't get
* at it and thus not resend it in case of redirects.
*/
void adoptRequest(QPointer<QNetworkReply> reply);
void slotFinished();
const QUrl _baseUrl;
const QString _path;
QUrlQuery _query;
std::chrono::milliseconds _timeout = httpTimeout;
bool _timedout = false; // set to true when the timeout slot is received
bool _aborted = false;
bool _finished = false;
bool _forceIgnoreCredentialFailure = false;
QNetworkRequest _request;
QByteArray _verb;
QPointer<QNetworkReply> _reply; // (QPointer because the NetworkManager may be destroyed before the jobs at exit)
// Set by the xyzRequest() functions and needed to be able to redirect
// requests, should it be required.
//
// Reparented to the currently running QNetworkReply.
QPointer<QIODevice> _requestBody;
bool _isAuthenticationJob = false;
int _retryCount = 0;
// by default, we don't intend to store responses in the cache (if one is set in the account's access manager)
bool _storeInCache = false;
// we use Qt's default cache load behavior unless the user explicitly requests a different behavior
std::optional<QNetworkRequest::CacheLoadControl> _cacheLoadControl = std::nullopt;
QNetworkRequest::Priority _priority = QNetworkRequest::NormalPriority;
friend QDebug(::operator<<)(QDebug debug, const AbstractNetworkJob *job);
};
/** Gets the SabreDAV-style error message from an error response.
*
* This assumes the response is XML with a 'error' tag that has a
* 'message' tag that contains the data to extract.
*
* Returns a null string if no message was found.
*/
QString QSFERA_SYNC_EXPORT extractErrorMessage(const QByteArray &errorResponse);
/** Builds a error message based on the error and the reply body. */
QString QSFERA_SYNC_EXPORT errorMessage(const QString &baseError, const QByteArray &body);
/** Nicer errorString() for QNetworkReply
*
* By default QNetworkReply::errorString() often produces messages like
* "Error downloading <url> - server replied: <reason>"
* but the "downloading" part invariably confuses people since the
* error might very well have been produced by a PUT request.
*
* This function produces clearer error messages for HTTP errors.
*/
QString QSFERA_SYNC_EXPORT networkReplyErrorString(const QNetworkReply &reply);
} // namespace OCC
+145
View File
@@ -0,0 +1,145 @@
/*
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include <QAuthenticator>
#include <QNetworkCookie>
#include <QUuid>
#include "accessmanager.h"
#include "common/utility.h"
#include "cookiejar.h"
#include "httplogger.h"
#include <algorithm>
namespace OCC {
Q_LOGGING_CATEGORY(lcAccessManager, "sync.accessmanager", QtInfoMsg)
AccessManager::AccessManager(QObject *parent)
: QNetworkAccessManager(parent)
{
setCookieJar(new CookieJar);
connect(this, &AccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList<QSslError> &errors) {
auto filtered = errors;
filtered.erase(std::remove_if(
filtered.begin(), filtered.end(), [this](const QSslError &e) {
return !_customTrustedCaCertificates.contains(e.certificate());
}),
filtered.end());
reply->ignoreSslErrors(filtered);
});
}
QByteArray AccessManager::generateRequestId()
{
return QUuid::createUuid().toByteArray(QUuid::WithoutBraces);
}
QNetworkReply *AccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
{
QNetworkRequest newRequest(request);
newRequest.setRawHeader(QByteArrayLiteral("User-Agent"), Utility::userAgentString());
// Some firewalls reject requests that have a "User-Agent" but no "Accept" header
newRequest.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("*/*"));
// Set the language, so messages from the server are localised correctly.
newRequest.setRawHeader("Accept-Language", QLocale().name().toUtf8());
// we don't follow redirects, if we receive one the ConnectionValidor is triggered
// -> default to manual redirection
if (newRequest.attribute(QNetworkRequest::RedirectPolicyAttribute).isNull()) {
newRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
}
QByteArray verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
// For PROPFIND (assumed to be a WebDAV op), set xml/utf8 as content type/encoding
// This needs extension
if (verb == QByteArrayLiteral("PROPFIND")) {
newRequest.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("text/xml; charset=utf-8"));
}
// Generate a new request id
const QByteArray requestId = generateRequestId();
newRequest.setRawHeader(QByteArrayLiteral("X-Request-ID"), requestId);
const auto originalIdKey = QByteArrayLiteral("Original-Request-ID");
if (!newRequest.hasRawHeader(originalIdKey)) {
newRequest.setRawHeader(originalIdKey, requestId);
}
static const bool http2DisabledEnv = qEnvironmentVariableIntValue("QSFERA_HTTP2_DISABLED") == 1;
if (http2DisabledEnv) {
newRequest.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
}
// allow http pipelining
newRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
auto sslConfiguration = newRequest.sslConfiguration();
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionTickets, false);
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionSharing, false);
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionPersistence, false);
if (!_customTrustedCaCertificates.isEmpty()) {
// for some reason, passing an empty list causes the default chain to be removed
// this behavior does not match the documentation
sslConfiguration.addCaCertificates({ _customTrustedCaCertificates.begin(), _customTrustedCaCertificates.end() });
}
newRequest.setSslConfiguration(sslConfiguration);
const auto reply = QNetworkAccessManager::createRequest(op, newRequest, outgoingData);
HttpLogger::logRequest(reply, op, outgoingData);
return reply;
}
QSet<QSslCertificate> AccessManager::customTrustedCaCertificates()
{
return _customTrustedCaCertificates;
}
void AccessManager::setCustomTrustedCaCertificates(const QSet<QSslCertificate> &certificates)
{
_customTrustedCaCertificates = certificates;
// we have to terminate the existing (cached) connection to make the access manager re-evaluate the certificate sent by the server
clearConnectionCache();
}
void AccessManager::addCustomTrustedCaCertificates(const QList<QSslCertificate> &certificates)
{
_customTrustedCaCertificates.unite({ certificates.begin(), certificates.end() });
// we have to terminate the existing (cached) connection to make the access manager re-evaluate the certificate sent by the server
clearConnectionCache();
}
CookieJar *AccessManager::openCloudCookieJar() const
{
auto jar = qobject_cast<CookieJar *>(cookieJar());
Q_ASSERT(jar);
return jar;
}
QList<QSslError> AccessManager::filterSslErrors(const QList<QSslError> &errors) const
{
auto filtered = errors;
filtered.erase(std::remove_if(filtered.begin(), filtered.end(),
[this](const QSslError &e) { return e.certificate().isNull() || _customTrustedCaCertificates.contains(e.certificate()); }),
filtered.end());
return filtered;
}
} // namespace OCC
+67
View File
@@ -0,0 +1,67 @@
/*
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef MIRALL_ACCESS_MANAGER_H
#define MIRALL_ACCESS_MANAGER_H
#include "qsferasynclib.h"
#include <QNetworkAccessManager>
class QByteArray;
class QUrl;
namespace OCC {
class CookieJar;
/**
* @brief The AccessManager class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT AccessManager : public QNetworkAccessManager
{
Q_OBJECT
public:
static QByteArray generateRequestId();
AccessManager(QObject *parent = nullptr);
QSet<QSslCertificate> customTrustedCaCertificates();
/***
* Warning calling those will break running network jobs
*/
void setCustomTrustedCaCertificates(const QSet<QSslCertificate> &certificates);
/***
* Warning calling those will break running network jobs
*/
void addCustomTrustedCaCertificates(const QList<QSslCertificate> &certificates);
CookieJar *openCloudCookieJar() const;
/***
* Remove all errors for already accepted certificates
*/
QList<QSslError> filterSslErrors(const QList<QSslError> &errors) const;
protected:
QNetworkReply *createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override;
private:
QSet<QSslCertificate> _customTrustedCaCertificates;
};
} // namespace OCC
#endif
+377
View File
@@ -0,0 +1,377 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "account.h"
#include "accessmanager.h"
#include "capabilities.h"
#include "common/asserts.h"
#include "cookiejar.h"
#include "creds/abstractcredentials.h"
#include "creds/credentialmanager.h"
#include "graphapi/spacesmanager.h"
#include "networkjobs.h"
#include "networkjobs/resources.h"
#include "theme.h"
#include <QAuthenticator>
#include <QDir>
#include <QLoggingCategory>
#include <QNetworkAccessManager>
#include <QNetworkCookieJar>
#include <QNetworkDiskCache>
#include <QSslKey>
#include <QStandardPaths>
namespace OCC {
Q_LOGGING_CATEGORY(lcAccount, "sync.account", QtInfoMsg)
QString Account::_customCommonCacheDirectory = {};
void Account::setCommonCacheDirectory(const QString &directory)
{
_customCommonCacheDirectory = directory;
}
QString Account::commonCacheDirectory()
{
if (_customCommonCacheDirectory.isEmpty()) {
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
}
return _customCommonCacheDirectory;
}
Account::Account(const QUuid &uuid, QObject *parent)
: QObject(parent)
, _uuid(uuid)
, _capabilities({}, {})
, _jobQueue(this)
, _queueGuard(&_jobQueue)
, _credentialManager(new CredentialManager(this))
, _spacesManager(new GraphApi::SpacesManager(this))
{
qRegisterMetaType<AccountPtr>("AccountPtr");
_cacheDirectory = QStringLiteral("%1/accounts/%2").arg(commonCacheDirectory(), _uuid.toString(QUuid::WithoutBraces));
QDir().mkpath(_cacheDirectory);
// we need to make sure the directory we pass to the resources cache exists
const QString resourcesCacheDir = QStringLiteral("%1/resources/").arg(_cacheDirectory);
QDir().mkpath(resourcesCacheDir);
_resourcesCache = new ResourcesCache(resourcesCacheDir, this);
}
AccountPtr Account::create(const QUuid &uuid)
{
AccountPtr acc = AccountPtr(new Account(uuid));
acc->setSharedThis(acc);
return acc;
}
Account::~Account()
{
}
void Account::setSharedThis(AccountPtr sharedThis)
{
_sharedThis = sharedThis.toWeakRef();
}
CredentialManager *Account::credentialManager() const
{
return _credentialManager;
}
GraphApi::SpacesManager *Account::spacesManager() const
{
return _spacesManager;
}
QUuid Account::uuid() const
{
return _uuid;
}
AccountPtr Account::sharedFromThis()
{
return _sharedThis.toStrongRef();
}
QIcon Account::avatar() const
{
return _avatarImg;
}
void Account::setAvatar(const QIcon &img)
{
_avatarImg = img;
Q_EMIT avatarChanged();
}
bool Account::hasAvatar() const
{
return !_avatarImg.isNull();
}
QString Account::displayNameWithHost() const
{
QString user = davDisplayName();
QString host = _url.host();
const int port = url().port();
if (port > 0 && port != 80 && port != 443) {
host += QStringLiteral(":%1").arg(QString::number(port));
}
return tr("%1@%2").arg(user, host);
}
QString Account::initials() const
{
QString out;
for (const auto &p : davDisplayName().split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
out.append(p.first(1));
}
return out;
}
QGradient::Preset Account::avatarGradient() const
{
return static_cast<QGradient::Preset>(qHash(displayNameWithHost()) % QGradient::NumPresets + 1);
}
QString Account::davDisplayName() const
{
return _displayName;
}
void Account::setDavDisplayName(const QString &newDisplayName)
{
if (_displayName != newDisplayName) {
_displayName = newDisplayName;
Q_EMIT displayNameChanged();
}
}
AbstractCredentials *Account::credentials() const
{
return _credentials.data();
}
void Account::setCredentials(AbstractCredentials *cred)
{
// set active credential manager
QNetworkCookieJar *jar = nullptr;
if (_am) {
jar = _am->cookieJar();
jar->setParent(nullptr);
_am->deleteLater();
}
// The order for these two is important! Reading the credential's
// settings accesses the account as well as account->_credentials,
_credentials.reset(cred);
cred->setAccount(this);
_am = _credentials->createAM();
// the network access manager takes ownership when setCache is called, so we have to reinitialize it every time we reset the manager
_networkCache = new QNetworkDiskCache(this);
const QString networkCacheLocation = (QStringLiteral("%1/network/").arg(_cacheDirectory));
qCDebug(lcAccount) << u"Cache location for account" << this << u"set to" << networkCacheLocation;
_networkCache->setCacheDirectory(networkCacheLocation);
_am->setCache(_networkCache);
if (jar) {
_am->setCookieJar(jar);
}
connect(_credentials.data(), &AbstractCredentials::fetched, this, [this] {
Q_EMIT credentialsFetched();
_queueGuard.unblock();
});
connect(_credentials.data(), &AbstractCredentials::authenticationStarted, this, [this] {
_queueGuard.block();
});
connect(_credentials.data(), &AbstractCredentials::authenticationFailed, this, [this] { _queueGuard.clear(); });
}
/**
* clear all cookies. (Session cookies or not)
*/
void Account::clearCookieJar()
{
qCInfo(lcAccount) << u"Clearing cookies";
_am->cookieJar()->deleteLater();
_am->setCookieJar(new CookieJar);
}
AccessManager *Account::accessManager()
{
return _am.data();
}
QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
{
Q_ASSERT(verb.isUpper());
req.setUrl(url);
if (verb == "HEAD" && !data) {
return _am->head(req);
} else if (verb == "GET" && !data) {
return _am->get(req);
} else if (verb == "POST") {
return _am->post(req, data);
} else if (verb == "PUT") {
return _am->put(req, data);
} else if (verb == "DELETE" && !data) {
return _am->deleteResource(req);
}
return _am->sendCustomRequest(req, verb, data);
}
void Account::setApprovedCerts(const QList<QSslCertificate> &certs)
{
_approvedCerts = { certs.begin(), certs.end() };
_am->setCustomTrustedCaCertificates(_approvedCerts);
}
void Account::addApprovedCerts(const QSet<QSslCertificate> &certs)
{
_approvedCerts.unite(certs);
_am->setCustomTrustedCaCertificates(_approvedCerts);
Q_EMIT wantsAccountSaved(this);
}
void Account::setUrl(const QUrl &url)
{
if (_url != url) {
_url = url;
Q_EMIT urlChanged();
}
}
QUrl Account::url() const
{
return _url;
}
QString Account::hostName() const
{
return _url.host();
}
JobQueue *Account::jobQueue()
{
return &_jobQueue;
}
void Account::clearAMCache()
{
_am->clearAccessCache();
}
const Capabilities &Account::capabilities() const
{
Q_ASSERT(hasCapabilities());
return _capabilities;
}
bool Account::hasCapabilities() const
{
return _capabilities.isValid();
}
void Account::setCapabilities(const Capabilities &caps)
{
if (_capabilities != caps) {
const bool versionChanged =
caps.status().legacyVersion != _capabilities.status().legacyVersion || caps.status().productversion != _capabilities.status().productversion;
_capabilities = caps;
Q_EMIT capabilitiesChanged();
if (versionChanged) {
Q_EMIT serverVersionChanged();
}
}
}
Account::ServerSupportLevel Account::serverSupportLevel() const
{
if (!hasCapabilities()) {
// not detected yet, assume it is fine.
return ServerSupportLevel::Supported;
}
// A compatible server exposes a product version in its status capabilities.
if (!capabilities().status().productversion.isEmpty()) {
return ServerSupportLevel::Supported;
}
return ServerSupportLevel::Unsupported;
}
bool Account::isHttp2Supported() const
{
return _http2Supported;
}
void Account::setHttp2Supported(bool value)
{
_http2Supported = value;
}
QString Account::defaultSyncRoot() const
{
Q_ASSERT(!_defaultSyncRoot.isEmpty());
return _defaultSyncRoot;
}
bool Account::hasDefaultSyncRoot() const
{
return !_defaultSyncRoot.isEmpty();
}
void Account::setDefaultSyncRoot(const QString &syncRoot)
{
Q_ASSERT(_defaultSyncRoot.isEmpty());
if (!syncRoot.isEmpty()) {
_defaultSyncRoot = syncRoot;
}
}
void Account::setAppProvider(AppProvider &&p)
{
_appProvider = std::move(p);
}
const AppProvider &Account::appProvider() const
{
return _appProvider;
}
void Account::invalidCredentialsEncountered()
{
Q_EMIT invalidCredentials(Account::QPrivateSignal());
}
ResourcesCache *Account::resourcesCache() const
{
return _resourcesCache;
}
} // namespace OCC
QDebug operator<<(QDebug debug, const OCC::Account *acc)
{
QDebugStateSaver saver(debug);
debug.setAutoInsertSpaces(false);
debug << u"OCC::Account(" << acc->displayNameWithHost() << u")";
return debug.maybeSpace();
}
+259
View File
@@ -0,0 +1,259 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef SERVERCONNECTION_H
#define SERVERCONNECTION_H
#include "common/utility.h"
#include "appprovider.h"
#include "capabilities.h"
#include "jobqueue.h"
#include "resources/resources.h"
#include <QByteArray>
#include <QGradient>
#include <QNetworkAccessManager>
#include <QNetworkCookie>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QPixmap>
#include <QSharedPointer>
#include <QSslCertificate>
#include <QSslCipher>
#include <QSslConfiguration>
#include <QSslError>
#include <QSslSocket>
#include <QUrl>
#include <QUuid>
#include <QtQmlIntegration/QtQmlIntegration>
class QSettings;
class QNetworkReply;
class QUrl;
class AccessManager;
namespace OCC {
class CredentialManager;
class AbstractCredentials;
class Account;
typedef QSharedPointer<Account> AccountPtr;
class AccessManager;
class SimpleNetworkJob;
namespace GraphApi {
class SpacesManager;
}
class ResourcesCache;
class QSFERA_SYNC_EXPORT Account : public QObject
{
Q_OBJECT
Q_PROPERTY(QUuid uid READ uuid CONSTANT)
Q_PROPERTY(QString davDisplayName READ davDisplayName NOTIFY displayNameChanged)
Q_PROPERTY(QString displayNameWithHost READ displayNameWithHost NOTIFY displayNameChanged)
Q_PROPERTY(QString initials READ initials NOTIFY displayNameChanged)
Q_PROPERTY(QString hostName READ hostName NOTIFY urlChanged)
Q_PROPERTY(bool hasAvatar READ hasAvatar NOTIFY avatarChanged)
Q_PROPERTY(QGradient::Preset avatarGradient READ avatarGradient NOTIFY displayNameChanged)
Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
QML_ELEMENT
QML_UNCREATABLE("Only created in the C++ code")
public:
/**
* Set a custom directory which all accounts created after this call will share to store their cached files in.
*/
static void setCommonCacheDirectory(const QString &directory);
static QString commonCacheDirectory();
static AccountPtr create(const QUuid &uuid);
~Account() override;
AccountPtr sharedFromThis();
/***
* The default folder containing all spaces.
* This function will assert if the sync root is empty.
*/
QString defaultSyncRoot() const;
/***
* Whether we have defaultSyncRoot defined.
*/
bool hasDefaultSyncRoot() const;
/***
* Set defaultSyncRoot and creates the path on the filesystem.
* Setting an empty string will have no effect.
*/
void setDefaultSyncRoot(const QString &syncRoot);
QString davDisplayName() const;
void setDavDisplayName(const QString &newDisplayName);
QIcon avatar() const;
void setAvatar(const QIcon &img);
bool hasAvatar() const;
/// The name of the account as shown in the toolbar
QString displayNameWithHost() const;
QString initials() const;
QGradient::Preset avatarGradient() const;
/** Server url of the account */
void setUrl(const QUrl &url);
QUrl url() const;
QString hostName() const;
/** Holds the accounts credentials */
AbstractCredentials *credentials() const;
void setCredentials(AbstractCredentials *cred);
/** Create a network request on the account's QNAM.
*
* Network requests in AbstractNetworkJobs are created through
* this function. Other places should prefer to use jobs or
* sendRequest().
*/
QNetworkReply *sendRawRequest(const QByteArray &verb,
const QUrl &url,
QNetworkRequest req = QNetworkRequest(),
QIODevice *data = nullptr);
/** The certificates of the account */
QSet<QSslCertificate> approvedCerts() const { return _approvedCerts; }
/***
* Warning calling those will break running network jobs on the current access manager
*/
void setApprovedCerts(const QList<QSslCertificate> &certs);
/***
* Warning calling those will break running network jobs on the current access manager
*/
void addApprovedCerts(const QSet<QSslCertificate> &certs);
/** Access the server capabilities */
const Capabilities &capabilities() const;
void setCapabilities(const Capabilities &caps);
bool hasCapabilities() const;
void setAppProvider(AppProvider &&p);
const AppProvider &appProvider() const;
enum class ServerSupportLevel {
Supported,
Unknown,
Unsupported
};
Q_ENUMS(ServerSupportLevel)
ServerSupportLevel serverSupportLevel() const;
/** True when the server connection is using HTTP2 */
bool isHttp2Supported() const;
void setHttp2Supported(bool value);
void clearCookieJar();
AccessManager *accessManager();
JobQueue *jobQueue();
QUuid uuid() const;
CredentialManager *credentialManager() const;
GraphApi::SpacesManager *spacesManager() const;
/**
* We encountered an authentication error.
*/
void invalidCredentialsEncountered();
ResourcesCache *resourcesCache() const;
public Q_SLOTS:
/// Used when forgetting credentials
void clearAMCache();
Q_SIGNALS:
/// Triggered by invalidCredentialsEncountered()
// this signal is emited when a network job failed due to invalid credentials
void invalidCredentials(QPrivateSignal);
void credentialsFetched();
void credentialsAsked();
// e.g. when the approved SSL certificates changed
void wantsAccountSaved(Account *acc);
void serverVersionChanged();
void capabilitiesChanged();
void avatarChanged();
void displayNameChanged();
void unknownConnectionState();
void requestUrlUpdate(const QUrl &newUrl);
// the signal exists on the Account object as the Approvider itself can change during runtime
void appProviderErrorOccured(const QString &error);
void urlChanged();
private:
// directory all newly created accounts store their various caches in
static QString _customCommonCacheDirectory;
Account(const QUuid &uuid, QObject *parent = nullptr);
void setSharedThis(AccountPtr sharedThis);
QWeakPointer<Account> _sharedThis;
QUuid _uuid;
QString _displayName;
QString _defaultSyncRoot;
QIcon _avatarImg;
QUrl _url;
QString _cacheDirectory;
QSet<QSslCertificate> _approvedCerts;
Capabilities _capabilities;
QPointer<AccessManager> _am;
QPointer<QNetworkDiskCache> _networkCache = nullptr;
QPointer<ResourcesCache> _resourcesCache;
QScopedPointer<AbstractCredentials> _credentials;
bool _http2Supported = true;
JobQueue _jobQueue;
JobQueueGuard _queueGuard;
CredentialManager *_credentialManager;
AppProvider _appProvider;
GraphApi::SpacesManager *_spacesManager = nullptr;
friend class AccountManager;
};
}
Q_DECLARE_METATYPE(OCC::AccountPtr)
QDebug QSFERA_SYNC_EXPORT operator<<(QDebug debug, const OCC::Account *job);
#endif //SERVERCONNECTION_H
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef SERVERFWD_H
#define SERVERFWD_H
#include <QPointer>
#include <QSharedPointer>
namespace OCC {
class Account;
class AccountState;
using AccountPtr = QSharedPointer<Account>;
using AccountStatePtr = QPointer<AccountState>;
} // namespace OCC
#endif //SERVERFWD
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "appprovider.h"
#include "common/utility.h"
#include "libsync/account.h"
#include "libsync/networkjobs/jsonjob.h"
#include <QCoreApplication>
#include <QDesktopServices>
#include <QJsonArray>
#include <QMimeDatabase>
using namespace OCC;
Q_LOGGING_CATEGORY(lcAppProvider, "sync.appprovider", QtInfoMsg)
AppProvider::Provider::Provider(const QJsonObject &provider)
: mimeType(provider.value(QStringLiteral("mime_type")).toString())
, extension(provider.value(QStringLiteral("extension")).toString())
, name(provider.value(QStringLiteral("name")).toString())
, description(provider.value(QStringLiteral("description")).toString())
, icon(provider.value(QStringLiteral("icon")).toString())
, defaultApplication(provider.value(QStringLiteral("default_application")).toString())
, allowCreation(provider.value(QStringLiteral("allow_creation")).toBool())
{
}
bool AppProvider::Provider::isValid() const
{
return !mimeType.isEmpty();
}
AppProvider::AppProvider(const QJsonObject &apps)
{
const auto mimTypes = apps.value(QStringLiteral("mime-types")).toArray();
_providers.reserve(apps.size());
for (const auto &type : mimTypes) {
Provider p(type.toObject());
_providers.insert(p.mimeType, std::move(p));
}
}
const AppProvider::Provider &AppProvider::app(const QMimeType &mimeType) const
{
if (auto it = Utility::optionalFind(_providers, mimeType.name())) {
return it->value();
}
static const AppProvider::Provider nullProvider { {} };
return nullProvider;
}
const AppProvider::Provider &AppProvider::app(const QString &localPath) const
{
QMimeDatabase db;
auto mimeType = db.mimeTypeForFile(localPath);
return app(mimeType);
}
bool AppProvider::open(const AccountPtr &account, const QString &localPath, const QByteArray &fileId) const
{
const auto &a = app(localPath);
if (a.isValid()) {
SimpleNetworkJob::UrlQuery query { { QStringLiteral("file_id"), QString::fromUtf8(fileId) } };
auto *job = new JsonJob(account, account->capabilities().appProviders().openWebUrl, {}, "POST", query);
QObject::connect(job, &JsonJob::finishedSignal, account.data(), [account, job, localPath] {
if (job->httpStatusCode() == 200) {
const auto url = QUrl(job->data().value(QStringLiteral("uri")).toString());
const auto result = QDesktopServices::openUrl(url);
qCDebug(lcAppProvider) << u"start browser" << url << result;
} else {
Q_EMIT account->appProviderErrorOccured(
QCoreApplication::translate("AppProvider", "Failed to open »%1« in web. Error: %2.").arg(localPath, job->reply()->errorString()));
}
});
job->start();
return true;
}
return false;
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "accountfwd.h"
#include "capabilities.h"
#include "qsferasynclib.h"
#include <QJsonObject>
#include <QMimeType>
#include <QObject>
#include <QUrl>
#include <QVariantMap>
namespace OCC {
class QSFERA_SYNC_EXPORT AppProvider
{
public:
struct QSFERA_SYNC_EXPORT Provider
{
// the server might provide multiple apps but no default
// for now we only support default apps
Provider() = default;
explicit Provider(const QJsonObject &provider);
QString mimeType;
QString extension;
QString name;
QString description;
QUrl icon;
QString defaultApplication;
bool allowCreation = false;
bool isValid() const;
};
explicit AppProvider(const QJsonObject &apps = {});
const Provider &app(const QMimeType &mimeType) const;
const Provider &app(const QString &localPath) const;
bool open(const AccountPtr &account, const QString &localPath, const QByteArray &fileId) const;
private:
QHash<QString, Provider> _providers;
};
}
+395
View File
@@ -0,0 +1,395 @@
/*
* Copyright (C) by Markus Goetz <markus@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "libsync/networkjobs/getfilejob.h"
#include "owncloudpropagator.h"
#include "propagateupload.h"
#include <QLoggingCategory>
#include <QTimer>
#include <QObject>
namespace OCC {
Q_LOGGING_CATEGORY(lcBandwidthManager, "sync.bandwidthmanager", QtInfoMsg)
// Because of the many layers of buffering inside Qt (and probably the OS and the network)
// we cannot lower this value much more. If we do, the estimated bw will be very high
// because the buffers fill fast while the actual network algorithms are not relevant yet.
static qint64 relativeLimitMeasuringTimerIntervalMsec = 1000 * 2;
// See also WritingState in http://code.woboq.org/qt5/qtbase/src/network/access/qhttpprotocolhandler.cpp.html#_ZN20QHttpProtocolHandler11sendRequestEv
// FIXME At some point:
// * Register device only after the QNR received its metaDataChanged() signal
// * Incorporate Qt buffer fill state (it's a negative absolute delta).
// * Incorporate SSL overhead (percentage)
// * For relative limiting, do less measuring and more delaying+giving quota
// * For relative limiting, smoothen measurements
BandwidthManager::BandwidthManager(OwncloudPropagator *p)
: QObject(p)
, _propagator(p)
, _relativeLimitCurrentMeasuredDevice(nullptr)
, _relativeUploadLimitProgressAtMeasuringRestart(0)
, _currentUploadLimit(0)
, _relativeLimitCurrentMeasuredJob(nullptr)
, _currentDownloadLimit(0)
{
// absolute uploads/downloads
QObject::connect(&_absoluteLimitTimer, &QTimer::timeout, this, &BandwidthManager::absoluteLimitTimerExpired);
_absoluteLimitTimer.setInterval(1000);
_absoluteLimitTimer.start();
// Relative uploads
QObject::connect(&_relativeUploadMeasuringTimer, &QTimer::timeout,
this, &BandwidthManager::relativeUploadMeasuringTimerExpired);
_relativeUploadMeasuringTimer.setInterval(relativeLimitMeasuringTimerIntervalMsec);
_relativeUploadMeasuringTimer.start();
_relativeUploadMeasuringTimer.setSingleShot(true); // will be restarted from the delay timer
QObject::connect(&_relativeUploadDelayTimer, &QTimer::timeout,
this, &BandwidthManager::relativeUploadDelayTimerExpired);
_relativeUploadDelayTimer.setSingleShot(true); // will be restarted from the measuring timer
// Relative downloads
QObject::connect(&_relativeDownloadMeasuringTimer, &QTimer::timeout,
this, &BandwidthManager::relativeDownloadMeasuringTimerExpired);
_relativeDownloadMeasuringTimer.setInterval(relativeLimitMeasuringTimerIntervalMsec);
_relativeDownloadMeasuringTimer.start();
_relativeDownloadMeasuringTimer.setSingleShot(true); // will be restarted from the delay timer
QObject::connect(&_relativeDownloadDelayTimer, &QTimer::timeout,
this, &BandwidthManager::relativeDownloadDelayTimerExpired);
_relativeDownloadDelayTimer.setSingleShot(true); // will be restarted from the measuring timer
}
BandwidthManager::~BandwidthManager()
{
}
void BandwidthManager::registerUploadDevice(UploadDevice *p)
{
_absoluteUploadDeviceList.push_back(p);
_relativeUploadDeviceList.push_back(p);
QObject::connect(p, &QObject::destroyed, this, &BandwidthManager::unregisterUploadDevice);
if (usingAbsoluteUploadLimit()) {
p->setBandwidthLimited(true);
p->setChoked(false);
} else if (usingRelativeUploadLimit()) {
p->setBandwidthLimited(true);
p->setChoked(true);
} else {
p->setBandwidthLimited(false);
p->setChoked(false);
}
}
void BandwidthManager::unregisterUploadDevice(QObject *o)
{
auto p = reinterpret_cast<UploadDevice *>(o); // note, we might already be in the ~QObject
_absoluteUploadDeviceList.remove(p);
_relativeUploadDeviceList.remove(p);
if (p == _relativeLimitCurrentMeasuredDevice) {
_relativeLimitCurrentMeasuredDevice = nullptr;
_relativeUploadLimitProgressAtMeasuringRestart = 0;
}
}
void BandwidthManager::registerDownloadJob(GETFileJob *j)
{
_downloadJobList.push_back(j);
connect(j, &GETFileJob::aboutToFinishSignal, this, [j, this] {
unregisterDownloadJob(j);
});
if (usingAbsoluteDownloadLimit()) {
j->setBandwidthLimited(true);
j->setChoked(false);
} else if (usingRelativeDownloadLimit()) {
j->setBandwidthLimited(true);
j->setChoked(true);
} else {
j->setBandwidthLimited(false);
j->setChoked(false);
}
}
void BandwidthManager::unregisterDownloadJob(GETFileJob *j)
{
j->setChoked(false);
j->setBandwidthLimited(false);
_downloadJobList.remove(j);
if (_relativeLimitCurrentMeasuredJob == j) {
_relativeLimitCurrentMeasuredJob = nullptr;
_relativeDownloadLimitProgressAtMeasuringRestart = 0;
}
}
void BandwidthManager::relativeUploadMeasuringTimerExpired()
{
if (!usingRelativeUploadLimit() || _relativeUploadDeviceList.empty()) {
// Not in this limiting mode, just wait 1 sec to continue the cycle
_relativeUploadDelayTimer.setInterval(1000);
_relativeUploadDelayTimer.start();
return;
}
if (_relativeLimitCurrentMeasuredDevice == nullptr) {
qCDebug(lcBandwidthManager) << u"No device set, just waiting 1 sec";
_relativeUploadDelayTimer.setInterval(1000);
_relativeUploadDelayTimer.start();
return;
}
qCDebug(lcBandwidthManager) << _relativeUploadDeviceList.size() << u"Starting Delay";
qint64 relativeLimitProgressMeasured = (_relativeLimitCurrentMeasuredDevice->_readWithProgress
+ _relativeLimitCurrentMeasuredDevice->_read)
/ 2;
qint64 relativeLimitProgressDifference = relativeLimitProgressMeasured - _relativeUploadLimitProgressAtMeasuringRestart;
qCDebug(lcBandwidthManager) << _relativeUploadLimitProgressAtMeasuringRestart
<< relativeLimitProgressMeasured << relativeLimitProgressDifference;
qint64 speedkBPerSec = (relativeLimitProgressDifference / relativeLimitMeasuringTimerIntervalMsec * 1000.0) / 1024.0;
qCDebug(lcBandwidthManager) << relativeLimitProgressDifference / 1024 << u"kB =>" << speedkBPerSec << u"kB/sec on full speed ("
<< _relativeLimitCurrentMeasuredDevice->_readWithProgress << _relativeLimitCurrentMeasuredDevice->_read
<< qAbs(_relativeLimitCurrentMeasuredDevice->_readWithProgress - _relativeLimitCurrentMeasuredDevice->_read) << u")";
qint64 uploadLimitPercent = -_currentUploadLimit;
// don't use too extreme values
uploadLimitPercent = qMin(uploadLimitPercent, qint64(90));
uploadLimitPercent = qMax(qint64(10), uploadLimitPercent);
qint64 wholeTimeMsec = (100.0 / uploadLimitPercent) * relativeLimitMeasuringTimerIntervalMsec;
qint64 waitTimeMsec = wholeTimeMsec - relativeLimitMeasuringTimerIntervalMsec;
qint64 realWaitTimeMsec = waitTimeMsec + wholeTimeMsec;
qCDebug(lcBandwidthManager) << waitTimeMsec << u" - " << realWaitTimeMsec << u" msec for " << uploadLimitPercent << u"%";
// We want to wait twice as long since we want to give all
// devices the same quota we used now since we don't want
// any upload to timeout
_relativeUploadDelayTimer.setInterval(realWaitTimeMsec);
_relativeUploadDelayTimer.start();
auto deviceCount = _relativeUploadDeviceList.size();
qint64 quotaPerDevice = relativeLimitProgressDifference * (uploadLimitPercent / 100.0) / deviceCount + 1.0;
for (auto *ud : _relativeUploadDeviceList) {
ud->setBandwidthLimited(true);
ud->setChoked(false);
ud->giveBandwidthQuota(quotaPerDevice);
qCDebug(lcBandwidthManager) << u"Gave" << quotaPerDevice / 1024.0 << u"kB to" << ud;
}
_relativeLimitCurrentMeasuredDevice = nullptr;
}
void BandwidthManager::relativeUploadDelayTimerExpired()
{
// Switch to measuring state
_relativeUploadMeasuringTimer.start(); // always start to continue the cycle
if (!usingRelativeUploadLimit()) {
return; // oh, not actually needed
}
if (_relativeUploadDeviceList.empty()) {
return;
}
qCDebug(lcBandwidthManager) << _relativeUploadDeviceList.size() << u"Starting measuring";
// Take first device and then append it again (= we round robin all devices)
_relativeLimitCurrentMeasuredDevice = _relativeUploadDeviceList.front();
_relativeUploadDeviceList.pop_front();
_relativeUploadDeviceList.push_back(_relativeLimitCurrentMeasuredDevice);
_relativeUploadLimitProgressAtMeasuringRestart = (_relativeLimitCurrentMeasuredDevice->_readWithProgress
+ _relativeLimitCurrentMeasuredDevice->_read)
/ 2;
_relativeLimitCurrentMeasuredDevice->setBandwidthLimited(false);
_relativeLimitCurrentMeasuredDevice->setChoked(false);
// choke all other UploadDevices
for (auto *ud : std::as_const(_relativeUploadDeviceList)) {
if (ud != _relativeLimitCurrentMeasuredDevice) {
ud->setBandwidthLimited(true);
ud->setChoked(true);
}
}
// now we're in measuring state
}
qint64 BandwidthManager::currentUploadLimit() const
{
return _currentUploadLimit;
}
void BandwidthManager::setCurrentUploadLimit(qint64 newUploadLimit)
{
if (newUploadLimit != _currentUploadLimit) {
qCInfo(lcBandwidthManager) << u"Upload Bandwidth limit changed" << _currentUploadLimit << newUploadLimit;
_currentUploadLimit = newUploadLimit;
for (auto *ud : _relativeUploadDeviceList) {
if (newUploadLimit == 0) {
ud->setBandwidthLimited(false);
ud->setChoked(false);
} else if (newUploadLimit > 0) {
ud->setBandwidthLimited(true);
ud->setChoked(false);
} else if (newUploadLimit < 0) {
ud->setBandwidthLimited(true);
ud->setChoked(true);
}
}
}
}
// for downloads:
void BandwidthManager::relativeDownloadMeasuringTimerExpired()
{
if (!usingRelativeDownloadLimit() || _downloadJobList.empty()) {
// Not in this limiting mode, just wait 1 sec to continue the cycle
_relativeDownloadDelayTimer.setInterval(1000);
_relativeDownloadDelayTimer.start();
return;
}
if (_relativeLimitCurrentMeasuredJob == nullptr) {
qCDebug(lcBandwidthManager) << u"No job set, just waiting 1 sec";
_relativeDownloadDelayTimer.setInterval(1000);
_relativeDownloadDelayTimer.start();
return;
}
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"Starting Delay";
qint64 relativeLimitProgressMeasured = _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
qint64 relativeLimitProgressDifference = relativeLimitProgressMeasured - _relativeDownloadLimitProgressAtMeasuringRestart;
qCDebug(lcBandwidthManager) << _relativeDownloadLimitProgressAtMeasuringRestart
<< relativeLimitProgressMeasured << relativeLimitProgressDifference;
qint64 speedkBPerSec = (relativeLimitProgressDifference / relativeLimitMeasuringTimerIntervalMsec * 1000.0) / 1024.0;
qCDebug(lcBandwidthManager) << relativeLimitProgressDifference / 1024 << u"kB =>" << speedkBPerSec << u"kB/sec on full speed ("
<< _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
qint64 downloadLimitPercent = -_currentDownloadLimit;
// don't use too extreme values
downloadLimitPercent = qMin(downloadLimitPercent, qint64(90));
downloadLimitPercent = qMax(qint64(10), downloadLimitPercent);
qint64 wholeTimeMsec = (100.0 / downloadLimitPercent) * relativeLimitMeasuringTimerIntervalMsec;
qint64 waitTimeMsec = wholeTimeMsec - relativeLimitMeasuringTimerIntervalMsec;
qint64 realWaitTimeMsec = waitTimeMsec + wholeTimeMsec;
qCDebug(lcBandwidthManager) << waitTimeMsec << u" - " << realWaitTimeMsec << u" msec for " << downloadLimitPercent << u"%";
// We want to wait twice as long since we want to give all
// devices the same quota we used now since we don't want
// any download to timeout
_relativeDownloadDelayTimer.setInterval(realWaitTimeMsec);
_relativeDownloadDelayTimer.start();
auto jobCount = _downloadJobList.size();
qint64 quota = relativeLimitProgressDifference * (downloadLimitPercent / 100.0);
if (quota > 20 * 1024) {
qCInfo(lcBandwidthManager) << u"ADJUSTING QUOTA FROM " << quota << u" TO " << quota - 20 * 1024;
quota -= 20 * 1024;
}
qint64 quotaPerJob = quota / jobCount + 1.0;
for (auto *gfj : _downloadJobList) {
gfj->setBandwidthLimited(true);
gfj->setChoked(false);
gfj->giveBandwidthQuota(quotaPerJob);
qCDebug(lcBandwidthManager) << u"Gave" << quotaPerJob / 1024.0 << u"kB to" << gfj;
}
_relativeLimitCurrentMeasuredDevice = nullptr;
}
void BandwidthManager::relativeDownloadDelayTimerExpired()
{
// Switch to measuring state
_relativeDownloadMeasuringTimer.start(); // always start to continue the cycle
if (!usingRelativeDownloadLimit()) {
return; // oh, not actually needed
}
if (_downloadJobList.empty()) {
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"No jobs?";
return;
}
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"Starting measuring";
// Take first device and then append it again (= we round robin all devices)
_relativeLimitCurrentMeasuredJob = _downloadJobList.front();
_downloadJobList.pop_front();
_downloadJobList.push_back(_relativeLimitCurrentMeasuredJob);
_relativeDownloadLimitProgressAtMeasuringRestart = _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
_relativeLimitCurrentMeasuredJob->setBandwidthLimited(false);
_relativeLimitCurrentMeasuredJob->setChoked(false);
// choke all other download jobs
for (auto *gfj : _downloadJobList) {
if (gfj != _relativeLimitCurrentMeasuredJob) {
gfj->setBandwidthLimited(true);
gfj->setChoked(true);
}
}
// now we're in measuring state
}
qint64 BandwidthManager::currentDownloadLimit() const
{
return _currentDownloadLimit;
}
void BandwidthManager::setCurrentDownloadLimit(qint64 newDownloadLimit)
{
if (newDownloadLimit != _currentDownloadLimit) {
qCInfo(lcBandwidthManager) << u"Download Bandwidth limit changed" << _currentDownloadLimit << newDownloadLimit;
_currentDownloadLimit = newDownloadLimit;
for (auto *j : _downloadJobList) {
if (usingAbsoluteDownloadLimit()) {
j->setBandwidthLimited(true);
j->setChoked(false);
} else if (usingRelativeDownloadLimit()) {
j->setBandwidthLimited(true);
j->setChoked(true);
} else {
j->setBandwidthLimited(false);
j->setChoked(false);
}
}
}
}
// end downloads
void BandwidthManager::absoluteLimitTimerExpired()
{
if (usingAbsoluteUploadLimit() && !_absoluteUploadDeviceList.empty()) {
qint64 quotaPerDevice = _currentUploadLimit / _absoluteUploadDeviceList.size();
qCDebug(lcBandwidthManager) << quotaPerDevice << _absoluteUploadDeviceList.size() << _currentUploadLimit;
for (auto *device : std::as_const(_absoluteUploadDeviceList)) {
device->giveBandwidthQuota(quotaPerDevice);
qCDebug(lcBandwidthManager) << u"Gave " << quotaPerDevice / 1024.0 << u" kB to" << device;
}
}
if (usingAbsoluteDownloadLimit() && !_downloadJobList.empty()) {
qint64 quotaPerJob = _currentDownloadLimit / _downloadJobList.size();
qCDebug(lcBandwidthManager) << quotaPerJob << _downloadJobList.size() << _currentDownloadLimit;
for (auto *j : _downloadJobList) {
j->giveBandwidthQuota(quotaPerJob);
qCDebug(lcBandwidthManager) << u"Gave " << quotaPerJob / 1024.0 << u" kB to" << j;
}
}
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* Copyright (C) by Markus Goetz <markus@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef BANDWIDTHMANAGER_H
#define BANDWIDTHMANAGER_H
#include <QObject>
#include <QTimer>
#include <QIODevice>
#include <list>
namespace OCC {
class UploadDevice;
class GETFileJob;
class OwncloudPropagator;
/**
* @brief The BandwidthManager class
* @ingroup libsync
*/
class BandwidthManager : public QObject
{
Q_OBJECT
public:
BandwidthManager(OwncloudPropagator *p);
~BandwidthManager() override;
bool usingAbsoluteUploadLimit() { return _currentUploadLimit > 0; }
bool usingRelativeUploadLimit() { return _currentUploadLimit < 0; }
bool usingAbsoluteDownloadLimit() { return _currentDownloadLimit > 0; }
bool usingRelativeDownloadLimit() { return _currentDownloadLimit < 0; }
qint64 currentDownloadLimit() const;
void setCurrentDownloadLimit(qint64 newCurrentDownloadLimit);
qint64 currentUploadLimit() const;
void setCurrentUploadLimit(qint64 newCurrentUploadLimit);
public Q_SLOTS:
void registerUploadDevice(UploadDevice *);
void unregisterUploadDevice(QObject *);
void registerDownloadJob(GETFileJob *);
void unregisterDownloadJob(GETFileJob *);
void absoluteLimitTimerExpired();
void relativeUploadMeasuringTimerExpired();
void relativeUploadDelayTimerExpired();
void relativeDownloadMeasuringTimerExpired();
void relativeDownloadDelayTimerExpired();
private:
// FIXME this timer and this variable should be replaced
// by the propagator emitting the changed limit values to us as signal
OwncloudPropagator *_propagator;
// for absolute up/down bw limiting
QTimer _absoluteLimitTimer;
// FIXME merge these two lists
std::list<UploadDevice *> _absoluteUploadDeviceList;
std::list<UploadDevice *> _relativeUploadDeviceList;
QTimer _relativeUploadMeasuringTimer;
// for relative bw limiting, we need to wait this amount before measuring again
QTimer _relativeUploadDelayTimer;
// the device measured
UploadDevice *_relativeLimitCurrentMeasuredDevice;
// for measuring how much progress we made at start
qint64 _relativeUploadLimitProgressAtMeasuringRestart;
qint64 _currentUploadLimit;
std::list<GETFileJob *> _downloadJobList;
QTimer _relativeDownloadMeasuringTimer;
// for relative bw limiting, we need to wait this amount before measuring again
QTimer _relativeDownloadDelayTimer;
// the device measured
GETFileJob *_relativeLimitCurrentMeasuredJob;
// for measuring how much progress we made at start
qint64 _relativeDownloadLimitProgressAtMeasuringRestart;
qint64 _currentDownloadLimit;
};
}
#endif
+328
View File
@@ -0,0 +1,328 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "capabilities.h"
#include <QDebug>
#include <QVariantMap>
using namespace std::chrono;
using namespace Qt::Literals::StringLiterals;
namespace OCC {
Capabilities::Capabilities(const QUrl &url, const QVariantMap &capabilities)
: _capabilities(capabilities)
, _fileSharingCapabilities(_capabilities.value(QStringLiteral("files_sharing")).toMap())
, _fileSharingPublicCapabilities(_fileSharingCapabilities.value(QStringLiteral("public"), {}).toMap())
, _tusSupport(_capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("tus_support")).toMap())
, _spaces(_capabilities.value(QStringLiteral("spaces")).toMap())
, _status(_capabilities.value(QStringLiteral("core")).toMap().value(QStringLiteral("status")).toMap())
, _checkForUpdates(_capabilities.value("core"_L1).toMap().value("check_for_updates"_L1, true).toBool())
, _appProviders(AppProviders::findVersion(
url, _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("app_providers")).toList(), QVersionNumber({1, 1, 0})))
, _filesSharing(_fileSharingCapabilities)
, _migration(_capabilities.value(QStringLiteral("migration")).toMap())
{
}
QVariantMap Capabilities::raw() const
{
return _capabilities;
}
bool Capabilities::operator==(const Capabilities &other) const
{
return raw() == other.raw();
}
bool Capabilities::shareAPI() const
{
// This was later added so if it is not present just assume the API is enabled.
return _fileSharingCapabilities.value(QStringLiteral("api_enabled"), true).toBool();
}
bool Capabilities::sharePublicLink() const
{
// This was later added so if it is not present just assume that link sharing is enabled.
return shareAPI() && _fileSharingPublicCapabilities.value(QStringLiteral("enabled"), true).toBool();
}
bool Capabilities::sharePublicLinkAllowUpload() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("upload")).toBool();
}
bool Capabilities::sharePublicLinkSupportsUploadOnly() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("supports_upload_only")).toBool();
}
static bool getEnforcePasswordCapability(const QVariantMap &fileSharingPublicCapabilities, const QString &name)
{
auto value = fileSharingPublicCapabilities[QStringLiteral("password")].toMap()[QStringLiteral("enforced_for")].toMap()[name];
if (!value.isValid())
return fileSharingPublicCapabilities[QStringLiteral("password")].toMap()[QStringLiteral("enforced")].toBool();
return value.toBool();
}
bool Capabilities::sharePublicLinkEnforcePasswordForReadOnly() const
{
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("read_only"));
}
bool Capabilities::sharePublicLinkEnforcePasswordForReadWrite() const
{
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("read_write"));
}
bool Capabilities::sharePublicLinkEnforcePasswordForUploadOnly() const
{
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("upload_only"));
}
bool Capabilities::sharePublicLinkDefaultExpire() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("enabled")).toBool();
}
int Capabilities::sharePublicLinkDefaultExpireDateDays() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("days")).toInt();
}
bool Capabilities::sharePublicLinkEnforceExpireDate() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("enforced")).toBool();
}
bool Capabilities::sharePublicLinkMultiple() const
{
return _fileSharingPublicCapabilities.value(QStringLiteral("multiple")).toBool();
}
bool Capabilities::shareResharing() const
{
return _fileSharingCapabilities.value(QStringLiteral("resharing")).toBool();
}
int Capabilities::defaultPermissions() const
{
return _fileSharingCapabilities.value(QStringLiteral("default_permissions"), 1).toInt();
}
std::chrono::seconds Capabilities::remotePollInterval() const
{
return duration_cast<seconds>(milliseconds(_capabilities.value(QStringLiteral("core")).toMap().value(QStringLiteral("pollinterval")).toInt()));
}
bool Capabilities::notificationsAvailable() const
{
// We require the OCS style API in 9.x, can't deal with the REST one only found in 8.2
return _capabilities.contains(QStringLiteral("notifications")) && _capabilities.value(QStringLiteral("notifications")).toMap().contains(QStringLiteral("ocs-endpoints"));
}
bool Capabilities::isValid() const
{
return !_capabilities.isEmpty();
}
QList<CheckSums::Algorithm> Capabilities::supportedChecksumTypes() const
{
// TODO: compute once
QList<CheckSums::Algorithm> list;
const auto &supportedTypes = _capabilities.value(QStringLiteral("checksums")).toMap().value(QStringLiteral("supportedTypes")).toList();
for (const auto &t : supportedTypes) {
list.push_back(CheckSums::fromByteArray(t.toByteArray()));
}
return list;
}
CheckSums::Algorithm Capabilities::preferredUploadChecksumType() const
{
return CheckSums::fromByteArray(
_capabilities.value(QStringLiteral("checksums")).toMap().value(QStringLiteral("preferredUploadType"), QStringLiteral("SHA1")).toByteArray());
}
CheckSums::Algorithm Capabilities::uploadChecksumType() const
{
auto preferred = preferredUploadChecksumType();
if (preferred != CheckSums::Algorithm::PARSE_ERROR)
return preferred;
QList<CheckSums::Algorithm> supported = supportedChecksumTypes();
if (!supported.isEmpty())
return supported.first();
return CheckSums::Algorithm::PARSE_ERROR;
}
const Status &Capabilities::status() const
{
return _status;
}
bool Capabilities::checkForUpdates() const
{
return _checkForUpdates;
}
const TusSupport &Capabilities::tusSupport() const
{
return _tusSupport;
}
const SpaceSupport &Capabilities::spacesSupport() const
{
return _spaces;
}
bool Capabilities::privateLinkPropertyAvailable() const
{
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("privateLinks")).toBool();
}
bool Capabilities::privateLinkDetailsParamAvailable() const
{
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("privateLinksDetailsParam")).toBool();
}
QList<int> Capabilities::httpErrorCodesThatResetFailingChunkedUploads() const
{
QList<int> list;
const auto &errorCodes = _capabilities.value(QStringLiteral("dav")).toMap().value(QStringLiteral("httpErrorCodesThatResetFailingChunkedUploads")).toList();
for (const auto &t : errorCodes) {
list.push_back(t.toInt());
}
return list;
}
QString Capabilities::invalidFilenameRegex() const
{
return _capabilities[QStringLiteral("dav")].toMap()[QStringLiteral("invalidFilenameRegex")].toString();
}
bool Capabilities::uploadConflictFiles() const
{
return _capabilities[QStringLiteral("uploadConflictFiles")].toBool();
}
bool Capabilities::versioningEnabled() const
{
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("versioning")).toBool();
}
QStringList Capabilities::blacklistedFiles() const
{
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("blacklisted_files")).toStringList();
}
Status::Status(const QVariantMap &status)
{
legacyVersion = QVersionNumber::fromString(status.value(QStringLiteral("version")).toString());
legacyVersionString = status.value(QStringLiteral("versionstring")).toString();
edition = status.value(QStringLiteral("edition")).toString();
productname = status.value(QStringLiteral("productname")).toString();
product = status.value(QStringLiteral("product")).toString();
productversion = status.value(QStringLiteral("productversion")).toString();
}
QVersionNumber Status::version() const
{
return productversion.isEmpty() ? legacyVersion : QVersionNumber::fromString(productversion);
}
QString Status::versionString() const
{
return productversion.isEmpty() ? legacyVersionString : productversion;
}
TusSupport::TusSupport(const QVariantMap &tus_support)
{
if (tus_support.isEmpty()) {
return;
}
version = QVersionNumber::fromString(tus_support.value(QStringLiteral("version")).toString());
resumable = QVersionNumber::fromString(tus_support.value(QStringLiteral("resumable")).toString());
extensions = tus_support.value(QStringLiteral("extension")).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
max_chunk_size = tus_support.value(QStringLiteral("max_chunk_size")).value<quint64>();
http_method_override = tus_support.value(QStringLiteral("http_method_override")).toString();
}
bool TusSupport::isValid() const
{
return !version.isNull();
}
SpaceSupport::SpaceSupport(const QVariantMap &spaces_support)
{
if (spaces_support.isEmpty()) {
return;
}
enabled = spaces_support.value(QStringLiteral("enabled")).toBool();
version = QVersionNumber::fromString(spaces_support.value(QStringLiteral("version")).toString());
}
bool SpaceSupport::isValid() const
{
return !version.isNull();
}
Capabilities::AppProviders::AppProviders(const QUrl &baseUrl, const QVariantMap &appProviders)
: enabled(appProviders.value(QStringLiteral("enabled")).toBool())
, version(QVersionNumber::fromString(appProviders.value(QStringLiteral("version")).toString()))
, appsUrl(baseUrl.resolved(appProviders.value(QStringLiteral("apps_url")).toUrl()))
, openUrl(baseUrl.resolved(appProviders.value(QStringLiteral("open_url")).toUrl()))
, newUrl(baseUrl.resolved(appProviders.value(QStringLiteral("new_url")).toUrl()))
, openWebUrl(baseUrl.resolved(appProviders.value(QStringLiteral("open_web_url")).toUrl()))
{
}
const Capabilities::AppProviders &Capabilities::appProviders() const
{
return _appProviders;
}
Capabilities::AppProviders Capabilities::AppProviders::findVersion(const QUrl &baseUrl, const QVariantList &list, const QVersionNumber &v)
{
auto it = std::find_if(list.cbegin(), list.cend(), [&v](const auto &it) {
return QVersionNumber::fromString(it.toMap().value(QStringLiteral("version")).toString()) == v;
});
return it != list.cend() ? Capabilities::AppProviders{baseUrl, it->toMap()} : Capabilities::AppProviders{};
}
const FilesSharing &Capabilities::filesSharing() const
{
return _filesSharing;
}
FilesSharing::FilesSharing(const QVariantMap &filesSharing)
: sharing_roles(filesSharing.value(QStringLiteral("sharing_roles"), false).toBool())
{
}
OCC::Migration::Migration(const QVariantMap &data)
{
const auto spaces = data.value(QStringLiteral("space_migration")).toMap();
space_migration.enabled = spaces.value(QStringLiteral("enabled")).toBool();
space_migration.endpoint = spaces.value(QStringLiteral("endpoint")).toString();
}
const Migration &Capabilities::migration() const
{
return _migration;
}
} // namespace OCC
+301
View File
@@ -0,0 +1,301 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef CAPABILITIES_H
#define CAPABILITIES_H
#include "qsferasynclib.h"
#include "common/checksumalgorithms.h"
#include <QVariantMap>
#include <QStringList>
#include <QVersionNumber>
namespace OCC {
struct QSFERA_SYNC_EXPORT Status
{
/**
<installed>1</installed>
<maintenance>0</maintenance>
<needsDbUpgrade>0</needsDbUpgrade>
<version>10.11.0.0</version>
<versionstring>10.11.0</versionstring>
<edition>Community</edition>
<productname>QSfera</productname>
<product>QSfera</product>
<productversion>1.0.0-beta1+7c2e3201b</productversion>
*/
Status(const QVariantMap &status);
// legacy version
QVersionNumber legacyVersion;
// legacy version
QString legacyVersionString;
QString edition;
QString productname;
QString product;
QString productversion;
QVersionNumber version() const;
QString versionString() const;
};
struct QSFERA_SYNC_EXPORT TusSupport
{
/**
<tus_support>
<version>1.0.0</version>
<resumable>1.0.0</resumable>
<extension>creation,creation-with-upload</extension>
<max_chunk_size>0</max_chunk_size>
<http_method_override/>
</tus_support>
*/
TusSupport(const QVariantMap &tus_support);
QVersionNumber version;
QVersionNumber resumable;
QStringList extensions;
quint64 max_chunk_size;
QString http_method_override;
bool isValid() const;
};
struct QSFERA_SYNC_EXPORT SpaceSupport
{
/**
"spaces": {
"version": "0.0.1",
"enabled": true
}
*/
SpaceSupport(const QVariantMap &spaces_support);
bool enabled = false;
QVersionNumber version;
bool isValid() const;
};
struct QSFERA_SYNC_EXPORT FilesSharing
{
/**
api_enabled": true,
"resharing": true,
"group_sharing": true,
"sharing_roles": true,
"auto_accept_share": true,
"share_with_group_members_only": true,
"share_with_membership_groups_only": true,
"search_min_length": 3,
"default_permissions": 22,
*/
FilesSharing(const QVariantMap &filesSharing);
// TODO: add more
bool sharing_roles = false;
};
struct QSFERA_SYNC_EXPORT Migration
{
struct SpacesMigration
{
bool enabled = false;
QString endpoint;
};
explicit Migration(const QVariantMap &spaces_support);
SpacesMigration space_migration;
};
/**
* @brief The Capabilities class represents the capabilities of the server
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT Capabilities
{
public:
struct QSFERA_SYNC_EXPORT AppProviders
{
/**
"app_providers": [
{
"enabled": true,
"version": "1.0.0",
"apps_url": "/app/list",
"open_url": "/app/open",
"new_url": "/app/new"
}
*/
AppProviders() = default;
static AppProviders findVersion(const QUrl &baseUrl, const QVariantList &list, const QVersionNumber &v);
AppProviders(const QUrl &baseUrl, const QVariantMap &appProviders);
bool enabled = false;
QVersionNumber version;
QUrl appsUrl;
QUrl openUrl;
QUrl newUrl;
QUrl openWebUrl;
};
Capabilities(const QUrl &url, const QVariantMap &capabilities);
bool shareAPI() const;
bool sharePublicLink() const;
bool sharePublicLinkAllowUpload() const;
bool sharePublicLinkSupportsUploadOnly() const;
/** Whether read-only link shares require a password.
*
* Returns sharePublicLinkEnforcePassword() if the fine-grained
* permission isn't available.
*/
bool sharePublicLinkEnforcePasswordForReadOnly() const;
bool sharePublicLinkEnforcePasswordForReadWrite() const;
bool sharePublicLinkEnforcePasswordForUploadOnly() const;
bool sharePublicLinkDefaultExpire() const;
int sharePublicLinkDefaultExpireDateDays() const;
bool sharePublicLinkEnforceExpireDate() const;
bool sharePublicLinkMultiple() const;
bool shareResharing() const;
/** Remote Poll interval.
*
* returns the requested poll interval in seconds to be used by the client.
* @returns 0 if no capability is set.
*/
std::chrono::seconds remotePollInterval() const;
// TODO: return SharePermission
int defaultPermissions() const;
const Status &status() const;
bool checkForUpdates() const;
const TusSupport &tusSupport() const;
const SpaceSupport &spacesSupport() const;
/// Whether the "privatelink" DAV property is available
bool privateLinkPropertyAvailable() const;
/// Whether the "privatelink" DAV property supports the 'details' param
bool privateLinkDetailsParamAvailable() const;
/// returns true if the capabilities report notifications
bool notificationsAvailable() const;
/// returns true if the capabilities are loaded already.
bool isValid() const;
/**
* Returns the checksum types the server understands.
*
* When the client uses one of these checksumming algorithms in
* the OC-Checksum header of a file upload, the server will use
* it to validate that data was transmitted correctly.
*
* Path: checksums/supportedTypes
* Default: []
* Possible entries: "Adler32", "MD5", "SHA1"
*/
QList<OCC::CheckSums::Algorithm> supportedChecksumTypes() const;
/**
* The checksum algorithm that the server recommends for file uploads.
* This is just a preference, any algorithm listed in supportedTypes may be used.
*
* Path: checksums/preferredUploadType
*/
OCC::CheckSums::Algorithm preferredUploadChecksumType() const;
/**
* Helper that returns the preferredUploadChecksumType() if set, or one
* of the supportedChecksumTypes() if it isn't.
*/
CheckSums::Algorithm uploadChecksumType() const;
/**
* List of HTTP error codes should be guaranteed to eventually reset
* failing chunked uploads.
*
* The resetting works by tracking UploadInfo::errorCount.
*
* Note that other error codes than the ones listed here may reset the
* upload as well.
*
* Motivation: See #5344. They should always be reset on 412 (possibly
* checksum error), but broken servers may also require resets on
* unusual error codes such as 503.
*
* Path: dav/httpErrorCodesThatResetFailingChunkedUploads
* Default: []
* Example: [503, 500]
*/
QList<int> httpErrorCodesThatResetFailingChunkedUploads() const;
/**
* Regex that, if contained in a filename, will result in it not being uploaded.
*
* For servers older than 8.1.0 it defaults to [\\:?*"<>|]
* For servers >= that version, it defaults to the empty regex (the server
* will indicate invalid characters through an upload error)
*
* Note that it just needs to be contained. The regex [ab] is contained in "car".
*/
QString invalidFilenameRegex() const;
/**
* return the list of filename that should not be uploaded
*/
QStringList blacklistedFiles() const;
/**
* Whether conflict files should remain local (default) or should be uploaded.
*/
bool uploadConflictFiles() const;
/** Is versioning available? */
bool versioningEnabled() const;
const AppProviders &appProviders() const;
const FilesSharing &filesSharing() const;
const Migration &migration() const;
QVariantMap raw() const;
bool operator==(const Capabilities &other) const;
bool operator!=(const Capabilities &other) const { return !(*this == other); }
private:
QVariantMap _capabilities;
QVariantMap _fileSharingCapabilities;
QVariantMap _fileSharingPublicCapabilities;
TusSupport _tusSupport;
SpaceSupport _spaces;
Status _status;
bool _checkForUpdates;
AppProviders _appProviders;
FilesSharing _filesSharing;
Migration _migration;
};
}
#endif //CAPABILITIES_H
+33
View File
@@ -0,0 +1,33 @@
configure_file(version.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/version.cpp @ONLY)
target_sources(libsync PRIVATE
checksums.cpp
checksumalgorithms.cpp
chronoelapsedtimer.cpp
filesystembase.cpp
ownsql.cpp
preparedsqlquerymanager.cpp
syncjournaldb.cpp
syncjournalfilerecord.cpp
utility.cpp
remotepermissions.cpp
pinstate.cpp
plugin.cpp
restartmanager.cpp
syncfilestatus.cpp
${CMAKE_CURRENT_BINARY_DIR}/version.cpp
)
if(WIN32)
target_sources(libsync PRIVATE
utility_win.cpp
)
elseif(APPLE)
target_sources(libsync PRIVATE
utility_mac.mm
)
elseif(UNIX AND NOT APPLE)
target_sources(libsync PRIVATE
utility_unix.cpp
)
endif()
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <qglobal.h>
#if defined(QT_FORCE_ASSERTS) || !defined(QT_NO_DEBUG)
#define OC_ASSERT_MSG qFatal
#else
#define OC_ASSERT_MSG qCritical
#endif
// Default assert: If the condition is false in debug builds, terminate.
//
// Prints a message on failure, even in release builds.
#define OC_ASSERT(cond) \
if (!(cond)) { \
OC_ASSERT_MSG("ASSERT: \"%s\" in file %s, line %d %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO); \
} else { \
}
#define OC_ASSERT_X(cond, message) \
if (!(cond)) { \
OC_ASSERT_MSG("ASSERT: \"%s\" in file %s, line %d %s with message: %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO, message); \
} else { \
}
// Enforce condition to be true, even in release builds.
//
// Prints 'message' and aborts execution if 'cond' is false.
#define OC_ENFORCE(cond) \
if (!(cond)) { \
qFatal("ENFORCE: \"%s\" in file %s, line %d %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO); \
} else { \
}
#define OC_ENFORCE_X(cond, message) \
if (!(cond)) { \
qFatal("ENFORCE: \"%s\" in file %s, line %d %s with message: %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO, message); \
} else { \
}
[[nodiscard]] inline bool __OC_ENSURE(bool condition, const char *cond, const char *file, int line, const char *info)
{
if (Q_UNLIKELY(!condition)) {
OC_ASSERT_MSG("ENSURE: \"%s\" in file %s, line %d %s", cond, file, line, info);
return false;
}
return true;
}
#define OC_ENSURE(cond) Q_LIKELY(__OC_ENSURE(cond, #cond, __FILE__, __LINE__, Q_FUNC_INFO))
#define OC_ENSURE_NOT(cond) OC_ENSURE(!cond)
// An assert that is only present in debug builds: typically used for
// asserts that are too expensive for release mode.
//
// Q_ASSERT
+212
View File
@@ -0,0 +1,212 @@
/*
* c_jhash.c Jenkins Hash
*
* Copyright (c) 1997 Bob Jenkins <bob_jenkins@burtleburtle.net>
*
* lookup8.c, by Bob Jenkins, January 4 1997, Public Domain.
* hash(), hash2(), hash3, and _c_mix() are externally useful functions.
* Routines to test the hash are included if SELF_TEST is defined.
* You can use this free for any purpose. It has no warranty.
*
* See http://burtleburtle.net/bob/hash/evahash.html
*/
/**
* @file common/c_jhash.h
*
* @brief Interface of the cynapses jhash implementation
*
* @defgroup cynJHashInternals cynapses libc jhash function
* @ingroup cynLibraryAPI
*
* @{
*/
#ifndef _C_JHASH_H
#define _C_JHASH_H
#include <QtCore/qglobal.h>
#include <stdint.h>
/**
* _c_mix64 -- Mix 3 64-bit values reversibly.
*
* _c_mix64() takes 48 machine instructions, but only 24 cycles on a superscalar
* machine (like Intel's new MMX architecture). It requires 4 64-bit
* registers for 4::2 parallelism.
* All 1-bit deltas, all 2-bit deltas, all deltas composed of top bits of
* (a,b,c), and all deltas of bottom bits were tested. All deltas were
* tested both on random keys and on keys that were nearly all zero.
* These deltas all cause every bit of c to change between 1/3 and 2/3
* of the time (well, only 113/400 to 287/400 of the time for some
* 2-bit delta). These deltas all cause at least 80 bits to change
* among (a,b,c) when the _c_mix is run either forward or backward (yes it
* is reversible).
* This implies that a hash using _c_mix64 has no funnels. There may be
* characteristics with 3-bit deltas or bigger, I didn't test for
* those.
*/
#define _c_mix64(a, b, c) \
{ \
a -= b; \
a -= c; \
a ^= (c >> 43); \
b -= c; \
b -= a; \
b ^= (a << 9); \
c -= a; \
c -= b; \
c ^= (b >> 8); \
a -= b; \
a -= c; \
a ^= (c >> 38); \
b -= c; \
b -= a; \
b ^= (a << 23); \
c -= a; \
c -= b; \
c ^= (b >> 5); \
a -= b; \
a -= c; \
a ^= (c >> 35); \
b -= c; \
b -= a; \
b ^= (a << 49); \
c -= a; \
c -= b; \
c ^= (b >> 11); \
a -= b; \
a -= c; \
a ^= (c >> 12); \
b -= c; \
b -= a; \
b ^= (a << 18); \
c -= a; \
c -= b; \
c ^= (b >> 22); \
}
/**
* @brief hash a variable-length key into a 64-bit value
*
* The best hash table sizes are powers of 2. There is no need to do
* mod a prime (mod is sooo slow!). If you need less than 64 bits,
* use a bitmask. For example, if you need only 10 bits, do
* h = (h & hashmask(10));
* In which case, the hash table should have hashsize(10) elements.
*
* Use for hash table lookup, or anything where one collision in 2^^64
* is acceptable. Do NOT use for cryptographic purposes.
*
* @param k The key (the unaligned variable-length array of bytes).
* @param length The length of the key, counting by bytes.
* @param intval Initial value, can be any 8-byte value.
*
* @return A 64-bit value. Every bit of the key affects every bit of
* the return value. No funnels. Every 1-bit and 2-bit delta
* achieves avalanche. About 41+5len instructions.
*/
static inline uint64_t c_jhash64(const uint8_t *k, uint64_t length, uint64_t intval)
{
uint64_t a, b, c, len;
/* Set up the internal state */
len = length;
a = b = intval; /* the previous hash value */
c = 0x9e3779b97f4a7c13LL; /* the golden ratio; an arbitrary value */
/* handle most of the key */
while (len >= 24) {
a += (k[0] + ((uint64_t)k[1] << 8) + ((uint64_t)k[2] << 16) + ((uint64_t)k[3] << 24) + ((uint64_t)k[4] << 32) + ((uint64_t)k[5] << 40)
+ ((uint64_t)k[6] << 48) + ((uint64_t)k[7] << 56));
b += (k[8] + ((uint64_t)k[9] << 8) + ((uint64_t)k[10] << 16) + ((uint64_t)k[11] << 24) + ((uint64_t)k[12] << 32) + ((uint64_t)k[13] << 40)
+ ((uint64_t)k[14] << 48) + ((uint64_t)k[15] << 56));
c += (k[16] + ((uint64_t)k[17] << 8) + ((uint64_t)k[18] << 16) + ((uint64_t)k[19] << 24) + ((uint64_t)k[20] << 32) + ((uint64_t)k[21] << 40)
+ ((uint64_t)k[22] << 48) + ((uint64_t)k[23] << 56));
_c_mix64(a, b, c);
k += 24;
len -= 24;
}
/* handle the last 23 bytes */
c += length;
switch (len) {
case 23:
c += ((uint64_t)k[22] << 56);
Q_FALLTHROUGH();
case 22:
c += ((uint64_t)k[21] << 48);
Q_FALLTHROUGH();
case 21:
c += ((uint64_t)k[20] << 40);
Q_FALLTHROUGH();
case 20:
c += ((uint64_t)k[19] << 32);
Q_FALLTHROUGH();
case 19:
c += ((uint64_t)k[18] << 24);
Q_FALLTHROUGH();
case 18:
c += ((uint64_t)k[17] << 16);
Q_FALLTHROUGH();
case 17:
c += ((uint64_t)k[16] << 8);
Q_FALLTHROUGH();
/* the first byte of c is reserved for the length */
case 16:
b += ((uint64_t)k[15] << 56);
Q_FALLTHROUGH();
case 15:
b += ((uint64_t)k[14] << 48);
Q_FALLTHROUGH();
case 14:
b += ((uint64_t)k[13] << 40);
Q_FALLTHROUGH();
case 13:
b += ((uint64_t)k[12] << 32);
Q_FALLTHROUGH();
case 12:
b += ((uint64_t)k[11] << 24);
Q_FALLTHROUGH();
case 11:
b += ((uint64_t)k[10] << 16);
Q_FALLTHROUGH();
case 10:
b += ((uint64_t)k[9] << 8);
Q_FALLTHROUGH();
case 9:
b += ((uint64_t)k[8]);
Q_FALLTHROUGH();
case 8:
a += ((uint64_t)k[7] << 56);
Q_FALLTHROUGH();
case 7:
a += ((uint64_t)k[6] << 48);
Q_FALLTHROUGH();
case 6:
a += ((uint64_t)k[5] << 40);
Q_FALLTHROUGH();
case 5:
a += ((uint64_t)k[4] << 32);
Q_FALLTHROUGH();
case 4:
a += ((uint64_t)k[3] << 24);
Q_FALLTHROUGH();
case 3:
a += ((uint64_t)k[2] << 16);
Q_FALLTHROUGH();
case 2:
a += ((uint64_t)k[1] << 8);
Q_FALLTHROUGH();
case 1:
a += ((uint64_t)k[0]);
/* case 0: nothing left to add */
}
_c_mix64(a, b, c);
return c;
}
/**
* }@
*/
#endif /* _C_JHASH_H */
@@ -0,0 +1,48 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "checksumalgorithms.h"
using namespace OCC;
CheckSums::Algorithm CheckSums::fromByteArray(const QByteArray &s)
{
// assert to ensure that all keys are uppercase
Q_ASSERT([] {
// ensure to run the check only once
static bool once = [] {
for (const auto &a : All) {
const QString s = Utility::enumToString(a.first);
if (s != s.toUpper()) {
return false;
}
}
return true;
}();
return once;
}());
return fromName(s.toUpper().constData());
}
template <>
QString Utility::enumToString(CheckSums::Algorithm algo)
{
const auto n = toString(algo);
return QString::fromUtf8(n.data(), static_cast<int>(n.size()));
}
#include "moc_checksumalgorithms.cpp"
@@ -0,0 +1,103 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "constexpr_list.h"
#include "utility.h"
#include <QCryptographicHash>
#include <QString>
namespace OCC {
namespace CheckSums {
QSFERA_SYNC_EXPORT Q_NAMESPACE;
enum class Algorithm {
NONE,
SHA3_256 = QCryptographicHash::Sha3_256,
SHA256 = QCryptographicHash::Sha256,
SHA1 = QCryptographicHash::Sha1,
MD5 = QCryptographicHash::Md5,
ADLER32 = 100,
DUMMY_FOR_TESTS,
PARSE_ERROR
};
Q_ENUM_NS(Algorithm);
constexpr std::string_view toString(Algorithm algo)
{
switch (algo) {
case Algorithm::SHA3_256:
return "SHA3-256";
case Algorithm::SHA256:
return "SHA256";
case Algorithm::SHA1:
return "SHA1";
case Algorithm::MD5:
return "MD5";
case Algorithm::ADLER32:
return "ADLER32";
case Algorithm::DUMMY_FOR_TESTS:
return "DUMMY_FOR_TESTS";
case Algorithm::PARSE_ERROR:
break;
case Algorithm::NONE:
// while none is a valid enum value, it is not valid when used to create a checksum string
Q_UNREACHABLE();
break;
}
return "ERROR";
}
inline QString toQString(Algorithm algo)
{
const auto n = toString(algo);
return QString::fromUtf8(n.data(), n.size());
}
constexpr auto pair(Algorithm a)
{
return std::make_pair(a, toString(a));
}
constexpr_list auto All = {
// Sorted by priority
pair(Algorithm::SHA3_256), pair(Algorithm::SHA256), pair(Algorithm::SHA1), pair(Algorithm::MD5), pair(Algorithm::ADLER32),
pair(Algorithm::DUMMY_FOR_TESTS)};
constexpr_list auto UnsafeAlgorithms = {pair(Algorithm::ADLER32), pair(Algorithm::DUMMY_FOR_TESTS)};
constexpr_list auto SafeAlgorithms = {pair(Algorithm::SHA3_256), pair(Algorithm::SHA256), pair(Algorithm::SHA1), pair(Algorithm::MD5)};
inline Algorithm fromName(std::string_view s)
{
auto it = std::find_if(All.begin(), All.end(), [s](const auto &it) { return it.second == s; });
if (it != All.end()) {
return it->first;
}
return Algorithm::PARSE_ERROR;
}
QSFERA_SYNC_EXPORT Algorithm fromByteArray(const QByteArray &s);
} // namespace CheckSums
template <>
QSFERA_SYNC_EXPORT QString Utility::enumToString(CheckSums::Algorithm algo);
} // namespace OCC
+382
View File
@@ -0,0 +1,382 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "common/checksums.h"
#include "asserts.h"
#include "common/chronoelapsedtimer.h"
#include "common/utility.h"
#include "filesystembase.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QLoggingCategory>
#include <QScopeGuard>
#include <QtConcurrentRun>
#include <zlib.h>
/** \file checksums.cpp
*
* \brief Computing and validating file checksums
*
* Overview
* --------
*
* Checksums are used in two distinct ways during synchronization:
*
* - to guard uploads and downloads against data corruption
* (transmission checksum)
* - to quickly check whether the content of a file has changed
* to avoid redundant uploads (content checksum)
*
* In principle both are independent and different checksumming
* algorithms can be used. To avoid redundant computations, it can
* make sense to use the same checksum algorithm though.
*
* Transmission Checksums
* ----------------------
*
* The usage of transmission checksums is currently optional and needs
* to be explicitly enabled by adding 'transmissionChecksum=TYPE' to
* the '[General]' section of the config file.
*
* When enabled, the checksum will be calculated on upload and sent to
* the server in the OC-Checksum header with the format 'TYPE:CHECKSUM'.
*
* On download, the header with the same name is read and if the
* received data does not have the expected checksum, the download is
* rejected.
*
* Transmission checksums guard a specific sync action and are not stored
* in the database.
*
* Content Checksums
* -----------------
*
* Sometimes the metadata of a local file changes while the content stays
* unchanged. Content checksums allow the sync client to avoid uploading
* the same data again by comparing the file's actual checksum to the
* checksum stored in the database.
*
* Content checksums are not sent to the server.
*
* Checksum Algorithms
* -------------------
*
* - Adler32 (requires zlib)
* - MD5
* - SHA1
* - SHA256
* - SHA3-256 (requires Qt 5.9)
*
*/
namespace {
QByteArray calcAdler32(QIODevice *device)
{
const qint64 BUFSIZE(500 * 1024); // 500 KiB
if (device->size() == 0) {
return QByteArray();
}
QByteArray buf(BUFSIZE, Qt::Uninitialized);
unsigned int adler = adler32(0L, nullptr, 0);
qint64 size;
while (!device->atEnd()) {
size = device->read(buf.data(), BUFSIZE);
if (size > 0)
adler = adler32(adler, (const Bytef *)buf.data(), size);
}
return QByteArray::number(adler, 16);
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcChecksums, "sync.checksums", QtInfoMsg)
Q_LOGGING_CATEGORY(lcChecksumsHeader, "sync.checksums.header", QtInfoMsg)
ChecksumHeader ChecksumHeader::parseChecksumHeader(const QByteArray &header)
{
if (header.isEmpty()) {
return {};
}
ChecksumHeader out;
const auto idx = header.indexOf(':');
if (idx < 0) {
out._error = QCoreApplication::translate("ChecksumHeader", "The checksum header is malformed: %1").arg(QString::fromUtf8(header));
} else {
out._checksumType = CheckSums::fromByteArray(header.left(idx));
if (out._checksumType == CheckSums::Algorithm::PARSE_ERROR) {
out._error = QCoreApplication::translate("ChecksumHeader", "The checksum header contained an unknown checksum type '%1'")
.arg(QString::fromUtf8(header.left(idx)));
}
out._checksum = header.mid(idx + 1);
}
if (!out.isValid()) {
qCDebug(lcChecksumsHeader) << u"Failed to parse" << header << out.error();
}
return out;
}
ChecksumHeader::ChecksumHeader(CheckSums::Algorithm type, const QByteArray &checksum)
: _checksumType(type)
, _checksum(checksum)
{
}
QByteArray ChecksumHeader::makeChecksumHeader() const
{
if (!isValid()) {
return {};
}
return Utility::enumToString(_checksumType).toUtf8() + ':' + _checksum;
}
CheckSums::Algorithm ChecksumHeader::type() const
{
return _checksumType;
}
QByteArray ChecksumHeader::checksum() const
{
return _checksum;
}
bool ChecksumHeader::isValid() const
{
return _checksumType != CheckSums::Algorithm::NONE && _checksumType != CheckSums::Algorithm::PARSE_ERROR && !_checksum.isEmpty();
}
QString ChecksumHeader::error() const
{
return _error;
}
bool ChecksumHeader::operator==(const ChecksumHeader &other) const
{
return _checksumType == other._checksumType && _checksum == other._checksum;
}
bool ChecksumHeader::operator!=(const ChecksumHeader &other) const
{
return !(*this == other);
}
QByteArray findBestChecksum(const QByteArray &_checksums)
{
if (_checksums.isEmpty()) {
return {};
}
// The order of the searches here defines the preference ordering.
// we assume _checksums to be utf-8 so toUpper is valid here
const auto i = [checksums = _checksums.toUpper()]() -> qsizetype {
for (const auto &algo : CheckSums::All) {
auto i = checksums.indexOf(algo.second.data());
if (i != -1) {
return i;
}
}
return -1;
}();
if (i != -1) {
// Now i is the start of the best checksum
// Grab it until the next space or end of xml or end of string.
int end = _checksums.indexOf(' ', i);
// workaround for https://github.com/owncloud/core/pull/38304
if (end == -1) {
end = _checksums.indexOf('<', i);
}
return _checksums.mid(i, end - i);
}
qCWarning(lcChecksums) << u"Failed to parse" << _checksums;
return {};
}
ComputeChecksum::ComputeChecksum(QObject *parent)
: QObject(parent)
{
}
ComputeChecksum::~ComputeChecksum() { }
void ComputeChecksum::setChecksumType(CheckSums::Algorithm type)
{
Q_ASSERT(type != CheckSums::Algorithm::NONE && type != CheckSums::Algorithm::PARSE_ERROR);
_checksumType = type;
}
CheckSums::Algorithm ComputeChecksum::checksumType() const
{
return _checksumType;
}
void ComputeChecksum::start(const QString &filePath)
{
qCInfo(lcChecksums) << u"Computing" << checksumType() << u"checksum of" << filePath << u"in a thread";
startImpl(std::make_unique<QFile>(filePath));
}
void ComputeChecksum::start(std::unique_ptr<QIODevice> device)
{
OC_ENFORCE(device);
qCInfo(lcChecksums) << u"Computing" << checksumType() << u"checksum of device" << device.get() << u"in a thread";
OC_ASSERT(!device->parent());
startImpl(std::move(device));
}
void ComputeChecksum::startImpl(std::unique_ptr<QIODevice> device)
{
connect(&_watcher, &QFutureWatcherBase::finished, this, &ComputeChecksum::slotCalculationDone, Qt::UniqueConnection);
// We'd prefer to move the unique_ptr into the lambda, but that's
// awkward with the C++ standard we're on
auto sharedDevice = QSharedPointer<QIODevice>(device.release());
// Bug: The thread will keep running even if ComputeChecksum is deleted.
auto type = checksumType();
_watcher.setFuture(QtConcurrent::run([sharedDevice, type]() {
if (!sharedDevice->open(QIODevice::ReadOnly)) {
if (auto file = qobject_cast<QFile *>(sharedDevice.data())) {
qCWarning(lcChecksums) << u"Could not open file" << file->fileName() << u"for reading to compute a checksum" << file->errorString();
} else {
qCWarning(lcChecksums) << u"Could not open device" << sharedDevice.data() << u"for reading to compute a checksum"
<< sharedDevice->errorString();
}
return QByteArray();
}
auto result = ComputeChecksum::computeNow(sharedDevice.data(), type);
sharedDevice->close();
return result;
}));
}
QByteArray ComputeChecksum::computeNowOnFile(const QString &filePath, CheckSums::Algorithm checksumType)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcChecksums) << u"Could not open file" << filePath << u"for reading and computing checksum" << file.errorString();
return QByteArray();
}
return computeNow(&file, checksumType);
}
QByteArray ComputeChecksum::computeNow(QIODevice *device, CheckSums::Algorithm algorithm)
{
// const cast to prevent stream to "device"
const auto log = qScopeGuard([device, algorithm, timer = Utility::ChronoElapsedTimer()] {
if (auto file = qobject_cast<QFile *>(device)) {
qCDebug(lcChecksums) << u"Finished" << algorithm << u"computation for" << file->fileName() << timer;
} else {
qCDebug(lcChecksums) << u"Finished" << algorithm << u"computation for" << device << timer;
}
});
switch (algorithm) {
case CheckSums::Algorithm::SHA3_256:
[[fallthrough]];
case CheckSums::Algorithm::SHA256:
[[fallthrough]];
case CheckSums::Algorithm::SHA1:
[[fallthrough]];
case CheckSums::Algorithm::MD5: {
QCryptographicHash crypto(static_cast<QCryptographicHash::Algorithm>(algorithm));
if (crypto.addData(device)) {
return crypto.result().toHex();
}
qCWarning(lcChecksums) << u"Failed to compoute checksum" << Utility::enumToString(algorithm);
return {};
}
case CheckSums::Algorithm::ADLER32:
return calcAdler32(device);
case CheckSums::Algorithm::DUMMY_FOR_TESTS:
return QByteArrayLiteral("0x1");
case CheckSums::Algorithm::PARSE_ERROR:
return {};
case CheckSums::Algorithm::NONE:
Q_UNREACHABLE();
}
Q_UNREACHABLE();
}
void ComputeChecksum::slotCalculationDone()
{
QByteArray checksum = _watcher.future().result();
if (!checksum.isNull()) {
Q_EMIT done(_checksumType, checksum);
} else {
Q_EMIT done(CheckSums::Algorithm::PARSE_ERROR, QByteArray());
}
}
ValidateChecksumHeader::ValidateChecksumHeader(QObject *parent)
: QObject(parent)
{
}
ComputeChecksum *ValidateChecksumHeader::prepareStart(const QByteArray &checksumHeader)
{
// If the incoming header is empty no validation can happen. Just continue.
if (checksumHeader.isEmpty()) {
Q_EMIT validated(CheckSums::Algorithm::PARSE_ERROR, QByteArray());
return nullptr;
}
_expectedChecksum = ChecksumHeader::parseChecksumHeader(checksumHeader);
if (!_expectedChecksum.isValid()) {
qCWarning(lcChecksums) << u"Checksum header malformed:" << checksumHeader;
Q_EMIT validationFailed(_expectedChecksum.error());
return nullptr;
}
auto calculator = new ComputeChecksum(this);
calculator->setChecksumType(_expectedChecksum.type());
connect(calculator, &ComputeChecksum::done, this, &ValidateChecksumHeader::slotChecksumCalculated);
return calculator;
}
void ValidateChecksumHeader::start(const QString &filePath, const QByteArray &checksumHeader)
{
if (auto calculator = prepareStart(checksumHeader))
calculator->start(filePath);
}
void ValidateChecksumHeader::start(std::unique_ptr<QIODevice> device, const QByteArray &checksumHeader)
{
if (auto calculator = prepareStart(checksumHeader))
calculator->start(std::move(device));
}
void ValidateChecksumHeader::slotChecksumCalculated(CheckSums::Algorithm checksumType, const QByteArray &checksum)
{
if (_expectedChecksum.type() == CheckSums::Algorithm::PARSE_ERROR) {
Q_EMIT validationFailed(_expectedChecksum.error());
return;
}
if (checksum != _expectedChecksum.checksum()) {
Q_EMIT validationFailed(tr("The downloaded file does not match the checksum, it will be resumed. '%1' != '%2'")
.arg(QString::fromUtf8(_expectedChecksum.checksum()), QString::fromUtf8(checksum)));
return;
}
Q_EMIT validated(checksumType, checksum);
}
}
+181
View File
@@ -0,0 +1,181 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "checksumalgorithms.h"
#include "libsync/qsferasynclib.h"
#include <QByteArray>
#include <QCryptographicHash>
#include <QFutureWatcher>
#include <QObject>
#include <memory>
class QFile;
namespace OCC {
/**
* Tags for checksum headers values.
* They are here for being shared between Upload- and Download Job
*/
class SyncJournalDb;
/**
* Returns the highest-quality checksum in a 'checksums'
* property retrieved from the server.
*
* Example: "ADLER32:1231 SHA1:ab124124 MD5:2131affa21"
* -> "SHA1:ab124124"
*/
QSFERA_SYNC_EXPORT QByteArray findBestChecksum(const QByteArray &checksums);
class QSFERA_SYNC_EXPORT ChecksumHeader
{
public:
static ChecksumHeader parseChecksumHeader(const QByteArray &header);
ChecksumHeader() = default;
ChecksumHeader(CheckSums::Algorithm type, const QByteArray &checksum);
QByteArray makeChecksumHeader() const;
CheckSums::Algorithm type() const;
QByteArray checksum() const;
bool isValid() const;
QString error() const;
bool operator==(const ChecksumHeader &other) const;
bool operator!=(const ChecksumHeader &other) const;
private:
CheckSums::Algorithm _checksumType = CheckSums::Algorithm::NONE;
QByteArray _checksum;
QString _error;
};
/**
* Computes the checksum of a file.
* \ingroup libsync
*/
class QSFERA_SYNC_EXPORT ComputeChecksum : public QObject
{
Q_OBJECT
public:
explicit ComputeChecksum(QObject *parent = nullptr);
~ComputeChecksum() override;
/**
* Sets the checksum type to be used.
*/
void setChecksumType(CheckSums::Algorithm type);
CheckSums::Algorithm checksumType() const;
/**
* Computes the checksum for the given file path.
*
* done() is emitted when the calculation finishes.
*/
void start(const QString &filePath);
/**
* Computes the checksum for the given device.
*
* done() is emitted when the calculation finishes.
*
* The device ownership transfers into the thread that
* will compute the checksum. It must not have a parent.
*/
void start(std::unique_ptr<QIODevice> device);
/**
* Computes the checksum synchronously.
*/
static QByteArray computeNow(QIODevice *device, CheckSums::Algorithm algo);
/**
* Computes the checksum synchronously on file. Convenience wrapper for computeNow().
*/
static QByteArray computeNowOnFile(const QString &filePath, CheckSums::Algorithm checksumType);
Q_SIGNALS:
void done(CheckSums::Algorithm checksumType, const QByteArray &checksum);
private Q_SLOTS:
void slotCalculationDone();
private:
void startImpl(std::unique_ptr<QIODevice> device);
CheckSums::Algorithm _checksumType;
// watcher for the checksum calculation thread
QFutureWatcher<QByteArray> _watcher;
};
/**
* Checks whether a file's checksum matches the expected value.
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT ValidateChecksumHeader : public QObject
{
Q_OBJECT
public:
explicit ValidateChecksumHeader(QObject *parent = nullptr);
/**
* Check a file's actual checksum against the provided checksumHeader
*
* If no checksum is there, or if a correct checksum is there, the signal validated()
* will be emitted. In case of any kind of error, the signal validationFailed() will
* be emitted.
*/
void start(const QString &filePath, const QByteArray &checksumHeader);
/**
* Check a device's actual checksum against the provided checksumHeader
*
* Like the other start() but works on an device.
*
* The device ownership transfers into the thread that
* will compute the checksum. It must not have a parent.
*/
void start(std::unique_ptr<QIODevice> device, const QByteArray &checksumHeader);
Q_SIGNALS:
void validated(CheckSums::Algorithm checksumType, const QByteArray &checksum);
void validationFailed(const QString &errMsg);
private Q_SLOTS:
void slotChecksumCalculated(CheckSums::Algorithm checksumType, const QByteArray &checksum);
private:
ComputeChecksum *prepareStart(const QByteArray &checksumHeader);
ChecksumHeader _expectedChecksum;
};
}
@@ -0,0 +1,72 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "chronoelapsedtimer.h"
#include <QtGlobal>
using namespace OCC::Utility;
using namespace std::chrono;
ChronoElapsedTimer::ChronoElapsedTimer(bool start)
: _start(steady_clock::now())
, _started(start)
{
}
bool ChronoElapsedTimer::isStarted() const
{
return _started;
}
void ChronoElapsedTimer::reset()
{
_start = steady_clock::now();
_end = {};
_started = true;
}
void ChronoElapsedTimer::stop()
{
Q_ASSERT(_end == steady_clock::time_point{});
Q_ASSERT(_started);
_end = steady_clock::now();
// don't set _started to false, so that we can still call duration() later
}
nanoseconds ChronoElapsedTimer::duration() const
{
if (!_started) {
return nanoseconds::max();
}
if (_end != steady_clock::time_point{}) {
return _end - _start;
}
return steady_clock::now() - _start;
}
QDebug operator<<(QDebug debug, const ChronoElapsedTimer &timer)
{
QDebugStateSaver save(debug);
debug.nospace();
auto duration = timer.duration();
const auto h = duration_cast<hours>(duration);
const auto min = duration_cast<minutes>(duration -= h);
const auto s = duration_cast<seconds>(duration -= min);
const auto ms = duration_cast<milliseconds>(duration -= s);
return debug << u"duration(" << h.count() << u"h, " << min.count() << u"min, " << s.count() << u"s, " << ms.count() << u"ms)";
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QDebug>
#include <chrono>
namespace OCC::Utility {
/**
* Meassure time using std::chrono::steady_clock
*/
class QSFERA_SYNC_EXPORT ChronoElapsedTimer
{
public:
ChronoElapsedTimer(bool start = true);
bool isStarted() const;
/**
* Resets and start the timer
*/
void reset();
/**
* Stops the timer
*/
void stop();
/**
* Returns the elapsed time.
* If the timer is stopped it is the time between start and stop of the timer.
*/
std::chrono::nanoseconds duration() const;
private:
std::chrono::steady_clock::time_point _start = {};
std::chrono::steady_clock::time_point _end = {};
bool _started = false;
};
}
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::Utility::ChronoElapsedTimer &in);
@@ -0,0 +1,28 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <qglobal.h>
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102921
// we hope it will be fixed in GCC >= 13
#if !defined(Q_CC_GNU) || Q_CC_GNU >= 1300
#define constexpr_list constexpr
#else
#define constexpr_list inline
#endif
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include <qglobal.h>
#ifdef Q_CC_MSVC
// _Pragma would require C99 ecm currently sets C90
#define OC_DISABLE_DEPRECATED_WARNING \
__pragma(warning(push)); \
__pragma(warning(disable : 4996))
#define OC_ENABLE_DEPRECATED_WARNING __pragma(warning(pop))
#else
#define OC_DISABLE_DEPRECATED_WARNING \
_Pragma("GCC diagnostic push"); \
_Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
#define OC_ENABLE_DEPRECATED_WARNING _Pragma("GCC diagnostic pop")
#endif
@@ -0,0 +1,587 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "filesystembase.h"
#include "common/asserts.h"
#include "utility.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QSettings>
#include <sys/stat.h>
#include <sys/types.h>
#ifdef Q_OS_WIN
#include "utility_win.h"
#include <fcntl.h>
#include <io.h>
#endif
using namespace Qt::Literals::StringLiterals;
namespace {
// Regarding
// https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
// the file name limit appears to be about 255 chars
// -10 for safety
constexpr qsizetype fileNameMaxC = 255 - 10;
constexpr auto replacementCharC = QLatin1Char('_');
}
namespace OCC {
Q_LOGGING_CATEGORY(lcFileSystem, "sync.filesystem", QtInfoMsg)
std::filesystem::path FileSystem::toFilesystemPath(const QString &path)
{
#ifdef Q_OS_WIN
return QtPrivate::toFilesystemPath(FileSystem::longWinPath(path));
#else
return QtPrivate::toFilesystemPath(path);
#endif
}
QString FileSystem::fromFilesystemPath(const std::filesystem::path &path)
{
#ifdef Q_OS_WIN
constexpr std::wstring_view prefix = LR"(\\?\)";
std::wstring nativePath = path.native();
auto view = std::wstring_view(nativePath);
if (nativePath.starts_with(prefix)) {
view = view.substr(prefix.size());
}
return QDir::fromNativeSeparators(QString::fromWCharArray(view.data(), view.length()));
#elif defined(Q_OS_MACOS)
// based on QFile::decodeName
return QtPrivate::fromFilesystemPath(path).normalized(QString::NormalizationForm_C);
#else
return QtPrivate::fromFilesystemPath(path);
#endif
}
QString FileSystem::longWinPath(const QString &inpath)
{
#ifndef Q_OS_WIN
return inpath;
#else
Q_ASSERT(QFileInfo(inpath).isAbsolute());
if (inpath.isEmpty()) {
return inpath;
}
QString out = QDir::toNativeSeparators(inpath);
const QLatin1Char sep('\\');
if (out.size() == 2 && out.at(1) == ':'_L1) {
// std::path handles C: incorrectly
out += sep;
}
// we already have a unc path
if (out.startsWith(sep + sep)) {
return out;
}
// prepend \\?\ and to support long names
if (out.at(0) == sep) {
// should not happen as we require the path to be absolute
return QStringLiteral("\\\\?") + out;
}
return QStringLiteral("\\\\?\\") + out;
#endif
}
void FileSystem::setFileHidden(const QString &filename, bool hidden)
{
#ifdef _WIN32
QString fName = longWinPath(filename);
DWORD dwAttrs;
dwAttrs = GetFileAttributesW((wchar_t *)fName.utf16());
if (dwAttrs != INVALID_FILE_ATTRIBUTES) {
if (hidden && !(dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs | FILE_ATTRIBUTE_HIDDEN);
} else if (!hidden && (dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs & ~FILE_ATTRIBUTE_HIDDEN);
}
}
#else
Q_UNUSED(filename);
Q_UNUSED(hidden);
#endif
}
static QFile::Permissions getDefaultWritePermissions()
{
QFile::Permissions result = QFile::WriteUser;
#ifndef Q_OS_WIN
mode_t mask = umask(0);
umask(mask);
if (!(mask & S_IWGRP)) {
result |= QFile::WriteGroup;
}
if (!(mask & S_IWOTH)) {
result |= QFile::WriteOther;
}
#endif
return result;
}
void FileSystem::setFileReadOnly(const QString &filename, bool readonly)
{
QFile file(filename);
QFile::Permissions permissions = file.permissions();
QFile::Permissions allWritePermissions = QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther | QFile::WriteOwner;
static QFile::Permissions defaultWritePermissions = getDefaultWritePermissions();
permissions &= ~allWritePermissions;
if (!readonly) {
permissions |= defaultWritePermissions;
}
// Warning: This function does not manipulate ACLs, which may limit its effectiveness.
file.setPermissions(permissions);
}
void FileSystem::setFolderMinimumPermissions(const QString &filename)
{
#ifdef Q_OS_MAC
QFile::Permissions perm = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
QFile file(filename);
file.setPermissions(perm);
#else
Q_UNUSED(filename);
#endif
}
void FileSystem::setFileReadOnlyWeak(const QString &filename, bool readonly)
{
QFile file(filename);
QFile::Permissions permissions = file.permissions();
if (!readonly && (permissions & QFile::WriteOwner)) {
return; // already writable enough
}
setFileReadOnly(filename, readonly);
}
bool FileSystem::rename(const QString &originFileName, const QString &destinationFileName, QString *errorString)
{
bool success = false;
QString error;
#ifdef Q_OS_WIN
const QString originalFileNameLong = longWinPath(originFileName);
const QString dest = longWinPath(destinationFileName);
if (FileSystem::isFileLocked(dest, FileSystem::LockMode::Exclusive)) {
error = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(destinationFileName);
} else if (FileSystem::isFileLocked(originalFileNameLong, FileSystem::LockMode::Exclusive)) {
error = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(originFileName);
} else if (isLnkFile(originFileName) || isLnkFile(destinationFileName)) {
success = MoveFileEx((wchar_t *)originalFileNameLong.utf16(), (wchar_t *)dest.utf16(), MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
if (!success) {
error = Utility::formatWinError(GetLastError());
}
} else
#endif
{
QFile orig(originFileName);
success = orig.rename(destinationFileName);
if (!success) {
error = orig.errorString();
}
}
if (!success) {
qCWarning(lcFileSystem) << u"Error renaming file" << originFileName << u"to" << destinationFileName << u"failed: " << error;
if (errorString) {
*errorString = error;
}
}
return success;
}
bool FileSystem::uncheckedRenameReplace(const QString &originFileName, const QString &destinationFileName, QString *errorString)
{
Q_ASSERT(errorString);
#ifndef Q_OS_WIN
bool success;
QFile orig(originFileName);
// We want a rename that also overwites. QFile::rename does not overwite.
// Qt 5.1 has QSaveFile::renameOverwrite we could use.
// ### FIXME
success = true;
bool destExists = fileExists(destinationFileName);
if (destExists && !QFile::remove(destinationFileName)) {
*errorString = orig.errorString();
qCWarning(lcFileSystem) << u"Target file could not be removed.";
success = false;
}
if (success) {
success = orig.rename(destinationFileName);
}
if (!success) {
*errorString = orig.errorString();
qCWarning(lcFileSystem) << u"Renaming temp file to final failed: " << *errorString;
return false;
}
#else // Q_OS_WIN
// You can not overwrite a read-only file on windows.
if (!QFileInfo(destinationFileName).isWritable()) {
setFileReadOnly(destinationFileName, false);
}
const QString orig = longWinPath(originFileName);
const QString dest = longWinPath(destinationFileName);
if (FileSystem::isFileLocked(dest, FileSystem::LockMode::Exclusive)) {
*errorString = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(destinationFileName);
qCWarning(lcFileSystem) << u"Renaming failed: " << *errorString;
return false;
}
if (FileSystem::isFileLocked(orig, FileSystem::LockMode::Exclusive)) {
*errorString = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(originFileName);
qCWarning(lcFileSystem) << u"Renaming failed: " << *errorString;
return false;
}
const BOOL ok = MoveFileEx(reinterpret_cast<const wchar_t *>(orig.utf16()), reinterpret_cast<const wchar_t *>(dest.utf16()),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
if (!ok) {
*errorString = Utility::formatWinError(GetLastError());
qCWarning(lcFileSystem) << u"Renaming temp file to final failed: " << *errorString;
return false;
}
#endif
return true;
}
bool FileSystem::openAndSeekFileSharedRead(QFile *file, QString *errorOrNull, qint64 seek)
{
QString errorDummy;
// avoid many if (errorOrNull) later.
QString &error = errorOrNull ? *errorOrNull : errorDummy;
error.clear();
#ifdef Q_OS_WIN
//
// The following code is adapted from Qt's QFSFileEnginePrivate::nativeOpen()
auto fileHandle = Utility::Handle::createHandle(toFilesystemPath(file->fileName()), {.accessMode = GENERIC_READ});
// Bail out on error.
if (!fileHandle) {
error = fileHandle.errorMessage();
return false;
}
// Convert the HANDLE to an fd and pass it to QFile's foreign-open
// function. The fd owns the handle, so when QFile later closes
// the fd the handle will be closed too.
int fd = _open_osfhandle((intptr_t)fileHandle.handle(), _O_RDONLY);
if (fd == -1) {
error = QStringLiteral("could not make fd from handle");
return false;
}
HANDLE basicHandle = fileHandle.release(); // we hand over the handle to qt
if (!file->open(fd, QIODevice::ReadOnly, QFile::AutoCloseHandle)) {
error = file->errorString();
_close(fd); // implicitly closes fileHandle
return false;
}
// Seek to the right spot
LARGE_INTEGER *li = reinterpret_cast<LARGE_INTEGER *>(&seek);
DWORD newFilePointer = SetFilePointer(basicHandle, li->LowPart, &li->HighPart, FILE_BEGIN);
const auto llastError = GetLastError();
if (newFilePointer == 0xFFFFFFFF && llastError != NO_ERROR) {
error = Utility::formatWinError(llastError);
return false;
}
return true;
#else
if (!file->open(QFile::ReadOnly)) {
error = file->errorString();
return false;
}
if (!file->seek(seek)) {
error = file->errorString();
return false;
}
return true;
#endif
}
#ifdef Q_OS_WIN
static bool fileExistsWin(const QString &filename)
{
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
QString fName = FileSystem::longWinPath(filename);
hFind = FindFirstFileW((wchar_t *)fName.utf16(), &FindFileData);
if (hFind == INVALID_HANDLE_VALUE) {
return false;
}
FindClose(hFind);
return true;
}
#endif
bool FileSystem::fileExists(const QString &filename, const QFileInfo &fileInfo)
{
#ifdef Q_OS_WIN
if (isLnkFile(filename)) {
// Use a native check.
return fileExistsWin(filename);
}
#endif
bool re = fileInfo.exists();
// if the filename is different from the filename in fileInfo, the fileInfo is
// not valid. There needs to be one initialised here. Otherwise the incoming
// fileInfo is re-used.
if (fileInfo.filePath() != filename) {
re = QFileInfo::exists(filename);
}
return re;
}
#ifdef Q_OS_WIN
QString FileSystem::fileSystemForPath(const QString &path)
{
// See also QStorageInfo (Qt >=5.4) and GetVolumeInformationByHandleW (>= Vista)
QString drive = path.left(2);
if (!drive.endsWith(QLatin1Char(':')))
return QString();
drive.append(QLatin1Char('\\'));
const size_t fileSystemBufferSize = 4096;
TCHAR fileSystemBuffer[fileSystemBufferSize];
if (!GetVolumeInformationW(reinterpret_cast<LPCWSTR>(drive.utf16()), nullptr, 0, nullptr, nullptr, nullptr, fileSystemBuffer, fileSystemBufferSize)) {
return QString();
}
return QString::fromUtf16(reinterpret_cast<const char16_t *>(fileSystemBuffer));
}
bool FileSystem::longPathsEnabledOnWindows()
{
static std::optional<bool> longPathsEnabledCached = {};
if (!longPathsEnabledCached.has_value()) {
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry#enable-long-paths-in-windows-10-version-1607-and-later
QSettings fsSettings(QStringLiteral("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\FileSystem"), QSettings::NativeFormat);
QVariant longPathsEnabled = fsSettings.value(QStringLiteral("LongPathsEnabled"));
qCDebug(lcFileSystem) << u"LongPathsEnabled:" << longPathsEnabled;
longPathsEnabledCached = longPathsEnabled.value<uint32_t>() == 1;
}
return longPathsEnabledCached.value();
}
#endif
bool FileSystem::remove(const QString &fileName, QString *errorString)
{
#ifdef Q_OS_WIN
// You cannot delete a read-only file on windows, but we want to
// allow that.
if (!QFileInfo(fileName).isWritable()) {
setFileReadOnly(fileName, false);
}
#endif
QFile f(fileName);
if (!f.remove()) {
if (errorString) {
*errorString = f.errorString();
}
qCWarning(lcFileSystem) << u"Failed to delete:" << fileName << u"Error:" << f.errorString();
return false;
}
return true;
}
#ifdef Q_OS_WIN
namespace {
/**
* This function creates a file handle with the desired LockMode
*/
Utility::Handle lockFile(const QString &fileName, FileSystem::LockMode mode)
{
// Check if file exists
const auto info = std::filesystem::directory_entry(FileSystem::toFilesystemPath(fileName));
uint32_t shareMode = 0;
uint32_t accessMode = GENERIC_READ | GENERIC_WRITE;
switch (mode) {
case FileSystem::LockMode::Exclusive:
shareMode = 0;
break;
case FileSystem::LockMode::Shared:
shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
break;
case FileSystem::LockMode::SharedRead:
shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
accessMode = GENERIC_READ;
break;
}
if (info.exists()) {
// Try to open the file with as much access as possible..
auto out = Utility::Handle::createHandle(info.path(), {.accessMode = accessMode, .shareMode = shareMode});
if (out) {
if (info.is_directory()) {
return out;
} else {
// try to lock the complete file explicitly, to check for ranged locks
LARGE_INTEGER start;
start.QuadPart = 0;
LARGE_INTEGER end;
end.QuadPart = -1;
if (LockFile(out.handle(), start.LowPart, start.HighPart, end.LowPart, end.HighPart)) {
OC_ENFORCE(UnlockFile(out.handle(), start.LowPart, start.HighPart, end.LowPart, end.HighPart));
return out;
} else {
return {};
}
}
}
return out;
}
return {};
}
}
#endif
bool FileSystem::isFileLocked(const QString &fileName, LockMode mode)
{
#ifdef Q_OS_WIN
const auto handle = lockFile(fileName, mode);
if (!handle) {
const auto error = GetLastError();
if (error == ERROR_SHARING_VIOLATION || error == ERROR_LOCK_VIOLATION) {
return true;
} else if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) {
const QFileInfo info(fileName);
qCWarning(lcFileSystem) << Q_FUNC_INFO << mode << fileName << (info.isDir() ? "isDir" : (info.exists() ? "isFile" : "does not exist"))
<< Utility::formatWinError(error);
}
}
#else
Q_UNUSED(fileName);
Q_UNUSED(mode);
#endif
return false;
}
bool FileSystem::isLnkFile(const QString &filename)
{
return filename.endsWith(QLatin1String(".lnk"));
}
bool FileSystem::isJunction(const QString &filename)
{
#ifdef Q_OS_WIN
WIN32_FIND_DATA findData;
HANDLE hFind =
FindFirstFileEx(reinterpret_cast<const wchar_t *>(longWinPath(filename).utf16()), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
if (hFind != INVALID_HANDLE_VALUE) {
FindClose(hFind);
return false;
}
return findData.dwFileAttributes != INVALID_FILE_ATTRIBUTES && findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
&& findData.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
#else
Q_UNUSED(filename);
return false;
#endif
}
FileSystem::ChildResults FileSystem::isChildPathOf2(QStringView child, QStringView parent)
{
// if it is a relative path assume a local file, resolve it based on root
const auto sensitivity = Utility::fsCaseSensitivity();
// Fast-path the 3 common cases in this if-else statement:
if (parent.isEmpty()) {
// The empty parent is often used as the sync root, as (child) items do not start with a `/`
return ChildResult::IsChild | ChildResult::IsParentEmpty;
}
if (child.compare(parent, sensitivity) == 0) {
return ChildResult::IsChild | ChildResult::IsEqual;
}
const auto isSeparator = [](QChar c) { return c == QLatin1Char('/') || (Utility::isWindows() && c == QLatin1Char('\\')); };
if (isSeparator(parent.back())) {
// Here we can do a normal prefix check, because the parent is "terminated" with a slash,
// and we can't walk into the case in the else below.
if (child.startsWith(parent, sensitivity)) {
return ChildResult::IsChild;
}
// else: do the `cleanPath` version below
}
// Slow path (`QDir::cleanPath` does lots of string operations):
const QString cleanParent = QDir::cleanPath(parent.toString());
const QString cleanChild = QDir::cleanPath(child.toString());
// both paths are the same /root/foo == /root/foo
if (cleanChild.compare(cleanParent, sensitivity) == 0) {
return ChildResult::IsChild | ChildResult::IsEqual;
}
// cleanPath removes trailing slashes, add one to parent to be sure we handle a child path
// /root/foo/bar is not a child of /root/fo, therefore, the trailing slash is important
if (cleanChild.startsWith(cleanParent + QLatin1Char('/'), sensitivity)) {
return ChildResult::IsChild;
}
return ChildResult::IsNoChild;
}
QString FileSystem::createPortableFileName(const QString &path, const QString &fileName, qsizetype reservedSize)
{
// remove leading and trailing whitespace
QString tmp = pathEscape(fileName);
// limit size to fileNameMaxC
tmp.resize(std::min<qsizetype>(tmp.size(), fileNameMaxC - reservedSize));
// remove eventual trailing whitespace after the resize
tmp = tmp.trimmed();
return QDir::cleanPath(path + QLatin1Char('/') + tmp);
}
QString FileSystem::pathEscape(const QString &s)
{
QString tmp = s;
tmp.replace(QLatin1String("../"), QString(replacementCharC));
#ifdef Q_OS_WIN
tmp.replace(QLatin1String("..\\"), QString(replacementCharC));
#endif
// escape the folder separator, "\" is handled in IllegalFilenameCharsWindows
tmp.replace(QLatin1Char('/'), replacementCharC);
// replace non portable characters
for (auto c : IllegalFilenameCharsWindows) {
tmp.replace(c, replacementCharC);
}
return tmp.trimmed();
}
} // namespace OCC
#include "moc_filesystembase.cpp"
+235
View File
@@ -0,0 +1,235 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "constexpr_list.h"
#include "libsync/qsferasynclib.h"
#include "utility.h"
#include <QFileInfo>
#include <QLoggingCategory>
#include <QString>
#include <cstdint>
class QFile;
namespace OCC {
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcFileSystem)
/**
* \addtogroup libsync
* @{
*/
/**
* @brief This file contains file system helper
*/
namespace FileSystem
{
QSFERA_SYNC_EXPORT Q_NAMESPACE;
QSFERA_SYNC_EXPORT std::filesystem::path toFilesystemPath(const QString &path);
QSFERA_SYNC_EXPORT QString fromFilesystemPath(const std::filesystem::path &path);
/**
* List of characters not allowd in filenames on Windows
*/
constexpr_list auto IllegalFilenameCharsWindows = {
QLatin1Char('\\'), QLatin1Char(':'), QLatin1Char('?'), QLatin1Char('*'), QLatin1Char('"'), QLatin1Char('>'), QLatin1Char('<'), QLatin1Char('|')};
/**
* @brief Mark the file as hidden (only has effects on windows)
*/
void QSFERA_SYNC_EXPORT setFileHidden(const QString &filename, bool hidden);
/**
* @brief Marks the file as read-only.
*
* On linux this either revokes all 'w' permissions or restores permissions
* according to the umask.
*/
void QSFERA_SYNC_EXPORT setFileReadOnly(const QString &filename, bool readonly);
/**
* @brief Marks the file as read-only.
*
* It's like setFileReadOnly(), but weaker: if readonly is false and the user
* already has write permissions, no change to the permissions is made.
*
* This means that it will preserve explicitly set rw-r--r-- permissions even
* when the umask is 0002. (setFileReadOnly() would adjust to rw-rw-r--)
*/
void QSFERA_SYNC_EXPORT setFileReadOnlyWeak(const QString &filename, bool readonly);
/**
* @brief Try to set permissions so that other users on the local machine can not
* go into the folder.
*/
void QSFERA_SYNC_EXPORT setFolderMinimumPermissions(const QString &filename);
/*
* This function takes a path and converts it to a UNC representation of the
* string. That means that it prepends a \\?\ (unless already UNC) and converts
* all slashes to backslashes.
*
* Note the following:
* - The string must be absolute.
* - it needs to contain a drive character to be a valid UNC
* - A conversion is only done if the path len is larger than 245. Otherwise
* the windows API functions work with the normal "unixoid" representation too.
*/
QString QSFERA_SYNC_EXPORT longWinPath(const QString &inpath);
/**
* @brief Checks whether a file exists.
*
* Use this over QFileInfo::exists() and QFile::exists() to avoid bugs with lnk
* files, see above.
*/
bool QSFERA_SYNC_EXPORT fileExists(const QString &filename, const QFileInfo & = QFileInfo());
/**
* @brief Rename the file \a originFileName to \a destinationFileName.
*
* It behaves as QFile::rename() but handles .lnk files correctly on Windows.
*/
bool QSFERA_SYNC_EXPORT rename(const QString &originFileName, const QString &destinationFileName, QString *errorString = nullptr);
/**
* Rename the file \a originFileName to \a destinationFileName, and
* overwrite the destination if it already exists - without extra checks.
*/
bool QSFERA_SYNC_EXPORT uncheckedRenameReplace(const QString &originFileName, const QString &destinationFileName, QString *errorString);
/**
* Removes a file.
*
* Equivalent to QFile::remove(), except on Windows, where it will also
* successfully remove read-only files.
*/
bool QSFERA_SYNC_EXPORT remove(const QString &fileName, QString *errorString = nullptr);
/**
* Replacement for QFile::open(ReadOnly) followed by a seek().
* This version sets a more permissive sharing mode on Windows.
*
* Warning: The resulting file may have an empty fileName and be unsuitable for use
* with QFileInfo! Calling seek() on the QFile with >32bit signed values will fail!
*/
bool QSFERA_SYNC_EXPORT openAndSeekFileSharedRead(QFile * file, QString * error, qint64 seek);
enum class LockMode { Shared, Exclusive, SharedRead };
Q_ENUM_NS(LockMode);
/**
* Returns true when a file is locked. (Windows only)
*/
bool QSFERA_SYNC_EXPORT isFileLocked(const QString &fileName, LockMode mode);
#ifdef Q_OS_WIN
bool QSFERA_SYNC_EXPORT longPathsEnabledOnWindows();
/**
* Returns the file system used at the given path.
*/
QString QSFERA_SYNC_EXPORT fileSystemForPath(const QString &path);
#endif
/**
* Returns whether the file is a shortcut file (ends with .lnk)
*/
bool QSFERA_SYNC_EXPORT isLnkFile(const QString &filename);
/**
* Returns whether the file is a junction (windows only)
*/
bool QSFERA_SYNC_EXPORT isJunction(const QString &filename);
/**
* Returns whether a Path is a child of another
*/
enum class ChildResult : uint8_t { IsNoChild = 0, IsChild = 0x1 << 0, IsEqual = 0x1 << 1, IsParentEmpty = 0x1 << 2 };
Q_FLAG_NS(ChildResult);
Q_DECLARE_FLAGS(ChildResults, ChildResult)
Q_DECLARE_OPERATORS_FOR_FLAGS(ChildResults)
ChildResults QSFERA_SYNC_EXPORT isChildPathOf2(QStringView child, QStringView parent);
inline bool isChildPathOf(QStringView child, QStringView parent)
{
return isChildPathOf2(child, parent) & ChildResult::IsChild;
}
/**
* Ensures the file name length is allowed on all platforms and the file name does not contain illegal characters
* reservedSize: The resulting path will be reservedSize < MAX and allows appending.
*/
QString QSFERA_SYNC_EXPORT createPortableFileName(const QString &path, const QString &fileName, qsizetype reservedSize = 0);
/*
* Replace path navigation elements from the string
*/
QString QSFERA_SYNC_EXPORT pathEscape(const QString &s);
namespace SizeLiterals {
constexpr auto BinaryBase = 1024;
constexpr auto DecimalBase = 1000;
constexpr unsigned long long operator"" _B(unsigned long long sz)
{
return sz;
}
constexpr unsigned long long operator"" _KiB(unsigned long long sz)
{
return operator"" _B(sz) * BinaryBase;
}
constexpr unsigned long long operator"" _MiB(unsigned long long sz)
{
return operator"" _KiB(sz) * BinaryBase;
}
constexpr unsigned long long operator"" _GiB(unsigned long long sz)
{
return operator"" _MiB(sz) * BinaryBase;
}
constexpr unsigned long long operator"" _kB(unsigned long long sz)
{
return operator"" _B(sz) * DecimalBase;
}
constexpr unsigned long long operator"" _MB(unsigned long long sz)
{
return operator"" _kB(sz) * DecimalBase;
}
constexpr unsigned long long operator"" _GB(unsigned long long sz)
{
return operator"" _MB(sz) * DecimalBase;
}
}
}
/** @} */
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include <QtGlobal>
#include <array>
#include <functional>
#include <vector>
namespace OCC {
/**
* A Fixed sized ring buffer optimized on continouous insertion
*/
template <typename TYPE>
class FixedSizeRingBuffer
{
public:
FixedSizeRingBuffer(size_t size)
: _data(size, TYPE())
{
}
constexpr size_t capacity() const { return _data.size(); }
constexpr bool isFull() const { return size() >= _data.size(); }
constexpr size_t size() const { return _end - _start; }
constexpr bool empty() const { return size() == 0; }
// iterators pointing to the raw data, the range might be smaller than _Size
// if ever needed we could implement a iterator for the data window
auto begin() { return _data.begin(); }
auto end() { return isFull() ? _data.end() : _data.begin() + size(); }
auto cbegin() const { return _data.cbegin(); }
auto cend() const { return isFull() ? _data.cend() : _data.cbegin() + size(); }
void pop_front()
{
// move the windows without changing the underlying data
_start++;
Q_ASSERT(_start < _end);
// adjust offset to prevent overflow
const auto s = size();
_start %= _data.size();
_end = _start + s;
}
void push_back(TYPE &&data)
{
Q_ASSERT(!isFull());
_data[convertToIndex(size())] = data;
_end++;
}
const TYPE &at(size_t index) const { return _data.at(convertToIndex(index)); }
/*
* Remove items if f returns true
* The filtered result is unordered
*/
void remove_if(const std::function<bool(const TYPE &)> &f)
{
// filter and sort the data
FixedSizeRingBuffer<TYPE> tmp(_data.size());
const auto start = convertToIndex(0);
for (auto it = begin() + start; it != end(); ++it) {
if (!f(*it)) {
tmp.push_back(std::move(*it));
}
}
for (auto it = begin(); it != begin() + start; ++it) {
if (!f(*it)) {
tmp.push_back(std::move(*it));
}
}
*this = std::move(tmp);
}
void reset(std::vector<TYPE> &&data)
{
Q_ASSERT(data.size() <= _data.size());
_start = 0;
_end = data.size();
std::move(data.begin(), data.end(), _data.begin());
}
private:
std::vector<TYPE> _data;
// the sliding window of the ring buffer
size_t _start = 0;
size_t _end = 0;
// converts an array index to the underlying array index
constexpr size_t convertToIndex(size_t i) const { return (_start + i) % _data.size(); }
};
}
+515
View File
@@ -0,0 +1,515 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QString>
#include "common/asserts.h"
#include "common/filesystembase.h"
#include "common/utility.h"
#include "ownsql.h"
#include <sqlite3.h>
#include <thread>
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono_literals;
namespace {
constexpr int SQLITE_REPEAT_COUNT = 20;
}
#define SQLITE_DO(A) \
if (1) { \
_errId = (A); \
if (_errId != SQLITE_OK && _errId != SQLITE_DONE && _errId != SQLITE_ROW) { \
_error = QString::fromUtf8(sqlite3_errmsg(_db)); \
} \
}
namespace OCC {
Q_LOGGING_CATEGORY(lcSql, "sync.database.sql", QtInfoMsg)
SqlDatabase::SqlDatabase()
: _db(nullptr)
, _errId(0)
{
}
SqlDatabase::~SqlDatabase()
{
close();
}
bool SqlDatabase::isOpen() const
{
return _db != nullptr;
}
bool SqlDatabase::openHelper(const QString &filename, int sqliteFlags)
{
if (isOpen()) {
return true;
}
sqliteFlags |= SQLITE_OPEN_NOMUTEX;
#ifdef Q_OS_WIN
// allow file paths > 260
// https://www.sqlite.org/vfs.html#standard_windows_vfses
const char *vfs = "win32-longpath";
#else
char *vfs = nullptr;
#endif
SQLITE_DO(sqlite3_open_v2(FileSystem::longWinPath(filename).toUtf8().constData(), &_db, sqliteFlags, vfs));
if (_errId != SQLITE_OK) {
qCWarning(lcSql) << u"Error:" << _error << u"for" << filename;
if (_errId == SQLITE_CANTOPEN) {
qCWarning(lcSql) << u"CANTOPEN extended errcode: " << sqlite3_extended_errcode(_db);
#if SQLITE_VERSION_NUMBER >= 3012000
qCWarning(lcSql) << u"CANTOPEN system errno: " << sqlite3_system_errno(_db);
#endif
}
close();
return false;
}
if (!_db) {
qCWarning(lcSql) << u"Error: no database for" << filename;
return false;
}
sqlite3_busy_timeout(_db, 5000);
return true;
}
SqlDatabase::CheckDbResult SqlDatabase::checkDb()
{
// quick_check can fail with a disk IO error when diskspace is low
SqlQuery quick_check(*this);
if (quick_check.prepare("PRAGMA quick_check;", /*allow_failure=*/true) != SQLITE_OK) {
qCWarning(lcSql) << u"Error preparing quick_check on database";
_errId = quick_check.errorId();
_error = quick_check.error();
return CheckDbResult::CantPrepare;
}
if (!quick_check.exec()) {
qCWarning(lcSql) << u"Error running quick_check on database";
_errId = quick_check.errorId();
_error = quick_check.error();
return CheckDbResult::CantExec;
}
quick_check.next();
QString result = quick_check.stringValue(0);
if (result != QLatin1String("ok")) {
qCWarning(lcSql) << u"quick_check returned failure:" << result;
return CheckDbResult::NotOk;
}
return CheckDbResult::Ok;
}
bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
{
if (isOpen()) {
return true;
}
if (!openHelper(filename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)) {
return false;
}
auto checkResult = checkDb();
if (checkResult != CheckDbResult::Ok) {
close();
if (checkResult == CheckDbResult::CantPrepare) {
// When disk space is low, preparing may fail even though the db is fine.
// Typically CANTOPEN or IOERR.
qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath());
if (freeSpace != -1 && freeSpace < 1000000) {
qCWarning(lcSql) << u"Can't prepare consistency check and disk space is low:" << freeSpace;
} else if (_errId == SQLITE_CANTOPEN) {
// Even when there's enough disk space, it might very well be that the
// file is on a read-only filesystem and can't be opened because of that.
qCWarning(lcSql) << u"Can't open db to prepare consistency check, aborting";
} else if (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY) {
qCWarning(lcSql) << u"Can't open db to prepare consistency check, the db is locked aborting" << _errId << _error;
}
return false;
}
qCCritical(lcSql) << u"Consistency check failed, removing broken db" << filename;
QFile fileToRemove(filename);
if (!fileToRemove.remove()) {
qCCritical(lcSql) << u"Failed to remove broken db" << filename << u":" << fileToRemove.errorString();
return false;
}
return openHelper(filename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
}
return true;
}
bool SqlDatabase::openReadOnly(const QString &filename)
{
if (isOpen()) {
return true;
}
if (!openHelper(filename, SQLITE_OPEN_READONLY)) {
return false;
}
if (checkDb() != CheckDbResult::Ok) {
qCWarning(lcSql) << u"Consistency check failed in readonly mode, giving up" << filename;
close();
return false;
}
return true;
}
QString SqlDatabase::error() const
{
const QString err(_error);
// _error.clear();
return err;
}
void SqlDatabase::close()
{
if (_db) {
// we need a copy because query is removed from a set in finish()
const auto queries = _queries;
for (auto *q : queries) {
q->finish();
}
SQLITE_DO(sqlite3_close(_db));
if (_errId != SQLITE_OK)
qCWarning(lcSql) << u"Closing database failed" << _error;
_db = nullptr;
}
}
bool SqlDatabase::transaction()
{
if (!_db) {
return false;
}
SQLITE_DO(sqlite3_exec(_db, "BEGIN", nullptr, nullptr, nullptr));
return _errId == SQLITE_OK;
}
bool SqlDatabase::commit()
{
if (!_db) {
return false;
}
SQLITE_DO(sqlite3_exec(_db, "COMMIT", nullptr, nullptr, nullptr));
return _errId == SQLITE_OK;
}
sqlite3 *SqlDatabase::sqliteDb()
{
return _db;
}
/* =========================================================================================== */
SqlQuery::SqlQuery(SqlDatabase &db)
: _sqldb(&db)
, _db(db.sqliteDb())
{
}
SqlQuery::~SqlQuery()
{
if (_stmt) {
finish();
}
}
SqlQuery::SqlQuery(const QByteArray &sql, SqlDatabase &db)
: _sqldb(&db)
, _db(db.sqliteDb())
{
prepare(sql);
}
int SqlQuery::prepare(const QByteArray &sql, bool allow_failure)
{
_sql = sql.trimmed();
if (_stmt) {
finish();
}
if (!_sql.isEmpty()) {
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
qCDebug(lcSql) << u"SQL prepare" << _sql << u"Try:" << n;
_errId = sqlite3_prepare_v2(_db, _sql.constData(), -1, &_stmt, nullptr);
if (_errId != SQLITE_OK) {
qCWarning(lcSql) << u"SQL prepare failed" << _sql << QString::fromUtf8(sqlite3_errmsg(_db));
if ((_errId == SQLITE_BUSY) || (_errId == SQLITE_LOCKED)) {
continue;
}
}
break;
}
if (_errId != SQLITE_OK) {
_error = QString::fromUtf8(sqlite3_errmsg(_db));
qCWarning(lcSql) << u"Sqlite prepare statement error:" << _errId << _error << u"in" << _sql;
OC_ENFORCE_X(allow_failure, "SQLITE Prepare error");
} else {
OC_ASSERT(_stmt);
_sqldb->_queries.insert(this);
if (lcSql().isDebugEnabled()) {
_boundValues.resize(sqlite3_bind_parameter_count(_stmt));
for (int i = 1; i <= _boundValues.size(); ++i) {
const auto name = QRegularExpression::escape(QString::fromUtf8(sqlite3_bind_parameter_name(_stmt, i)));
_boundValues[i - 1].name = QRegularExpression(uR"(%1\b)"_s.arg(name));
}
}
}
}
return _errId;
}
/**
* There is no overloads to QByteArray::startWith that takes Qt::CaseInsensitive.
* Returns true if 'a' starts with 'b' in a case insensitive way
*/
static bool startsWithInsensitive(const QByteArray &a, const QByteArray &b)
{
return a.size() >= b.size() && qstrnicmp(a.constData(), b.constData(), b.size()) == 0;
}
bool SqlQuery::isSelect()
{
return startsWithInsensitive(_sql, QByteArrayLiteral("SELECT"));
}
bool SqlQuery::isPragma()
{
return startsWithInsensitive(_sql, QByteArrayLiteral("PRAGMA"));
}
bool SqlQuery::exec()
{
if (!_stmt) {
qCWarning(lcSql) << u"Can't exec query, statement unprepared.";
return false;
}
// Don't do anything for selects, that is how we use the lib :-|
if (!isSelect() && !isPragma()) {
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
if (lcSql().isDebugEnabled()) {
if (!_boundValues.isEmpty()) {
// Try to create a pretty printed version of the sql query
// the result can be inaccurate
QString query = QString::fromUtf8(_sql);
for (const auto &v : _boundValues) {
query.replace(v.name, v.value);
}
char *actualQuery = sqlite3_expanded_sql(_stmt);
qCDebug(lcSql) << u"SQL exec: Estimated query:" << query << u"Actual query:" << QString::fromUtf8(actualQuery) << u"Try:" << n;
sqlite3_free(actualQuery);
} else {
qCDebug(lcSql) << u"SQL exec:" << _sql << u"Try:" << n;
}
}
_errId = sqlite3_step(_stmt);
if (_errId != SQLITE_DONE && _errId != SQLITE_ROW) {
qCWarning(lcSql) << u"SQL exec failed" << _sql << QString::fromUtf8(sqlite3_errmsg(_db));
if (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY) {
continue;
}
}
break;
}
if (_errId != SQLITE_DONE && _errId != SQLITE_ROW) {
_error = QString::fromUtf8(sqlite3_errmsg(_db));
qCWarning(lcSql) << u"Sqlite exec statement error:" << _errId << _error << u"in" << _sql;
if (_errId == SQLITE_IOERR) {
qCWarning(lcSql) << u"IOERR extended errcode: " << sqlite3_extended_errcode(_db);
#if SQLITE_VERSION_NUMBER >= 3012000
qCWarning(lcSql) << u"IOERR system errno: " << sqlite3_system_errno(_db);
#endif
}
} else {
qCDebug(lcSql) << u"Last exec affected" << numRowsAffected() << u"rows.";
}
return (_errId == SQLITE_DONE); // either SQLITE_ROW or SQLITE_DONE
}
return true;
}
auto SqlQuery::next() -> NextResult
{
const bool firstStep = !sqlite3_stmt_busy(_stmt);
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
_errId = sqlite3_step(_stmt);
if (!(firstStep && (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY))) {
// For the first step: retry if the database is locked or busy.
// For any other iteration: the first one succeeded, so subsequent steps should also
// succeed. If not, bail out and report an error (done below).
break;
}
}
NextResult result;
result.ok = _errId == SQLITE_ROW || _errId == SQLITE_DONE;
result.hasData = _errId == SQLITE_ROW;
if (!result.ok) {
_error = QString::fromUtf8(sqlite3_errmsg(_db));
qCWarning(lcSql) << u"Sqlite step statement error:" << _errId << _error << u"in" << _sql;
}
return result;
}
void SqlQuery::bindValueInternal(int pos, const QVariant &value)
{
int res = -1;
if (!_stmt) {
OC_ASSERT(false);
return;
}
switch (value.typeId()) {
case QMetaType::Int:
case QMetaType::Bool:
res = sqlite3_bind_int(_stmt, pos, value.toInt());
break;
case QMetaType::Double:
res = sqlite3_bind_double(_stmt, pos, value.toDouble());
break;
case QMetaType::UInt:
case QMetaType::Long:
case QMetaType::ULong:
case QMetaType::LongLong:
case QMetaType::ULongLong:
res = sqlite3_bind_int64(_stmt, pos, value.toLongLong());
break;
case QMetaType::QString: {
if (!value.isNull()) {
// lifetime of string == lifetime of its qvariant
const QString *str = static_cast<const QString *>(value.constData());
res = sqlite3_bind_text16(_stmt, pos, str->utf16(), static_cast<int>(str->size() * sizeof(ushort)), SQLITE_TRANSIENT);
} else {
res = sqlite3_bind_null(_stmt, pos);
}
break;
}
case QMetaType::QByteArray: {
auto ba = value.toByteArray();
res = sqlite3_bind_text(_stmt, pos, ba.constData(), ba.size(), SQLITE_TRANSIENT);
break;
}
default: {
qCWarning(lcSql) << u"Unsupported raw type detected" << value.typeId() << value.typeName() << value.metaType().sizeOf() << value;
break;
}
}
if (res != SQLITE_OK) {
qCWarning(lcSql) << u"ERROR binding SQL value:" << value << u"error:" << res;
}
OC_ASSERT(res == SQLITE_OK);
}
bool SqlQuery::nullValue(int index)
{
return sqlite3_column_type(_stmt, index) == SQLITE_NULL;
}
QString SqlQuery::stringValue(int index)
{
return QString::fromUtf16(static_cast<const char16_t *>(sqlite3_column_text16(_stmt, index)));
}
int SqlQuery::intValue(int index)
{
return sqlite3_column_int(_stmt, index);
}
quint64 SqlQuery::int64Value(int index)
{
return sqlite3_column_int64(_stmt, index);
}
QByteArray SqlQuery::baValue(int index)
{
return QByteArray(static_cast<const char *>(sqlite3_column_blob(_stmt, index)), sqlite3_column_bytes(_stmt, index));
}
QString SqlQuery::error() const
{
return _error;
}
int SqlQuery::errorId() const
{
return _errId;
}
const QByteArray &SqlQuery::lastQuery() const
{
return _sql;
}
int SqlQuery::numRowsAffected()
{
return sqlite3_changes(_db);
}
void SqlQuery::finish()
{
if (!_stmt)
return;
SQLITE_DO(sqlite3_finalize(_stmt));
_stmt = nullptr;
if (_sqldb) {
_sqldb->_queries.remove(this);
}
}
void SqlQuery::reset_and_clear_bindings()
{
if (_stmt) {
SQLITE_DO(sqlite3_reset(_stmt));
SQLITE_DO(sqlite3_clear_bindings(_stmt));
}
}
} // namespace OCC
+193
View File
@@ -0,0 +1,193 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef OWNSQL_H
#define OWNSQL_H
#include <QLoggingCategory>
#include <QObject>
#include <QSet>
#include <QVariant>
#include "libsync/qsferasynclib.h"
#include <QRegularExpression>
struct sqlite3;
struct sqlite3_stmt;
namespace OCC {
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcSql)
class SqlQuery;
/**
* @brief The SqlDatabase class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT SqlDatabase
{
Q_DISABLE_COPY(SqlDatabase)
public:
explicit SqlDatabase();
~SqlDatabase();
bool isOpen() const;
bool openOrCreateReadWrite(const QString &filename);
bool openReadOnly(const QString &filename);
bool transaction();
bool commit();
void close();
QString error() const;
sqlite3 *sqliteDb();
private:
enum class CheckDbResult {
Ok,
CantPrepare,
CantExec,
NotOk,
};
bool openHelper(const QString &filename, int sqliteFlags);
CheckDbResult checkDb();
sqlite3 *_db;
QString _error; // last error string
int _errId;
friend class SqlQuery;
QSet<SqlQuery *> _queries;
};
/**
* @brief The SqlQuery class
* @ingroup libsync
*
* There is basically 3 ways to initialize and use a query:
*
SqlQuery q1;
[...]
q1.initOrReset(...);
q1.bindValue(...);
q1.exec(...)
*
SqlQuery q2(db);
q2.prepare(...);
[...]
q2.reset_and_clear_bindings();
q2.bindValue(...);
q2.exec(...)
*
SqlQuery q3("...", db);
q3.bindValue(...);
q3.exec(...)
*
*/
class QSFERA_SYNC_EXPORT SqlQuery
{
Q_DISABLE_COPY(SqlQuery)
public:
explicit SqlQuery() = default;
explicit SqlQuery(SqlDatabase &db);
explicit SqlQuery(const QByteArray &sql, SqlDatabase &db);
/**
* Prepare the SqlQuery.
* If the query was already prepared, this will first call finish(), and re-prepare it.
* This function must only be used if the constructor was setting a SqlDatabase
*/
int prepare(const QByteArray &sql, bool allow_failure = false);
~SqlQuery();
QString error() const;
int errorId() const;
/// Checks whether the value at the given column index is NULL
bool nullValue(int index);
QString stringValue(int index);
int intValue(int index);
quint64 int64Value(int index);
QByteArray baValue(int index);
bool isSelect();
bool isPragma();
bool exec();
struct NextResult
{
bool ok = false;
bool hasData = false;
};
NextResult next();
template <class T>
void bindValue(int pos, const T &value)
{
const auto converted = convertValue(value);
if (lcSql().isDebugEnabled() && !_boundValues.isEmpty()) {
QString sqlValue;
auto stream = QDebug(&sqlValue).noquote().nospace() << '\'' << value;
if (typeid(converted) != typeid(value)) {
stream << " [" << converted << ']';
}
stream << '\'';
_boundValues[pos - 1].value = sqlValue;
}
bindValueInternal(pos, QVariant::fromValue(converted));
}
const QByteArray &lastQuery() const;
int numRowsAffected();
void reset_and_clear_bindings();
private:
template <class T, typename std::enable_if<std::is_enum<T>::value, int>::type = 0>
auto convertValue(const T &value)
{
return static_cast<int>(value);
}
template <class T, typename std::enable_if<!std::is_enum<T>::value, int>::type = 0>
auto convertValue(const T &value)
{
return value;
}
void bindValueInternal(int pos, const QVariant &value);
void finish();
SqlDatabase *_sqldb = nullptr;
sqlite3 *_db = nullptr;
sqlite3_stmt *_stmt = nullptr;
QString _error;
int _errId;
QByteArray _sql;
struct Binding
{
QRegularExpression name;
QString value;
};
QVector<Binding> _boundValues;
friend class SqlDatabase;
friend class PreparedSqlQueryManager;
};
} // namespace OCC
#endif // OWNSQL_H
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "pinstate.h"
#include "moc_pinstate.cpp"
#include <QCoreApplication>
using namespace OCC;
template <>
QString Utility::enumToDisplayName(VfsItemAvailability availability)
{
switch (availability) {
case VfsItemAvailability::AlwaysLocal:
return QCoreApplication::translate("pinstate", "Always available locally");
case VfsItemAvailability::AllHydrated:
return QCoreApplication::translate("pinstate", "Currently available locally");
case VfsItemAvailability::Mixed:
return QCoreApplication::translate("pinstate", "Some available online only");
case VfsItemAvailability::AllDehydrated:
[[fallthrough]];
case VfsItemAvailability::OnlineOnly:
return QCoreApplication::translate("pinstate", "Available online only");
}
Q_UNREACHABLE();
}
+142
View File
@@ -0,0 +1,142 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef PINSTATE_H
#define PINSTATE_H
#include "libsync/qsferasynclib.h"
#include "utility.h"
#include <QObject>
namespace OCC {
namespace PinStateEnums {
QSFERA_SYNC_EXPORT Q_NAMESPACE
/** Determines whether items should be available locally permanently or not
*
* The idea is that files and folders can be marked with the user intent
* on availability.
*
* The Inherited state is used for resetting a pin state to what its
* parent path would do.
*
* The pin state of a directory usually only matters for the initial pin and
* hydration state of new remote files. It's perfectly possible for a
* AlwaysLocal directory to have only OnlineOnly items. (though setting pin
* states is usually done recursively, so one'd need to set the folder to
* pinned and then each contained item to unpinned)
*
* Note: This enum intentionally mimics CF_PIN_STATE of Windows cfapi.
*/
enum class PinState : uint8_t {
/** The pin state is derived from the state of the parent folder.
*
* For example new remote files start out in this state, following
* the state of their parent folder.
*
* This state is used purely for resetting pin states to their derived
* value. The effective state for an item will never be "Inherited".
*/
Inherited = 0,
/** The file shall be available and up to date locally.
*
* Also known as "pinned". Pinned dehydrated files shall be hydrated
* as soon as possible.
*/
AlwaysLocal = 1,
/** File shall be a dehydrated placeholder, filled on demand.
*
* Also known as "unpinned". Unpinned hydrated files shall be dehydrated
* as soon as possible.
*
* If a unpinned file becomes hydrated (such as due to an implicit hydration
* where the user requested access to the file's data) its pin state changes
* to Unspecified.
*/
OnlineOnly = 2,
/** The user hasn't made a decision. The client or platform may hydrate or
* dehydrate as they see fit.
*
* New remote files in unspecified directories start unspecified, and
* dehydrated (which is an arbitrary decision).
*/
Unspecified = 3,
/**
* Special case for files that are excluded
*/
Excluded = 4
};
Q_ENUM_NS(PinState);
/** A user-facing version of PinState.
*
* PinStates communicate availability intent for an item, but particular
* situations can get complex: An AlwaysLocal folder can have OnlineOnly
* files or directories.
*
* For users this is condensed to a few useful cases.
*
* Note that this is only about *intent*. The file could still be out of date,
* or not have been synced for other reasons, like errors.
*
* NOTE: The numerical values and ordering of this enum are relevant.
*/
enum class VfsItemAvailability : uint8_t {
/** The item and all its subitems are hydrated and pinned AlwaysLocal.
*
* This guarantees that all contents will be kept in sync.
*/
AlwaysLocal = 0,
/** The item and all its subitems are hydrated.
*
* This may change if the platform or client decide to dehydrate items
* that have Unspecified pin state.
*
* A folder with no file contents will have this availability.
*/
AllHydrated = 1,
/** There are dehydrated and hydrated items.
*
* This would happen if a dehydration happens to a Unspecified item that
* used to be hydrated.
*/
Mixed = 2,
/** There are only dehydrated items but the pin state isn't all OnlineOnly.
*/
AllDehydrated = 3,
/** The item and all its subitems are dehydrated and OnlineOnly.
*
* This guarantees that contents will not take up space.
*/
OnlineOnly = 4,
};
Q_ENUM_NS(VfsItemAvailability)
}
using namespace PinStateEnums;
template <>
QSFERA_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability);
}
#endif
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (C) by Dominik Schmidt <dschmidt@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "plugin.h"
namespace OCC {
PluginFactory::~PluginFactory() = default;
bool PluginFactory::checkAvailability() const
{
return true;
}
QString pluginFileName(const QString &type, const QString &name)
{
return QStringLiteral("QSfera_%2_%3").arg(type, name);
}
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) by Dominik Schmidt <dschmidt@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "libsync/common/result.h"
#include "libsync/qsferasynclib.h"
#include <QObject>
namespace OCC {
class QSFERA_SYNC_EXPORT PluginFactory
{
public:
virtual ~PluginFactory();
virtual QObject *create(QObject *parent) = 0;
[[nodiscard]] virtual bool checkAvailability() const;
/**
* @param path The path for which the plugin should be prepared
* @param accountUuid The UUID of the account for which the plugin should be prepared (might be null during account setup)
* @return Nothing or an error string
*/
[[nodiscard]] virtual Result<void, QString> prepare(const QString &path, const QUuid &accountUuid) const = 0;
};
template <class PluginClass>
class DefaultPluginFactory : public PluginFactory
{
public:
QObject *create(QObject *parent) override { return new PluginClass(parent); }
};
/// Return the expected name of a plugin, for use with QPluginLoader
QString pluginFileName(const QString &type, const QString &name);
}
Q_DECLARE_INTERFACE(OCC::PluginFactory, "eu.qsfera.PluginFactory")
@@ -0,0 +1,56 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "preparedsqlquerymanager.h"
#include <sqlite3.h>
using namespace OCC;
PreparedSqlQuery::PreparedSqlQuery(SqlQuery *query, bool ok)
: _query(query)
, _ok(ok)
{
}
PreparedSqlQuery::~PreparedSqlQuery()
{
_query->reset_and_clear_bindings();
}
const PreparedSqlQuery PreparedSqlQueryManager::get(PreparedSqlQueryManager::Key key)
{
auto &query = _queries[key];
OC_ENFORCE(query._stmt);
Q_ASSERT(!sqlite3_stmt_busy(query._stmt));
return {&query};
}
const PreparedSqlQuery PreparedSqlQueryManager::get(PreparedSqlQueryManager::Key key, const QByteArray &sql, SqlDatabase &db)
{
auto &query = _queries[key];
Q_ASSERT(!sqlite3_stmt_busy(query._stmt));
OC_ENFORCE(!query._sqldb || &db == query._sqldb);
if (!query._stmt) {
query._sqldb = &db;
query._db = db.sqliteDb();
return {&query, query.prepare(sql) == 0};
}
return {&query};
}
@@ -0,0 +1,117 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "common/asserts.h"
#include "libsync/qsferasynclib.h"
#include "ownsql.h"
namespace OCC {
class QSFERA_SYNC_EXPORT PreparedSqlQuery
{
public:
~PreparedSqlQuery();
operator bool() const { return _ok; }
SqlQuery *operator->() const
{
Q_ASSERT(_ok);
return _query;
}
SqlQuery &operator*() const &
{
Q_ASSERT(_ok);
return *_query;
}
private:
PreparedSqlQuery(SqlQuery *query, bool ok = true);
SqlQuery *_query;
bool _ok;
friend class PreparedSqlQueryManager;
};
/**
* @brief Manage PreparedSqlQuery
*/
class QSFERA_SYNC_EXPORT PreparedSqlQueryManager
{
public:
enum Key {
GetFileRecordQuery,
GetFileRecordQueryByInode,
GetFileRecordQueryByFileId,
GetFilesBelowPathQuery,
GetAllFilesQuery,
ListFilesInPathQuery,
SetFileRecordQuery,
SetFileRecordChecksumQuery,
GetDownloadInfoQuery,
SetDownloadInfoQuery,
DeleteDownloadInfoQuery,
GetUploadInfoQuery,
GetAllUploadInfoQuery,
SetUploadInfoQuery,
DeleteUploadInfoQuery,
DeleteFileRecordPhash,
DeleteFileRecordRecursively,
GetErrorBlacklistQuery,
SetErrorBlacklistQuery,
GetSelectiveSyncListQuery,
GetChecksumTypeIdQuery,
GetChecksumTypeQuery,
InsertChecksumTypeQuery,
GetDataFingerprintQuery,
SetDataFingerprintQuery1,
SetDataFingerprintQuery2,
GetConflictRecordQuery,
SetConflictRecordQuery,
DeleteConflictRecordQuery,
GetRawPinStateQuery,
GetEffectivePinStateQuery,
GetSubPinsQuery,
CountDehydratedFilesQuery,
SetPinStateQuery,
WipePinStateQuery,
GetFileReocrdsWithDirtyPlaceholdersQuery,
PreparedQueryCount
};
PreparedSqlQueryManager() = default;
/**
* The queries are reset in the destructor to prevent wal locks
*/
const PreparedSqlQuery get(Key key);
/**
* Prepare the SqlQuery if it was not prepared yet.
*/
const PreparedSqlQuery get(Key key, const QByteArray &sql, SqlDatabase &db);
private:
SqlQuery _queries[PreparedQueryCount];
Q_DISABLE_COPY(PreparedSqlQueryManager)
};
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "remotepermissions.h"
#include <cstring>
namespace OCC {
static const char letters[] = " WDNVCKRSMmz";
template <typename Char>
void RemotePermissions::fromArray(const Char *p)
{
_value = notNullMask;
if (!p)
return;
while (*p) {
if (auto res = std::strchr(letters, static_cast<char>(*p)))
_value |= (1 << (res - letters));
++p;
}
}
QByteArray RemotePermissions::toDbValue() const
{
QByteArray result;
if (isNull())
return result;
result.reserve(PermissionsCount);
for (uint i = 1; i <= PermissionsCount; ++i) {
if (_value & (1 << i))
result.append(letters[i]);
}
if (result.isEmpty()) {
// Make sure it is not empty so we can differentiate null and empty permissions
result.append(' ');
}
return result;
}
QString RemotePermissions::toString() const
{
return QString::fromUtf8(toDbValue());
}
RemotePermissions RemotePermissions::fromDbValue(const QByteArray &value)
{
if (value.isEmpty())
return RemotePermissions();
RemotePermissions perm;
perm.fromArray(value.constData());
return perm;
}
RemotePermissions RemotePermissions::fromServerString(const QString &value)
{
RemotePermissions perm;
perm.fromArray(value.utf16());
return perm;
}
} // namespace OCC
@@ -0,0 +1,92 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QDebug>
#include <QMetaType>
#include <QString>
namespace OCC {
/**
* Class that store in a memory efficient way the remote permission
*/
class QSFERA_SYNC_EXPORT RemotePermissions
{
private:
// The first bit tells if the value is set or not
// The remaining bits correspond to know if the value is set
quint16 _value = 0;
static constexpr int notNullMask = 0x1;
template <typename Char> // can be 'char' or 'ushort' if conversion from QString
void fromArray(const Char *p);
public:
enum Permissions {
CanWrite = 1, // W
CanDelete = 2, // D
CanRename = 3, // N
CanMove = 4, // V
CanAddFile = 5, // C
CanAddSubDirectories = 6, // K
CanReshare = 7, // R
// Note: on the server, this means SharedWithMe, but in discoveryphase.cpp we also set
// this permission when the server reports the any "share-types"
IsShared = 8, // S
IsMounted = 9, // M
IsMountedSub = 10, // m (internal: set if the parent dir has IsMounted)
DeprecatedRemoved = 11, // z (Deprecated, don't use)
// Note: when adding support for more permissions, we need to invalid the cache in the database.
// (by setting forceRemoteDiscovery in SyncJournalDb::checkConnect)
PermissionsCount = DeprecatedRemoved
};
/// null permissions
RemotePermissions() = default;
/// array with one character per permission, "" is null, " " is non-null but empty
QByteArray toDbValue() const;
/// output for display purposes, no defined format (same as toDbValue in practice)
QString toString() const;
/// read value that was written with toDbValue()
static RemotePermissions fromDbValue(const QByteArray &);
/// read a permissions string received from the server, never null
static RemotePermissions fromServerString(const QString &);
bool hasPermission(Permissions p) const { return _value & (1 << static_cast<int>(p)); }
void setPermission(Permissions p) { _value |= (1 << static_cast<int>(p)) | notNullMask; }
void unsetPermission(Permissions p) { _value &= ~(1 << static_cast<int>(p)); }
bool isNull() const { return !(_value & notNullMask); }
friend bool operator==(RemotePermissions a, RemotePermissions b) { return a._value == b._value; }
friend bool operator!=(RemotePermissions a, RemotePermissions b) { return !(a == b); }
friend QDebug operator<<(QDebug &dbg, RemotePermissions p) { return dbg << p.toString(); }
};
} // namespace OCC
Q_DECLARE_METATYPE(OCC::RemotePermissions)
@@ -0,0 +1,74 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "restartmanager.h"
#include "utility.h"
#include <QCoreApplication>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QProcess>
#include <QTimer>
Q_LOGGING_CATEGORY(lcRestart, "sync.restartmanager", QtInfoMsg)
using namespace OCC;
RestartManager *RestartManager::_instance = nullptr;
RestartManager::RestartManager(std::function<int(int, char **)> &&main)
: _main(main)
{
Q_ASSERT(!_instance);
_instance = this;
}
RestartManager::~RestartManager()
{
if (!_applicationToRestart.isEmpty()) {
QProcess process;
process.setProgram(_applicationToRestart);
process.setArguments(_args);
qint64 pid;
qCDebug(lcRestart) << u"Detaching" << _applicationToRestart << _args;
if (process.startDetached(&pid)) {
qCDebug(lcRestart) << u"Successfully restarted. New process PID" << pid;
} else {
qCCritical(lcRestart) << u"Failed to restart" << process.error() << process.errorString();
}
}
}
int RestartManager::exec(int argc, char **argv) const
{
return _main(argc, argv);
}
void RestartManager::requestRestart()
{
Q_ASSERT(_instance);
qCInfo(lcRestart) << u"Restarting application with PID" << QCoreApplication::applicationPid();
QString pathToLaunch = QCoreApplication::applicationFilePath();
#ifdef Q_OS_LINUX
if (Utility::runningInAppImage()) {
pathToLaunch = Utility::appImageLocation();
}
#endif
_instance->_applicationToRestart = QFileInfo(pathToLaunch).absoluteFilePath();
// remove arg0
_instance->_args = QCoreApplication::arguments().sliced(1);
QTimer::singleShot(0, QCoreApplication::instance(), &QCoreApplication::quit);
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QStringList>
#include <functional>
namespace OCC {
class QSFERA_SYNC_EXPORT RestartManager
{
public:
RestartManager(std::function<int(int, char **)> &&main);
~RestartManager();
int exec(int argc, char **argv) const;
static void requestRestart();
private:
static RestartManager *_instance;
std::function<int(int, char **)> _main;
QString _applicationToRestart;
QStringList _args;
};
}
+174
View File
@@ -0,0 +1,174 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "asserts.h"
namespace OCC {
/**
* A Result of type T, or an Error
*/
template <typename T, typename Error>
class Result
{
union {
T _result;
Error _error;
};
bool _isError;
public:
Result(T value)
: _result(std::move(value))
, _isError(false)
{
static_assert(!std::is_same<T, bool>::value, "Bool is not supported as this class overrides the bool operator");
}
// TODO: This doesn't work if T and Error are too similar
Result(Error error)
: _error(std::move(error))
, _isError(true)
{
}
Result(Result &&other)
: _isError(other._isError)
{
if (_isError) {
new (&_error) Error(std::move(other._error));
} else {
new (&_result) T(std::move(other._result));
}
}
Result(const Result &other)
: _isError(other._isError)
{
if (_isError) {
new (&_error) Error(other._error);
} else {
new (&_result) T(other._result);
}
}
Result &operator=(Result &&other)
{
if (&other != this) {
_isError = other._isError;
if (_isError) {
new (&_error) Error(std::move(other._error));
} else {
new (&_result) T(std::move(other._result));
}
}
return *this;
}
Result &operator=(const Result &other)
{
if (&other != this) {
_isError = other._isError;
if (_isError) {
new (&_error) Error(other._error);
} else {
new (&_result) T(other._result);
}
}
return *this;
}
~Result()
{
if (_isError)
_error.~Error();
else
_result.~T();
}
explicit operator bool() const { return !_isError; }
const T &operator*() const &
{
OC_ASSERT(!_isError);
return _result;
}
T operator*() &&
{
OC_ASSERT(!_isError);
return std::move(_result);
}
const T *operator->() const
{
OC_ASSERT(!_isError);
return &_result;
}
const T &get() const
{
OC_ASSERT(!_isError);
return _result;
}
const Error &error() const &
{
OC_ASSERT(_isError);
return _error;
}
Error error() &&
{
OC_ASSERT(_isError);
return std::move(_error);
}
};
namespace detail {
struct NoResultData
{
};
}
template <typename Error>
class Result<void, Error> : public Result<detail::NoResultData, Error>
{
public:
using Result<detail::NoResultData, Error>::Result;
Result()
: Result(detail::NoResultData{})
{
}
};
namespace detail {
struct OptionalNoErrorData
{
};
}
template <typename T>
class Optional : public Result<T, detail::OptionalNoErrorData>
{
public:
using Result<T, detail::OptionalNoErrorData>::Result;
Optional()
: Optional(detail::OptionalNoErrorData{})
{
}
};
} // namespace OCC
@@ -0,0 +1,95 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "syncfilestatus.h"
#include <QDebug>
namespace OCC {
SyncFileStatus::SyncFileStatus()
: _tag(StatusNone)
, _shared(false)
{
}
SyncFileStatus::SyncFileStatus(SyncFileStatusTag tag)
: _tag(tag)
, _shared(false)
{
}
void SyncFileStatus::set(SyncFileStatusTag tag)
{
_tag = tag;
}
SyncFileStatus::SyncFileStatusTag SyncFileStatus::tag() const
{
return _tag;
}
void SyncFileStatus::setShared(bool isShared)
{
_shared = isShared;
}
bool SyncFileStatus::shared() const
{
return _shared;
}
QString SyncFileStatus::toSocketAPIString() const
{
QString statusString;
bool canBeShared = true;
switch (_tag) {
case StatusNone:
statusString = QStringLiteral("NOP");
canBeShared = false;
break;
case StatusSync:
statusString = QStringLiteral("SYNC");
break;
case StatusWarning:
// The protocol says IGNORE, but all implementations show a yellow warning sign.
statusString = QStringLiteral("IGNORE");
break;
case StatusUpToDate:
statusString = QStringLiteral("OK");
break;
case StatusError:
statusString = QStringLiteral("ERROR");
break;
case StatusExcluded:
// The protocol says IGNORE, but all implementations show a yellow warning sign.
statusString = QStringLiteral("IGNORE");
break;
}
if (canBeShared && _shared) {
statusString += QLatin1String("+SWM");
}
return statusString;
}
}
QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item)
{
QDebugStateSaver saver(debug);
debug.setAutoInsertSpaces(false);
debug << u"OCC::SyncFileStatus(shared=" << item.shared() << u", tag=" << item.tag() << u")";
return debug;
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef SYNCFILESTATUS_H
#define SYNCFILESTATUS_H
#include <QMetaType>
#include <QObject>
#include <QString>
#include "libsync/qsferasynclib.h"
namespace OCC {
/**
* @brief The SyncFileStatus class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT SyncFileStatus
{
Q_GADGET
public:
enum SyncFileStatusTag : uint8_t {
StatusNone,
StatusSync,
StatusWarning,
StatusUpToDate,
StatusError,
StatusExcluded,
};
Q_ENUM(SyncFileStatusTag);
SyncFileStatus();
SyncFileStatus(SyncFileStatusTag);
void set(SyncFileStatusTag tag);
SyncFileStatusTag tag() const;
void setShared(bool isShared);
bool shared() const;
QString toSocketAPIString() const;
private:
SyncFileStatusTag _tag;
bool _shared;
};
inline bool operator==(const SyncFileStatus &a, const SyncFileStatus &b)
{
return a.tag() == b.tag() && a.shared() == b.shared();
}
inline bool operator!=(const SyncFileStatus &a, const SyncFileStatus &b)
{
return !(a == b);
}
}
QSFERA_SYNC_EXPORT QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item);
Q_DECLARE_METATYPE(OCC::SyncFileStatus)
#endif // SYNCFILESTATUS_H
File diff suppressed because it is too large Load Diff
+323
View File
@@ -0,0 +1,323 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef SYNCJOURNALDB_H
#define SYNCJOURNALDB_H
#include "libsync/common/checksumalgorithms.h"
#include "libsync/common/ownsql.h"
#include "libsync/common/preparedsqlquerymanager.h"
#include "libsync/common/result.h"
#include "libsync/common/syncjournalfilerecord.h"
#include "libsync/common/utility.h"
#include <QMutex>
namespace OCC {
class SyncJournalFileRecord;
/**
* @brief Class that handles the sync database
*
* This class is thread safe. All public functions lock the mutex.
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT SyncJournalDb : public QObject
{
Q_OBJECT
public:
explicit SyncJournalDb(const QString &dbFilePath, QObject *parent = nullptr);
~SyncJournalDb() override;
/// Create a journal path for a specific configuration
static QString makeDbName(const QString &localPath, const QString &infix = QStringLiteral("journal"));
static bool dbIsTooNewForClient(const QString &dbFilePath);
// To verify that the record could be found check with SyncJournalFileRecord::isValid()
/**
* Returns the file record for the given filename.
* @param filename The filename, which must be a relative path (relative to the sync root).
* Passing an absolute path is not allowed and will not yield a result.
*/
SyncJournalFileRecord getFileRecord(const QString &filename);
SyncJournalFileRecord getFileRecordByInode(quint64 inode);
bool getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
bool getFilesBelowPath(const QString &path, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
bool listFilesInPath(const QString &path, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
const QVector<SyncJournalFileRecord> getFileRecordsWithDirtyPlaceholders() const;
Result<void, QString> setFileRecord(const SyncJournalFileRecord &record);
bool deleteFileRecord(const QString &filename, bool recursively = false);
bool updateFileRecordChecksum(const QString &filename, const QByteArray &contentChecksum, CheckSums::Algorithm contentChecksumType);
/// Return value for hasHydratedOrDehydratedFiles()
struct HasHydratedDehydrated
{
bool hasHydrated = false;
bool hasDehydrated = false;
};
/** Returns whether the item or any subitems are dehydrated */
Optional<HasHydratedDehydrated> hasHydratedOrDehydratedFiles(const QByteArray &filename);
bool exists();
void walCheckpoint();
QString databaseFilePath() const;
void setErrorBlacklistEntry(const SyncJournalErrorBlacklistRecord &item);
void wipeErrorBlacklistEntry(const QString &relativeFile);
void wipeErrorBlacklistEntry(const QString &relativeFile, SyncJournalErrorBlacklistRecord::Category category);
void wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::Category category);
int wipeErrorBlacklist();
int errorBlackListEntryCount();
struct DownloadInfo
{
DownloadInfo()
: _errorCount(0)
, _valid(false)
{
}
QString _tmpfile;
QByteArray _etag;
int _errorCount;
bool _valid;
};
struct UploadInfo
{
bool _valid = false;
qint64 _size = 0;
qint64 _modtime = 0;
QByteArray _contentChecksum;
QUrl _url; // upload url (tus)
QString _path; // stored as utf16, used in local discovery
bool validate(qint64 size, qint64 modtime, const QByteArray &checksum) const
{
Q_ASSERT(!checksum.isEmpty());
Q_ASSERT(!_valid || !_contentChecksum.isEmpty());
return _valid && _size == size && _modtime == modtime && _contentChecksum == checksum;
}
};
DownloadInfo getDownloadInfo(const QString &file);
void setDownloadInfo(const QString &file, const DownloadInfo &i);
QVector<DownloadInfo> getAndDeleteStaleDownloadInfos(const QSet<QString> &keep);
int downloadInfoCount();
UploadInfo getUploadInfo(const QString &file);
std::vector<UploadInfo> getUploadInfos();
void setUploadInfo(const QString &file, const UploadInfo &i);
bool deleteStaleUploadInfos(const QSet<QString> &keep);
SyncJournalErrorBlacklistRecord errorBlacklistEntry(const QString &);
bool deleteStaleErrorBlacklistEntries(const QSet<QString> &keep);
void avoidRenamesOnNextSync(const QString &path) { avoidRenamesOnNextSync(path.toUtf8()); }
void avoidRenamesOnNextSync(const QByteArray &path);
enum SelectiveSyncListType {
/** The black list is the list of folders that are unselected in the selective sync dialog.
* For the sync engine, those folders are considered as if they were not there, so the local
* folders will be deleted */
SelectiveSyncBlackList = 1,
/** When a shared folder has a size bigger than a configured size, it is by default not sync'ed
* Unless it is in the white list, in which case the folder is sync'ed and all its children.
* If a folder is both on the black and the white list, the black list wins */
SelectiveSyncWhiteList = 2,
/** List of big sync folders that have not been confirmed by the user yet and that the UI
* should notify about */
SelectiveSyncUndecidedList = 3
};
Q_ENUM(SelectiveSyncListType)
/* return the specified list from the database */
QSet<QString> getSelectiveSyncList(SelectiveSyncListType type, bool *ok);
/* Write the selective sync list (remove all other entries of that list */
void setSelectiveSyncList(SelectiveSyncListType type, const QSet<QString> &list);
/**
* Make sure that on the next sync fileName and its parents are discovered from the server.
*
* That means its metadata and, if it's a directory, its direct contents.
*
* Specifically, etag (md5 field) of fileName and all its parents are set to _invalid_.
* That causes a metadata difference and a resulting discovery from the remote for the
* affected folders.
*
* Since folders in the selective sync list will not be rediscovered (csync_ftw,
* _csync_detect_update skip them), the _invalid_ marker will stay. And any
* child items in the db will be ignored when reading a remote tree from the database.
*
* Any setFileRecord() call to affected directories before the next sync run will be
* adjusted to retain the invalid etag via _etagStorageFilter.
*/
void schedulePathForRemoteDiscovery(const QString &fileName) { schedulePathForRemoteDiscovery(fileName.toUtf8()); }
void schedulePathForRemoteDiscovery(const QByteArray &fileName);
/**
* Wipe _etagStorageFilter. Also done implicitly on close().
*/
void clearEtagStorageFilter();
/**
* Ensures full remote discovery happens on the next sync.
*
* Equivalent to calling schedulePathForRemoteDiscovery() for all files.
*/
void forceRemoteDiscoveryNextSync();
/* Because sqlite transactions are really slow, we encapsulate everything in big transactions
* Commit will actually commit the transaction and create a new one.
*/
void commit(const QString &context, bool startTrans = true);
void commitIfNeededAndStartNewTransaction(const QString &context);
/** Open the db if it isn't already.
*
* This usually creates some temporary files next to the db file, like
* $dbfile-shm or $dbfile-wal.
*
* returns true if it could be openend or is currently opened.
*/
bool open();
/** Returns whether the db is currently openend. */
bool isOpen() const;
/** Close the database */
void close();
/**
* allow to reopen/recreate the db after it was closed (unit tests)
* This is usually not allowed to prevent accidential recreation of db.
*/
void allowReopen();
/**
* Returns the checksum type for an id.
*/
QByteArray getChecksumType(int checksumTypeId);
// Conflict record functions
/// Store a new or updated record in the database
void setConflictRecord(const ConflictRecord &record);
/// Retrieve a conflict record by path of the file with the conflict tag
ConflictRecord conflictRecord(const QByteArray &path);
/// Delete a conflict record by path of the file with the conflict tag
void deleteConflictRecord(const QByteArray &path);
/// Return all paths of files with a conflict tag in the name and records in the db
QByteArrayList conflictRecordPaths();
/** Find the base name for a conflict file name, using journal or name pattern
*
* The path must by sync-folder relative.
*
* Will return an empty string if it's not even a conflict file by pattern.
*/
QString conflictFileBaseName(const QString &conflictName);
/**
* Delete any file entry. This will force the next sync to re-sync everything as if it was new,
* restoring everyfile on every remote. If a file is there both on the client and server side,
* it will be a conflict that will be automatically resolved if the file is the same.
*/
void clearFileTable();
/**
* Only used for auto-test:
* when positive, will decrease the counter for every database operation.
* reaching 0 makes the operation fails
*/
int autotestFailCounter = -1;
private:
int getFileRecordCount();
bool updateDatabaseStructure();
bool updateMetadataTableStructure();
bool updateErrorBlacklistTableStructure();
bool sqlFail(const QString &log, const SqlQuery &query);
void commitInternal(const QString &context, bool startTrans = true);
void startTransaction();
void commitTransaction();
QVector<QByteArray> tableColumns(const QByteArray &table);
bool checkConnect();
bool createUploadInfo();
// Same as forceRemoteDiscoveryNextSync but without acquiring the lock
void forceRemoteDiscoveryNextSyncLocked();
// Returns the integer id of the checksum type
//
// Returns 0 on failure and for empty checksum types.
int mapChecksumType(CheckSums::Algorithm checksumType);
SqlDatabase _db;
QString _dbFile;
mutable QRecursiveMutex _mutex; // Public functions are protected with the mutex.
QMap<CheckSums::Algorithm, int> _checksymTypeCache;
int _transaction;
bool _metadataTableIsEmpty;
/* Storing etags to these folders, or their parent folders, is filtered out.
*
* When schedulePathForRemoteDiscovery() is called some etags to _invalid_ in the
* database. If this is done during a sync run, a later propagation job might
* undo that by writing the correct etag to the database instead. This filter
* will prevent this write and instead guarantee the _invalid_ etag stays in
* place.
*
* The list is cleared on close() (end of sync run) and explicitly with
* clearEtagStorageFilter() (start of sync run).
*
* The contained paths have a trailing /.
*/
QList<QByteArray> _etagStorageFilter;
/** The journal mode to use for the db.
*
* Typically WAL initially, but may be set to other modes via environment
* variable, for specific filesystems, or when WAL fails in a particular way.
*/
QByteArray _journalMode;
mutable PreparedSqlQueryManager _queryManager;
/**
* Whether the db was already closed, prevent recreation
*/
bool _closed = false;
};
bool QSFERA_SYNC_EXPORT operator==(const SyncJournalDb::DownloadInfo &lhs, const SyncJournalDb::DownloadInfo &rhs);
bool QSFERA_SYNC_EXPORT operator==(const SyncJournalDb::UploadInfo &lhs, const SyncJournalDb::UploadInfo &rhs);
} // namespace OCC
#endif // SYNCJOURNALDB_H
@@ -0,0 +1,258 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "libsync/common/syncjournalfilerecord.h"
#include "libsync/common/utility.h"
#include "libsync/filesystem.h"
#include "libsync/syncfileitem.h"
Q_LOGGING_CATEGORY(lcSyncJournalFileRecord, "sync.syncjournalfilerecord", QtInfoMsg)
using namespace Qt::Literals::StringLiterals;
using namespace OCC;
class OCC::SyncJournalFileRecordData : public QSharedData
{
public:
SyncJournalFileRecordData() = default;
~SyncJournalFileRecordData() = default;
QString _path;
quint64 _inode = 0;
std::optional<qint64> _modtime;
ItemType _type = ItemTypeUnsupported;
QString _etag;
QByteArray _fileId;
qint64 _fileSize = 0;
RemotePermissions _remotePerm;
bool _serverHasIgnoredFiles = false;
bool _hasDirtyPlaceholder = false;
QByteArray _checksumHeader;
QString _error;
};
bool SyncJournalErrorBlacklistRecord::isValid() const
{
return !_file.isEmpty() && (!_lastTryEtag.isEmpty() || _lastTryModtime != 0) && _lastTryTime > 0;
}
SyncJournalFileRecord::SyncJournalFileRecord()
: d([] {
static auto nullData = QExplicitlySharedDataPointer{new SyncJournalFileRecordData{}};
return nullData;
}())
{
}
SyncJournalFileRecord::SyncJournalFileRecord(const QString &path, ItemType type)
: d(new SyncJournalFileRecordData)
{
d->_path = path;
d->_type = type;
}
bool SyncJournalFileRecord::validateRecord()
{
Q_ASSERT(d != SyncJournalFileRecord().d);
Q_ASSERT(!remotePerm().isNull());
Q_ASSERT(inode() != 0);
Q_ASSERT(d->_modtime.has_value());
Q_ASSERT(!fileId().isEmpty());
Q_ASSERT(type() != ItemTypeUnsupported);
return true;
}
SyncJournalFileRecord::SyncJournalFileRecord(const QString &error)
: d(new SyncJournalFileRecordData)
{
d->_error = error;
}
SyncJournalFileRecord::~SyncJournalFileRecord() = default;
SyncJournalFileRecord::SyncJournalFileRecord(const SyncJournalFileRecord &other) = default;
SyncJournalFileRecord &SyncJournalFileRecord::operator=(const SyncJournalFileRecord &other) = default;
QByteArray SyncJournalFileRecord::query()
{
return QByteArrayLiteral("SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize,"
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum,"
" hasDirtyPlaceholder"
" FROM metadata"
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id ");
}
SyncJournalFileRecord SyncJournalFileRecord::fromSqlQuery(OCC::SqlQuery &query)
{
// keep in sync with query
SyncJournalFileRecord rec(QString::fromUtf8(query.baValue(0)), static_cast<ItemType>(query.intValue(3)));
rec.d->_inode = query.int64Value(1);
rec.d->_modtime = query.int64Value(2);
rec.d->_etag = QString::fromUtf8(query.baValue(4));
rec.d->_fileId = query.baValue(5);
rec.d->_remotePerm = OCC::RemotePermissions::fromDbValue(query.baValue(6));
rec.d->_fileSize = query.int64Value(7);
rec.d->_serverHasIgnoredFiles = (query.intValue(8) > 0);
rec.d->_checksumHeader = query.baValue(9);
rec.d->_hasDirtyPlaceholder = query.intValue(10);
qCDebug(lcSyncJournalFileRecord) << u"Restored from db:" << rec;
Q_ASSERT(rec.validateRecord());
return rec;
}
SyncJournalFileRecord SyncJournalFileRecord::fromSyncFileItem(const SyncFileItem &syncFile)
{
SyncJournalFileRecord rec(syncFile.destination(), syncFile._type);
rec.d->_modtime = syncFile._modtime;
// Some types should never be written to the database when propagation completes
if (rec.d->_type == ItemTypeVirtualFileDownload)
rec.d->_type = ItemTypeFile;
if (rec.d->_type == ItemTypeVirtualFileDehydration)
rec.d->_type = ItemTypeVirtualFile;
rec.d->_etag = syncFile._etag;
rec.d->_fileId = syncFile._fileId;
rec.d->_fileSize = syncFile._size;
rec.d->_inode = syncFile._inode;
rec.d->_remotePerm = syncFile._remotePerm;
rec.d->_serverHasIgnoredFiles = syncFile._serverHasIgnoredFiles;
rec.d->_checksumHeader = syncFile._checksumHeader;
Q_ASSERT(rec.validateRecord());
return rec;
}
bool SyncJournalFileRecord::isValid() const
{
return !hasError() && !d->_path.isEmpty();
}
QDateTime SyncJournalFileRecord::modDateTime() const
{
return Utility::qDateTimeFromTime_t(modtime());
}
bool SyncJournalFileRecord::isDirectory() const
{
return d->_type == ItemTypeDirectory;
}
bool SyncJournalFileRecord::isFile() const
{
return d->_type == ItemTypeFile || d->_type == ItemTypeVirtualFileDehydration;
}
bool SyncJournalFileRecord::isVirtualFile() const
{
return d->_type == ItemTypeVirtualFile || d->_type == ItemTypeVirtualFileDownload;
}
QString SyncJournalFileRecord::path() const
{
return d->_path;
}
QString SyncJournalFileRecord::name() const
{
return path().mid(path().lastIndexOf('/'_L1) + 1);
}
QString SyncJournalFileRecord::etag() const
{
return d->_etag;
}
QByteArray SyncJournalFileRecord::fileId() const
{
return d->_fileId;
}
RemotePermissions SyncJournalFileRecord::remotePerm() const
{
return d->_remotePerm;
}
QByteArray SyncJournalFileRecord::checksumHeader() const
{
return d->_checksumHeader;
}
time_t SyncJournalFileRecord::modtime() const
{
return d->_modtime.value_or(0);
}
int64_t SyncJournalFileRecord::size() const
{
return d->_fileSize;
}
uint64_t SyncJournalFileRecord::inode() const
{
return d->_inode;
}
ItemType SyncJournalFileRecord::type() const
{
return d->_type;
}
bool SyncJournalFileRecord::hasDirtyPlaceholder() const
{
return d->_hasDirtyPlaceholder;
}
void SyncJournalFileRecord::setDirtyPlaceholder(bool b)
{
d.detach();
d->_hasDirtyPlaceholder = b;
}
bool SyncJournalFileRecord::serverHasIgnoredFiles() const
{
return d->_serverHasIgnoredFiles;
}
QString SyncJournalFileRecord::error() const
{
return d->_error;
}
bool SyncJournalFileRecord::hasError() const
{
return !d->_error.isEmpty();
}
QDebug OCC::operator<<(QDebug debug, const SyncJournalFileRecord &record)
{
QDebugStateSaver saver(debug);
debug.nospace() << u"SyncJournalFileRecord(";
if (record.hasError()) {
debug << u"Error: " << record.error();
} else {
debug << u"path: " << record.path() << u", inode: " << record.inode() << u", modtime: " << record.modtime() << u", type: " << record.type()
<< u", etag: " << record.etag() << u", fileId: " << record.fileId() << u", remotePerm: " << record.remotePerm().toString() << u", size: "
<< record.size() << u", checksum: " << record.checksumHeader() << u", serverHasIgnoredFiles: " << record.serverHasIgnoredFiles()
<< u", hasDirtyPlaceholder: " << record.hasDirtyPlaceholder();
}
return debug << u")";
}
bool operator==(const SyncJournalFileRecord &lhs, const SyncJournalFileRecord &rhs)
{
return lhs.path() == rhs.path() && lhs.inode() == rhs.inode() && lhs.modtime() == rhs.modtime() && lhs.type() == rhs.type() && lhs.etag() == rhs.etag()
&& lhs.fileId() == rhs.fileId() && lhs.size() == rhs.size() && lhs.remotePerm() == rhs.remotePerm()
&& lhs.serverHasIgnoredFiles() == rhs.serverHasIgnoredFiles() && lhs.checksumHeader() == rhs.checksumHeader();
}
@@ -0,0 +1,184 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef SYNCJOURNALFILERECORD_H
#define SYNCJOURNALFILERECORD_H
#include "libsync/common/utility.h"
#include "libsync/csync.h"
#include "libsync/qsferasynclib.h"
#include "remotepermissions.h"
namespace OCC {
class SqlQuery;
class SyncFileItem;
class SyncJournalFileRecordData;
/**
* @brief The SyncJournalFileRecord class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT SyncJournalFileRecord
{
public:
SyncJournalFileRecord();
SyncJournalFileRecord(const QString &error);
~SyncJournalFileRecord();
SyncJournalFileRecord(const SyncJournalFileRecord &other);
SyncJournalFileRecord &operator=(const SyncJournalFileRecord &other);
void swap(SyncJournalFileRecord &other) noexcept { d.swap(other.d); }
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(SyncJournalFileRecord);
static QByteArray query();
static SyncJournalFileRecord fromSqlQuery(SqlQuery &query);
static SyncJournalFileRecord fromSyncFileItem(const SyncFileItem &syncFile);
bool isValid() const;
QDateTime modDateTime() const;
bool isDirectory() const;
bool isFile() const;
bool isVirtualFile() const;
QString path() const;
QString name() const;
QString etag() const;
QByteArray fileId() const;
RemotePermissions remotePerm() const;
QByteArray checksumHeader() const;
time_t modtime() const;
int64_t size() const;
uint64_t inode() const;
ItemType type() const;
bool hasDirtyPlaceholder() const;
void setDirtyPlaceholder(bool b);
bool serverHasIgnoredFiles() const;
QString error() const;
bool hasError() const;
private:
// constructor for the non null object
SyncJournalFileRecord(const QString &path, ItemType type);
bool validateRecord();
QExplicitlySharedDataPointer<SyncJournalFileRecordData> d;
};
QDebug QSFERA_SYNC_EXPORT operator<<(QDebug debug, const SyncJournalFileRecord &record);
class QSFERA_SYNC_EXPORT SyncJournalErrorBlacklistRecord
{
Q_GADGET
public:
enum class Category {
/// Normal errors have no special behavior
Normal = 0,
/// These get a special summary message
InsufficientRemoteStorage,
LocalSoftError
};
Q_ENUM(Category)
SyncJournalErrorBlacklistRecord()
: _retryCount(0)
, _errorCategory(Category::Normal)
, _lastTryModtime(0)
, _lastTryTime(0)
, _ignoreDuration(0)
{
}
/// The number of times the operation was unsuccessful so far.
int _retryCount;
/// The last error string.
QString _errorString;
/// The error category. Sometimes used for special actions.
Category _errorCategory;
qint64 _lastTryModtime;
QByteArray _lastTryEtag;
/// The last time the operation was attempted (in s since epoch).
qint64 _lastTryTime;
/// The number of seconds the file shall be ignored.
qint64 _ignoreDuration;
QString _file;
QString _renameTarget;
/// The last X-Request-ID of the request that failled
QByteArray _requestId;
bool isValid() const;
};
/** Represents a conflict in the conflicts table.
*
* In the following the "conflict file" is the file that has the conflict
* tag in the filename, and the base file is the file that it's a conflict for.
* So if "a/foo.txt" is the base file, its conflict file could be
* "a/foo (conflicted copy 1234).txt".
*/
class QSFERA_SYNC_EXPORT ConflictRecord
{
public:
/** Path to the file with the conflict tag in the name
*
* The path is sync-folder relative.
*/
QByteArray path;
/// File id of the base file
QByteArray baseFileId;
/** Modtime of the base file
*
* may not be available and be -1
*/
qint64 baseModtime = -1;
/** Etag of the base file
*
* may not be available and empty
*/
QByteArray baseEtag;
/**
* The path of the original file at the time the conflict was created
*
* Note that in nearly all cases one should query the db by baseFileId and
* thus retrieve the *current* base path instead!
*
* maybe be empty if not available
*/
QByteArray initialBasePath;
bool isValid() const { return !path.isEmpty(); }
};
}
QSFERA_SYNC_EXPORT bool operator==(const OCC::SyncJournalFileRecord &, const OCC::SyncJournalFileRecord &);
Q_DECLARE_SHARED(OCC::SyncJournalFileRecord);
#endif // SYNCJOURNALFILERECORD_H
+504
View File
@@ -0,0 +1,504 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "common/utility.h"
#include "common/asserts.h"
#include "common/filesystembase.h"
#include "common/version.h"
// Note: This file must compile without QtGui
#include <QCollator>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QObject>
#include <QProcess>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QSettings>
#include <QStandardPaths>
#include <QSysInfo>
#include <QTextStream>
#include <QThread>
#include <QTimeZone>
#include <QUrl>
#ifdef Q_OS_UNIX
#include <sys/statvfs.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#ifdef Q_OS_WIN
#include <qt_windows.h>
#endif
#include <cstring>
#include <math.h>
#include <stdarg.h>
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
namespace {
auto RFC1123PatternC()
{
return QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcUtility, "sync.utility", QtMsgType::QtInfoMsg)
QString Utility::octetsToString(qint64 octets)
{
OC_ASSERT(octets >= 0)
using namespace FileSystem::SizeLiterals;
// We do what macOS 10.8 and above do: 0 fraction digits for bytes and KB; 1 fraction digits for MB; 2 for GB and above.
// See also https://developer.apple.com/documentation/foundation/nsbytecountformatter/1417887-adaptive
int precision = 0;
if (quint64(octets) >= 1_GiB) {
precision = 2;
} else if (quint64(octets) >= 1_MiB) {
precision = 1;
}
return QLocale().formattedDataSize(octets, precision, QLocale::DataSizeTraditionalFormat);
}
static QLatin1String platform()
{
#if defined(Q_OS_WIN)
return QLatin1String("Windows");
#elif defined(Q_OS_MAC)
return QLatin1String("Macintosh");
#elif defined(Q_OS_LINUX)
return QLatin1String("Linux");
#elif defined(__DragonFly__) // Q_OS_FREEBSD also defined
return QLatin1String("DragonFlyBSD");
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL)
return QLatin1String("FreeBSD");
#elif defined(Q_OS_NETBSD)
return QLatin1String("NetBSD");
#elif defined(Q_OS_OPENBSD)
return QLatin1String("OpenBSD");
#elif defined(Q_OS_SOLARIS)
return QLatin1String("Solaris");
#else
return QSysInfo::productType();
#endif
}
QByteArray Utility::userAgentString()
{
return QStringLiteral("Mozilla/5.0 (%1) mirall/%2 (%3, %4-%5 ClientArchitecture: %6 OsArchitecture: %7)")
.arg(platform(), OCC::Version::displayString(),
// accessing the theme to fetch the string is rather difficult
// since this is only needed server-side to identify clients, the app name (as of 2.9, the short name) is good enough
qApp->applicationName(), QSysInfo::productType(), QSysInfo::kernelVersion(), QSysInfo::buildCpuArchitecture(), Utility::currentCpuArch())
.toLatin1();
}
qint64 Utility::freeDiskSpace(const QString &path)
{
#if defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
struct statvfs stat;
if (statvfs(path.toLocal8Bit().data(), &stat) == 0) {
return (qint64)stat.f_bavail * stat.f_frsize;
}
#elif defined(Q_OS_UNIX)
struct statvfs64 stat;
if (statvfs64(path.toLocal8Bit().data(), &stat) == 0) {
return (qint64)stat.f_bavail * stat.f_frsize;
}
#elif defined(Q_OS_WIN)
ULARGE_INTEGER freeBytes;
freeBytes.QuadPart = 0L;
if (GetDiskFreeSpaceEx(reinterpret_cast<const wchar_t *>(FileSystem::longWinPath(path).utf16()), &freeBytes, nullptr, nullptr)) {
return freeBytes.QuadPart;
}
#endif
return -1;
}
QString Utility::compactFormatDouble(double value, int prec, const QString &unit)
{
QLocale locale = QLocale::system();
const QString decPoint = locale.decimalPoint();
QString str = locale.toString(value, 'f', prec);
while (str.endsWith(QLatin1Char('0')) || str.endsWith(decPoint)) {
if (str.endsWith(decPoint)) {
str.chop(1);
break;
}
str.chop(1);
}
if (!unit.isEmpty())
str += (QLatin1Char(' ') + unit);
return str;
}
QString Utility::escape(const QString &in)
{
return in.toHtmlEscaped();
}
// This can be overridden from the tests
QSFERA_SYNC_EXPORT bool fsCasePreserving_override = Utility::isWindows() || Utility::isMac();
bool Utility::fsCasePreserving()
{
return fsCasePreserving_override;
}
bool Utility::fileNamesEqual(const QString &fn1, const QString &fn2)
{
const QDir fd1(fn1);
const QDir fd2(fn2);
// Attention: If the path does not exist, canonicalPath returns ""
// ONLY use this function with existing pathes.
const QString a = fd1.canonicalPath();
const QString b = fd2.canonicalPath();
bool re = !a.isEmpty() && QString::compare(a, b, fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive) == 0;
return re;
}
QDateTime Utility::qDateTimeFromTime_t(qint64 t)
{
return QDateTime::fromMSecsSinceEpoch(t * 1000);
}
qint64 Utility::qDateTimeToTime_t(const QDateTime &t)
{
return t.toMSecsSinceEpoch() / 1000;
}
namespace {
constexpr struct
{
const char *const name;
const std::chrono::milliseconds msec;
QString description(int value) const { return QCoreApplication::translate("Utility", name, nullptr, value); }
} periods[] = {
{QT_TRANSLATE_N_NOOP("Utility", "%n year(s)"), 24h * 365},
{QT_TRANSLATE_N_NOOP("Utility", "%n month(s)"), 24h * 30},
{QT_TRANSLATE_N_NOOP("Utility", "%n day(s)"), 24h},
{QT_TRANSLATE_N_NOOP("Utility", "%n hour(s)"), 1h},
{QT_TRANSLATE_N_NOOP("Utility", "%n minute(s)"), 1min},
{QT_TRANSLATE_N_NOOP("Utility", "%n second(s)"), 1s},
{nullptr, {}},
};
} // anonymous namespace
QString Utility::durationToDescriptiveString2(std::chrono::milliseconds msecs)
{
int p = 0;
while (periods[p + 1].name && msecs < periods[p].msec) {
p++;
}
const QString firstPart = periods[p].description(static_cast<int>(msecs / periods[p].msec));
if (!periods[p + 1].name) {
return firstPart;
}
const quint64 secondPartNum = qRound(static_cast<double>(msecs.count() % periods[p].msec.count()) / periods[p + 1].msec.count());
if (secondPartNum == 0) {
return firstPart;
}
return QCoreApplication::translate("Utility", "%1 %2").arg(firstPart, periods[p + 1].description(secondPartNum));
}
QString Utility::durationToDescriptiveString1(std::chrono::milliseconds msecs)
{
int p = 0;
while (periods[p + 1].name && msecs < periods[p].msec) {
p++;
}
return periods[p].description(qRound(static_cast<double>(msecs.count()) / periods[p].msec.count()));
}
QString Utility::fileNameForGuiUse(const QString &fName)
{
if (isMac()) {
QString n(fName);
return n.replace(QLatin1Char(':'), QLatin1Char('/'));
}
return fName;
}
QString Utility::normalizeEtag(QStringView etag)
{
if (etag.isEmpty()) {
return {};
}
const auto unQuote = [&] {
if (etag.startsWith(QLatin1Char('"')) && etag.endsWith(QLatin1Char('"'))) {
etag = etag.mid(1);
etag.chop(1);
}
};
// Weak E-Tags can appear when gzip compression is on, see #3946
if (etag.startsWith(QLatin1String("W/"))) {
etag = etag.mid(2);
}
/* strip normal quotes and quotes around "XXXX-gzip" */
unQuote();
// https://github.com/owncloud/client/issues/1195
const QLatin1String gzipSuffix("-gzip");
if (etag.endsWith(gzipSuffix)) {
etag.chop(gzipSuffix.size());
}
/* strip normal quotes */
unQuote();
return etag.toString();
}
QString Utility::platformName()
{
return QSysInfo::prettyProductName();
}
void Utility::crash()
{
volatile int *a = (int *)nullptr;
*a = 1;
}
QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
{
const QDateTime now = from.isValid() ? from : QDateTime::currentDateTimeUtc();
if (dt.daysTo(now) > 0) {
return QObject::tr("%n day(s) ago", "", dt.daysTo(now));
}
const qint64 secs = dt.secsTo(now);
if (secs < 0) {
return QObject::tr("in the future");
}
if (floor(secs / 3600.0) > 0) {
const int hours = floor(secs / 3600.0);
return (QObject::tr("%n hour(s) ago", "", hours));
}
const int minutes = qRound(secs / 60.0);
if (minutes == 0) {
if (secs < 5) {
return QObject::tr("now");
} else {
return QObject::tr("less than a minute ago");
}
}
return (QObject::tr("%n minute(s) ago", "", minutes));
}
void Utility::sortFilenames(QStringList &fileNames)
{
QCollator collator;
collator.setNumericMode(true);
collator.setCaseSensitivity(Qt::CaseInsensitive);
std::sort(fileNames.begin(), fileNames.end(), collator);
}
QString Utility::concatUrlPathItems(QStringList &&items, const QLatin1Char delimiter)
{
for (QString &item : items) {
while (item.endsWith(QLatin1Char('/'))) {
item.chop(1);
}
}
return items.join(delimiter);
}
QUrl Utility::concatUrlPath(const QUrl &url, const QString &concatPath, const QUrlQuery &queryItems)
{
QString path = url.path();
if (!concatPath.isEmpty()) {
// avoid '//'
if (path.endsWith(QLatin1Char('/')) && concatPath.startsWith(QLatin1Char('/'))) {
path.chop(1);
} // avoid missing '/'
else if (!path.endsWith(QLatin1Char('/')) && !concatPath.startsWith(QLatin1Char('/'))) {
path += QLatin1Char('/');
}
path += concatPath; // put the complete path together
}
Q_ASSERT(!path.contains(QStringLiteral("//")));
Q_ASSERT(url.query().isEmpty());
QUrl tmpUrl = url;
tmpUrl.setPath(path);
tmpUrl.setQuery(queryItems);
return tmpUrl;
}
bool Utility::urlEqual(QUrl url1, QUrl url2)
{
// ensure https://demo.opencloud.eu/ matches https://demo.opencloud.eu
// the empty path was the legacy formating before 2.9
if (url1.path().isEmpty()) {
url1.setPath(QStringLiteral("/"));
}
if (url2.path().isEmpty()) {
url2.setPath(QStringLiteral("/"));
}
return url1.matches(url2, QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
}
QString Utility::makeConflictFileName(const QString &fn, const QDateTime &dt, const QString &user)
{
QString conflictFileName(fn);
// Add conflict tag before the extension.
int dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
dotLocation = conflictFileName.size();
}
QString conflictMarker = QStringLiteral(" (conflicted copy ");
if (!user.isEmpty()) {
// Don't allow parens in the user name, to ensure
// we can find the beginning and end of the conflict tag.
const auto userName = sanitizeForFileName(user).replace(QLatin1Char('('), QLatin1Char('_')).replace(QLatin1Char(')'), QLatin1Char('_'));
;
conflictMarker += userName + QLatin1Char(' ');
}
conflictMarker += dt.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');
conflictFileName.insert(dotLocation, conflictMarker);
return conflictFileName;
}
bool Utility::isConflictFile(QStringView name)
{
auto bname = name.mid(name.lastIndexOf(QLatin1Char('/')) + 1);
if (bname.contains(QStringLiteral("_conflict-")))
return true;
if (bname.contains(QStringLiteral("(conflicted copy")))
return true;
return false;
}
QString Utility::conflictFileBaseNameFromPattern(const QString &conflictName)
{
// This function must be able to deal with conflict files for conflict files.
// To do this, we scan backwards, for the outermost conflict marker and
// strip only that to generate the conflict file base name.
auto startOld = conflictName.lastIndexOf("_conflict-"_L1);
// A single space before "(conflicted copy" is considered part of the tag
auto startNew = conflictName.lastIndexOf("(conflicted copy"_L1);
if (startNew > 0 && conflictName[startNew - 1] == ' '_L1)
startNew -= 1;
// The rightmost tag is relevant
auto tagStart = qMax(startOld, startNew);
if (tagStart == -1)
return QString();
// Find the end of the tag
auto tagEnd = conflictName.size();
auto dot = conflictName.lastIndexOf('.'_L1); // dot could be part of user name for new tag!
if (dot > tagStart)
tagEnd = dot;
if (tagStart == startNew) {
auto paren = conflictName.indexOf(')'_L1, tagStart);
if (paren != -1)
tagEnd = paren + 1;
}
return conflictName.left(tagStart) + conflictName.mid(tagEnd);
}
QString Utility::sanitizeForFileName(const QString &name)
{
const auto invalid = QStringLiteral("/?<>\\:*|\"");
QString result;
result.reserve(name.size());
for (const auto &c : name) {
if (!invalid.contains(c) && c.category() != QChar::Other_Control && c.category() != QChar::Other_Format) {
result.append(c);
}
}
return result;
}
#ifdef Q_OS_LINUX
QString Utility::appImageLocation()
{
Q_ASSERT(Utility::runningInAppImage());
static const auto value = qEnvironmentVariable("APPIMAGE");
return value;
}
bool Utility::runningInAppImage()
{
return qEnvironmentVariableIsSet("APPIMAGE");
}
#endif
QDateTime Utility::parseRFC1123Date(const QString &date)
{
if (!date.isEmpty()) {
auto out = QDateTime::fromString(date, RFC1123PatternC());
Q_ASSERT(out.isValid());
out.setTimeZone(QTimeZone::utc());
return out;
}
return {};
}
QString Utility::formatRFC1123Date(const QDateTime &date)
{
return date.toUTC().toString(RFC1123PatternC());
}
#ifndef Q_OS_MAC
QString Utility::currentCpuArch()
{
return QSysInfo::currentCpuArchitecture();
}
#endif
} // namespace OCC
QDebug operator<<(QDebug debug, nanoseconds in)
{
QDebugStateSaver save(debug);
debug.nospace();
const auto h = duration_cast<hours>(in);
const auto min = duration_cast<minutes>(in -= h);
const auto s = duration_cast<seconds>(in -= min);
const auto ms = duration_cast<milliseconds>(in -= s);
return debug << u"duration(" << h.count() << u"h, " << min.count() << u"min, " << s.count() << u"s, " << ms.count() << u"ms)";
}
+380
View File
@@ -0,0 +1,380 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QByteArray>
#include <QDateTime>
#include <QElapsedTimer>
#include <QLoggingCategory>
#include <QMetaEnum>
#include <QUrl>
#include <QUrlQuery>
#include <functional>
#include <memory>
#include <optional>
class QSettings;
class QQuickWidget;
namespace OCC {
class SyncJournal;
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcUtility)
/** \addtogroup libsync
* @{
*/
namespace Utility
{
QSFERA_SYNC_EXPORT void setupFavLink(const QString &folder);
QSFERA_SYNC_EXPORT QString octetsToString(qint64 octets);
QSFERA_SYNC_EXPORT QByteArray userAgentString();
/**
* @brief Return whether launch on startup is enabled system wide.
*
* If this returns true, the checkbox for user specific launch
* on startup will be hidden.
*
* Currently only implemented on Windows.
*/
QSFERA_SYNC_EXPORT bool hasSystemLaunchOnStartup(const QString &appName);
/**
* @brief Return if launch on startup is enabled for the current user.
* @param appName the name of the application (because of branding)
*/
QSFERA_SYNC_EXPORT bool hasLaunchOnStartup(const QString &appName);
QSFERA_SYNC_EXPORT void setLaunchOnStartup(const QString &appName, const QString &guiName, bool launch);
/**
* Return the amount of free space available.
*
* \a path must point to a directory
*/
QSFERA_SYNC_EXPORT qint64 freeDiskSpace(const QString &path);
/**
* @brief compactFormatDouble - formats a double value human readable.
*
* @param value the value to format.
* @param prec the precision.
* @param unit an optional unit that is appended if present.
* @return the formatted string.
*/
QSFERA_SYNC_EXPORT QString compactFormatDouble(double value, int prec, const QString &unit = QString());
// porting methods
QSFERA_SYNC_EXPORT QString escape(const QString &);
// conversion function QDateTime <-> time_t (because the ones builtin work on only unsigned 32bit)
QSFERA_SYNC_EXPORT QDateTime qDateTimeFromTime_t(qint64 t);
QSFERA_SYNC_EXPORT qint64 qDateTimeToTime_t(const QDateTime &t);
/**
* @brief Convert milliseconds duration to human readable string.
* @param msecs the milliseconds to convert to string.
* @return an HMS representation of the milliseconds value.
*
* durationToDescriptiveString1 describes the duration in a single
* unit, like "5 minutes" or "2 days".
*
* durationToDescriptiveString2 uses two units where possible, so
* "5 minutes 43 seconds" or "1 month 3 days".
*/
QSFERA_SYNC_EXPORT QString durationToDescriptiveString1(std::chrono::milliseconds msecs);
QSFERA_SYNC_EXPORT QString durationToDescriptiveString2(std::chrono::milliseconds msecs);
/**
* @brief hasDarkSystray - determines whether the systray is dark or light.
*
* Use this to check if the OS has a dark or a light systray.
*
* The value might change during the execution of the program
* (e.g. on OS X 10.10).
*
* @return bool which is true for systems with dark systray.
*/
QSFERA_SYNC_EXPORT bool hasDarkSystray();
// convenience OS detection methods
constexpr bool isWindows();
constexpr bool isMac();
constexpr bool isUnix();
constexpr bool isLinux(); // use with care
constexpr bool isBSD(); // use with care, does not match OS X
QSFERA_SYNC_EXPORT QString platformName();
// crash helper for --debug
QSFERA_SYNC_EXPORT void crash();
// Case preserving file system underneath?
// if this function returns true, the file system is case preserving,
// that means "test" means the same as "TEST" for filenames.
// if false, the two cases are two different files.
QSFERA_SYNC_EXPORT bool fsCasePreserving();
inline auto fsCaseSensitivity()
{
return fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive;
}
// Check if two pathes that MUST exist are equal. This function
// uses QDir::canonicalPath() to judge and cares for the systems
// case sensitivity.
QSFERA_SYNC_EXPORT bool fileNamesEqual(const QString &fn1, const QString &fn2);
inline auto stripTrailingSlash(QStringView s)
{
if (s.endsWith(QLatin1Char('/'))) {
s.chop(1);
}
return s.toString();
}
inline auto stripLeadingSlash(QStringView s)
{
if (s.startsWith(QLatin1Char('/'))) {
s.slice(1);
}
return s.toString();
}
inline QString ensureTrailingSlash(QStringView s)
{
if (!s.endsWith(QLatin1Char('/'))) {
return s.toString() + QLatin1Char('/');
}
return s.toString();
}
// Call the given command with the switch --version and rerun the first line
// of the output.
// If command is empty, the function calls the running application which, on
// Linux, might have changed while this one is running.
// For Mac and Windows, it returns QString()
QSFERA_SYNC_EXPORT QString versionOfInstalledBinary(const QString &command = QString());
QSFERA_SYNC_EXPORT QString fileNameForGuiUse(const QString &fName);
QSFERA_SYNC_EXPORT QString normalizeEtag(QStringView etag);
/**
* @brief timeAgoInWords - human readable time span
*
* Use this to get a string that describes the timespan between the first and
* the second timestamp in a human readable and understandable form.
*
* If the second parameter is ommitted, the current time is used.
*/
QSFERA_SYNC_EXPORT QString timeAgoInWords(const QDateTime &dt, const QDateTime &from = QDateTime());
/**
* @brief Sort a QStringList in a way that's appropriate for filenames
*/
QSFERA_SYNC_EXPORT void sortFilenames(QStringList & fileNames);
/**
* Concatenate parts of a URL path with a given delimiter, eliminating duplicate
*/
QSFERA_SYNC_EXPORT QString concatUrlPathItems(QStringList && items, const QLatin1Char delimiter = QLatin1Char('/'));
/** Appends concatPath and queryItems to the url */
QSFERA_SYNC_EXPORT QUrl concatUrlPath(const QUrl &url, const QString &concatPath, const QUrlQuery &queryItems = {});
/** Compares two urls and ignores whether thei end wit / */
QSFERA_SYNC_EXPORT bool urlEqual(QUrl a, QUrl b);
/** Returns a new settings pre-set in a specific group. The Settings will be created
with the given parent. If no parent is specified, the caller must destroy the settings */
QSFERA_SYNC_EXPORT std::unique_ptr<QSettings> settingsWithGroup(const QString &group, QObject *parent = nullptr);
/** Sanitizes a string that shall become part of a filename.
*
* Filters out reserved characters like
* - unicode control and format characters
* - reserved characters: /, ?, <, >, \, :, *, |, and "
*
* Warning: This does not sanitize the whole resulting string, so
* - unix reserved filenames ('.', '..')
* - trailing periods and spaces
* - windows reserved filenames ('CON' etc)
* will pass unchanged.
*/
QSFERA_SYNC_EXPORT QString sanitizeForFileName(const QString &name);
/** Returns a file name based on \a fn that's suitable for a conflict.
*/
QSFERA_SYNC_EXPORT QString makeConflictFileName(const QString &fn, const QDateTime &dt, const QString &user);
/** Returns whether a file name indicates a conflict file
*/
QSFERA_SYNC_EXPORT bool isConflictFile(QStringView name);
/** Find the base name for a conflict file name, using name pattern only
*
* Will return an empty string if it's not a conflict file.
*
* Prefer to use the data from the conflicts table in the journal to determine
* a conflict's base file, see SyncJournal::conflictFileBaseName()
*/
QSFERA_SYNC_EXPORT QString conflictFileBaseNameFromPattern(const QString &conflictName);
template <class E>
E stringToEnum(const char *key)
{
bool ok;
auto out = static_cast<E>(QMetaEnum::fromType<E>().keyToValue(key, &ok));
Q_ASSERT(ok);
return out;
}
template <class E>
E stringToEnum(const QString &key)
{
return stringToEnum<E>(key.toUtf8().constData());
}
template <class E>
QString enumToString(E value)
{
return QString::fromUtf8(QMetaEnum::fromType<E>().valueToKeys(static_cast<int>(value)));
}
template <class E = void>
QString enumToDisplayName(E)
{
static_assert(std::is_same<E, void>::value, "Not implemented");
Q_UNREACHABLE();
}
template <typename T>
class asKeyValueRange
{
// https://www.kdab.com/qt-range-based-for-loops-and-structured-bindings/
public:
asKeyValueRange(const T &data)
: _data{data}
{
}
auto begin() { return _data.constKeyValueBegin(); }
auto end() { return _data.constKeyValueEnd(); }
private:
const T &_data;
};
/**
* Perform a const find on a Qt container and returns an std::optional<const_iterator>
* This allows performant access to the container with in a simple if condition.
* if (auto it = optionalFind("key"))
*/
template <typename T, typename K>
auto optionalFind(const T &container, const K &key)
{
auto it = container.constFind(key);
return it == container.cend() ? std::nullopt : std::make_optional(it);
}
#ifdef Q_OS_LINUX
QSFERA_SYNC_EXPORT QString appImageLocation();
QSFERA_SYNC_EXPORT bool runningInAppImage();
#else
inline QString appImageLocation()
{
Q_UNREACHABLE();
};
constexpr bool runningInAppImage()
{
return false;
};
#endif
QSFERA_SYNC_EXPORT QDateTime parseRFC1123Date(const QString &date);
QSFERA_SYNC_EXPORT QString formatRFC1123Date(const QDateTime &date);
// replacement for QSysInfo::currentCpuArchitecture() that respects macOS's rosetta2
QSFERA_SYNC_EXPORT QString currentCpuArch();
} // Utility namespace
/** @} */ // \addtogroup
constexpr bool Utility::isWindows()
{
#ifdef Q_OS_WIN
return true;
#else
return false;
#endif
}
constexpr bool Utility::isMac()
{
#ifdef Q_OS_MAC
return true;
#else
return false;
#endif
}
constexpr bool Utility::isUnix()
{
#ifdef Q_OS_UNIX
return true;
#else
return false;
#endif
}
constexpr bool Utility::isLinux()
{
#if defined(Q_OS_LINUX)
return true;
#else
return false;
#endif
}
constexpr bool Utility::isBSD()
{
#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
return true;
#else
return false;
#endif
}
} // OCC namespace
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, std::chrono::nanoseconds in);
/**
*
* Helper macro for enumToDisplayName(), it enables us to handle plain C++ enum values
* @param Enum an enum value
*/
#define OCC_UTIL_ENUM_DISPLAY_NAME(Enum) \
case Enum: \
return QStringLiteral(#Enum);
+298
View File
@@ -0,0 +1,298 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "result.h"
#include "utility.h"
#include <QCoreApplication>
#include <QDir>
#include <QLoggingCategory>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QtMacExtras/QtMacExtras>
#endif
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSUserDefaults.h>
#include <sys/sysctl.h>
namespace OCC {
void Utility::setupFavLink(const QString &folder)
{
// Finder: Place under "Places"/"Favorites" on the left sidebar
CFStringRef folderCFStr = CFStringCreateWithCString(nullptr, folder.toUtf8().data(), kCFStringEncodingUTF8);
QScopeGuard freeFolder([folderCFStr]() { CFRelease(folderCFStr); });
CFURLRef urlRef = CFURLCreateWithFileSystemPath(nullptr, folderCFStr, kCFURLPOSIXPathStyle, true);
QScopeGuard freeUrl([urlRef]() { CFRelease(urlRef); });
LSSharedFileListRef placesItems = LSSharedFileListCreate(nullptr, kLSSharedFileListFavoriteItems, nullptr);
QScopeGuard freePlaces([placesItems]() { CFRelease(placesItems); });
if (placesItems) {
// Insert an item to the list.
if (LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(placesItems, kLSSharedFileListItemLast, nullptr, nullptr, urlRef, nullptr, nullptr)) {
CFRelease(item);
}
}
}
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
{
Q_UNUSED(appName)
return false;
}
static Result<void, QString> writePlistToFile(NSString *plistFile, NSDictionary *plist)
{
NSError *error = nil;
// Check if the directory exists. On a fresh installation for example, the directory does not
// exist, so writing the plist file below will fail.
QDir plistDir = QFileInfo(QString::fromNSString(plistFile)).dir();
if (!plistDir.exists()) {
if (!QDir().mkpath(plistDir.path())) {
return QString(QStringLiteral("Failed to create directory '%1'")).arg(plistDir.path());
}
// Permissions always seem to be 0700, so set that.
// Note: the Permission enum documentation states that on unix the owner permissions are
// returned, but: "This behavior might change in a future Qt version." So we play it safe,
// and set both the user and the owner permissions to rwx.
if (!QFile(plistDir.path())
.setPermissions(QFileDevice::ReadOwner | QFileDevice::ReadUser | QFileDevice::WriteOwner | QFileDevice::WriteUser | QFileDevice::ExeOwner
| QFileDevice::ExeUser)) {
qCInfo(lcUtility()) << "Failed to set directory permmissions for" << plistDir.path();
}
}
// Now write the file.
qCInfo(lcUtility()) << "Writing plist file"
<< QString::fromNSString(plistFile); // Especially for branded clients: log the file name, so it can be found when debugging.
if (![plist writeToURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
return QString::fromNSString(error.localizedDescription);
}
return {};
}
static Result<NSDictionary *, QString> readPlistFromFile(NSString *plistFile)
{
NSError *error = nil;
if (NSDictionary *plist = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
return plist;
} else {
return QString::fromNSString(error.localizedDescription);
}
}
bool Utility::hasLaunchOnStartup(const QString &appName)
{
Q_UNUSED(appName)
@autoreleasepool {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];
if ([fileManager fileExistsAtPath:plistFile]) {
auto maybePlist = readPlistFromFile(plistFile);
if (!maybePlist) {
qCInfo(lcUtility()).nospace() << "Cannot read '" << QString::fromNSString(plistFile) << "', probably not a valid plist file";
return false;
}
if (NSDictionary *plist = *maybePlist) {
// yes, there is a valid plist file...
if (id label = plist[@"Label"]) {
// ... with a label...
if ([appIdentifier isEqualToString:label]) {
// ... and yes, it's the correct app-id...
if (id program = plist[@"Program"]) {
// .. and there is a program mentioned ...
// (Note: case insensitive compare, because most fs setups on mac are case insensitive)
if ([QCoreApplication::applicationFilePath().toNSString() compare:program options:NSCaseInsensitiveSearch] == NSOrderedSame) {
// ... and it's our executable ..
if (NSNumber *value = plist[@"RunAtLoad"]) {
// yes, there is even a RunAtLoad key, so use it!
return [value boolValue];
}
}
}
}
}
}
}
}
return false;
}
static Result<void, QString> writeNewPlistFile(NSString *plistFile, NSString *fullPath, bool enable)
{
NSDictionary *plistTemplate =
@{@"Label" : QCoreApplication::organizationDomain().toNSString(),
@"KeepAlive" : @NO,
@"Program" : fullPath,
@"RunAtLoad" : enable ? @YES : @NO};
return writePlistToFile(plistFile, plistTemplate);
}
static Result<void, QString> modifyPlist(NSString *plistFile, NSDictionary *plist, bool enable)
{
if (NSNumber *value = plist[@"RunAtLoad"]) {
// ok, there is a key
if ([value boolValue] == enable) {
// nothing to do
return {};
}
}
// now either the key was missing, or it had the wrong value, so set the key and write the plist back
NSMutableDictionary *newPlist = [plist mutableCopy];
newPlist[@"RunAtLoad"] = enable ? @YES : @NO;
return writePlistToFile(plistFile, newPlist);
}
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
{
Q_UNUSED(appName)
Q_UNUSED(guiName)
@autoreleasepool {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *fullPath = QCoreApplication::applicationFilePath().toNSString();
NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];
// An error might occur in the code below, but we cannot report anything, so we just ignore them.
if ([fileManager fileExistsAtPath:plistFile]) {
auto maybePlist = readPlistFromFile(plistFile);
if (!maybePlist) {
// broken plist, overwrite it
auto result = writeNewPlistFile(plistFile, fullPath, enable);
if (!result) {
qCWarning(lcUtility) << Q_FUNC_INFO << result.error();
}
return;
}
NSDictionary *plist = *maybePlist;
id programValue = plist[@"Program"];
if (programValue == nil) {
// broken plist, overwrite it
auto result = writeNewPlistFile(plistFile, fullPath, enable);
if (!result) {
qCWarning(lcUtility) << result.error();
}
} else if (![fileManager fileExistsAtPath:programValue]) {
// Ok, a plist from some removed program, overwrite it
auto result = writeNewPlistFile(plistFile, fullPath, enable);
if (!result) {
qCWarning(lcUtility) << result.error();
}
} else if ([fullPath compare:programValue options:NSCaseInsensitiveSearch]
== NSOrderedSame) { // (Note: case insensitive compare, because most fs setups on mac are case insensitive)
// Wohoo, it's ours! Now carefully change only the RunAtLoad entry. If any value for
// e.g. KeepAlive was changed, we leave it as-is.
auto result = modifyPlist(plistFile, plist, enable);
if (!result) {
qCWarning(lcUtility) << result.error();
}
} else if ([fullPath hasPrefix:@"/Applications/"]) {
// ok, we seem to be an officially installed application, overwrite the file
auto result = writeNewPlistFile(plistFile, fullPath, enable);
if (!result) {
qCWarning(lcUtility) << result.error();
}
} else {
qCInfo(lcUtility) << "We're not an installed application, there is anoter executable "
"mentioned in the plist file, and that executable seems to exist, "
"so let's not touch the file.";
}
} else {
if (enable) {
// plist doens't exist, write a new one.
auto result = writeNewPlistFile(plistFile, fullPath, enable);
if (!result) {
qCWarning(lcUtility) << result.error();
}
} else {
// Do nothing: if the file doesn't exist, the client won't be auto-started.
}
}
}
}
#ifndef TOKEN_AUTH_ONLY
bool Utility::hasDarkSystray()
{
@autoreleasepool {
if (auto style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]) {
return [style isEqualToString:@"Dark"];
}
}
return false;
}
#endif
QString Utility::currentCpuArch()
{
static const QString rv = []() {
int procTranslated = 0;
size_t size = sizeof(procTranslated);
const auto rv = sysctlbyname("sysctl.proc_translated", &procTranslated, &size, nullptr, 0);
// should be backed up if not evaluated immediately
const auto error = errno;
qCDebug(lcUtility) << "proc_translated:" << procTranslated;
qCDebug(lcUtility) << "error:" << strerror(error);
if (rv != -1) {
if (procTranslated == 1) {
qCInfo(lcUtility) << "process translated with rosetta2";
return QStringLiteral("arm64");
} else {
qCInfo(lcUtility) << "process not translated";
}
} else {
if (error == ENOENT) {
qCDebug(lcUtility) << "not running on Apple Silicon";
} else {
qCDebug(lcUtility) << "unknown error trying to detect whether proc is translated";
}
}
const auto qtArch = QSysInfo::currentCpuArchitecture();
qCDebug(lcUtility) << "CPU arch reported by Qt:" << qtArch;
return qtArch;
}();
return rv;
}
} // namespace OCC
+136
View File
@@ -0,0 +1,136 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "utility.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QSettings>
#include <QStandardPaths>
#include <QString>
#include <QTextStream>
namespace OCC {
void Utility::setupFavLink(const QString &folder)
{
// Nautilus: add to ~/.gtk-bookmarks
QFile gtkBookmarks(QDir::homePath() + QLatin1String("/.config/gtk-3.0/bookmarks"));
const QByteArray folderUrl = QUrl::fromLocalFile(folder).toEncoded(QUrl::ComponentFormattingOption::EncodeSpaces);
if (gtkBookmarks.open(QFile::ReadWrite)) {
QByteArray places = gtkBookmarks.readAll();
if (!places.contains(folderUrl)) {
places += folderUrl;
gtkBookmarks.reset();
gtkBookmarks.write(places + '\n');
}
}
}
// returns the autostart directory the linux way
// and respects the XDG_CONFIG_HOME env variable
static QString getUserAutostartDir()
{
QString config = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
config += QLatin1String("/autostart/");
return config;
}
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
{
Q_UNUSED(appName)
return false;
}
bool Utility::hasLaunchOnStartup(const QString &appName)
{
QString desktopFileLocation = getUserAutostartDir() + appName + QLatin1String(".desktop");
return QFile::exists(desktopFileLocation);
}
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
{
QString userAutoStartPath = getUserAutostartDir();
QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop");
if (enable) {
if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) {
qCWarning(lcUtility) << u"Could not create autostart folder" << userAutoStartPath;
return;
}
QFile iniFile(desktopFileLocation);
if (!iniFile.open(QIODevice::WriteOnly)) {
qCWarning(lcUtility) << u"Could not write auto start entry" << desktopFileLocation;
return;
}
auto autostartApplicationPath = []() -> QString {
// $APPIMAGE will be set to the AppImage's path by the AppImage runtime
// if it is set, we can assume to be run from within an AppImage
// in that case, the desktop file should point to the AppImage rather than the
// main binary, which will be in a temporary mount point
if (Utility::runningInAppImage()) {
return Utility::appImageLocation();
}
// we need to launch the client via its provided AppRun script (or at least source the AppRun hooks) to make sure it is displayed correctly
// if installed as a native package generated by linuxdeploy-plugin-native_packages, a linuxdeploy.conf file will be available that points to the
// location of the installed AppDir
QString linuxdeployConfPath = qApp->applicationDirPath() + QStringLiteral("/linuxdeploy.conf");
if (QFile(linuxdeployConfPath).exists()) {
QSettings linuxdeployConf(linuxdeployConfPath, QSettings::IniFormat);
const auto appdirInstallatedPath = linuxdeployConf.value(QStringLiteral("native_packages/appdir_installed_path"));
if (!appdirInstallatedPath.isNull()) {
return appdirInstallatedPath.toString() + QStringLiteral("/AppRun");
}
}
// otherwise, we just use the application binary's own path, which should work for distribution-packaged installations and also for development
return QCoreApplication::applicationFilePath();
}();
QTextStream ts(&iniFile);
ts.setEncoding(QStringConverter::Utf8);
ts << QStringLiteral("[Desktop Entry]\n" //
"Name=%1\n" //
"GenericName=File Synchronizer\n" //
"Exec=%2\n" //
"Terminal=false\n" //
"Icon=%3\n" //
"Categories=Network\n" //
"Type=Application\n" //
"StartupNotify=false\n" //
"X-GNOME-Autostart-enabled=true\n" //
"X-GNOME-Autostart-Delay=10")
.arg(guiName, autostartApplicationPath, appName.toLower());
} else {
if (!QFile::remove(desktopFileLocation)) {
qCWarning(lcUtility) << u"Could not remove autostart desktop file";
}
}
}
bool Utility::hasDarkSystray()
{
return true;
}
} // namespace OCC
+163
View File
@@ -0,0 +1,163 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "utility_win.h"
#include "utility.h"
#include "libsync/common/asserts.h"
#include "libsync/common/filesystembase.h"
#include "libsync/filesystem.h"
#include <comdef.h>
#include <qt_windows.h>
#include <shlguid.h>
#include <shlobj.h>
#include <string>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
namespace {
const QString systemRunPathC()
{
return QStringLiteral("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Run");
}
const QString runPathC()
{
return QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run");
}
const QString systemThemesC()
{
return QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
}
}
namespace OCC {
void Utility::setupFavLink(const QString &) { }
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
{
QSettings settings(systemRunPathC(), QSettings::NativeFormat);
return settings.contains(appName);
}
bool Utility::hasLaunchOnStartup(const QString &appName)
{
QSettings settings(runPathC(), QSettings::NativeFormat);
return settings.contains(appName);
}
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
{
Q_UNUSED(guiName)
QSettings settings(runPathC(), QSettings::NativeFormat);
if (enable) {
settings.setValue(appName, QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
} else {
settings.remove(appName);
}
}
bool Utility::hasDarkSystray()
{
const QSettings settings(systemThemesC(), QSettings::NativeFormat);
return !settings.value(QStringLiteral("SystemUsesLightTheme"), false).toBool();
}
void Utility::UnixTimeToLargeIntegerFiletime(time_t t, LARGE_INTEGER *hundredNSecs)
{
hundredNSecs->QuadPart = FileSystem::time_tToFileTime(t).time_since_epoch().count();
}
QString Utility::formatWinError(long errorCode)
{
return QStringLiteral("WindowsError: 0x%1: %2")
.arg(QString::number(static_cast<ulong>(errorCode), 16), QString::fromWCharArray(_com_error(errorCode).ErrorMessage()));
}
Utility::Handle::Handle(HANDLE h, std::function<void(HANDLE)> &&close, uint32_t error)
: _handle(h)
, _close(std::move(close))
, _error(error)
{
if (_handle == INVALID_HANDLE_VALUE && _error == NO_ERROR) {
_error = GetLastError();
}
}
Utility::Handle Utility::Handle::createHandle(const std::filesystem::path &path, const CreateHandleParameter &p)
{
uint32_t flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS;
if (!p.followSymlinks) {
flags |= FILE_FLAG_OPEN_REPARSE_POINT;
}
if (p.async) {
flags |= FILE_FLAG_OVERLAPPED;
}
auto handle = Utility::Handle{CreateFileW(path.native().data(), p.accessMode, p.shareMode, nullptr, p.creationFlags, flags, nullptr)};
handle._path = path;
return handle;
}
Utility::Handle::Handle(HANDLE h)
: Handle(h, &CloseHandle)
{
}
Utility::Handle::~Handle()
{
close();
}
void Utility::Handle::close()
{
if (_handle != INVALID_HANDLE_VALUE) {
_close(_handle);
_handle = INVALID_HANDLE_VALUE;
}
}
uint32_t Utility::Handle::error() const
{
return _error;
}
bool Utility::Handle::hasError() const
{
return _error != NO_ERROR;
}
const std::filesystem::path &Utility::Handle::path() const
{
return _path;
}
QString Utility::Handle::errorMessage() const
{
return formatWinError(_error);
}
} // namespace OCC
+106
View File
@@ -0,0 +1,106 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QString>
#include <qt_windows.h>
#include <filesystem>
#include <functional>
namespace OCC {
namespace Utility {
class QSFERA_SYNC_EXPORT Handle
{
public:
/**
* A RAAI for Windows Handles
*/
Handle() = default;
explicit Handle(HANDLE h);
explicit Handle(HANDLE h, std::function<void(HANDLE)> &&close, uint32_t error = NO_ERROR);
struct CreateHandleParameter
{
uint32_t accessMode = 0;
uint32_t shareMode = FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
uint32_t creationFlags = OPEN_EXISTING;
bool followSymlinks = true;
bool async = false;
};
static Handle createHandle(const std::filesystem::path &path, const CreateHandleParameter &p = {});
Handle(const Handle &) = delete;
Handle &operator=(const Handle &) = delete;
Handle(Handle &&other)
{
std::swap(_handle, other._handle);
std::swap(_close, other._close);
}
Handle &operator=(Handle &&other)
{
if (this != &other) {
std::swap(_handle, other._handle);
std::swap(_close, other._close);
std::swap(_error, other._error);
}
return *this;
}
~Handle();
const HANDLE &handle() const { return _handle; }
void close();
explicit operator bool() const { return _handle != INVALID_HANDLE_VALUE; }
operator HANDLE() const { return _handle; }
HANDLE release() { return std::exchange(_handle, INVALID_HANDLE_VALUE); }
uint32_t error() const;
bool hasError() const;
/**
*
* Returns the file system path associated with this handle, if it was set during handle creation.
*
* This can be useful for error reporting, logging, or debugging, as it allows you to identify
* the file or resource that the handle refers to. If no path was set, the returned path may be empty.
*
* @return Reference to the associated std::filesystem::path, or an empty path if not set.
*/
const std::filesystem::path &path() const;
QString errorMessage() const;
private:
HANDLE _handle = INVALID_HANDLE_VALUE;
std::function<void(HANDLE)> _close;
uint32_t _error = NO_ERROR;
std::filesystem::path _path;
};
QSFERA_SYNC_EXPORT void UnixTimeToLargeIntegerFiletime(time_t t, LARGE_INTEGER *hundredNSecs);
QSFERA_SYNC_EXPORT QString formatWinError(long error);
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "common/version.h"
const QVersionNumber &OCC::Version::version()
{
static const auto v = QVersionNumber({ @MIRALL_VERSION_MAJOR@, @MIRALL_VERSION_MINOR@, @MIRALL_VERSION_PATCH@ });
return v;
}
const QVersionNumber &OCC::Version::versionWithBuildNumber()
{
static const auto v = QVersionNumber({ @MIRALL_VERSION_MAJOR@, @MIRALL_VERSION_MINOR@, @MIRALL_VERSION_PATCH@, @MIRALL_VERSION_BUILD@ });
return v;
}
int OCC::Version::buildNumber()
{
return versionWithBuildNumber().segmentAt(3);
}
QString OCC::Version::gitSha()
{
return QStringLiteral("@GIT_SHA1@");
}
QString OCC::Version::displayString()
{
// ensure a well-defined version string
constexpr std::string_view versionString("@MIRALL_VERSION_STRING@");
static_assert(versionString.find("@MIRALL_VERSION_MAJOR@.@MIRALL_VERSION_MINOR@.@MIRALL_VERSION_PATCH@") == 0);
return QStringLiteral("@MIRALL_VERSION_STRING@");
}
bool OCC::Version::isBeta()
{
#cmakedefine01 BETA_CHANNEL_BUILD
return BETA_CHANNEL_BUILD;
}
bool OCC::Version::withUpdateNotification(){
#cmakedefine01 WITH_UPDATE_NOTIFICATION
return WITH_UPDATE_NOTIFICATION;
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "libsync/qsferasynclib.h"
#include <QString>
#include <QVersionNumber>
namespace OCC::Version {
QSFERA_SYNC_EXPORT const QVersionNumber &version();
QSFERA_SYNC_EXPORT const QVersionNumber &versionWithBuildNumber();
QSFERA_SYNC_EXPORT int buildNumber();
/**
* The commit id
*/
QSFERA_SYNC_EXPORT QString gitSha();
QSFERA_SYNC_EXPORT QString displayString();
QSFERA_SYNC_EXPORT bool isBeta();
QSFERA_SYNC_EXPORT bool withUpdateNotification();
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#cmakedefine01 WITH_CRASHREPORTER
#cmakedefine THEME_CLASS @THEME_CLASS@
#cmakedefine THEME_INCLUDE "@THEME_INCLUDE@"
#cmakedefine APPLICATION_NAME "@APPLICATION_NAME@"
#cmakedefine APPLICATION_VENDOR "@APPLICATION_VENDOR@"
#cmakedefine APPLICATION_REV_DOMAIN "@APPLICATION_REV_DOMAIN@"
#cmakedefine APPLICATION_SHORTNAME "@APPLICATION_SHORTNAME@"
#cmakedefine APPLICATION_EXECUTABLE "@APPLICATION_EXECUTABLE@"
#cmakedefine APPLICATION_UPDATE_URL "@APPLICATION_UPDATE_URL@"
#cmakedefine APPLICATION_DEFAULT_SERVER_URL "@APPLICATION_DEFAULT_SERVER_URL@"
#cmakedefine APPLICATION_ICON_NAME "@APPLICATION_ICON_NAME@"
+520
View File
@@ -0,0 +1,520 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "configfile.h"
#include "common/asserts.h"
#include "common/utility.h"
#include "common/version.h"
#include "libsync/globalconfig.h"
#include "logger.h"
#include "theme.h"
#include "creds/abstractcredentials.h"
#include "csync_exclude.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QLoggingCategory>
#include <QOperatingSystemVersion>
#include <QSettings>
#include <QStandardPaths>
#include <chrono>
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
namespace OCC {
namespace chrono = std::chrono;
Q_LOGGING_CATEGORY(lcConfigFile, "sync.configfile", QtInfoMsg)
namespace {
const QString logHttpC() { return QStringLiteral("logHttp"); }
const QString remotePollIntervalC()
{
return QStringLiteral("remotePollInterval");
}
const QString fullLocalDiscoveryIntervalC()
{
return QStringLiteral("fullLocalDiscoveryInterval");
}
const QString crashReporterC()
{
return QStringLiteral("crashReporter");
}
const QString skipUpdateCheckC() { return QStringLiteral("skipUpdateCheck"); }
const QString updateCheckIntervalC() { return QStringLiteral("updateCheckInterval"); }
const QString updateChannelC() { return QStringLiteral("updateChannel"); }
const QString uiLanguageC() { return QStringLiteral("uiLanguage"); }
const QString geometryC() { return QStringLiteral("geometry"); }
const QString timeoutC()
{
return QStringLiteral("timeout");
}
const QString automaticLogDirC() { return QStringLiteral("logToTemporaryLogDir"); }
const QString numberOfLogsToKeepC()
{
return QStringLiteral("numberOfLogsToKeep");
}
// The key `clientVersion` stores the version *with* build number of the config file. It is named
// this way, because before 5.0, only the version *without* build number was stored.
const QString clientVersionC() { return QStringLiteral("clientVersion"); }
const QString useUploadLimitC() { return QStringLiteral("BWLimit/useUploadLimit"); }
const QString useDownloadLimitC() { return QStringLiteral("BWLimit/useDownloadLimit"); }
const QString uploadLimitC() { return QStringLiteral("BWLimit/uploadLimit"); }
const QString downloadLimitC() { return QStringLiteral("BWLimit/downloadLimit"); }
const QString pauseSyncWhenMeteredC()
{
return QStringLiteral("pauseWhenMetered");
}
const QString moveToTrashC() { return QStringLiteral("moveToTrash"); }
const QString issuesWidgetFilterC()
{
return QStringLiteral("issuesWidgetFilter");
}
QString excludeFileNameC()
{
return QStringLiteral("sync-exclude.lst");
}
} // anonymous namespace
QString ConfigFile::_confDir = QString();
const std::chrono::seconds DefaultRemotePollInterval { 30 };
static chrono::milliseconds millisecondsValue(const QSettings &setting, const QString &key,
chrono::milliseconds defaultValue)
{
return chrono::milliseconds(setting.value(key, qlonglong(defaultValue.count())).toLongLong());
}
ConfigFile::ConfigFile()
{
QSettings::setDefaultFormat(QSettings::IniFormat);
}
bool ConfigFile::setConfDir(const QString &value)
{
QString dirPath = value;
if (dirPath.isEmpty())
return false;
QFileInfo fi(dirPath);
if (!fi.exists()) {
QDir().mkpath(dirPath);
fi.setFile(dirPath);
}
if (fi.exists() && fi.isDir()) {
dirPath = fi.absoluteFilePath();
qCInfo(lcConfigFile) << u"Using custom config dir " << dirPath;
_confDir = dirPath;
return true;
}
return false;
}
std::optional<QStringList> ConfigFile::issuesWidgetFilter() const
{
auto settings = makeQSettings();
if (settings.contains(issuesWidgetFilterC())) {
return settings.value(issuesWidgetFilterC()).toStringList();
}
return {};
}
void ConfigFile::setIssuesWidgetFilter(const QStringList &checked)
{
auto settings = makeQSettings();
settings.setValue(issuesWidgetFilterC(), checked);
settings.sync();
}
std::chrono::seconds ConfigFile::timeout() const
{
auto settings = makeQSettings();
const auto val = settings.value(timeoutC()).toInt(); // default to 5 min
return val ? std::chrono::seconds(val) : 5min;
}
void ConfigFile::saveGeometry(QWidget *w)
{
OC_ASSERT(!w->objectName().isNull());
auto settings = makeQSettings();
settings.beginGroup(w->objectName());
settings.setValue(geometryC(), w->saveGeometry());
settings.sync();
}
void ConfigFile::restoreGeometry(QWidget *w)
{
w->restoreGeometry(getValue(QStringLiteral("%1/%2").arg(geometryC(), w->objectName())).toByteArray());
}
void ConfigFile::saveGeometryHeader(QHeaderView *header)
{
if (!header)
return;
OC_ASSERT(!header->objectName().isEmpty());
auto settings = makeQSettings();
settings.beginGroup(header->objectName());
settings.setValue(geometryC(), header->saveState());
settings.sync();
}
bool ConfigFile::restoreGeometryHeader(QHeaderView *header)
{
Q_ASSERT(header && !header->objectName().isNull());
auto settings = makeQSettings();
settings.beginGroup(header->objectName());
if (settings.contains(geometryC())) {
header->restoreState(settings.value(geometryC()).toByteArray());
return true;
}
return false;
}
QString ConfigFile::configPath()
{
if (_confDir.isEmpty()) {
// On Unix, use the AppConfigLocation for the settings, that's configurable with the XDG_CONFIG_HOME env variable.
// On Windows, use AppDataLocation, that's where the roaming data is and where we should store the config file
_confDir = QStandardPaths::writableLocation(Utility::isWindows() ? QStandardPaths::AppDataLocation : QStandardPaths::AppConfigLocation);
}
QString dir = _confDir;
if (!dir.endsWith(QLatin1Char('/')))
dir.append(QLatin1Char('/'));
return dir;
}
QString ConfigFile::excludeFile(Scope scope) const
{
switch (scope) {
case UserScope:
return configPath() + excludeFileNameC();
case SystemScope:
return ConfigFile::defaultExcludeFile();
}
Q_UNREACHABLE();
}
QString ConfigFile::defaultExcludeFile()
{
return QStringLiteral(":/client/QSfera/theme/universal/%1").arg(excludeFileNameC());
}
QString ConfigFile::backup() const
{
QString baseFile = configFile();
auto versionString = clientVersionWithBuildNumberString();
if (!versionString.isEmpty())
versionString.prepend(QLatin1Char('_'));
const QString backupFile =
QStringLiteral("%1.backup_%2%3")
.arg(baseFile, QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss")), versionString);
// If this exact file already exists it's most likely that a backup was
// already done. (two backup calls directly after each other, potentially
// even with source alterations in between!)
if (!QFile::exists(backupFile)) {
QFile f(baseFile);
f.copy(backupFile);
}
return backupFile;
}
QString ConfigFile::configFile()
{
return configPath() + Theme::instance()->configFileName();
}
QSettings ConfigFile::makeQSettings()
{
return {configFile(), QSettings::IniFormat};
}
std::unique_ptr<QSettings> ConfigFile::makeQSettingsPointer()
{
return std::unique_ptr<QSettings>(new QSettings(makeQSettings()));
}
bool ConfigFile::exists()
{
return QFileInfo::exists(configFile());
}
chrono::milliseconds ConfigFile::remotePollInterval(std::chrono::seconds defaultVal) const
{
auto settings = makeQSettings();
auto defaultPollInterval { DefaultRemotePollInterval };
// The server default-capabilities was set to 60 in some server releases,
// which, if interpreted in milliseconds, is pretty small.
// If the value is above 5 seconds, it was set intentionally.
// Server admins have to set the value in Milliseconds!
// i.e. set to greater than 5000 milliseconds on the server to be effective.
if (defaultVal > chrono::seconds(5)) {
defaultPollInterval = defaultVal;
}
auto remoteInterval = millisecondsValue(settings, remotePollIntervalC(), defaultPollInterval);
if (remoteInterval < chrono::seconds(5)) {
remoteInterval = defaultPollInterval;
qCWarning(lcConfigFile) << u"Remote Interval is less than 5 seconds, reverting to" << remoteInterval.count();
}
return remoteInterval;
}
chrono::milliseconds OCC::ConfigFile::fullLocalDiscoveryInterval() const
{
auto settings = makeQSettings();
return millisecondsValue(settings, fullLocalDiscoveryIntervalC(), 1h);
}
chrono::milliseconds ConfigFile::updateCheckInterval() const
{
auto settings = makeQSettings();
auto defaultInterval = chrono::hours(10);
auto interval = millisecondsValue(settings, updateCheckIntervalC(), defaultInterval);
auto minInterval = chrono::minutes(5);
if (interval < minInterval) {
qCWarning(lcConfigFile) << u"Update check interval less than five minutes, resetting to 5 minutes";
interval = minInterval;
}
return interval;
}
bool ConfigFile::skipUpdateCheck() const
{
const auto fallback = getValue(skipUpdateCheckC(), false);
#ifdef Q_OS_WIN
return GlobalConfig::getPolicySetting(skipUpdateCheckC(), fallback).toBool();
#else
return fallback.toBool();
#endif
}
void ConfigFile::setSkipUpdateCheck(bool skip)
{
auto settings = makeQSettings();
settings.setValue(skipUpdateCheckC(), QVariant(skip));
settings.sync();
}
QString ConfigFile::updateChannel() const
{
auto settings = makeQSettings();
return settings.value(updateChannelC(), OCC::Version::isBeta() ? u"stable"_s : u"beta"_s).toString();
}
void ConfigFile::setUpdateChannel(const QString &channel)
{
auto settings = makeQSettings();
settings.setValue(updateChannelC(), channel);
}
QString ConfigFile::uiLanguage() const
{
auto settings = makeQSettings();
return settings.value(uiLanguageC(), QString()).toString();
}
void ConfigFile::setUiLanguage(const QString &uiLanguage)
{
auto settings = makeQSettings();
settings.setValue(uiLanguageC(), uiLanguage);
}
QVariant ConfigFile::getValue(const QString &param, const QVariant &defaultValue) const
{
auto setting = makeQSettings().value(param);
if (!setting.isValid()) {
return GlobalConfig::getValue(param, defaultValue);
}
return setting;
}
void ConfigFile::setValue(const QString &key, const QVariant &value)
{
auto settings = makeQSettings();
settings.setValue(key, value);
}
int ConfigFile::useUploadLimit() const
{
return getValue(useUploadLimitC(), 0).toInt();
}
int ConfigFile::useDownloadLimit() const
{
return getValue(useDownloadLimitC(), 0).toInt();
}
void ConfigFile::setUseUploadLimit(int val)
{
setValue(useUploadLimitC(), val);
}
void ConfigFile::setUseDownloadLimit(int val)
{
setValue(useDownloadLimitC(), val);
}
int ConfigFile::uploadLimit() const
{
return getValue(uploadLimitC(), 10).toInt();
}
int ConfigFile::downloadLimit() const
{
return getValue(downloadLimitC(), 80).toInt();
}
void ConfigFile::setUploadLimit(int kbytes)
{
setValue(uploadLimitC(), kbytes);
}
void ConfigFile::setDownloadLimit(int kbytes)
{
setValue(downloadLimitC(), kbytes);
}
bool ConfigFile::pauseSyncWhenMetered() const
{
return getValue(pauseSyncWhenMeteredC(), false).toBool();
}
void ConfigFile::setPauseSyncWhenMetered(bool isChecked)
{
setValue(pauseSyncWhenMeteredC(), isChecked);
}
bool ConfigFile::moveToTrash() const
{
if (Theme::instance()->enableMoveToTrash()) {
return getValue(moveToTrashC(), false).toBool();
}
return false;
}
void ConfigFile::setMoveToTrash(bool isChecked)
{
setValue(moveToTrashC(), isChecked);
}
bool ConfigFile::crashReporter() const
{
auto settings = makeQSettings();
return settings.value(crashReporterC(), true).toBool();
}
void ConfigFile::setCrashReporter(bool enabled)
{
auto settings = makeQSettings();
settings.setValue(crashReporterC(), enabled);
}
bool ConfigFile::automaticLogDir() const
{
auto settings = makeQSettings();
return settings.value(automaticLogDirC(), false).toBool();
}
void ConfigFile::setAutomaticLogDir(bool enabled)
{
auto settings = makeQSettings();
settings.setValue(automaticLogDirC(), enabled);
}
int ConfigFile::automaticDeleteOldLogs() const
{
auto settings = makeQSettings();
return settings.value(numberOfLogsToKeepC()).toInt();
}
void ConfigFile::setAutomaticDeleteOldLogs(int number)
{
auto settings = makeQSettings();
settings.setValue(numberOfLogsToKeepC(), number);
}
void ConfigFile::configureHttpLogging(std::optional<bool> enable)
{
if (enable == std::nullopt) {
enable = logHttp();
}
auto settings = makeQSettings();
settings.setValue(logHttpC(), enable.value());
static const QSet<QString> rule = { QStringLiteral("sync.httplogger=true") };
if (enable.value()) {
Logger::instance()->addLogRule(rule);
} else {
Logger::instance()->removeLogRule(rule);
}
}
bool ConfigFile::logHttp() const
{
auto settings = makeQSettings();
return settings.value(logHttpC(), false).toBool();
}
QString ConfigFile::clientVersionWithBuildNumberString() const
{
auto settings = makeQSettings();
return settings.value(clientVersionC(), QString()).toString();
}
void ConfigFile::setClientVersionWithBuildNumberString(const QString &version)
{
auto settings = makeQSettings();
settings.setValue(clientVersionC(), version);
}
void ConfigFile::setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles)
{
ConfigFile cfg;
QString systemList = cfg.excludeFile(ConfigFile::SystemScope);
qCInfo(lcConfigFile) << u"Adding system ignore list to csync:" << systemList;
excludedFiles.addExcludeFilePath(systemList);
QString userList = cfg.excludeFile(ConfigFile::UserScope);
if (QFile::exists(userList)) {
qCInfo(lcConfigFile) << u"Adding user defined ignore list to csync:" << userList;
excludedFiles.addExcludeFilePath(userList);
}
}
}
+159
View File
@@ -0,0 +1,159 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef CONFIGFILE_H
#define CONFIGFILE_H
#include "qsferasynclib.h"
#include <QSettings>
#include <chrono>
#include <memory>
#include <optional>
class QWidget;
class QHeaderView;
class ExcludedFiles;
namespace OCC {
class AbstractCredentials;
/**
* @brief The ConfigFile class
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT ConfigFile
{
public:
static QString configPath();
static QString configFile();
static QSettings makeQSettings();
static std::unique_ptr<QSettings> makeQSettingsPointer();
static bool exists();
ConfigFile();
enum Scope {
UserScope,
SystemScope
};
QString excludeFile(Scope scope) const;
static QString defaultExcludeFile(); // doesn't access config dir
/**
* Creates a backup of the file
*
* Returns the path of the new backup.
*/
QString backup() const;
/* Server poll interval in milliseconds */
std::chrono::milliseconds remotePollInterval(std::chrono::seconds defaultVal) const;
/* Set poll interval. Value in milliseconds has to be larger than 5000 */
void setRemotePollInterval(std::chrono::milliseconds interval);
/**
* Interval in milliseconds within which full local discovery is required
*
* Use -1 to disable regular full local discoveries.
*/
std::chrono::milliseconds fullLocalDiscoveryInterval() const;
bool crashReporter() const;
void setCrashReporter(bool enabled);
/** Whether to set up logging to a temp directory on startup.
*
* Configured via the log window. Not used if command line sets up logging.
*/
bool automaticLogDir() const;
void setAutomaticLogDir(bool enabled);
/** Number of log files to keep */
int automaticDeleteOldLogs() const;
void setAutomaticDeleteOldLogs(int number);
/** Whether to log http traffic */
bool logHttp() const;
/**
* Set up HTTP logging.
* This method should be called during application startup to make sure no messages are missed.
*/
void configureHttpLogging(std::optional<bool> enable = std::nullopt);
/** 0: no limit, 1: manual, >0: automatic */
int useUploadLimit() const;
int useDownloadLimit() const;
void setUseUploadLimit(int);
void setUseDownloadLimit(int);
/** in kbyte/s */
int uploadLimit() const;
int downloadLimit() const;
void setUploadLimit(int kbytes);
void setDownloadLimit(int kbytes);
bool pauseSyncWhenMetered() const;
void setPauseSyncWhenMetered(bool isChecked);
/** If we should move the files deleted on the server in the trash */
bool moveToTrash() const;
void setMoveToTrash(bool);
/// Used for testing, so we do not change the user's config file.
static bool setConfDir(const QString &value);
std::optional<QStringList> issuesWidgetFilter() const;
void setIssuesWidgetFilter(const QStringList &checked);
std::chrono::seconds timeout() const;
void saveGeometry(QWidget *w);
void restoreGeometry(QWidget *w);
// how often the check about new versions runs
std::chrono::milliseconds updateCheckInterval() const;
bool skipUpdateCheck() const;
void setSkipUpdateCheck(bool);
QString updateChannel() const;
void setUpdateChannel(const QString &channel);
QString uiLanguage() const;
void setUiLanguage(const QString &uiLanguage);
void saveGeometryHeader(QHeaderView *header);
bool restoreGeometryHeader(QHeaderView *header);
/** The client version that last used this settings file.
Updated by configVersionMigration() at client startup. */
QString clientVersionWithBuildNumberString() const;
void setClientVersionWithBuildNumberString(const QString &version);
/// Add the system and user exclude file path to the ExcludedFiles instance.
static void setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles);
private:
QVariant getValue(const QString &param, const QVariant &defaultValue = QVariant()) const;
void setValue(const QString &key, const QVariant &value);
static QString _confDir;
};
}
#endif // CONFIGFILE_H
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "cookiejar.h"
// empty file for moc
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "qsferasynclib.h"
#include <QNetworkCookieJar>
namespace OCC {
class QSFERA_SYNC_EXPORT CookieJar : public QNetworkCookieJar
{
Q_OBJECT
public:
using QNetworkCookieJar::QNetworkCookieJar;
// expose protected functions
using QNetworkCookieJar::setAllCookies;
using QNetworkCookieJar::allCookies;
};
} // namespace OCC
@@ -0,0 +1,38 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include <QLoggingCategory>
#include "account.h"
#include "common/asserts.h"
#include "creds/abstractcredentials.h"
namespace OCC {
Q_LOGGING_CATEGORY(lcCredentials, "sync.credentials", QtInfoMsg)
AbstractCredentials::AbstractCredentials()
: _account(nullptr)
, _wasFetched(false)
{
}
void AbstractCredentials::setAccount(Account *account)
{
OC_ENFORCE_X(!_account, "should only setAccount once");
_account = account;
}
void AbstractCredentials::checkCredentials(QNetworkReply *) { }
} // namespace OCC
@@ -0,0 +1,115 @@
/*
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef MIRALL_CREDS_ABSTRACT_CREDENTIALS_H
#define MIRALL_CREDS_ABSTRACT_CREDENTIALS_H
#include <QObject>
#include "accessmanager.h"
#include "accountfwd.h"
#include "qsferasynclib.h"
#include <../csync.h>
class QNetworkAccessManager;
class QNetworkReply;
namespace OCC {
class AbstractNetworkJob;
class QSFERA_SYNC_EXPORT AbstractCredentials : public QObject
{
Q_OBJECT
public:
AbstractCredentials();
// No need for virtual destructor - QObject already has one.
/** The bound account for the credentials instance.
*
* Credentials are always used in conjunction with an account.
* Calling Account::setCredentials() will call this function.
* Credentials only live as long as the underlying account object.
*/
virtual void setAccount(Account *account);
virtual AccessManager *createAM() const = 0;
/** Whether there are credentials that can be used for a connection attempt. */
virtual bool ready() const = 0;
/** Whether fetchFromKeychain() was called before. */
bool wasFetched() const { return _wasFetched; }
/** Trigger (async) fetching of credential information
*
* Should set _wasFetched = true, and later Q_EMIT fetched() when done.
*/
virtual void fetchFromKeychain() = 0;
/** Ask credentials from the user (typically async)
*
* Should Q_EMIT asked() when done.
*/
virtual void restartOauth() = 0;
virtual void checkCredentials(QNetworkReply *reply);
virtual void persist() = 0;
/** Invalidates token used to authorize requests, it will no longer be used.
*
* For http auth, this would be the session cookie.
*
* Note that sensitive data (like the password used to acquire the
* session cookie) may be retained. See forgetSensitiveData().
*
* ready() must return false afterwards.
*/
virtual void invalidateToken() = 0;
/** Clears out all sensitive data; used for fully signing out users.
*
* This should always imply invalidateToken() but may go beyond it.
*
* For http auth, this would clear the session cookie and password.
*/
virtual void forgetSensitiveData() = 0;
Q_SIGNALS:
/** Emitted when fetchFromKeychain() is done.
*
* Note that ready() can be true or false, depending on whether there was useful
* data in the keychain.
*/
// TODO: rename
void fetched();
void authenticationStarted();
void authenticationFailed();
/*
* Request to log out.
* The connected account should be marked as logged out
* and no automatic tries to connect should be made.
*/
void requestLogout();
protected:
Account *_account;
bool _wasFetched;
};
} // namespace OCC
#endif
@@ -0,0 +1,240 @@
#include "credentialmanager.h"
#include "account.h"
#include "configfile.h"
#include "theme.h"
#include "common/asserts.h"
#include "common/chronoelapsedtimer.h"
#include <QCborValue>
#include <QLoggingCategory>
#include <QTimer>
#include <chrono>
using namespace std::chrono_literals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcCredentialsManager, "sync.credentials.manager", QtDebugMsg)
namespace {
constexpr auto tiemoutC = 5s;
QString credentialKeyC()
{
return QStringLiteral("%1_credentials").arg(Theme::instance()->appName());
}
QString accountKey(const Account *acc)
{
OC_ASSERT(!acc->url().isEmpty());
return QStringLiteral("%1:%2:%3").arg(credentialKeyC(), acc->url().host(), acc->uuid().toString(QUuid::WithoutBraces));
}
QString scope(const CredentialManager *const manager)
{
return manager->account() ? accountKey(manager->account()) : credentialKeyC();
}
QString scopedKey(const CredentialManager *const manager, const QString &key)
{
return scope(manager) + QLatin1Char(':') + key;
}
}
CredentialManager::CredentialManager(Account *acc)
: QObject(acc)
, _account(acc)
{
}
CredentialManager::CredentialManager(QObject *parent)
: QObject(parent)
{
}
CredentialJob *CredentialManager::get(const QString &key)
{
qCInfo(lcCredentialsManager) << u"get" << scopedKey(this, key);
auto out = new CredentialJob(this, key);
out->start();
return out;
}
QKeychain::Job *CredentialManager::set(const QString &key, const QVariant &data)
{
OC_ASSERT(!data.isNull());
qCInfo(lcCredentialsManager) << u"set" << scopedKey(this, key);
auto writeJob = new QKeychain::WritePasswordJob(Theme::instance()->appName());
writeJob->setKey(scopedKey(this, key));
auto timer = new QTimer(writeJob);
timer->setInterval(tiemoutC);
Utility::ChronoElapsedTimer elapsedTimer;
connect(timer, &QTimer::timeout, writeJob,
[writeJob, elapsedTimer] { qCWarning(lcCredentialsManager) << u"set" << writeJob->key() << u"has not yet finished." << elapsedTimer; });
connect(writeJob, &QKeychain::WritePasswordJob::finished, this, [writeJob, key, elapsedTimer, this] {
if (writeJob->error() == QKeychain::NoError) {
qCInfo(lcCredentialsManager) << u"added" << writeJob->key() << u"after" << elapsedTimer;
// just a list, the values don't matter
credentialsList().setValue(key, true);
} else {
qCWarning(lcCredentialsManager) << u"Failed to set:" << writeJob->key() << writeJob->errorString() << u"after" << elapsedTimer;
}
});
writeJob->setBinaryData(QCborValue::fromVariant(data).toCbor());
// start is delayed so we can directly call it
writeJob->start();
timer->start();
return writeJob;
}
QKeychain::Job *CredentialManager::remove(const QString &key)
{
OC_ASSERT(contains(key));
// remove immediately to prevent double invocation by clear()
credentialsList().remove(key);
qCInfo(lcCredentialsManager) << u"del" << scopedKey(this, key);
auto keychainJob = new QKeychain::DeletePasswordJob(Theme::instance()->appName());
keychainJob->setKey(scopedKey(this, key));
connect(keychainJob, &QKeychain::DeletePasswordJob::finished, this, [keychainJob, key, this] {
OC_ASSERT(keychainJob->error() != QKeychain::EntryNotFound);
if (keychainJob->error() == QKeychain::NoError) {
qCInfo(lcCredentialsManager) << u"removed" << scopedKey(this, key);
} else {
qCWarning(lcCredentialsManager) << u"Failed to remove:" << scopedKey(this, key) << keychainJob->errorString();
}
});
// start is delayed so we can directly call it
keychainJob->start();
return keychainJob;
}
QVector<QPointer<QKeychain::Job>> CredentialManager::clear(const QString &group)
{
OC_ENFORCE(_account || !group.isEmpty());
const auto keys = knownKeys(group);
QVector<QPointer<QKeychain::Job>> out;
out.reserve(keys.size());
for (const auto &key : keys) {
out << remove(key);
}
return out;
}
const Account *CredentialManager::account() const
{
return _account;
}
bool CredentialManager::contains(const QString &key) const
{
return credentialsList().contains(key);
}
QStringList CredentialManager::knownKeys(const QString &group) const
{
if (group.isEmpty()) {
return credentialsList().allKeys();
}
credentialsList().beginGroup(group);
const auto keys = credentialsList().allKeys();
QStringList out;
out.reserve(keys.size());
for (const auto &k : keys) {
out.append(group + QLatin1Char('/') + k);
}
credentialsList().endGroup();
return out;
}
/**
* Utility function to lazily create the settings (group).
*
* IMPORTANT: the underlying storage is a std::unique_ptr, so do *NOT* store this reference anywhere!
*/
QSettings &CredentialManager::credentialsList() const
{
// delayed init as scope requires a fully inizialised acc
if (!_credentialsList) {
_credentialsList = ConfigFile::makeQSettingsPointer();
_credentialsList->beginGroup(QStringLiteral("Credentials/") + scope(this));
}
return *_credentialsList;
}
CredentialJob::CredentialJob(CredentialManager *parent, const QString &key)
: QObject(parent)
, _key(key)
, _parent(parent)
{
connect(this, &CredentialJob::finished, this, &CredentialJob::deleteLater);
}
QString CredentialJob::errorString() const
{
return _errorString;
}
const QVariant &CredentialJob::data() const
{
return _data;
}
QKeychain::Error CredentialJob::error() const
{
return _error;
}
void CredentialJob::start()
{
if (!_parent->contains(_key)) {
_error = QKeychain::EntryNotFound;
// QKeychain is started delayed, Q_EMIT the signal delayed to make sure we are connected
qCDebug(lcCredentialsManager) << u"We don't know" << _key << u"skipping retrieval from keychain";
QTimer::singleShot(0, this, &CredentialJob::finished);
return;
}
_job = new QKeychain::ReadPasswordJob(Theme::instance()->appName());
_job->setKey(scopedKey(_parent, _key));
connect(_job, &QKeychain::ReadPasswordJob::finished, this, [this] {
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
if (_retryOnKeyChainError && (_job->error() == QKeychain::NoBackendAvailable || _job->error() == QKeychain::OtherError)) {
// Could be that the backend was not yet available. Wait some extra seconds.
// (Issues #4274 and #6522)
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain)
qCInfo(lcCredentialsManager) << u"Backend unavailable (yet?) Retrying in a few seconds." << _job->errorString();
QTimer::singleShot(10s, this, &CredentialJob::start);
_retryOnKeyChainError = false;
}
#endif
OC_ASSERT(_job->error() != QKeychain::EntryNotFound);
if (_job->error() == QKeychain::NoError) {
QCborParserError error;
const auto obj = QCborValue::fromCbor(_job->binaryData(), &error);
if (error.error != QCborError::NoError) {
_error = QKeychain::OtherError;
_errorString = tr("Failed to parse credentials %1").arg(error.errorString());
return;
}
_data = obj.toVariant();
OC_ASSERT(_data.isValid());
} else {
qCWarning(lcCredentialsManager) << u"Failed to get password" << scopedKey(_parent, _key) << _job->errorString();
_error = _job->error();
_errorString = _job->errorString();
}
Q_EMIT finished();
});
_job->start();
}
QString CredentialJob::key() const
{
return _key;
}
@@ -0,0 +1,81 @@
#pragma once
#include <QSettings>
#include <QVariant>
#include "qsferasynclib.h"
#include <qt6keychain/keychain.h>
#include <memory>
namespace OCC {
class Account;
class CredentialJob;
class QSFERA_SYNC_EXPORT CredentialManager : public QObject
{
Q_OBJECT
public:
// global credentials
CredentialManager(QObject *parent);
// account related credentials
explicit CredentialManager(Account *acc);
CredentialJob *get(const QString &key);
QKeychain::Job *set(const QString &key, const QVariant &data);
QKeychain::Job *remove(const QString &key);
/**
* Delete all credentials asigned with an account
*/
QVector<QPointer<QKeychain::Job>> clear(const QString &group = {});
bool contains(const QString &key) const;
const Account *account() const;
private:
QSettings &credentialsList() const;
// TestCredentialManager
QStringList knownKeys(const QString &group = {}) const;
const Account *const _account = nullptr;
mutable std::unique_ptr<QSettings> _credentialsList;
friend class TestCredentialManager;
};
class QSFERA_SYNC_EXPORT CredentialJob : public QObject
{
Q_OBJECT
public:
QString key() const;
QKeychain::Error error() const;
const QVariant &data() const;
QString errorString() const;
Q_SIGNALS:
void finished();
private:
CredentialJob(CredentialManager *parent, const QString &key);
void start();
QString _key;
QVariant _data;
QKeychain::Error _error = QKeychain::NoError;
QString _errorString;
bool _retryOnKeyChainError = true;
QKeychain::ReadPasswordJob *_job;
CredentialManager *const _parent;
friend class CredentialManager;
friend class TestCredentialManager;
};
}
@@ -0,0 +1,317 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "creds/httpcredentials.h"
#include "accessmanager.h"
#include "account.h"
#include "configfile.h"
#include "creds/credentialmanager.h"
#include "oauth.h"
#include "syncengine.h"
#include <QAuthenticator>
#include <QLoggingCategory>
#include <QNetworkInformation>
#include <QNetworkReply>
#include <chrono>
using namespace std::chrono_literals;
Q_LOGGING_CATEGORY(lcHttpCredentials, "sync.credentials.http", QtInfoMsg)
namespace {
constexpr int TokenRefreshMaxRetries = 3;
constexpr std::chrono::seconds TokenRefreshDefaultTimeout = 30s;
const char authenticationFailedC[] = "qsfera-authentication-failed";
auto refreshTokenKeyC()
{
return QStringLiteral("http/oauthtoken");
}
}
namespace OCC {
class HttpCredentialsAccessManager : public AccessManager
{
Q_OBJECT
public:
HttpCredentialsAccessManager(const HttpCredentials *cred, QObject *parent = nullptr)
: AccessManager(parent)
, _cred(cred)
{
}
protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
{
QNetworkRequest req(request);
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
if (_cred && !_cred->_accessToken.isEmpty()) {
req.setRawHeader("Authorization", "Bearer " + _cred->_accessToken.toUtf8());
}
}
return AccessManager::createRequest(op, req, outgoingData);
}
private:
// The credentials object dies along with the account, while the QNAM might
// outlive both.
QPointer<const HttpCredentials> _cred;
};
HttpCredentials::HttpCredentials(const QString &accessToken)
: _accessToken(accessToken)
, _ready(true)
{
}
AccessManager *HttpCredentials::createAM() const
{
AccessManager *am = new HttpCredentialsAccessManager(this);
connect(am, &QNetworkAccessManager::authenticationRequired,
this, &HttpCredentials::slotAuthentication);
return am;
}
bool HttpCredentials::ready() const
{
return _ready;
}
void HttpCredentials::fetchFromKeychain()
{
_wasFetched = true;
if (!_ready && !_refreshToken.isEmpty()) {
// This happens if the credentials are still loaded from the keychain, bur we are called
// here because the auth is invalid, so this means we simply need to refresh the credentials
refreshAccessToken();
return;
}
if (_ready) {
Q_EMIT fetched();
} else {
fetchFromKeychainHelper();
}
}
void HttpCredentials::fetchFromKeychainHelper()
{
auto job = _account->credentialManager()->get(refreshTokenKeyC());
connect(job, &CredentialJob::finished, this, [job, this] {
auto handleError = [job, this] {
qCWarning(lcHttpCredentials) << u"Could not retrieve client password from keychain" << job->errorString();
// we come here if the password is empty or any other keychain
// error happend.
_fetchErrorString = job->error() != QKeychain::EntryNotFound ? job->errorString() : QString();
_accessToken.clear();
_ready = false;
Q_EMIT fetched();
};
if (job->error() != QKeychain::NoError) {
handleError();
return;
}
const auto data = job->data().toString();
if (OC_ENSURE(!data.isEmpty())) {
_refreshToken = data;
refreshAccessToken();
} else {
handleError();
}
});
}
void HttpCredentials::checkCredentials(QNetworkReply *reply)
{
// The function is called in order to determine whether we need to ask the user for a password
// if we are using OAuth, we already started a refresh in slotAuthentication, at least in theory, ensure the auth is started.
// If the refresh fails, we are going to Q_EMIT authenticationFailed ourselves
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
slotAuthentication(reply, nullptr);
}
}
void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator)
{
qCDebug(lcHttpCredentials) << Q_FUNC_INFO << reply;
if (!_ready)
return;
Q_UNUSED(authenticator)
// Because of issue #4326, we need to set the login and password manually at every requests
// Thus, if we reach this signal, those credentials were invalid and we terminate.
qCWarning(lcHttpCredentials) << u"Stop request: Authentication failed for " << reply->url().toString() << reply->request().rawHeader("Original-Request-ID");
reply->setProperty(authenticationFailedC, true);
if (!_oAuthJob) {
qCInfo(lcHttpCredentials) << u"Refreshing token";
refreshAccessToken();
}
}
bool HttpCredentials::refreshAccessToken()
{
return refreshAccessTokenInternal(0);
}
bool HttpCredentials::refreshAccessTokenInternal(int tokenRefreshRetriesCount)
{
if (_refreshToken.isEmpty())
return false;
if (_oAuthJob) {
return true;
}
// don't touch _ready or the account state will start a new authentication
// _ready = false;
// parent with nam to ensure we reset when the nam is reset
_oAuthJob = new AccountBasedOAuth(_account->sharedFromThis(), _account->accessManager());
connect(_oAuthJob, &AccountBasedOAuth::refreshError, this, [tokenRefreshRetriesCount, this](QNetworkReply::NetworkError error, const QString &) {
_oAuthJob->deleteLater();
auto networkUnavailable = []() {
if (auto qni = QNetworkInformation::instance()) {
if (qni->reachability() == QNetworkInformation::Reachability::Disconnected) {
return true;
}
}
return false;
};
int nextTry = tokenRefreshRetriesCount + 1;
std::chrono::seconds timeout = {};
if (networkUnavailable()) {
nextTry = 0;
timeout = TokenRefreshDefaultTimeout;
} else {
switch (error) {
case QNetworkReply::ContentNotFoundError:
// 404: bigip f5?
timeout = 0s;
break;
case QNetworkReply::HostNotFoundError:
[[fallthrough]];
case QNetworkReply::TimeoutError:
[[fallthrough]];
// Qt reports OperationCanceledError if the request timed out
case QNetworkReply::OperationCanceledError:
[[fallthrough]];
case QNetworkReply::TemporaryNetworkFailureError:
[[fallthrough]];
// VPN not ready?
case QNetworkReply::ConnectionRefusedError:
nextTry = 0;
[[fallthrough]];
default:
timeout = TokenRefreshDefaultTimeout;
}
}
if (nextTry >= TokenRefreshMaxRetries) {
qCWarning(lcHttpCredentials) << u"Too many failed refreshes" << nextTry << u"-> log out";
forgetSensitiveData();
Q_EMIT authenticationFailed();
Q_EMIT fetched();
return;
}
QTimer::singleShot(timeout, this, [nextTry, this] {
refreshAccessTokenInternal(nextTry);
});
Q_EMIT authenticationFailed();
});
connect(_oAuthJob, &AccountBasedOAuth::refreshFinished, this, [this](const QString &accessToken, const QString &refreshToken) {
_oAuthJob->deleteLater();
if (refreshToken.isEmpty()) {
// an error occured, log out
forgetSensitiveData();
Q_EMIT authenticationFailed();
Q_EMIT fetched();
return;
}
_refreshToken = refreshToken;
if (!accessToken.isNull()) {
_ready = true;
_accessToken = accessToken;
persist();
}
Q_EMIT fetched();
});
Q_EMIT authenticationStarted();
_oAuthJob->refreshAuthentication(_refreshToken);
return true;
}
void HttpCredentials::invalidateToken()
{
qCWarning(lcHttpCredentials) << u"Invalidating the credentials";
if (!_accessToken.isEmpty()) {
_previousPassword = _accessToken;
}
_accessToken = QString();
_ready = false;
// clear the session cookie.
_account->clearCookieJar();
if (!_refreshToken.isEmpty()) {
// Only invalidate the access_token (_password) but keep the _refreshToken in the keychain
// (when coming from forgetSensitiveData, the _refreshToken is cleared)
return;
}
_account->credentialManager()->clear(QStringLiteral("http"));
// let QNAM forget about the password
// This needs to be done later in the event loop because we might be called (directly or
// indirectly) from QNetworkAccessManagerPrivate::authenticationRequired, which itself
// is a called from a BlockingQueuedConnection from the Qt HTTP thread. And clearing the
// cache needs to synchronize again with the HTTP thread.
QTimer::singleShot(0, _account, &Account::clearAMCache);
}
void HttpCredentials::forgetSensitiveData()
{
// need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
_refreshToken.clear();
invalidateToken();
_previousPassword.clear();
}
void HttpCredentials::persist()
{
// write secrets to the keychain
// _refreshToken should only be empty when we are logged out...
if (!_refreshToken.isEmpty()) {
_account->credentialManager()->set(refreshTokenKeyC(), _refreshToken);
}
}
} // namespace OCC
#include "httpcredentials.moc"
@@ -0,0 +1,89 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#ifndef MIRALL_CREDS_HTTP_CREDENTIALS_H
#define MIRALL_CREDS_HTTP_CREDENTIALS_H
#include "creds/abstractcredentials.h"
#include "creds/oauth.h"
#include "networkjobs.h"
#include <QSslKey>
#include <QNetworkRequest>
class QNetworkReply;
class QAuthenticator;
namespace OCC {
class OAuth;
/*
The authentication system is this way because of Shibboleth.
There used to be two different ways to authenticate: Shibboleth and HTTP Basic Auth.
AbstractCredentials can be inherited from both ShibbolethCrendentials and HttpCredentials.
HttpCredentials is then split in HttpCredentials and HttpCredentialsGui.
This class handle both HTTP Basic Auth and OAuth. But anything that needs GUI to ask the user
is in HttpCredentialsGui.
*/
class QSFERA_SYNC_EXPORT HttpCredentials : public AbstractCredentials
{
Q_OBJECT
friend class HttpCredentialsAccessManager;
public:
/// Don't add credentials if this is set on a QNetworkRequest
static constexpr QNetworkRequest::Attribute DontAddCredentialsAttribute = QNetworkRequest::User;
explicit HttpCredentials(const QString &accessToken);
AccessManager *createAM() const override;
bool ready() const override;
void fetchFromKeychain() override;
void checkCredentials(QNetworkReply *reply) override;
void persist() override;
void invalidateToken() override;
void forgetSensitiveData() override;
/* If we still have a valid refresh token, try to refresh it assynchronously and Q_EMIT fetched()
* otherwise return false
*/
bool refreshAccessToken();
protected:
HttpCredentials() = default;
void slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator);
void fetchFromKeychainHelper();
QString _accessToken;
QString _refreshToken;
QString _previousPassword;
QString _fetchErrorString;
bool _ready = false;
QPointer<AccountBasedOAuth> _oAuthJob;
private:
bool refreshAccessTokenInternal(int tokenRefreshRetriesCount);
};
} // namespace OCC
#endif
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "idtoken.h"
#include <QJsonArray>
using namespace Qt::Literals::StringLiterals;
using namespace OCC;
IdToken::IdToken() { }
IdToken::IdToken(const QJsonObject &payload)
: _payload(payload)
{
}
QVariantList IdToken::aud() const
{
const auto aud = _payload.value("aud"_L1);
if (aud.isArray()) {
return aud.toArray().toVariantList();
}
return {aud.toString()};
}
QString IdToken::sub() const
{
return _payload.value("sub"_L1).toString();
}
QString IdToken::preferred_username() const
{
return _payload.value("preferred_username"_L1).toString();
}
QString IdToken::name() const
{
return _payload.value("name"_L1).toString();
}
bool IdToken::isValid() const
{
return !_payload.isEmpty();
}
QJsonObject IdToken::toJson() const
{
return _payload;
}
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include <QJsonObject>
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
namespace OCC {
class IdToken
{
public:
IdToken();
explicit IdToken(const QJsonObject &paylod);
QVariantList aud() const;
QString sub() const;
QString preferred_username() const;
QString name() const;
bool isValid() const;
QJsonObject toJson() const;
private:
QJsonObject _payload;
};
}
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "jwt.h"
OCC::JWT::JWT(const QByteArray &jwt)
{
auto parts = jwt.split('.');
if (parts.size() != 3) {
return;
}
auto parse = [](const QByteArray &part) { return QJsonDocument::fromJson(QByteArray::fromBase64(part)).object(); };
_header = parse(parts[0]);
_payload = parse(parts[1]);
_signauture = parts[2];
}
QByteArray OCC::JWT::serialize() const
{
if (!isValid()) {
return {};
}
auto encode = [](const QJsonObject &part) {
return QJsonDocument(part).toJson(QJsonDocument::Compact).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
};
return encode(_header) + '.' + encode(_payload) + '.' + _signauture;
}
bool OCC::JWT::isValid() const
{
return !_header.isEmpty() && !_payload.isEmpty() && !_signauture.isEmpty();
}
QJsonObject OCC::JWT::header() const
{
return _header;
}
QJsonObject OCC::JWT::payload() const
{
return _payload;
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
namespace OCC {
class JWT
{
public:
/*
* Basic helper class for JWT
* As always require https, we are not requireed to verify the signature
*/
explicit JWT(const QByteArray &jwt);
QByteArray serialize() const;
bool isValid() const;
QJsonObject header() const;
QJsonObject payload() const;
protected:
QJsonObject _header;
QJsonObject _payload;
QByteArray _signauture;
};
}
+790
View File
@@ -0,0 +1,790 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "creds/oauth.h"
#include "account.h"
#include "common/version.h"
#include "credentialmanager.h"
#include "creds/httpcredentials.h"
#include "networkjobs/checkserverjobfactory.h"
#include "networkjobs/fetchuserinfojobfactory.h"
#include "networkjobs/jsonjob.h"
#include "resources/template.h"
#include "theme.h"
#include <QBuffer>
#include <QDesktopServices>
#include <QFile>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPixmap>
#include <QRandomGenerator>
using namespace std::chrono;
using namespace std::chrono_literals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcOauth, "sync.credentials.oauth", QtInfoMsg)
namespace {
const QString wellKnownPathC = QStringLiteral("/.well-known/openid-configuration");
QString redirectUrlC()
{
return QStringLiteral("http://127.0.0.1");
}
auto defaultOauthPromtValue()
{
static const auto promptValue = [] {
OAuth::PromptValuesSupportedFlags out = OAuth::PromptValuesSupported::none;
// convert the legacy openIdConnectPrompt() to QFlags
for (const auto &x : Theme::instance()->openIdConnectPrompt().split(QLatin1Char(' '))) {
out |= Utility::stringToEnum<OAuth::PromptValuesSupported>(x);
}
return out;
}();
return promptValue;
}
QString renderHttpTemplate(const QString &title, const QString &content)
{
auto loadFile = [](const QString &font) {
QFile f(font);
OC_ASSERT(f.open(QFile::ReadOnly));
return f.readAll().toBase64();
};
return Resources::Template::renderTemplateFromFile(QStringLiteral(":/client/resources/oauth/oauth.html.in"),
{
{"TITLE", title}, //
{"CONTENT", content}, //
{"ICON", loadFile(QStringLiteral(":/client/QSfera/theme/universal/wizard_logo.svg"))}, //
{"BACKGROUND_COLOR", Theme::instance()->wizardHeaderBackgroundColor().name()}, //
{"FONT_COLOR", Theme::instance()->wizardHeaderTitleColor().name()}, //
});
}
auto defaultTimeout()
{
// as the OAuth process can be interactive we don't want 5min of inactivity
return qMin<milliseconds>(30s, OCC::AbstractNetworkJob::httpTimeout);
}
auto defaultTimeoutMs()
{
return static_cast<int>(duration_cast<milliseconds>(defaultTimeout()).count());
}
QString dynamicRegistrationDataC()
{
return QStringLiteral("oauth/dynamicRegistration");
}
QString idTokenC()
{
return QStringLiteral("oauth/id_token");
}
QVariant getRequiredField(const QVariantMap &json, const QString &s, QString *error)
{
const auto out = json.constFind(s);
if (out == json.constEnd()) {
error->append(QStringLiteral("\tError: Missing field %1\n").arg(s));
return {};
}
return *out;
}
void httpReplyAndClose(const QPointer<QTcpSocket> &socket, const QString &code, const QString &title, const QString &body = {}, const QStringList &additionalHeader = {})
{
if (!socket) {
return; // socket can have been deleted if the browser was closed
}
const QByteArray content = renderHttpTemplate(title, body.isEmpty() ? title : body).toUtf8();
QString header = QStringLiteral("HTTP/1.1 %1\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n"
"Content-Length: %2\r\n")
.arg(code, QString::number(content.length()));
if (!additionalHeader.isEmpty()) {
const QString nl = QStringLiteral("\r\n");
header += additionalHeader.join(nl) + nl;
}
const QByteArray msg = header.toUtf8() + QByteArrayLiteral("\r\n") + content;
qCDebug(lcOauth) << u"replying with HTTP response and closing socket:" << msg;
socket->write(msg);
socket->disconnectFromHost();
// We don't want that deleting the server too early prevent queued data to be sent on this socket.
// The socket will be deleted after disconnection because disconnected is connected to deleteLater
socket->setParent(nullptr);
}
class RegisterClientJob : public QObject
{
Q_OBJECT
public:
RegisterClientJob(QNetworkAccessManager *networkAccessManager, QVariantMap &dynamicRegistrationData, const QUrl &registrationEndpoint, QObject *parent)
: QObject(parent)
, _networkAccessManager(networkAccessManager)
, _dynamicRegistrationData(dynamicRegistrationData)
, _registrationEndpoint(registrationEndpoint)
{
connect(this, &RegisterClientJob::errorOccured, this, &RegisterClientJob::deleteLater);
connect(this, &RegisterClientJob::finished, this, &RegisterClientJob::deleteLater);
}
void start()
{
if (!_dynamicRegistrationData.isEmpty()) {
registerClientFinished(_dynamicRegistrationData);
} else {
registerClientOnline();
}
}
Q_SIGNALS:
void finished(const QString &clientId, const QString &clientSecret, const QVariantMap &dynamicRegistrationData);
void errorOccured(const QString &error);
private:
void registerClientOnline()
{
const QJsonObject json(
{{QStringLiteral("client_name"), QStringLiteral("%1 %2").arg(Theme::instance()->appNameGUI(), OCC::Version::versionWithBuildNumber().toString())},
{QStringLiteral("redirect_uris"), QJsonArray{QStringLiteral("http://127.0.0.1")}},
{QStringLiteral("application_type"), QStringLiteral("native")}, //
{QStringLiteral("token_endpoint_auth_method"), QStringLiteral("none")}});
QNetworkRequest req;
req.setUrl(_registrationEndpoint);
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
req.setTransferTimeout(defaultTimeoutMs());
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json"));
auto reply = _networkAccessManager->post(req, QJsonDocument(json).toJson());
connect(reply, &QNetworkReply::finished, this, [reply, this] {
// https://datatracker.ietf.org/doc/html/rfc7591#section-3.2
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 201) {
const auto data = reply->readAll();
QJsonParseError error{};
const auto json = QJsonDocument::fromJson(data, &error);
if (error.error == QJsonParseError::NoError) {
registerClientFinished(json.object().toVariantMap());
} else {
qCWarning(lcOauth) << u"Failed to register the client" << error.errorString() << data;
Q_EMIT errorOccured(error.errorString());
}
} else {
Q_EMIT errorOccured(reply->errorString());
}
});
}
void registerClientFinished(const QVariantMap &data)
{
// extracting these values could be done by the signal receiver, too, but that'd require duplicating the error handling code
// therefore, we extract the values here and pass them separately in the signal
// sure, the data will be redundant, but it's worth it
QString error;
const auto client_id = getRequiredField(data, QStringLiteral("client_id"), &error).toString();
if (!error.isEmpty()) {
Q_EMIT errorOccured(error);
return;
}
Q_EMIT finished(client_id, {}, data);
}
private:
QNetworkAccessManager *_networkAccessManager;
QVariantMap _dynamicRegistrationData;
QUrl _registrationEndpoint;
};
void logCredentialsJobResult(CredentialJob *credentialsJob)
{
qCDebug(lcOauth) << u"credentials job has finished";
if (!credentialsJob->data().isValid()) {
qCInfo(lcOauth) << u"Failed to read client id" << credentialsJob->errorString();
}
}
}
OAuth::OAuth(const QUrl &serverUrl, QNetworkAccessManager *networkAccessManager, const QVariantMap &dynamicRegistrationData, QObject *parent)
: QObject(parent)
, _serverUrl(serverUrl)
, _dynamicRegistrationData(dynamicRegistrationData)
, _networkAccessManager(networkAccessManager)
, _clientId(Theme::instance()->oauthClientId())
, _clientSecret(Theme::instance()->oauthClientSecret())
, _supportedPromtValues(defaultOauthPromtValue())
{
}
OAuth::~OAuth() = default;
void OAuth::setIdToken(IdToken &&idToken)
{
_idToken = std::move(idToken);
}
const IdToken &OAuth::idToken() const
{
return _idToken;
}
QVariantMap OAuth::dynamicRegistrationData() const
{
return _dynamicRegistrationData;
}
void OAuth::startAuthentication()
{
qCDebug(lcOauth) << u"starting authentication";
// Listen on the socket to get a port which will be used in the redirect_uri
for (const auto port : Theme::instance()->oauthPorts()) {
if (_server.listen(QHostAddress::LocalHost, port)) {
break;
}
qCDebug(lcOauth) << u"Creating local server Port:" << port << u"failed. Error:" << _server.errorString();
}
if (!_server.isListening()) {
qCDebug(lcOauth) << u"server is not listening";
Q_EMIT result(Error, {});
return;
}
_pkceCodeVerifier = generateRandomString(24);
OC_ASSERT(_pkceCodeVerifier.size() == 128)
_state = generateRandomString(8);
connect(this, &OAuth::fetchWellKnownFinished, this, [this] {
connect(this, &AccountBasedOAuth::dynamicRegistrationDataReceived, this, &OAuth::authorisationLinkChanged);
updateDynamicRegistration();
});
fetchWellKnown();
QObject::connect(&_server, &QTcpServer::newConnection, this, [this] {
while (QPointer<QTcpSocket> socket = _server.nextPendingConnection()) {
qCDebug(lcOauth) << u"accepted client connection from" << socket->peerAddress();
QObject::connect(socket.data(), &QTcpSocket::disconnected, socket.data(), &QTcpSocket::deleteLater);
QObject::connect(socket.data(), &QIODevice::readyRead, this, [this, socket] {
const QByteArray peek = socket->peek(qMin(socket->bytesAvailable(), 4000LL)); //The code should always be within the first 4K
// wait until we find a \n
if (!peek.contains('\n')) {
return;
}
qCDebug(lcOauth) << u"Server provided:" << peek;
const auto getPrefix = QByteArrayLiteral("GET /?");
if (!peek.startsWith(getPrefix)) {
httpReplyAndClose(socket, QStringLiteral("404 Not Found"), QStringLiteral("404 Not Found"));
return;
}
const auto endOfUrl = peek.indexOf(' ', getPrefix.length());
const QUrlQuery args(QUrl::fromPercentEncoding(peek.mid(getPrefix.length(), endOfUrl - getPrefix.length())));
if (args.queryItemValue(QStringLiteral("state")).toUtf8() != _state) {
httpReplyAndClose(socket, QStringLiteral("400 Bad Request"), QStringLiteral("400 Bad Request"));
return;
}
// server port cannot be queried any more after server has been closed, which we want to do as early as possible in the processing chain
// therefore we have to store it beforehand
const auto serverPort = _server.serverPort();
// we only allow one response
qCDebug(lcOauth) << u"Received the first valid response, closing server socket";
_server.close();
auto reply = postTokenRequest({
{QStringLiteral("grant_type"), QStringLiteral("authorization_code")},
{QStringLiteral("code"), args.queryItemValue(QStringLiteral("code"))},
{QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(redirectUrlC(), QString::number(serverPort))},
{QStringLiteral("code_verifier"), QString::fromUtf8(_pkceCodeVerifier)},
});
connect(reply, &QNetworkReply::finished, this, [reply, socket, this] {
const auto jsonData = reply->readAll();
QJsonParseError jsonParseError;
const auto data = QJsonDocument::fromJson(jsonData, &jsonParseError).object().toVariantMap();
QString fieldsError;
const QString accessToken = getRequiredField(data, QStringLiteral("access_token"), &fieldsError).toString();
const QString refreshToken = getRequiredField(data, QStringLiteral("refresh_token"), &fieldsError).toString();
const QString tokenType = getRequiredField(data, QStringLiteral("token_type"), &fieldsError).toString().toLower();
const QUrl messageUrl = QUrl::fromEncoded(data[QStringLiteral("message_url")].toByteArray());
auto idToken = IdToken(JWT(getRequiredField(data, QStringLiteral("id_token"), &fieldsError).toByteArray()).payload());
auto reportError = [socket, this](const QString &errorReason) {
qCWarning(lcOauth) << u"Error when getting the accessToken" << errorReason;
httpReplyAndClose(
socket, QStringLiteral("500 Internal Server Error"), tr("Login Error"), tr("<h1>Login Error</h1><p>%1</p>").arg(errorReason));
Q_EMIT result(Error);
};
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|| !fieldsError.isEmpty()
|| tokenType != QLatin1String("bearer")) {
// do we have error message suitable for users?
QString errorReason = data[QStringLiteral("error_description")].toString();
if (errorReason.isEmpty()) {
// fall back to technical error
errorReason = data[QStringLiteral("error")].toString();
}
if (!errorReason.isEmpty()) {
reportError(tr("Error returned from the server: <em>%1</em>").arg(errorReason.toHtmlEscaped()));
} else if (reply->error() != QNetworkReply::NoError) {
reportError(tr("There was an error accessing the 'token' endpoint: <br><em>%1</em>").arg(reply->errorString().toHtmlEscaped()));
} else if (jsonParseError.error != QJsonParseError::NoError) {
reportError(tr("Could not parse the JSON returned from the server: <br><em>%1</em>").arg(jsonParseError.errorString()));
} else if (tokenType != QStringLiteral("bearer")) {
reportError(tr("Unsupported token type: %1").arg(tokenType));
} else if (!fieldsError.isEmpty()) {
reportError(tr("The reply from the server did not contain all expected fields\n:%1").arg(fieldsError));
} else {
reportError(tr("Unknown Error"));
}
} else if (!idToken.aud().contains(_clientId)) {
reportError(tr("The audience of the id_token did not contain \"%1\"").arg(_clientId));
} else if (_idToken.isValid() && _idToken.sub() != idToken.sub()) {
// Connected with the wrong user
qCWarning(lcOauth) << u"We expected the user" << _idToken.toJson() << u"but the server answered with user" << idToken.toJson();
const QString expectedName = !_idToken.preferred_username().isEmpty() ? _idToken.preferred_username() : _idToken.name();
const QString actualName = !idToken.preferred_username().isEmpty() ? idToken.preferred_username() : idToken.name();
QString message;
if (!expectedName.isEmpty() && !actualName.isEmpty() && expectedName != actualName) {
message = tr("<h1>Incorrect user</h1>"
"<p>You logged-in as user <em>%1</em>, but must login with user <em>%2</em>.<br>"
"Please return to the %3 and restart the authentication.</p>")
.arg(actualName, expectedName, Theme::instance()->appNameGUI());
} else {
message = tr("<h1>Incorrect user</h1>"
"<p>You logged-in as a different user than is associated with this account.<br>"
"Please return to the %1 and restart the authentication.</p>")
.arg(Theme::instance()->appNameGUI());
}
httpReplyAndClose(socket, QStringLiteral("403 Forbidden"), tr("Incorrect user"), message);
Q_EMIT result(Error);
} else {
setIdToken(std::move(idToken));
finalize(socket, accessToken, refreshToken, messageUrl);
}
});
});
}
});
}
void OAuth::finalize(const QPointer<QTcpSocket> &socket, const QString &accessToken, const QString &refreshToken, const QUrl &messageUrl)
{
const QString loginSuccessfulHtml = tr("<h1>Login successful</h1><p>You can close this window.</p>");
const QString loginSuccessfulTitle = tr("Login successful");
if (messageUrl.isValid()) {
httpReplyAndClose(socket, QStringLiteral("303 See Other"), loginSuccessfulTitle, loginSuccessfulHtml,
{QStringLiteral("Location: %1").arg(QString::fromUtf8(messageUrl.toEncoded()))});
} else {
httpReplyAndClose(socket, QStringLiteral("200 OK"), loginSuccessfulTitle, loginSuccessfulHtml);
}
Q_EMIT result(LoggedIn, accessToken, refreshToken);
}
QNetworkReply *OAuth::postTokenRequest(QUrlQuery &&queryItems)
{
QNetworkRequest req;
req.setTransferTimeout(defaultTimeoutMs());
switch (_endpointAuthMethod) {
case TokenEndpointAuthMethods::client_secret_basic:
req.setRawHeader("Authorization", "Basic " + QStringLiteral("%1:%2").arg(_clientId, _clientSecret).toUtf8().toBase64());
break;
case TokenEndpointAuthMethods::client_secret_post:
queryItems.addQueryItem(QStringLiteral("client_id"), _clientId);
queryItems.addQueryItem(QStringLiteral("client_secret"), _clientSecret);
break;
case TokenEndpointAuthMethods::none:
queryItems.addQueryItem(QStringLiteral("client_id"), _clientId);
break;
}
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8"));
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes())));
req.setUrl(_tokenEndpoint);
return _networkAccessManager->post(req, queryItems.toString(QUrl::FullyEncoded).toUtf8());
}
QByteArray OAuth::generateRandomString(size_t size) const
{
// TODO: do we need a varaible size?
std::vector<quint32> buffer(size, 0);
QRandomGenerator::global()->fillRange(buffer.data(), static_cast<qsizetype>(size));
return QByteArray(reinterpret_cast<char *>(buffer.data()), static_cast<int>(size * sizeof(quint32))).toBase64(QByteArray::Base64UrlEncoding);
}
QUrl OAuth::authorisationLink() const
{
Q_ASSERT(_server.isListening());
Q_ASSERT(_wellKnownFinished);
Q_ASSERT(_authEndpoint.isValid());
const QByteArray code_challenge = QCryptographicHash::hash(_pkceCodeVerifier, QCryptographicHash::Sha256)
.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QUrlQuery query{
{QStringLiteral("response_type"), QStringLiteral("code")},
{QStringLiteral("client_id"), _clientId},
{QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(redirectUrlC(), QString::number(_server.serverPort()))},
{QStringLiteral("code_challenge"), QString::fromLatin1(code_challenge)},
{QStringLiteral("code_challenge_method"), QStringLiteral("S256")},
{QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))},
{QStringLiteral("prompt"), QString::fromUtf8(QUrl::toPercentEncoding(toString(_supportedPromtValues)))},
{QStringLiteral("state"), QString::fromUtf8(_state)},
};
if (!_idToken.preferred_username().isEmpty()) {
query.addQueryItem(QStringLiteral("login_hint"), QString::fromUtf8(QUrl::toPercentEncoding(_idToken.preferred_username())));
}
return Utility::concatUrlPath(_authEndpoint, {}, query);
}
QString OAuth::clientId() const
{
return _clientId;
}
QString OAuth::clientSecret() const
{
return _clientSecret;
}
void OAuth::persist(const OCC::AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken)
{
if (!dynamicRegistrationData.isEmpty()) {
accountPtr->credentialManager()->set(dynamicRegistrationDataC(), dynamicRegistrationData);
} else {
accountPtr->credentialManager()->clear(dynamicRegistrationDataC());
}
if (idToken.isValid()) {
accountPtr->credentialManager()->set(idTokenC(), idToken.toJson());
} else {
accountPtr->credentialManager()->clear(idTokenC());
}
}
void OAuth::updateDynamicRegistration()
{
// this slightly complicated construct allows us to log case-specific messages
if (!Theme::instance()->oidcEnableDynamicRegistration()) {
qCDebug(lcOauth) << u"dynamic registration disabled by theme";
} else if (!_registrationEndpoint.isValid()) {
qCDebug(lcOauth) << u"registration endpoint not provided or empty:" << _registrationEndpoint.toString()
<< u"we asume dynamic registration is not supported by the server";
} else {
auto registerJob = new RegisterClientJob(_networkAccessManager, _dynamicRegistrationData, _registrationEndpoint, this);
connect(registerJob, &RegisterClientJob::finished, this,
[this](const QString &clientId, const QString &clientSecret, const QVariantMap &dynamicRegistrationData) {
qCDebug(lcOauth) << u"client registration finished successfully";
_clientId = clientId;
_clientSecret = clientSecret;
_dynamicRegistrationData = dynamicRegistrationData;
Q_EMIT dynamicRegistrationDataReceived();
});
connect(registerJob, &RegisterClientJob::errorOccured, this, [this](const QString &error) {
qCWarning(lcOauth) << u"Failed to dynamically register the client, try the default client id" << error;
Q_EMIT dynamicRegistrationDataReceived();
});
registerJob->start();
return;
}
Q_EMIT dynamicRegistrationDataReceived();
}
void OAuth::fetchWellKnown()
{
const QPair<QString, QString> urls = Theme::instance()->oauthOverrideAuthUrl();
if (!urls.first.isNull()) {
OC_ASSERT(!urls.second.isNull());
_authEndpoint = QUrl(urls.first);
_tokenEndpoint = QUrl(urls.second);
qCDebug(lcOauth) << u"override URL set, using auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint;
_wellKnownFinished = true;
Q_EMIT fetchWellKnownFinished();
} else {
qCDebug(lcOauth) << u"fetching" << wellKnownPathC;
QNetworkRequest req;
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
req.setUrl(Utility::concatUrlPath(_serverUrl, wellKnownPathC));
req.setTransferTimeout(defaultTimeoutMs());
auto reply = _networkAccessManager->get(req);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
_wellKnownFinished = true;
if (reply->error() != QNetworkReply::NoError) {
qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error();
if (_isRefreshingToken) {
Q_EMIT refreshError(reply->error(), reply->errorString());
} else {
Q_EMIT result(Error);
}
return;
}
QJsonParseError err = {};
QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object();
if (err.error == QJsonParseError::NoError) {
_authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8());
_tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8());
_registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8());
if (_clientSecret.isEmpty()) {
_endpointAuthMethod = TokenEndpointAuthMethods::none;
} else {
const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray();
if (authMethods.contains(QStringLiteral("none"))) {
_endpointAuthMethod = TokenEndpointAuthMethods::none;
} else if (authMethods.contains(QStringLiteral("client_secret_post"))) {
_endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post;
} else if (authMethods.contains(QStringLiteral("client_secret_basic"))) {
_endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic;
} else {
OC_ASSERT_X(
false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods))));
}
}
const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray();
if (!promtValuesSupported.isEmpty()) {
_supportedPromtValues = PromptValuesSupported::none;
for (const auto &x : promtValuesSupported) {
const auto flag = Utility::stringToEnum<PromptValuesSupported>(x.toString());
// only use flags present in Theme::instance()->openIdConnectPrompt()
if (flag & defaultOauthPromtValue())
_supportedPromtValues |= flag;
}
}
qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint
<< u"and registration endpoint" << _registrationEndpoint;
} else if (err.error == QJsonParseError::IllegalValue) {
qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC";
} else {
qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error;
Q_EMIT result(Error);
}
Q_EMIT fetchWellKnownFinished();
});
}
}
/**
* Checks whether a URL returned by the server is valid.
* @param url URL to validate
* @return true if validation is successful, false otherwise
*/
bool isUrlValid(const QUrl &url)
{
qCDebug(lcOauth()) << u"Checking URL for validity:" << url;
// we have hardcoded the oauthOverrideAuth
const auto overrideUrl = Theme::instance()->oauthOverrideAuthUrl();
if (!overrideUrl.first.isEmpty()) {
return QUrl::fromUserInput(overrideUrl.first).matches(url, QUrl::RemoveQuery);
}
// the following allowlist contains URL schemes accepted as valid
// OAuth 2.0 URLs must be HTTPS to be in compliance with the specification
// for unit tests, we also permit the nonexisting oauthtest scheme
const QStringList allowedSchemes({ QStringLiteral("https"), QStringLiteral("oauthtest") });
return allowedSchemes.contains(url.scheme());
}
void OAuth::openBrowser()
{
Q_ASSERT(!authorisationLink().isEmpty());
qCDebug(lcOauth) << u"opening browser";
if (!isUrlValid(authorisationLink())) {
qCWarning(lcOauth) << u"URL validation failed";
Q_EMIT result(ErrorInsecureUrl, QString());
return;
}
if (!QDesktopServices::openUrl(authorisationLink())) {
qCWarning(lcOauth) << u"QDesktopServices::openUrl Failed";
Q_EMIT result(Error, {});
}
}
AccountBasedOAuth::AccountBasedOAuth(AccountPtr account, QObject *parent)
: OAuth(account->url(), account->accessManager(), {}, parent)
, _account(account)
{
connect(this, &AccountBasedOAuth::result, this, [account, this](OAuth::Result result, const QString &, const QString &) {
if (result == OAuth::LoggedIn) {
persist(account, dynamicRegistrationData(), idToken());
}
});
}
void AccountBasedOAuth::startAuthentication()
{
qCDebug(lcOauth) << u"fetching dynamic registration data";
connect(this, &AccountBasedOAuth::restored, this, [this] {
// explicitly call base implementation, this can't be done directly in the connect
OAuth::startAuthentication();
});
restore();
}
void AccountBasedOAuth::fetchWellKnown()
{
qCDebug(lcOauth) << u"starting CheckServerJob before fetching" << wellKnownPathC;
auto *checkServerJob = CheckServerJobFactory::createFromAccount(_account, true, this).startJob(_serverUrl, this);
connect(checkServerJob, &CoreJob::finished, this, [checkServerJob, this]() {
if (checkServerJob->success()) {
qCDebug(lcOauth) << u"CheckServerJob succeeded, fetching" << wellKnownPathC;
OAuth::fetchWellKnown();
} else {
qCDebug(lcOauth) << u"CheckServerJob failed, error:" << checkServerJob->errorMessage();
if (_isRefreshingToken) {
Q_EMIT refreshError(checkServerJob->reply()->error(), checkServerJob->errorMessage());
} else {
Q_EMIT result(Error);
}
}
});
}
void AccountBasedOAuth::restore()
{
if (_restored) {
Q_EMIT restored(QPrivateSignal());
return;
}
_restored = true;
auto idTokenJob = _account->credentialManager()->get(idTokenC());
connect(idTokenJob, &CredentialJob::finished, this, [idTokenJob, this] {
if (idTokenJob->error() == QKeychain::EntryNotFound) {
qCWarning(lcOauth) << u"idToken token token credential not found";
} else if (idTokenJob->error() != QKeychain::NoError) {
Q_EMIT result(Error);
return;
} else {
setIdToken(IdToken(idTokenJob->data().value<QJsonObject>()));
}
auto credentialsJob = _account->credentialManager()->get(dynamicRegistrationDataC());
connect(credentialsJob, &CredentialJob::finished, this, [this, credentialsJob] {
qCDebug(lcOauth) << u"fetched dynamic registration data" << credentialsJob->errorString();
logCredentialsJobResult(credentialsJob);
_dynamicRegistrationData = credentialsJob->data().value<QVariantMap>();
Q_EMIT restored(QPrivateSignal());
});
});
}
void AccountBasedOAuth::refreshAuthentication(const QString &refreshToken)
{
if (!OC_ENSURE(!_isRefreshingToken)) {
qCDebug(lcOauth) << u"already refreshing token, aborting";
return;
}
_isRefreshingToken = true;
connect(this, &AccountBasedOAuth::restored, this, [refreshToken, this] {
connect(this, &OAuth::fetchWellKnownFinished, this, [refreshToken, this] {
connect(this, &AccountBasedOAuth::dynamicRegistrationDataReceived, this, [refreshToken, this] {
auto reply =
postTokenRequest({{QStringLiteral("grant_type"), QStringLiteral("refresh_token")}, {QStringLiteral("refresh_token"), refreshToken}});
connect(reply, &QNetworkReply::finished, this, [reply, refreshToken, this]() {
const auto jsonData = reply->readAll();
QJsonParseError jsonParseError;
const auto data = QJsonDocument::fromJson(jsonData, &jsonParseError).object().toVariantMap();
QString accessToken;
QString newRefreshToken = refreshToken;
// https://developer.okta.com/docs/reference/api/oidc/#response-properties-2
const QString errorString = data.value(QStringLiteral("error")).toString();
if (!errorString.isEmpty()) {
if (errorString == QLatin1String("invalid_grant") || errorString == QLatin1String("invalid_request")) {
newRefreshToken.clear();
} else {
qCWarning(lcOauth) << u"Error while refreshing the token:" << errorString
<< data.value(QStringLiteral("error_description")).toString();
Q_EMIT refreshError(QNetworkReply::NoError, data.value(QStringLiteral("error_description")).toString());
return;
}
} else if (reply->error() != QNetworkReply::NoError) {
qCWarning(lcOauth) << u"Error while refreshing the token:" << reply->error() << u":" << reply->errorString()
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
Q_EMIT refreshError(reply->error(), reply->errorString());
return;
} else {
if (jsonParseError.error != QJsonParseError::NoError || data.isEmpty()) {
// Invalid or empty JSON: Network error maybe?
qCWarning(lcOauth) << u"Error while refreshing the token:" << jsonParseError.errorString();
} else {
QString error;
accessToken = getRequiredField(data, QStringLiteral("access_token"), &error).toString();
if (!error.isEmpty()) {
qCWarning(lcOauth) << u"The reply from the server did not contain all expected fields:" << error;
}
const auto refresh_token = data.find(QStringLiteral("refresh_token"));
if (refresh_token != data.constEnd()) {
newRefreshToken = refresh_token.value().toString();
}
}
}
Q_EMIT refreshFinished(accessToken, newRefreshToken);
});
});
updateDynamicRegistration();
});
fetchWellKnown();
});
restore();
}
QString OCC::toString(OAuth::PromptValuesSupportedFlags s)
{
QStringList out;
for (auto k : {OAuth::PromptValuesSupported::consent, OAuth::PromptValuesSupported::select_account})
if (s & k) {
out += Utility::enumToString(k);
}
return out.join(QLatin1Char(' '));
}
#include "oauth.moc"
+174
View File
@@ -0,0 +1,174 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "accountfwd.h"
#include "libsync/creds/idtoken.h"
#include "libsync/creds/jwt.h"
#include "qsferasynclib.h"
#include <QNetworkReply>
#include <QPointer>
#include <QTcpServer>
#include <QUrl>
namespace OCC {
class JsonJob;
/**
* Job that do the authorization grant and fetch the access token
*
* Normal workflow:
*
* --> start()
* |
* +----> fetchWellKnown() query the ".well-known/openid-configuration" endpoint
* |
* +----> openBrowser() open the browser after fetchWellKnown finished to the specified page
* | (or the default 'oauth2/authorize' if fetchWellKnown does not exist)
* | Then the browser will redirect to http://127.0.0.1:xxx
* |
* +----> _server starts listening on a TCP port waiting for an HTTP request with a 'code'
* |
* v
* request the access_token and the refresh_token via 'apps/oauth2/api/v1/token'
* |
* +-> Request the user_id is not present
* | |
* v v
* finalize(...): Q_EMIT result(...)
*
*/
class QSFERA_SYNC_EXPORT OAuth : public QObject
{
Q_OBJECT
public:
enum Result { LoggedIn, Error, ErrorInsecureUrl };
Q_ENUM(Result)
enum class TokenEndpointAuthMethods : char { none, client_secret_basic, client_secret_post };
Q_ENUM(TokenEndpointAuthMethods)
enum class PromptValuesSupported : char { none = 0, consent = 1 << 0, select_account = 1 << 1, login = 1 << 2 };
Q_ENUM(PromptValuesSupported)
Q_DECLARE_FLAGS(PromptValuesSupportedFlags, PromptValuesSupported)
OAuth(const QUrl &serverUrl, QNetworkAccessManager *networkAccessManager, const QVariantMap &dynamicRegistrationData, QObject *parent);
~OAuth() override;
void setIdToken(IdToken &&idToken);
const IdToken &idToken() const;
QVariantMap dynamicRegistrationData() const;
virtual void startAuthentication();
void openBrowser();
QUrl authorisationLink() const;
// TODO: private api for tests
QString clientId() const;
QString clientSecret() const;
static void persist(const AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken);
Q_SIGNALS:
/**
* The state has changed.
* when logged in, token has the value of the token.
*/
void result(OAuth::Result result, const QString &token = QString(), const QString &refreshToken = QString());
/**
* emitted when the call to the well-known endpoint is finished
*/
void authorisationLinkChanged();
void fetchWellKnownFinished();
void dynamicRegistrationDataReceived();
void refreshError(QNetworkReply::NetworkError error, const QString &errorString);
protected:
void updateDynamicRegistration();
QUrl _serverUrl;
QVariantMap _dynamicRegistrationData;
QNetworkAccessManager *_networkAccessManager;
bool _isRefreshingToken = false;
QString _clientId;
QString _clientSecret;
QUrl _registrationEndpoint;
virtual void fetchWellKnown();
QNetworkReply *postTokenRequest(QUrlQuery &&queryItems);
private:
void finalize(const QPointer<QTcpSocket> &socket, const QString &accessToken, const QString &refreshToken, const QUrl &messageUrl);
QByteArray generateRandomString(size_t size) const;
QTcpServer _server;
bool _wellKnownFinished = false;
QUrl _authEndpoint;
QUrl _tokenEndpoint;
QByteArray _pkceCodeVerifier;
QByteArray _state;
IdToken _idToken;
TokenEndpointAuthMethods _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic;
PromptValuesSupportedFlags _supportedPromtValues = {PromptValuesSupported::consent, PromptValuesSupported::select_account};
};
/**
* This variant of OAuth uses an account's network access manager etc.
* Instead of relying on the user to provide a working server URL, a CheckServerJob is run upon start(), which also stores the fetched cookies in the account's state.
* Furthermore, it takes care of storing and loading the dynamic registration data in the account's credentials manager.
*/
class QSFERA_SYNC_EXPORT AccountBasedOAuth : public OAuth
{
Q_OBJECT
public:
explicit AccountBasedOAuth(AccountPtr account, QObject *parent = nullptr);
void startAuthentication() override;
void refreshAuthentication(const QString &refreshToken);
Q_SIGNALS:
void refreshFinished(const QString &accessToken, const QString &refreshToken);
void restored(QPrivateSignal);
protected:
void fetchWellKnown() override;
void restore();
private:
AccountPtr _account;
bool _restored = false;
};
QString QSFERA_SYNC_EXPORT toString(OAuth::PromptValuesSupportedFlags s);
Q_DECLARE_OPERATORS_FOR_FLAGS(OAuth::PromptValuesSupportedFlags)
} // namespce OCC
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "webfinger.h"
#include "common/asserts.h"
#include "httpcredentials.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLoggingCategory>
#include <QNetworkAccessManager>
Q_LOGGING_CATEGORY(lcWebFinger, "sync.credentials.webfinger", QtInfoMsg)
using namespace OCC;
WebFinger::WebFinger(QNetworkAccessManager *nam, QObject *parent)
: QObject(parent)
, _nam(nam)
{
}
void WebFinger::start(const QUrl &url, const QString &resourceId)
{
// GET /.well-known/webfinger?rel=http://webfinger.opencloud/rel/server-instance&resource=acct:test@opencloud.eu HTTP/1.1
if (OC_ENSURE(url.scheme() == QLatin1String("https"))) {
QUrlQuery query;
query.setQueryItems({ { QStringLiteral("resource"), QString::fromUtf8(QUrl::toPercentEncoding(resourceId)) },
{ QStringLiteral("rel"), relId() } });
QNetworkRequest req;
req.setUrl(Utility::concatUrlPath(url, QStringLiteral(".well-known/webfinger"), query));
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
auto *reply = _nam->get(req);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
const auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status == 200) {
const auto data = reply->readAll();
auto obj = QJsonDocument::fromJson(data, &_error).object();
if (_error.error == QJsonParseError::NoError) {
const auto links = obj.value(QLatin1String("links")).toArray();
if (!links.empty()) {
_href = QUrl::fromEncoded(links.first().toObject().value(QLatin1String("href")).toString().toUtf8());
qCInfo(lcWebFinger) << u"Webfinger provided" << _href << u"as server";
} else {
qCWarning(lcWebFinger) << reply->url() << u"Did not reply a valid link";
}
} else {
qCWarning(lcWebFinger) << u"Failed with" << _error.errorString();
}
} else {
qCWarning(lcWebFinger) << u"Failed with status code" << status;
_error.error = QJsonParseError::MissingObject;
}
Q_EMIT finished();
});
} else {
Q_EMIT finished();
}
}
const QJsonParseError &WebFinger::error() const
{
return _error;
}
const QUrl &WebFinger::href() const
{
return _href;
}
QString WebFinger::relId()
{
return QStringLiteral("http://webfinger.opencloud/rel/server-instance");
}
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "qsferasynclib.h"
#include "account.h"
#include <QJsonParseError>
namespace OCC {
class QSFERA_SYNC_EXPORT WebFinger : public QObject
{
Q_OBJECT
public:
WebFinger(QNetworkAccessManager *nam, QObject *parent = nullptr);
void start(const QUrl &url, const QString &resourceId);
const QJsonParseError &error() const;
const QUrl &href() const;
/***
* ID used to describe our rel attribute
* Defined on the server in services/webfinger/pkg/relations/owncloud_instance.go as OpenCloudInstanceRel
* The server uses "http://webfinger.opencloud/rel/server-instance" (without .eu)
*
* According to RFC 7033 (WebFinger), the "rel" is either a URI or a registered relation type
* as specified in RFC 5988 (Web Linking). For custom relation types like this one,
* a URI format is used as described in RFC 7033 section 10.3:
* https://datatracker.ietf.org/doc/html/rfc7033#section-10.3
*/
static QString relId();
Q_SIGNALS:
void finished();
private:
QNetworkAccessManager *_nam;
QJsonParseError _error;
QUrl _href;
};
}
+31
View File
@@ -0,0 +1,31 @@
/*
* libcsync -- a library to sync a directory with another
*
* Copyright (c) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "csync.h"
#include <QMetaEnum>
QDebug operator<<(QDebug debug, const SyncInstructions &enumValue)
{
static const QMetaEnum me = QMetaEnum::fromType<SyncInstruction>();
QDebugStateSaver saver(debug);
debug.nospace().noquote() << me.enumName() << u"(" << me.valueToKeys(enumValue) << u")";
return debug;
}
+121
View File
@@ -0,0 +1,121 @@
/*
* libcsync -- a library to sync a directory with another
*
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
* Copyright (c) 2012-2013 by Klaas Freitag <freitag@owncloud.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file csync.h
*
* @brief Application developer interface for csync.
*
* @defgroup csyncPublicAPI csync public API
*
* @{
*/
#ifndef _CSYNC_H
#define _CSYNC_H
#include "libsync/qsferasynclib.h"
#include <QObject>
namespace OCC {
class SyncJournalFileRecord;
}
namespace CSyncEnums {
QSFERA_SYNC_EXPORT Q_NAMESPACE
/**
* Instruction enum. In the file traversal structure, it describes
* the csync state of a file.
*/
// clang-format off
enum SyncInstruction : uint16_t {
CSYNC_INSTRUCTION_NONE = 1 << 1, /* Nothing to do (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_REMOVE = 1 << 2, /* The file need to be removed (RECONCILE) */
CSYNC_INSTRUCTION_RENAME = 1 << 3, /* The file need to be renamed (RECONCILE) */
CSYNC_INSTRUCTION_NEW = 1 << 4, /* The file is new compared to the db (UPDATE) */
CSYNC_INSTRUCTION_CONFLICT = 1 << 5, /* The file need to be downloaded because it is a conflict (RECONCILE) */
CSYNC_INSTRUCTION_IGNORE = 1 << 6, /* The file is ignored (UPDATE|RECONCILE)
* Identical to CSYNC_INSTRUCTION_NONE but logged to the user.
*/
CSYNC_INSTRUCTION_SYNC = 1 << 7, /* The file need to be pushed to the other remote (RECONCILE) */
CSYNC_INSTRUCTION_ERROR = 1 << 8,
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
Used when the type of something changes from directory to file
or back. */
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
but without any propagation (UPDATE|RECONCILE) */
};
// clang-format on
Q_FLAG_NS(SyncInstruction)
Q_DECLARE_FLAGS(SyncInstructions, SyncInstruction)
Q_DECLARE_OPERATORS_FOR_FLAGS(SyncInstructions)
// Also, this value is stored in the database, so beware of value changes.
enum ItemType : uint8_t {
ItemTypeFile = 0,
ItemTypeSymLink = 1,
ItemTypeDirectory = 2,
ItemTypeUnsupported = 3,
/** The file is a dehydrated placeholder, meaning data isn't available locally */
ItemTypeVirtualFile = 4,
/** A ItemTypeVirtualFile that wants to be hydrated.
*
* Actions may put this in the db as a request to a future sync, such as
* implicit hydration (when the user wants to access file data) when using
* suffix vfs. For pin-state driven hydrations changing the database is
* not necessary.
*
* For some vfs plugins the placeholder files on disk may be marked for
* (de-)hydration (like with a file attribute) and then the local discovery
* will return this item type.
*
* The discovery will also use this item type to mark entries for hydration
* if an item's pin state mandates it, such as when encountering a AlwaysLocal
* file that is dehydrated.
*/
ItemTypeVirtualFileDownload = 5,
/** A ItemTypeFile that wants to be dehydrated.
*
* Similar to ItemTypeVirtualFileDownload, but there's currently no situation
* where it's stored in the database since there is no action that triggers a
* file dehydration without changing the pin state.
*/
ItemTypeVirtualFileDehydration = 6,
};
Q_ENUM_NS(ItemType)
}
using namespace CSyncEnums;
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const SyncInstructions &job);
/**
* }@
*/
#endif /* _CSYNC_H */
/* vim: set ft=c.doxygen ts=8 sw=2 et cindent: */
+782
View File
@@ -0,0 +1,782 @@
/*
* libcsync -- a library to sync a directory with another
*
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "libsync/csync_exclude.h"
#include "common/filesystembase.h"
#include "common/utility.h"
#include "common/version.h"
#include <QFile>
#include <QFileInfo>
#include <QString>
using namespace Qt::Literals::StringLiterals;
namespace {
// See http://support.microsoft.com/kb/74496 and
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
// Additionally, we ignore '$Recycle.Bin', see https://github.com/owncloud/client/issues/2955
const QLatin1String win_device_names[] = {QLatin1String("CON"), QLatin1String("PRN"), QLatin1String("AUX"), QLatin1String("NUL"), QLatin1String("COM1"),
QLatin1String("COM2"), QLatin1String("COM3"), QLatin1String("COM4"), QLatin1String("COM5"), QLatin1String("COM6"), QLatin1String("COM7"),
QLatin1String("COM8"), QLatin1String("COM9"), QLatin1String("LPT1"), QLatin1String("LPT2"), QLatin1String("LPT3"), QLatin1String("LPT4"),
QLatin1String("LPT5"), QLatin1String("LPT6"), QLatin1String("LPT7"), QLatin1String("LPT8"), QLatin1String("LPT9"), QLatin1String("CLOCK$")};
const QLatin1String win_system_files[] = {QLatin1String("$Recycle.Bin"), QLatin1String("System Volume Information")};
}
/** Expands C-like escape sequences (in place)
*/
QSFERA_SYNC_EXPORT void csync_exclude_expand_escapes(QByteArray &input)
{
qsizetype o = 0;
char *line = input.data();
auto len = input.size();
for (int i = 0; i < len; ++i) {
if (line[i] == '\\') {
// at worst input[i+1] is \0
switch (line[i + 1]) {
case '\'':
line[o++] = '\'';
break;
case '"':
line[o++] = '"';
break;
case '?':
line[o++] = '?';
break;
case '#':
line[o++] = '#';
break;
case 'a':
line[o++] = '\a';
break;
case 'b':
line[o++] = '\b';
break;
case 'f':
line[o++] = '\f';
break;
case 'n':
line[o++] = '\n';
break;
case 'r':
line[o++] = '\r';
break;
case 't':
line[o++] = '\t';
break;
case 'v':
line[o++] = '\v';
break;
default:
// '\*' '\?' '\[' '\\' will be processed during regex translation
// '\\' is intentionally not expanded here (to avoid '\\*' and '\*'
// ending up meaning the same thing)
line[o++] = line[i];
line[o++] = line[i + 1];
break;
}
++i;
} else {
line[o++] = line[i];
}
}
input.resize(o);
}
/**
* @brief Checks if filename is considered reserved by Windows
* @param filename filename
* @return true if file is reserved, false otherwise
*/
QSFERA_SYNC_EXPORT bool csync_is_windows_reserved_word(QStringView filename)
{
size_t len_filename = filename.size();
// Drive letters
if (len_filename == 2 && filename.at(1) == QLatin1Char(':')) {
if (filename.at(0) >= QLatin1Char('a') && filename.at(0) <= QLatin1Char('z')) {
return true;
}
if (filename.at(0) >= QLatin1Char('A') && filename.at(0) <= QLatin1Char('Z')) {
return true;
}
}
// win_reserved_words is ordered so we can stop once we encounter a longer word
Q_ASSERT(std::is_sorted(
std::begin(win_device_names), std::end(win_device_names), [](const QLatin1String &a, const QLatin1String &b) { return a.size() < b.size(); }));
for (const auto &word : win_device_names) {
if (static_cast<size_t>(word.size()) > len_filename) {
break;
}
// until windows 11, not only the device names where illegal file names
// also COM9.png was illegal
if ((static_cast<size_t>(word.size()) == len_filename || filename.at(word.size()) == QLatin1Char('.'))
&& filename.startsWith(word, Qt::CaseInsensitive)) {
return true;
}
}
// win_reserved_words is ordered so we can stop once we encounter a longer word
Q_ASSERT(std::is_sorted(
std::begin(win_system_files), std::end(win_system_files), [](const QLatin1String &a, const QLatin1String &b) { return a.size() < b.size(); }));
for (const auto &word : win_system_files) {
if (static_cast<size_t>(word.size()) > len_filename) {
break;
}
if (static_cast<size_t>(word.size()) == len_filename && filename.compare(word, Qt::CaseInsensitive) == 0) {
return true;
}
}
return false;
}
static CSYNC_EXCLUDE_TYPE _csync_excluded_common(QStringView path, bool excludeConflictFiles)
{
/* split up the path */
QStringView bname(path);
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
if (lastSlash >= 0) {
bname = path.mid(lastSlash + 1);
}
qsizetype blen = bname.size();
// 9 = strlen(".sync_.db")
if (blen >= 9 && bname.at(0) == QLatin1Char('.')) {
if (bname.contains(QLatin1String(".db"))) {
if (bname.startsWith(QLatin1String("._sync_"), Qt::CaseInsensitive) // "._sync_*.db*"
|| bname.startsWith(QLatin1String(".sync_"), Qt::CaseInsensitive) // ".sync_*.db*"
|| bname.startsWith(QLatin1String(".csync_journal.db"), Qt::CaseInsensitive)) { // ".csync_journal.db*"
return CSYNC_FILE_SILENTLY_EXCLUDED;
}
}
if (bname.startsWith(QLatin1String(".QSferaSync.log"), Qt::CaseInsensitive)) { // ".QSferaSync.log*"
return CSYNC_FILE_SILENTLY_EXCLUDED;
}
}
// check the strlen and ignore the file if its name is longer than 254 chars.
// whenever changing this also check createDownloadTmpFileName
if (blen > 254) {
return CSYNC_FILE_EXCLUDE_LONG_FILENAME;
}
#ifdef _WIN32
// Windows cannot sync files ending in spaces (#2176). It also cannot
// distinguish files ending in '.' from files without an ending,
// as '.' is a separator that is not stored internally, so let's
// not allow to sync those to avoid file loss/ambiguities (#416)
if (blen > 1) {
if (bname.at(blen - 1) == QLatin1Char(' ')) {
return CSYNC_FILE_EXCLUDE_TRAILING_SPACE;
} else if (bname.at(blen - 1) == QLatin1Char('.')) {
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
}
}
if (csync_is_windows_reserved_word(bname)) {
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
}
// Filter out characters not allowed in a filename on windows
// see https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
for (auto c : path) {
if (c.unicode() < 32) {
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
}
if (std::find_if(OCC::FileSystem::IllegalFilenameCharsWindows.begin(), OCC::FileSystem::IllegalFilenameCharsWindows.end(),
[c](const auto illegal) { return c == illegal; })
!= OCC::FileSystem::IllegalFilenameCharsWindows.end()) {
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
}
}
#endif
/* Do not sync desktop.ini files anywhere in the tree. */
if (bname.compare("desktop.ini"_L1, Qt::CaseInsensitive) == 0) {
return CSYNC_FILE_SILENTLY_EXCLUDED;
}
if (excludeConflictFiles && OCC::Utility::isConflictFile(path)) {
return CSYNC_FILE_EXCLUDE_CONFLICT;
}
return CSYNC_NOT_EXCLUDED;
}
using namespace OCC;
ExcludedFiles::ExcludedFiles()
: _clientVersion(OCC::Version::version())
{
// Windows used to use PathMatchSpec which allows *foo to match abc/deffoo.
_wildcardsMatchSlash = Utility::isWindows();
}
ExcludedFiles::~ExcludedFiles() { }
void ExcludedFiles::addExcludeFilePath(const QString &path)
{
_excludeFiles.insert(path);
}
void ExcludedFiles::setExcludeConflictFiles(bool onoff)
{
_excludeConflictFiles = onoff;
}
void ExcludedFiles::addManualExclude(const QString &expr)
{
_manualExcludes.append(expr);
_allExcludes.append(expr);
prepare();
}
void ExcludedFiles::clearManualExcludes()
{
_manualExcludes.clear();
reloadExcludeFiles();
}
void ExcludedFiles::setWildcardsMatchSlash(bool onoff)
{
_wildcardsMatchSlash = onoff;
prepare();
}
void ExcludedFiles::setClientVersion(const QVersionNumber &version)
{
_clientVersion = version;
}
bool ExcludedFiles::reloadExcludeFiles()
{
_allExcludes.clear();
bool success = true;
for (const auto &file : std::as_const(_excludeFiles)) {
QFile f(file);
if (!f.open(QIODevice::ReadOnly)) {
success = false;
continue;
}
while (!f.atEnd()) {
QByteArray line = f.readLine().trimmed();
if (line.startsWith("#!version")) {
if (!versionDirectiveKeepNextLine(line))
f.readLine();
}
if (line.isEmpty() || line.startsWith('#'))
continue;
csync_exclude_expand_escapes(line);
_allExcludes.append(QString::fromUtf8(line));
}
}
_allExcludes.append(_manualExcludes);
prepare();
return success;
}
bool ExcludedFiles::versionDirectiveKeepNextLine(const QByteArray &directive) const
{
if (!directive.startsWith("#!version"))
return true;
QByteArrayList args = directive.split(' ');
if (args.size() != 3)
return true;
QByteArray op = args[1];
QByteArrayList argVersions = args[2].split('.');
if (argVersions.size() != 3)
return true;
const auto argVersion = QVersionNumber(argVersions[0].toInt(), argVersions[1].toInt(), argVersions[2].toInt());
if (op == "<=")
return _clientVersion <= argVersion;
if (op == "<")
return _clientVersion < argVersion;
if (op == ">")
return _clientVersion > argVersion;
if (op == ">=")
return _clientVersion >= argVersion;
if (op == "==")
return _clientVersion == argVersion;
return true;
}
bool ExcludedFiles::isExcluded(QStringView filePath, QStringView basePath, bool excludeHidden) const
{
Q_ASSERT(!filePath.isEmpty());
Q_ASSERT(!basePath.isEmpty());
#ifdef Q_OS_WIN
Q_ASSERT_X(!filePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
Q_ASSERT_X(!basePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
#endif
const auto isChild = FileSystem::isChildPathOf2(filePath, basePath);
if (isChild == FileSystem::ChildResult::IsNoChild) {
// Mark paths we're not responsible for as excluded...
return true;
}
if (isChild & FileSystem::ChildResult::IsEqual) {
// both paths are equal
return false;
}
const QFileInfo fileInfo(filePath.toString());
if (!fileInfo.exists()) {
if (excludeHidden && fileInfo.fileName().startsWith(QLatin1Char('.'))) {
return true;
}
// recursively call isExcluded with the path of non-existing file's parent directory
return isExcluded(fileInfo.path(), basePath, excludeHidden);
}
if (excludeHidden) {
QFileInfo fi = fileInfo;
// Check all path subcomponents, but to *not* check the base path:
// We do want to be able to sync with a hidden folder as the target.
while (fi.filePath().size() > basePath.size()) {
if (fi.isHidden()) {
return true;
}
// Get the parent path
fi = QFileInfo{fi.path()};
}
}
ItemType type = ItemTypeFile;
if (fileInfo.isDir()) {
type = ItemTypeDirectory;
}
return isExcludedRemote(filePath, basePath, excludeHidden, type);
}
bool ExcludedFiles::isExcludedRemote(QStringView filePath, QStringView basePath, bool excludeHidden, ItemType type) const
{
Q_ASSERT(!filePath.isEmpty());
Q_ASSERT(!basePath.isEmpty());
#ifdef Q_OS_WIN
Q_ASSERT_X(!filePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
Q_ASSERT_X(!basePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
#endif
const auto isChild = FileSystem::isChildPathOf2(filePath, basePath);
if (isChild == FileSystem::ChildResult::IsNoChild) {
// Mark paths we're not responsible for as excluded...
return true;
}
if (isChild & FileSystem::ChildResult::IsEqual) {
// both paths are equal
return false;
}
auto relativePath = filePath.mid(basePath.size());
Q_ASSERT(!relativePath.isEmpty());
if (relativePath.endsWith(QLatin1Char('/'))) {
relativePath.chop(1);
}
if (excludeHidden) {
// Check their parent directories for . hidden files
if (relativePath.startsWith(QLatin1Char('.')) || relativePath.contains(QLatin1String("/."))) {
return true;
}
}
return fullPatternMatch(relativePath, type) != CSYNC_NOT_EXCLUDED;
}
CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(QStringView path, ItemType filetype) const
{
auto match = _csync_excluded_common(path, _excludeConflictFiles);
if (match != CSYNC_NOT_EXCLUDED)
return match;
if (_allExcludes.isEmpty())
return CSYNC_NOT_EXCLUDED;
// Check the bname part of the path to see whether the full
// regex should be run.
QStringView bnameStr(path);
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
if (lastSlash >= 0) {
bnameStr = path.mid(lastSlash + 1);
}
QRegularExpressionMatch m;
if (filetype == ItemTypeDirectory) {
m = _bnameTraversalRegexDir.matchView(bnameStr);
} else {
m = _bnameTraversalRegexFile.matchView(bnameStr);
}
if (!m.hasMatch())
return CSYNC_NOT_EXCLUDED;
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
return CSYNC_FILE_EXCLUDE_LIST;
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
}
// third capture: full path matching is triggered
QStringView pathStr = path;
if (filetype == ItemTypeDirectory) {
m = _fullTraversalRegexDir.matchView(pathStr);
} else {
m = _fullTraversalRegexFile.matchView(pathStr);
}
if (m.hasMatch()) {
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
return CSYNC_FILE_EXCLUDE_LIST;
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
}
}
return CSYNC_NOT_EXCLUDED;
}
CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(QStringView p, ItemType filetype) const
{
auto match = _csync_excluded_common(p, _excludeConflictFiles);
if (match != CSYNC_NOT_EXCLUDED)
return match;
if (_allExcludes.isEmpty())
return CSYNC_NOT_EXCLUDED;
QRegularExpressionMatch m;
if (filetype == ItemTypeDirectory) {
m = _fullRegexDir.matchView(p);
} else {
m = _fullRegexFile.matchView(p);
}
if (m.hasMatch()) {
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
return CSYNC_FILE_EXCLUDE_LIST;
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
}
}
return CSYNC_NOT_EXCLUDED;
}
/**
* On linux we used to use fnmatch with FNM_PATHNAME, but the windows function we used
* didn't have that behavior. wildcardsMatchSlash can be used to control which behavior
* the resulting regex shall use.
*/
QString ExcludedFiles::convertToRegexpSyntax(QString exclude, bool wildcardsMatchSlash)
{
// Translate *, ?, [...] to their regex variants.
// The escape sequences \*, \?, \[. \\ have a special meaning,
// the other ones have already been expanded before
// (like "\\n" being replaced by "\n").
//
// QString being UTF-16 makes unicode-correct escaping tricky.
// If we escaped each UTF-16 code unit we'd end up splitting 4-byte
// code points. To avoid problems we delegate as much work as possible to
// QRegularExpression::escape(): It always receives as long a sequence
// as code units as possible.
QString regex;
int i = 0;
int charsToEscape = 0;
auto flush = [&]() {
regex.append(QRegularExpression::escape(exclude.mid(i - charsToEscape, charsToEscape)));
charsToEscape = 0;
};
auto len = exclude.size();
for (; i < len; ++i) {
switch (exclude[i].unicode()) {
case '*':
flush();
if (wildcardsMatchSlash) {
regex.append(QLatin1String(".*"));
} else {
regex.append(QLatin1String("[^/]*"));
}
break;
case '?':
flush();
if (wildcardsMatchSlash) {
regex.append(QLatin1Char('.'));
} else {
regex.append(QStringLiteral("[^/]"));
}
break;
case '[': {
flush();
// Find the end of the bracket expression
auto j = i + 1;
for (; j < len; ++j) {
if (exclude[j] == QLatin1Char(']'))
break;
if (j != len - 1 && exclude[j] == QLatin1Char('\\') && exclude[j + 1] == QLatin1Char(']'))
++j;
}
if (j == len) {
// no matching ], just insert the escaped [
regex.append(QStringLiteral("\\["));
break;
}
// Translate [! to [^
QString bracketExpr = exclude.mid(i, j - i + 1);
if (bracketExpr.startsWith(QLatin1String("[!")))
bracketExpr[1] = QLatin1Char('^');
regex.append(bracketExpr);
i = j;
break;
}
case '\\':
flush();
if (i == len - 1) {
regex.append(QStringLiteral("\\\\"));
break;
}
// '\*' -> '\*', but '\z' -> '\\z'
switch (exclude[i + 1].unicode()) {
case '*':
case '?':
case '[':
case '\\':
regex.append(QRegularExpression::escape(exclude.mid(i + 1, 1)));
break;
default:
charsToEscape += 2;
break;
}
++i;
break;
default:
++charsToEscape;
break;
}
}
flush();
return regex;
}
QString ExcludedFiles::extractBnameTrigger(const QString &exclude, bool wildcardsMatchSlash)
{
// We can definitely drop everything to the left of a / - that will never match
// any bname.
QString pattern = exclude.mid(exclude.lastIndexOf(QLatin1Char('/')) + 1);
// Easy case, nothing else can match a slash, so that's it.
if (!wildcardsMatchSlash)
return pattern;
// Otherwise it's more complicated. Examples:
// - "foo*bar" can match "fooX/Xbar", pattern is "*bar"
// - "foo*bar*" can match "fooX/XbarX", pattern is "*bar*"
// - "foo?bar" can match "foo/bar" but also "fooXbar", pattern is "*bar"
auto isWildcard = [](QChar c) { return c == QLatin1Char('*') || c == QLatin1Char('?'); };
// First, skip wildcards on the very right of the pattern
int i = pattern.size() - 1;
while (i >= 0 && isWildcard(pattern[i]))
--i;
// Then scan further until the next wildcard that could match a /
while (i >= 0 && !isWildcard(pattern[i]))
--i;
// Everything to the right is part of the pattern
pattern = pattern.mid(i + 1);
// And if there was a wildcard, it starts with a *
if (i >= 0)
pattern.prepend(QLatin1Char('*'));
return pattern;
}
void ExcludedFiles::prepare()
{
// Build regular expressions for the different cases.
//
// To compose the _bnameTraversalRegex, _fullTraversalRegex and _fullRegex
// patterns we collect several subgroups of patterns here.
//
// * The "full" group will contain all patterns that contain a non-trailing
// slash. They only make sense in the fullRegex and fullTraversalRegex.
// * The "bname" group contains all patterns without a non-trailing slash.
// These need separate handling in the _fullRegex (slash-containing
// patterns must be anchored to the front, these don't need it)
// * The "bnameTrigger" group contains the bname part of all patterns in the
// "full" group. These and the "bname" group become _bnameTraversalRegex.
//
// To complicate matters, the exclude patterns have two binary attributes
// meaning we'll end up with 4 variants:
// * "]" patterns mean "EXCLUDE_AND_REMOVE", they get collected in the
// pattern strings ending in "Remove". The others go to "Keep".
// * trailing-slash patterns match directories only. They get collected
// in the pattern strings saying "Dir", the others go into "FileDir"
// because they match files and directories.
QString fullFileDirKeep;
QString fullFileDirRemove;
QString fullDirKeep;
QString fullDirRemove;
QString bnameFileDirKeep;
QString bnameFileDirRemove;
QString bnameDirKeep;
QString bnameDirRemove;
QString bnameTriggerFileDir;
QString bnameTriggerDir;
auto regexAppend = [](QString &fileDirPattern, QString &dirPattern, const QString &appendMe, bool dirOnly) {
QString &pattern = dirOnly ? dirPattern : fileDirPattern;
if (!pattern.isEmpty())
pattern.append(QLatin1Char('|'));
pattern.append(appendMe);
};
for (auto exclude : std::as_const(_allExcludes)) {
if (exclude[0] == QLatin1Char('\n'))
continue; // empty line
if (exclude[0] == QLatin1Char('\r'))
continue; // empty line
bool matchDirOnly = exclude.endsWith(QLatin1Char('/'));
if (matchDirOnly)
exclude = exclude.left(exclude.size() - 1);
bool removeExcluded = (exclude[0] == QLatin1Char(']'));
if (removeExcluded)
exclude = exclude.mid(1);
bool fullPath = exclude.contains(QLatin1Char('/'));
/* Use QRegularExpression, append to the right pattern */
auto &bnameFileDir = removeExcluded ? bnameFileDirRemove : bnameFileDirKeep;
auto &bnameDir = removeExcluded ? bnameDirRemove : bnameDirKeep;
auto &fullFileDir = removeExcluded ? fullFileDirRemove : fullFileDirKeep;
auto &fullDir = removeExcluded ? fullDirRemove : fullDirKeep;
auto regexExclude = convertToRegexpSyntax(exclude, _wildcardsMatchSlash);
if (!fullPath) {
regexAppend(bnameFileDir, bnameDir, regexExclude, matchDirOnly);
} else {
regexAppend(fullFileDir, fullDir, regexExclude, matchDirOnly);
// For activation, trigger on the 'bname' part of the full pattern.
QString bnameExclude = extractBnameTrigger(exclude, _wildcardsMatchSlash);
auto regexBname = convertToRegexpSyntax(bnameExclude, true);
regexAppend(bnameTriggerFileDir, bnameTriggerDir, regexBname, matchDirOnly);
}
}
// The empty pattern would match everything - change it to match-nothing
auto emptyMatchNothing = [](QString &pattern) {
if (pattern.isEmpty())
pattern = QStringLiteral("a^");
};
emptyMatchNothing(fullFileDirKeep);
emptyMatchNothing(fullFileDirRemove);
emptyMatchNothing(fullDirKeep);
emptyMatchNothing(fullDirRemove);
emptyMatchNothing(bnameFileDirKeep);
emptyMatchNothing(bnameFileDirRemove);
emptyMatchNothing(bnameDirKeep);
emptyMatchNothing(bnameDirRemove);
emptyMatchNothing(bnameTriggerFileDir);
emptyMatchNothing(bnameTriggerDir);
// The bname regex is applied to the bname only, so it must be
// anchored in the beginning and in the end. It has the structure:
// (exclude)|(excluderemove)|(bname triggers).
// If the third group matches, the fullActivatedRegex needs to be applied
// to the full path.
_bnameTraversalRegexFile.setPattern(QStringLiteral("^(?P<exclude>%1)$|"
"^(?P<excluderemove>%2)$|"
"^(?P<trigger>%3)$")
.arg(bnameFileDirKeep, bnameFileDirRemove, bnameTriggerFileDir));
_bnameTraversalRegexDir.setPattern(QStringLiteral("^(?P<exclude>%1|%2)$|"
"^(?P<excluderemove>%3|%4)$|"
"^(?P<trigger>%5|%6)$")
.arg(bnameFileDirKeep, bnameDirKeep, bnameFileDirRemove, bnameDirRemove, bnameTriggerFileDir, bnameTriggerDir));
// The full traveral regex is applied to the full path if the trigger capture of
// the bname regex matches. Its basic form is (exclude)|(excluderemove)".
// This pattern can be much simpler than fullRegex since we can assume a traversal
// situation and doesn't need to look for bname patterns in parent paths.
_fullTraversalRegexFile.setPattern(
// Full patterns are anchored to the beginning
QStringLiteral("^(?P<exclude>%1)(?:$|/)"
"|"
"^(?P<excluderemove>%2)(?:$|/)")
.arg(fullFileDirKeep, fullFileDirRemove));
_fullTraversalRegexDir.setPattern(QStringLiteral("^(?P<exclude>%1|%2)(?:$|/)"
"|"
"^(?P<excluderemove>%3|%4)(?:$|/)")
.arg(fullFileDirKeep, fullDirKeep, fullFileDirRemove, fullDirRemove));
// The full regex is applied to the full path and incorporates both bname and
// full-path patterns. It has the form "(exclude)|(excluderemove)".
_fullRegexFile.setPattern(QStringLiteral("(?P<exclude>"
// Full patterns are anchored to the beginning
"^(?:%1)(?:$|/)|"
// Simple bname patterns can be any path component
"(?:^|/)(?:%2)(?:$|/)|"
// When checking a file for exclusion we must check all parent paths
// against the dir-only patterns as well.
"(?:^|/)(?:%3)/)"
"|"
"(?P<excluderemove>"
"^(?:%4)(?:$|/)|"
"(?:^|/)(?:%5)(?:$|/)|"
"(?:^|/)(?:%6)/)")
.arg(fullFileDirKeep, bnameFileDirKeep, bnameDirKeep, fullFileDirRemove, bnameFileDirRemove, bnameDirRemove));
_fullRegexDir.setPattern(
QStringLiteral("(?P<exclude>"
"^(?:%1|%2)(?:$|/)|"
"(?:^|/)(?:%3|%4)(?:$|/))"
"|"
"(?P<excluderemove>"
"^(?:%5|%6)(?:$|/)|"
"(?:^|/)(?:%7|%8)(?:$|/))")
.arg(fullFileDirKeep, fullDirKeep, bnameFileDirKeep, bnameDirKeep, fullFileDirRemove, fullDirRemove, bnameFileDirRemove, bnameDirRemove));
QRegularExpression::PatternOptions patternOptions = QRegularExpression::NoPatternOption;
if (OCC::Utility::fsCasePreserving())
patternOptions |= QRegularExpression::CaseInsensitiveOption;
_bnameTraversalRegexFile.setPatternOptions(patternOptions);
_bnameTraversalRegexFile.optimize();
_bnameTraversalRegexDir.setPatternOptions(patternOptions);
_bnameTraversalRegexDir.optimize();
_fullTraversalRegexFile.setPatternOptions(patternOptions);
_fullTraversalRegexFile.optimize();
_fullTraversalRegexDir.setPatternOptions(patternOptions);
_fullTraversalRegexDir.optimize();
_fullRegexFile.setPatternOptions(patternOptions);
_fullRegexFile.optimize();
_fullRegexDir.setPatternOptions(patternOptions);
_fullRegexDir.optimize();
}
+246
View File
@@ -0,0 +1,246 @@
/*
* libcsync -- a library to sync a directory with another
*
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _CSYNC_EXCLUDE_H
#define _CSYNC_EXCLUDE_H
#include "libsync/qsferasynclib.h"
#include "csync.h"
#include <QObject>
#include <QRegularExpression>
#include <QSet>
#include <QString>
#include <QVersionNumber>
#include <functional>
enum CSYNC_EXCLUDE_TYPE : uint8_t {
CSYNC_NOT_EXCLUDED = 0,
CSYNC_FILE_SILENTLY_EXCLUDED,
CSYNC_FILE_EXCLUDE_AND_REMOVE, // TODO: is there still a difference to CSYNC_FILE_SILENTLY_EXCLUDED
CSYNC_FILE_EXCLUDE_LIST,
CSYNC_FILE_EXCLUDE_INVALID_CHAR,
CSYNC_FILE_EXCLUDE_TRAILING_SPACE,
CSYNC_FILE_EXCLUDE_LONG_FILENAME,
CSYNC_FILE_EXCLUDE_HIDDEN,
CSYNC_FILE_EXCLUDE_CONFLICT,
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
CSYNC_FILE_EXCLUDE_RESERVED,
};
/**
* Manages file/directory exclusion.
*
* Most commonly exclude patterns are loaded from files. See
* addExcludeFilePath() and reloadExcludeFiles().
*
* Excluded files are primarily relevant for sync runs, and for
* file watcher filtering.
*
* Excluded files and ignored files are the same thing. But the
* selective sync blacklist functionality is a different thing
* entirely.
*/
class QSFERA_SYNC_EXPORT ExcludedFiles : public QObject
{
Q_OBJECT
public:
ExcludedFiles();
~ExcludedFiles() override;
/**
* Adds a new path to a file containing exclude patterns.
*
* Does not load the file. Use reloadExcludeFiles() afterwards.
*/
void addExcludeFilePath(const QString &path);
/**
* Whether conflict files shall be excluded.
*
* Defaults to true.
*/
void setExcludeConflictFiles(bool onoff);
/**
* Checks whether a file or directory should be excluded.
*
* @param filePath the absolute path to the file
* @param basePath folder path from which to apply exclude rules, ends with a /
*/
bool isExcluded(QStringView filePath, QStringView basePath, bool excludeHidden) const;
/**
* Checks whether a remote file or directory should be excluded.
*
* @param filePath the absolute path to the file
* @param basePath folder path from which to apply exclude rules, ends with a /
*/
bool isExcludedRemote(QStringView filePath, QStringView basePath, bool excludeHidden, ItemType type) const;
/**
* Adds an exclude pattern.
*
* Primarily used in tests. Patterns added this way are preserved when
* reloadExcludeFiles() is called.
*/
void addManualExclude(const QString &expr);
/**
* Removes all manually added exclude patterns.
*
* Primarily used in tests.
*/
void clearManualExcludes();
/**
* Adjusts behavior of wildcards. Only used for testing.
*/
void setWildcardsMatchSlash(bool onoff);
/**
* Sets the client version, only used for testing.
*/
void setClientVersion(const QVersionNumber &version);
/**
* @brief Check if the given path should be excluded in a traversal situation.
*
* It does only part of the work that full() does because it's assumed
* that all leading directories have been run through traversal()
* before. This can be significantly faster.
*
* That means for 'foo/bar/file' only ('foo/bar/file', 'file') is checked
* against the exclude patterns.
*
* @param Path is folder-relative, should not start with a /.
*
* Note that this only matches patterns. It does not check whether the file
* or directory pointed to is hidden (or whether it even exists).
*/
CSYNC_EXCLUDE_TYPE traversalPatternMatch(QStringView path, ItemType filetype) const;
public Q_SLOTS:
/**
* Reloads the exclude patterns from the registered paths.
*/
bool reloadExcludeFiles();
private:
/**
* Returns true if the version directive indicates the next line
* should be skipped.
*
* A version directive has the form "#!version <op> <version>"
* where <op> can be <, <=, ==, >, >= and <version> can be any version
* like 2.5.0.
*
* Example:
*
* #!version < 2.5.0
* myexclude
*
* Would enable the "myexclude" pattern only for versions before 2.5.0.
*/
bool versionDirectiveKeepNextLine(const QByteArray &directive) const;
/**
* @brief Match the exclude pattern against the full path.
*
* @param Path is folder-relative, should not start with a /.
*
* Note that this only matches patterns. It does not check whether the file
* or directory pointed to is hidden (or whether it even exists).
*/
CSYNC_EXCLUDE_TYPE fullPatternMatch(QStringView path, ItemType filetype) const;
/**
* Generate optimized regular expressions for the exclude patterns.
*
* The optimization works in two steps: First, all supported patterns are put
* into _fullRegexFile/_fullRegexDir. These regexes can be applied to the full
* path to determine whether it is excluded or not.
*
* The second is a performance optimization. The particularly common use
* case for excludes during a sync run is "traversal": Instead of checking
* the full path every time, we check each parent path with the traversal
* function incrementally.
*
* Example: When the sync run eventually arrives at "a/b/c it can assume
* that the traversal matching has already been run on "a", "a/b"
* and just needs to run the traversal matcher on "a/b/c".
*
* The full matcher is equivalent to or-combining the traversal match results
* of all parent paths:
* full("a/b/c/d") == traversal("a") || traversal("a/b") || traversal("a/b/c")
*
* The traversal matcher can be extremely fast because it has a fast early-out
* case: It checks the bname part of the path against _bnameTraversalRegex
* and only runs a simplified _fullTraversalRegex on the whole path if bname
* activation for it was triggered.
*
* Note: The traversal matcher will return not-excluded on some paths that the
* full matcher would exclude. Example: "b" is excluded. traversal("b/c")
* returns not-excluded because "c" isn't a bname activation pattern.
*/
void prepare();
static QString extractBnameTrigger(const QString &exclude, bool wildcardsMatchSlash);
static QString convertToRegexpSyntax(QString exclude, bool wildcardsMatchSlash);
/// Files to load excludes from
QSet<QString> _excludeFiles;
/// Exclude patterns added with addManualExclude()
QStringList _manualExcludes;
/// List of all active exclude patterns
QStringList _allExcludes;
/// see prepare()
QRegularExpression _bnameTraversalRegexFile;
QRegularExpression _bnameTraversalRegexDir;
QRegularExpression _fullTraversalRegexFile;
QRegularExpression _fullTraversalRegexDir;
QRegularExpression _fullRegexFile;
QRegularExpression _fullRegexDir;
bool _excludeConflictFiles = true;
/**
* Whether * and ? in patterns can match a /
*
* Unfortunately this was how matching was done on Windows so
* it continues to be enabled there.
*/
bool _wildcardsMatchSlash = false;
/**
* The client version. Used to evaluate version-dependent excludes,
* see versionDirectiveKeepNextLine().
*/
QVersionNumber _clientVersion;
friend class TestExcludedFiles;
};
#endif /* _CSYNC_EXCLUDE_H */
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "libsync/common/syncjournaldb.h"
#include "libsync/discoveryinfo.h"
#include "libsync/discoveryphase.h"
#include "libsync/syncfileitem.h"
#include <deque>
class ExcludedFiles;
namespace OCC {
class SyncJournalDb;
/**
* Job that handles discovery of a directory.
*
* This includes:
* - Do a DiscoverySingleDirectoryJob network job which will do a PROPFIND of this directory
* - Stat all the entries in the local file system for this directory
* - Merge all information (and the information from the database) in order to know what needs
* to be done for every file within this directory.
* - For every sub-directory within this directory, "recursively" create a new ProcessDirectoryJob.
*
* This job is tightly coupled with the DiscoveryPhase class.
*
* After being start()'ed this job will perform work asynchronously and Q_EMIT finished() when done.
*
* Internally, this job will call DiscoveryPhase::scheduleMoreJobs when one of its sub-jobs is
* finished. DiscoveryPhase::scheduleMoreJobs will call processSubJobs() to continue work until
* the job is finished.
*
* Results are fed outwards via the DiscoveryPhase::itemDiscovered() signal.
*/
class ProcessDirectoryJob : public QObject
{
Q_OBJECT
struct PathTuple;
public:
enum QueryMode {
NormalQuery,
ParentDontExist, // Do not query this folder because it does not exist
ParentNotChanged, // No need to query this folder because it has not changed from what is in the DB
InBlackList // Do not query this folder because it is in the blacklist (remote entries only)
};
Q_ENUM(QueryMode)
/** For creating the root job
*
* The base pin state is used if the root dir's pin state can't be retrieved.
*/
explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, QObject *parent)
: QObject(parent)
, _discoveryData(data)
{
computePinState(basePinState);
}
/// For creating subjobs
explicit ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem,
QueryMode queryLocal, QueryMode queryServer,
ProcessDirectoryJob *parent)
: QObject(parent)
, _dirItem(dirItem)
, _queryServer(queryServer)
, _queryLocal(queryLocal)
, _discoveryData(parent->_discoveryData)
, _currentFolder(path)
{
computePinState(parent->_pinState);
}
void start();
/** Start up to nbJobs, return the number of job started; Q_EMIT finished() when done */
int processSubJobs(int nbJobs);
SyncFileItemPtr _dirItem;
private:
/** Structure representing a path during discovery. A same path may have different value locally
* or on the server in case of renames.
*
* These strings never start or ends with slashes. They are all relative to the folder's root.
* Usually they are all the same and are even shared instance of the same QString.
*
* _server and _local paths will differ if there are renames, example:
* remote renamed A/ to B/ and local renamed A/X to A/Y then
* target: B/Y/file
* original: A/X/file
* local: A/Y/file
* server: B/X/file
*/
struct PathTuple
{
QString _original; // Path as in the DB (before the sync)
QString _target; // Path that will be the result after the sync (and will be in the DB)
QString _server; // Path on the server (before the sync)
QString _local; // Path locally (before the sync)
static QString pathAppend(const QString &base, const QString &name)
{
return base.isEmpty() ? name : base + QLatin1Char('/') + name;
}
PathTuple addName(const QString &name) const
{
PathTuple result;
result._original = pathAppend(_original, name);
auto buildString = [&](const QString &other) {
// Optimize by trying to keep all string implicitly shared if they are the same (common case)
return other == _original ? result._original : pathAppend(other, name);
};
result._target = buildString(_target);
result._server = buildString(_server);
result._local = buildString(_local);
return result;
}
};
/** Iterate over entries inside the directory (non-recursively).
*
* Called once _serverEntries and _localEntries are filled
* Calls processFile() for each non-excluded one.
* Will start scheduling subdir jobs when done.
*/
void process();
// return true if the file is excluded.
// path is the full relative path of the file. localName is the base name of the local entry.
bool handleExcluded(const QString &path, const QString &localName, bool isDirectory,
bool isHidden, bool isSymlink);
/** Reconcile local/remote/db information for a single item.
*
* Can be a file or a directory.
* Usually ends up emitting itemDiscovered() or creating a subdirectory job.
*
* This main function delegates some work to the processFile* functions.
*/
void processFile(const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
/// processFile helper for when remote information is available, typically flows into AnalyzeLocalInfo when done
void processFileAnalyzeRemoteInfo(const SyncFileItemPtr &item, PathTuple, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
/// processFile helper for reconciling local changes
void processFileAnalyzeLocalInfo(const SyncFileItemPtr &item, const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &, QueryMode recurseQueryServer);
/// processFile helper for local/remote conflicts
void processFileConflict(const SyncFileItemPtr &item, const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
/// processFile helper for common final processing
void processFileFinalize(const SyncFileItemPtr &item, PathTuple, bool recurse, QueryMode recurseQueryLocal, QueryMode recurseQueryServer);
/** Checks the permission for this item, if needed, change the item to a restoration item.
* @return false indicate that this is an error and if it is a directory, one should not recurse
* inside it.
*/
bool checkPermissions(const SyncFileItemPtr &item);
struct MovePermissionResult
{
// whether moving/renaming the source is ok
bool sourceOk;
// whether the destination accepts (always true for renames)
bool destinationOk;
// whether creating a new file/dir in the destination is ok
bool destinationNewOk;
};
/**
* Check if the move is of a specified file within this directory is allowed.
* Return true if it is allowed, false otherwise
*/
MovePermissionResult checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath, bool isDirectory);
void processBlacklisted(const PathTuple &, const LocalInfo &, const SyncJournalFileRecord &dbEntry);
void subJobFinished();
/** An DB operation failed */
void dbError();
/** Start a remote discovery network job
*
* It fills _serverNormalQueryEntries and sets _serverQueryDone when done.
*/
DiscoverySingleDirectoryJob *startAsyncServerQuery();
/** Discover the local directory
*
* Fills _localNormalQueryEntries.
*/
void startAsyncLocalQuery();
/** Sets _pinState, the directory's pin state
*
* If the folder exists locally its state is retrieved, otherwise the
* parent's pin state is inherited.
*/
void computePinState(PinState parentState);
QueryMode _queryServer = QueryMode::NormalQuery;
QueryMode _queryLocal = QueryMode::NormalQuery;
// Holds entries that resulted from a NormalQuery
QVector<RemoteInfo> _serverNormalQueryEntries;
QVector<LocalInfo> _localNormalQueryEntries;
// Whether the local/remote directory item queries are done. Will be set
// even even for do-nothing (!= NormalQuery) queries.
bool _serverQueryDone = false;
bool _localQueryDone = false;
RemotePermissions _rootPermissions;
QPointer<DiscoverySingleDirectoryJob> _serverJob;
/** Number of currently running async jobs.
*
* These "async jobs" have nothing to do with the jobs for subdirectories
* which are being tracked by _queuedJobs and _runningJobs.
*
* They are jobs that need to be completed to finish processing of directory
* entries. This variable is used to ensure this job doesn't finish while
* these jobs are still in flight.
*/
int _pendingAsyncJobs = 0;
/** The queued and running jobs for subdirectories.
*
* The jobs are enqueued while processind directory entries and
* then gradually run via calls to processSubJobs().
*/
std::deque<ProcessDirectoryJob *> _queuedJobs;
QVector<ProcessDirectoryJob *> _runningJobs;
DiscoveryPhase *_discoveryData;
PathTuple _currentFolder;
bool _childModified = false; // the directory contains modified item what would prevent deletion
bool _childIgnored = false; // The directory contains ignored item that would prevent deletion
PinState _pinState = PinState::Unspecified; // The directory's pin-state, see computePinState()
Q_SIGNALS:
void finished();
// The root etag of this directory was fetched
void etag(const QString &, const QDateTime &time);
};
}
+165
View File
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "libsync/discoveryinfo.h"
#include "libsync/filesystem.h"
#ifdef Q_OS_WIN
#include "libsync/common/utility_win.h"
#else
#include <sys/stat.h>
#endif
Q_LOGGING_CATEGORY(lcLocalInfo, "sync.discovery.localinfo", QtInfoMsg)
using namespace OCC;
class OCC::LocalInfoData : public QSharedData
{
public:
LocalInfoData() = default;
~LocalInfoData() = default;
LocalInfoData(const std::filesystem::directory_entry &dirent, ItemType type)
: _name(FileSystem::fromFilesystemPath(dirent.path().filename()))
, _type(type)
{
Q_ASSERT(!dirent.is_symlink() || type == ItemTypeSymLink);
#ifdef Q_OS_WIN
auto h = Utility::Handle::createHandle(dirent.path(), {.followSymlinks = false});
if (!h) {
qCWarning(lcLocalInfo) << dirent.path().native() << h.errorMessage();
_name.clear();
return;
}
BY_HANDLE_FILE_INFORMATION fileInfo = {};
if (!GetFileInformationByHandle(h, &fileInfo)) {
const auto error = GetLastError();
qCCritical(lcLocalInfo) << u"GetFileInformationByHandle failed on" << dirent.path().native() << OCC::Utility::formatWinError(error);
_name.clear();
return;
}
_isHidden = fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN;
_inode = ULARGE_INTEGER{{ fileInfo.nFileIndexLow, fileInfo.nFileIndexHigh }}.QuadPart;
_size = ULARGE_INTEGER{{ fileInfo.nFileSizeLow, fileInfo.nFileSizeHigh }}.QuadPart;
_modtime = FileSystem::fileTimeToTime_t(std::filesystem::file_time_type{std::filesystem::file_time_type::duration {
ULARGE_INTEGER{fileInfo.ftLastWriteTime.dwLowDateTime, fileInfo.ftLastWriteTime.dwHighDateTime}.QuadPart
}});
#else
struct stat sb;
if (lstat(dirent.path().native().data(), &sb) < 0) {
qCCritical(lcLocalInfo) << u"lstat failed on" << dirent.path().native();
_name.clear();
return;
}
_inode = sb.st_ino;
_size = sb.st_size;
_modtime = sb.st_mtime;
#ifdef Q_OS_MAC
_isHidden = sb.st_flags & UF_HIDDEN;
#endif
#endif
}
QString _name;
ItemType _type = ItemTypeUnsupported;
time_t _modtime = 0;
int64_t _size = 0;
uint64_t _inode = 0;
bool _isHidden = false;
};
LocalInfo::LocalInfo()
: d([] {
static QExplicitlySharedDataPointer<LocalInfoData> nullData{new LocalInfoData{}};
return nullData;
}())
{
}
LocalInfo::LocalInfo(const std::filesystem::directory_entry &dirent, ItemType type)
: d(new LocalInfoData(dirent, type))
{
}
LocalInfo::~LocalInfo() = default;
LocalInfo::LocalInfo(const LocalInfo &other) = default;
LocalInfo &LocalInfo::operator=(const LocalInfo &other) = default;
LocalInfo::LocalInfo(const std::filesystem::directory_entry &dirent)
: LocalInfo(dirent, LocalInfo::typeFromDirectoryEntry(dirent))
{
}
LocalInfo::LocalInfo(const std::filesystem::path &path)
: LocalInfo(std::filesystem::directory_entry{path})
{
}
ItemType LocalInfo::typeFromDirectoryEntry(const std::filesystem::directory_entry &dirent)
{
// a file can be a symlink but point to a regular file, so we check for symlink first
if (dirent.is_symlink()) {
return ItemTypeSymLink;
}
if (dirent.is_regular_file()) {
return ItemTypeFile;
}
if (dirent.is_directory()) {
return ItemTypeDirectory;
}
return ItemTypeUnsupported;
}
bool LocalInfo::isHidden() const
{
return d->_isHidden;
}
QString LocalInfo::name() const
{
return d->_name;
}
time_t LocalInfo::modtime() const
{
return d->_modtime;
}
int64_t LocalInfo::size() const
{
return d->_size;
}
uint64_t LocalInfo::inode() const
{
return d->_inode;
}
ItemType LocalInfo::type() const
{
return d->_type;
}
bool LocalInfo::isDirectory() const
{
return d->_type == ItemTypeDirectory;
}
bool LocalInfo::isVirtualFile() const
{
return d->_type == ItemTypeVirtualFile || d->_type == ItemTypeVirtualFileDownload;
}
bool LocalInfo::isSymLink() const
{
return d->_type == ItemTypeSymLink;
}
bool LocalInfo::isValid() const
{
return !d->_name.isNull();
}
+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "libsync/csync.h"
#include <QSharedData>
#include <filesystem>
namespace OCC {
class LocalInfoData;
class QSFERA_SYNC_EXPORT LocalInfo
{
public:
LocalInfo();
~LocalInfo();
LocalInfo(const LocalInfo &other);
LocalInfo &operator=(const LocalInfo &other);
void swap(LocalInfo &other) noexcept { d.swap(other.d); }
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(LocalInfo);
LocalInfo(const std::filesystem::directory_entry &dirent, ItemType type);
LocalInfo(const std::filesystem::directory_entry &dirent);
// TODO: consume path by default
LocalInfo(const std::filesystem::path &path);
static ItemType typeFromDirectoryEntry(const std::filesystem::directory_entry &dirent);
bool isHidden() const;
/** FileName of the entry (this does not contain any directory or path, just the plain name */
QString name() const;
time_t modtime() const;
int64_t size() const;
uint64_t inode() const;
ItemType type() const;
bool isDirectory() const;
bool isVirtualFile() const;
bool isSymLink() const;
bool isValid() const;
private:
QExplicitlySharedDataPointer<LocalInfoData> d;
};
}
Q_DECLARE_SHARED(OCC::LocalInfo);
+316
View File
@@ -0,0 +1,316 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "discoveryphase.h"
#include "discovery.h"
#include "account.h"
#include "common/asserts.h"
#include "common/checksums.h"
#include "filesystem.h"
#include <QDateTime>
#include <QFile>
#include <QLoggingCategory>
#include <QUrl>
#include <cstring>
using namespace Qt::Literals::StringLiterals;
namespace OCC {
Q_LOGGING_CATEGORY(lcDiscovery, "sync.discovery", QtInfoMsg)
/* Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list*/
static bool findPathInList(const std::set<QString> &list, const QString &path)
{
if (list.size() == 1 && *list.cbegin() == QLatin1String("/")) {
// Special case for the case "/" is there, it matches everything
return true;
}
QString pathSlash = path + QLatin1Char('/');
// Since the list is sorted, we can do a binary search.
// If the path is a prefix of another item or right after in the lexical order.
auto it = std::lower_bound(list.begin(), list.end(), pathSlash);
if (it != list.end() && *it == pathSlash) {
return true;
}
if (it == list.begin()) {
return false;
}
--it;
Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that
return pathSlash.startsWith(*it);
}
bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const
{
if (_selectiveSyncBlackList.empty()) {
// If there is no black list, everything is allowed
return false;
}
// Block if it is in the black list
if (findPathInList(_selectiveSyncBlackList, path)) {
return true;
}
return false;
}
/* Given a path on the remote, give the path as it is when the rename is done */
QString DiscoveryPhase::adjustRenamedPath(const QString &original, SyncFileItem::Direction d) const
{
return OCC::adjustRenamedPath(d == SyncFileItem::Down ? _renamedItemsRemote : _renamedItemsLocal, original);
}
QString adjustRenamedPath(const QHash<QString, QString> &renamedItems, const QString &original)
{
int slashPos = original.size();
while ((slashPos = original.lastIndexOf(QLatin1Char('/'), slashPos - 1)) > 0) {
auto it = renamedItems.constFind(original.left(slashPos));
if (it != renamedItems.constEnd()) {
return *it + original.mid(slashPos);
}
}
return original;
}
QPair<bool, QString> DiscoveryPhase::findAndCancelDeletedJob(const QString &originalPath)
{
bool result = false;
QString oldEtag;
auto it = _deletedItem.constFind(originalPath);
if (it != _deletedItem.cend()) {
const auto &item = *it;
const SyncInstructions instruction = item->instruction();
if (instruction == CSYNC_INSTRUCTION_IGNORE && item->_type == ItemTypeVirtualFile) {
// re-creation of virtual files count as a delete
// restoration after a prohibited move
// a file might be in an error state and thus gets marked as CSYNC_INSTRUCTION_IGNORE
// after it was initially marked as CSYNC_INSTRUCTION_REMOVE
// return true, to not trigger any additional actions on that file that could elad to dataloss
result = true;
oldEtag = item->_etag;
} else {
if (!(instruction == CSYNC_INSTRUCTION_REMOVE
// re-creation of virtual files count as a delete
|| (item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)
|| (item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)
// we encountered an ignored error
|| (item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE))) {
qCWarning(lcDiscovery) << u"OC_ENFORCE(FAILING)" << originalPath;
qCWarning(lcDiscovery) << u"instruction == CSYNC_INSTRUCTION_REMOVE" << (instruction == CSYNC_INSTRUCTION_REMOVE);
qCWarning(lcDiscovery) << u"(item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)"
<< (item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW);
qCWarning(lcDiscovery) << u"(item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)"
<< (item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW);
qCWarning(lcDiscovery) << u"(item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE)"
<< (item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE);
qCWarning(lcDiscovery) << u"instruction" << instruction;
qCWarning(lcDiscovery) << u"item->_type" << item->_type;
qCWarning(lcDiscovery) << u"item->_isRestoration " << item->_isRestoration;
qCWarning(lcDiscovery) << u"item->_remotePerm" << item->_remotePerm;
OC_ENFORCE(false);
}
item->setInstruction(CSYNC_INSTRUCTION_NONE);
result = true;
oldEtag = item->_etag;
}
_deletedItem.erase(it);
}
if (auto *otherJob = _queuedDeletedDirectories.take(originalPath)) {
oldEtag = otherJob->_dirItem->_etag;
delete otherJob;
result = true;
}
return { result, oldEtag };
}
void DiscoveryPhase::startJob(ProcessDirectoryJob *job)
{
OC_ENFORCE(!_currentRootJob);
connect(job, &ProcessDirectoryJob::finished, this, [this, job] {
OC_ENFORCE(_currentRootJob == sender());
_currentRootJob = nullptr;
if (job->_dirItem)
Q_EMIT itemDiscovered(job->_dirItem);
job->deleteLater();
// Once the main job has finished recurse here to execute the remaining
// jobs for queued deleted directories.
if (!_queuedDeletedDirectories.isEmpty()) {
auto nextJob = _queuedDeletedDirectories.take(_queuedDeletedDirectories.firstKey());
startJob(nextJob);
} else {
Q_EMIT finished();
}
});
_currentRootJob = job;
job->start();
}
void DiscoveryPhase::setSelectiveSyncBlackList(const QSet<QString> &list)
{
_selectiveSyncBlackList = {list.cbegin(), list.cend()};
}
void DiscoveryPhase::setSelectiveSyncWhiteList(const QSet<QString> &list)
{
_selectiveSyncWhiteList = {list.cbegin(), list.cend()};
}
void DiscoveryPhase::scheduleMoreJobs()
{
auto limit = std::max(1, _syncOptions._parallelNetworkJobs());
if (_currentRootJob && _currentlyActiveJobs < limit) {
_currentRootJob->processSubJobs(limit - _currentlyActiveJobs);
}
}
DiscoverySingleLocalDirectoryJob::DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent)
: QObject(parent)
, QRunnable()
, _localPath(localPath)
, _account(account)
, _vfs(vfs)
{
qRegisterMetaType<QVector<LocalInfo> >("QVector<LocalInfo>");
}
// Use as QRunnable
void DiscoverySingleLocalDirectoryJob::run() {
std::error_code ec;
const auto localPath = FileSystem::toFilesystemPath(_localPath);
QVector<LocalInfo> results;
for (const auto &dirent : std::filesystem::directory_iterator{localPath, ec}) {
ItemType type = LocalInfo::typeFromDirectoryEntry(dirent);
if (type == ItemTypeUnsupported) {
continue;
}
auto info = _vfs->statTypeVirtualFile(dirent, type);
if (!info.isValid()) {
continue;
}
results.push_back(std::move(info));
}
if (ec) {
qCCritical(lcDiscovery) << u"Error while opening directory" << _localPath << ec.message();
QString errorString = tr("Error while opening directory »%1«").arg(_localPath);
if (ec.value() == EACCES) {
errorString = tr("Directory not accessible on client, permission denied");
Q_EMIT finishedNonFatalError(errorString);
return;
} else if (ec.value() == ENOENT) {
errorString = tr("Directory not found: »%1«").arg(_localPath);
} else if (ec.value() == ENOTDIR) {
// Not a directory..
// Just consider it is empty
return;
}
Q_EMIT finishedFatalError(errorString);
return;
}
Q_EMIT finished(results);
}
DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, const QUrl &baseUrl, const QString &path, QObject *parent)
: QObject(parent)
, _subPath(path)
, _account(account)
, _baseUrl(baseUrl)
, _ignoredFirst(false)
, _isRootPath(false)
{
}
void DiscoverySingleDirectoryJob::start()
{
// Start the actual HTTP job
_proFindJob = new PropfindJob(_account, _baseUrl, _subPath, PropfindJob::Depth::One, this);
_proFindJob->setProperties({
"resourcetype"_ba,
"getlastmodified"_ba,
"getcontentlength"_ba,
"getetag"_ba,
"http://owncloud.org/ns:id"_ba,
"http://owncloud.org/ns:permissions"_ba,
"http://owncloud.org/ns:checksums"_ba,
});
QObject::connect(_proFindJob, &PropfindJob::directoryListingIterated,
this, &DiscoverySingleDirectoryJob::directoryListingIteratedSlot);
QObject::connect(_proFindJob, &PropfindJob::finishedWithError, this, [this] {
QString msg = _proFindJob->errorString();
if (_proFindJob->reply()->error() == QNetworkReply::NoError
&& !_proFindJob->reply()->header(QNetworkRequest::ContentTypeHeader).toString().contains(QLatin1String("application/xml; charset=utf-8"))) {
msg = tr("Server error: PROPFIND reply is not XML formatted!");
}
Q_EMIT finished(HttpError{_proFindJob->httpStatusCode(), msg});
deleteLater();
});
QObject::connect(_proFindJob, &PropfindJob::finishedWithoutError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot);
_proFindJob->start();
}
void DiscoverySingleDirectoryJob::abort()
{
if (_proFindJob) {
_proFindJob->abort();
}
}
void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &file, const QMap<QString, QString> &map)
{
if (!_ignoredFirst) {
// The first entry is for the folder itself, we should process it differently.
_ignoredFirst = true;
if (auto it = Utility::optionalFind(map, QStringLiteral("permissions"))) {
auto perm = RemotePermissions::fromServerString(it->value());
Q_EMIT firstDirectoryPermissions(perm);
}
} else {
_results.emplace_back(file, map);
}
//This works in concerto with the RequestEtagJob and the Folder object to check if the remote folder changed.
if (_firstEtag.isEmpty()) {
if (auto it = Utility::optionalFind(map, QStringLiteral("getetag"))) {
_firstEtag = Utility::normalizeEtag(it->value()); // for directory itself
}
}
}
void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot()
{
if (!_ignoredFirst) {
// This is a sanity check, if we haven't _ignoredFirst then it means we never received any directoryListingIteratedSlot
// which means somehow the server XML was bogus
Q_EMIT finished(HttpError{0, tr("Server error: PROPFIND reply is not XML formatted!")});
deleteLater();
return;
} else if (!_error.isEmpty()) {
Q_EMIT finished(HttpError{0, _error});
deleteLater();
return;
}
Q_EMIT etag(_firstEtag, _proFindJob->responseQTimeStamp());
Q_EMIT finished(_results);
deleteLater();
}
}
+237
View File
@@ -0,0 +1,237 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "libsync/discoveryremoteinfo.h"
#include "networkjobs.h"
#include "syncfileitem.h"
#include "syncoptions.h"
#include <QMap>
#include <QObject>
#include <QRunnable>
#include <QStringList>
class ExcludedFiles;
namespace OCC {
class Vfs;
enum class LocalDiscoveryStyle {
FilesystemOnly, //< read all local data from the filesystem
DatabaseAndFilesystem, //< read from the db, except for listed paths
};
class Account;
class SyncJournalDb;
class ProcessDirectoryJob;
/**
* @brief Run list on a local directory and process the results for Discovery
*
* @ingroup libsync
*/
class DiscoverySingleLocalDirectoryJob : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent = nullptr);
void run() override;
Q_SIGNALS:
void finished(QVector<LocalInfo> result);
void finishedFatalError(QString errorString);
void finishedNonFatalError(QString errorString);
void itemDiscovered(SyncFileItemPtr item);
void childIgnored(bool b);
private:
QString _localPath;
AccountPtr _account;
OCC::Vfs *_vfs;
};
/**
* @brief Run a PROPFIND on a directory and process the results for Discovery
*
* @ingroup libsync
*/
class DiscoverySingleDirectoryJob : public QObject
{
Q_OBJECT
public:
explicit DiscoverySingleDirectoryJob(const AccountPtr &account, const QUrl &baseUrl, const QString &path, QObject *parent = nullptr);
// Specify that this is the root and we need to check the data-fingerprint
void setIsRootPath() { _isRootPath = true; }
bool isRootPath() const { return _isRootPath; }
void start();
void abort();
// This is not actually a network job, it is just a job
Q_SIGNALS:
void firstDirectoryPermissions(RemotePermissions);
void etag(const QString &, const QDateTime &time);
void finished(const HttpResult<QVector<RemoteInfo>> &result);
private Q_SLOTS:
void directoryListingIteratedSlot(const QString &, const QMap<QString, QString> &);
void lsJobFinishedWithoutErrorSlot();
private:
QVector<RemoteInfo> _results;
QString _subPath;
QString _firstEtag;
AccountPtr _account;
const QUrl _baseUrl;
// The first result is for the directory itself and need to be ignored.
// This flag is true if it was already ignored.
bool _ignoredFirst;
// Set to true if this is the root path and we need to check the data-fingerprint
bool _isRootPath;
// If set, the discovery will finish with an error
QString _error;
QPointer<PropfindJob> _proFindJob;
};
class DiscoveryPhase : public QObject
{
Q_OBJECT
friend class ProcessDirectoryJob;
QPointer<ProcessDirectoryJob> _currentRootJob;
/** Maps the db-path of a deleted item to its SyncFileItem.
*
* If it turns out the item was renamed after all, the instruction
* can be changed. See findAndCancelDeletedJob(). Note that
* itemDiscovered() will already have been emitted for the item.
*/
QHash<QString, SyncFileItemPtr> _deletedItem;
/** Maps the db-path of a deleted folder to its queued job.
*
* If a folder is deleted and must be recursed into, its job isn't
* executed immediately. Instead it's queued here and only run
* once the rest of the discovery has finished and we are certain
* that the folder wasn't just renamed. This avoids running the
* discovery on contents in the old location of renamed folders.
*
* See findAndCancelDeletedJob().
*/
// needs to be ordered
QMap<QString, ProcessDirectoryJob *> _queuedDeletedDirectories;
// map source (original path) -> destinations (current server or local path)
QHash<QString, QString> _renamedItemsRemote;
QHash<QString, QString> _renamedItemsLocal;
// set of paths that should not be removed even though they are removed locally:
// there was a move to an invalid destination and now the source should be restored
//
// This applies recursively to subdirectories.
// All entries should have a trailing slash (even files), so lookup with
// lowerBound() is reliable.
//
// needs to be sorted
std::set<QString> _forbiddenDeletes;
/** Returns whether the db-path has been renamed locally or on the remote.
*
* Useful for avoiding processing of items that have already been claimed in
* a rename (would otherwise be discovered as deletions).
*/
bool isRenamed(const QString &p) const { return _renamedItemsLocal.contains(p) || _renamedItemsRemote.contains(p); }
int _currentlyActiveJobs = 0;
// both must contain a sorted list
std::set<QString> _selectiveSyncBlackList;
std::set<QString> _selectiveSyncWhiteList;
void scheduleMoreJobs();
bool isInSelectiveSyncBlackList(const QString &path) const;
/** Given an original path, return the target path obtained when renaming is done.
*
* Note that it only considers parent directory renames. So if A/B got renamed to C/D,
* checking A/B/file would yield C/D/file, but checking A/B would yield A/B.
*/
QString adjustRenamedPath(const QString &original, SyncFileItem::Direction) const;
/** If the db-path is scheduled for deletion, abort it.
*
* Check if there is already a job to delete that item:
* If that's not the case, return { false, QByteArray() }.
* If there is such a job, cancel that job and return true and the old etag.
*
* Used when having detected a rename: The rename source may have been
* discovered before and would have looked like a delete.
*
* See _deletedItem and _queuedDeletedDirectories.
*/
QPair<bool, QString> findAndCancelDeletedJob(const QString &originalPath);
public:
// input
DiscoveryPhase(const AccountPtr &account, const SyncOptions &options, const QUrl &baseUrl, QObject *parent = nullptr)
: QObject(parent)
, _account(account)
, _syncOptions(options)
, _baseUrl(baseUrl)
{
}
AccountPtr _account;
const SyncOptions _syncOptions;
const QUrl _baseUrl;
QString _localDir; // absolute path to the local directory. ends with '/'
QString _remoteFolder; // remote folder, ends with '/'
SyncJournalDb *_statedb;
ExcludedFiles *_excludes;
QRegularExpression _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles
QStringList _serverBlacklistedFiles; // The blacklist from the capabilities
bool _ignoreHiddenFiles = false;
std::function<bool(const QString &)> _shouldDiscoverLocaly;
void startJob(ProcessDirectoryJob *);
void setSelectiveSyncBlackList(const QSet<QString> &list);
void setSelectiveSyncWhiteList(const QSet<QString> &list);
// output
bool _anotherSyncNeeded = false;
Q_SIGNALS:
void fatalError(const QString &errorString);
void itemDiscovered(const SyncFileItemPtr &item);
void finished();
/** For excluded items that don't show up in itemDiscovered()
*
* The path is relative to the sync folder, similar to item->_file
*/
void silentlyExcluded(const QString &folderPath);
void excluded(const QString &folderPath);
};
/// Implementation of DiscoveryPhase::adjustRenamedPath
QString adjustRenamedPath(const QHash<QString, QString> &renamedItems, const QString &original);
}
+150
View File
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "libsync/discoveryremoteinfo.h"
#include "libsync/common/checksums.h"
#include <QApplication>
using namespace OCC;
using namespace Qt::Literals::StringLiterals;
class OCC::RemoteInfoData : public QSharedData
{
public:
RemoteInfoData() = default;
RemoteInfoData(const QString &fileName, const QMap<QString, QString> &map)
{
QStringList errors;
_name = fileName.mid(fileName.lastIndexOf('/'_L1) + 1);
if (auto it = Utility::optionalFind(map, "resourcetype"_L1)) {
_isDirectory = it->value().contains(QStringLiteral("collection"));
}
if (auto it = Utility::optionalFind(map, "getlastmodified"_L1)) {
const auto date = Utility::parseRFC1123Date(**it);
Q_ASSERT(date.isValid());
_modtime = date.toSecsSinceEpoch();
}
if (_isDirectory) {
_size = 0;
} else {
if (auto it = Utility::optionalFind(map, "getcontentlength"_L1)) {
// See #4573, sometimes negative size values are returned
_size = std::max<int64_t>(0, it->value().toLongLong());
} else {
errors.append(u"size"_s);
}
}
if (auto it = Utility::optionalFind(map, "getetag"_L1)) {
_etag = Utility::normalizeEtag(it->value());
}
if (auto it = Utility::optionalFind(map, "id"_L1)) {
_fileId = it->value().toUtf8();
}
if (auto it = Utility::optionalFind(map, "checksums"_L1)) {
_checksumHeader = findBestChecksum(it->value().toUtf8());
}
if (auto it = Utility::optionalFind(map, "permissions"_L1)) {
_remotePerm = RemotePermissions::fromServerString(it->value());
}
if (_etag.isEmpty()) {
errors.append(u"etag"_s);
}
if (_fileId.isEmpty()) {
errors.append(u"id"_s);
}
if (_size != 0 && _checksumHeader.isEmpty()) {
errors.append(u"checksum"_s);
}
if (_remotePerm.isNull()) {
errors.append(u"permissions"_s);
}
if (!errors.empty()) {
_error = QApplication::translate("RemoteInfo", "server reported no %1").arg(errors.join(u", "_s));
}
}
QString _name;
QString _etag;
QByteArray _fileId;
QByteArray _checksumHeader;
RemotePermissions _remotePerm;
time_t _modtime = 0;
int64_t _size = 0;
bool _isDirectory = false;
QString _error;
};
RemoteInfo::RemoteInfo()
: d([] {
static QExplicitlySharedDataPointer<RemoteInfoData> nullData{new RemoteInfoData{}};
return nullData;
}())
{
}
RemoteInfo::RemoteInfo(const QString &fileName, const QMap<QString, QString> &map)
: d(new RemoteInfoData(fileName, map))
{
}
RemoteInfo::~RemoteInfo() = default;
RemoteInfo::RemoteInfo(const RemoteInfo &other) = default;
RemoteInfo &RemoteInfo::operator=(const RemoteInfo &other) = default;
QString RemoteInfo::name() const
{
return d->_name;
}
bool RemoteInfo::isDirectory() const
{
return d->_isDirectory;
}
QString RemoteInfo::etag() const
{
return d->_etag;
}
QByteArray RemoteInfo::fileId() const
{
return d->_fileId;
}
QByteArray RemoteInfo::checksumHeader() const
{
return d->_checksumHeader;
}
RemotePermissions RemoteInfo::remotePerm() const
{
return d->_remotePerm;
}
time_t RemoteInfo::modtime() const
{
return d->_modtime;
}
int64_t RemoteInfo::size() const
{
return d->_size;
}
QString RemoteInfo::error() const
{
return d->_error;
}
bool RemoteInfo::isValid() const
{
return !d->_name.isNull();
}
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "libsync/common/remotepermissions.h"
#include <QSharedData>
namespace OCC {
class RemoteInfoData;
/**
* Represent all the meta-data about a file in the server
*/
class QSFERA_SYNC_EXPORT RemoteInfo
{
public:
RemoteInfo();
RemoteInfo(const QString &fileName, const QMap<QString, QString> &map);
~RemoteInfo();
RemoteInfo(const RemoteInfo &other);
RemoteInfo &operator=(const RemoteInfo &other);
void swap(RemoteInfo &other) noexcept { d.swap(other.d); }
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(RemoteInfo);
/** FileName of the entry (this does not contain any directory or path, just the plain name */
QString name() const;
bool isDirectory() const;
QString etag() const;
QByteArray fileId() const;
QByteArray checksumHeader() const;
RemotePermissions remotePerm() const;
time_t modtime() const;
int64_t size() const;
QString error() const;
bool isValid() const;
private:
QExplicitlySharedDataPointer<RemoteInfoData> d;
};
}
Q_DECLARE_SHARED(OCC::RemoteInfo);
+359
View File
@@ -0,0 +1,359 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "filesystem.h"
#include "common/asserts.h"
#include "common/utility.h"
#include "libsync/discoveryinfo.h"
#include "libsync/xattr.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include "csync.h"
#include "syncfileitem.h"
#include <sys/stat.h>
#ifdef Q_OS_WIN32
#include "common/utility_win.h"
#include <winsock2.h>
#endif
using namespace Qt::Literals::StringLiterals;
namespace {
static constexpr unsigned MaxValueSize = 1023; // This is without a terminating NUL character
} // anonymous namespace
namespace OCC {
bool FileSystem::fileEquals(const QString &fn1, const QString &fn2)
{
// compare two files with given filename and return true if they have the same content
QFile f1(fn1);
QFile f2(fn2);
if (!f1.open(QIODevice::ReadOnly) || !f2.open(QIODevice::ReadOnly)) {
qCWarning(lcFileSystem) << u"fileEquals: Failed to open " << fn1 << u"or" << fn2;
return false;
}
if (getSize(QFileInfo{fn1}) != getSize(QFileInfo{fn2})) {
return false;
}
const int BufferSize = 16 * 1024;
QByteArray buffer1(BufferSize, 0);
QByteArray buffer2(BufferSize, 0);
// the files have the same size, compare all of it
while (!f1.atEnd()) {
f1.read(buffer1.data(), BufferSize);
f2.read(buffer2.data(), BufferSize);
if (buffer1 != buffer2) {
return false;
}
};
return true;
}
time_t FileSystem::fileTimeToTime_t(std::filesystem::file_time_type fileTime)
{
#ifdef HAS_CLOCK_CAST
return std::chrono::system_clock::to_time_t(std::chrono::clock_cast<std::chrono::system_clock>(fileTime));
#else
const auto systemTime = std::chrono::time_point_cast<std::chrono::system_clock::duration>(std::chrono::file_clock::to_sys(fileTime));
return std::chrono::system_clock::to_time_t(systemTime);
#endif
}
std::filesystem::file_time_type FileSystem::time_tToFileTime(time_t fileTime)
{
#ifdef HAS_CLOCK_CAST
return std::chrono::clock_cast<std::chrono::file_clock>(std::chrono::system_clock::from_time_t(fileTime));
#else
return std::chrono::file_clock::from_sys(std::chrono::system_clock::from_time_t(fileTime));
#endif
}
time_t FileSystem::getModTime(const std::filesystem::path &filename)
{
std::error_code rc;
const auto fileTime = std::filesystem::last_write_time(filename, rc);
if (rc) {
Q_ASSERT(!rc);
return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
} else {
return fileTimeToTime_t(fileTime);
}
}
bool FileSystem::setModTime(const std::filesystem::path &filename, time_t modTime)
{
std::error_code rc;
std::filesystem::last_write_time(filename, time_tToFileTime(modTime), rc);
if (rc) {
qCWarning(lcFileSystem) << u"Error setting mtime for" << filename << u"failed: rc" << rc.value() << u", error message:" << rc.message();
Q_ASSERT(!rc);
return false;
}
return true;
}
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncFileItem(const SyncFileItem *const item)
{
return {.size = item->_size, .mtime = item->_modtime, .inode = (item->_inode == 0 ? std::optional<quint64>{} : item->_inode), .type = item->_type};
}
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncFileItemPrevious(const SyncFileItem *const item)
{
return {.size = item->_previousSize, .mtime = item->_previousModtime, .type = item->_type};
}
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncJournalFileRecord(const SyncJournalFileRecord &record)
{
if (!record.isValid()) {
return {};
}
return {.size = record.size(), .mtime = record.modtime(), .inode = record.inode(), .type = record.type()};
}
bool FileSystem::fileChanged(const std::filesystem::path &path, const FileChangedInfo &previousInfo)
{
const auto dirent = std::filesystem::directory_entry{path};
if (!dirent.exists()) {
if (previousInfo.mtime.has_value()) {
qCDebug(lcFileSystem) << path.native() << u"was removed";
return true;
} else {
// the file didn't exist and doesn't exist
Q_ASSERT(false); // pointless call
return false;
}
}
const auto type = LocalInfo::typeFromDirectoryEntry(dirent);
if (previousInfo.type != ItemTypeUnsupported) {
// only check for dir and file, as virtual files are irrelevant here
if (previousInfo.type == ItemTypeDirectory && type == ItemTypeFile) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from dir to file";
return true;
}
if (previousInfo.type == ItemTypeFile && type == ItemTypeDirectory) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from file to dir";
return true;
}
}
const auto info = LocalInfo(dirent, type);
if (previousInfo.inode.has_value() && previousInfo.inode.value() != info.inode()) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: inode" << previousInfo.inode.value() << u"<-->" << info.inode();
return true;
}
if (info.isDirectory()) {
if (previousInfo.size != 0) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from file to dir";
return true;
}
} else if (info.size() != previousInfo.size) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: size: " << previousInfo.size << u"<->" << info.size();
return true;
}
if (info.modtime() != previousInfo.mtime) {
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: mtime: " << previousInfo.mtime << u"<->" << info.modtime();
return true;
}
return false;
}
std::filesystem::path FileSystem::canonicalPath(const std::filesystem::path &p)
{
std::error_code ec;
if (!std::filesystem::exists(p, ec) && !ec) {
const auto normalized = p.lexically_normal();
const auto parentPath = normalized.parent_path();
// last invocation will return /
if (parentPath == p) {
return p;
}
if (normalized.filename().empty()) {
return canonicalPath(parentPath);
} else {
return canonicalPath(parentPath) / normalized.filename();
}
}
if (ec) {
qCWarning(lcFileSystem) << "Failed to check existence of path:" << p << ec.message();
}
const auto out = std::filesystem::canonical(p, ec);
if (ec) {
qCWarning(lcFileSystem) << "Failed to canonicalize path:" << p << ec.message();
return p;
}
#ifdef Q_OS_WIN
// std::filesystem::canonical removes the ucn prefix
const auto ucn = std::wstring(LR"(\\?\)");
Q_ASSERT(!out.native().starts_with(ucn));
return std::filesystem::path(ucn + out.native());
#else
return out;
#endif
}
QString FileSystem::canonicalPath(const QString &p)
{
// clean path to normalize path back to Qt form
return FileSystem::fromFilesystemPath(canonicalPath(FileSystem::toFilesystemPath(p)));
}
qint64 FileSystem::getSize(const std::filesystem::path &filename)
{
std::error_code ec;
const quint64 size = std::filesystem::file_size(filename, ec);
if (ec) {
if (!std::filesystem::is_directory(filename)) {
qCCritical(lcFileSystem) << u"Error getting size for" << filename << ec.value() << ec.message();
} else {
qCWarning(lcFileSystem) << u"Called getFileSize on a directory";
Q_ASSERT(false);
}
return 0;
}
return size;
}
// Code inspired from Qt5's QDir::removeRecursively
bool FileSystem::removeRecursively(const QString &path,
RemoveEntryList *success,
RemoveEntryList *locked,
RemoveErrorList *errors)
{
bool allRemoved = true;
QDirIterator di(path, QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
QString removeError;
while (di.hasNext()) {
di.next();
const QFileInfo &fi = di.fileInfo();
// The use of isSymLink here is okay:
// we never want to go into this branch for .lnk files
const bool isDir = fi.isDir() && !fi.isSymLink() && !FileSystem::isJunction(fi.absoluteFilePath());
if (isDir) {
allRemoved &= removeRecursively(path + QLatin1Char('/') + di.fileName(), success, locked, errors); // recursive
} else {
if (FileSystem::isFileLocked(di.filePath(), FileSystem::LockMode::Exclusive)) {
locked->push_back({ di.filePath(), isDir });
allRemoved = false;
} else {
if (FileSystem::remove(di.filePath(), &removeError)) {
success->push_back({ di.filePath(), isDir });
} else {
errors->push_back({ { di.filePath(), isDir }, removeError });
qCWarning(lcFileSystem) << u"Error removing " << di.filePath() << ':' << removeError;
allRemoved = false;
}
}
}
}
if (allRemoved) {
allRemoved = QDir().rmdir(path);
if (allRemoved) {
success->push_back({ path, true });
} else {
errors->push_back({ { path, true }, QCoreApplication::translate("FileSystem", "Could not remove folder") });
qCWarning(lcFileSystem) << u"Error removing folder" << path;
}
}
return allRemoved;
}
std::optional<uint64_t> FileSystem::getInode(const std::filesystem::path &filename)
{
const LocalInfo info(filename);
Q_ASSERT(info.isValid());
if (!info.isValid()) {
return {};
}
return info.inode();
}
std::optional<QString> FileSystem::Tags::get(const QString &path, const QString &key)
{
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
QString platformKey = key;
if (Utility::isLinux()) {
platformKey = QStringLiteral("user.") + platformKey;
}
if (const auto d = Xattr::getxattr(toFilesystemPath(path), platformKey)) {
return QString::fromUtf8(*d);
}
#elif defined(Q_OS_WIN)
QFile file(QStringLiteral("%1:%2").arg(path, key));
if (file.open(QIODevice::ReadOnly)) {
return QString::fromUtf8(file.readAll());
}
#endif // Q_OS_MAC || Q_OS_LINUX
return {};
}
OCC::Result<void, QString> FileSystem::Tags::set(const QString &path, const QString &key, const QString &value)
{
OC_ASSERT(value.size() < MaxValueSize)
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
QString platformKey = key;
if (Utility::isLinux()) {
platformKey = QStringLiteral("user.") + platformKey;
}
return Xattr::setxattr(toFilesystemPath(path), platformKey, value.toUtf8());
#elif defined(Q_OS_WIN)
QFile file(QStringLiteral("%1:%2").arg(path, key));
if (!file.open(QIODevice::WriteOnly)) {
return file.errorString();
}
const auto data = value.toUtf8();
auto bytesWritten = file.write(data);
if (bytesWritten != data.size()) {
return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(data.size()));
}
return {};
#else
return u"Not implemented"_s;
#endif // Q_OS_MAC || Q_OS_LINUX
}
OCC::Result<void, QString> FileSystem::Tags::remove(const QString &path, const QString &key)
{
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
QString platformKey = key;
if (Utility::isLinux()) {
platformKey = QStringLiteral("user.%1").arg(platformKey);
}
return Xattr::removexattr(toFilesystemPath(path), platformKey);
#elif defined(Q_OS_WIN)
const auto fsPath = toFilesystemPath(u"%1:%2"_s.arg(path, key));
std::error_code fileError;
std::filesystem::remove(fsPath, fileError);
if (fileError) {
qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << fsPath << u"error:" << fileError.message();
return QString::fromStdString(fileError.message());
}
return {};
#else
return u"Not implemented"_s;
#endif // Q_OS_MAC || Q_OS_LINUX
}
} // namespace OCC
+146
View File
@@ -0,0 +1,146 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "qsferasynclib.h"
// Chain in the base include and extend the namespace
#include "common/filesystembase.h"
#include "common/result.h"
#include "libsync/csync.h"
class QFile;
namespace OCC {
class SyncJournal;
class SyncFileItem;
/**
* \addtogroup libsync
* @{
*/
/**
* @brief This file contains file system helper
*/
namespace FileSystem {
/**
* @brief compare two files with given filename and return true if they have the same content
*/
bool fileEquals(const QString &fn1, const QString &fn2);
QSFERA_SYNC_EXPORT time_t fileTimeToTime_t(std::filesystem::file_time_type fileTime);
QSFERA_SYNC_EXPORT std::filesystem::file_time_type time_tToFileTime(time_t fileTime);
/**
* @brief Get the mtime for a filepath
*
* Use this over QFileInfo::lastModified() to avoid timezone related bugs. See
* owncloud/core#9781 for details.
*/
time_t QSFERA_SYNC_EXPORT getModTime(const std::filesystem::path &filename);
inline time_t getModTime(const QString &filename)
{
return getModTime(toFilesystemPath(filename));
}
bool QSFERA_SYNC_EXPORT setModTime(const std::filesystem::path &filename, time_t modTime);
inline time_t setModTime(const QString &filename, time_t modTime)
{
return setModTime(toFilesystemPath(filename), modTime);
}
/**
* @brief Get the size for a file
*
* Use this over QFileInfo::size() to avoid bugs with lnk files on Windows.
* See https://bugreports.qt.io/browse/QTBUG-24831.
*/
qint64 QSFERA_SYNC_EXPORT getSize(const std::filesystem::path &filename);
inline qint64 getSize(const QFileInfo &info)
{
return getSize(toFilesystemPath(info.absoluteFilePath()));
}
/**
* @brief Retrieve a file inode with csync
*/
[[nodiscard]] std::optional<uint64_t> QSFERA_SYNC_EXPORT getInode(const std::filesystem::path &filename);
[[nodiscard]] inline auto getInode(const QString &filename)
{
return getInode(toFilesystemPath(filename));
}
/**
* @brief Check if \a fileName has changed given previous size and mtime
*
* Nonexisting files are covered through mtime: they have an previousMtime of -1.
*
* @return true if the file's mtime or size are not what is expected.
*/
struct FileChangedInfo
{
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncFileItem(const SyncFileItem *const item);
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncFileItemPrevious(const SyncFileItem *const item);
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncJournalFileRecord(const SyncJournalFileRecord &record);
qint64 size = {};
std::optional<time_t> mtime = {};
std::optional<quint64> inode = {};
CSyncEnums::ItemType type = CSyncEnums::ItemTypeUnsupported;
};
bool QSFERA_SYNC_EXPORT fileChanged(const std::filesystem::path &path, const FileChangedInfo &previousInfo);
// canonicalPath returns an empty string if the file does not exist.
// This function also works with files that does not exist and resolve the symlinks in the
// parent directories.
std::filesystem::path QSFERA_SYNC_EXPORT canonicalPath(const std::filesystem::path &p);
QString QSFERA_SYNC_EXPORT canonicalPath(const QString &p);
struct RemoveEntry
{
const QString path;
const bool isDir;
};
struct RemoveError
{
const RemoveEntry entry;
const QString error;
};
using RemoveEntryList = std::vector<RemoveEntry>;
using RemoveErrorList = std::vector<RemoveError>;
/**
* Removes a directory and its contents recursively
*
* Returns true if all removes succeeded.
*/
bool QSFERA_SYNC_EXPORT removeRecursively(const QString &path, RemoveEntryList *success, RemoveEntryList *locked, RemoveErrorList *errors);
namespace Tags {
std::optional<QString> QSFERA_SYNC_EXPORT get(const QString &path, const QString &key);
OCC::Result<void, QString> QSFERA_SYNC_EXPORT set(const QString &path, const QString &key, const QString &value);
OCC::Result<void, QString> QSFERA_SYNC_EXPORT remove(const QString &path, const QString &key);
}
}
/** @} */
}
+55
View File
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "libsync/globalconfig.h"
#include "config.h"
#include "libsync/theme.h"
#include <QSettings>
using namespace OCC;
QUrl GlobalConfig::serverUrl()
{
const auto configuredUrl = getValue("Wizard/ServerUrl"
#ifdef APPLICATION_DEFAULT_SERVER_URL
,
QUrl(QStringLiteral(APPLICATION_DEFAULT_SERVER_URL))
#endif
);
return configuredUrl.toUrl();
}
QVariant GlobalConfig::getValue(QAnyStringView param, const QVariant &defaultValue)
{
static const QSettings systemSettings = {
#ifdef Q_OS_MAC
QStringLiteral("/Library/Preferences/%1.plist").arg(Theme::instance()->orgDomainName()), QSettings::NativeFormat
#elif defined(Q_OS_UNIX)
QStringLiteral("/etc/%1/%1.conf").arg(Theme::instance()->appName()), QSettings::NativeFormat
#elif defined(Q_OS_WIN)
QStringLiteral(R"(HKEY_LOCAL_MACHINE\Software\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()), QSettings::NativeFormat
#else
#error "Unsupported platform"
#endif
};
return systemSettings.value(param, defaultValue);
}
#ifdef Q_OS_WIN
QVariant GlobalConfig::getPolicySetting(QAnyStringView setting, const QVariant &defaultValue)
{
// check for policies first and return immediately if a value is found.
QVariant out = QSettings(QStringLiteral(R"(HKEY_CURRENT_USER\Software\Policies\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()),
QSettings::NativeFormat)
.value(setting);
if (!out.isValid()) {
out = QSettings(QStringLiteral(R"(HKEY_LOCAL_MACHINE\Software\Policies\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()),
QSettings::NativeFormat)
.value(setting, defaultValue);
}
return out;
}
#endif
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "libsync/qsferasynclib.h"
#include <QVariant>
#include <QtQmlIntegration/QtQmlIntegration>
namespace OCC {
class QSFERA_SYNC_EXPORT GlobalConfig : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl serverUrl READ serverUrl CONSTANT)
QML_ELEMENT
QML_SINGLETON
public:
using QObject::QObject;
static QVariant getValue(QAnyStringView param, const QVariant &defaultValue = {});
#ifdef Q_OS_WIN
static QVariant getPolicySetting(QAnyStringView policy, const QVariant &defaultValue = {});
#endif
static QUrl serverUrl();
};
}
@@ -0,0 +1,6 @@
target_sources(libsync PRIVATE
jobs/drives.cpp
space.cpp
spacesmanager.cpp)
target_include_directories(libsync PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
@@ -0,0 +1,49 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "drives.h"
#include "account.h"
#include <OAICollection_of_drives.h>
#include <OAIDrive.h>
using namespace OCC;
using namespace GraphApi;
namespace {
const auto mountpointC = QLatin1String("mountpoint");
}
Drives::Drives(const AccountPtr &account, QObject *parent)
: JsonJob(account, account->url(), QStringLiteral("/graph/v1.0/me/drives"), "GET", {}, parent)
{
}
Drives::~Drives() { }
const QList<OpenAPI::OAIDrive> &Drives::drives() const
{
if (_drives.isEmpty() && parseError().error == QJsonParseError::NoError) {
OpenAPI::OAICollection_of_drives drives;
drives.fromJsonObject(data());
_drives = drives.getValue();
// At the moment we don't support mountpoints but use the Share Jail
_drives.erase(
std::remove_if(_drives.begin(), _drives.end(), [](const OpenAPI::OAIDrive &it) { return it.getDriveType() == mountpointC; }), _drives.end());
}
return _drives;
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "networkjobs/jsonjob.h"
#include "qsferasynclib.h"
#include <OAIDrive.h>
namespace OCC {
namespace GraphApi {
class QSFERA_SYNC_EXPORT Drives : public JsonJob
{
Q_OBJECT
public:
Drives(const AccountPtr &account, QObject *parent = nullptr);
~Drives();
const QList<OpenAPI::OAIDrive> &drives() const;
private:
mutable QList<OpenAPI::OAIDrive> _drives;
};
}
}
+149
View File
@@ -0,0 +1,149 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "space.h"
#include "fonticon.h"
#include "libsync/account.h"
#include "libsync/graphapi/spacesmanager.h"
#include "libsync/networkjobs.h"
#include "libsync/networkjobs/resources.h"
#include "resources/resources.h"
using namespace OCC;
using namespace GraphApi;
namespace {
const auto personalC = QLatin1String("personal");
// https://github.com/cs3org/reva/blob/0cde0a3735beaa14ebdfd8988c3eb77b3c2ab0e6/pkg/utils/utils.go#L56-L59
const auto sharesIdC = QLatin1String("a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668");
}
Space::Space(SpacesManager *spacesManager, const OpenAPI::OAIDrive &drive)
: QObject(spacesManager)
, _spaceManager(spacesManager)
, _image(new SpaceImage(this))
{
setDrive(drive);
connect(_image, &SpaceImage::imageChanged, this, &Space::imageChanged);
}
OpenAPI::OAIDrive Space::drive() const
{
return _drive;
}
void Space::setDrive(const OpenAPI::OAIDrive &drive)
{
_drive = drive;
_image->update();
}
SpaceImage::SpaceImage(Space *space)
: QObject(space)
, _space(space)
{
}
QIcon SpaceImage::image() const
{
if (_image.isNull()) {
// remix icons is not compatible with nerdfonts so the preview will be broken
if (_space->drive().getDriveType() == personalC) {
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
} else if (_space->drive().getId() == sharesIdC) {
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
}
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
}
return _image;
}
QUrl SpaceImage::qmlImageUrl() const
{
// while the icon is being loaded we provide a different url, qml caches the icon based on the url
if (_fetched) {
return QUrl(QStringLiteral("image://space/%1/%2").arg(etag(), _space->id()));
} else {
return QUrl(QStringLiteral("image://space/invalid/%1").arg(_space->id()));
}
}
void SpaceImage::update()
{
const auto &special = _space->drive().getSpecial();
const auto img = std::find_if(special.cbegin(), special.cend(), [](const auto &it) { return it.getSpecialFolder().getName() == QLatin1String("image"); });
if (img != special.cend()) {
_fetched = false;
_url = QUrl(img->getWebDavUrl());
_etag = Utility::normalizeEtag(img->getETag());
auto job = _space->_spaceManager->account()->resourcesCache()->makeGetJob(_url, {}, _space);
QObject::connect(job, &SimpleNetworkJob::finishedSignal, _space, [job, this] {
_fetched = true;
if (job->httpStatusCode() == 200) {
_image = job->asIcon();
Q_EMIT imageChanged();
}
});
job->start();
} else {
// nothing to fetch
_fetched = true;
}
}
QString Space::displayName() const
{
if (_drive.getDriveType() == personalC) {
return tr("Personal");
} else if (_drive.getId() == sharesIdC) {
// don't call it ShareJail
return tr("Shares");
}
return _drive.getName();
}
uint32_t Space::priority() const
{
if (_drive.getDriveType() == personalC) {
return 100;
} else if (_drive.getId() == sharesIdC) {
return 50;
}
return 0;
}
bool Space::disabled() const
{
// this is how disabled spaces are represented in the graph API
return _drive.getRoot().getDeleted().getState() == QLatin1String("trashed");
}
SpaceImage *Space::image() const
{
return _image;
}
QString Space::id() const
{
return _drive.getRoot().getId();
}
QUrl Space::webdavUrl() const
{
return QUrl(_drive.getRoot().getWebDavUrl());
}
+111
View File
@@ -0,0 +1,111 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "qsferasynclib.h"
#include "libsync/accountfwd.h"
#include <OAIDrive.h>
#include <QIcon>
#include <QtQmlIntegration>
namespace OCC {
namespace GraphApi {
class SpacesManager;
class Space;
class QSFERA_SYNC_EXPORT SpaceImage : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl qmlImageUrl READ qmlImageUrl NOTIFY imageChanged)
QML_ELEMENT
QML_UNCREATABLE("C++ only")
public:
SpaceImage(Space *space);
[[nodiscard]] QUrl url() const { return _url; }
[[nodiscard]] QString etag() const { return _etag; }
[[nodiscard]] QIcon image() const;
[[nodiscard]] QUrl qmlImageUrl() const;
Q_SIGNALS:
void imageChanged();
private:
void update();
QUrl _url;
QString _etag;
QIcon _image;
Space *_space = nullptr;
bool _fetched = false;
friend class Space;
};
class QSFERA_SYNC_EXPORT Space : public QObject
{
Q_OBJECT
Q_PROPERTY(SpaceImage *image READ image NOTIFY imageChanged)
QML_ELEMENT
QML_UNCREATABLE("Spaces can only be created by the SpacesManager")
public:
/***
* Returns the display name of the drive.
* This is identical to drive.getName() for most drives.
* Exceptions: Personal spaces
*/
QString displayName() const;
QString id() const;
OpenAPI::OAIDrive drive() const;
/***
* Asign a priority to a drive, used for sorting
*/
uint32_t priority() const;
/**
* Whether a drive object has been deleted.
*/
bool disabled() const;
SpaceImage *image() const;
QUrl webdavUrl() const;
Q_SIGNALS:
void imageChanged();
private:
Space(SpacesManager *spaceManager, const OpenAPI::OAIDrive &drive);
void setDrive(const OpenAPI::OAIDrive &drive);
SpacesManager *_spaceManager;
OpenAPI::OAIDrive _drive;
SpaceImage *_image;
friend class SpacesManager;
friend class SpaceImage;
};
} // OCC
} // GraphApi
@@ -0,0 +1,115 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "spacesmanager.h"
#include "libsync/account.h"
#include "libsync/creds/abstractcredentials.h"
#include "libsync/graphapi/jobs/drives.h"
#include <QTimer>
#include <chrono>
using namespace std::chrono_literals;
using namespace OCC;
using namespace GraphApi;
namespace {
constexpr auto refreshTimeoutC = 30s;
}
SpacesManager::SpacesManager(Account *parent)
: QObject(parent)
, _account(parent)
, _refreshTimer(new QTimer(this))
{
_refreshTimer->setInterval(refreshTimeoutC);
// the timer will be restarted once we received drives data
_refreshTimer->setSingleShot(true);
connect(_refreshTimer, &QTimer::timeout, this, &SpacesManager::refresh);
connect(_account, &Account::credentialsFetched, this, &SpacesManager::refresh);
// legacy signal which is going to be removed in 5.0
connect(_account, &Account::credentialsAsked, this, &SpacesManager::refresh);
}
void SpacesManager::refresh()
{
if (!OC_ENSURE(_account->accessManager())) {
return;
}
if (!_account->credentials()->ready()) {
return;
}
// TODO: leak the job until we fixed the onwership https://github.com/owncloud/client/issues/11203
auto drivesJob = new Drives(_account->sharedFromThis(), nullptr);
drivesJob->setTimeout(refreshTimeoutC);
connect(drivesJob, &Drives::finishedSignal, this, [drivesJob, this] {
drivesJob->deleteLater();
if (drivesJob->httpStatusCode() == 200) {
auto oldKeys = _spacesMap.keys();
for (const auto &dr : drivesJob->drives()) {
auto *space = this->space(dr.getId());
oldKeys.removeAll(dr.getId());
if (!space) {
space = new Space(this, dr);
_spacesMap.insert(dr.getId(), space);
} else {
space->setDrive(dr);
}
Q_EMIT spaceChanged(space);
}
for (const QString &id : oldKeys) {
auto *oldSpace = _spacesMap.take(id);
oldSpace->deleteLater();
}
if (!_ready) {
_ready = true;
Q_EMIT ready();
}
}
Q_EMIT updated();
_refreshTimer->start();
});
_refreshTimer->stop();
drivesJob->start();
}
Space *SpacesManager::space(const QString &id) const
{
return _spacesMap.value(id);
}
Account *SpacesManager::account() const
{
return _account;
}
QVector<Space *> SpacesManager::spaces() const
{
return {_spacesMap.begin(), _spacesMap.end()};
}
void SpacesManager::checkReady()
{
// see constructor for calls to refresh
if (_ready) {
Q_EMIT ready();
} else {
refresh();
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#pragma once
#include "qsferasynclib.h"
#include "libsync/accountfwd.h"
#include "libsync/graphapi/space.h"
#include <OAIDrive.h>
#include <algorithm>
#include <QFuture>
class QTimer;
namespace OCC {
namespace GraphApi {
class QSFERA_SYNC_EXPORT SpacesManager : public QObject
{
Q_OBJECT
public:
SpacesManager(Account *parent);
Space *space(const QString &id) const;
QVector<Space *> spaces() const;
Account *account() const;
/**
* Only relevant during bootstraping or when disconnected
*/
void checkReady();
Q_SIGNALS:
void spaceChanged(Space *space) const;
void updated();
void ready() const;
private:
void refresh();
Account *_account;
QTimer *_refreshTimer;
QMap<QString, Space *> _spacesMap;
bool _ready = false;
};
}
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License
* for more details.
*/
#include "httplogger.h"
#include "common/chronoelapsedtimer.h"
#include <QBuffer>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLoggingCategory>
#include <QPointer>
#include <QRegularExpression>
#include <memory>
using namespace std::chrono;
using namespace Qt::Literals::StringLiterals;
namespace {
Q_LOGGING_CATEGORY(lcNetworkHttp, "sync.httplogger", QtWarningMsg)
const qint64 PeekSize = 1024 * 1024;
bool isTextBody(const QString &s)
{
static const QRegularExpression regexp(QStringLiteral("^(text/.*?|(application/(xml|.*?json|x-www-form-urlencoded)(;|$)))"));
return regexp.match(s).hasMatch();
}
struct HttpContext
{
HttpContext(const QNetworkRequest &request)
: originalUrl(request.url().toString())
, lastUrl(request.url())
, id(QString::fromUtf8(request.rawHeader(QByteArrayLiteral("X-Request-ID"))))
{
}
void addRedirect(const QUrl &url)
{
lastUrl = url;
redirectUrls.append(url.toString());
}
const QString originalUrl;
QUrl lastUrl;
QStringList redirectUrls;
const QString id;
OCC::Utility::ChronoElapsedTimer timer;
bool send = false;
bool receivedBeforeSent = false;
};
void logHttp(const QByteArray &verb, HttpContext *ctx, QJsonObject &&header, QIODevice *device, bool cached = false)
{
static const bool redact = !qEnvironmentVariableIsSet("QSFERA_HTTPLOGGER_NO_REDACT");
const auto reply = qobject_cast<QNetworkReply *>(device);
const auto contentLength = device ? device->size() : 0;
if (redact) {
const auto authKey = "authorization"_L1;
const QString auth = header.value(authKey).toString();
if (!auth.isEmpty()) {
header.insert(authKey, auth.startsWith(QStringLiteral("Bearer ")) ? QStringLiteral("Bearer [redacted]") : QStringLiteral("Basic [redacted]"));
}
}
QJsonObject info{{QStringLiteral("method"), QString::fromUtf8(verb)}, {QStringLiteral("id"), ctx->id}, {QStringLiteral("url"), ctx->originalUrl}};
if (!ctx->redirectUrls.isEmpty()) {
info.insert(QStringLiteral("redirects"), QJsonArray::fromStringList(ctx->redirectUrls));
}
if (reply) {
// respond
QJsonObject replyInfo{{QStringLiteral("status"), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()},
{QStringLiteral("cached"), reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()},
// downcast to int, this is json
{QStringLiteral("duration"), static_cast<int>(duration_cast<milliseconds>(ctx->timer.duration()).count())}, //
{QStringLiteral("durationString"), QDebug::toString(ctx->timer)},
{QStringLiteral("version"),
QStringLiteral("HTTP %1").arg(
reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool() ? QStringLiteral("1.1") : QStringLiteral("2"))}};
if (reply->error() != QNetworkReply::NoError) {
replyInfo.insert(QStringLiteral("error"), reply->errorString());
}
info.insert(QStringLiteral("reply"), replyInfo);
} else {
// request was not actually sent but will be returned from cache
info.insert(QStringLiteral("cached"), cached);
}
QJsonObject body = {{QStringLiteral("length"), contentLength}};
if (contentLength > 0) {
const QString contentType = header.value("content-type"_L1).toString();
if (isTextBody(contentType)) {
if (!device->isOpen()) {
Q_ASSERT(dynamic_cast<QBuffer *>(device));
// should we close it again?
device->open(QIODevice::ReadOnly);
}
Q_ASSERT(device->pos() == 0);
QString data = QString::fromUtf8(device->peek(PeekSize));
if (PeekSize < contentLength)
{
data += QStringLiteral("...(%1 bytes elided)").arg(QString::number(contentLength - PeekSize));
}
body[QStringLiteral("data")] = data;
} else {
body[QStringLiteral("data")] = QStringLiteral("%1 bytes of %2 data").arg(QString::number(contentLength), contentType);
}
}
qCInfo(lcNetworkHttp).noquote() << (reply ? "RESPONSE" : "REQUEST") << ctx->id
<< QJsonDocument{QJsonObject{{reply ? QStringLiteral("response") : QStringLiteral("request"),
QJsonObject{{QStringLiteral("info"), info}, {QStringLiteral("header"), header},
{QStringLiteral("body"), body}}}}}
.toJson(QJsonDocument::Compact);
}
}
namespace OCC {
void HttpLogger::logRequest(QNetworkReply *reply, QNetworkAccessManager::Operation operation, QIODevice *device)
{
if (!lcNetworkHttp().isInfoEnabled()) {
return;
}
auto ctx = std::make_unique<HttpContext>(reply->request());
// device should still exist, lets still use a qpointer to ensure we have valid data
const auto logSend = [ctx = ctx.get(), operation, reply, device = QPointer<QIODevice>(device), deviceRaw = device](bool cached = false) {
Q_ASSERT(!deviceRaw || device);
if (!ctx->send) {
ctx->send = true;
ctx->timer.reset();
} else {
// this is a redirect
if (ctx->lastUrl != reply->url()) {
ctx->addRedirect(reply->url());
} else if (ctx->receivedBeforeSent) {
qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" was sent after we received a result";
} else {
qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" resending";
}
}
const auto request = reply->request();
QJsonObject header;
for (const auto &key : request.rawHeaderList()) {
header[QString::fromUtf8(key)] = QString::fromUtf8(request.rawHeader(key));
}
logHttp(requestVerb(operation, request), ctx, std::move(header), device, cached);
};
QObject::connect(reply, &QNetworkReply::requestSent, reply, logSend, Qt::DirectConnection);
QObject::connect(
reply, &QNetworkReply::finished, reply,
[reply, ctx = std::move(ctx), logSend] {
if (!ctx->send) {
ctx->receivedBeforeSent = true;
logSend(reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool());
}
ctx->timer.stop();
QJsonObject header;
for (const auto &[key, value] : reply->rawHeaderPairs()) {
header[QString::fromUtf8(key)] = QString::fromUtf8(value);
}
logHttp(requestVerb(*reply), ctx.get(), std::move(header), reply);
},
Qt::DirectConnection);
}
QByteArray HttpLogger::requestVerb(QNetworkAccessManager::Operation operation, const QNetworkRequest &request)
{
switch (operation) {
case QNetworkAccessManager::HeadOperation:
return QByteArrayLiteral("HEAD");
case QNetworkAccessManager::GetOperation:
return QByteArrayLiteral("GET");
case QNetworkAccessManager::PutOperation:
return QByteArrayLiteral("PUT");
case QNetworkAccessManager::PostOperation:
return QByteArrayLiteral("POST");
case QNetworkAccessManager::DeleteOperation:
return QByteArrayLiteral("DELETE");
case QNetworkAccessManager::CustomOperation:
return request.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
case QNetworkAccessManager::UnknownOperation:
break;
}
Q_UNREACHABLE();
}
}

Some files were not shown because too many files have changed in this diff Show More