1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qnetworkaccessfilebackend_p.h" |
5 | #include "qfileinfo.h" |
6 | #include "qdir.h" |
7 | #include "private/qnoncontiguousbytedevice_p.h" |
8 | |
9 | #include <QtCore/QCoreApplication> |
10 | #include <QtCore/QDateTime> |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | using namespace Qt::StringLiterals; |
15 | |
16 | QStringList QNetworkAccessFileBackendFactory::supportedSchemes() const |
17 | { |
18 | QStringList schemes; |
19 | schemes << QStringLiteral("file" ) |
20 | << QStringLiteral("qrc" ); |
21 | #if defined(Q_OS_ANDROID) |
22 | schemes << QStringLiteral("assets" ); |
23 | #endif |
24 | return schemes; |
25 | } |
26 | |
27 | QNetworkAccessBackend * |
28 | QNetworkAccessFileBackendFactory::create(QNetworkAccessManager::Operation op, |
29 | const QNetworkRequest &request) const |
30 | { |
31 | // is it an operation we know of? |
32 | switch (op) { |
33 | case QNetworkAccessManager::GetOperation: |
34 | case QNetworkAccessManager::PutOperation: |
35 | break; |
36 | |
37 | default: |
38 | // no, we can't handle this operation |
39 | return nullptr; |
40 | } |
41 | |
42 | QUrl url = request.url(); |
43 | if (url.scheme().compare(other: "qrc"_L1 , cs: Qt::CaseInsensitive) == 0 |
44 | #if defined(Q_OS_ANDROID) |
45 | || url.scheme().compare("assets"_L1 , Qt::CaseInsensitive) == 0 |
46 | #endif |
47 | || url.isLocalFile()) { |
48 | return new QNetworkAccessFileBackend; |
49 | } else if (!url.scheme().isEmpty() && url.authority().isEmpty() && (url.scheme().size() > 1)) { |
50 | // check if QFile could, in theory, open this URL via the file engines |
51 | // it has to be in the format: |
52 | // prefix:path/to/file |
53 | // or prefix:/path/to/file |
54 | // |
55 | // this construct here must match the one below in open() |
56 | QFileInfo fi(url.toString(options: QUrl::RemoveAuthority | QUrl::RemoveFragment | QUrl::RemoveQuery)); |
57 | if (fi.exists() || (op == QNetworkAccessManager::PutOperation && fi.dir().exists())) |
58 | return new QNetworkAccessFileBackend; |
59 | } |
60 | |
61 | return nullptr; |
62 | } |
63 | |
64 | // We pass TargetType::Local even though it's kind of Networked but we're using a QFile to access |
65 | // the resource so it cannot use proxies anyway |
66 | QNetworkAccessFileBackend::QNetworkAccessFileBackend() |
67 | : QNetworkAccessBackend(QNetworkAccessBackend::TargetType::Local), |
68 | totalBytes(0), |
69 | hasUploadFinished(false) |
70 | { |
71 | } |
72 | |
73 | QNetworkAccessFileBackend::~QNetworkAccessFileBackend() |
74 | { |
75 | } |
76 | |
77 | void QNetworkAccessFileBackend::open() |
78 | { |
79 | QUrl url = this->url(); |
80 | |
81 | if (url.host() == "localhost"_L1 ) |
82 | url.setHost(host: QString()); |
83 | #if !defined(Q_OS_WIN) |
84 | // do not allow UNC paths on Unix |
85 | if (!url.host().isEmpty()) { |
86 | // we handle only local files |
87 | error(code: QNetworkReply::ProtocolInvalidOperationError, |
88 | errorString: QCoreApplication::translate(context: "QNetworkAccessFileBackend" , key: "Request for opening non-local file %1" ).arg(a: url.toString())); |
89 | finished(); |
90 | return; |
91 | } |
92 | #endif // !defined(Q_OS_WIN) |
93 | if (url.path().isEmpty()) |
94 | url.setPath(path: "/"_L1 ); |
95 | setUrl(url); |
96 | |
97 | QString fileName = url.toLocalFile(); |
98 | if (fileName.isEmpty()) { |
99 | if (url.scheme() == "qrc"_L1 ) { |
100 | fileName = u':' + url.path(); |
101 | } else { |
102 | #if defined(Q_OS_ANDROID) |
103 | if (url.scheme() == "assets"_L1 ) |
104 | fileName = "assets:"_L1 + url.path(); |
105 | else |
106 | #endif |
107 | fileName = url.toString(options: QUrl::RemoveAuthority | QUrl::RemoveFragment | QUrl::RemoveQuery); |
108 | } |
109 | } |
110 | file.setFileName(fileName); |
111 | |
112 | if (operation() == QNetworkAccessManager::GetOperation) { |
113 | if (!loadFileInfo()) |
114 | return; |
115 | } |
116 | |
117 | QIODevice::OpenMode mode; |
118 | switch (operation()) { |
119 | case QNetworkAccessManager::GetOperation: |
120 | mode = QIODevice::ReadOnly; |
121 | break; |
122 | case QNetworkAccessManager::PutOperation: |
123 | mode = QIODevice::WriteOnly | QIODevice::Truncate; |
124 | createUploadByteDevice(); |
125 | QObject::connect(sender: uploadByteDevice(), SIGNAL(readyRead()), receiver: this, SLOT(uploadReadyReadSlot())); |
126 | QMetaObject::invokeMethod(obj: this, member: "uploadReadyReadSlot" , c: Qt::QueuedConnection); |
127 | break; |
128 | default: |
129 | Q_ASSERT_X(false, "QNetworkAccessFileBackend::open" , |
130 | "Got a request operation I cannot handle!!" ); |
131 | return; |
132 | } |
133 | |
134 | mode |= QIODevice::Unbuffered; |
135 | bool opened = file.open(flags: mode); |
136 | if (file.isSequential()) |
137 | connect(sender: &file, signal: &QIODevice::readChannelFinished, context: this, slot: [this]() { finished(); }); |
138 | |
139 | // could we open the file? |
140 | if (!opened) { |
141 | QString msg = QCoreApplication::translate(context: "QNetworkAccessFileBackend" , key: "Error opening %1: %2" ) |
142 | .arg(args: this->url().toString(), args: file.errorString()); |
143 | |
144 | // why couldn't we open the file? |
145 | // if we're opening for reading, either it doesn't exist, or it's access denied |
146 | // if we're opening for writing, not existing means it's access denied too |
147 | if (file.exists() || operation() == QNetworkAccessManager::PutOperation) |
148 | error(code: QNetworkReply::ContentAccessDenied, errorString: msg); |
149 | else |
150 | error(code: QNetworkReply::ContentNotFoundError, errorString: msg); |
151 | finished(); |
152 | } |
153 | } |
154 | |
155 | void QNetworkAccessFileBackend::uploadReadyReadSlot() |
156 | { |
157 | if (hasUploadFinished) |
158 | return; |
159 | |
160 | forever { |
161 | QByteArray data(16 * 1024, Qt::Uninitialized); |
162 | qint64 haveRead = uploadByteDevice()->peek(data: data.data(), maxlen: data.size()); |
163 | if (haveRead == -1) { |
164 | // EOF |
165 | hasUploadFinished = true; |
166 | file.flush(); |
167 | file.close(); |
168 | finished(); |
169 | break; |
170 | } else if (haveRead == 0) { |
171 | // nothing to read right now, we will be called again later |
172 | break; |
173 | } else { |
174 | qint64 haveWritten; |
175 | data.truncate(pos: haveRead); |
176 | haveWritten = file.write(data); |
177 | |
178 | if (haveWritten < 0) { |
179 | // write error! |
180 | QString msg = QCoreApplication::translate(context: "QNetworkAccessFileBackend" , key: "Write error writing to %1: %2" ) |
181 | .arg(args: url().toString(), args: file.errorString()); |
182 | error(code: QNetworkReply::ProtocolFailure, errorString: msg); |
183 | |
184 | finished(); |
185 | return; |
186 | } else { |
187 | uploadByteDevice()->skip(maxSize: haveWritten); |
188 | } |
189 | |
190 | |
191 | file.flush(); |
192 | } |
193 | } |
194 | } |
195 | |
196 | void QNetworkAccessFileBackend::close() |
197 | { |
198 | if (operation() == QNetworkAccessManager::GetOperation) { |
199 | file.close(); |
200 | } |
201 | } |
202 | |
203 | bool QNetworkAccessFileBackend::loadFileInfo() |
204 | { |
205 | QFileInfo fi(file); |
206 | setHeader(header: QNetworkRequest::LastModifiedHeader, value: fi.lastModified()); |
207 | setHeader(header: QNetworkRequest::ContentLengthHeader, value: fi.size()); |
208 | |
209 | // signal we're open |
210 | metaDataChanged(); |
211 | |
212 | if (fi.isDir()) { |
213 | error(code: QNetworkReply::ContentOperationNotPermittedError, |
214 | errorString: QCoreApplication::translate(context: "QNetworkAccessFileBackend" , key: "Cannot open %1: Path is a directory" ).arg(a: url().toString())); |
215 | finished(); |
216 | return false; |
217 | } |
218 | |
219 | return true; |
220 | } |
221 | |
222 | qint64 QNetworkAccessFileBackend::bytesAvailable() const |
223 | { |
224 | if (operation() != QNetworkAccessManager::GetOperation) |
225 | return 0; |
226 | return file.bytesAvailable(); |
227 | } |
228 | |
229 | qint64 QNetworkAccessFileBackend::read(char *data, qint64 maxlen) |
230 | { |
231 | if (operation() != QNetworkAccessManager::GetOperation) |
232 | return 0; |
233 | qint64 actuallyRead = file.read(data, maxlen); |
234 | if (actuallyRead <= 0) { |
235 | // EOF or error |
236 | if (file.error() != QFile::NoError) { |
237 | QString msg = QCoreApplication::translate(context: "QNetworkAccessFileBackend" , key: "Read error reading from %1: %2" ) |
238 | .arg(args: url().toString(), args: file.errorString()); |
239 | error(code: QNetworkReply::ProtocolFailure, errorString: msg); |
240 | |
241 | finished(); |
242 | return -1; |
243 | } |
244 | |
245 | finished(); |
246 | return actuallyRead; |
247 | } |
248 | if (!file.isSequential() && file.atEnd()) |
249 | finished(); |
250 | totalBytes += actuallyRead; |
251 | return actuallyRead; |
252 | } |
253 | |
254 | QT_END_NAMESPACE |
255 | |
256 | #include "moc_qnetworkaccessfilebackend_p.cpp" |
257 | |