mirror of
https://github.com/overte-org/overte.git
synced 2025-04-11 08:54:01 +02:00
expanded Assets scripting interface
This commit is contained in:
parent
15fcf66d0e
commit
8c7a8f0df3
12 changed files with 1090 additions and 47 deletions
libraries
networking/src
AssetClient.cppAssetClient.hAssetRequest.cppAssetRequest.hAssetResourceRequest.cppAssetUtils.cppAssetUtils.hBaseAssetScriptingInterface.cppBaseAssetScriptingInterface.h
script-engine/src
scripts/developer/tests/unit_tests
|
@ -20,6 +20,7 @@
|
|||
#include <QtNetwork/QNetworkDiskCache>
|
||||
|
||||
#include <shared/GlobalAppProperties.h>
|
||||
#include <shared/MiniPromises.h>
|
||||
|
||||
#include "AssetRequest.h"
|
||||
#include "AssetUpload.h"
|
||||
|
@ -72,6 +73,30 @@ void AssetClient::init() {
|
|||
networkAccessManager.setCache(cache);
|
||||
qInfo() << "ResourceManager disk cache setup at" << _cacheDir
|
||||
<< "(size:" << MAXIMUM_CACHE_SIZE / BYTES_PER_GIGABYTES << "GB)";
|
||||
} else {
|
||||
auto cache = qobject_cast<QNetworkDiskCache*>(networkAccessManager.cache());
|
||||
qInfo() << "ResourceManager disk cache already setup at" << cache->cacheDirectory()
|
||||
<< "(size:" << cache->maximumCacheSize() / BYTES_PER_GIGABYTES << "GB)";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AssetClient::cacheInfoRequest(MiniPromise::Promise deferred) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
if (!QMetaType::isRegistered(qMetaTypeId<MiniPromise::Promise>())) {
|
||||
qRegisterMetaType<MiniPromise::Promise>();
|
||||
}
|
||||
QMetaObject::invokeMethod(this, "cacheInfoRequest", Q_ARG(MiniPromise::Promise, deferred));
|
||||
return;
|
||||
}
|
||||
if (auto* cache = qobject_cast<QNetworkDiskCache*>(NetworkAccessManager::getInstance().cache())) {
|
||||
deferred->resolve({
|
||||
{ "cacheDirectory", cache->cacheDirectory() },
|
||||
{ "cacheSize", cache->cacheSize() },
|
||||
{ "maximumCacheSize", cache->maximumCacheSize() },
|
||||
});
|
||||
} else {
|
||||
deferred->reject("Cache not available");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include <map>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <shared/MiniPromises.h>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ByteRange.h"
|
||||
|
@ -66,6 +67,7 @@ public slots:
|
|||
void init();
|
||||
|
||||
void cacheInfoRequest(QObject* reciever, QString slot);
|
||||
void cacheInfoRequest(MiniPromise::Promise deferred);
|
||||
void clearCache();
|
||||
|
||||
private slots:
|
||||
|
|
|
@ -134,3 +134,14 @@ void AssetRequest::start() {
|
|||
emit progress(totalReceived, total);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const QString AssetRequest::getErrorString() const {
|
||||
QString result;
|
||||
if (_error != Error::NoError) {
|
||||
QVariant v;
|
||||
v.setValue(_error);
|
||||
result = v.toString(); // courtesy of Q_ENUM
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ public:
|
|||
NetworkError,
|
||||
UnknownError
|
||||
};
|
||||
|
||||
Q_ENUM(Error)
|
||||
AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange());
|
||||
virtual ~AssetRequest() override;
|
||||
|
||||
|
@ -51,6 +51,7 @@ public:
|
|||
const QByteArray& getData() const { return _data; }
|
||||
const State& getState() const { return _state; }
|
||||
const Error& getError() const { return _error; }
|
||||
const QString getErrorString() const;
|
||||
QUrl getUrl() const { return ::getATPUrl(_hash); }
|
||||
QString getHash() const { return _hash; }
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ void AssetResourceRequest::requestHash(const AssetHash& hash) {
|
|||
_assetRequest = assetClient->createRequest(hash, _byteRange);
|
||||
|
||||
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::onDownloadProgress);
|
||||
connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
|
||||
connect(_assetRequest, &AssetRequest::finished, this, [this, hash](AssetRequest* req) {
|
||||
Q_ASSERT(_state == InProgress);
|
||||
Q_ASSERT(req == _assetRequest);
|
||||
Q_ASSERT(req->getState() == AssetRequest::Finished);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QFileInfo> // for baseName
|
||||
#include <QtNetwork/QAbstractNetworkCache>
|
||||
|
||||
#include "NetworkAccessManager.h"
|
||||
|
@ -22,8 +23,36 @@
|
|||
|
||||
#include "ResourceManager.h"
|
||||
|
||||
QUrl getATPUrl(const QString& hash) {
|
||||
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash));
|
||||
// Extract the valid AssetHash portion from atp: URLs like "[atp:]HASH[.fbx][?query]"
|
||||
// (or an invalid AssetHash if not found)
|
||||
AssetHash extractAssetHash(const QString& input) {
|
||||
if (isValidHash(input)) {
|
||||
return input;
|
||||
}
|
||||
QString path = getATPUrl(input).path();
|
||||
QString baseName = QFileInfo(path).baseName();
|
||||
if (isValidHash(baseName)) {
|
||||
return baseName;
|
||||
}
|
||||
return AssetHash();
|
||||
}
|
||||
|
||||
// Get the normalized ATP URL for a raw hash, /path or "atp:" input string.
|
||||
QUrl getATPUrl(const QString& input) {
|
||||
QUrl url = input;
|
||||
if (!url.scheme().isEmpty() && url.scheme() != URL_SCHEME_ATP) {
|
||||
return QUrl();
|
||||
}
|
||||
// this strips extraneous info from the URL (while preserving fragment/querystring)
|
||||
QString path = url.toEncoded(
|
||||
QUrl::RemoveAuthority | QUrl::RemoveScheme |
|
||||
QUrl::StripTrailingSlash | QUrl::NormalizePathSegments
|
||||
);
|
||||
QString baseName = QFileInfo(path).baseName();
|
||||
if (isValidPath(path) || isValidHash(baseName)) {
|
||||
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP).arg(path));
|
||||
}
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
QByteArray hashData(const QByteArray& data) {
|
||||
|
|
|
@ -71,7 +71,8 @@ struct MappingInfo {
|
|||
|
||||
using AssetMapping = std::map<AssetPath, MappingInfo>;
|
||||
|
||||
QUrl getATPUrl(const QString& hash);
|
||||
QUrl getATPUrl(const QString& input);
|
||||
AssetHash extractAssetHash(const QString& input);
|
||||
|
||||
QByteArray hashData(const QByteArray& data);
|
||||
|
||||
|
@ -84,4 +85,18 @@ bool isValidHash(const QString& hashString);
|
|||
|
||||
QString bakingStatusToString(BakingStatus status);
|
||||
|
||||
// backwards-compatible namespace aliases
|
||||
// (allows new code to be explicit -- eg: `AssetUtils::isValidPath(path)` vs. `isValidPath(path)`)
|
||||
namespace AssetUtils {
|
||||
static const auto& loadFromCache = ::loadFromCache;
|
||||
static const auto& saveToCache = ::saveToCache;
|
||||
static const auto& hashData = ::hashData;
|
||||
static const auto& getATPUrl = ::getATPUrl;
|
||||
static const auto& extractAssetHash = ::extractAssetHash;
|
||||
static const auto& isValidFilePath = ::isValidFilePath;
|
||||
static const auto& isValidPath = ::isValidPath;
|
||||
static const auto& isValidHash = ::isValidHash;
|
||||
};
|
||||
|
||||
|
||||
#endif // hifi_AssetUtils_h
|
||||
|
|
399
libraries/networking/src/BaseAssetScriptingInterface.cpp
Normal file
399
libraries/networking/src/BaseAssetScriptingInterface.cpp
Normal file
|
@ -0,0 +1,399 @@
|
|||
//
|
||||
// BaseAssetScriptingInterface.cpp
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
#include "BaseAssetScriptingInterface.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QMimeDatabase>
|
||||
#include <QThread>
|
||||
|
||||
#include "AssetRequest.h"
|
||||
#include "AssetUpload.h"
|
||||
#include "AssetUtils.h"
|
||||
#include "MappingRequest.h"
|
||||
#include "NetworkLogging.h"
|
||||
|
||||
#include <RegisteredMetaTypes.h>
|
||||
|
||||
#include <shared/QtHelpers.h>
|
||||
#include "Gzip.h"
|
||||
|
||||
using Promise = MiniPromise::Promise;
|
||||
|
||||
QSharedPointer<AssetClient> BaseAssetScriptingInterface::assetClient() {
|
||||
return DependencyManager::get<AssetClient>();
|
||||
}
|
||||
|
||||
BaseAssetScriptingInterface::BaseAssetScriptingInterface(QObject* parent) : QObject(parent), _cache(this) {
|
||||
}
|
||||
|
||||
|
||||
bool BaseAssetScriptingInterface::initializeCache() {
|
||||
qDebug() << "BaseAssetScriptingInterface::getCacheStatus -- current values" << _cache.cacheDirectory() << _cache.cacheSize() << _cache.maximumCacheSize();
|
||||
|
||||
// NOTE: *instances* of QNetworkDiskCache are not thread-safe -- however, different threads can effectively
|
||||
// use the same underlying cache if configured with identical settings. Once AssetClient's disk cache settings
|
||||
// become available we configure our instance to match.
|
||||
auto assets = assetClient();
|
||||
if (!assets) {
|
||||
return false; // not yet possible to initialize the cache
|
||||
}
|
||||
if (_cache.cacheDirectory().size()) {
|
||||
return true; // cache is ready
|
||||
}
|
||||
|
||||
// attempt to initialize the cache
|
||||
qDebug() << "BaseAssetScriptingInterface::getCacheStatus -- invoking AssetClient::init" << assetClient().data();
|
||||
QMetaObject::invokeMethod(assetClient().data(), "init");
|
||||
|
||||
qDebug() << "BaseAssetScriptingInterface::getCacheStatus querying cache status" << QThread::currentThread();
|
||||
Promise deferred = makePromise("BaseAssetScriptingInterface--queryCacheStatus");
|
||||
deferred->then([&](QVariantMap result) {
|
||||
qDebug() << "//queryCacheStatus" << QThread::currentThread();
|
||||
auto cacheDirectory = result.value("cacheDirectory").toString();
|
||||
auto cacheSize = result.value("cacheSize").toLongLong();
|
||||
auto maximumCacheSize = result.value("maximumCacheSize").toLongLong();
|
||||
qDebug() << "///queryCacheStatus" << cacheDirectory << cacheSize << maximumCacheSize;
|
||||
_cache.setCacheDirectory(cacheDirectory);
|
||||
_cache.setMaximumCacheSize(maximumCacheSize);
|
||||
});
|
||||
deferred->fail([&](QString error) {
|
||||
qDebug() << "//queryCacheStatus ERROR" << QThread::currentThread() << error;
|
||||
});
|
||||
assets->cacheInfoRequest(deferred);
|
||||
return false; // cache is not ready yet
|
||||
}
|
||||
|
||||
QVariantMap BaseAssetScriptingInterface::getCacheStatus() {
|
||||
//assetClient()->cacheInfoRequest(this, "onCacheInfoResponse");
|
||||
return {
|
||||
{ "cacheDirectory", _cache.cacheDirectory() },
|
||||
{ "cacheSize", _cache.cacheSize() },
|
||||
{ "maximumCacheSize", _cache.maximumCacheSize() },
|
||||
};
|
||||
}
|
||||
|
||||
QVariantMap BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) {
|
||||
QNetworkCacheMetaData metaData = _cache.metaData(url);
|
||||
QVariantMap attributes, rawHeaders;
|
||||
|
||||
QHashIterator<QNetworkRequest::Attribute, QVariant> i(metaData.attributes());
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
attributes[QString::number(i.key())] = i.value();
|
||||
}
|
||||
for (const auto& i : metaData.rawHeaders()) {
|
||||
rawHeaders[i.first] = i.second;
|
||||
}
|
||||
return {
|
||||
{ "isValid", metaData.isValid() },
|
||||
{ "url", metaData.url() },
|
||||
{ "expirationDate", metaData.expirationDate() },
|
||||
{ "lastModified", metaData.lastModified().toString().isEmpty() ? QDateTime() : metaData.lastModified() },
|
||||
{ "saveToDisk", metaData.saveToDisk() },
|
||||
{ "attributes", attributes },
|
||||
{ "rawHeaders", rawHeaders },
|
||||
};
|
||||
}
|
||||
|
||||
QVariantMap BaseAssetScriptingInterface::loadFromCache(const QUrl& url) {
|
||||
qDebug() << "loadFromCache" << url;
|
||||
QVariantMap result = {
|
||||
{ "metadata", queryCacheMeta(url) },
|
||||
{ "data", QByteArray() },
|
||||
};
|
||||
// caller is responsible for the deletion of the ioDevice, hence the unique_ptr
|
||||
if (auto ioDevice = std::unique_ptr<QIODevice>(_cache.data(url))) {
|
||||
QByteArray data = ioDevice->readAll();
|
||||
qCDebug(asset_client) << url.toDisplayString() << "loaded from disk cache (" << data.size() << " bytes)";
|
||||
result["data"] = data;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// parse RFC 1123 HTTP date format
|
||||
QDateTime parseHttpDate(const QString& dateString) {
|
||||
QDateTime dt = QDateTime::fromString(dateString.left(25), "ddd, dd MMM yyyy HH:mm:ss");
|
||||
dt.setTimeSpec(Qt::UTC);
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
bool BaseAssetScriptingInterface::saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& headers) {
|
||||
QDateTime lastModified = headers.contains("last-modified") ?
|
||||
parseHttpDate(headers["last-modified"].toString()) :
|
||||
QDateTime::currentDateTimeUtc();
|
||||
QDateTime expirationDate = headers.contains("expires") ?
|
||||
parseHttpDate(headers["expires"].toString()) :
|
||||
QDateTime(); // never expires
|
||||
QNetworkCacheMetaData metaData;
|
||||
metaData.setUrl(url);
|
||||
metaData.setSaveToDisk(true);
|
||||
metaData.setLastModified(lastModified);
|
||||
metaData.setExpirationDate(expirationDate);
|
||||
if (auto ioDevice = _cache.prepare(metaData)) {
|
||||
ioDevice->write(data);
|
||||
_cache.insert(ioDevice);
|
||||
qCDebug(asset_client) << url.toDisplayString() << "saved to disk cache ("<< data.size()<<" bytes)";
|
||||
return true;
|
||||
}
|
||||
qCWarning(asset_client) << "Could not save" << url.toDisplayString() << "to disk cache.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::loadAsset(QString asset, bool decompress, QString responseType) {
|
||||
auto hash = AssetUtils::extractAssetHash(asset);
|
||||
auto url = AssetUtils::getATPUrl(hash).toString();
|
||||
|
||||
QVariantMap metaData = {
|
||||
{ "_asset", asset },
|
||||
{ "_type", "download" },
|
||||
{ "hash", hash },
|
||||
{ "url", url },
|
||||
{ "responseType", responseType },
|
||||
};
|
||||
|
||||
Promise fetched = makePromise("loadAsset::fetched"),
|
||||
loaded = makePromise("loadAsset::loaded");
|
||||
|
||||
downloadBytes(hash)
|
||||
->mixin(metaData)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
Q_ASSERT(thread() == QThread::currentThread());
|
||||
fetched->mixin(result);
|
||||
if (decompress) {
|
||||
qDebug() << "loadAsset::decompressBytes...";
|
||||
decompressBytes(result.value("data").toByteArray())
|
||||
->mixin(result)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
fetched->handle(error, result);
|
||||
});
|
||||
} else {
|
||||
fetched->handle(error, result);
|
||||
}
|
||||
});
|
||||
|
||||
fetched->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "loadAsset::fetched" << error;
|
||||
if (responseType == "arraybuffer") {
|
||||
loaded->resolve(NoError, result);
|
||||
} else {
|
||||
convertBytes(result.value("data").toByteArray(), responseType)
|
||||
->mixin(result)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
loaded->resolve(NoError, result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::convertBytes(const QByteArray& dataByteArray, const QString& responseType) {
|
||||
QVariantMap result;
|
||||
Promise conversion = makePromise(__FUNCTION__);
|
||||
if (dataByteArray.size() == 0) {
|
||||
result["response"] = QString();
|
||||
} else if (responseType == "text") {
|
||||
result["response"] = QString::fromUtf8(dataByteArray);
|
||||
} else if (responseType == "json") {
|
||||
QJsonParseError status;
|
||||
auto parsed = QJsonDocument::fromJson(dataByteArray, &status);
|
||||
if (status.error == QJsonParseError::NoError) {
|
||||
result["response"] = parsed.isArray() ?
|
||||
QVariant(parsed.array().toVariantList()) :
|
||||
QVariant(parsed.object().toVariantMap());
|
||||
} else {
|
||||
QVariantMap errorResult = {
|
||||
{ "error", status.error },
|
||||
{ "offset", status.offset },
|
||||
};
|
||||
return conversion->reject("JSON Parse Error: " + status.errorString(), errorResult);
|
||||
}
|
||||
} else if (responseType == "arraybuffer") {
|
||||
result["response"] = dataByteArray;
|
||||
}
|
||||
return conversion->resolve(NoError, result);
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::decompressBytes(const QByteArray& dataByteArray) {
|
||||
QByteArray inflated;
|
||||
auto start = usecTimestampNow();
|
||||
if (gunzip(dataByteArray, inflated)) {
|
||||
auto end = usecTimestampNow();
|
||||
return makePromise(__FUNCTION__)->resolve(NoError, {
|
||||
{ "_compressedByteLength", dataByteArray.size() },
|
||||
{ "_compressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() },
|
||||
{ "_compressMS", (double)(end - start) / 1000.0 },
|
||||
{ "decompressed", true },
|
||||
{ "byteLength", inflated.size() },
|
||||
{ "contentType", QMimeDatabase().mimeTypeForData(inflated).name() },
|
||||
{ "data", inflated },
|
||||
});
|
||||
} else {
|
||||
return makePromise(__FUNCTION__)->reject("gunzip error", {});
|
||||
}
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::compressBytes(const QByteArray& dataByteArray, int level) {
|
||||
QByteArray deflated;
|
||||
auto start = usecTimestampNow();
|
||||
if (gzip(dataByteArray, deflated, level)) {
|
||||
auto end = usecTimestampNow();
|
||||
return makePromise(__FUNCTION__)->resolve(NoError, {
|
||||
{ "_uncompressedByteLength", dataByteArray.size() },
|
||||
{ "_uncompressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() },
|
||||
{ "_compressMS", (double)(end - start) / 1000.0 },
|
||||
{ "compressed", true },
|
||||
{ "byteLength", deflated.size() },
|
||||
{ "contentType", QMimeDatabase().mimeTypeForData(deflated).name() },
|
||||
{ "data", deflated },
|
||||
});
|
||||
} else {
|
||||
return makePromise(__FUNCTION__)->reject("gzip error", {});
|
||||
}
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::downloadBytes(QString hash) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
QPointer<AssetRequest> assetRequest = assetClient->createRequest(hash);
|
||||
Promise deferred = makePromise(__FUNCTION__);
|
||||
|
||||
QObject::connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) {
|
||||
qDebug() << "...BaseAssetScriptingInterface::downloadBytes" << request->getErrorString();
|
||||
// note: we are now on the "Resource Manager" thread
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
Q_ASSERT(request->getState() == AssetRequest::Finished);
|
||||
QString error;
|
||||
QVariantMap result;
|
||||
if (request->getError() == AssetRequest::Error::NoError) {
|
||||
QByteArray data = request->getData();
|
||||
result = {
|
||||
{ "url", request->getUrl() },
|
||||
{ "hash", request->getHash() },
|
||||
{ "cached", request->loadedFromCache() },
|
||||
{ "content-type", QMimeDatabase().mimeTypeForData(data).name() },
|
||||
{ "data", data },
|
||||
};
|
||||
} else {
|
||||
error = request->getError();
|
||||
result = { { "error", request->getError() } };
|
||||
}
|
||||
qDebug() << "//BaseAssetScriptingInterface::downloadBytes" << error << result.keys();
|
||||
// forward thread-safe copies back to our thread
|
||||
deferred->handle(error, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
assetRequest->start();
|
||||
return deferred;
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::uploadBytes(const QByteArray& bytes) {
|
||||
Promise deferred = makePromise(__FUNCTION__);
|
||||
QPointer<AssetUpload> upload = DependencyManager::get<AssetClient>()->createUpload(bytes);
|
||||
|
||||
QObject::connect(upload, &AssetUpload::finished, upload, [=](AssetUpload* upload, const QString& hash) {
|
||||
Q_ASSERT(QThread::currentThread() == upload->thread());
|
||||
// note: we are now on the "Resource Manager" thread
|
||||
QString error;
|
||||
QVariantMap result;
|
||||
if (upload->getError() == AssetUpload::NoError) {
|
||||
result = {
|
||||
{ "hash", hash },
|
||||
{ "url", AssetUtils::getATPUrl(hash).toString() },
|
||||
{ "filename", upload->getFilename() },
|
||||
};
|
||||
} else {
|
||||
error = upload->getErrorString();
|
||||
result = { { "error", upload->getError() } };
|
||||
}
|
||||
// forward thread-safe copies back to our thread
|
||||
deferred->handle(error, result);
|
||||
upload->deleteLater();
|
||||
});
|
||||
upload->start();
|
||||
return deferred;
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::getAssetInfo(QString asset) {
|
||||
auto deferred = makePromise(__FUNCTION__);
|
||||
auto url = AssetUtils::getATPUrl(asset);
|
||||
auto path = url.path();
|
||||
auto hash = AssetUtils::extractAssetHash(asset);
|
||||
if (AssetUtils::isValidHash(hash)) {
|
||||
// already a valid ATP hash -- nothing to do
|
||||
deferred->resolve(NoError, {
|
||||
{ "hash", hash },
|
||||
{ "path", path },
|
||||
{ "url", url },
|
||||
});
|
||||
} else if (AssetUtils::isValidFilePath(path)) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
QPointer<GetMappingRequest> request = assetClient->createGetMappingRequest(path);
|
||||
|
||||
QObject::connect(request, &GetMappingRequest::finished, request, [=]() {
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
// note: we are now on the "Resource Manager" thread
|
||||
QString error;
|
||||
QVariantMap result;
|
||||
if (request->getError() == GetMappingRequest::NoError) {
|
||||
result = {
|
||||
{ "_hash", hash },
|
||||
{ "_path", path },
|
||||
{ "_url", url },
|
||||
{ "hash", request->getHash() },
|
||||
{ "wasRedirected", request->wasRedirected() },
|
||||
{ "path", request->wasRedirected() ? request->getRedirectedPath() : path },
|
||||
};
|
||||
} else {
|
||||
error = request->getErrorString();
|
||||
result = { { "error", request->getError() } };
|
||||
}
|
||||
// forward thread-safe copies back to our thread
|
||||
deferred->handle(error, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
request->start();
|
||||
} else {
|
||||
deferred->reject("invalid ATP file path: " + asset + "("+path+")", {});
|
||||
}
|
||||
return deferred;
|
||||
}
|
||||
|
||||
Promise BaseAssetScriptingInterface::symlinkAsset(QString hash, QString path) {
|
||||
auto deferred = makePromise(__FUNCTION__);
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
QPointer<SetMappingRequest> setMappingRequest = assetClient->createSetMappingRequest(path, hash);
|
||||
|
||||
connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [=](SetMappingRequest* request) {
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
// we are now on the "Resource Manager" thread
|
||||
QString error;
|
||||
QVariantMap result;
|
||||
if (request->getError() == SetMappingRequest::NoError) {
|
||||
result = {
|
||||
{ "_hash", hash },
|
||||
{ "_path", path },
|
||||
{ "hash", request->getHash() },
|
||||
{ "path", request->getPath() },
|
||||
{ "url", AssetUtils::getATPUrl(request->getPath()).toString() },
|
||||
};
|
||||
} else {
|
||||
error = request->getErrorString();
|
||||
result = { { "error", request->getError() } };
|
||||
}
|
||||
// forward results back to our thread
|
||||
deferred->handle(error, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
setMappingRequest->start();
|
||||
return deferred;
|
||||
}
|
78
libraries/networking/src/BaseAssetScriptingInterface.h
Normal file
78
libraries/networking/src/BaseAssetScriptingInterface.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// BaseAssetScriptingInterface.h
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
// BaseAssetScriptingInterface contains the engine-agnostic support code that can be used from
|
||||
// both QML JS and QScriptEngine JS engine implementations
|
||||
|
||||
#ifndef hifi_BaseAssetScriptingInterface_h
|
||||
#define hifi_BaseAssetScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QThread>
|
||||
#include "AssetClient.h"
|
||||
#include <shared/MiniPromises.h>
|
||||
#include "NetworkAccessManager.h"
|
||||
#include <QtNetwork/QNetworkDiskCache>
|
||||
|
||||
/**jsdoc
|
||||
* @namespace Assets
|
||||
*/
|
||||
class BaseAssetScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Promise = MiniPromise::Promise;
|
||||
QSharedPointer<AssetClient> assetClient();
|
||||
|
||||
BaseAssetScriptingInterface(QObject* parent = nullptr);
|
||||
|
||||
public slots:
|
||||
/**jsdoc
|
||||
* Get the current status of the disk cache (if available)
|
||||
* @function Assets.uploadData
|
||||
* @static
|
||||
* @return {String} result.cacheDirectory (path to the current disk cache)
|
||||
* @return {Number} result.cacheSize (used cache size in bytes)
|
||||
* @return {Number} result.maximumCacheSize (maxmimum cache size in bytes)
|
||||
*/
|
||||
QVariantMap getCacheStatus();
|
||||
|
||||
/**jsdoc
|
||||
* Initialize the disk cache (returns true if already initialized)
|
||||
* @function Assets.initializeCache
|
||||
* @static
|
||||
*/
|
||||
bool initializeCache();
|
||||
|
||||
virtual bool isValidPath(QString input) { return AssetUtils::isValidPath(input); }
|
||||
virtual bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); }
|
||||
QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); }
|
||||
QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); }
|
||||
bool isValidHash(QString input) { return AssetUtils::isValidHash(input); }
|
||||
QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); }
|
||||
|
||||
virtual QVariantMap queryCacheMeta(const QUrl& url);
|
||||
virtual QVariantMap loadFromCache(const QUrl& url);
|
||||
virtual bool saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata = QVariantMap());
|
||||
|
||||
protected:
|
||||
//void onCacheInfoResponse(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize);
|
||||
QNetworkDiskCache _cache;
|
||||
const QString NoError{};
|
||||
//virtual bool jsAssert(bool condition, const QString& error) = 0;
|
||||
Promise loadAsset(QString asset, bool decompress, QString responseType);
|
||||
Promise getAssetInfo(QString asset);
|
||||
Promise downloadBytes(QString hash);
|
||||
Promise uploadBytes(const QByteArray& bytes);
|
||||
Promise compressBytes(const QByteArray& bytes, int level = -1);
|
||||
Promise convertBytes(const QByteArray& dataByteArray, const QString& responseType);
|
||||
Promise decompressBytes(const QByteArray& bytes);
|
||||
Promise symlinkAsset(QString hash, QString path);
|
||||
};
|
||||
#endif // hifi_BaseAssetScriptingInterface_h
|
|
@ -11,43 +11,70 @@
|
|||
|
||||
#include "AssetScriptingInterface.h"
|
||||
|
||||
#include <QMimeDatabase>
|
||||
#include <QThread>
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
#include <AssetRequest.h>
|
||||
#include <AssetUpload.h>
|
||||
#include <AssetUtils.h>
|
||||
#include <MappingRequest.h>
|
||||
#include <NetworkLogging.h>
|
||||
#include <NodeList.h>
|
||||
|
||||
AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
|
||||
_engine(engine)
|
||||
{
|
||||
#include <RegisteredMetaTypes.h>
|
||||
|
||||
#include <shared/QtHelpers.h>
|
||||
#include "Gzip.h"
|
||||
#include "ScriptEngine.h"
|
||||
|
||||
//using Promise = MiniPromise::Promise;
|
||||
|
||||
AssetScriptingInterface::AssetScriptingInterface(QObject* parent) : BaseAssetScriptingInterface(parent) {
|
||||
if (auto engine = qobject_cast<QScriptEngine*>(parent)) {
|
||||
registerMetaTypes(engine);
|
||||
}
|
||||
}
|
||||
#define JS_ASSERT(cond, error) { if (!this->jsAssert(cond, error)) { return; } }
|
||||
|
||||
void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {
|
||||
auto handler = makeScopedHandlerObject(thisObject(), callback);
|
||||
QByteArray dataByteArray = data.toUtf8();
|
||||
auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);
|
||||
|
||||
QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QString url = "atp:" + hash;
|
||||
QScriptValueList args { url, hash };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
Promise deferred = makePromise(__FUNCTION__)
|
||||
->ready([this, handler](QString error, QVariantMap result) {
|
||||
auto url = result.value("url").toString();
|
||||
auto hash = result.value("hash").toString();
|
||||
jsCallback(handler, url, hash);
|
||||
});
|
||||
|
||||
connect(upload, &AssetUpload::finished, upload, [this, deferred](AssetUpload* upload, const QString& hash) {
|
||||
// we are now on the "Resource Manager" thread (and "hash" being a *reference* makes it unsafe to use directly)
|
||||
Q_ASSERT(QThread::currentThread() == upload->thread());
|
||||
deferred->resolve(NoError, {
|
||||
{ "url", "atp:" + hash },
|
||||
{ "hash", hash },
|
||||
});
|
||||
upload->deleteLater();
|
||||
});
|
||||
upload->start();
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValue callback) {
|
||||
auto setMappingRequest = DependencyManager::get<AssetClient>()->createSetMappingRequest(path, hash);
|
||||
auto handler = makeScopedHandlerObject(thisObject(), callback);
|
||||
auto setMappingRequest = assetClient()->createSetMappingRequest(path, hash);
|
||||
Promise deferred = makePromise(__FUNCTION__)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
|
||||
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QString error = request->getErrorString();
|
||||
QScriptValueList args { error };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [this, deferred](SetMappingRequest* request) {
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
// we are now on the "Resource Manager" thread
|
||||
QString error = request->getErrorString();
|
||||
// forward a thread-safe values back to our thread
|
||||
deferred->handle(error, { { "error", request->getError() } });
|
||||
request->deleteLater();
|
||||
});
|
||||
setMappingRequest->start();
|
||||
|
@ -55,48 +82,63 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
|
|||
|
||||
|
||||
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
|
||||
// FIXME: historically this API method failed silently when given a non-atp prefixed
|
||||
// urlString (or if the AssetRequest failed).
|
||||
// .. is that by design or could we update without breaking things to provide better feedback to scripts?
|
||||
|
||||
if (!urlString.startsWith(ATP_SCHEME)) {
|
||||
// ... for now at least log a message so user can check logs
|
||||
qDebug() << "AssetScriptingInterface::downloadData ERROR: url does not start with " << ATP_SCHEME;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make request to atp
|
||||
auto path = urlString.right(urlString.length() - ATP_SCHEME.length());
|
||||
auto parts = path.split(".", QString::SkipEmptyParts);
|
||||
auto hash = parts.length() > 0 ? parts[0] : "";
|
||||
|
||||
QString hash = AssetUtils::extractAssetHash(urlString);
|
||||
auto handler = makeScopedHandlerObject(thisObject(), callback);
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto assetRequest = assetClient->createRequest(hash);
|
||||
|
||||
_pendingRequests << assetRequest;
|
||||
Promise deferred = makePromise(__FUNCTION__)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
// FIXME: to remain backwards-compatible the signature here is "callback(data, n/a)"
|
||||
jsCallback(handler, result.value("data").toString(), { { "errorMessage", error } });
|
||||
});
|
||||
|
||||
connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable {
|
||||
connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) {
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
// we are now on the "Resource Manager" thread
|
||||
Q_ASSERT(request->getState() == AssetRequest::Finished);
|
||||
|
||||
if (request->getError() == AssetRequest::Error::NoError) {
|
||||
if (callback.isFunction()) {
|
||||
QString data = QString::fromUtf8(request->getData());
|
||||
QScriptValueList args { data };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
QString data = QString::fromUtf8(request->getData());
|
||||
// forward a thread-safe values back to our thread
|
||||
deferred->resolve(NoError, { { "data", data } });
|
||||
} else {
|
||||
// FIXME: propagate error to scripts? (requires changing signature or inverting param order above..)
|
||||
//deferred->resolve(request->getErrorString(), { { "error", requet->getError() } });
|
||||
qDebug() << "AssetScriptingInterface::downloadData ERROR: " << request->getErrorString();
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
_pendingRequests.remove(request);
|
||||
});
|
||||
|
||||
assetRequest->start();
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) {
|
||||
auto handler = makeScopedHandlerObject(thisObject(), callback);
|
||||
auto setBakingEnabledRequest = DependencyManager::get<AssetClient>()->createSetBakingEnabledRequest({ path }, enabled);
|
||||
|
||||
QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QString error = request->getErrorString();
|
||||
QScriptValueList args{ error };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
Promise deferred = makePromise(__FUNCTION__)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
|
||||
connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, setBakingEnabledRequest, [this, deferred](SetBakingEnabledRequest* request) {
|
||||
Q_ASSERT(QThread::currentThread() == request->thread());
|
||||
// we are now on the "Resource Manager" thread
|
||||
|
||||
QString error = request->getErrorString();
|
||||
// forward thread-safe values back to our thread
|
||||
deferred->handle(error, {});
|
||||
request->deleteLater();
|
||||
});
|
||||
setBakingEnabledRequest->start();
|
||||
|
@ -111,3 +153,238 @@ void AssetScriptingInterface::sendFakedHandshake() {
|
|||
}
|
||||
|
||||
#endif
|
||||
|
||||
void AssetScriptingInterface::getMapping(QString asset, QScriptValue callback) {
|
||||
auto path = AssetUtils::getATPUrl(asset).path();
|
||||
auto handler = makeScopedHandlerObject(thisObject(), callback);
|
||||
JS_ASSERT(AssetUtils::isValidFilePath(path), "invalid ATP file path: " + asset + "(path:"+path+")");
|
||||
JS_ASSERT(callback.isFunction(), "expected second parameter to be a callback function");
|
||||
qDebug() << ">>getMapping//getAssetInfo" << path;
|
||||
getAssetInfo(path)->ready([this, handler](QString error, QVariantMap result) {
|
||||
qDebug() << "//getMapping//getAssetInfo" << error << result.keys();
|
||||
jsCallback(handler, error, result.value("hash").toString());
|
||||
});
|
||||
}
|
||||
|
||||
/////////////////////////// new APIS ////////////////////////////////////
|
||||
|
||||
bool AssetScriptingInterface::jsAssert(bool condition, const QString& error) {
|
||||
if (condition) {
|
||||
return true;
|
||||
}
|
||||
if (context()) {
|
||||
context()->throwError(error);
|
||||
} else {
|
||||
qDebug() << "WARNING -- jsAssert failed outside of a valid JS context: " + error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::jsCallback(const QScriptValue& handler,
|
||||
const QScriptValue& error, const QScriptValue& result) {
|
||||
Q_ASSERT(thread() == QThread::currentThread());
|
||||
auto errorValue = !error.toBool() ? QScriptValue::NullValue : error;
|
||||
JS_ASSERT(handler.isObject() && handler.property("callback").isFunction(),
|
||||
QString("jsCallback -- .callback is not a function (%1)")
|
||||
.arg(handler.property("callback").toVariant().typeName()));
|
||||
#if 1 || DEGUG_JSCALLBACK
|
||||
QScriptValue debug = result;
|
||||
debug.setProperty("toString", handler.engine()->evaluate("1,function() { return JSON.stringify(this, 0, 2); }"));
|
||||
#endif
|
||||
::callScopedHandlerObject(handler, errorValue, result);
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::jsCallback(const QScriptValue& handler,
|
||||
const QScriptValue& error, const QVariantMap& result) {
|
||||
Q_ASSERT(thread() == QThread::currentThread());
|
||||
Q_ASSERT(handler.engine());
|
||||
auto engine = handler.engine();
|
||||
jsCallback(handler, error, engine->toScriptValue(result));
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
jsAssert(false, "TODO: deleteAsset API");
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
JS_ASSERT(options.isObject() || options.isString(), "expected request options Object or URL as first parameter");
|
||||
|
||||
auto decompress = options.property("decompress").toBool() || options.property("compressed").toBool();
|
||||
auto responseType = options.property("responseType").toString().toLower();
|
||||
auto url = options.property("url").toString();
|
||||
if (options.isString()) {
|
||||
url = options.toString();
|
||||
}
|
||||
if (responseType.isEmpty() || responseType == "string") {
|
||||
responseType = "text";
|
||||
}
|
||||
auto asset = AssetUtils::getATPUrl(url).path();
|
||||
auto handler = makeScopedHandlerObject(scope, callback);
|
||||
|
||||
JS_ASSERT(handler.property("callback").isFunction(),
|
||||
QString("Invalid callback function (%1)").arg(handler.property("callback").toVariant().typeName()));
|
||||
JS_ASSERT(AssetUtils::isValidHash(asset) || AssetUtils::isValidFilePath(asset),
|
||||
QString("Invalid ATP url '%1'").arg(url));
|
||||
JS_ASSERT(RESPONSE_TYPES.contains(responseType),
|
||||
QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | ")));
|
||||
|
||||
Promise resolved = makePromise("resolved"),
|
||||
loaded = makePromise("loaded");
|
||||
|
||||
loaded->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//loaded" << error;
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
|
||||
resolved->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//resolved" << result.value("hash");
|
||||
QString hash = result.value("hash").toString();
|
||||
if (!error.isEmpty() || !AssetUtils::isValidHash(hash)) {
|
||||
loaded->reject(error.isEmpty() ? "internal hash error: " + hash : error, result);
|
||||
} else {
|
||||
loadAsset(hash, decompress, responseType)
|
||||
->mixin(result)
|
||||
->ready([this, loaded, hash](QString error, QVariantMap result) {
|
||||
qDebug() << "//getAssetInfo/loadAsset" << error << hash;
|
||||
loaded->resolve(NoError, result);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (AssetUtils::isValidHash(asset)) {
|
||||
resolved->resolve(NoError, { { "hash", asset } });
|
||||
} else {
|
||||
getAssetInfo(asset)
|
||||
->ready([this, resolved](QString error, QVariantMap result) {
|
||||
qDebug() << "//getAssetInfo" << error << result.value("hash") << result.value("path");
|
||||
resolved->resolve(error, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
const QString& URL{ "url" };
|
||||
|
||||
auto url = (options.isString() ? options : options.property(URL)).toString();
|
||||
auto asset = AssetUtils::getATPUrl(url).path();
|
||||
auto handler = makeScopedHandlerObject(scope, callback);
|
||||
|
||||
JS_ASSERT(AssetUtils::isValidFilePath(asset) || AssetUtils::isValidHash(asset),
|
||||
"expected options to be an asset URL or request options containing .url property");
|
||||
JS_ASSERT(handler.property("callback").isFunction(), "invalid callback function");
|
||||
getAssetInfo(asset)->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//resolveAsset/getAssetInfo" << error << result.value("hash");
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
auto data = options.property("data");
|
||||
QByteArray dataByteArray = qscriptvalue_cast<QByteArray>(data);
|
||||
auto handler = makeScopedHandlerObject(scope, callback);
|
||||
auto responseType = options.property("responseType").toString().toLower();
|
||||
if (responseType.isEmpty()) {
|
||||
responseType = "text";
|
||||
}
|
||||
decompressBytes(dataByteArray)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
if (responseType == "arraybuffer") {
|
||||
jsCallback(handler, error, result);
|
||||
} else {
|
||||
convertBytes(result.value("data").toByteArray(), responseType)
|
||||
->mixin(result)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
auto data = options.property("data");
|
||||
QByteArray dataByteArray = data.isString() ?
|
||||
data.toString().toUtf8() :
|
||||
qscriptvalue_cast<QByteArray>(data);
|
||||
auto handler = makeScopedHandlerObject(scope, callback);
|
||||
auto level = options.property("level").toInt32();
|
||||
if (level < -1 || level > 9) {
|
||||
level = -1;
|
||||
}
|
||||
compressBytes(dataByteArray, level)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) {
|
||||
auto compress = options.property("compress").toBool() ||
|
||||
options.property("compressed").toBool();
|
||||
auto handler = makeScopedHandlerObject(scope, callback);
|
||||
auto data = options.property("data");
|
||||
auto rawPath = options.property("path").toString();
|
||||
auto path = AssetUtils::getATPUrl(rawPath).path();
|
||||
|
||||
QByteArray dataByteArray = data.isString() ?
|
||||
data.toString().toUtf8() :
|
||||
qscriptvalue_cast<QByteArray>(data);
|
||||
//auto rawByteLength = dataByteArray.size();
|
||||
|
||||
JS_ASSERT(path.isEmpty() || AssetUtils::isValidFilePath(path),
|
||||
QString("expected valid ATP file path '%1' ('%2')").arg(rawPath).arg(path));
|
||||
JS_ASSERT(handler.property("callback").isFunction(),
|
||||
"invalid callback function");
|
||||
JS_ASSERT(dataByteArray.size() > 0,
|
||||
QString("expected non-zero .data (got %1 / #%2 bytes)")
|
||||
.arg(data.toVariant().typeName())
|
||||
.arg(dataByteArray.size()));
|
||||
|
||||
|
||||
// [compressed] => uploaded to server => [mapped to path]
|
||||
Promise prepared = makePromise("putAsset::prepared"),
|
||||
uploaded = makePromise("putAsset::uploaded"),
|
||||
finished = makePromise("putAsset::finished");
|
||||
|
||||
if (compress) {
|
||||
qDebug() << "putAsset::compressBytes...";
|
||||
compressBytes(dataByteArray, -1)
|
||||
->finally([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//putAsset::compressedBytes" << error << result.keys();
|
||||
prepared->handle(error, result);
|
||||
});
|
||||
} else {
|
||||
prepared->resolve(NoError, {{ "data", dataByteArray }});
|
||||
}
|
||||
|
||||
prepared->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//putAsset::prepared" << error << result.value("data").toByteArray().size() << result.keys();
|
||||
uploadBytes(result.value("data").toByteArray())
|
||||
->mixin(result)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "===========//putAsset::prepared/uploadBytes" << error << result.keys();
|
||||
uploaded->handle(error, result);
|
||||
});
|
||||
});
|
||||
|
||||
uploaded->ready([=](QString error, QVariantMap result) {
|
||||
QString hash = result.value("hash").toString();
|
||||
qDebug() << "//putAsset::uploaded" << error << hash << result.keys();
|
||||
if (path.isEmpty()) {
|
||||
finished->handle(error, result);
|
||||
} else if (!AssetUtils::isValidHash(hash)) {
|
||||
finished->reject("path mapping requested, but did not receive valid hash", result);
|
||||
} else {
|
||||
qDebug() << "symlinkAsset" << hash << path << QThread::currentThread();
|
||||
symlinkAsset(hash, path)
|
||||
->mixin(result)
|
||||
->ready([=](QString error, QVariantMap result) {
|
||||
finished->handle(error, result);
|
||||
qDebug() << "//symlinkAsset" << hash << path << result.keys();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
finished->ready([=](QString error, QVariantMap result) {
|
||||
qDebug() << "//putAsset::finished" << error << result.keys();
|
||||
jsCallback(handler, error, result);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,17 +15,21 @@
|
|||
#define hifi_AssetScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtScript/QScriptValue>
|
||||
|
||||
#include <QtScript/QScriptable>
|
||||
#include <AssetClient.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
#include <BaseAssetScriptingInterface.h>
|
||||
#include <QtNetwork/QNetworkDiskCache>
|
||||
|
||||
/**jsdoc
|
||||
* @namespace Assets
|
||||
*/
|
||||
class AssetScriptingInterface : public QObject {
|
||||
class AssetScriptingInterface : public BaseAssetScriptingInterface, QScriptable {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AssetScriptingInterface(QScriptEngine* engine);
|
||||
AssetScriptingInterface(QObject* parent = nullptr);
|
||||
|
||||
/**jsdoc
|
||||
* Upload content to the connected domain's asset server.
|
||||
|
@ -75,16 +79,37 @@ public:
|
|||
* @param {string} error
|
||||
*/
|
||||
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE void getMapping(QString path, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback);
|
||||
|
||||
#if (PR_BUILD || DEV_BUILD)
|
||||
Q_INVOKABLE void sendFakedHandshake();
|
||||
#endif
|
||||
|
||||
// Advanced APIs
|
||||
// getAsset(options, scope[callback(error, result)]) -- fetches an Asset from the Server
|
||||
// [options.url] an "atp:" style URL, hash, or relative mapped path to fetch
|
||||
// [options.responseType] the desired reponse type (text | arraybuffer | json)
|
||||
// [options.decompress] whether to apply gunzip decompression on the stream
|
||||
// [scope[callback]] continuation-style (error, { responseType, data, byteLength, ... }) callback
|
||||
const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" };
|
||||
Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
// putAsset(options, scope[callback(error, result)]) -- upload a new Aset to the Server
|
||||
// [options.data] -- (ArrayBuffer|String)
|
||||
// [options.compress] -- (true|false)
|
||||
// [options.path=undefined] -- option path mapping to set on the created hash result
|
||||
// [
|
||||
Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
|
||||
|
||||
protected:
|
||||
QSet<AssetRequest*> _pendingRequests;
|
||||
QScriptEngine* _engine;
|
||||
void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QVariantMap& result);
|
||||
void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QScriptValue& result);
|
||||
bool jsAssert(bool condition, const QString& error);
|
||||
};
|
||||
|
||||
#endif // hifi_AssetScriptingInterface_h
|
||||
|
|
181
scripts/developer/tests/unit_tests/assetUnitTests.js
Normal file
181
scripts/developer/tests/unit_tests/assetUnitTests.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
/* eslint-env jasmine */
|
||||
|
||||
instrument_testrunner(true);
|
||||
|
||||
describe("Assets", function () {
|
||||
var context = {
|
||||
definedHash: null,
|
||||
definedPath: null,
|
||||
definedContent: null,
|
||||
};
|
||||
var NULL_HASH = new Array(64+1).join('0'); // 64 hex
|
||||
var SAMPLE_FOLDER = '/assetUnitTests';
|
||||
var SAMPLE_FILE_PATH = SAMPLE_FOLDER + "/test.json";
|
||||
var SAMPLE_CONTENTS = 'Test Run on ' + JSON.stringify(Date());
|
||||
var IS_ASSET_HASH_REGEX = /^[a-f0-9]{64}$/i;
|
||||
var IS_ASSET_URL = /^atp:/;
|
||||
|
||||
it('Entities.canWriteAssets', function() {
|
||||
expect(Entities.canWriteAssets()).toBe(true);
|
||||
});
|
||||
|
||||
it('Assets.extractAssetHash(input)', function() {
|
||||
// new helper method that provides a catch-all way to get at the sha256 hash
|
||||
// considering the multiple, different ways ATP hash URLs are found across the
|
||||
// system and in content.
|
||||
|
||||
var POSITIVE_TEST_URLS = [
|
||||
'atp:HASH',
|
||||
'atp:HASH.obj',
|
||||
'atp:HASH.fbx?cache-buster',
|
||||
'atp:/.baked/HASH/asset.fbx',
|
||||
'HASH'
|
||||
];
|
||||
var NEGATIVE_TEST_URLS = [
|
||||
'asdf',
|
||||
'http://domain.tld',
|
||||
'/path/filename.fbx',
|
||||
'atp:/path/filename.fbx?#HASH',
|
||||
''
|
||||
];
|
||||
pending();
|
||||
});
|
||||
|
||||
// new APIs
|
||||
|
||||
it('Assets.getAsset(options, {callback(error, result)})', function() { pending(); });
|
||||
it('Assets.putAsset(options, {callback(error, result)})', function() { pending(); });
|
||||
it('Assets.deleteAsset(options, {callback(error, result)})', function() { pending(); });
|
||||
it('Assets.resolveAsset(options, {callback(error, result)})', function() { pending(); });
|
||||
|
||||
|
||||
// existing APIs
|
||||
it('Assets.getATPUrl(input)', function() { pending(); });
|
||||
it('Assets.isValidPath(input)', function() { pending(); });
|
||||
it('Assets.isValidFilePath(input)', function() { pending(); });
|
||||
it('Assets.isValidHash(input)', function() { pending(); });
|
||||
it('Assets.setBakingEnabled(path, enabled, {callback(error)})', function() { pending(); });
|
||||
|
||||
it("Assets.uploadData(data, {callback(url, hash)})", function (done) {
|
||||
Assets.uploadData(SAMPLE_CONTENTS, function(url, hash) {
|
||||
expect(url).toMatch(IS_ASSET_URL);
|
||||
expect(hash).toMatch(IS_ASSET_HASH_REGEX);
|
||||
context.definedHash = hash; // used in later tests
|
||||
context.definedContent = SAMPLE_CONTENTS;
|
||||
print('context.definedHash = ' + context.definedHash);
|
||||
print('context.definedContent = ' + context.definedContent);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("Assets.setMapping(path, hash {callback(error)})", function (done) {
|
||||
expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX);
|
||||
Assets.setMapping(SAMPLE_FILE_PATH, context.definedHash, function(error) {
|
||||
if (error) error += ' ('+JSON.stringify([SAMPLE_FILE_PATH, context.definedHash])+')';
|
||||
expect(error).toBe(null);
|
||||
context.definedPath = SAMPLE_FILE_PATH;
|
||||
print('context.definedPath = ' + context.definedPath);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("Assets.getMapping(path, {callback(error, hash)})", function (done) {
|
||||
expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf');
|
||||
expect(context.definedPath).toMatch(/^\//, 'asfasdf');
|
||||
Assets.getMapping(context.definedPath, function(error, hash) {
|
||||
if (error) error += ' ('+JSON.stringify([context.definedPath])+')';
|
||||
expect(error).toBe(null);
|
||||
expect(hash).toMatch(IS_ASSET_HASH_REGEX);
|
||||
expect(hash).toEqual(context.definedHash);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Assets.getMapping(nullHash, {callback(data)})', function(done) {
|
||||
// FIXME: characterization test -- current behavior is that downloadData silently fails
|
||||
// when given an asset that doesn't exist
|
||||
Assets.downloadData(NULL_HASH, function(result) {
|
||||
throw new Error("this callback 'should' not be triggered");
|
||||
});
|
||||
setTimeout(function() { done(); }, 2000);
|
||||
});
|
||||
|
||||
it('Assets.downloadData(hash, {callback(data)})', function(done) {
|
||||
expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf');
|
||||
Assets.downloadData('atp:' + context.definedHash, function(result) {
|
||||
expect(result).toEqual(context.definedContent);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Assets.downloadData(nullHash, {callback(data)})', function(done) {
|
||||
// FIXME: characterization test -- current behavior is that downloadData silently fails
|
||||
// when given an asset doesn't exist
|
||||
Assets.downloadData(NULL_HASH, function(result) {
|
||||
throw new Error("this callback 'should' not be triggered");
|
||||
});
|
||||
setTimeout(function() {
|
||||
done();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
describe('exceptions', function() {
|
||||
describe('Asset.setMapping', function() {
|
||||
it('-- invalid path', function(done) {
|
||||
Assets.setMapping('foo', NULL_HASH, function(error/*, result*/) {
|
||||
expect(error).toEqual('Path is invalid');
|
||||
/* expect(result).not.toMatch(IS_ASSET_URL); */
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('-- invalid hash', function(done) {
|
||||
Assets.setMapping(SAMPLE_FILE_PATH, 'bar', function(error/*, result*/) {
|
||||
expect(error).toEqual('Hash is invalid');
|
||||
/* expect(result).not.toMatch(IS_ASSET_URL); */
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Asset.getMapping', function() {
|
||||
it('-- invalid path throws immediate', function() {
|
||||
expect(function() {
|
||||
Assets.getMapping('foo', function(error, hash) {
|
||||
throw new Error("should never make it here...");
|
||||
});
|
||||
throw new Error("should never make it here...");
|
||||
}).toThrowError(/invalid.*path/i);
|
||||
});
|
||||
it('-- non-existing path', function(done) {
|
||||
Assets.getMapping('/foo/bar/'+Date.now(), function(error, hash) {
|
||||
expect(error).toEqual('Asset not found');
|
||||
expect(hash).not.toMatch(IS_ASSET_HASH_REGEX);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// this stuff allows the unit tests to be loaded indepenently and/or as part of testRunner.js execution
|
||||
function run() {}
|
||||
function instrument_testrunner(force) {
|
||||
if (force || typeof describe === 'undefined') {
|
||||
// Include testing library
|
||||
Script.include('../../libraries/jasmine/jasmine.js');
|
||||
Script.include('../../libraries/jasmine/hifi-boot.js');
|
||||
|
||||
run = function() {
|
||||
if (!/:console/.test(Script.resolvePath(''))) {
|
||||
// invoke Script.stop (after any async tests complete)
|
||||
jasmine.getEnv().addReporter({ jasmineDone: Script.stop });
|
||||
} else {
|
||||
jasmine.getEnv().addReporter({ jasmineDone: function() { print("JASMINE DONE"); } });
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
jasmine.getEnv().execute();
|
||||
};
|
||||
}
|
||||
}
|
||||
run();
|
Loading…
Reference in a new issue