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
18using namespace Quotient;
19class Q_DECL_HIDDEN DownloadFileJob::Private {
20public:
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
36QUrl 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
42DownloadFileJob::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
53DownloadFileJob::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
66QString DownloadFileJob::targetFileName() const
67{
68 return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
69}
70
71void 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
89void 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 sizeHeader = 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
118void DownloadFileJob::beforeAbandon()
119{
120 if (d->targetFile)
121 d->targetFile->remove();
122 d->tempFile->remove();
123}
124
125#ifdef Quotient_E2EE_ENABLED
126void 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
136BaseJob::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