diff options
Diffstat (limited to 'examples/webdavcommon/webdav.cpp')
-rw-r--r-- | examples/webdavcommon/webdav.cpp | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/examples/webdavcommon/webdav.cpp b/examples/webdavcommon/webdav.cpp new file mode 100644 index 0000000..35f7fc2 --- /dev/null +++ b/examples/webdavcommon/webdav.cpp | |||
@@ -0,0 +1,277 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2018 Christian Mollekopf <chrigi_1@fastmail.fm> | ||
3 | * | ||
4 | * This program is free software; you can redistribute it and/or modify | ||
5 | * it under the terms of the GNU General Public License as published by | ||
6 | * the Free Software Foundation; either version 2 of the License, or | ||
7 | * (at your option) any later version. | ||
8 | * | ||
9 | * This program is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | * GNU General Public License for more details. | ||
13 | * | ||
14 | * You should have received a copy of the GNU General Public License | ||
15 | * along with this program; if not, write to the | ||
16 | * Free Software Foundation, Inc., | ||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
18 | */ | ||
19 | |||
20 | #include "webdav.h" | ||
21 | |||
22 | #include "applicationdomaintype.h" | ||
23 | #include "resourceconfig.h" | ||
24 | |||
25 | #include <KDAV2/DavCollectionsFetchJob> | ||
26 | #include <KDAV2/DavItemFetchJob> | ||
27 | #include <KDAV2/DavItemsListJob> | ||
28 | #include <KDAV2/EtagCache> | ||
29 | |||
30 | #include <QNetworkReply> | ||
31 | |||
32 | static int translateDavError(KJob *job) | ||
33 | { | ||
34 | using Sink::ApplicationDomain::ErrorCode; | ||
35 | |||
36 | const int responseCode = dynamic_cast<KDAV2::DavJobBase *>(job)->latestResponseCode(); | ||
37 | |||
38 | switch (responseCode) { | ||
39 | case QNetworkReply::HostNotFoundError: | ||
40 | return ErrorCode::NoServerError; | ||
41 | // Since we don't login we will just not have the necessary permissions ot view the object | ||
42 | case QNetworkReply::OperationCanceledError: | ||
43 | return ErrorCode::LoginError; | ||
44 | } | ||
45 | return ErrorCode::UnknownError; | ||
46 | } | ||
47 | |||
48 | static KAsync::Job<void> runJob(QSharedPointer<KJob> job) | ||
49 | { | ||
50 | return KAsync::start<void>([job(std::move(job))] { | ||
51 | if (job->exec()) { | ||
52 | SinkTrace() << "Job exec success"; | ||
53 | } else { | ||
54 | SinkTrace() << "Job exec failure"; | ||
55 | } | ||
56 | }); | ||
57 | |||
58 | // For some reason, this code doesn't work | ||
59 | |||
60 | /* | ||
61 | return KAsync::start<void>([job](KAsync::Future<void> &future) { | ||
62 | QObject::connect(job, &KJob::result, [&future](KJob *job) { | ||
63 | SinkTrace() << "Job done: " << job->metaObject()->className(); | ||
64 | if (job->error()) { | ||
65 | SinkWarning() | ||
66 | << "Job failed: " << job->errorString() << job->metaObject()->className() | ||
67 | << job->error() << static_cast<KDAV2::DavJobBase *>(job)->latestResponseCode(); | ||
68 | future.setError(translateDavError(job), job->errorString()); | ||
69 | } else { | ||
70 | future.setFinished(); | ||
71 | } | ||
72 | }); | ||
73 | SinkTrace() << "Starting job: " << job->metaObject()->className(); | ||
74 | job->start(); | ||
75 | }); | ||
76 | */ | ||
77 | } | ||
78 | |||
79 | WebDavSynchronizer::WebDavSynchronizer(const Sink::ResourceContext &context, | ||
80 | KDAV2::Protocol protocol, QByteArray collectionName, QByteArray itemName) | ||
81 | : Sink::Synchronizer(context), | ||
82 | protocol(protocol), | ||
83 | collectionName(std::move(collectionName)), | ||
84 | itemName(std::move(itemName)) | ||
85 | { | ||
86 | auto config = ResourceConfig::getConfiguration(context.instanceId()); | ||
87 | |||
88 | server = QUrl::fromUserInput(config.value("server").toString()); | ||
89 | username = config.value("username").toString(); | ||
90 | } | ||
91 | |||
92 | QList<Sink::Synchronizer::SyncRequest> WebDavSynchronizer::getSyncRequests(const Sink::QueryBase &query) | ||
93 | { | ||
94 | QList<Synchronizer::SyncRequest> list; | ||
95 | if (!query.type().isEmpty()) { | ||
96 | // We want to synchronize something specific | ||
97 | list << Synchronizer::SyncRequest{ query }; | ||
98 | } else { | ||
99 | // We want to synchronize everything | ||
100 | |||
101 | // Item synchronization does the collections anyway | ||
102 | // list << Synchronizer::SyncRequest{ Sink::QueryBase(collectionName) }; | ||
103 | list << Synchronizer::SyncRequest{ Sink::QueryBase(itemName) }; | ||
104 | } | ||
105 | return list; | ||
106 | } | ||
107 | |||
108 | KAsync::Job<void> WebDavSynchronizer::synchronizeWithSource(const Sink::QueryBase &query) | ||
109 | { | ||
110 | if (query.type() != collectionName && query.type() != itemName) { | ||
111 | return KAsync::null<void>(); | ||
112 | } | ||
113 | |||
114 | SinkLog() << "Synchronizing" << query.type() << "through WebDAV at:" << serverUrl().url(); | ||
115 | |||
116 | auto collectionsFetchJob = QSharedPointer<KDAV2::DavCollectionsFetchJob>::create(serverUrl()); | ||
117 | |||
118 | auto job = runJob(collectionsFetchJob).then([this, collectionsFetchJob](const KAsync::Error &error) { | ||
119 | if (error) { | ||
120 | SinkWarning() << "Failed to synchronize collections:" << collectionsFetchJob->errorString(); | ||
121 | } else { | ||
122 | updateLocalCollections(collectionsFetchJob->collections()); | ||
123 | } | ||
124 | |||
125 | return collectionsFetchJob->collections(); | ||
126 | }); | ||
127 | |||
128 | if (query.type() == collectionName) { | ||
129 | // Do nothing more | ||
130 | return job; | ||
131 | } else if (query.type() == itemName) { | ||
132 | auto progress = QSharedPointer<int>::create(0); | ||
133 | auto total = QSharedPointer<int>::create(0); | ||
134 | |||
135 | // Will contain the resource Id of all collections to be able to scan | ||
136 | // for collections to be removed. | ||
137 | auto collectionResourceIDs = QSharedPointer<QSet<QByteArray>>::create(); | ||
138 | |||
139 | // Same but for items. | ||
140 | // Quirk: may contain a collection Id (see below) | ||
141 | auto itemsResourceIDs = QSharedPointer<QSet<QByteArray>>::create(); | ||
142 | |||
143 | return job | ||
144 | .serialEach([this, progress(std::move(progress)), total(std::move(total)), collectionResourceIDs, | ||
145 | itemsResourceIDs](const KDAV2::DavCollection &collection) { | ||
146 | auto collectionResourceID = resourceID(collection); | ||
147 | |||
148 | collectionResourceIDs->insert(collectionResourceID); | ||
149 | |||
150 | if (unchanged(collection)) { | ||
151 | SinkTrace() << "Collection unchanged:" << collectionResourceID; | ||
152 | |||
153 | // It seems that doing this prevent the items in the | ||
154 | // collection to be removed when doing scanForRemovals | ||
155 | // below (since the collection is unchanged, we do not go | ||
156 | // through all of its items). | ||
157 | // Behaviour copied from the previous code. | ||
158 | itemsResourceIDs->insert(collectionResourceID); | ||
159 | |||
160 | return KAsync::null<void>(); | ||
161 | } | ||
162 | |||
163 | SinkTrace() << "Syncing collection:" << collectionResourceID; | ||
164 | return synchronizeCollection(collection, progress, total, itemsResourceIDs); | ||
165 | }) | ||
166 | .then([this, collectionResourceIDs(std::move(collectionResourceIDs)), | ||
167 | itemsResourceIDs(std::move(itemsResourceIDs))]() { | ||
168 | scanForRemovals(collectionName, [&collectionResourceIDs](const QByteArray &remoteId) { | ||
169 | return collectionResourceIDs->contains(remoteId); | ||
170 | }); | ||
171 | scanForRemovals(itemName, [&itemsResourceIDs](const QByteArray &remoteId) { | ||
172 | return itemsResourceIDs->contains(remoteId); | ||
173 | }); | ||
174 | }); | ||
175 | } else { | ||
176 | SinkWarning() << "Unknown query type"; | ||
177 | return KAsync::null<void>(); | ||
178 | } | ||
179 | } | ||
180 | |||
181 | KAsync::Job<void> WebDavSynchronizer::synchronizeCollection(const KDAV2::DavCollection &collection, | ||
182 | QSharedPointer<int> progress, QSharedPointer<int> total, | ||
183 | QSharedPointer<QSet<QByteArray>> itemsResourceIDs) | ||
184 | { | ||
185 | auto collectionRid = resourceID(collection); | ||
186 | auto ctag = collection.CTag().toLatin1(); | ||
187 | |||
188 | auto localRid = collectionLocalResourceID(collection); | ||
189 | |||
190 | // The ETag cache is useless here, since `sinkStore()` IS the cache. | ||
191 | auto cache = std::make_shared<KDAV2::EtagCache>(); | ||
192 | auto davItemsListJob = QSharedPointer<KDAV2::DavItemsListJob>::create(collection.url(), std::move(cache)); | ||
193 | |||
194 | return runJob(davItemsListJob) | ||
195 | .then([this, davItemsListJob, total] { | ||
196 | auto items = davItemsListJob->items(); | ||
197 | *total += items.size(); | ||
198 | return KAsync::value(items); | ||
199 | }) | ||
200 | .serialEach([this, collectionRid, localRid, progress(std::move(progress)), total(std::move(total)), | ||
201 | itemsResourceIDs(std::move(itemsResourceIDs))](const KDAV2::DavItem &item) { | ||
202 | auto itemRid = resourceID(item); | ||
203 | |||
204 | itemsResourceIDs->insert(itemRid); | ||
205 | |||
206 | if (unchanged(item)) { | ||
207 | SinkTrace() << "Item unchanged:" << itemRid; | ||
208 | return KAsync::null<void>(); | ||
209 | } | ||
210 | |||
211 | SinkTrace() << "Syncing item:" << itemRid; | ||
212 | return synchronizeItem(item, localRid, progress, total); | ||
213 | }) | ||
214 | .then([this, collectionRid, ctag] { | ||
215 | // Update the local CTag to be able to tell if the collection is unchanged | ||
216 | syncStore().writeValue(collectionRid + "_ctag", ctag); | ||
217 | }); | ||
218 | } | ||
219 | |||
220 | KAsync::Job<void> WebDavSynchronizer::synchronizeItem(const KDAV2::DavItem &item, | ||
221 | const QByteArray &collectionLocalRid, QSharedPointer<int> progress, QSharedPointer<int> total) | ||
222 | { | ||
223 | auto etag = item.etag().toLatin1(); | ||
224 | |||
225 | auto itemFetchJob = QSharedPointer<KDAV2::DavItemFetchJob>::create(item); | ||
226 | return runJob(itemFetchJob) | ||
227 | .then([this, itemFetchJob(std::move(itemFetchJob)), collectionLocalRid] { | ||
228 | auto item = itemFetchJob->item(); | ||
229 | updateLocalItem(item, collectionLocalRid); | ||
230 | return item; | ||
231 | }) | ||
232 | .then([this, etag, progress(std::move(progress)), total(std::move(total))](const KDAV2::DavItem &item) { | ||
233 | // Update the local ETag to be able to tell if the item is unchanged | ||
234 | syncStore().writeValue(resourceID(item) + "_etag", etag); | ||
235 | |||
236 | *progress += 1; | ||
237 | reportProgress(*progress, *total); | ||
238 | if ((*progress % 5) == 0) { | ||
239 | commit(); | ||
240 | } | ||
241 | }); | ||
242 | } | ||
243 | |||
244 | QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavCollection &collection) | ||
245 | { | ||
246 | return collection.url().toDisplayString().toUtf8(); | ||
247 | } | ||
248 | |||
249 | QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavItem &item) | ||
250 | { | ||
251 | return item.url().toDisplayString().toUtf8(); | ||
252 | } | ||
253 | |||
254 | bool WebDavSynchronizer::unchanged(const KDAV2::DavCollection &collection) | ||
255 | { | ||
256 | auto ctag = collection.CTag().toLatin1(); | ||
257 | return ctag == syncStore().readValue(resourceID(collection) + "_ctag"); | ||
258 | } | ||
259 | |||
260 | bool WebDavSynchronizer::unchanged(const KDAV2::DavItem &item) | ||
261 | { | ||
262 | auto etag = item.etag().toLatin1(); | ||
263 | return etag == syncStore().readValue(resourceID(item) + "_etag"); | ||
264 | } | ||
265 | |||
266 | KDAV2::DavUrl WebDavSynchronizer::serverUrl() const | ||
267 | { | ||
268 | if (secret().isEmpty()) { | ||
269 | return {}; | ||
270 | } | ||
271 | |||
272 | auto result = server; | ||
273 | result.setUserName(username); | ||
274 | result.setPassword(secret()); | ||
275 | |||
276 | return KDAV2::DavUrl{ result, protocol }; | ||
277 | } | ||