History

expanded Assets scripting interface

humbletim committed 8 years ago

Commit 8c7a8f01 parent 15fcf6612 files · +1090 −47Browse files

Showing the changes to libraries/script-engine/src/AssetScriptingInterface.cpp (1 of 12 files). Show all

Unified · Split

libraries/script-engine/src/AssetScriptingInterface.cpp+313−36
@@ -11,43 +11,70 @@ · 10 unmodified lines
1111
1212 #include "AssetScriptingInterface.h"
1313
14+#include <QMimeDatabase>
15+#include <QThread>
1416 #include <QtScript/QScriptEngine>
1517
1618 #include <AssetRequest.h>
1719 #include <AssetUpload.h>
20+#include <AssetUtils.h>
1821 #include <MappingRequest.h>
1922 #include <NetworkLogging.h>
2023 #include <NodeList.h>
2124
22−AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
23− _engine(engine)
24−{
25+#include <RegisteredMetaTypes.h>
26+
27+#include <shared/QtHelpers.h>
28+#include "Gzip.h"
29+#include "ScriptEngine.h"
30+
31+//using Promise = MiniPromise::Promise;
32+
33+AssetScriptingInterface::AssetScriptingInterface(QObject* parent) : BaseAssetScriptingInterface(parent) {
34+ if (auto engine = qobject_cast<QScriptEngine*>(parent)) {
35+ registerMetaTypes(engine);
36+ }
2537 }
38+#define JS_ASSERT(cond, error) { if (!this->jsAssert(cond, error)) { return; } }
2639
2740 void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {
41+ auto handler = makeScopedHandlerObject(thisObject(), callback);
2842 QByteArray dataByteArray = data.toUtf8();
2943 auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);
3044
31− QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable {
32− if (callback.isFunction()) {
33− QString url = "atp:" + hash;
34− QScriptValueList args { url, hash };
35− callback.call(_engine->currentContext()->thisObject(), args);
36− }
45+ Promise deferred = makePromise(__FUNCTION__)
46+ ->ready([this, handler](QString error, QVariantMap result) {
47+ auto url = result.value("url").toString();
48+ auto hash = result.value("hash").toString();
49+ jsCallback(handler, url, hash);
50+ });
51+
52+ connect(upload, &AssetUpload::finished, upload, [this, deferred](AssetUpload* upload, const QString& hash) {
53+ // we are now on the "Resource Manager" thread (and "hash" being a *reference* makes it unsafe to use directly)
54+ Q_ASSERT(QThread::currentThread() == upload->thread());
55+ deferred->resolve(NoError, {
56+ { "url", "atp:" + hash },
57+ { "hash", hash },
58+ });
3759 upload->deleteLater();
3860 });
3961 upload->start();
4062 }
4163
4264 void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValue callback) {
43− auto setMappingRequest = DependencyManager::get<AssetClient>()->createSetMappingRequest(path, hash);
65+ auto handler = makeScopedHandlerObject(thisObject(), callback);
66+ auto setMappingRequest = assetClient()->createSetMappingRequest(path, hash);
67+ Promise deferred = makePromise(__FUNCTION__)
68+ ->ready([=](QString error, QVariantMap result) {
69+ jsCallback(handler, error, result);
70+ });
4471
45− QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
46− if (callback.isFunction()) {
47− QString error = request->getErrorString();
48− QScriptValueList args { error };
49− callback.call(_engine->currentContext()->thisObject(), args);
50− }
72+ connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [this, deferred](SetMappingRequest* request) {
73+ Q_ASSERT(QThread::currentThread() == request->thread());
74+ // we are now on the "Resource Manager" thread
75+ QString error = request->getErrorString();
76+ // forward a thread-safe values back to our thread
77+ deferred->handle(error, { { "error", request->getError() } });
5178 request->deleteLater();
5279 });
5380 setMappingRequest->start();
@@ -55,48 +82,63 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu · 1 unmodified line
5582
5683
5784 void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
85+ // FIXME: historically this API method failed silently when given a non-atp prefixed
86+ // urlString (or if the AssetRequest failed).
87+ // .. is that by design or could we update without breaking things to provide better feedback to scripts?
5888
5989 if (!urlString.startsWith(ATP_SCHEME)) {
90+ // ... for now at least log a message so user can check logs
91+ qDebug() << "AssetScriptingInterface::downloadData ERROR: url does not start with " << ATP_SCHEME;
6092 return;
6193 }
62−
63− // Make request to atp
64− auto path = urlString.right(urlString.length() - ATP_SCHEME.length());
65− auto parts = path.split(".", QString::SkipEmptyParts);
66− auto hash = parts.length() > 0 ? parts[0] : "";
67−
94+ QString hash = AssetUtils::extractAssetHash(urlString);
95+ auto handler = makeScopedHandlerObject(thisObject(), callback);
6896 auto assetClient = DependencyManager::get<AssetClient>();
6997 auto assetRequest = assetClient->createRequest(hash);
7098
71− _pendingRequests << assetRequest;
99+ Promise deferred = makePromise(__FUNCTION__)
100+ ->ready([=](QString error, QVariantMap result) {
101+ // FIXME: to remain backwards-compatible the signature here is "callback(data, n/a)"
102+ jsCallback(handler, result.value("data").toString(), { { "errorMessage", error } });
103+ });
72104
73− connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable {
105+ connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) {
106+ Q_ASSERT(QThread::currentThread() == request->thread());
107+ // we are now on the "Resource Manager" thread
74108 Q_ASSERT(request->getState() == AssetRequest::Finished);
75109
76110 if (request->getError() == AssetRequest::Error::NoError) {
77− if (callback.isFunction()) {
78− QString data = QString::fromUtf8(request->getData());
79− QScriptValueList args { data };
80− callback.call(_engine->currentContext()->thisObject(), args);
81− }
111+ QString data = QString::fromUtf8(request->getData());
112+ // forward a thread-safe values back to our thread
113+ deferred->resolve(NoError, { { "data", data } });
114+ } else {
115+ // FIXME: propagate error to scripts? (requires changing signature or inverting param order above..)
116+ //deferred->resolve(request->getErrorString(), { { "error", requet->getError() } });
117+ qDebug() << "AssetScriptingInterface::downloadData ERROR: " << request->getErrorString();
82118 }
83119
84120 request->deleteLater();
85− _pendingRequests.remove(request);
86121 });
87122
88123 assetRequest->start();
89124 }
90125
91126 void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) {
127+ auto handler = makeScopedHandlerObject(thisObject(), callback);
92128 auto setBakingEnabledRequest = DependencyManager::get<AssetClient>()->createSetBakingEnabledRequest({ path }, enabled);
93129
94− QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {
95− if (callback.isFunction()) {
96− QString error = request->getErrorString();
97− QScriptValueList args{ error };
98− callback.call(_engine->currentContext()->thisObject(), args);
99− }
130+ Promise deferred = makePromise(__FUNCTION__)
131+ ->ready([=](QString error, QVariantMap result) {
132+ jsCallback(handler, error, result);
133+ });
134+
135+ connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, setBakingEnabledRequest, [this, deferred](SetBakingEnabledRequest* request) {
136+ Q_ASSERT(QThread::currentThread() == request->thread());
137+ // we are now on the "Resource Manager" thread
138+
139+ QString error = request->getErrorString();
140+ // forward thread-safe values back to our thread
141+ deferred->handle(error, {});
100142 request->deleteLater();
101143 });
102144 setBakingEnabledRequest->start();
@@ -111,3 +153,238 @@ void AssetScriptingInterface::sendFakedHandshake() { · 8 unmodified lines
111153 }
112154
113155 #endif
156+
157+void AssetScriptingInterface::getMapping(QString asset, QScriptValue callback) {
158+ auto path = AssetUtils::getATPUrl(asset).path();
159+ auto handler = makeScopedHandlerObject(thisObject(), callback);
160+ JS_ASSERT(AssetUtils::isValidFilePath(path), "invalid ATP file path: " + asset + "(path:"+path+")");
161+ JS_ASSERT(callback.isFunction(), "expected second parameter to be a callback function");
162+ qDebug() << ">>getMapping//getAssetInfo" << path;
163+ getAssetInfo(path)->ready([this, handler](QString error, QVariantMap result) {
164+ qDebug() << "//getMapping//getAssetInfo" << error << result.keys();
165+ jsCallback(handler, error, result.value("hash").toString());
166+ });
167+}
168+
169+/////////////////////////// new APIS ////////////////////////////////////
170+
171+bool AssetScriptingInterface::jsAssert(bool condition, const QString& error) {
172+ if (condition) {
173+ return true;
174+ }
175+ if (context()) {
176+ context()->throwError(error);
177+ } else {
178+ qDebug() << "WARNING -- jsAssert failed outside of a valid JS context: " + error;
179+ }
180+ return false;
181+}
182+
183+void AssetScriptingInterface::jsCallback(const QScriptValue& handler,
184+ const QScriptValue& error, const QScriptValue& result) {
185+ Q_ASSERT(thread() == QThread::currentThread());
186+ auto errorValue = !error.toBool() ? QScriptValue::NullValue : error;
187+ JS_ASSERT(handler.isObject() && handler.property("callback").isFunction(),
188+ QString("jsCallback -- .callback is not a function (%1)")
189+ .arg(handler.property("callback").toVariant().typeName()));
190+#if 1 || DEGUG_JSCALLBACK
191+ QScriptValue debug = result;
192+ debug.setProperty("toString", handler.engine()->evaluate("1,function() { return JSON.stringify(this, 0, 2); }"));
193+#endif
194+ ::callScopedHandlerObject(handler, errorValue, result);
195+}
196+
197+void AssetScriptingInterface::jsCallback(const QScriptValue& handler,
198+ const QScriptValue& error, const QVariantMap& result) {
199+ Q_ASSERT(thread() == QThread::currentThread());
200+ Q_ASSERT(handler.engine());
201+ auto engine = handler.engine();
202+ jsCallback(handler, error, engine->toScriptValue(result));
203+}
204+
205+void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
206+ jsAssert(false, "TODO: deleteAsset API");
207+}
208+
209+void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
210+ JS_ASSERT(options.isObject() || options.isString(), "expected request options Object or URL as first parameter");
211+
212+ auto decompress = options.property("decompress").toBool() || options.property("compressed").toBool();
213+ auto responseType = options.property("responseType").toString().toLower();
214+ auto url = options.property("url").toString();
215+ if (options.isString()) {
216+ url = options.toString();
217+ }
218+ if (responseType.isEmpty() || responseType == "string") {
219+ responseType = "text";
220+ }
221+ auto asset = AssetUtils::getATPUrl(url).path();
222+ auto handler = makeScopedHandlerObject(scope, callback);
223+
224+ JS_ASSERT(handler.property("callback").isFunction(),
225+ QString("Invalid callback function (%1)").arg(handler.property("callback").toVariant().typeName()));
226+ JS_ASSERT(AssetUtils::isValidHash(asset) || AssetUtils::isValidFilePath(asset),
227+ QString("Invalid ATP url '%1'").arg(url));
228+ JS_ASSERT(RESPONSE_TYPES.contains(responseType),
229+ QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | ")));
230+
231+ Promise resolved = makePromise("resolved"),
232+ loaded = makePromise("loaded");
233+
234+ loaded->ready([=](QString error, QVariantMap result) {
235+ qDebug() << "//loaded" << error;
236+ jsCallback(handler, error, result);
237+ });
238+
239+ resolved->ready([=](QString error, QVariantMap result) {
240+ qDebug() << "//resolved" << result.value("hash");
241+ QString hash = result.value("hash").toString();
242+ if (!error.isEmpty() || !AssetUtils::isValidHash(hash)) {
243+ loaded->reject(error.isEmpty() ? "internal hash error: " + hash : error, result);
244+ } else {
245+ loadAsset(hash, decompress, responseType)
246+ ->mixin(result)
247+ ->ready([this, loaded, hash](QString error, QVariantMap result) {
248+ qDebug() << "//getAssetInfo/loadAsset" << error << hash;
249+ loaded->resolve(NoError, result);
250+ });
251+ }
252+ });
253+
254+ if (AssetUtils::isValidHash(asset)) {
255+ resolved->resolve(NoError, { { "hash", asset } });
256+ } else {
257+ getAssetInfo(asset)
258+ ->ready([this, resolved](QString error, QVariantMap result) {
259+ qDebug() << "//getAssetInfo" << error << result.value("hash") << result.value("path");
260+ resolved->resolve(error, result);
261+ });
262+ }
263+}
264+
265+void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
266+ const QString& URL{ "url" };
267+
268+ auto url = (options.isString() ? options : options.property(URL)).toString();
269+ auto asset = AssetUtils::getATPUrl(url).path();
270+ auto handler = makeScopedHandlerObject(scope, callback);
271+
272+ JS_ASSERT(AssetUtils::isValidFilePath(asset) || AssetUtils::isValidHash(asset),
273+ "expected options to be an asset URL or request options containing .url property");
274+ JS_ASSERT(handler.property("callback").isFunction(), "invalid callback function");
275+ getAssetInfo(asset)->ready([=](QString error, QVariantMap result) {
276+ qDebug() << "//resolveAsset/getAssetInfo" << error << result.value("hash");
277+ jsCallback(handler, error, result);
278+ });
279+}
280+
281+void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
282+ auto data = options.property("data");
283+ QByteArray dataByteArray = qscriptvalue_cast<QByteArray>(data);
284+ auto handler = makeScopedHandlerObject(scope, callback);
285+ auto responseType = options.property("responseType").toString().toLower();
286+ if (responseType.isEmpty()) {
287+ responseType = "text";
288+ }
289+ decompressBytes(dataByteArray)
290+ ->ready([=](QString error, QVariantMap result) {
291+ if (responseType == "arraybuffer") {
292+ jsCallback(handler, error, result);
293+ } else {
294+ convertBytes(result.value("data").toByteArray(), responseType)
295+ ->mixin(result)
296+ ->ready([=](QString error, QVariantMap result) {
297+ jsCallback(handler, error, result);
298+ });
299+ }
300+ });
301+}
302+
303+void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
304+ auto data = options.property("data");
305+ QByteArray dataByteArray = data.isString() ?
306+ data.toString().toUtf8() :
307+ qscriptvalue_cast<QByteArray>(data);
308+ auto handler = makeScopedHandlerObject(scope, callback);
309+ auto level = options.property("level").toInt32();
310+ if (level < -1 || level > 9) {
311+ level = -1;
312+ }
313+ compressBytes(dataByteArray, level)
314+ ->ready([=](QString error, QVariantMap result) {
315+ jsCallback(handler, error, result);
316+ });
317+}
318+
319+void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
320+ auto compress = options.property("compress").toBool() ||
321+ options.property("compressed").toBool();
322+ auto handler = makeScopedHandlerObject(scope, callback);
323+ auto data = options.property("data");
324+ auto rawPath = options.property("path").toString();
325+ auto path = AssetUtils::getATPUrl(rawPath).path();
326+
327+ QByteArray dataByteArray = data.isString() ?
328+ data.toString().toUtf8() :
329+ qscriptvalue_cast<QByteArray>(data);
330+ //auto rawByteLength = dataByteArray.size();
331+
332+ JS_ASSERT(path.isEmpty() || AssetUtils::isValidFilePath(path),
333+ QString("expected valid ATP file path '%1' ('%2')").arg(rawPath).arg(path));
334+ JS_ASSERT(handler.property("callback").isFunction(),
335+ "invalid callback function");
336+ JS_ASSERT(dataByteArray.size() > 0,
337+ QString("expected non-zero .data (got %1 / #%2 bytes)")
338+ .arg(data.toVariant().typeName())
339+ .arg(dataByteArray.size()));
340+
341+
342+ // [compressed] => uploaded to server => [mapped to path]
343+ Promise prepared = makePromise("putAsset::prepared"),
344+ uploaded = makePromise("putAsset::uploaded"),
345+ finished = makePromise("putAsset::finished");
346+
347+ if (compress) {
348+ qDebug() << "putAsset::compressBytes...";
349+ compressBytes(dataByteArray, -1)
350+ ->finally([=](QString error, QVariantMap result) {
351+ qDebug() << "//putAsset::compressedBytes" << error << result.keys();
352+ prepared->handle(error, result);
353+ });
354+ } else {
355+ prepared->resolve(NoError, {{ "data", dataByteArray }});
356+ }
357+
358+ prepared->ready([=](QString error, QVariantMap result) {
359+ qDebug() << "//putAsset::prepared" << error << result.value("data").toByteArray().size() << result.keys();
360+ uploadBytes(result.value("data").toByteArray())
361+ ->mixin(result)
362+ ->ready([=](QString error, QVariantMap result) {
363+ qDebug() << "===========//putAsset::prepared/uploadBytes" << error << result.keys();
364+ uploaded->handle(error, result);
365+ });
366+ });
367+
368+ uploaded->ready([=](QString error, QVariantMap result) {
369+ QString hash = result.value("hash").toString();
370+ qDebug() << "//putAsset::uploaded" << error << hash << result.keys();
371+ if (path.isEmpty()) {
372+ finished->handle(error, result);
373+ } else if (!AssetUtils::isValidHash(hash)) {
374+ finished->reject("path mapping requested, but did not receive valid hash", result);
375+ } else {
376+ qDebug() << "symlinkAsset" << hash << path << QThread::currentThread();
377+ symlinkAsset(hash, path)
378+ ->mixin(result)
379+ ->ready([=](QString error, QVariantMap result) {
380+ finished->handle(error, result);
381+ qDebug() << "//symlinkAsset" << hash << path << result.keys();
382+ });
383+ }
384+ });
385+
386+ finished->ready([=](QString error, QVariantMap result) {
387+ qDebug() << "//putAsset::finished" << error << result.keys();
388+ jsCallback(handler, error, result);
389+ });
390+}