1 /*
2  * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
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, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12  * for more details.
13  */
14 
15 #include "account.h"
16 #include "common/asserts.h"
17 #include "common/checksums.h"
18 #include "common/syncjournaldb.h"
19 #include "common/syncjournalfilerecord.h"
20 #include "common/utility.h"
21 #include "filesystem.h"
22 #include "httplogger.h"
23 #include "networkjobs.h"
24 #include "owncloudpropagator_p.h"
25 #include "propagateremotedelete.h"
26 #include "propagateupload.h"
27 #include "propagateuploadtus.h"
28 #include "propagatorjobs.h"
29 #include "syncengine.h"
30 
31 #include <QNetworkAccessManager>
32 #include <QFileInfo>
33 #include <QDir>
34 #include <cmath>
35 #include <cstring>
36 #include <memory>
37 
38 namespace {
uploadURL(const OCC::AccountPtr & account)39 QUrl uploadURL(const OCC::AccountPtr &account)
40 {
41     return OCC::Utility::concatUrlPath(account->url(), QStringLiteral("remote.php/dav/files/%1/").arg(account->davUser()));
42 }
43 
uploadOffset()44 QByteArray uploadOffset()
45 {
46     return QByteArrayLiteral("Upload-Offset");
47 }
48 
setTusVersionHeader(QNetworkRequest & req)49 void setTusVersionHeader(QNetworkRequest &req){
50     req.setRawHeader(QByteArrayLiteral("Tus-Resumable"), QByteArrayLiteral("1.0.0"));
51 }
52 }
53 
54 namespace OCC {
55 // be very verbose for now
56 Q_LOGGING_CATEGORY(lcPropagateUploadTUS, "sync.propagator.upload.tus", QtDebugMsg)
57 
58 
prepareDevice(const quint64 & chunkSize)59 UploadDevice *PropagateUploadFileTUS::prepareDevice(const quint64 &chunkSize)
60 {
61     const QString localFileName = propagator()->fullLocalPath(_item->_file);
62     auto device = new UploadDevice(localFileName, _currentOffset, chunkSize, &propagator()->_bandwidthManager);
63     if (!device->open(QIODevice::ReadOnly)) {
64         qCWarning(lcPropagateUploadTUS) << "Could not prepare upload device: " << device->errorString();
65 
66         // If the file is currently locked, we want to retry the sync
67         // when it becomes available again.
68         if (FileSystem::isFileLocked(localFileName)) {
69             emit propagator()->seenLockedFile(localFileName);
70         }
71         // Soft error because this is likely caused by the user modifying his files while syncing
72         abortWithError(SyncFileItem::SoftError, device->errorString());
73         return nullptr;
74     }
75     return device;
76 }
77 
78 
makeCreationWithUploadJob(QNetworkRequest * request,UploadDevice * device)79 SimpleNetworkJob *PropagateUploadFileTUS::makeCreationWithUploadJob(QNetworkRequest *request, UploadDevice *device)
80 {
81     Q_ASSERT(propagator()->account()->capabilities().tusSupport().extensions.contains(QStringLiteral("creation-with-upload")));
82     // in difference to the old protocol the algrithm and the value are space seperated
83     const auto checkSum = _transmissionChecksumHeader.replace(':', ' ').toBase64();
84     qCDebug(lcPropagateUploadTUS) << "FullPath:" << propagator()->fullRemotePath(_item->_file);
85     request->setRawHeader(QByteArrayLiteral("Upload-Metadata"), "filename " + propagator()->fullRemotePath(_item->_file).toUtf8().toBase64() + ",checksum " + checkSum);
86     request->setRawHeader(QByteArrayLiteral("Upload-Length"), QByteArray::number(_item->_size));
87     return propagator()->account()->sendRequest("POST", uploadURL(propagator()->account()), *request, device);
88 }
89 
prepareRequest(const quint64 & chunkSize)90 QNetworkRequest PropagateUploadFileTUS::prepareRequest(const quint64 &chunkSize)
91 {
92     QNetworkRequest request;
93     const auto headers = PropagateUploadFileCommon::headers();
94     for (auto it = headers.cbegin(); it != headers.cend(); ++it) {
95         request.setRawHeader(it.key(), it.value());
96     }
97 
98     request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/offset+octet-stream"));
99     request.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(chunkSize));
100     request.setRawHeader(uploadOffset(), QByteArray::number(_currentOffset));
101     setTusVersionHeader(request);
102     return request;
103 }
104 
PropagateUploadFileTUS(OwncloudPropagator * propagator,const SyncFileItemPtr & item)105 PropagateUploadFileTUS::PropagateUploadFileTUS(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
106     : PropagateUploadFileCommon(propagator, item)
107 {
108 }
109 
doStartUpload()110 void PropagateUploadFileTUS::doStartUpload()
111 {
112     propagator()->reportProgress(*_item, 0);
113     startNextChunk();
114     propagator()->_activeJobList.append(this);
115 }
116 
startNextChunk()117 void PropagateUploadFileTUS::startNextChunk()
118 {
119     if (propagator()->_abortRequested)
120         return;
121     const quint64 chunkSize = [&] {
122         auto chunkSize = _item->_size - _currentOffset;
123         if (propagator()->account()->capabilities().tusSupport().max_chunk_size) {
124             chunkSize = qMin(chunkSize - _currentOffset, propagator()->account()->capabilities().tusSupport().max_chunk_size);
125         }
126         return chunkSize;
127     }();
128 
129     QNetworkRequest req = prepareRequest(chunkSize);
130     auto device = prepareDevice(chunkSize);
131     if (!device) {
132         return;
133     }
134 
135     SimpleNetworkJob *job;
136     if (_currentOffset != 0) {
137         qCDebug(lcPropagateUploadTUS) << "Starting to patch upload:" << propagator()->fullRemotePath(_item->_file);
138         job = propagator()->account()->sendRequest("PATCH", _location, req, device);
139     } else {
140         OC_ASSERT(_location.isEmpty());
141         qCDebug(lcPropagateUploadTUS) << "Starting creation with upload:" << propagator()->fullRemotePath(_item->_file);
142         job = makeCreationWithUploadJob(&req, device);
143     }
144     qCDebug(lcPropagateUploadTUS) << "Offset:" << _currentOffset << _currentOffset  / (_item->_size + 1) * 100
145                                   << "Chunk:" << chunkSize << chunkSize / (_item->_size + 1) * 100;
146 
147     _jobs.append(job);
148     connect(job->reply(), &QNetworkReply::uploadProgress, device, &UploadDevice::slotJobUploadProgress);
149     connect(job->reply(), &QNetworkReply::uploadProgress, this,[this](qint64 bytesSent, qint64){
150        propagator()->reportProgress(*_item, _currentOffset + bytesSent);
151 
152     });
153     connect(job, &SimpleNetworkJob::finishedSignal, this, &PropagateUploadFileTUS::slotChunkFinished);
154     connect(job, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
155     job->start();
156 }
157 
slotChunkFinished()158 void PropagateUploadFileTUS::slotChunkFinished()
159 {
160     SimpleNetworkJob *job = qobject_cast<SimpleNetworkJob *>(sender());
161     OC_ASSERT(job);
162     slotJobDestroyed(job); // remove it from the _jobs list
163     qCDebug(lcPropagateUploadTUS) << propagator()->fullRemotePath(_item->_file) << HttpLogger::requestVerb(*job->reply());
164 
165     _item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
166     _item->_responseTimeStamp = job->responseTimestamp();
167     _item->_requestId = job->requestId();
168 
169     QNetworkReply::NetworkError err = job->reply()->error();
170     if (err != QNetworkReply::NoError) {
171         // try to get the offset if possible, only try once
172         if (err == QNetworkReply::TimeoutError && !_location.isEmpty() && HttpLogger::requestVerb(*job->reply())  != "HEAD")
173         {
174             qCWarning(lcPropagateUploadTUS) << propagator()->fullRemotePath(_item->_file) << "Encountered a timeout -> get progrss for" << _location;
175             QNetworkRequest req;
176             setTusVersionHeader(req);
177             auto updateJob = propagator()->account()->sendRequest("HEAD", _location, req);
178             _jobs.append(updateJob);
179             connect(updateJob, &SimpleNetworkJob::finishedSignal, this, &PropagateUploadFileTUS::slotChunkFinished);
180             connect(updateJob, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
181             updateJob->start();
182             return;
183 
184         }
185         commonErrorHandling(job);
186         return;
187     }
188 
189     const qint64 offset = job->reply()->rawHeader(uploadOffset()).toLongLong();
190     propagator()->reportProgress(*_item, offset);
191     _currentOffset = offset;
192     // first response after a POST request
193     if (_location.isEmpty()) {
194         _location = job->reply()->header(QNetworkRequest::LocationHeader).toUrl();
195     }
196 
197 
198     _finished = offset == _item->_size;
199 
200     // Check if the file still exists
201     const QString fullFilePath(propagator()->fullLocalPath(_item->_file));
202     if (!FileSystem::fileExists(fullFilePath)) {
203         if (!_finished) {
204             abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync."));
205             return;
206         } else {
207             propagator()->_anotherSyncNeeded = true;
208         }
209     }
210 
211     // Check whether the file changed since discovery.
212     if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
213         propagator()->_anotherSyncNeeded = true;
214         if (!_finished) {
215             abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync."));
216             // FIXME:  the legacy code was retrying for a few seconds.
217             //         and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
218             return;
219         }
220     }
221     if (!_finished) {
222         startNextChunk();
223         return;
224     }
225     const QByteArray etag = getEtagFromReply(job->reply());
226 
227     _finished = !etag.isEmpty();
228     if (!_finished) {
229         auto check = new PropfindJob(propagator()->account(), propagator()->fullRemotePath(_item->_file));
230         _jobs.append(check);
231         check->setProperties({ "http://owncloud.org/ns:fileid", "http://owncloud.org/ns:permissions", "getetag" });
232         connect(check, &PropfindJob::result, this, [this, check](const QVariantMap &map) {
233             _finished = true;
234             _item->_remotePerm = RemotePermissions::fromServerString(map.value(QStringLiteral("permissions")).toString());
235             finalize(Utility::normalizeEtag(map.value(QStringLiteral("getetag")).toByteArray()), map.value(QStringLiteral("fileid")).toByteArray());
236             slotJobDestroyed(check);
237         });
238         connect(check, &QObject::destroyed, this, &PropagateUploadFileCommon::slotJobDestroyed);
239         check->start();
240         return;
241     }
242     // the file id should only be empty for new files up- or downloaded
243     finalize(etag, job->reply()->rawHeader("OC-FileID"));
244 }
245 
finalize(const QByteArray & etag,const QByteArray & fileId)246 void PropagateUploadFileTUS::finalize(const QByteArray &etag, const QByteArray &fileId)
247 {
248     OC_ASSERT(_finished);
249     qCDebug(lcPropagateUploadTUS) << _item->_etag << etag << fileId;
250     _item->_etag = etag;
251     if (!fileId.isEmpty()) {
252         if (!_item->_fileId.isEmpty() && _item->_fileId != fileId) {
253             qCWarning(lcPropagateUploadTUS) << "File ID changed!" << _item->_fileId << fileId;
254         }
255         _item->_fileId = fileId;
256     }
257     propagator()->_activeJobList.removeOne(this);
258     PropagateUploadFileCommon::finalize();
259 }
260 
abort(PropagatorJob::AbortType abortType)261 void PropagateUploadFileTUS::abort(PropagatorJob::AbortType abortType)
262 {
263     abortNetworkJobs(
264         abortType,
265         [](AbstractNetworkJob *) {
266             // TODO
267             return true;
268         });
269 }
270 
271 }
272