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
11 11
12#include "AssetScriptingInterface.h"12#include "AssetScriptingInterface.h"
13 13
14#include <QMimeDatabase>
15#include <QThread>
14#include <QtScript/QScriptEngine>16#include <QtScript/QScriptEngine>
15 17
16#include <AssetRequest.h>18#include <AssetRequest.h>
17#include <AssetUpload.h>19#include <AssetUpload.h>
20#include <AssetUtils.h>
18#include <MappingRequest.h>21#include <MappingRequest.h>
19#include <NetworkLogging.h>22#include <NetworkLogging.h>
20#include <NodeList.h>23#include <NodeList.h>
21 24
22AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :25#include <RegisteredMetaTypes.h>
23 _engine(engine)26
24{27#include <shared/QtHelpers.h>
28#include "Gzip.h"
29#include "ScriptEngine.h"
30
31//using Promise = MiniPromise::Promise;
32
33AssetScriptingInterface::AssetScriptingInterface(QObject* parent) : BaseAssetScriptingInterface(parent) {
34 if (auto engine = qobject_cast<QScriptEngine*>(parent)) {
35 registerMetaTypes(engine);
36 }
25}37}
38#define JS_ASSERT(cond, error) { if (!this->jsAssert(cond, error)) { return; } }
26 39
27void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {40void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {
41 auto handler = makeScopedHandlerObject(thisObject(), callback);
28 QByteArray dataByteArray = data.toUtf8();42 QByteArray dataByteArray = data.toUtf8();
29 auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);43 auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);
30 44
31 QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable {45 Promise deferred = makePromise(__FUNCTION__)
32 if (callback.isFunction()) {46 ->ready([this, handler](QString error, QVariantMap result) {
33 QString url = "atp:" + hash;47 auto url = result.value("url").toString();
34 QScriptValueList args { url, hash };48 auto hash = result.value("hash").toString();
35 callback.call(_engine->currentContext()->thisObject(), args);49 jsCallback(handler, url, hash);
36 }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 });
37 upload->deleteLater();59 upload->deleteLater();
38 });60 });
39 upload->start();61 upload->start();
40}62}
41 63
42void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValue callback) {64void 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 });
44 71
45 QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {72 connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [this, deferred](SetMappingRequest* request) {
46 if (callback.isFunction()) {73 Q_ASSERT(QThread::currentThread() == request->thread());
47 QString error = request->getErrorString();74 // we are now on the "Resource Manager" thread
48 QScriptValueList args { error };75 QString error = request->getErrorString();
49 callback.call(_engine->currentContext()->thisObject(), args);76 // forward a thread-safe values back to our thread
50 }77 deferred->handle(error, { { "error", request->getError() } });
51 request->deleteLater();78 request->deleteLater();
52 });79 });
53 setMappingRequest->start();80 setMappingRequest->start();
@@ -55,48 +82,63 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu · 1 unmodified line
55 82
56 83
57void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {84void 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?
58 88
59 if (!urlString.startsWith(ATP_SCHEME)) {89 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;
60 return;92 return;
61 }93 }
62 94 QString hash = AssetUtils::extractAssetHash(urlString);
63 // Make request to atp95 auto handler = makeScopedHandlerObject(thisObject(), callback);
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
68 auto assetClient = DependencyManager::get<AssetClient>();96 auto assetClient = DependencyManager::get<AssetClient>();
69 auto assetRequest = assetClient->createRequest(hash);97 auto assetRequest = assetClient->createRequest(hash);
70 98
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 });
72 104
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
74 Q_ASSERT(request->getState() == AssetRequest::Finished);108 Q_ASSERT(request->getState() == AssetRequest::Finished);
75 109
76 if (request->getError() == AssetRequest::Error::NoError) {110 if (request->getError() == AssetRequest::Error::NoError) {
77 if (callback.isFunction()) {111 QString data = QString::fromUtf8(request->getData());
78 QString data = QString::fromUtf8(request->getData());112 // forward a thread-safe values back to our thread
79 QScriptValueList args { data };113 deferred->resolve(NoError, { { "data", data } });
80 callback.call(_engine->currentContext()->thisObject(), args);114 } else {
81 }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();
82 }118 }
83 119
84 request->deleteLater();120 request->deleteLater();
85 _pendingRequests.remove(request);
86 });121 });
87 122
88 assetRequest->start();123 assetRequest->start();
89}124}
90 125
91void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) {126void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) {
127 auto handler = makeScopedHandlerObject(thisObject(), callback);
92 auto setBakingEnabledRequest = DependencyManager::get<AssetClient>()->createSetBakingEnabledRequest({ path }, enabled);128 auto setBakingEnabledRequest = DependencyManager::get<AssetClient>()->createSetBakingEnabledRequest({ path }, enabled);
93 129
94 QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {130 Promise deferred = makePromise(__FUNCTION__)
95 if (callback.isFunction()) {131 ->ready([=](QString error, QVariantMap result) {
96 QString error = request->getErrorString();132 jsCallback(handler, error, result);
97 QScriptValueList args{ error };133 });
98 callback.call(_engine->currentContext()->thisObject(), args);134
99 }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, {});
100 request->deleteLater();142 request->deleteLater();
101 });143 });
102 setBakingEnabledRequest->start();144 setBakingEnabledRequest->start();
@@ -111,3 +153,238 @@ void AssetScriptingInterface::sendFakedHandshake() { · 8 unmodified lines
111}153}
112 154
113#endif155#endif
156
157void 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
171bool 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
183void 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
197void 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
205void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
206 jsAssert(false, "TODO: deleteAsset API");
207}
208
209void 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
265void 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
281void 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
303void 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
319void 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}