Qt 6.x
The Qt SDK
Loading...
Searching...
No Matches
qwasmlocalfileaccess.cpp
Go to the documentation of this file.
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
5#include "qlocalfileapi_p.h"
6#include <private/qstdweb_p.h>
7#include <emscripten.h>
8#include <emscripten/bind.h>
9#include <emscripten/html5.h>
10#include <emscripten/val.h>
11
12#include <QtCore/qregularexpression.h>
13
15
17namespace FileDialog {
18namespace {
19bool hasLocalFilesApi()
20{
21 return !qstdweb::window()["showOpenFilePicker"].isUndefined();
22}
23
24void showOpenViaHTMLPolyfill(const QStringList &accept, FileSelectMode fileSelectMode,
25 qstdweb::PromiseCallbacks onFilesSelected)
26{
27 // Create file input html element which will display a native file dialog
28 // and call back to our onchange handler once the user has selected
29 // one or more files.
30 emscripten::val document = emscripten::val::global("document");
31 emscripten::val input = document.call<emscripten::val>("createElement", std::string("input"));
32 input.set("type", "file");
33 input.set("style", "display:none");
34 input.set("accept", LocalFileApi::makeFileInputAccept(accept));
35 Q_UNUSED(accept);
36 input.set("multiple", emscripten::val(fileSelectMode == FileSelectMode::MultipleFiles));
37
38 // Note: there is no event in case the user cancels the file dialog.
39 static std::unique_ptr<qstdweb::EventCallback> changeEvent;
40 auto callback = [=](emscripten::val) { onFilesSelected.thenFunc(input["files"]); };
41 changeEvent = std::make_unique<qstdweb::EventCallback>(input, "change", callback);
42
43 // Activate file input
44 emscripten::val body = document["body"];
45 body.call<void>("appendChild", input);
46 input.call<void>("click");
47 body.call<void>("removeChild", input);
48}
49
50void showOpenViaLocalFileApi(const QStringList &accept, FileSelectMode fileSelectMode,
52{
53 using namespace qstdweb;
54
55 auto options = LocalFileApi::makeOpenFileOptions(accept, fileSelectMode == FileSelectMode::MultipleFiles);
56
57 Promise::make(
58 window(), QStringLiteral("showOpenFilePicker"),
59 {
60 .thenFunc = [=](emscripten::val fileHandles) mutable {
61 std::vector<emscripten::val> filePromises;
62 filePromises.reserve(fileHandles["length"].as<int>());
63 for (int i = 0; i < fileHandles["length"].as<int>(); ++i)
64 filePromises.push_back(fileHandles[i].call<emscripten::val>("getFile"));
65 Promise::all(std::move(filePromises), callbacks);
66 },
67 .catchFunc = callbacks.catchFunc,
68 .finallyFunc = callbacks.finallyFunc,
69 }, std::move(options));
70}
71
72void showSaveViaLocalFileApi(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks)
73{
74 using namespace qstdweb;
75 using namespace emscripten;
76
77 auto options = LocalFileApi::makeSaveFileOptions(QStringList(), fileNameHint);
78
79 Promise::make(
80 window(), QStringLiteral("showSaveFilePicker"),
81 std::move(callbacks), std::move(options));
82}
83} // namespace
84
85void showOpen(const QStringList &accept, FileSelectMode fileSelectMode,
87{
88 hasLocalFilesApi() ?
89 showOpenViaLocalFileApi(accept, fileSelectMode, std::move(callbacks)) :
90 showOpenViaHTMLPolyfill(accept, fileSelectMode, std::move(callbacks));
91}
92
94{
95 return hasLocalFilesApi();
96}
97
98void showSave(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks)
99{
101 showSaveViaLocalFileApi(fileNameHint, std::move(callbacks));
102}
103} // namespace FileDialog
104
105namespace {
106void readFiles(const qstdweb::FileList &fileList,
107 const std::function<char *(uint64_t size, const std::string name)> &acceptFile,
108 const std::function<void ()> &fileDataReady)
109{
110 auto readFile = std::make_shared<std::function<void(int)>>();
111
112 *readFile = [=](int fileIndex) mutable {
113 // Stop when all files have been processed
114 if (fileIndex >= fileList.length()) {
115 readFile.reset();
116 return;
117 }
118
119 const qstdweb::File file = qstdweb::File(fileList[fileIndex]);
120
121 // Ask caller if the file should be accepted
122 char *buffer = acceptFile(file.size(), file.name());
123 if (buffer == nullptr) {
124 (*readFile)(fileIndex + 1);
125 return;
126 }
127
128 // Read file data into caller-provided buffer
129 file.stream(buffer, [readFile = readFile.get(), fileIndex, fileDataReady]() {
130 fileDataReady();
131 (*readFile)(fileIndex + 1);
132 });
133 };
134
135 (*readFile)(0);
136}
137
138QStringList acceptListFromQtFormat(const std::string &qtAcceptList)
139{
140 // copy of qt_make_filter_list() from qfiledialog.cpp
141 auto make_filter_list = [](const QString &filter) -> QStringList
142 {
143 if (filter.isEmpty())
144 return QStringList();
145
146 QString sep(";;");
147 if (!filter.contains(sep) && filter.contains(u'\n'))
148 sep = u'\n';
149
150 return filter.split(sep);
151 };
152
153 const QStringList fileFilter = make_filter_list(QString::fromStdString(qtAcceptList));
154 QStringList transformed;
155 for (const auto &element : fileFilter) {
156 // Accepts either a string in format:
157 // GROUP3
158 // or in this format:
159 // GROUP1 (GROUP2)
160 // Group 1 is treated as the description, whereas group 2 or 3 are treated as the filter
161 // list.
162 static QRegularExpression regex(
163 QString(QStringLiteral("(?:([^(]*)\\(([^()]+)\\)[^)]*)|([^()]+)")));
164 static QRegularExpression wordCharacterRegex(QString(QStringLiteral("\\w")));
165 const auto match = regex.match(element);
166
167 if (!match.hasMatch())
168 continue;
169
170 constexpr size_t FilterListFromParensIndex = 2;
171 constexpr size_t PlainFilterListIndex = 3;
172 QString filterList = match.captured(match.hasCaptured(FilterListFromParensIndex)
173 ? FilterListFromParensIndex
174 : PlainFilterListIndex);
175 for (auto singleExtension : filterList.split(QStringLiteral(" "), Qt::SkipEmptyParts)) {
176 // Checks for a filter that matches everything:
177 // Any number of asterisks or any number of asterisks with a '.' between them.
178 // The web filter does not support wildcards.
180 QString(QStringLiteral("[*]+|[*]+\\.[*]+"))));
181 if (qtAcceptAllRegex.match(singleExtension).hasMatch())
182 continue;
183
184 // Checks for correctness. The web filter only allows filename extensions and does not
185 // filter the actual filenames, therefore we check whether the filter provided only
186 // filters for the extension.
187 static QRegularExpression qtFilenameMatcherRegex(QRegularExpression::anchoredPattern(
188 QString(QStringLiteral("(\\*?)(\\.[^*]+)"))));
189
190 auto extensionMatch = qtFilenameMatcherRegex.match(singleExtension);
191 if (extensionMatch.hasMatch())
192 transformed.append(extensionMatch.captured(2));
193 }
194 }
195 return transformed;
196}
197
198}
199
200void downloadDataAsFile(const char *content, size_t size, const std::string &fileNameHint)
201{
202 // Save a file by creating programmatically clicking a download
203 // link to an object url to a Blob containing a copy of the file
204 // content. The copy is made so that the passed in content buffer
205 // can be released as soon as this function returns.
206 qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content, size);
207 emscripten::val document = emscripten::val::global("document");
208 emscripten::val window = qstdweb::window();
209 emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val());
210 emscripten::val contentLink = document.call<emscripten::val>("createElement", std::string("a"));
211 contentLink.set("href", contentUrl);
212 contentLink.set("download", fileNameHint);
213 contentLink.set("style", "display:none");
214
215 emscripten::val body = document["body"];
216 body.call<void>("appendChild", contentLink);
217 contentLink.call<void>("click");
218 body.call<void>("removeChild", contentLink);
219
220 window["URL"].call<emscripten::val>("revokeObjectURL", contentUrl);
221}
222
223void openFiles(const std::string &accept, FileSelectMode fileSelectMode,
224 const std::function<void (int fileCount)> &fileDialogClosed,
225 const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
226 const std::function<void()> &fileDataReady)
227{
228 FileDialog::showOpen(acceptListFromQtFormat(accept), fileSelectMode, {
229 .thenFunc = [=](emscripten::val result) {
231 fileDialogClosed(files.length());
232 readFiles(files, acceptFile, fileDataReady);
233 },
234 .catchFunc = [=](emscripten::val) {
235 fileDialogClosed(0);
236 }
237 });
238}
239
240void openFile(const std::string &accept,
241 const std::function<void (bool fileSelected)> &fileDialogClosed,
242 const std::function<char *(uint64_t size, const std::string& name)> &acceptFile,
243 const std::function<void()> &fileDataReady)
244{
245 auto fileDialogClosedWithInt = [=](int fileCount) { fileDialogClosed(fileCount != 0); };
246 openFiles(accept, FileSelectMode::SingleFile, fileDialogClosedWithInt, acceptFile, fileDataReady);
247}
248
249void saveDataToFileInChunks(emscripten::val fileHandle, const QByteArray &data)
250{
251 using namespace emscripten;
252 using namespace qstdweb;
253
254 Promise::make(fileHandle, QStringLiteral("createWritable"), {
255 .thenFunc = [=](val writable) {
256 struct State {
257 size_t written;
258 std::function<void(val result)> continuation;
259 };
260
261 static constexpr size_t desiredChunkSize = 1024u;
262#if defined(__EMSCRIPTEN_SHARED_MEMORY__)
263 qstdweb::Uint8Array chunkArray(desiredChunkSize);
264#endif
265
266 auto state = std::make_shared<State>();
267 state->written = 0u;
268 state->continuation = [=](val) mutable {
269 const size_t remaining = data.size() - state->written;
270 if (remaining == 0) {
271 Promise::make(writable, QStringLiteral("close"), { .thenFunc = [=](val) {} });
272 state.reset();
273 return;
274 }
275
276 const auto currentChunkSize = std::min(remaining, desiredChunkSize);
277
278#if defined(__EMSCRIPTEN_SHARED_MEMORY__)
279 // If shared memory is used, WebAssembly.Memory is instantiated with the 'shared'
280 // option on. Passing a typed_memory_view to SharedArrayBuffer to
281 // FileSystemWritableFileStream.write is disallowed by security policies, so we
282 // need to make a copy of the data to a chunk array buffer.
283 Promise::make(
284 writable, QStringLiteral("write"),
285 {
286 .thenFunc = state->continuation,
287 },
288 chunkArray.copyFrom(data.constData() + state->written, currentChunkSize)
289 .val()
290 .call<emscripten::val>("subarray", emscripten::val(0),
291 emscripten::val(currentChunkSize)));
292#else
293 Promise::make(writable, QStringLiteral("write"),
294 {
295 .thenFunc = state->continuation,
296 },
297 val(typed_memory_view(currentChunkSize, data.constData() + state->written)));
298#endif
299 state->written += currentChunkSize;
300 };
301
302 state->continuation(val::undefined());
303 },
304 });
305}
306
307void saveFile(const QByteArray &data, const std::string &fileNameHint)
308{
310 downloadDataAsFile(data.constData(), data.size(), fileNameHint);
311 return;
312 }
313
314 FileDialog::showSave(fileNameHint, {
315 .thenFunc = [=](emscripten::val result) {
317 },
318 });
319}
320
321void saveFile(const char *content, size_t size, const std::string &fileNameHint)
322{
324 downloadDataAsFile(content, size, fileNameHint);
325 return;
326 }
327
328 FileDialog::showSave(fileNameHint, {
329 .thenFunc = [=](emscripten::val result) {
331 },
332 });
333}
334
335} // namespace QWasmLocalFileAccess
336
\inmodule QtCore
Definition qbytearray.h:57
qint64 size() const override
\reimp
Definition qfile.cpp:1156
\inmodule QtCore \reentrant
static QString anchoredPattern(const QString &expression)
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
static QString fromStdString(const std::string &s)
Definition qstring.h:1322
emscripten::val val()
Definition qstdweb.cpp:535
static Blob copyFrom(const char *buffer, uint32_t size, std::string mimeType)
Definition qstdweb.cpp:518
int length() const
Definition qstdweb.cpp:597
static Uint8Array copyFrom(const char *buffer, uint32_t size)
Definition qstdweb.cpp:730
emscripten::val val()
Definition qstdweb.cpp:743
else opt state
[0]
std::string makeFileInputAccept(const QStringList &filterList)
emscripten::val makeOpenFileOptions(const QStringList &filterList, bool acceptMultiple)
emscripten::val makeSaveFileOptions(const QStringList &filterList, const std::string &suggestedName)
Combined button and popup list for selecting options.
void showSave(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks)
void showOpen(const QStringList &accept, FileSelectMode fileSelectMode, qstdweb::PromiseCallbacks callbacks)
void saveFile(const QByteArray &data, const std::string &fileNameHint)
void saveDataToFileInChunks(emscripten::val fileHandle, const QByteArray &data)
void openFile(const std::string &accept, const std::function< void(bool fileSelected)> &fileDialogClosed, const std::function< char *(uint64_t size, const std::string &name)> &acceptFile, const std::function< void()> &fileDataReady)
void downloadDataAsFile(const char *content, size_t size, const std::string &fileNameHint)
void openFiles(const std::string &accept, FileSelectMode fileSelectMode, const std::function< void(int fileCount)> &fileDialogClosed, const std::function< char *(uint64_t size, const std::string &name)> &acceptFile, const std::function< void()> &fileDataReady)
@ SkipEmptyParts
Definition qnamespace.h:127
emscripten::val window()
Definition qstdweb_p.h:205
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLenum GLuint buffer
GLint GLint GLint GLint GLint GLint GLint GLbitfield GLenum filter
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLuint name
GLuint GLfloat * val
GLuint64EXT * result
[6]
GLenum GLenum GLenum input
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
static void split(QT_FT_Vector *b)
static constexpr QChar sep
PromiseCallbacks callbacks
Definition qstdweb.cpp:270
#define QStringLiteral(str)
#define Q_UNUSED(x)
static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
QFile file
[0]
QStringList files
[8]
aWidget window() -> setWindowTitle("New Window Title")
[2]
std::function< void(emscripten::val)> thenFunc
Definition qstdweb_p.h:173