//// ScriptsModel.cpp// interface/src//// Created by Ryan Huffman on 05/12/14.// Copyright 2014 High Fidelity, Inc.//// S3 request code written with ModelBrowser as a reference.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include <QUrl>#include <QUrlQuery>#include <QXmlStreamReader> #include <NetworkAccessManager.h> #include "Application.h"#include "Menu.h"#include "InterfaceLogging.h" #include "ScriptsModel.h" static const QString S3_URL = "http://s3.amazonaws.com/hifi-public";static const QString PUBLIC_URL = "http://public.highfidelity.io";static const QString MODELS_LOCATION = "scripts/"; static const QString PREFIX_PARAMETER_NAME = "prefix";static const QString MARKER_PARAMETER_NAME = "marker";static const QString IS_TRUNCATED_NAME = "IsTruncated";static const QString CONTAINER_NAME = "Contents";static const QString KEY_NAME = "Key"; TreeNodeBase::TreeNodeBase(TreeNodeFolder* parent, const QString& name, TreeNodeType type) : _parent(parent), _type(type), _name(name) {}; TreeNodeScript::TreeNodeScript(const QString& localPath, const QString& fullPath, ScriptOrigin origin) : TreeNodeBase(NULL, localPath.split("/").last(), TREE_NODE_TYPE_SCRIPT), _localPath(localPath), _fullPath(fullPath), _origin(origin) {}; TreeNodeFolder::TreeNodeFolder(const QString& foldername, TreeNodeFolder* parent) : TreeNodeBase(parent, foldername, TREE_NODE_TYPE_FOLDER) {}; ScriptsModel::ScriptsModel(QObject* parent) : QAbstractItemModel(parent), _loadingScripts(false), _localDirectory(), _fsWatcher(), _treeNodes(){ _localDirectory.setFilter(QDir::Files | QDir::Readable); _localDirectory.setNameFilters(QStringList("*.js")); updateScriptsLocation(qApp->getScriptsLocation()); connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles); connect(qApp, &Application::scriptLocationChanged, this, &ScriptsModel::updateScriptsLocation); reloadLocalFiles(); reloadRemoteFiles();} ScriptsModel::~ScriptsModel() { for (int i = _treeNodes.size() - 1; i >= 0; i--) { delete _treeNodes.at(i); } _treeNodes.clear();} TreeNodeBase* ScriptsModel::getTreeNodeFromIndex(const QModelIndex& index) const { if (index.isValid()) { return static_cast<TreeNodeBase*>(index.internalPointer()); } return NULL;} QModelIndex ScriptsModel::index(int row, int column, const QModelIndex& parent) const { if (row < 0 || column < 0) { return QModelIndex(); } return createIndex(row, column, getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).at(row));} QModelIndex ScriptsModel::parent(const QModelIndex& child) const { TreeNodeFolder* parent = (static_cast<TreeNodeBase*>(child.internalPointer()))->getParent(); if (!parent) { return QModelIndex(); } TreeNodeFolder* grandParent = parent->getParent(); int row = getFolderNodes(grandParent).indexOf(parent); return createIndex(row, 0, parent);} QVariant ScriptsModel::data(const QModelIndex& index, int role) const { TreeNodeBase* node = getTreeNodeFromIndex(index); if (node->getType() == TREE_NODE_TYPE_SCRIPT) { TreeNodeScript* script = static_cast<TreeNodeScript*>(node); if (role == Qt::DisplayRole) { return QVariant(script->getName() + (script->getOrigin() == SCRIPT_ORIGIN_LOCAL ? " (local)" : "")); } else if (role == ScriptPath) { return QVariant(script->getFullPath()); } } else if (node->getType() == TREE_NODE_TYPE_FOLDER) { TreeNodeFolder* folder = static_cast<TreeNodeFolder*>(node); if (role == Qt::DisplayRole) { return QVariant(folder->getName()); } } return QVariant();} int ScriptsModel::rowCount(const QModelIndex& parent) const { return getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).count();} int ScriptsModel::columnCount(const QModelIndex& parent) const { return 1;} void ScriptsModel::updateScriptsLocation(const QString& newPath) { _fsWatcher.removePath(_localDirectory.absolutePath()); if (!newPath.isEmpty()) { _localDirectory.setPath(newPath); if (!_localDirectory.absolutePath().isEmpty()) { _fsWatcher.addPath(_localDirectory.absolutePath()); } } reloadLocalFiles();} void ScriptsModel::reloadRemoteFiles() { if (!_loadingScripts) { _loadingScripts = true; for (int i = _treeNodes.size() - 1; i >= 0; i--) { TreeNodeBase* node = _treeNodes.at(i); if (node->getType() == TREE_NODE_TYPE_SCRIPT && static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_REMOTE) { delete node; _treeNodes.removeAt(i); } } requestRemoteFiles(); }} void ScriptsModel::requestRemoteFiles(QString marker) { QUrl url(S3_URL); QUrlQuery query; query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION); if (!marker.isEmpty()) { query.addQueryItem(MARKER_PARAMETER_NAME, marker); } url.setQuery(query); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest request(url); request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); QNetworkReply* reply = networkAccessManager.get(request); connect(reply, SIGNAL(finished()), SLOT(downloadFinished()));} void ScriptsModel::downloadFinished() { QNetworkReply* reply = static_cast<QNetworkReply*>(sender()); bool finished = true; if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); if (!data.isEmpty()) { finished = parseXML(data); } else { qCDebug(interfaceapp) << "Error: Received no data when loading remote scripts"; } } reply->deleteLater(); sender()->deleteLater(); if (finished) { _loadingScripts = false; }} bool ScriptsModel::parseXML(QByteArray xmlFile) { beginResetModel(); QXmlStreamReader xml(xmlFile); QRegExp jsRegex(".*\\.js"); bool truncated = false; QString lastKey; while (!xml.atEnd() && !xml.hasError()) { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) { while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) { xml.readNext(); if (xml.text().toString() == "true") { truncated = true; } } } if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) { while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) { if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) { xml.readNext(); lastKey = xml.text().toString(); if (jsRegex.exactMatch(xml.text().toString())) { _treeNodes.append(new TreeNodeScript(lastKey.mid(MODELS_LOCATION.length()), S3_URL + "/" + lastKey, SCRIPT_ORIGIN_REMOTE)); } } xml.readNext(); } } xml.readNext(); } rebuildTree(); endResetModel(); // Error handling if (xml.hasError()) { qCDebug(interfaceapp) << "Error loading remote scripts: " << xml.errorString(); return true; } if (truncated) { requestRemoteFiles(lastKey); } // If this request was not truncated, we are done. return !truncated;} void ScriptsModel::reloadLocalFiles() { beginResetModel(); for (int i = _treeNodes.size() - 1; i >= 0; i--) { TreeNodeBase* node = _treeNodes.at(i); if (node->getType() == TREE_NODE_TYPE_SCRIPT && static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_LOCAL) { delete node; _treeNodes.removeAt(i); } } _localDirectory.refresh(); const QFileInfoList localFiles = _localDirectory.entryInfoList(); for (int i = 0; i < localFiles.size(); i++) { QFileInfo file = localFiles[i]; _treeNodes.append(new TreeNodeScript(file.fileName(), file.absoluteFilePath(), SCRIPT_ORIGIN_LOCAL)); } rebuildTree(); endResetModel();} void ScriptsModel::rebuildTree() { for (int i = _treeNodes.size() - 1; i >= 0; i--) { if (_treeNodes.at(i)->getType() == TREE_NODE_TYPE_FOLDER) { delete _treeNodes.at(i); _treeNodes.removeAt(i); } } QHash<QString, TreeNodeFolder*> folders; for (int i = 0; i < _treeNodes.size(); i++) { TreeNodeBase* node = _treeNodes.at(i); if (node->getType() == TREE_NODE_TYPE_SCRIPT) { TreeNodeScript* script = static_cast<TreeNodeScript*>(node); TreeNodeFolder* parent = NULL; QString hash; QStringList pathList = script->getLocalPath().split(tr("/")); pathList.removeLast(); QStringList::const_iterator pathIterator; for (pathIterator = pathList.constBegin(); pathIterator != pathList.constEnd(); ++pathIterator) { hash.append(*pathIterator + "/"); if (!folders.contains(hash)) { folders[hash] = new TreeNodeFolder(*pathIterator, parent); } parent = folders[hash]; } script->setParent(parent); } } QHash<QString, TreeNodeFolder*>::const_iterator folderIterator; for (folderIterator = folders.constBegin(); folderIterator != folders.constEnd(); ++folderIterator) { _treeNodes.append(*folderIterator); } folders.clear();} QList<TreeNodeBase*> ScriptsModel::getFolderNodes(TreeNodeFolder* parent) const { QList<TreeNodeBase*> result; for (int i = 0; i < _treeNodes.size(); i++) { TreeNodeBase* node = _treeNodes.at(i); if (node->getParent() == parent) { result.append(node); } } return result;}
//
// ScriptsModel.cpp
// interface/src
//
// Created by Ryan Huffman on 05/12/14.
// Copyright 2014 High Fidelity, Inc.
//
// S3 request code written with ModelBrowser as a reference.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QUrl>
#include <QUrlQuery>
#include <QXmlStreamReader>
#include <NetworkAccessManager.h>
#include "Application.h"
#include "Menu.h"
#include "InterfaceLogging.h"
#include "ScriptsModel.h"
static const QString S3_URL = "http://s3.amazonaws.com/hifi-public";
static const QString PUBLIC_URL = "http://public.highfidelity.io";
static const QString MODELS_LOCATION = "scripts/";
static const QString PREFIX_PARAMETER_NAME = "prefix";
static const QString MARKER_PARAMETER_NAME = "marker";
static const QString IS_TRUNCATED_NAME = "IsTruncated";
static const QString CONTAINER_NAME = "Contents";
static const QString KEY_NAME = "Key";
TreeNodeBase::TreeNodeBase(TreeNodeFolder* parent, const QString& name, TreeNodeType type) :
_parent(parent),
_type(type),
_name(name) {
};
TreeNodeScript::TreeNodeScript(const QString& localPath, const QString& fullPath, ScriptOrigin origin) :
TreeNodeBase(NULL, localPath.split("/").last(), TREE_NODE_TYPE_SCRIPT),
_localPath(localPath),
_fullPath(fullPath),
_origin(origin) {
};
TreeNodeFolder::TreeNodeFolder(const QString& foldername, TreeNodeFolder* parent) :
TreeNodeBase(parent, foldername, TREE_NODE_TYPE_FOLDER) {
};
ScriptsModel::ScriptsModel(QObject* parent) :
QAbstractItemModel(parent),
_loadingScripts(false),
_localDirectory(),
_fsWatcher(),
_treeNodes()
{
_localDirectory.setFilter(QDir::Files | QDir::Readable);
_localDirectory.setNameFilters(QStringList("*.js"));
updateScriptsLocation(qApp->getScriptsLocation());
connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles);
connect(qApp, &Application::scriptLocationChanged, this, &ScriptsModel::updateScriptsLocation);
reloadLocalFiles();
reloadRemoteFiles();
}
ScriptsModel::~ScriptsModel() {
for (int i = _treeNodes.size() - 1; i >= 0; i--) {
delete _treeNodes.at(i);
}
_treeNodes.clear();
}
TreeNodeBase* ScriptsModel::getTreeNodeFromIndex(const QModelIndex& index) const {
if (index.isValid()) {
return static_cast<TreeNodeBase*>(index.internalPointer());
}
return NULL;
}
QModelIndex ScriptsModel::index(int row, int column, const QModelIndex& parent) const {
if (row < 0 || column < 0) {
return QModelIndex();
}
return createIndex(row, column, getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).at(row));
}
QModelIndex ScriptsModel::parent(const QModelIndex& child) const {
TreeNodeFolder* parent = (static_cast<TreeNodeBase*>(child.internalPointer()))->getParent();
if (!parent) {
return QModelIndex();
}
TreeNodeFolder* grandParent = parent->getParent();
int row = getFolderNodes(grandParent).indexOf(parent);
return createIndex(row, 0, parent);
}
QVariant ScriptsModel::data(const QModelIndex& index, int role) const {
TreeNodeBase* node = getTreeNodeFromIndex(index);
if (node->getType() == TREE_NODE_TYPE_SCRIPT) {
TreeNodeScript* script = static_cast<TreeNodeScript*>(node);
if (role == Qt::DisplayRole) {
return QVariant(script->getName() + (script->getOrigin() == SCRIPT_ORIGIN_LOCAL ? " (local)" : ""));
} else if (role == ScriptPath) {
return QVariant(script->getFullPath());
}
} else if (node->getType() == TREE_NODE_TYPE_FOLDER) {
TreeNodeFolder* folder = static_cast<TreeNodeFolder*>(node);
if (role == Qt::DisplayRole) {
return QVariant(folder->getName());
}
}
return QVariant();
}
int ScriptsModel::rowCount(const QModelIndex& parent) const {
return getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).count();
}
int ScriptsModel::columnCount(const QModelIndex& parent) const {
return 1;
}
void ScriptsModel::updateScriptsLocation(const QString& newPath) {
_fsWatcher.removePath(_localDirectory.absolutePath());
if (!newPath.isEmpty()) {
_localDirectory.setPath(newPath);
if (!_localDirectory.absolutePath().isEmpty()) {
_fsWatcher.addPath(_localDirectory.absolutePath());
}
}
reloadLocalFiles();
}
void ScriptsModel::reloadRemoteFiles() {
if (!_loadingScripts) {
_loadingScripts = true;
for (int i = _treeNodes.size() - 1; i >= 0; i--) {
TreeNodeBase* node = _treeNodes.at(i);
if (node->getType() == TREE_NODE_TYPE_SCRIPT &&
static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_REMOTE)
{
delete node;
_treeNodes.removeAt(i);
}
}
requestRemoteFiles();
}
}
void ScriptsModel::requestRemoteFiles(QString marker) {
QUrl url(S3_URL);
QUrlQuery query;
query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION);
if (!marker.isEmpty()) {
query.addQueryItem(MARKER_PARAMETER_NAME, marker);
}
url.setQuery(query);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
QNetworkReply* reply = networkAccessManager.get(request);
connect(reply, SIGNAL(finished()), SLOT(downloadFinished()));
}
void ScriptsModel::downloadFinished() {
QNetworkReply* reply = static_cast<QNetworkReply*>(sender());
bool finished = true;
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
finished = parseXML(data);
} else {
qCDebug(interfaceapp) << "Error: Received no data when loading remote scripts";
}
}
reply->deleteLater();
sender()->deleteLater();
if (finished) {
_loadingScripts = false;
}
}
bool ScriptsModel::parseXML(QByteArray xmlFile) {
beginResetModel();
QXmlStreamReader xml(xmlFile);
QRegExp jsRegex(".*\\.js");
bool truncated = false;
QString lastKey;
while (!xml.atEnd() && !xml.hasError()) {
if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) {
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) {
xml.readNext();
if (xml.text().toString() == "true") {
truncated = true;
}
}
}
if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) {
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) {
if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) {
xml.readNext();
lastKey = xml.text().toString();
if (jsRegex.exactMatch(xml.text().toString())) {
_treeNodes.append(new TreeNodeScript(lastKey.mid(MODELS_LOCATION.length()), S3_URL + "/" + lastKey, SCRIPT_ORIGIN_REMOTE));
}
}
xml.readNext();
}
}
xml.readNext();
}
rebuildTree();
endResetModel();
// Error handling
if (xml.hasError()) {
qCDebug(interfaceapp) << "Error loading remote scripts: " << xml.errorString();
return true;
}
if (truncated) {
requestRemoteFiles(lastKey);
}
// If this request was not truncated, we are done.
return !truncated;
}
void ScriptsModel::reloadLocalFiles() {
beginResetModel();
for (int i = _treeNodes.size() - 1; i >= 0; i--) {
TreeNodeBase* node = _treeNodes.at(i);
if (node->getType() == TREE_NODE_TYPE_SCRIPT &&
static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_LOCAL)
{
delete node;
_treeNodes.removeAt(i);
}
}
_localDirectory.refresh();
const QFileInfoList localFiles = _localDirectory.entryInfoList();
for (int i = 0; i < localFiles.size(); i++) {
QFileInfo file = localFiles[i];
_treeNodes.append(new TreeNodeScript(file.fileName(), file.absoluteFilePath(), SCRIPT_ORIGIN_LOCAL));
}
rebuildTree();
endResetModel();
}
void ScriptsModel::rebuildTree() {
for (int i = _treeNodes.size() - 1; i >= 0; i--) {
if (_treeNodes.at(i)->getType() == TREE_NODE_TYPE_FOLDER) {
delete _treeNodes.at(i);
_treeNodes.removeAt(i);
}
}
QHash<QString, TreeNodeFolder*> folders;
for (int i = 0; i < _treeNodes.size(); i++) {
TreeNodeBase* node = _treeNodes.at(i);
if (node->getType() == TREE_NODE_TYPE_SCRIPT) {
TreeNodeScript* script = static_cast<TreeNodeScript*>(node);
TreeNodeFolder* parent = NULL;
QString hash;
QStringList pathList = script->getLocalPath().split(tr("/"));
pathList.removeLast();
QStringList::const_iterator pathIterator;
for (pathIterator = pathList.constBegin(); pathIterator != pathList.constEnd(); ++pathIterator) {
hash.append(*pathIterator + "/");
if (!folders.contains(hash)) {
folders[hash] = new TreeNodeFolder(*pathIterator, parent);
}
parent = folders[hash];
}
script->setParent(parent);
}
}
QHash<QString, TreeNodeFolder*>::const_iterator folderIterator;
for (folderIterator = folders.constBegin(); folderIterator != folders.constEnd(); ++folderIterator) {
_treeNodes.append(*folderIterator);
}
folders.clear();
}
QList<TreeNodeBase*> ScriptsModel::getFolderNodes(TreeNodeFolder* parent) const {
QList<TreeNodeBase*> result;
for (int i = 0; i < _treeNodes.size(); i++) {
TreeNodeBase* node = _treeNodes.at(i);
if (node->getParent() == parent) {
result.append(node);
}
}
return result;
}