1 | // SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> |
2 | // SPDX-License-Identifier: LGPL-2.1-or-later |
3 | |
4 | #include "downloadfilejob.h" |
5 | |
6 | #include "../logging_categories_p.h" |
7 | |
8 | #include <QtCore/QFile> |
9 | #include <QtCore/QTemporaryFile> |
10 | #include <QtNetwork/QNetworkReply> |
11 | |
12 | #ifdef Quotient_E2EE_ENABLED |
13 | # include <Quotient/events/filesourceinfo.h> |
14 | |
15 | # include <QtCore/QCryptographicHash> |
16 | #endif |
17 | |
18 | using namespace Quotient; |
19 | class Q_DECL_HIDDEN DownloadFileJob::Private { |
20 | public: |
21 | Private() : tempFile(new QTemporaryFile()) {} |
22 | |
23 | explicit Private(const QString& localFilename) |
24 | : targetFile(new QFile(localFilename)) |
25 | , tempFile(new QFile(targetFile->fileName() + ".qtntdownload"_ls )) |
26 | {} |
27 | |
28 | QScopedPointer<QFile> targetFile; |
29 | QScopedPointer<QFile> tempFile; |
30 | |
31 | #ifdef Quotient_E2EE_ENABLED |
32 | Omittable<EncryptedFileMetadata> encryptedFileMetadata; |
33 | #endif |
34 | }; |
35 | |
36 | QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) |
37 | { |
38 | return makeRequestUrl(baseUrl: std::move(baseUrl), serverName: mxcUri.authority(), |
39 | mediaId: mxcUri.path().mid(position: 1)); |
40 | } |
41 | |
42 | DownloadFileJob::DownloadFileJob(const QString& serverName, |
43 | const QString& mediaId, |
44 | const QString& localFilename) |
45 | : GetContentJob(serverName, mediaId) |
46 | , d(localFilename.isEmpty() ? makeImpl<Private>() |
47 | : makeImpl<Private>(args: localFilename)) |
48 | { |
49 | setObjectName(QStringLiteral("DownloadFileJob" )); |
50 | } |
51 | |
52 | #ifdef Quotient_E2EE_ENABLED |
53 | DownloadFileJob::DownloadFileJob(const QString& serverName, |
54 | const QString& mediaId, |
55 | const EncryptedFileMetadata& file, |
56 | const QString& localFilename) |
57 | : GetContentJob(serverName, mediaId) |
58 | , d(localFilename.isEmpty() ? makeImpl<Private>() |
59 | : makeImpl<Private>(localFilename)) |
60 | { |
61 | setObjectName(QStringLiteral("DownloadFileJob" )); |
62 | d->encryptedFileMetadata = file; |
63 | } |
64 | #endif |
65 | |
66 | QString DownloadFileJob::targetFileName() const |
67 | { |
68 | return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); |
69 | } |
70 | |
71 | void DownloadFileJob::doPrepare() |
72 | { |
73 | if (d->targetFile && !d->targetFile->isReadable() |
74 | && !d->targetFile->open(flags: QIODevice::WriteOnly)) { |
75 | qCWarning(JOBS) << "Couldn't open the file" << d->targetFile->fileName() |
76 | << "for writing" ; |
77 | setStatus(code: FileError, message: "Could not open the target file for writing"_ls ); |
78 | return; |
79 | } |
80 | if (!d->tempFile->isReadable() && !d->tempFile->open(flags: QIODevice::ReadWrite)) { |
81 | qCWarning(JOBS) << "Couldn't open the temporary file" |
82 | << d->tempFile->fileName() << "for writing" ; |
83 | setStatus(code: FileError, message: "Could not open the temporary download file"_ls ); |
84 | return; |
85 | } |
86 | qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); |
87 | } |
88 | |
89 | void DownloadFileJob::onSentRequest(QNetworkReply* reply) |
90 | { |
91 | connect(sender: reply, signal: &QNetworkReply::metaDataChanged, context: this, slot: [this, reply] { |
92 | if (!status().good()) |
93 | return; |
94 | auto = reply->header(header: QNetworkRequest::ContentLengthHeader); |
95 | if (sizeHeader.isValid()) { |
96 | auto targetSize = sizeHeader.toLongLong(); |
97 | if (targetSize != -1) |
98 | if (!d->tempFile->resize(sz: targetSize)) { |
99 | qCWarning(JOBS) << "Failed to allocate" << targetSize |
100 | << "bytes for" << d->tempFile->fileName(); |
101 | setStatus(code: FileError, |
102 | message: "Could not reserve disk space for download"_ls ); |
103 | } |
104 | } |
105 | }); |
106 | connect(sender: reply, signal: &QIODevice::readyRead, context: this, slot: [this, reply] { |
107 | if (!status().good()) |
108 | return; |
109 | auto bytes = reply->read(maxlen: reply->bytesAvailable()); |
110 | if (!bytes.isEmpty()) |
111 | d->tempFile->write(data: bytes); |
112 | else |
113 | qCWarning(JOBS) << "Unexpected empty chunk when downloading from" |
114 | << reply->url() << "to" << d->tempFile->fileName(); |
115 | }); |
116 | } |
117 | |
118 | void DownloadFileJob::beforeAbandon() |
119 | { |
120 | if (d->targetFile) |
121 | d->targetFile->remove(); |
122 | d->tempFile->remove(); |
123 | } |
124 | |
125 | #ifdef Quotient_E2EE_ENABLED |
126 | void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata, |
127 | QFile& targetFile) |
128 | { |
129 | sourceFile.seek(0); |
130 | const auto encrypted = sourceFile.readAll(); // TODO: stream decryption |
131 | const auto decrypted = decryptFile(encrypted, metadata); |
132 | targetFile.write(decrypted); |
133 | } |
134 | #endif |
135 | |
136 | BaseJob::Status DownloadFileJob::prepareResult() |
137 | { |
138 | if (d->targetFile) { |
139 | #ifdef Quotient_E2EE_ENABLED |
140 | if (d->encryptedFileMetadata.has_value()) { |
141 | decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); |
142 | d->tempFile->remove(); |
143 | } else { |
144 | #endif |
145 | d->targetFile->close(); |
146 | if (!d->targetFile->remove()) { |
147 | qWarning(catFunc: JOBS) << "Failed to remove the target file placeholder" ; |
148 | return { FileError, "Couldn't finalise the download"_ls }; |
149 | } |
150 | if (!d->tempFile->rename(newName: d->targetFile->fileName())) { |
151 | qWarning(catFunc: JOBS) << "Failed to rename" << d->tempFile->fileName() |
152 | << "to" << d->targetFile->fileName(); |
153 | return { FileError, "Couldn't finalise the download"_ls }; |
154 | } |
155 | #ifdef Quotient_E2EE_ENABLED |
156 | } |
157 | #endif |
158 | } else { |
159 | #ifdef Quotient_E2EE_ENABLED |
160 | if (d->encryptedFileMetadata.has_value()) { |
161 | QTemporaryFile tempTempFile; // Assuming it to be next to tempFile |
162 | decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); |
163 | d->tempFile->close(); |
164 | if (!d->tempFile->remove()) { |
165 | qWarning(JOBS) |
166 | << "Failed to remove the decrypted file placeholder" ; |
167 | return { FileError, "Couldn't finalise the download"_ls }; |
168 | } |
169 | if (!tempTempFile.rename(d->tempFile->fileName())) { |
170 | qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() |
171 | << "to" << d->tempFile->fileName(); |
172 | return { FileError, "Couldn't finalise the download"_ls }; |
173 | } |
174 | } else { |
175 | #endif |
176 | d->tempFile->close(); |
177 | #ifdef Quotient_E2EE_ENABLED |
178 | } |
179 | #endif |
180 | } |
181 | qDebug(catFunc: JOBS) << "Saved a file as" << targetFileName(); |
182 | return Success; |
183 | } |
184 | |